diff --git a/legacyworlds-server-beans-simple/src/main/java/com/deepclone/lw/beans/updates/GameUpdateBean.java b/legacyworlds-server-beans-simple/src/main/java/com/deepclone/lw/beans/updates/GameUpdateBean.java deleted file mode 100644 index 3973457..0000000 --- a/legacyworlds-server-beans-simple/src/main/java/com/deepclone/lw/beans/updates/GameUpdateBean.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.deepclone.lw.beans.updates; - - -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionTemplate; - -import com.deepclone.lw.cmd.admin.logs.LogLevel; -import com.deepclone.lw.interfaces.eventlog.Logger; -import com.deepclone.lw.interfaces.eventlog.SystemLogger; -import com.deepclone.lw.interfaces.game.UpdatesDAO; -import com.deepclone.lw.interfaces.sys.MaintenanceStatusException; -import com.deepclone.lw.interfaces.sys.SystemStatus; -import com.deepclone.lw.interfaces.sys.TickStatusException; -import com.deepclone.lw.interfaces.sys.Ticker; -import com.deepclone.lw.interfaces.sys.Ticker.Frequency; - - - -public class GameUpdateBean - implements InitializingBean , Runnable -{ - private Ticker ticker; - private SystemStatus systemStatus; - private SystemLogger logger; - - private TransactionTemplate tTemplate; - private UpdatesDAO updatesDao; - - - @Autowired( required = true ) - public void setTicker( Ticker ticker ) - { - this.ticker = ticker; - } - - - @Autowired( required = true ) - public void setSystemStatus( SystemStatus systemStatus ) - { - this.systemStatus = systemStatus; - } - - - @Autowired( required = true ) - public void setLogger( Logger logger ) - { - this.logger = logger.getSystemLogger( "GameUpdate" ); - } - - - @Autowired( required = true ) - public void setTransactionManager( PlatformTransactionManager transactionManager ) - { - this.tTemplate = new TransactionTemplate( transactionManager ); - } - - - @Autowired( required = true ) - public void setUpdatesDAO( UpdatesDAO updatesDao ) - { - this.updatesDao = updatesDao; - } - - - @Override - public void afterPropertiesSet( ) - { - try { - this.endPreviousTick( ); - } catch ( MaintenanceStatusException e ) { - // Do nothing - } - this.ticker.registerTask( Frequency.MINUTE , "Game update" , this ); - } - - - @Override - public void run( ) - { - // Attempt to end the previous tick, if e.g. maintenance mode was initiated while it was - // being processed - try { - this.endPreviousTick( ); - } catch ( MaintenanceStatusException e1 ) { - return; - } - - // Initiate next tick - long tickId; - try { - tickId = this.systemStatus.startTick( ); - } catch ( TickStatusException e ) { - throw new RuntimeException( "tick initiated while previous tick still being processed" , e ); - } catch ( MaintenanceStatusException e ) { - return; - } - - // Execute tick - this.logger.log( LogLevel.DEBUG , "Tick " + tickId + " started" ).flush( ); - this.executeTick( tickId ); - } - - - private void endPreviousTick( ) - throws MaintenanceStatusException - { - Long currentTick = this.systemStatus.checkStuckTick( ); - if ( currentTick == null ) { - return; - } - - this.logger.log( LogLevel.WARNING , "Tick " + currentTick + " restarted" ).flush( ); - this.executeTick( currentTick.longValue( ) ); - } - - - private void executeTick( final long tickId ) - { - boolean hasMore; - do { - hasMore = this.tTemplate.execute( new TransactionCallback< Boolean >( ) { - - @Override - public Boolean doInTransaction( TransactionStatus status ) - { - return updatesDao.processUpdates( tickId ); - } - - } ); - } while ( hasMore ); - this.logger.log( LogLevel.TRACE , "Tick " + tickId + " completed" ).flush( ); - } - -} diff --git a/legacyworlds-server-beans-simple/src/main/java/com/deepclone/lw/beans/updates/UpdatesDAOBean.java b/legacyworlds-server-beans-simple/src/main/java/com/deepclone/lw/beans/updates/UpdatesDAOBean.java deleted file mode 100644 index 77a1bbd..0000000 --- a/legacyworlds-server-beans-simple/src/main/java/com/deepclone/lw/beans/updates/UpdatesDAOBean.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.deepclone.lw.beans.updates; - - -import java.sql.Types; -import java.util.Map; - -import javax.sql.DataSource; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.jdbc.core.SqlOutParameter; -import org.springframework.jdbc.core.SqlParameter; -import org.springframework.jdbc.core.simple.SimpleJdbcCall; - -import com.deepclone.lw.interfaces.game.UpdatesDAO; - - - -public class UpdatesDAOBean - implements UpdatesDAO -{ - - private SimpleJdbcCall process; - - - @Autowired( required = true ) - public void setDataSource( DataSource dataSource ) - { - this.process = new SimpleJdbcCall( dataSource ); - this.process.withCatalogName( "sys" ).withFunctionName( "process_updates" ); - this.process.withoutProcedureColumnMetaDataAccess( ); - this.process.addDeclaredParameter( new SqlParameter( "tick_id" , Types.BIGINT ) ); - this.process.addDeclaredParameter( new SqlOutParameter( "has_more" , Types.BOOLEAN ) ); - } - - - @Override - public boolean processUpdates( long tickId ) - { - Map< String , Object > m = this.process.execute( tickId ); - return (Boolean) m.get( "has_more" ); - } - -} diff --git a/legacyworlds-server-beans-simple/src/main/resources/configuration/simple-beans.xml b/legacyworlds-server-beans-simple/src/main/resources/configuration/simple-beans.xml index ffd1068..a6219d9 100644 --- a/legacyworlds-server-beans-simple/src/main/resources/configuration/simple-beans.xml +++ b/legacyworlds-server-beans-simple/src/main/resources/configuration/simple-beans.xml @@ -10,13 +10,11 @@ - - diff --git a/legacyworlds-server-beans-simple/src/main/resources/configuration/simple/game-update-bean.xml b/legacyworlds-server-beans-simple/src/main/resources/configuration/simple/game-update-bean.xml deleted file mode 100644 index 54534ff..0000000 --- a/legacyworlds-server-beans-simple/src/main/resources/configuration/simple/game-update-bean.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - diff --git a/legacyworlds-server-beans-updates/pom.xml b/legacyworlds-server-beans-updates/pom.xml new file mode 100644 index 0000000..d719f44 --- /dev/null +++ b/legacyworlds-server-beans-updates/pom.xml @@ -0,0 +1,17 @@ + + 4.0.0 + + + legacyworlds-server-beans + com.deepclone.lw + 1.0.0 + ../legacyworlds-server-beans/pom.xml + + + legacyworlds-server-beans-updates + Legacy Worlds - Server - Components - Game updates + ${legacyworlds.version.main}.${legacyworlds.version.release}-${legacyworlds.version.build} + This module contains the components which process the game updates and allow the administration interface to display information about them. + + \ No newline at end of file diff --git a/legacyworlds-server-beans-updates/src/main/java/.empty b/legacyworlds-server-beans-updates/src/main/java/.empty new file mode 100644 index 0000000..e69de29 diff --git a/legacyworlds-server-beans-updates/src/main/java/com/deepclone/lw/beans/updates/GameUpdateProcessorBean.java b/legacyworlds-server-beans-updates/src/main/java/com/deepclone/lw/beans/updates/GameUpdateProcessorBean.java new file mode 100644 index 0000000..73da48f --- /dev/null +++ b/legacyworlds-server-beans-updates/src/main/java/com/deepclone/lw/beans/updates/GameUpdateProcessorBean.java @@ -0,0 +1,238 @@ +package com.deepclone.lw.beans.updates; + + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import com.deepclone.lw.cmd.admin.logs.LogLevel; +import com.deepclone.lw.interfaces.eventlog.Logger; +import com.deepclone.lw.interfaces.eventlog.SystemLogger; +import com.deepclone.lw.interfaces.game.updates.GameUpdateProcessor; +import com.deepclone.lw.interfaces.game.updates.UpdatesDAO; +import com.deepclone.lw.interfaces.sys.MaintenanceStatusException; +import com.deepclone.lw.interfaces.sys.SystemStatus; +import com.deepclone.lw.interfaces.sys.TickStatusException; + + + +/** + * Game update processor component + * + *

+ * This component implements game updates processing. It will update the system status as necessary, + * checking for maintenance mode, and of course implement the necessary operations for game updates. + * + * @author E. Benoît + */ +class GameUpdateProcessorBean + implements GameUpdateProcessor +{ + /** System state management component */ + private SystemStatus systemStatus; + + /** Logger used by this component */ + private SystemLogger logger; + + /** Transaction template used to run update batches */ + private TransactionTemplate tTemplate; + + /** Update processing database interface */ + private UpdatesDAO updatesDao; + + /** Server-level update processors registry */ + private ServerProcessorRegistry registry; + + /** Game update processor lock */ + private boolean executing; + + + /** + * Dependency injector that sets the sytstem state management component + * + * @param systemStatus + * the sytstem state management component + */ + @Autowired( required = true ) + public void setSystemStatus( SystemStatus systemStatus ) + { + this.systemStatus = systemStatus; + } + + + /** + * Dependency injector that initialises the logger from the logging component + * + * @param logger + * the logging component + */ + @Autowired( required = true ) + public void setLogger( Logger logger ) + { + this.logger = logger.getSystemLogger( "GameUpdate" ); + } + + + /** + * Dependency injector that initialises the transaction template from the transaction manager + * + * @param transactionManager + * the transaction manager + */ + @Autowired( required = true ) + public void setTransactionManager( PlatformTransactionManager transactionManager ) + { + this.tTemplate = new TransactionTemplate( transactionManager ); + } + + + /** + * Dependency injector that sets the database access component for game updates + * + * @param updatesDao + * the database access component for game updates + */ + @Autowired( required = true ) + public void setUpdatesDAO( UpdatesDAO updatesDao ) + { + this.updatesDao = updatesDao; + } + + + /** + * Dependency injector that sets the processor registry + * + * @param registry + * the processor registry + */ + @Autowired( required = true ) + public void setRegistry( ServerProcessorRegistry registry ) + { + this.registry = registry; + } + + + /** + * Try and lock the processor + * + *

+ * Test the {@link #executing} field, setting it to true and returning + * true if it wasn't or returning false if it was. + */ + @Override + public synchronized boolean tryLock( ) + { + if ( this.executing ) { + return false; + } + this.executing = true; + return true; + } + + + /** + * Unlock the processor + * + *

+ * Set {@link #executing} back to false. If it was already set to + * false, throw an {@link IllegalStateException}. + */ + @Override + public synchronized void unlock( ) + throws IllegalStateException + { + if ( !this.executing ) { + throw new IllegalStateException( "Trying to unlock game update processor, but it isn't locked" ); + } + this.executing = false; + } + + + /** + * Check for an unfinished update cycle and terminate it + * + *

+ * Check for a "stuck" update cycle. If there is one, issue a warning and process it through the + * {@link #executeTick(long)} method. + */ + @Override + public boolean endPreviousCycle( ) + { + Long currentTick; + try { + currentTick = this.systemStatus.checkStuckTick( ); + } catch ( MaintenanceStatusException e ) { + return true; + } + + if ( currentTick == null ) { + return false; + } + + this.logger.log( LogLevel.WARNING , "Tick " + currentTick + " restarted" ).flush( ); + this.executeTick( currentTick ); + return true; + } + + + /** + * Execute an update cycle + * + *

+ * Start by checking for, and executing if necessary, a "stuck" update cycle. If one is found, + * return. Otherwise, start a new tick through the system state manager, and call + * {@link #executeTick(long)} to process it. + * + *

+ * If maintenance mode is active, the new tick will not be started. + */ + @Override + public void executeUpdateCycle( ) + { + // Attempt to end the previous tick, if e.g. maintenance mode was initiated while it was + // being processed + if ( this.endPreviousCycle( ) ) { + return; + } + + // Initiate next tick + long tickId; + try { + tickId = this.systemStatus.startTick( ); + } catch ( TickStatusException e ) { + throw new RuntimeException( "tick initiated while previous tick still being processed" , e ); + } catch ( MaintenanceStatusException e ) { + return; + } + + // Execute tick + this.logger.log( LogLevel.DEBUG , "Tick " + tickId + " started" ).flush( ); + this.executeTick( tickId ); + } + + + /** + * Process all remaining update batches for an update cycle + * + *

+ * This method will try and process all update batches using a {@link GameUpdateTransaction} + * instance. It loops until the transaction indicates that no further updates are to be + * processed. However, the loop will exit if maintenance mode becomes active between update + * batches. + * + * @param tickId + * the game update cycle's identifier + */ + private void executeTick( final long tickId ) + { + GameUpdateTransaction transaction = new GameUpdateTransaction( this.updatesDao , this.registry , tickId ); + while ( !this.tTemplate.execute( transaction ) ) { + if ( this.systemStatus.checkMaintenance( ) != null ) { + this.logger.log( LogLevel.INFO , "Tick " + tickId + " interrupted by system maintenance" ).flush( ); + return; + } + } + this.logger.log( LogLevel.TRACE , "Tick " + tickId + " completed" ).flush( ); + } + +} diff --git a/legacyworlds-server-beans-updates/src/main/java/com/deepclone/lw/beans/updates/GameUpdateTaskBean.java b/legacyworlds-server-beans-updates/src/main/java/com/deepclone/lw/beans/updates/GameUpdateTaskBean.java new file mode 100644 index 0000000..6878e1b --- /dev/null +++ b/legacyworlds-server-beans-updates/src/main/java/com/deepclone/lw/beans/updates/GameUpdateTaskBean.java @@ -0,0 +1,96 @@ +package com.deepclone.lw.beans.updates; + + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; + +import com.deepclone.lw.interfaces.game.updates.GameUpdateProcessor; +import com.deepclone.lw.interfaces.sys.Ticker; +import com.deepclone.lw.interfaces.sys.Ticker.Frequency; + + + +/** + * Game update ticker task + * + *

+ * This component will register itself as a ticker task to be executed once per minute. When it + * runs, it will try and execute an update cycle. + * + * @author E. Benoît + */ +class GameUpdateTaskBean + implements InitializingBean , Runnable +{ + /** The ticker component */ + private Ticker ticker; + + /** The game updates processor */ + private GameUpdateProcessor gameUpdateProcessor; + + + /** + * Dependency injector that sets the ticker component + * + * @param ticker + * the ticker component + */ + @Autowired( required = true ) + public void setTicker( Ticker ticker ) + { + this.ticker = ticker; + } + + + /** + * Dependency injector that sets the game update processor + * + * @param gameUpdateProcessor + * the game update processor + */ + @Autowired( required = true ) + public void setGameUpdateProcessor( GameUpdateProcessor gameUpdateProcessor ) + { + this.gameUpdateProcessor = gameUpdateProcessor; + } + + + /** + * Finish a previous update cycle if there is one, then register the component to the ticker + */ + @Override + public void afterPropertiesSet( ) + { + if ( this.gameUpdateProcessor.tryLock( ) ) { + + try { + this.gameUpdateProcessor.endPreviousCycle( ); + } finally { + this.gameUpdateProcessor.unlock( ); + } + + } + this.ticker.registerTask( Frequency.MINUTE , "Game update" , this ); + } + + + /** + * Run a game update cycle + * + *

+ * Try locking the game update processor. On success, execute an update cycle, then unlock the + * processor. + */ + @Override + public void run( ) + { + if ( !this.gameUpdateProcessor.tryLock( ) ) { + return; + } + try { + this.gameUpdateProcessor.executeUpdateCycle( ); + } finally { + this.gameUpdateProcessor.unlock( ); + } + } +} diff --git a/legacyworlds-server-beans-updates/src/main/java/com/deepclone/lw/beans/updates/GameUpdateTransaction.java b/legacyworlds-server-beans-updates/src/main/java/com/deepclone/lw/beans/updates/GameUpdateTransaction.java new file mode 100644 index 0000000..b040555 --- /dev/null +++ b/legacyworlds-server-beans-updates/src/main/java/com/deepclone/lw/beans/updates/GameUpdateTransaction.java @@ -0,0 +1,89 @@ +package com.deepclone.lw.beans.updates; + + +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; + +import com.deepclone.lw.interfaces.game.updates.UpdateBatchProcessor; +import com.deepclone.lw.interfaces.game.updates.UpdatesDAO; +import com.deepclone.lw.sqld.sys.GameUpdateResult; + + + +/** + * Game update transaction + * + *

+ * This class is responsible for executing the necessary operations during a game update + * transaction. It is used by the {@link GameUpdateProcessorBean} component to implement its main + * processing loop. + * + * @author E. Benoît + */ +class GameUpdateTransaction + implements TransactionCallback< Boolean > +{ + /** The game updates database access interface */ + private final UpdatesDAO updatesDao; + + /** The registry of server-side update batch processors */ + private final ServerProcessorRegistry registry; + + /** The identifier of the current update cycle */ + private final long tickId; + + + /** + * Initialise the various dependencies and set the identifier of the current cycle + * + * @param updatesDao + * the game updates database access interface + * @param registry + * the registry of server-side update batch processors + * @param tickId + * the identifier of the current update cycle + */ + GameUpdateTransaction( UpdatesDAO updatesDao , ServerProcessorRegistry registry , long tickId ) + { + this.updatesDao = updatesDao; + this.registry = registry; + this.tickId = tickId; + } + + + /** + * Update batch processing transaction + * + *

+ * Call the database's game update processing function, returning immediately if the function + * indicates that the cycle has been completed. Otherwise, execute a server-side processor if + * necessary. + * + *

+ * If the database requests server-side processing but no processor is found, an + * {@link UnsupportedUpdateException} will be thrown. + * + * @return true if the current update cycle has been completed, false + * if more updates are needed. + */ + @Override + public Boolean doInTransaction( TransactionStatus status ) + { + GameUpdateResult result = this.updatesDao.processUpdates( this.tickId ); + String local = result.getLocalExecution( ); + if ( result.isFinished( ) ) { + return true; + } else if ( local == null ) { + return false; + } + + UpdateBatchProcessor processor = this.registry.getProcessorFor( local ); + if ( processor == null ) { + throw new UnsupportedUpdateException( local ); + } + + processor.processBatch( this.tickId ); + return false; + } + +} diff --git a/legacyworlds-server-beans-updates/src/main/java/com/deepclone/lw/beans/updates/ServerProcessorRegistry.java b/legacyworlds-server-beans-updates/src/main/java/com/deepclone/lw/beans/updates/ServerProcessorRegistry.java new file mode 100644 index 0000000..423c7cc --- /dev/null +++ b/legacyworlds-server-beans-updates/src/main/java/com/deepclone/lw/beans/updates/ServerProcessorRegistry.java @@ -0,0 +1,33 @@ +package com.deepclone.lw.beans.updates; + + +import com.deepclone.lw.interfaces.game.updates.UpdateBatchProcessor; + + + +/** + * Server-side update processor registry + * + *

+ * This interface allows server-side game update batch processors to be looked up depending on the + * type of the updates to process. + * + * @author E. Benoît + * + */ +interface ServerProcessorRegistry +{ + + /** + * Get a batch processor for some type of update + * + * @param type + * the type of updates to process, as stored in the sys.update_types + * database table + * + * @return null if no processor for the specified type has been registered, or the + * processor to use. + */ + public UpdateBatchProcessor getProcessorFor( String type ); + +} diff --git a/legacyworlds-server-beans-updates/src/main/java/com/deepclone/lw/beans/updates/ServerProcessorRegistryBean.java b/legacyworlds-server-beans-updates/src/main/java/com/deepclone/lw/beans/updates/ServerProcessorRegistryBean.java new file mode 100644 index 0000000..9055ae9 --- /dev/null +++ b/legacyworlds-server-beans-updates/src/main/java/com/deepclone/lw/beans/updates/ServerProcessorRegistryBean.java @@ -0,0 +1,68 @@ +package com.deepclone.lw.beans.updates; + + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.config.BeanPostProcessor; + +import com.deepclone.lw.interfaces.game.updates.UpdateBatchProcessor; + + + +/** + * Server-side update processor registry component + * + *

+ * This component will look through registered Spring components for update processors, and use the + * results as the registry's contents. + * + * @author E. Benoît + */ +class ServerProcessorRegistryBean + implements ServerProcessorRegistry , BeanPostProcessor +{ + + /** Known server-side update batch processors */ + private final Map< String , UpdateBatchProcessor > processors = new HashMap< String , UpdateBatchProcessor >( ); + + + /** Do nothing - we're not interested in uninitialised components */ + @Override + public Object postProcessBeforeInitialization( Object bean , String beanName ) + { + return bean; + } + + + /** + * If an initialised component implements the {@link UpdateBatchProcessor} interface, add it to + * the {@link #processors} map. A {@link BeanInitializationException} will be thrown if two + * components seem to process the same update type. + */ + @Override + public Object postProcessAfterInitialization( Object bean , String beanName ) + throws BeansException + { + if ( bean instanceof UpdateBatchProcessor ) { + UpdateBatchProcessor processor = (UpdateBatchProcessor) bean; + String updType = processor.getUpdateType( ); + if ( this.processors.containsKey( updType ) ) { + throw new BeanInitializationException( "Duplicate update processor for update type " + updType ); + } + this.processors.put( updType , processor ); + } + return bean; + } + + + /** Obtain the processor from the {@link #processors} map */ + @Override + public UpdateBatchProcessor getProcessorFor( String type ) + { + return this.processors.get( type ); + } + +} diff --git a/legacyworlds-server-beans-updates/src/main/java/com/deepclone/lw/beans/updates/UnsupportedUpdateException.java b/legacyworlds-server-beans-updates/src/main/java/com/deepclone/lw/beans/updates/UnsupportedUpdateException.java new file mode 100644 index 0000000..c167bf1 --- /dev/null +++ b/legacyworlds-server-beans-updates/src/main/java/com/deepclone/lw/beans/updates/UnsupportedUpdateException.java @@ -0,0 +1,29 @@ +package com.deepclone.lw.beans.updates; + + +/** + * Unsupported update exception + * + *

+ * This exception is thrown by the {@link GameUpdateTransaction} when there is no server-side + * processor for some update type. + * + * @author E. Benoît + */ +@SuppressWarnings( "serial" ) +public class UnsupportedUpdateException + extends RuntimeException +{ + + /** + * Initialise the exception + * + * @param name + * the name of the update type for which no processor is registered. + */ + UnsupportedUpdateException( String name ) + { + super( "No processor found for update type " + name ); + } + +} diff --git a/legacyworlds-server-beans-updates/src/main/java/com/deepclone/lw/beans/updates/UpdatesDAOBean.java b/legacyworlds-server-beans-updates/src/main/java/com/deepclone/lw/beans/updates/UpdatesDAOBean.java new file mode 100644 index 0000000..8de9681 --- /dev/null +++ b/legacyworlds-server-beans-updates/src/main/java/com/deepclone/lw/beans/updates/UpdatesDAOBean.java @@ -0,0 +1,69 @@ +package com.deepclone.lw.beans.updates; + + +import java.sql.Types; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Autowired; + +import com.deepclone.lw.interfaces.game.updates.UpdatesDAO; +import com.deepclone.lw.sqld.sys.GameUpdateResult; +import com.deepclone.lw.utils.StoredProc; + + + +/** + * Updates data access component + * + *

+ * This component accesses the database procedures and views which constitute the game update + * system. + * + * @author E. Benoît + */ +class UpdatesDAOBean + implements UpdatesDAO +{ + + /** Game update processor stored procedure */ + private StoredProc process; + + + /** + * Dependency injector for the data source + * + *

+ * When Spring injects the data source component, initialise the stored procedures used by the + * component. + * + * @param dataSource + * the data source component + */ + @Autowired( required = true ) + public void setDataSource( DataSource dataSource ) + { + this.process = new StoredProc( dataSource , "sys" , "process_updates" ).addParameter( "tick_id" , Types.BIGINT ) + .addOutput( "_has_more" , Types.BOOLEAN ).addOutput( "_process_externally" , Types.VARCHAR ); + } + + + /** + * Execute the processor's stored procedure + * + *

+ * This implementation simply runs the database's sys.process_updates() stored + * procedure, and converts the two values it returns into a {@link GameUpdateResult} instance. + */ + @Override + public GameUpdateResult processUpdates( long tickId ) + { + Map< String , Object > m = this.process.execute( tickId ); + if ( !(Boolean) m.get( "_has_more" ) ) { + return new GameUpdateResult( ); + } + return new GameUpdateResult( (String) m.get( "_process_externally" ) ); + } + +} diff --git a/legacyworlds-server-beans-updates/src/main/resources/.empty b/legacyworlds-server-beans-updates/src/main/resources/.empty new file mode 100644 index 0000000..e69de29 diff --git a/legacyworlds-server-beans-simple/src/main/resources/configuration/simple/updates-dao-bean.xml b/legacyworlds-server-beans-updates/src/main/resources/configuration/game.xml similarity index 61% rename from legacyworlds-server-beans-simple/src/main/resources/configuration/simple/updates-dao-bean.xml rename to legacyworlds-server-beans-updates/src/main/resources/configuration/game.xml index eb24044..687e57a 100644 --- a/legacyworlds-server-beans-simple/src/main/resources/configuration/simple/updates-dao-bean.xml +++ b/legacyworlds-server-beans-updates/src/main/resources/configuration/game.xml @@ -2,8 +2,8 @@ + http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd"> - + - + \ No newline at end of file diff --git a/legacyworlds-server-beans-updates/src/main/resources/configuration/game/updates.xml b/legacyworlds-server-beans-updates/src/main/resources/configuration/game/updates.xml new file mode 100644 index 0000000..b957642 --- /dev/null +++ b/legacyworlds-server-beans-updates/src/main/resources/configuration/game/updates.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/legacyworlds-server-beans-updates/src/test/java/.empty b/legacyworlds-server-beans-updates/src/test/java/.empty new file mode 100644 index 0000000..e69de29 diff --git a/legacyworlds-server-beans-updates/src/test/resources/.empty b/legacyworlds-server-beans-updates/src/test/resources/.empty new file mode 100644 index 0000000..e69de29 diff --git a/legacyworlds-server-beans/pom.xml b/legacyworlds-server-beans/pom.xml index 6e138de..a03d945 100644 --- a/legacyworlds-server-beans/pom.xml +++ b/legacyworlds-server-beans/pom.xml @@ -21,15 +21,16 @@ - ../legacyworlds-server-beans-i18n - ../legacyworlds-server-beans-eventlog ../legacyworlds-server-beans-accounts + ../legacyworlds-server-beans-bt + ../legacyworlds-server-beans-eventlog + ../legacyworlds-server-beans-i18n ../legacyworlds-server-beans-mailer - ../legacyworlds-server-beans-system ../legacyworlds-server-beans-naming ../legacyworlds-server-beans-resources - ../legacyworlds-server-beans-bt - ../legacyworlds-server-beans-user ../legacyworlds-server-beans-simple + ../legacyworlds-server-beans-system + ../legacyworlds-server-beans-updates + ../legacyworlds-server-beans-user diff --git a/legacyworlds-server-data/db-structure/database.sql b/legacyworlds-server-data/db-structure/database.sql index 50b7913..9552e38 100644 --- a/legacyworlds-server-data/db-structure/database.sql +++ b/legacyworlds-server-data/db-structure/database.sql @@ -48,4 +48,4 @@ BEGIN; COMMIT; /* Delete loader script */ -\! rm loader.tmp \ No newline at end of file +\! rm loader.tmp diff --git a/legacyworlds-server-data/db-structure/parts/030-data/140-status.sql b/legacyworlds-server-data/db-structure/parts/030-data/140-status.sql index a3c76bb..ef0b8d4 100644 --- a/legacyworlds-server-data/db-structure/parts/030-data/140-status.sql +++ b/legacyworlds-server-data/db-structure/parts/030-data/140-status.sql @@ -1,7 +1,7 @@ -- LegacyWorlds Beta 6 -- PostgreSQL database scripts -- --- System & game updates status +-- System & ticker status -- -- Copyright(C) 2004-2010, DeepClone Development -- -------------------------------------------------------- @@ -53,55 +53,3 @@ INSERT INTO sys.ticker( task_name , status ) VALUES ( 'Game update' , 'STOPPED' ); GRANT SELECT ON sys.ticker TO :dbuser; - - - --- --- Updates --- -CREATE TABLE sys.updates( - id BIGSERIAL NOT NULL PRIMARY KEY , - gu_type update_type NOT NULL , - status processing_status NOT NULL DEFAULT 'FUTURE' , - last_tick BIGINT NOT NULL DEFAULT -1 -); - -CREATE INDEX idx_updates_finder - ON sys.updates (gu_type, status, last_tick); - - --- --- Planet updates --- -CREATE TABLE verse.updates( - update_id BIGINT NOT NULL PRIMARY KEY , - planet_id INT NOT NULL -); - -CREATE INDEX idx_planetupdates_planet - ON verse.updates (planet_id); - -ALTER TABLE verse.updates - ADD CONSTRAINT fk_planetupdates_update - FOREIGN KEY ( update_id ) REFERENCES sys.updates , - ADD CONSTRAINT fk_planetupdates_planet - FOREIGN KEY ( planet_id ) REFERENCES verse.planets; - - --- --- Empire updates --- -CREATE TABLE emp.updates( - update_id BIGINT NOT NULL PRIMARY KEY , - empire_id INT NOT NULL -); - -CREATE INDEX idx_empireupdates_empire - ON emp.updates( empire_id ); - -ALTER TABLE emp.updates - ADD CONSTRAINT fk_empireupdates_update - FOREIGN KEY ( update_id ) REFERENCES sys.updates , - ADD CONSTRAINT fk_empireupdates_empire - FOREIGN KEY ( empire_id ) REFERENCES emp.empires - ON DELETE CASCADE; diff --git a/legacyworlds-server-data/db-structure/parts/030-data/145-updates.sql b/legacyworlds-server-data/db-structure/parts/030-data/145-updates.sql new file mode 100644 index 0000000..8ce13f5 --- /dev/null +++ b/legacyworlds-server-data/db-structure/parts/030-data/145-updates.sql @@ -0,0 +1,172 @@ +-- LegacyWorlds Beta 6 +-- PostgreSQL database scripts +-- +-- Game updates control tables +-- +-- Copyright(C) 2004-2012, DeepClone Development +-- -------------------------------------------------------- + + +/* + * Update targets + * --------------- + * + * An update target refers to a table whose rows are the units to be updated + * by some step of the game update. + * + * Inserting rows into this control table will generate a new, specific table + * to store references, and a set of triggers. + */ +DROP TABLE IF EXISTS sys.update_targets CASCADE; +CREATE TABLE sys.update_targets( + /* Automatically generated identifier of the target type */ + updtgt_id SERIAL NOT NULL PRIMARY KEY , + + /* Name of the target, displayed in the administration interface */ + updtgt_name VARCHAR( 32 ) NOT NULL , + + /* Schema in which the target table resides */ + updtgt_schema NAME NOT NULL , + + /* Name of the target table */ + updtgt_table NAME NOT NULL +); + +/* Update target names are unique */ +CREATE UNIQUE INDEX idx_updtgt_name + ON sys.update_targets ( LOWER( updtgt_name ) ); +/* Schema / table combinations are unique */ +CREATE UNIQUE INDEX idx_updtgt_target + ON sys.update_targets ( updtgt_schema , updtgt_table ); + + + +/* + * Update type definitions + * ------------------------ + * + * An update type corresponds to a procedure which will be applied at each + * game update cycle (once per minute, unless defaults have been modified), + * and which performs a computation or set thereof on the game's data. + */ +DROP TABLE IF EXISTS sys.update_types CASCADE; +CREATE TABLE sys.update_types( + /* Automatically generated identifier of the update type */ + updtype_id SERIAL NOT NULL , + + /* The type of target this update refers to */ + updtgt_id INT NOT NULL , + + /* Name of the update type, used in the administration interface and when + * updates need to be executed externally. + */ + updtype_name VARCHAR( 64 ) NOT NULL , + + /* Ordering index of the update type. This field is always re-generated + * when update types are added or removed. + */ + updtype_ordering INT NOT NULL , + + /* Description of the update type to be included in the administration + * interface. + */ + updtype_description TEXT NOT NULL , + + /* Name of the stored procedure which handles the update. When this is + * NULL, the update type is assumed to be supported externally (i.e. by + * the game server) rather than internally. Otherwise, a stored procedure + * bearing that name, accepting a BIGINT as its parameter and returning + * VOID, must exist in the sys schema. + */ + updtype_proc_name NAME , + + /* Size of the update batch. If this is NULL, the global value from the + * game.batchSize constant will be used. + */ + updtype_batch_size INT , + + /* The primary key includes both the update type identifier and the target + * identifier for coherence reasons. + */ + PRIMARY KEY( updtype_id , updtgt_id ) , + + /* Batch sizes are either NULL or strictly positive */ + CHECK( updtype_batch_size IS NULL + OR updtype_batch_size > 0 ) +); + +/* Update names must be unique, independently of the case */ +CREATE UNIQUE INDEX idx_updtype_name + ON sys.update_types ( LOWER( updtype_name ) ); +/* Update ordering index must be unique */ +CREATE UNIQUE INDEX idx_updtype_ordering + ON sys.update_types ( updtype_ordering ); +/* Procedure names must be unique */ +CREATE UNIQUE INDEX idx_updtype_procname + ON sys.update_types ( updtype_proc_name ); + + + +ALTER TABLE sys.update_types + ADD CONSTRAINT fk_updtype_target + FOREIGN KEY ( updtgt_id ) REFERENCES sys.update_targets( updtgt_id ); + + + +/* + * Update state type + * ------------------ + * + * This type represents the possible states of a game update + */ +DROP TYPE IF EXISTS sys.update_state_type CASCADE; +CREATE TYPE sys.update_state_type + AS ENUM ( + /* The row will be included in the current game update */ + 'FUTURE' , + /* The row is being processed */ + 'PROCESSING' , + /* The row has been processed by the current or previous game update */ + 'PROCESSED' + ); + + +/* + * Main updates table + * ------------------- + * + * This table lists all update rows, including their type and target, as well + * as their state. + */ +DROP TABLE IF EXISTS sys.updates CASCADE; +CREATE TABLE sys.updates( + /* The update row's automatically generated identifier */ + update_id BIGSERIAL NOT NULL , + + /* The type of update this row is about */ + updtype_id INT NOT NULL , + + /* The type of target for the update */ + updtgt_id INT NOT NULL , + + /* The update row's current state */ + update_state sys.update_state_type + NOT NULL + DEFAULT 'FUTURE' , + + /* The tick identifier corresponding to the last game update in which + * this row was processed. + */ + update_last BIGINT NOT NULL + DEFAULT -1 , + + /* The primary key includes the automatically generated identifier but + * also the type and target type. + */ + PRIMARY KEY( update_id , updtype_id , updtgt_id ) +); + +ALTER TABLE sys.updates + ADD CONSTRAINT fk_update_type + FOREIGN KEY ( updtype_id , updtgt_id ) REFERENCES sys.update_types + ON DELETE CASCADE; diff --git a/legacyworlds-server-data/db-structure/parts/040-functions/003-updates.sql b/legacyworlds-server-data/db-structure/parts/040-functions/003-updates.sql new file mode 100644 index 0000000..0a2e5f3 --- /dev/null +++ b/legacyworlds-server-data/db-structure/parts/040-functions/003-updates.sql @@ -0,0 +1,571 @@ +-- LegacyWorlds Beta 6 +-- PostgreSQL database scripts +-- +-- Game updates - support functions for update definitions +-- +-- Copyright(C) 2004-2012, DeepClone Development +-- -------------------------------------------------------- + + +/* + * Type that represents a field from a primary key + * ------------------------------------------------ + * + * This type represents both the field itself (as a name) and its type. + */ +DROP TYPE IF EXISTS sys.pk_field_type CASCADE; +CREATE TYPE sys.pk_field_type AS ( + f_name NAME , + f_type NAME +); + + +/* + * Get a table's primary key fields and field types + * ------------------------------------------------- + * + * This function will list all fields from a table's primary key along with + * their types. + * + * Parameters: + * _schema_name The name of the schema the table resides in + * _table_name The name of the table + * + * Returns: + * ? A set of sys.pk_field_type records + */ +DROP FUNCTION IF EXISTS sys.get_table_pkey( NAME , NAME ) CASCADE; +CREATE FUNCTION sys.get_table_pkey( _schema_name NAME , _table_name NAME ) + RETURNS SETOF sys.pk_field_type + LANGUAGE SQL + STRICT IMMUTABLE + SECURITY INVOKER +AS $get_table_pkey$ + + SELECT _attr.attname AS f_name , _type.typname AS f_type + + FROM pg_namespace _schema + INNER JOIN pg_class _table + ON _table.relnamespace = _schema.oid + INNER JOIN ( + SELECT indrelid , unnest( indkey ) AS indattnum + FROM pg_index WHERE indisprimary + ) _index ON _index.indrelid = _table.oid + INNER JOIN pg_attribute _attr + ON _attr.attrelid = _table.oid + AND _attr.attnum = _index.indattnum + INNER JOIN pg_type _type + ON _type.oid = _attr.atttypid + + WHERE _schema.nspname = $1 + AND _table.relname = $2 + + ORDER BY _index.indattnum; + +$get_table_pkey$; + +REVOKE EXECUTE + ON FUNCTION sys.get_table_pkey( NAME , NAME ) + FROM PUBLIC; + + + +/* + * Automatic update row insertion + * ------------------------------- + * + * This trigger function is added to all type-specific update tables. It is + * called before insertions, and will make sure a generic update row exists + * for the new row. + */ +DROP FUNCTION IF EXISTS sys.tgf_speupd_before_insert( ) CASCADE; +CREATE FUNCTION sys.tgf_speupd_before_insert( ) + RETURNS TRIGGER + LANGUAGE PLPGSQL + STRICT VOLATILE + SECURITY INVOKER + AS $tgf_speupd_before_insert$ + +DECLARE + _update_id BIGINT; + +BEGIN + + INSERT INTO sys.updates ( updtype_id , updtgt_id ) + VALUES ( NEW.updtype_id , NEW.updtgt_id ) + RETURNING update_id INTO _update_id; + + NEW.update_id := _update_id; + RETURN NEW; +END; +$tgf_speupd_before_insert$; + +REVOKE EXECUTE + ON FUNCTION sys.tgf_speupd_before_insert( ) + FROM PUBLIC; + + + +/* + * Automatic update row removal + * ----------------------------- + * + * This trigger function is added to all type-specific update tables. It is + * called once a row has been deleted, and will remove the corresponding + * generic update row. + */ +DROP FUNCTION IF EXISTS sys.tgf_speupd_after_delete( ) CASCADE; +CREATE FUNCTION sys.tgf_speupd_after_delete( ) + RETURNS TRIGGER + LANGUAGE PLPGSQL + STRICT VOLATILE + SECURITY INVOKER + AS $tgf_speupd_after_delete$ +BEGIN + + DELETE FROM sys.updates + WHERE update_id = OLD.update_id + AND updtype_id = OLD.updtype_id + AND updtgt_id = OLD.updtgt_id; + + RETURN OLD; +END; +$tgf_speupd_after_delete$; + +REVOKE EXECUTE + ON FUNCTION sys.tgf_speupd_after_delete( ) + FROM PUBLIC; + + + +/* + * Add missing update rows + * ------------------------ + * + * This function adds rows missing from the update and specific update tables + * for a given target type. + * + * Parameters: + * _target_type The identifier of the target type for which missing + * rows are to be inserted. + */ +DROP FUNCTION IF EXISTS sys.insert_missing_updates( INT ) CASCADE; +CREATE FUNCTION sys.insert_missing_updates( _target_type INT ) + RETURNS VOID + LANGUAGE PLPGSQL + STRICT VOLATILE + SECURITY INVOKER + AS $insert_missing_updates$ + +DECLARE + _t_schema TEXT; + _t_table TEXT; + _field_list TEXT; + _query TEXT; + +BEGIN + SELECT INTO _t_schema , _t_table updtgt_schema , updtgt_table + FROM sys.update_targets + WHERE updtgt_id = _target_type; + SELECT INTO _field_list array_to_string( array_agg( f_name ) , ' , ' ) + FROM sys.get_table_pkey( _t_schema , _t_table ); + + _query := 'INSERT INTO "' || _t_schema || '"."' || _t_table || '_updates" ' + || '( updtgt_id , updtype_id , ' || _field_list || ') ' + || 'SELECT updtgt_id , updtype_id , ' || _field_list + || ' FROM sys.update_types CROSS JOIN "' + || _t_schema || '"."' || _t_table || '" LEFT OUTER JOIN "' + || _t_schema || '"."' || _t_table + || '_updates" USING ( updtgt_id , updtype_id , ' || _field_list + || ') WHERE update_id IS NULL AND updtgt_id = ' || _target_type; + EXECUTE _query; + +END; +$insert_missing_updates$; + +REVOKE EXECUTE + ON FUNCTION sys.insert_missing_updates( INT ) + FROM PUBLIC; + + + +/* + * Trigger function that adds missing update rows + * ---------------------------------------------- + * + * This function calls sys.insert_missing_updates. The main difference between + * the original function and this one is that the former cannot be used in + * triggers, while the latter is meant to be used as a trigger on the target + * table. + */ +DROP FUNCTION IF EXISTS sys.tgf_insert_missing_updates( ) CASCADE; +CREATE FUNCTION sys.tgf_insert_missing_updates( ) + RETURNS TRIGGER + LANGUAGE PLPGSQL + STRICT VOLATILE + SECURITY INVOKER + AS $tgf_insert_missing_updates$ +BEGIN + IF TG_NARGS <> 1 THEN + RAISE EXCEPTION 'This trigger function requires one argument'; + END IF; + PERFORM sys.insert_missing_updates( TG_ARGV[ 0 ]::INT ); + RETURN NULL; +END; +$tgf_insert_missing_updates$; + +REVOKE EXECUTE + ON FUNCTION sys.tgf_insert_missing_updates( ) + FROM PUBLIC; + + +/* + * Handle new update types + * ------------------------ + * + * This function is triggered when a new update type is created; it will insert + * the update rows for the new type. + */ +DROP FUNCTION IF EXISTS sys.tgf_updtype_after_insert_row( ) CASCADE; +CREATE FUNCTION sys.tgf_updtype_after_insert_row( ) + RETURNS TRIGGER + LANGUAGE PLPGSQL + STRICT VOLATILE + SECURITY INVOKER + AS $tgf_updtype_after_insert$ +BEGIN + PERFORM sys.insert_missing_updates( NEW.updtgt_id ); + RETURN NEW; +END; +$tgf_updtype_after_insert$; + +REVOKE EXECUTE + ON FUNCTION sys.tgf_updtype_after_insert_row( ) + FROM PUBLIC; + +CREATE TRIGGER tg_updtype_after_insert_row + AFTER INSERT ON sys.update_types + FOR EACH ROW EXECUTE PROCEDURE sys.tgf_updtype_after_insert_row( ); + + +/* + * Update type definition check + * ----------------------------- + * + * This trigger function is called when a new update type is added or when + * an update type's stored procedure is updated. It makes sure that the + * corresponding stored procedure actually exists. + */ +DROP FUNCTION IF EXISTS sys.tgf_check_update_type_proc( ) CASCADE; +CREATE FUNCTION sys.tgf_check_update_type_proc( ) + RETURNS TRIGGER + LANGUAGE PLPGSQL + STRICT VOLATILE + SECURITY INVOKER + AS $tgf_check_update_type_proc$ +BEGIN + + IF NEW.updtype_proc_name IS NULL THEN + RETURN NEW; + END IF; + + PERFORM 1 + FROM pg_namespace _schema + INNER JOIN pg_proc _proc + ON _proc.pronamespace = _schema.oid + WHERE _schema.nspname = 'sys' + AND _proc.proname = NEW.updtype_proc_name; + IF NOT FOUND THEN + RAISE EXCEPTION 'Update type % has invalid update function sys.%' , + NEW.updtype_name , NEW.updtype_proc_name; + END IF; + + RETURN NEW; +END; +$tgf_check_update_type_proc$; + +REVOKE EXECUTE + ON FUNCTION sys.tgf_check_update_type_proc( ) + FROM PUBLIC; + +CREATE TRIGGER tg_check_update_type_proc_insert + BEFORE INSERT ON sys.update_types + FOR EACH ROW EXECUTE PROCEDURE sys.tgf_check_update_type_proc( ); +CREATE TRIGGER tg_check_update_type_proc_update + BEFORE UPDATE OF updtype_proc_name ON sys.update_types + FOR EACH ROW EXECUTE PROCEDURE sys.tgf_check_update_type_proc( ); + + +/* + * Trigger that reorders the update types + * --------------------------------------- + * + * This function will update the ordering field of update types whenever rows + * are inserted or deleted. + */ +DROP FUNCTION IF EXISTS sys.tgf_reorder_update_types( ) CASCADE; +CREATE FUNCTION sys.tgf_reorder_update_types( ) + RETURNS TRIGGER + LANGUAGE PLPGSQL + STRICT VOLATILE + SECURITY INVOKER + AS $tgf_reorder_update_types$ + +DECLARE + _max_ordering INT; + +BEGIN + + SELECT INTO _max_ordering MAX( updtype_ordering ) FROM sys.update_types; + IF _max_ordering IS NOT NULL THEN + UPDATE sys.update_types + SET updtype_ordering = updtype_ordering + _max_ordering * 2; + END IF; + + UPDATE sys.update_types + SET updtype_ordering = updtype_rownumber * 2 + FROM ( + SELECT updtype_id AS id, + row_number( ) OVER ( + ORDER BY updtype_ordering + ) AS updtype_rownumber + FROM sys.update_types + ) _row + WHERE updtype_id = _row.id; + + RETURN NULL; +END; +$tgf_reorder_update_types$; + +REVOKE EXECUTE + ON FUNCTION sys.tgf_reorder_update_types( ) + FROM PUBLIC; + +CREATE TRIGGER tg_updtype_after_insert_stmt + AFTER INSERT ON sys.update_types + FOR EACH STATEMENT EXECUTE PROCEDURE sys.tgf_reorder_update_types( ); + +CREATE TRIGGER tg_updtype_after_delete_stmt + AFTER DELETE ON sys.update_types + FOR EACH STATEMENT EXECUTE PROCEDURE sys.tgf_reorder_update_types( ); + + + +/* + * Check target tables + * -------------------- + * + * Before a new row is inserted into sys.update_targets, make sure + * the target table it contains actually exists. + */ +DROP FUNCTION IF EXISTS sys.tgf_updtgt_before_insert( ) CASCADE; +CREATE FUNCTION sys.tgf_updtgt_before_insert( ) + RETURNS TRIGGER + LANGUAGE PLPGSQL + STRICT VOLATILE + SECURITY INVOKER + AS $tgf_updtgt_before_insert$ +BEGIN + PERFORM 1 + FROM pg_class pc + INNER JOIN pg_namespace pn + ON pn.oid = pc.relnamespace + INNER JOIN pg_roles pr + ON pr.oid = pc.relowner + WHERE pn.nspname = NEW.updtgt_schema + AND pc.relname = NEW.updtgt_table + AND pc.reltype <> 0 AND pc.relkind = 'r' + AND pr.rolname <> 'postgres'; + IF NOT FOUND THEN + RAISE EXCEPTION 'Update target table %.% not found' , + NEW.updtgt_schema , NEW.updtgt_table + USING ERRCODE = 'foreign_key_violation'; + END IF; + + RETURN NEW; +END; +$tgf_updtgt_before_insert$; + +REVOKE EXECUTE + ON FUNCTION sys.tgf_updtgt_before_insert( ) + FROM PUBLIC; + +CREATE TRIGGER tg_updtgt_before_insert + BEFORE INSERT ON sys.update_targets + FOR EACH ROW EXECUTE PROCEDURE sys.tgf_updtgt_before_insert( ); + + + +/* + * Create update list and triggers for target table + * ------------------------------------------------- + * + * After a new update type has been registered, a table listing updates + * as well as a set of triggers on the target table need to be created. + */ +DROP FUNCTION IF EXISTS sys.tgf_updtgt_after_insert( ) CASCADE; +CREATE FUNCTION sys.tgf_updtgt_after_insert( ) + RETURNS TRIGGER + LANGUAGE PLPGSQL + STRICT VOLATILE + AS $tgf_updtgt_after_insert$ + +DECLARE + _table_name TEXT; + _query TEXT; + _field_name NAME; + _field_type NAME; + _field_list TEXT; + +BEGIN + _table_name := '"' || NEW.updtgt_schema || '"."' || NEW.updtgt_table || '_updates"'; + _query := 'CREATE TABLE ' || _table_name || $_table_base_fields$( + update_id BIGINT NOT NULL , + updtype_id INT NOT NULL , + updtgt_id INT NOT NULL + $_table_base_fields$; + + -- List target table's primary key fields + _field_list := ''; + FOR _field_name , _field_type IN + SELECT * FROM sys.get_table_pkey( NEW.updtgt_schema , NEW.updtgt_table ) + LOOP + _query := _query || ', "' || _field_name || '" "' || _field_type + || '" NOT NULL'; + IF _field_list <> '' THEN + _field_list := _field_list || ' , '; + END IF; + _field_list := _field_list || '"' || _field_name || '"'; + END LOOP; + + _query := _query || + ' , PRIMARY KEY ( updtgt_id , updtype_id , update_id , ' + || _field_list || ' ) , CHECK ( updtgt_id = ' || NEW.updtgt_id || ' ) )'; + EXECUTE _query; + + -- Add foreign keys + _query := 'ALTER TABLE ' || _table_name + || ' ADD CONSTRAINT fk_upd_' || NEW.updtgt_table || '_update ' + || 'FOREIGN KEY ( update_id , updtype_id , updtgt_id ) REFERENCES sys.updates ' + || 'ON DELETE CASCADE , ' + || ' ADD CONSTRAINT fk_upd_' || NEW.updtgt_table || '_target ' + || 'FOREIGN KEY ( ' || _field_list + || ' ) REFERENCES "' || NEW.updtgt_schema || '"."' || NEW.updtgt_table + || '" ON DELETE CASCADE'; + EXECUTE _query; + + -- Create automatic update creation/deletion triggers + _query := 'CREATE TRIGGER tg_speupd_before_insert BEFORE INSERT ON ' || _table_name + || ' FOR EACH ROW EXECUTE PROCEDURE sys.tgf_speupd_before_insert( )'; + EXECUTE _query; + _query := 'CREATE TRIGGER tg_speupd_after_delete AFTER DELETE ON ' || _table_name + || ' FOR EACH ROW EXECUTE PROCEDURE sys.tgf_speupd_after_delete( )'; + EXECUTE _query; + + -- Create triggers that will insert/delete update rows when new items are added/removed + _query := 'CREATE TRIGGER tg_speupd_after_insert AFTER INSERT ON "' + || NEW.updtgt_schema || '"."' || NEW.updtgt_table + || '" FOR EACH STATEMENT EXECUTE PROCEDURE sys.tgf_insert_missing_updates( ' + || NEW.updtgt_id || ' )'; + EXECUTE _query; + + RETURN NEW; +END; +$tgf_updtgt_after_insert$; + +REVOKE EXECUTE + ON FUNCTION sys.tgf_updtgt_after_insert( ) + FROM PUBLIC; + +CREATE TRIGGER tg_updtgt_after_insert + AFTER INSERT ON sys.update_targets + FOR EACH ROW EXECUTE PROCEDURE sys.tgf_updtgt_after_insert( ); + + +/* + * Trigger function that deletes specific update tables + * ----------------------------------------------------- + * + * This trigger function is called after a row has been deleted from the + * update target definitions table. It will destroy the corresponding table. + */ +DROP FUNCTION IF EXISTS sys.tgf_updtgt_after_delete( ); +CREATE FUNCTION sys.tgf_updtgt_after_delete( ) + RETURNS TRIGGER + LANGUAGE PLPGSQL + STRICT VOLATILE + SECURITY INVOKER + AS $tgf_updtgt_after_delete$ +DECLARE + _query TEXT; +BEGIN + _query := 'DROP TABLE "' || OLD.updtgt_schema || '"."' || OLD.updtgt_table || '_updates"'; + EXECUTE _query; + RETURN NULL; +END; +$tgf_updtgt_after_delete$; + +REVOKE EXECUTE + ON FUNCTION sys.tgf_updtgt_after_delete( ) + FROM PUBLIC; + +CREATE TRIGGER tg_updtgt_after_delete + AFTER DELETE ON sys.update_targets + FOR EACH ROW EXECUTE PROCEDURE sys.tgf_updtgt_after_delete( ); + + + +/* + * Stored update type registration + * -------------------------------- + * + * This function can be called to register an update type that uses a stored + * procedure. The new update type is added at the end of the list of updates. + * + * Since this function is meant to be used from the SQL definition code only, + * it does not handle errors and will raise exceptions when something goes + * wrong. + * + * Parameters: + * _target The name of the target for this update type + * _name The name of the update type + * _description The update type's description + * _proc The name of the stored procedure + */ +DROP FUNCTION IF EXISTS sys.register_update_type( TEXT , TEXT , TEXT , NAME ); +CREATE FUNCTION sys.register_update_type( _target TEXT , _name TEXT , _description TEXT , _proc NAME ) + RETURNS VOID + LANGUAGE PLPGSQL + STRICT VOLATILE + SECURITY INVOKER + AS $register_update_type$ + +DECLARE + _target_id INT; + _max_ordering INT; + +BEGIN + SELECT INTO _target_id updtgt_id + FROM sys.update_targets + WHERE updtgt_name = _target; + + SELECT INTO _max_ordering MAX( updtype_ordering ) + FROM sys.update_types; + IF _max_ordering IS NULL THEN + _max_ordering := 1; + END IF; + + INSERT INTO sys.update_types( + updtgt_id , updtype_name , updtype_ordering , + updtype_description , updtype_proc_name + ) VALUES ( + _target_id , _name , _max_ordering + 1 , + _description , _proc + ); +END; +$register_update_type$; + + +REVOKE EXECUTE + ON FUNCTION sys.register_update_type( TEXT , TEXT , TEXT , NAME ) + FROM PUBLIC; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/parts/040-functions/040-empire.sql b/legacyworlds-server-data/db-structure/parts/040-functions/040-empire.sql index 7963876..63ac90e 100644 --- a/legacyworlds-server-data/db-structure/parts/040-functions/040-empire.sql +++ b/legacyworlds-server-data/db-structure/parts/040-functions/040-empire.sql @@ -45,18 +45,6 @@ BEGIN -- Add empire resources INSERT INTO emp.resources ( empire_id , resource_name_id ) SELECT _name_id , resource_name_id FROM defs.resources; - - -- Add empire update records - FOR _update_type IN SELECT _type - FROM unnest( enum_range( NULL::update_type ) ) AS _type - WHERE _type::text LIKE 'EMPIRE_%' - LOOP - INSERT INTO sys.updates( gu_type ) - VALUES ( _update_type ) - RETURNING id INTO _update; - INSERT INTO emp.updates ( update_id , empire_id ) - VALUES ( _update , _name_id ); - END LOOP; END; $$ LANGUAGE plpgsql; diff --git a/legacyworlds-server-data/db-structure/parts/040-functions/060-universe.sql b/legacyworlds-server-data/db-structure/parts/040-functions/060-universe.sql index 79a7f62..ca6718e 100644 --- a/legacyworlds-server-data/db-structure/parts/040-functions/060-universe.sql +++ b/legacyworlds-server-data/db-structure/parts/040-functions/060-universe.sql @@ -196,17 +196,6 @@ BEGIN -- FIXME: for now, just stick data about resources in the appropriate table INSERT INTO verse.planet_resources ( planet_id , resource_name_id ) SELECT pnid , resource_name_id FROM defs.resources; - - -- Add planet update records - FOR utp IN SELECT x FROM unnest( enum_range( NULL::update_type ) ) AS x - WHERE x::text LIKE 'PLANET_%' - LOOP - INSERT INTO sys.updates( gu_type ) - VALUES ( utp ) - RETURNING id INTO uid; - INSERT INTO verse.updates ( update_id , planet_id ) - VALUES ( uid , pnid ); - END LOOP; END; $$ LANGUAGE plpgsql; diff --git a/legacyworlds-server-data/db-structure/parts/050-updates/000-updates-ctrl.sql b/legacyworlds-server-data/db-structure/parts/050-updates/000-updates-ctrl.sql index 9133bf8..7ab8252 100644 --- a/legacyworlds-server-data/db-structure/parts/050-updates/000-updates-ctrl.sql +++ b/legacyworlds-server-data/db-structure/parts/050-updates/000-updates-ctrl.sql @@ -8,17 +8,29 @@ --- --- Start a tick --- - -CREATE OR REPLACE FUNCTION sys.start_tick( OUT tick_id BIGINT ) +/* + * Start a game update cycle + * -------------------------- + * + * This function prepares the execution of a cycle of game updates. It will + * try to find the identifier of the next update, and mark all game updates + * as requiring an update for this identifier. + * + * Returns: + * tick_id The identifier of the new update cycle, or NULL if an + * update was already in progress. + */ +DROP FUNCTION IF EXISTS sys.start_tick( ) CASCADE; +CREATE FUNCTION sys.start_tick( OUT tick_id BIGINT ) + LANGUAGE PLPGSQL STRICT VOLATILE SECURITY DEFINER - AS $$ + AS $start_tick$ + DECLARE n_tick BIGINT; c_tick BIGINT; + BEGIN -- Get next / current tick SELECT INTO n_tick , c_tick next_tick , current_tick @@ -31,17 +43,27 @@ BEGIN END IF; -- Prepare game updates - UPDATE sys.updates SET last_tick = n_tick , status = 'FUTURE' - WHERE last_tick < n_tick; + UPDATE sys.updates + SET update_last = n_tick , + update_state = 'FUTURE' + WHERE update_last < n_tick; -- Update system status - UPDATE sys.status SET current_tick = n_tick , next_tick = n_tick + 1; + UPDATE sys.status + SET current_tick = n_tick , + next_tick = n_tick + 1; tick_id := n_tick; -END; -$$ LANGUAGE plpgsql; -GRANT EXECUTE ON FUNCTION sys.start_tick( ) TO :dbuser; +END; +$start_tick$; + +REVOKE EXECUTE + ON FUNCTION sys.start_tick( ) + FROM PUBLIC; +GRANT EXECUTE + ON FUNCTION sys.start_tick( ) + TO :dbuser; @@ -64,17 +86,29 @@ $$ LANGUAGE plpgsql; --- --- Check if a tick got "stuck" --- - -CREATE OR REPLACE FUNCTION sys.check_stuck_tick( OUT tick_id BIGINT ) +/* + * Check if a game update cycle got "stuck" + * ----------------------------------------- + * + * Check sys.status for an in-progress game update identifier. If there is + * one, check if there are further updates to execute in the cycle in + * question. + * + * Returns: + * tick_id The stuck tick's identifier, or NULL if there is no stuck + * tick or if maintenance mode is enabled + */ +DROP FUNCTION IF EXISTS sys.check_stuck_tick( ) CASCADE; +CREATE FUNCTION sys.check_stuck_tick( OUT tick_id BIGINT ) + LANGUAGE PLPGSQL STRICT VOLATILE SECURITY DEFINER - AS $$ + AS $check_stuck_tick$ + DECLARE c_tick BIGINT; u_count INT; + BEGIN -- Get next / current tick SELECT INTO c_tick current_tick @@ -87,71 +121,123 @@ BEGIN END IF; -- Are there any updates left? - SELECT INTO u_count count(*) FROM sys.updates - WHERE status = 'FUTURE' AND last_tick = c_tick; + SELECT INTO u_count count(*) + FROM sys.updates + WHERE update_state = 'FUTURE' + AND update_last = c_tick; IF u_count = 0 THEN PERFORM sys.end_tick( c_tick ); tick_id := NULL; ELSE tick_id := c_tick; END IF; + END; -$$ LANGUAGE plpgsql; +$check_stuck_tick$; -GRANT EXECUTE ON FUNCTION sys.check_stuck_tick( ) TO :dbuser; +REVOKE EXECUTE + ON FUNCTION sys.check_stuck_tick( ) + FROM PUBLIC; +GRANT EXECUTE + ON FUNCTION sys.check_stuck_tick( ) + TO :dbuser; --- --- Process game updates --- --- Parameters: --- c_tick Current tick --- --- Returns: --- TRUE if the function must be called again, FALSE otherwise --- - -CREATE OR REPLACE FUNCTION sys.process_updates( IN c_tick BIGINT , OUT has_more BOOLEAN ) +/* + * Process game updates + * --------------------- + * + * This function checks for game update elements, marking some of them for + * processing depending on their type. If there are no more updates to run, + * end the update cycle; otherwise, depending on the type of update, process + * the items internally or return a value that indicates they are to be + * processed by the external Java code. + * + * Parameters: + * _current_update Current game update's identifier + * + * Returns: + * _has_more TRUE if the function must be called again, FALSE + * otherwise + * _process_externally NULL if there is no update to process or if the + * updates were processed in the database, or + * the name of the update type if external + * processing is needed. + */ +DROP FUNCTION IF EXISTS sys.process_updates( BIGINT ) CASCADE; +CREATE FUNCTION sys.process_updates( + IN _current_update BIGINT , + OUT _has_more BOOLEAN , + OUT _process_externally TEXT ) + LANGUAGE PLPGSQL STRICT VOLATILE SECURITY DEFINER - AS $$ + AS $process_updates$ + DECLARE - b_size INT; - p_utype update_type; - utype update_type; - uid BIGINT; + _current_type INT; + _batch_size INT; + _type_name TEXT; + _proc_name NAME; + BEGIN - b_size := sys.get_constant( 'game.batchSize' ); - p_utype := NULL; + -- Mark all entries that were being processed as having been processed + UPDATE sys.updates + SET update_state = 'PROCESSED' + WHERE update_state = 'PROCESSING' + AND update_last = _current_update; - -- Mark at most b_size entries as being updated - FOR uid , utype IN SELECT id , gu_type FROM sys.updates - WHERE last_tick = c_tick AND status = 'FUTURE' - ORDER BY gu_type LIMIT b_size - LOOP - IF p_utype IS NULL THEN - p_utype := utype; + -- Find the next type of update to process and its specific parameters + SELECT INTO _current_type , _batch_size , _type_name , _proc_name + updtype_id , updtype_batch_size , updtype_name , updtype_proc_name + FROM sys.update_types + INNER JOIN sys.updates + USING ( updtype_id , updtgt_id ) + WHERE update_state = 'FUTURE' + AND update_last = _current_update + ORDER BY updtype_ordering + LIMIT 1; + + _has_more := FOUND; + IF _has_more THEN + -- Check batch size + IF _batch_size IS NULL THEN + _batch_size := sys.get_constant( 'game.batchSize' ); END IF; - EXIT WHEN utype <> p_utype; - UPDATE sys.updates SET status = 'PROCESSING' WHERE id = uid; - END LOOP; + + -- Mark at most _batch_size entries of the right type as being updated + UPDATE sys.updates + SET update_state = 'PROCESSING' + WHERE update_id IN ( + SELECT update_id FROM sys.updates + WHERE updtype_id = _current_type + AND update_state = 'FUTURE' + AND update_last = _current_update + LIMIT _batch_size + ); - has_more := p_utype IS NOT NULL; - IF has_more THEN - -- Execute actual updates - EXECUTE 'SELECT sys.process_' || lower( p_utype::TEXT ) || '_updates( $1 )' - USING c_tick; - UPDATE sys.updates SET status = 'PROCESSED' - WHERE status = 'PROCESSING' AND last_tick = c_tick; + IF _proc_name IS NULL THEN + -- External processing is required + _process_externally := _type_name; + ELSE + -- Process updates using a stored procedure + EXECUTE 'SELECT sys."' || _proc_name || '"( $1 )' + USING _current_update; + END IF; ELSE - -- If nothing was found, we're done - PERFORM sys.end_tick( c_tick ); + -- No updates left to run + PERFORM sys.end_tick( _current_update ); END IF; END; -$$ LANGUAGE plpgsql; +$process_updates$; -GRANT EXECUTE ON FUNCTION sys.process_updates( BIGINT ) TO :dbuser; +REVOKE EXECUTE + ON FUNCTION sys.process_updates( BIGINT ) + FROM PUBLIC; +GRANT EXECUTE + ON FUNCTION sys.process_updates( BIGINT ) + TO :dbuser; diff --git a/legacyworlds-server-data/db-structure/parts/050-updates/005-core-update-targets.sql b/legacyworlds-server-data/db-structure/parts/050-updates/005-core-update-targets.sql new file mode 100644 index 0000000..8fa557e --- /dev/null +++ b/legacyworlds-server-data/db-structure/parts/050-updates/005-core-update-targets.sql @@ -0,0 +1,16 @@ +-- LegacyWorlds Beta 6 +-- PostgreSQL database scripts +-- +-- Game updates - core update targets +-- +-- Copyright(C) 2004-2012, DeepClone Development +-- -------------------------------------------------------- + +INSERT INTO sys.update_targets ( + updtgt_name , updtgt_schema , updtgt_table + ) VALUES ( + 'Planets' , 'verse' , 'planets' + ) , ( + 'Empires' , 'emp' , 'empires' + ); + diff --git a/legacyworlds-server-data/db-structure/parts/050-updates/010-empire-money.sql b/legacyworlds-server-data/db-structure/parts/050-updates/010-empire-money.sql index a0bb8b1..26e3088 100644 --- a/legacyworlds-server-data/db-structure/parts/050-updates/010-empire-money.sql +++ b/legacyworlds-server-data/db-structure/parts/050-updates/010-empire-money.sql @@ -18,13 +18,14 @@ DECLARE c_debt REAL; BEGIN -- Lock empires for update - PERFORM e.name_id FROM sys.updates su - INNER JOIN emp.updates eu - ON eu.update_id = su.id + PERFORM e.name_id + FROM sys.updates su + INNER JOIN emp.empires_updates eu + USING ( updtgt_id , updtype_id , update_id ) INNER JOIN emp.empires e - ON eu.empire_id = e.name_id - WHERE su.last_tick = c_tick AND su.status = 'PROCESSING' - AND su.gu_type = 'EMPIRE_MONEY' + USING ( name_id ) + WHERE su.update_last = c_tick + AND su.update_state = 'PROCESSING' FOR UPDATE OF e; -- Select all money-related data from empires being updated @@ -32,14 +33,16 @@ BEGIN ( pov.planet_income - pov.planet_upkeep ) AS p_money , fov.fleet_upkeep AS f_money , ( v.status = 'PROCESSED' ) AS on_vacation FROM sys.updates su - INNER JOIN emp.updates eu ON eu.update_id = su.id - INNER JOIN emp.empires e ON eu.empire_id = e.name_id + INNER JOIN emp.empires_updates eu + USING ( updtgt_id , updtype_id , update_id ) + INNER JOIN emp.empires e + USING ( name_id ) INNER JOIN emp.fleets_overview fov ON fov.empire = e.name_id INNER JOIN emp.planets_overview pov ON pov.empire = e.name_id INNER JOIN naming.empire_names en ON en.id = e.name_id LEFT OUTER JOIN users.vacations v ON v.account_id = en.owner_id - WHERE su.last_tick = c_tick AND su.status = 'PROCESSING' - AND su.gu_type = 'EMPIRE_MONEY' + WHERE su.update_last = c_tick + AND su.update_state = 'PROCESSING' LOOP -- Compute new cash reserve c_cash := 0; @@ -78,4 +81,12 @@ BEGIN WHERE name_id = rec.id; END LOOP; END; -$$ LANGUAGE plpgsql; \ No newline at end of file +$$ LANGUAGE plpgsql; + + +SELECT sys.register_update_type( 'Empires' , 'EmpireMoney' , + 'Empires'' money is being updated using the previous update''s results. ' + || 'This update type should disappear, as everything it does will ' + || 'be replaced by the resources update.' , + 'process_empire_money_updates' + ); \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/parts/050-updates/015-empire-resources.sql b/legacyworlds-server-data/db-structure/parts/050-updates/015-empire-resources.sql index 89c40e2..6004945 100644 --- a/legacyworlds-server-data/db-structure/parts/050-updates/015-empire-resources.sql +++ b/legacyworlds-server-data/db-structure/parts/050-updates/015-empire-resources.sql @@ -42,17 +42,16 @@ AS $process_empire_resources_updates$ END FROM sys.updates _upd_sys - INNER JOIN emp.updates _upd_emp - ON _upd_emp.update_id = _upd_sys.id + INNER JOIN emp.empires_updates _upd_emp + USING ( updtgt_id , updtype_id , update_id ) INNER JOIN emp.planets _emp_planets - USING ( empire_id ) + ON empire_id = name_id INNER JOIN verse.planet_resources _pl_resources USING ( planet_id ) - WHERE _upd_sys.last_tick = $1 - AND _upd_sys.status = 'PROCESSING' - AND _upd_sys.gu_type = 'EMPIRE_RESOURCES' - AND _emp_resources.empire_id = _upd_emp.empire_id + WHERE _upd_sys.update_last = $1 + AND _upd_sys.update_state = 'PROCESSING' + AND _emp_resources.empire_id = _upd_emp.name_id AND _emp_resources.resource_name_id = _pl_resources.resource_name_id; $process_empire_resources_updates$ LANGUAGE SQL; @@ -61,3 +60,10 @@ $process_empire_resources_updates$ LANGUAGE SQL; REVOKE EXECUTE ON FUNCTION sys.process_empire_resources_updates( BIGINT ) FROM PUBLIC; + + + +SELECT sys.register_update_type( 'Empires' , 'EmpireResources' , + 'Empires'' resources are being updated using the previous update''s results. ' , + 'process_empire_resources_updates' + ); \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/parts/050-updates/020-empire-research.sql b/legacyworlds-server-data/db-structure/parts/050-updates/020-empire-research.sql index d9e3cfe..152f83f 100644 --- a/legacyworlds-server-data/db-structure/parts/050-updates/020-empire-research.sql +++ b/legacyworlds-server-data/db-structure/parts/050-updates/020-empire-research.sql @@ -18,28 +18,31 @@ DECLARE tu_rec RECORD; BEGIN -- Lock empires for update and planets for share - PERFORM e.name_id FROM sys.updates su - INNER JOIN emp.updates eu ON eu.update_id = su.id - INNER JOIN emp.empires e ON eu.empire_id = e.name_id + PERFORM e.name_id + FROM sys.updates _upd_sys + INNER JOIN emp.empires_updates eu + USING ( updtgt_id , updtype_id , update_id ) + INNER JOIN emp.empires e USING ( name_id ) INNER JOIN emp.planets ep ON ep.empire_id = e.name_id INNER JOIN verse.planets p ON p.name_id = ep.planet_id - WHERE su.last_tick = c_tick AND su.status = 'PROCESSING' - AND su.gu_type = 'EMPIRE_RESEARCH' + WHERE _upd_sys.update_last = c_tick + AND _upd_sys.update_state = 'PROCESSING' FOR UPDATE OF e FOR SHARE OF ep , p; -- Process empires FOR rec IN SELECT e.name_id AS id , ( v.status = 'PROCESSED' ) AS on_vacation , sum( p.population ) AS population - FROM sys.updates su - INNER JOIN emp.updates eu ON eu.update_id = su.id - INNER JOIN emp.empires e ON eu.empire_id = e.name_id + FROM sys.updates _upd_sys + INNER JOIN emp.empires_updates eu + USING ( updtgt_id , updtype_id , update_id ) + INNER JOIN emp.empires e USING ( name_id ) INNER JOIN emp.planets ep ON ep.empire_id = e.name_id INNER JOIN verse.planets p ON p.name_id = ep.planet_id INNER JOIN naming.empire_names en ON en.id = e.name_id LEFT OUTER JOIN users.vacations v ON v.account_id = en.owner_id - WHERE su.last_tick = c_tick AND su.status = 'PROCESSING' - AND su.gu_type = 'EMPIRE_RESEARCH' + WHERE _upd_sys.update_last = c_tick + AND _upd_sys.update_state = 'PROCESSING' GROUP BY e.name_id , v.status LOOP -- Insert any missing tech line @@ -83,4 +86,9 @@ BEGIN END LOOP; END LOOP; END; -$$ LANGUAGE plpgsql; \ No newline at end of file +$$ LANGUAGE plpgsql; + +SELECT sys.register_update_type( 'Empires' , 'EmpireResearch' , + 'Empire research points are being attributed to technologies.' , + 'process_empire_research_updates' + ); \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/parts/050-updates/025-empire-debt.sql b/legacyworlds-server-data/db-structure/parts/050-updates/025-empire-debt.sql index 7bcaa9f..aa1e849 100644 --- a/legacyworlds-server-data/db-structure/parts/050-updates/025-empire-debt.sql +++ b/legacyworlds-server-data/db-structure/parts/050-updates/025-empire-debt.sql @@ -25,11 +25,13 @@ BEGIN bld_dr := sys.get_constant( 'game.debt.buildings'); FOR empire, debt IN SELECT e.name_id AS id , e.debt - FROM sys.updates su - INNER JOIN emp.updates eu ON eu.update_id = su.id - INNER JOIN emp.empires e ON eu.empire_id = e.name_id - WHERE su.last_tick = c_tick AND su.status = 'PROCESSING' - AND su.gu_type = 'EMPIRE_DEBT' AND e.debt > 0 + FROM sys.updates _upd_sys + INNER JOIN emp.empires_updates eu + USING ( updtgt_id , updtype_id , update_id ) + INNER JOIN emp.empires e USING ( name_id ) + WHERE _upd_sys.update_last = c_tick + AND _upd_sys.update_state = 'PROCESSING' + AND e.debt > 0 FOR UPDATE LOOP PERFORM sys.write_log( 'EmpireDebt' , 'DEBUG'::log_level , 'Handling debt for empire #' @@ -59,4 +61,9 @@ BEGIN PERFORM verse.handle_debt( empire , upkeep , debt , bld_dr ); END LOOP; END; -$$ LANGUAGE plpgsql; \ No newline at end of file +$$ LANGUAGE plpgsql; + +SELECT sys.register_update_type( 'Empires' , 'EmpireDebt' , + 'The effects of empires'' debts are being computed.' , + 'process_empire_debt_updates' + ); \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/parts/050-updates/030-fleet-arrivals.sql b/legacyworlds-server-data/db-structure/parts/050-updates/030-fleet-arrivals.sql index c9dd59e..b5a939b 100644 --- a/legacyworlds-server-data/db-structure/parts/050-updates/030-fleet-arrivals.sql +++ b/legacyworlds-server-data/db-structure/parts/050-updates/030-fleet-arrivals.sql @@ -7,7 +7,7 @@ -- -------------------------------------------------------- -CREATE OR REPLACE FUNCTION sys.process_planet_fleet_arrivals_updates( c_tick BIGINT ) +CREATE OR REPLACE FUNCTION sys.process_fleet_arrivals_updates( c_tick BIGINT ) RETURNS VOID STRICT VOLATILE SECURITY INVOKER @@ -18,14 +18,16 @@ DECLARE f_ids BIGINT[]; BEGIN -- Lock all records - PERFORM f.id FROM sys.updates su - INNER JOIN verse.updates vu ON vu.update_id = su.id - INNER JOIN verse.planets p ON p.name_id = vu.planet_id + PERFORM 1 + FROM sys.updates su + INNER JOIN verse.planets_updates vu + USING ( update_id , updtype_id , updtgt_id ) + INNER JOIN verse.planets p + USING ( name_id ) INNER JOIN fleets.fleets f ON f.location_id = p.name_id INNER JOIN fleets.movements fm ON fm.fleet_id = f.id INNER JOIN emp.empires e ON e.name_id = f.owner_id - WHERE su.last_tick = c_tick AND su.status = 'PROCESSING' - AND su.gu_type = 'PLANET_FLEET_ARRIVALS' + WHERE su.update_last = c_tick AND su.update_state = 'PROCESSING' AND f.status = 'AVAILABLE' AND fm.time_left = 1 FOR UPDATE; @@ -34,8 +36,10 @@ BEGIN f.owner_id AS fleet_owner , ( v.status = 'PROCESSED' AND b.id IS NULL ) AS on_vacation , bool_or( f.attacking ) AS attacking FROM sys.updates su - INNER JOIN verse.updates vu ON vu.update_id = su.id - INNER JOIN verse.planets p ON p.name_id = vu.planet_id + INNER JOIN verse.planets_updates vu + USING ( update_id , updtype_id , updtgt_id ) + INNER JOIN verse.planets p + USING ( name_id ) INNER JOIN fleets.fleets f ON f.location_id = p.name_id INNER JOIN fleets.movements fm ON fm.fleet_id = f.id LEFT OUTER JOIN emp.planets ep ON ep.planet_id = p.name_id @@ -43,9 +47,8 @@ BEGIN LEFT OUTER JOIN users.vacations v ON v.account_id = en.owner_id LEFT OUTER JOIN battles.battles b ON b.location_id = p.name_id AND b.last_tick IS NULL - WHERE su.last_tick = c_tick AND su.status = 'PROCESSING' - AND su.gu_type = 'PLANET_FLEET_ARRIVALS' - AND f.status = 'AVAILABLE' AND fm.time_left = 1 + WHERE su.update_last = c_tick AND su.update_state = 'PROCESSING' + AND f.status = 'AVAILABLE' AND fm.time_left = 1 GROUP BY p.name_id , ep.empire_id , f.owner_id , v.status , b.id LOOP -- Fleets owned by the planet's owner are never attacking, same for fleets arriving on @@ -103,16 +106,17 @@ BEGIN SELECT f.location_id , ln.name , f.owner_id , fon.name , f.name , fs.power , f.attacking , fm.source_id , sn.name FROM sys.updates su - INNER JOIN verse.updates vu ON vu.update_id = su.id - INNER JOIN verse.planets p ON p.name_id = vu.planet_id + INNER JOIN verse.planets_updates vu + USING ( update_id , updtype_id , updtgt_id ) + INNER JOIN verse.planets p + USING ( name_id ) INNER JOIN fleets.fleets f ON f.location_id = p.name_id INNER JOIN fleets.movements fm ON fm.fleet_id = f.id INNER JOIN fleets.stats_view fs ON fs.id = f.id INNER JOIN naming.empire_names fon ON fon.id = f.owner_id INNER JOIN naming.map_names ln ON ln.id = f.location_id INNER JOIN naming.map_names sn ON sn.id = fm.source_id - WHERE su.last_tick = c_tick AND su.status = 'PROCESSING' - AND su.gu_type = 'PLANET_FLEET_ARRIVALS' + WHERE su.update_last = c_tick AND su.update_state = 'PROCESSING' AND f.status = 'AVAILABLE' AND fm.time_left = 1; -- Delete movement records, set redeployment penalties, update battles @@ -120,15 +124,16 @@ BEGIN f.attacking AS attacking , b.id AS battle , f.location_id AS location FROM sys.updates su - INNER JOIN verse.updates vu ON vu.update_id = su.id - INNER JOIN verse.planets p ON p.name_id = vu.planet_id + INNER JOIN verse.planets_updates vu + USING ( update_id , updtype_id , updtgt_id ) + INNER JOIN verse.planets p + USING ( name_id ) INNER JOIN fleets.fleets f ON f.location_id = p.name_id INNER JOIN fleets.movements fm ON fm.fleet_id = f.id INNER JOIN fleets.stats_view fs ON fs.id = f.id LEFT OUTER JOIN battles.battles b ON b.location_id = p.name_id AND b.last_tick IS NULL - WHERE su.last_tick = c_tick AND su.status = 'PROCESSING' - AND su.gu_type = 'PLANET_FLEET_ARRIVALS' + WHERE su.update_last = c_tick AND su.update_state = 'PROCESSING' AND f.status = 'AVAILABLE' AND fm.time_left = 1 LOOP DELETE FROM fleets.movements @@ -145,4 +150,10 @@ BEGIN -- Send fleet arrival events PERFORM events.commit_fleet_arrivals( c_tick ); END; -$$ LANGUAGE plpgsql; \ No newline at end of file +$$ LANGUAGE plpgsql; + + +SELECT sys.register_update_type( 'Planets' , 'FleetArrivals' , + 'Fleets which were one update away are arriving at their destinations.' , + 'process_fleet_arrivals_updates' + ); \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/parts/050-updates/040-fleet-movements.sql b/legacyworlds-server-data/db-structure/parts/050-updates/040-fleet-movements.sql index 184aa0a..2151325 100644 --- a/legacyworlds-server-data/db-structure/parts/050-updates/040-fleet-movements.sql +++ b/legacyworlds-server-data/db-structure/parts/050-updates/040-fleet-movements.sql @@ -7,7 +7,7 @@ -- -------------------------------------------------------- -CREATE OR REPLACE FUNCTION sys.process_planet_fleet_movements_updates( c_tick BIGINT ) +CREATE OR REPLACE FUNCTION sys.process_fleet_movements_updates( c_tick BIGINT ) RETURNS VOID STRICT VOLATILE SECURITY INVOKER @@ -18,13 +18,14 @@ DECLARE BEGIN -- Lock all records PERFORM f.id FROM sys.updates su - INNER JOIN verse.updates vu ON vu.update_id = su.id - INNER JOIN verse.planets p ON p.name_id = vu.planet_id + INNER JOIN verse.planets_updates vu + USING ( update_id , updtype_id , updtgt_id ) + INNER JOIN verse.planets p + USING ( name_id ) INNER JOIN fleets.fleets f ON f.location_id = p.name_id INNER JOIN fleets.movements fm ON fm.fleet_id = f.id INNER JOIN emp.empires e ON e.name_id = f.owner_id - WHERE su.last_tick = c_tick AND su.status = 'PROCESSING' - AND su.gu_type = 'PLANET_FLEET_MOVEMENTS' + WHERE su.update_last = c_tick AND su.update_state = 'PROCESSING' AND f.status = 'AVAILABLE' FOR UPDATE; @@ -36,8 +37,10 @@ BEGIN rp.system_id AS is_ref_point_system , rp.orbit AS is_ref_point_orbit , rps.x AS is_ref_point_x , rps.y AS is_ref_point_y FROM sys.updates su - INNER JOIN verse.updates vu ON vu.update_id = su.id - INNER JOIN verse.planets p ON p.name_id = vu.planet_id + INNER JOIN verse.planets_updates vu + USING ( update_id , updtype_id , updtgt_id ) + INNER JOIN verse.planets p + USING ( name_id ) INNER JOIN verse.systems s ON s.id = p.system_id INNER JOIN fleets.fleets f ON f.location_id = p.name_id INNER JOIN fleets.ships fs ON fs.fleet_id = f.id @@ -46,8 +49,7 @@ BEGIN LEFT OUTER JOIN fleets.ms_system isms ON isms.movement_id = f.id LEFT OUTER JOIN verse.planets rp ON isms.ref_point_id = rp.name_id LEFT OUTER JOIN verse.systems rps ON rps.id = rp.system_id - WHERE su.last_tick = c_tick AND su.status = 'PROCESSING' - AND su.gu_type = 'PLANET_FLEET_MOVEMENTS' + WHERE su.update_last = c_tick AND su.update_state = 'PROCESSING' AND f.status = 'AVAILABLE' AND m.state_time_left = 1 GROUP BY f.id , s.x , s.y , s.id , isms.ref_point_id , isms.outwards , isms.past_ref_point , rp.system_id , rp.orbit , rps.x , rps.y @@ -98,13 +100,19 @@ BEGIN time_left = time_left - 1 WHERE fleet_id IN ( SELECT f.id FROM sys.updates su - INNER JOIN verse.updates vu ON vu.update_id = su.id - INNER JOIN verse.planets p ON p.name_id = vu.planet_id + INNER JOIN verse.planets_updates vu + USING ( update_id , updtype_id , updtgt_id ) + INNER JOIN verse.planets p + USING ( name_id ) INNER JOIN fleets.fleets f ON f.location_id = p.name_id INNER JOIN fleets.movements fm ON fm.fleet_id = f.id INNER JOIN emp.empires e ON e.name_id = f.owner_id - WHERE su.last_tick = c_tick AND su.status = 'PROCESSING' - AND su.gu_type = 'PLANET_FLEET_MOVEMENTS' + WHERE su.update_last = c_tick AND su.update_state = 'PROCESSING' AND f.status = 'AVAILABLE' ); END; -$$ LANGUAGE plpgsql; \ No newline at end of file +$$ LANGUAGE plpgsql; + +SELECT sys.register_update_type( 'Planets' , 'FleetMovements' , + 'Fleets are moving.' , + 'process_fleet_movements_updates' + ); \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/parts/050-updates/050-fleet-status.sql b/legacyworlds-server-data/db-structure/parts/050-updates/050-fleet-status.sql index 7249954..c7b1fca 100644 --- a/legacyworlds-server-data/db-structure/parts/050-updates/050-fleet-status.sql +++ b/legacyworlds-server-data/db-structure/parts/050-updates/050-fleet-status.sql @@ -7,7 +7,7 @@ -- -------------------------------------------------------- -CREATE OR REPLACE FUNCTION sys.process_planet_fleet_status_updates( c_tick BIGINT ) +CREATE OR REPLACE FUNCTION sys.process_fleet_status_updates( c_tick BIGINT ) RETURNS VOID STRICT VOLATILE SECURITY INVOKER @@ -18,26 +18,27 @@ DECLARE BEGIN -- Lock all records PERFORM f.id FROM sys.updates su - INNER JOIN verse.updates vu ON vu.update_id = su.id - INNER JOIN verse.planets p ON p.name_id = vu.planet_id + INNER JOIN verse.planets_updates vu + USING ( update_id , updtype_id , updtgt_id ) + INNER JOIN verse.planets p + USING ( name_id ) INNER JOIN fleets.fleets f ON f.location_id = p.name_id INNER JOIN emp.empires e ON e.name_id = f.owner_id - WHERE su.last_tick = c_tick AND su.status = 'PROCESSING' - AND su.gu_type = 'PLANET_FLEET_STATUS' + WHERE su.update_last = c_tick AND su.update_state = 'PROCESSING' FOR UPDATE; -- Fleet deployments FOR rec IN SELECT f.id AS fleet , f.owner_id AS owner , f.location_id AS location , b.id AS battle FROM sys.updates su - INNER JOIN verse.updates vu ON vu.update_id = su.id - INNER JOIN fleets.fleets f ON f.location_id = vu.planet_id + INNER JOIN verse.planets_updates vu + USING ( update_id , updtype_id , updtgt_id ) + INNER JOIN fleets.fleets f ON f.location_id = vu.name_id AND f.status = 'DEPLOYING' AND f.penalty = 1 INNER JOIN emp.empires e ON e.name_id = f.owner_id LEFT OUTER JOIN battles.battles b ON b.location_id = f.location_id AND b.last_tick IS NULL - WHERE su.last_tick = c_tick AND su.status = 'PROCESSING' - AND su.gu_type = 'PLANET_FLEET_STATUS' + WHERE su.update_last = c_tick AND su.update_state = 'PROCESSING' LOOP -- Add fleet to battle (will be ignored if battle is NULL) PERFORM battles.add_fleet( rec.battle , rec.fleet , TRUE , c_tick ); @@ -62,16 +63,21 @@ BEGIN -- Fleets that must become available UPDATE fleets.fleets f SET status = 'AVAILABLE' , penalty = 0 - FROM sys.updates su , verse.updates vu - WHERE vu.update_id = su.id AND f.location_id = vu.planet_id - AND f.penalty = 1 AND su.status = 'PROCESSING' - AND su.gu_type = 'PLANET_FLEET_STATUS' AND su.last_tick = c_tick; + FROM sys.updates su , verse.planets_updates vu + WHERE vu.update_id = su.update_id AND f.location_id = vu.name_id + AND f.penalty = 1 + AND su.update_state = 'PROCESSING' AND su.update_last = c_tick; -- Fleets that still have a penalty UPDATE fleets.fleets f SET penalty = penalty - 1 - FROM sys.updates su , verse.updates vu - WHERE vu.update_id = su.id AND f.location_id = vu.planet_id - AND f.penalty > 1 AND su.status = 'PROCESSING' - AND su.gu_type = 'PLANET_FLEET_STATUS' AND su.last_tick = c_tick; + FROM sys.updates su , verse.planets_updates vu + WHERE vu.update_id = su.update_id AND f.location_id = vu.name_id + AND f.penalty > 1 + AND su.update_state = 'PROCESSING' AND su.update_last = c_tick; END; -$$ LANGUAGE plpgsql; \ No newline at end of file +$$ LANGUAGE plpgsql; + +SELECT sys.register_update_type( 'Planets' , 'FleetStatus' , + 'Fleet states (e.g. "deploying", "unavailable", etc...) are being updated.' , + 'process_fleet_status_updates' + ); \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/parts/050-updates/060-planet-battle.sql b/legacyworlds-server-data/db-structure/parts/050-updates/060-planet-battle.sql index d944f65..2cfb2e9 100644 --- a/legacyworlds-server-data/db-structure/parts/050-updates/060-planet-battle.sql +++ b/legacyworlds-server-data/db-structure/parts/050-updates/060-planet-battle.sql @@ -7,7 +7,7 @@ -- -------------------------------------------------------- -CREATE OR REPLACE FUNCTION sys.process_planet_battle_start_updates( c_tick BIGINT ) +CREATE OR REPLACE FUNCTION sys.process_battle_start_updates( c_tick BIGINT ) RETURNS VOID STRICT VOLATILE SECURITY INVOKER @@ -17,12 +17,14 @@ DECLARE BEGIN FOR p_id IN SELECT p.name_id FROM sys.updates su - INNER JOIN verse.updates vu ON vu.update_id = su.id - INNER JOIN verse.planets p ON vu.planet_id = p.name_id + INNER JOIN verse.planets_updates vu + USING ( update_id , updtype_id , updtgt_id ) + INNER JOIN verse.planets p + USING ( name_id ) LEFT OUTER JOIN battles.battles b ON b.location_id = p.name_id AND b.last_tick IS NULL - WHERE su.last_tick = c_tick AND su.status = 'PROCESSING' - AND su.gu_type = 'PLANET_BATTLE_START' AND b.location_id IS NULL + WHERE su.update_last = c_tick AND su.update_state = 'PROCESSING' + AND b.location_id IS NULL FOR UPDATE OF p LOOP IF battles.check_start( p_id ) THEN @@ -32,10 +34,13 @@ BEGIN END; $$ LANGUAGE plpgsql; +SELECT sys.register_update_type( 'Planets' , 'BattleStart' , + 'Battles are being started.' , + 'process_battle_start_updates' + ); - -CREATE OR REPLACE FUNCTION sys.process_planet_battle_main_updates( c_tick BIGINT ) +CREATE OR REPLACE FUNCTION sys.process_battle_main_updates( c_tick BIGINT ) RETURNS VOID STRICT VOLATILE SECURITY INVOKER @@ -63,13 +68,14 @@ BEGIN FOR rec IN SELECT b.id AS battle , b.first_tick AS first_tick , b.location_id AS location , ( ph.current / p.population )::REAL AS happiness FROM sys.updates su - INNER JOIN verse.updates vu ON vu.update_id = su.id - INNER JOIN verse.planets p ON vu.planet_id = p.name_id + INNER JOIN verse.planets_updates vu + USING ( update_id , updtype_id , updtgt_id ) + INNER JOIN verse.planets p + USING ( name_id ) INNER JOIN verse.planet_happiness ph ON ph.planet_id = p.name_id INNER JOIN battles.battles b ON b.location_id = p.name_id AND b.last_tick IS NULL - WHERE su.last_tick = c_tick AND su.status = 'PROCESSING' - AND su.gu_type = 'PLANET_BATTLE_MAIN' + WHERE su.update_last = c_tick AND su.update_state = 'PROCESSING' FOR UPDATE OF p , b LOOP PERFORM sys.write_log( 'BattleUpdate' , 'DEBUG'::log_level , 'Handling battle #' || rec.battle @@ -115,10 +121,14 @@ BEGIN END; $$ LANGUAGE plpgsql; +SELECT sys.register_update_type( 'Planets' , 'BattleCompute' , + 'In-progress battles are being updated.' , + 'process_battle_main_updates' + ); -CREATE OR REPLACE FUNCTION sys.process_planet_battle_end_updates( c_tick BIGINT ) +CREATE OR REPLACE FUNCTION sys.process_battle_end_updates( c_tick BIGINT ) RETURNS VOID STRICT VOLATILE SECURITY INVOKER @@ -129,12 +139,13 @@ DECLARE BEGIN FOR rec IN SELECT b.id AS battle , b.location_id AS location FROM sys.updates su - INNER JOIN verse.updates vu ON vu.update_id = su.id - INNER JOIN verse.planets p ON vu.planet_id = p.name_id + INNER JOIN verse.planets_updates vu + USING ( update_id , updtype_id , updtgt_id ) + INNER JOIN verse.planets p + USING ( name_id ) INNER JOIN battles.battles b ON b.location_id = p.name_id AND b.last_tick IS NULL - WHERE su.last_tick = c_tick AND su.status = 'PROCESSING' - AND su.gu_type = 'PLANET_BATTLE_END' + WHERE su.update_last = c_tick AND su.update_state = 'PROCESSING' FOR UPDATE OF p , b LOOP IF battles.get_fleets_power( rec.battle , c_tick , TRUE ) = 0 THEN @@ -179,3 +190,7 @@ BEGIN END; $$ LANGUAGE plpgsql; +SELECT sys.register_update_type( 'Planets' , 'BattleEnd' , + 'Finished battles are being ended.' , + 'process_battle_end_updates' + ); \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/parts/050-updates/070-planet-abandon.sql b/legacyworlds-server-data/db-structure/parts/050-updates/070-planet-abandon.sql index 85128b7..1cdb636 100644 --- a/legacyworlds-server-data/db-structure/parts/050-updates/070-planet-abandon.sql +++ b/legacyworlds-server-data/db-structure/parts/050-updates/070-planet-abandon.sql @@ -16,24 +16,31 @@ DECLARE p_id INT; BEGIN -- Lock all records - PERFORM p.name_id FROM sys.updates su - INNER JOIN verse.updates vu ON vu.update_id = su.id - INNER JOIN verse.planets p ON p.name_id = vu.planet_id - INNER JOIN emp.planets ep ON p.name_id = vu.planet_id - INNER JOIN emp.empires e ON e.name_id = ep.empire_id - INNER JOIN emp.abandon a ON a.planet_id = p.name_id - WHERE su.last_tick = c_tick AND su.status = 'PROCESSING' - AND su.gu_type = 'PLANET_ABANDON' + PERFORM p.name_id + FROM sys.updates su + INNER JOIN verse.planets_updates vu + USING ( update_id , updtype_id , updtgt_id ) + INNER JOIN verse.planets p + USING ( name_id ) + INNER JOIN emp.planets ep + ON ep.planet_id = p.name_id + INNER JOIN emp.empires e + ON e.name_id = ep.empire_id + INNER JOIN emp.abandon a + ON a.planet_id = p.name_id + WHERE su.update_last = c_tick AND su.update_state = 'PROCESSING' FOR UPDATE; -- Handle planets where time has run out FOR p_id IN SELECT p.name_id FROM sys.updates su - INNER JOIN verse.updates vu ON vu.update_id = su.id - INNER JOIN verse.planets p ON p.name_id = vu.planet_id - INNER JOIN emp.abandon a ON a.planet_id = p.name_id - WHERE su.last_tick = c_tick AND su.status = 'PROCESSING' - AND su.gu_type = 'PLANET_ABANDON' AND a.time_left = 1 + INNER JOIN verse.planets_updates vu + USING ( update_id , updtype_id , updtgt_id ) + INNER JOIN verse.planets p + USING ( name_id ) + INNER JOIN emp.abandon a ON a.planet_id = p.name_id + WHERE su.update_last = c_tick AND su.update_state = 'PROCESSING' + AND a.time_left = 1 LOOP PERFORM emp.leave_planet( p_id ); END LOOP; @@ -41,8 +48,14 @@ BEGIN -- Update all abandon records UPDATE emp.abandon a SET time_left = a.time_left - 1 FROM sys.updates su - INNER JOIN verse.updates vu ON vu.update_id = su.id - WHERE su.last_tick = c_tick AND su.status = 'PROCESSING' - AND su.gu_type = 'PLANET_ABANDON' AND a.planet_id = vu.planet_id; + INNER JOIN verse.planets_updates vu + USING ( update_id , updtype_id , updtgt_id ) + WHERE su.update_last = c_tick AND su.update_state = 'PROCESSING' + AND a.planet_id = vu.name_id; END; -$$ LANGUAGE plpgsql; \ No newline at end of file +$$ LANGUAGE plpgsql; + +SELECT sys.register_update_type( 'Planets' , 'PlanetAbandon' , + 'Planets are being abandoned.' , + 'process_planet_abandon_updates' + ); \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/parts/050-updates/080-planet-construction.sql b/legacyworlds-server-data/db-structure/parts/050-updates/080-planet-construction.sql index 37470af..e9281bb 100644 --- a/legacyworlds-server-data/db-structure/parts/050-updates/080-planet-construction.sql +++ b/legacyworlds-server-data/db-structure/parts/050-updates/080-planet-construction.sql @@ -46,8 +46,10 @@ BEGIN qi.destroy AS destroy , qi.building_id AS building , b.work AS req_work , b.cost AS req_cost FROM sys.updates su - INNER JOIN verse.updates vu ON vu.update_id = su.id - INNER JOIN verse.planets p ON vu.planet_id = p.name_id + INNER JOIN verse.planets_updates vu + USING ( update_id , updtype_id , updtgt_id ) + INNER JOIN verse.planets p + USING ( name_id ) INNER JOIN emp.planets ep ON ep.planet_id = p.name_id INNER JOIN emp.empires e ON e.name_id = ep.empire_id INNER JOIN verse.planet_happiness ph ON ph.planet_id = p.name_id @@ -56,8 +58,7 @@ BEGIN INNER JOIN tech.buildables b ON b.name_id = qi.building_id INNER JOIN naming.empire_names en ON en.id = e.name_id LEFT OUTER JOIN users.vacations v ON v.account_id = en.owner_id - WHERE su.last_tick = c_tick AND su.status = 'PROCESSING' - AND su.gu_type = 'PLANET_CONSTRUCTION' + WHERE su.update_last = c_tick AND su.update_state = 'PROCESSING' AND ( v.account_id IS NULL OR v.status <> 'PROCESSED' ) ORDER BY e.name_id , p.name_id , qi.queue_order FOR UPDATE OF p , e , q , qi @@ -215,3 +216,8 @@ BEGIN END IF; END; $$ LANGUAGE plpgsql; + +SELECT sys.register_update_type( 'Planets' , 'CivilianConstruction' , + 'Civilian build queues are being processed.' , + 'process_planet_construction_updates' + ); \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/parts/050-updates/090-planet-military.sql b/legacyworlds-server-data/db-structure/parts/050-updates/090-planet-military.sql index f3c250a..1645fac 100644 --- a/legacyworlds-server-data/db-structure/parts/050-updates/090-planet-military.sql +++ b/legacyworlds-server-data/db-structure/parts/050-updates/090-planet-military.sql @@ -44,8 +44,10 @@ BEGIN qi.queue_order AS qorder , qi.amount AS amount , qi.ship_id AS ship , s.work AS req_work , s.cost AS req_cost FROM sys.updates su - INNER JOIN verse.updates vu ON vu.update_id = su.id - INNER JOIN verse.planets p ON vu.planet_id = p.name_id + INNER JOIN verse.planets_updates vu + USING ( update_id , updtype_id , updtgt_id ) + INNER JOIN verse.planets p + USING ( name_id ) INNER JOIN emp.planets ep ON ep.planet_id = p.name_id INNER JOIN emp.empires e ON e.name_id = ep.empire_id INNER JOIN verse.planet_happiness ph ON ph.planet_id = p.name_id @@ -54,8 +56,7 @@ BEGIN INNER JOIN tech.buildables s ON s.name_id = qi.ship_id INNER JOIN naming.empire_names en ON en.id = e.name_id LEFT OUTER JOIN users.vacations v ON v.account_id = en.owner_id - WHERE su.last_tick = c_tick AND su.status = 'PROCESSING' - AND su.gu_type = 'PLANET_MILITARY' + WHERE su.update_last = c_tick AND su.update_state = 'PROCESSING' AND ( v.account_id IS NULL OR v.status <> 'PROCESSED' ) ORDER BY e.name_id , p.name_id , qi.queue_order FOR UPDATE OF p , e , q , qi @@ -225,3 +226,8 @@ BEGIN DROP TABLE blt_ships; END; $$ LANGUAGE plpgsql; + +SELECT sys.register_update_type( 'Planets' , 'MilitaryConstruction' , + 'Military build queues are being processed.' , + 'process_planet_military_updates' + ); \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/parts/050-updates/100-planet-population.sql b/legacyworlds-server-data/db-structure/parts/050-updates/100-planet-population.sql index 2a805f8..16a3d77 100644 --- a/legacyworlds-server-data/db-structure/parts/050-updates/100-planet-population.sql +++ b/legacyworlds-server-data/db-structure/parts/050-updates/100-planet-population.sql @@ -37,11 +37,12 @@ BEGIN ph.target AS target , ph.current AS happy_pop , ( ph.current / p.population )::REAL AS current FROM sys.updates su - INNER JOIN verse.updates vu ON vu.update_id = su.id - INNER JOIN verse.planets p ON vu.planet_id = p.name_id + INNER JOIN verse.planets_updates vu + USING ( update_id , updtype_id , updtgt_id ) + INNER JOIN verse.planets p + USING ( name_id ) INNER JOIN verse.planet_happiness ph ON ph.planet_id = p.name_id - WHERE su.last_tick = c_tick AND su.status = 'PROCESSING' - AND su.gu_type = 'PLANET_POPULATION' + WHERE su.update_last = c_tick AND su.update_state = 'PROCESSING' FOR UPDATE OF p, ph LOOP IF round( rec.target / rel_ch ) = round( rec.current / rel_ch ) THEN @@ -101,4 +102,9 @@ BEGIN END IF; END LOOP; END; -$$ LANGUAGE plpgsql; \ No newline at end of file +$$ LANGUAGE plpgsql; + +SELECT sys.register_update_type( 'Planets' , 'PlanetPopulation' , + 'Planet populations are being updated.' , + 'process_planet_population_updates' + ); \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/parts/050-updates/105-planet-resource-regeneration.sql b/legacyworlds-server-data/db-structure/parts/050-updates/105-planet-resource-regeneration.sql index 9e59a4b..a0a6db3 100644 --- a/legacyworlds-server-data/db-structure/parts/050-updates/105-planet-resource-regeneration.sql +++ b/legacyworlds-server-data/db-structure/parts/050-updates/105-planet-resource-regeneration.sql @@ -16,7 +16,7 @@ * Parameters: * _tick The identifier of the game update */ -CREATE OR REPLACE FUNCTION sys.process_planet_res_regen_updates( _tick BIGINT ) +CREATE OR REPLACE FUNCTION sys.process_res_regen_updates( _tick BIGINT ) RETURNS VOID STRICT VOLATILE SECURITY INVOKER @@ -31,17 +31,21 @@ AS $process_planet_res_regen_updates$ ) FROM sys.updates _upd_sys - INNER JOIN verse.updates _upd_verse - ON _upd_verse.update_id = _upd_sys.id + INNER JOIN verse.planets_updates _upd_verse + USING ( update_id , updtype_id , updtgt_id ) - WHERE _upd_sys.last_tick = $1 - AND _upd_sys.status = 'PROCESSING' - AND _upd_sys.gu_type = 'PLANET_RES_REGEN' - AND _provider.planet_id = _upd_verse.planet_id; + WHERE _upd_sys.update_last = $1 + AND _upd_sys.update_state = 'PROCESSING' + AND _provider.planet_id = _upd_verse.name_id; $process_planet_res_regen_updates$ LANGUAGE SQL; REVOKE EXECUTE - ON FUNCTION sys.process_planet_res_regen_updates( BIGINT ) + ON FUNCTION sys.process_res_regen_updates( BIGINT ) FROM PUBLIC; + +SELECT sys.register_update_type( 'Planets' , 'ResourceRegeneration' , + 'Resource providers are being regenerated.' , + 'process_res_regen_updates' + ); \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/parts/050-updates/110-planet-money.sql b/legacyworlds-server-data/db-structure/parts/050-updates/110-planet-money.sql index cbcc432..1822d7f 100644 --- a/legacyworlds-server-data/db-structure/parts/050-updates/110-planet-money.sql +++ b/legacyworlds-server-data/db-structure/parts/050-updates/110-planet-money.sql @@ -20,13 +20,14 @@ BEGIN ( ph.current / p.population )::REAL AS happiness , ( ea.planet_id IS NULL ) AS produces_income FROM sys.updates su - INNER JOIN verse.updates vu ON vu.update_id = su.id - INNER JOIN verse.planets p ON vu.planet_id = p.name_id + INNER JOIN verse.planets_updates vu + USING ( update_id , updtype_id , updtgt_id ) + INNER JOIN verse.planets p + USING ( name_id ) INNER JOIN verse.planet_happiness ph ON ph.planet_id = p.name_id INNER JOIN verse.planet_money pm ON pm.planet_id = p.name_id LEFT OUTER JOIN emp.abandon ea ON ea.planet_id = p.name_id - WHERE su.last_tick = c_tick AND su.status = 'PROCESSING' - AND su.gu_type = 'PLANET_MONEY' + WHERE su.update_last = c_tick AND su.update_state = 'PROCESSING' FOR UPDATE OF p, pm LOOP IF rec.produces_income THEN @@ -42,4 +43,9 @@ BEGIN WHERE planet_id = rec.id; END LOOP; END; -$$ LANGUAGE plpgsql; \ No newline at end of file +$$ LANGUAGE plpgsql; + +SELECT sys.register_update_type( 'Planets' , 'PlanetMoney' , + 'Planets'' income and upkeep are being updated.' , + 'process_planet_money_updates' + ); \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/parts/050-updates/120-planet-mining.sql b/legacyworlds-server-data/db-structure/parts/050-updates/120-planet-mining.sql index b641762..6f9c53f 100644 --- a/legacyworlds-server-data/db-structure/parts/050-updates/120-planet-mining.sql +++ b/legacyworlds-server-data/db-structure/parts/050-updates/120-planet-mining.sql @@ -127,12 +127,12 @@ AS $gu_pmc_get_data$ pmc_total AS total_weight FROM sys.updates _upd_sys - INNER JOIN verse.updates _upd_verse - ON _upd_sys.id = _upd_verse.update_id + INNER JOIN verse.planets_updates _upd_verse + USING ( update_id , updtype_id , updtgt_id ) INNER JOIN verse.planets _planet - ON _planet.name_id = _upd_verse.planet_id + USING ( name_id ) INNER JOIN verse.resource_providers _resprov - USING ( planet_id ) + ON planet_id = name_id INNER JOIN verse.planet_resources _pres USING ( planet_id , resource_name_id ) LEFT OUTER JOIN ( @@ -141,10 +141,10 @@ AS $gu_pmc_get_data$ pmc_total , _happ.current AS happiness FROM sys.updates _upd_sys - INNER JOIN verse.updates _upd_verse - ON _upd_sys.id = _upd_verse.update_id + INNER JOIN verse.planets_updates _upd_verse + USING ( update_id , updtype_id , updtgt_id ) INNER JOIN emp.planets _emp_planet - USING ( planet_id ) + ON _emp_planet.planet_id = _upd_verse.name_id INNER JOIN emp.empires _emp ON _emp_planet.empire_id = _emp.name_id INNER JOIN emp.mining_settings _emset @@ -161,17 +161,15 @@ AS $gu_pmc_get_data$ ) AS _pmset USING ( empire_id , planet_id , resource_name_id ) - WHERE _upd_sys.last_tick = $1 - AND _upd_sys.status = 'PROCESSING' - AND _upd_sys.gu_type = 'PLANET_MINING' + WHERE _upd_sys.update_last = $1 + AND _upd_sys.update_state = 'PROCESSING' FOR SHARE OF _emp_planet , _emp , _emset , _happ ) AS _owner USING ( planet_id , resource_name_id ) - WHERE _upd_sys.last_tick = $1 - AND _upd_sys.status = 'PROCESSING' - AND _upd_sys.gu_type = 'PLANET_MINING' + WHERE _upd_sys.update_last = $1 + AND _upd_sys.update_state = 'PROCESSING' FOR UPDATE OF _resprov , _pres FOR SHARE OF _planet ; @@ -286,3 +284,8 @@ $process_planet_mining_updates$ LANGUAGE PLPGSQL; REVOKE EXECUTE ON FUNCTION sys.process_planet_mining_updates( BIGINT ) FROM PUBLIC; + +SELECT sys.register_update_type( 'Planets' , 'PlanetMining' , + 'Resources are being extracted from mines.' , + 'process_planet_mining_updates' + ); \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/admin/040-functions/003-updates/010-target-definition.sql b/legacyworlds-server-data/db-structure/tests/admin/040-functions/003-updates/010-target-definition.sql new file mode 100644 index 0000000..5438a50 --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/admin/040-functions/003-updates/010-target-definition.sql @@ -0,0 +1,70 @@ +/* + * Tests of the update target definitions + */ +BEGIN; + -- Make sure we get rid of all existing definitions first + DELETE FROM sys.update_types; + DELETE FROM sys.update_targets; + + -- Create a test schema & table which will be used as the target + CREATE SCHEMA test + CREATE TABLE test( + test_id SERIAL NOT NULL PRIMARY KEY + ) + CREATE TABLE test2( + test_id SERIAL NOT NULL PRIMARY KEY + ); + + -- ***** TESTS BEGIN HERE ***** + SELECT plan( 8 ); + + SELECT diag_test_name( 'Update target definitions - Failure if schema/table do not exist' ); + SELECT throws_ok( + $$ INSERT INTO sys.update_targets( + updtgt_name , updtgt_schema , updtgt_table + ) VALUES ( + 'Test' , 'test' , 'does_not_exist' + ); $$ , + 23503 + ); + + SELECT diag_test_name( 'Update target definitions - Successful insertion' ); + SELECT lives_ok( $$ + INSERT INTO sys.update_targets( + updtgt_name , updtgt_schema , updtgt_table + ) VALUES ( + 'Test' , 'test' , 'test' + ); + $$ ); + SELECT diag_test_name( 'Update target definitions - Specific update table - Exists after insertion' ); + SELECT has_table( 'test' , 'test_updates' , 'No specific update table' ); + SELECT diag_test_name( 'Update target definitions - Specific update table - Primary key' ); + SELECT index_is_primary( 'test' , 'test_updates' , 'test_updates_pkey' ); + SELECT diag_test_name( 'Update target definitions - Specific update table - Primary key definition' ); + SELECT has_index( 'test' , 'test_updates' , 'test_updates_pkey' , + ARRAY[ 'updtgt_id' , 'updtype_id' , 'update_id' , 'test_id' ] ); + + SELECT diag_test_name( 'Update target definitions - Duplicate name' ); + SELECT throws_ok( $$ + INSERT INTO sys.update_targets( + updtgt_name , updtgt_schema , updtgt_table + ) VALUES ( + 'Test' , 'test' , 'test2' + ); + $$ , 23505 ); + + SELECT diag_test_name( 'Update target definitions - Duplicate table reference' ); + SELECT throws_ok( $$ + INSERT INTO sys.update_targets( + updtgt_name , updtgt_schema , updtgt_table + ) VALUES ( + 'Test 2' , 'test' , 'test' + ); + $$ , 23505 ); + + SELECT diag_test_name( 'Update target definitions - Deletion drops the specific update table' ); + DELETE FROM sys.update_targets; + SELECT hasnt_table( 'test' , 'test_updates' , 'Specific update table still exists' ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/admin/040-functions/003-updates/020-type-definitions.sql b/legacyworlds-server-data/db-structure/tests/admin/040-functions/003-updates/020-type-definitions.sql new file mode 100644 index 0000000..895aa72 --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/admin/040-functions/003-updates/020-type-definitions.sql @@ -0,0 +1,150 @@ +/* + * Tests of the update type definitions + */ +BEGIN; + -- Make sure we get rid of all existing definitions first + DELETE FROM sys.update_types; + DELETE FROM sys.update_targets; + + -- Create a test schema & tables which will be used as targets + CREATE SCHEMA test + CREATE TABLE test( + test_id SERIAL NOT NULL PRIMARY KEY + ); + + -- Create target definitions + INSERT INTO sys.update_targets( + updtgt_id , updtgt_name , updtgt_schema , updtgt_table + ) VALUES ( + 1 , 'Test 1' , 'test' , 'test' + ); + + -- Create a dummy stored procedure in the sys schema + CREATE FUNCTION sys.dummy_update_func( c_tick BIGINT ) + RETURNS INT LANGUAGE SQL AS 'SELECT 1;'; + + -- ***** TESTS BEGIN HERE ***** + SELECT plan( 14 ); + + SELECT diag_test_name( 'Update types - Definition with invalid target' ); + SELECT throws_ok( $$ + INSERT INTO sys.update_types( + updtype_id , updtgt_id , updtype_name , updtype_description , updtype_ordering + ) VALUES ( + 1 , 5 , 'Test' , '' , 1 + ); + $$ , 23503 ); + + SELECT diag_test_name( 'Update types - Definition with invalid stored procedure name' ); + SELECT throws_ok( $$ + INSERT INTO sys.update_types( + updtype_id , updtgt_id , updtype_name , updtype_description , updtype_ordering , updtype_proc_name + ) VALUES ( + 1 , 5 , 'Test' , '' , 1 , 'does_not_exist' + ); + $$ , 'P0001' ); + + SELECT diag_test_name( 'Update types - Successful insertion with no procedure name' ); + SELECT lives_ok( $$ + INSERT INTO sys.update_types( + updtype_id , updtgt_id , updtype_name , updtype_description , updtype_ordering + ) VALUES ( + 1 , 1 , 'Test' , '' , 1 + ); + $$ ); + DELETE FROM sys.update_types; + + SELECT diag_test_name( 'Update types - Successful insertion with a procedure name' ); + SELECT lives_ok( $$ + INSERT INTO sys.update_types( + updtype_id , updtgt_id , updtype_name , updtype_description , updtype_ordering , updtype_proc_name + ) VALUES ( + 1 , 1 , 'Test' , '' , 1 , 'dummy_update_func' + ); + $$ ); + + SELECT diag_test_name( 'Update types - Setting an existing row''s stored procedure to something invalid fails' ); + SELECT throws_ok( $$ + UPDATE sys.update_types + SET updtype_proc_name = 'does_not_exist' + WHERE updtype_id = 1 + $$ , 'P0001' ); + + SELECT diag_test_name( 'Update types - Failure on duplicate name' ); + SELECT throws_ok( $$ + INSERT INTO sys.update_types( + updtype_id , updtgt_id , updtype_name , updtype_description , updtype_ordering + ) VALUES ( + 2 , 1 , 'Test' , '' , 1 + ); + $$ , 23505 ); + + SELECT diag_test_name( 'Update types - Failure on duplicate stored procedure' ); + SELECT throws_ok( $$ + INSERT INTO sys.update_types( + updtype_id , updtgt_id , updtype_name , updtype_description , updtype_ordering , updtype_proc_name + ) VALUES ( + 3 , 1 , 'Test 2' , '' , 1 , 'dummy_update_func' + ); + $$ , 23505 ); + + SELECT diag_test_name( 'Update types - Target deletion fails if type definitions exist' ); + SELECT throws_ok( $$ DELETE FROM sys.update_targets WHERE updtgt_id = 1 $$ , 23503 ); + + DELETE FROM sys.update_types; + + INSERT INTO sys.update_types( + updtype_id , updtgt_id , updtype_name , updtype_description , updtype_ordering + ) VALUES ( + 1 , 1 , 'Test 1' , '' , 1 + ) , ( + 2 , 1 , 'Test 3' , '' , 75 + ) , ( + 3 , 1 , 'Test 2' , '' , 2 + ); + SELECT diag_test_name( 'Update types - Ordering column is updated' ); + SELECT set_eq( + $$ SELECT updtype_id , updtype_ordering FROM sys.update_types $$ , + $$ VALUES ( 1 , 2 ) , ( 3 , 4 ) , ( 2 , 6 ) $$ + ); + DELETE FROM sys.update_types; + + + INSERT INTO test.test VALUES ( 1 ) , ( 2 ); + INSERT INTO sys.update_types( + updtype_id , updtgt_id , updtype_name , updtype_description , updtype_ordering + ) VALUES ( + 1 , 1 , 'Test 1' , '' , 1 + ); + SELECT diag_test_name( 'Update types - Type creation adds specific rows for missing items' ); + SELECT set_eq( + $$ SELECT updtgt_id , updtype_id , test_id FROM test.test_updates $$ , + $$ VALUES ( 1 , 1 , 1 ) , ( 1 , 1 , 2 ) $$ + ); + SELECT diag_test_name( 'Update types - Type creation adds genertic rows for missing items' ); + SELECT set_eq( + $$ SELECT test_id , update_state , update_last + FROM test.test_updates + INNER JOIN sys.updates USING ( updtgt_id , updtype_id , update_id ) $$ , + $$ VALUES ( 1 , 'FUTURE'::sys.update_state_type , -1 ) , + ( 2 , 'FUTURE'::sys.update_state_type , '-1' ) $$ + ); + + INSERT INTO test.test VALUES ( 3 ); + SELECT diag_test_name( 'Update types - New items have automatically inserted update rows' ); + SELECT set_eq( + $$ SELECT test_id , updtype_id , update_state , update_last + FROM test.test_updates + INNER JOIN sys.updates USING ( updtgt_id , updtype_id , update_id ) + WHERE test_id = 3 $$ , + $$ VALUES ( 3 , 1 , 'FUTURE'::sys.update_state_type , -1 ) $$ + ); + + DELETE FROM sys.update_types WHERE updtype_id = 1; + SELECT diag_test_name( 'Update types - Type deletion cascades to specific rows' ); + SELECT is_empty( $$ SELECT * FROM test.test_updates $$ ); + SELECT diag_test_name( 'Update types - Type deletion cascades to generic rows' ); + SELECT is_empty( $$ SELECT * FROM sys.updates $$ ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/admin/040-functions/040-empire/010-create-empire.sql b/legacyworlds-server-data/db-structure/tests/admin/040-functions/040-empire/010-create-empire.sql index 72671df..6520694 100644 --- a/legacyworlds-server-data/db-structure/tests/admin/040-functions/040-empire/010-create-empire.sql +++ b/legacyworlds-server-data/db-structure/tests/admin/040-functions/040-empire/010-create-empire.sql @@ -16,7 +16,7 @@ BEGIN; SELECT _create_emp_names( 1 , 'testEmp' ); /***** TESTS BEGIN HERE *****/ - SELECT plan( 8 ); + SELECT plan( 7 ); SELECT emp.create_empire( _get_emp_name( 'testEmp1' ) , _get_map_name( 'testPlanet1' ) , @@ -57,16 +57,5 @@ BEGIN; FROM emp.resources WHERE empire_id = _get_emp_name( 'testEmp1' ); - SELECT diag_test_name( 'emp.create_empire() - Empire update records' ); - SELECT is( _eur.quantity , _utv.quantity) - FROM ( - SELECT COUNT(*) AS quantity FROM emp.updates - WHERE empire_id = _get_emp_name( 'testEmp1' ) - ) AS _eur , ( - SELECT COUNT(*) AS quantity - FROM unnest( enum_range( NULL::update_type ) ) AS _row - WHERE _row::TEXT LIKE 'EMPIRE_%' - ) AS _utv; - SELECT * FROM finish( ); ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/admin/050-updates/000-updates-ctrl/010-start-tick.sql b/legacyworlds-server-data/db-structure/tests/admin/050-updates/000-updates-ctrl/010-start-tick.sql new file mode 100644 index 0000000..85e5731 --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/admin/050-updates/000-updates-ctrl/010-start-tick.sql @@ -0,0 +1,53 @@ +/* + * Test the sys.start_tick() function + */ +BEGIN; + -- Delete all registered update types and targets + DELETE FROM sys.update_types; + DELETE FROM sys.update_targets; + + -- Create a new update target / type which will be used in tests + CREATE SCHEMA test + CREATE TABLE test( test_id INT NOT NULL PRIMARY KEY ); + INSERT INTO sys.update_targets VALUES ( 1 , 'Test' , 'test' , 'test' ); + INSERT INTO sys.update_types VALUES ( 1 , 1 , 'Test' , 2 , '' , NULL , NULL ); + + -- Insert a few update rows + INSERT INTO test.test VALUES ( 1 ) , ( 2 ) , ( 3 ); + + -- ***** TESTS BEGIN HERE ***** + SELECT plan( 5 ); + + UPDATE sys.status SET maintenance_start = now( ) , current_tick = NULL; + SELECT diag_test_name( 'sys.start_tick() - Ticks do not start if maintenance mode is active' ); + SELECT ok( sys.start_tick( ) IS NULL ); + + UPDATE sys.status SET maintenance_start = NULL , current_tick = next_tick; + SELECT diag_test_name( 'sys.start_tick() - Ticks do not start if current_tick is set' ); + SELECT ok( sys.start_tick( ) IS NULL ); + + UPDATE sys.status SET current_tick = NULL , next_tick = 2; + UPDATE sys.updates SET update_state = 'PROCESSED' , update_last = -1; + UPDATE sys.updates su SET update_last = 514 + FROM test.test_updates tu + WHERE su.update_id = tu.update_id + AND tu.test_id = 3; + SELECT diag_test_name( 'sys.start_tick() - Returns next tick identifier when ticks can start' ); + SELECT is( sys.start_tick( ) , 2::BIGINT ); + SELECT diag_test_name( 'sys.start_tick() - System status is updated' ); + SELECT set_eq( + $$ SELECT current_tick , next_tick FROM sys.status $$ , + $$ VALUES ( 2 , 3 ) $$ + ); + SELECT diag_test_name( 'sys.start_tick() - Update rows are marked for processing when necessary' ); + SELECT set_eq( + $$ SELECT test_id + FROM test.test_updates + INNER JOIN sys.updates + USING ( update_id , updtgt_id , updtype_id ) + WHERE update_state = 'FUTURE' AND update_last = 2 $$ , + $$ VALUES ( 1 ) , ( 2 ) $$ + ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/admin/050-updates/000-updates-ctrl/020-check-stuck-tick.sql b/legacyworlds-server-data/db-structure/tests/admin/050-updates/000-updates-ctrl/020-check-stuck-tick.sql new file mode 100644 index 0000000..cc5eaf9 --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/admin/050-updates/000-updates-ctrl/020-check-stuck-tick.sql @@ -0,0 +1,60 @@ +/* + * Tests of the sys.check_stuck_tick() function + */ +BEGIN; + -- Delete all registered update types and targets + DELETE FROM sys.update_types; + DELETE FROM sys.update_targets; + + -- Create a new update target / type which will be used in tests + CREATE SCHEMA test + CREATE TABLE test( test_id INT NOT NULL PRIMARY KEY ); + INSERT INTO sys.update_targets VALUES ( 1 , 'Test' , 'test' , 'test' ); + INSERT INTO sys.update_types VALUES ( 1 , 1 , 'Test' , 2 , '' , NULL , NULL ); + + -- Insert a few update rows + INSERT INTO test.test VALUES ( 1 ) , ( 2 ) , ( 3 ); + + -- Replace sys.end_tick() with a function that inserts into some table + CREATE TABLE end_tick_calls( tick_id BIGINT NOT NULL ); + CREATE OR REPLACE FUNCTION sys.end_tick( tick_id BIGINT ) + RETURNS VOID LANGUAGE SQL + AS 'INSERT INTO end_tick_calls VALUES( $1 );'; + + -- ***** TESTS BEGIN HERE ***** + SELECT plan( 8 ); + + UPDATE sys.status SET maintenance_start = NULL , current_tick = NULL; + SELECT diag_test_name( 'sys.check_stuck_tick() - Returns NULL if no tick is in progress' ); + SELECT ok( sys.check_stuck_tick() IS NULL ); + SELECT diag_test_name( 'sys.check_stuck_tick() - Does not call sys.end_tick() if no tick is in progress' ); + SELECT is_empty( $$ SELECT * FROM end_tick_calls $$ ); + DELETE FROM end_tick_calls; + + UPDATE sys.status SET maintenance_start = now( ) , current_tick = 2; + SELECT diag_test_name( 'sys.check_stuck_tick() - Returns NULL if maintenance mode is enabled' ); + SELECT ok( sys.check_stuck_tick() IS NULL ); + SELECT diag_test_name( 'sys.check_stuck_tick() - Does not call sys.end_tick() if maintenance mode is enabled' ); + SELECT is_empty( $$ SELECT * FROM end_tick_calls $$ ); + DELETE FROM end_tick_calls; + + UPDATE sys.status SET maintenance_start = NULL , current_tick = 2; + UPDATE sys.updates SET update_state = 'PROCESSED' , update_last = 2; + SELECT diag_test_name( 'sys.check_stuck_tick() - Returns NULL if a tick is in progress but there are no updates left' ); + SELECT ok( sys.check_stuck_tick() IS NULL ); + SELECT diag_test_name( 'sys.check_stuck_tick() - Calls sys.end_tick() if a tick is in progress but there are no updates left' ); + SELECT set_eq( + $$ SELECT * FROM end_tick_calls $$ , + $$ VALUES ( 2 ) $$ + ); + DELETE FROM end_tick_calls; + + UPDATE sys.status SET maintenance_start = NULL , current_tick = 2; + UPDATE sys.updates SET update_state = 'FUTURE' , update_last = 2; + SELECT diag_test_name( 'sys.check_stuck_tick() - Returns tick identifier if a tick is in progress and there are updates left' ); + SELECT is( sys.check_stuck_tick() , 2::BIGINT ); + SELECT diag_test_name( 'sys.check_stuck_tick() - Does not call sys.end_tick() if a tick is in progress and there are updates left' ); + SELECT is_empty( $$ SELECT * FROM end_tick_calls $$ ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/admin/050-updates/000-updates-ctrl/030-process-updates.sql b/legacyworlds-server-data/db-structure/tests/admin/050-updates/000-updates-ctrl/030-process-updates.sql new file mode 100644 index 0000000..ab25ff1 --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/admin/050-updates/000-updates-ctrl/030-process-updates.sql @@ -0,0 +1,179 @@ +/* + * Tests for the sys.process_updates() function + */ +BEGIN; + -- Delete all registered update types and targets + DELETE FROM sys.update_types; + DELETE FROM sys.update_targets; + + -- Create a new update target / type which will be used in tests + CREATE SCHEMA test + CREATE TABLE test( test_id INT NOT NULL PRIMARY KEY ); + INSERT INTO sys.update_targets VALUES ( 1 , 'Test' , 'test' , 'test' ); + INSERT INTO sys.update_types VALUES ( 1 , 1 , 'Test 1' , 2 , '' , NULL , NULL ); + INSERT INTO sys.update_types VALUES ( 2 , 1 , 'Test 2' , 4 , '' , NULL , NULL ); + + -- Replace sys.end_tick() with a function that inserts into some table + CREATE TABLE end_tick_calls( tick_id BIGINT NOT NULL ); + CREATE OR REPLACE FUNCTION sys.end_tick( tick_id BIGINT ) + RETURNS VOID LANGUAGE SQL + AS 'INSERT INTO end_tick_calls VALUES( $1 );'; + + -- Create a sys.process_dummy_update() function that inserts into some other table + CREATE TABLE process_update_calls( tick_id BIGINT NOT NULL ); + CREATE OR REPLACE FUNCTION sys.process_dummy_update( tick_id BIGINT ) + RETURNS VOID LANGUAGE SQL + AS 'INSERT INTO process_update_calls VALUES( $1 );'; + + -- Set the general batch size to 1 + SELECT sys.uoc_constant( 'game.batchSize' , '(test)' , 'Game updates' , 1 ); + + -- Insert a few entries in the test table + INSERT INTO test.test + SELECT * FROM generate_series( 1 , 10 ); + + -- ***** TESTS BEGIN HERE ***** + SELECT plan( 21 ); + + /* + * Case: + * Updates have not been processed yet, + * + * Expected results: + * 1) New updates of the first type have been marked for processing, + * 2) The quantity of updates marked for processing corresponds to game.batchSize + */ + UPDATE sys.updates + SET update_state = 'FUTURE' , update_last = 1; + SELECT diag_test_name( 'sys.process_updates() - General batch size - Return value' ); + SELECT ok( sys.process_updates( 1 ) = ROW( TRUE , 'Test 1'::TEXT ) ); + SELECT diag_test_name( 'sys.process_updates() - General batch size - Update type' ); + SELECT set_eq( + $$ SELECT DISTINCT updtype_id FROM sys.updates WHERE update_state = 'PROCESSING'; $$ , + $$ VALUES ( 1 ) $$ + ); + SELECT diag_test_name( 'sys.process_updates() - General batch size - Update count' ); + SELECT is( COUNT(*)::INT , 1 ) FROM sys.updates WHERE update_state = 'PROCESSING'; + SELECT diag_test_name( 'sys.process_updates() - General batch size - sys.end_tick() not called' ); + SELECT is_empty( $$ SELECT * FROM end_tick_calls $$ ); + DELETE FROM end_tick_calls; + + /* + * Case: + * Updates have not been processed yet, + * Update type has a specific batch size, + * + * Expected results: + * 1) New updates have been marked for processing, + * 2) The quantity of updates marked for processing corresponds to the specific batch size + * 3) sys.end_tick() has not been called + */ + UPDATE sys.update_types SET updtype_batch_size = 2 WHERE updtype_id = 1; + UPDATE sys.updates SET update_state = 'FUTURE' , update_last = 1; + SELECT diag_test_name( 'sys.process_updates() - Specific batch size - Return value' ); + SELECT ok( sys.process_updates( 1 ) = ROW( TRUE , 'Test 1'::TEXT ) ); + SELECT diag_test_name( 'sys.process_updates() - Specific batch size - Update type' ); + SELECT set_eq( + $$ SELECT DISTINCT updtype_id FROM sys.updates WHERE update_state = 'PROCESSING'; $$ , + $$ VALUES ( 1 ) $$ + ); + SELECT diag_test_name( 'sys.process_updates() - Specific batch size - Update count' ); + SELECT is( COUNT(*)::INT , 2 ) FROM sys.updates WHERE update_state = 'PROCESSING'; + SELECT diag_test_name( 'sys.process_updates() - Specific batch size - sys.end_tick() not called' ); + SELECT is_empty( $$ SELECT * FROM end_tick_calls $$ ); + DELETE FROM end_tick_calls; + + /* + * Case: + * Some but not all updates of the first type have been processed, + * Batch size is greater than the quantity of updates left to process for the type. + * + * Expected results: + * 1) All updates of the first type are marked either for processing or as processed. + * 2) sys.end_tick() has not been called + */ + UPDATE sys.update_types SET updtype_batch_size = 2 WHERE updtype_id = 1; + UPDATE sys.updates SET update_state = 'FUTURE' , update_last = 1; + UPDATE sys.updates + SET update_state = 'PROCESSED' + WHERE update_id IN ( + SELECT update_id FROM sys.updates WHERE updtype_id = 1 LIMIT 9 + ); + UPDATE sys.update_types SET updtype_batch_size = 2 WHERE updtype_id = 1; + SELECT diag_test_name( 'sys.process_updates() - End of type updates - Return value' ); + SELECT ok( sys.process_updates( 1 ) = ROW( TRUE , 'Test 1'::TEXT ) ); + SELECT diag_test_name( 'sys.process_updates() - End of type updates - Update type' ); + SELECT set_eq( + $$ SELECT DISTINCT updtype_id FROM sys.updates WHERE update_state <> 'FUTURE'; $$ , + $$ VALUES ( 1 ) $$ + ); + SELECT diag_test_name( 'sys.process_updates() - End of type updates - Update count' ); + SELECT is( COUNT(*)::INT , 10 ) FROM sys.updates WHERE update_state <> 'FUTURE'; + SELECT diag_test_name( 'sys.process_updates() - End of type updates - sys.end_tick() not called' ); + SELECT is_empty( $$ SELECT * FROM end_tick_calls $$ ); + DELETE FROM end_tick_calls; + + /* + * Case: + * All updates of the first type have been processed. + * No updates of the second type have been processed. + * + * Expected results: + * 1) Some updates from the second type have been marked for processing. + * 2) sys.end_tick() has not been called + */ + UPDATE sys.updates SET update_state = 'FUTURE' , update_last = 1; + UPDATE sys.updates + SET update_state = 'PROCESSED' + WHERE updtype_id = 1; + SELECT diag_test_name( 'sys.process_updates() - Update type transition - Return value' ); + SELECT ok( sys.process_updates( 1 ) = ROW( TRUE , 'Test 2'::TEXT ) ); + SELECT diag_test_name( 'sys.process_updates() - Update type transition - Update type' ); + SELECT set_eq( + $$ SELECT DISTINCT updtype_id FROM sys.updates WHERE update_state = 'PROCESSING'; $$ , + $$ VALUES ( 2 ) $$ + ); + SELECT diag_test_name( 'sys.process_updates() - Update type transition - Update count' ); + SELECT is( COUNT(*)::INT , 1 ) FROM sys.updates WHERE update_state = 'PROCESSING'; + SELECT diag_test_name( 'sys.process_updates() - Update type transition - sys.end_tick() not called' ); + SELECT is_empty( $$ SELECT * FROM end_tick_calls $$ ); + DELETE FROM end_tick_calls; + + /* + * Case: + * There no updates to process + * Updates are marked as 'PROCESSING' + * + * Expected results: + * 1) _has_more return value is FALSE, + * 2) All updates are marked as 'PROCESSED' + * 3) sys.end_tick() has been called. + */ + UPDATE sys.updates SET update_state = 'PROCESSING' , update_last = 1; + SELECT diag_test_name( 'sys.process_updates() - No updates left - Return value' ); + SELECT ok( sys.process_updates( 1 ) = ROW( FALSE , NULL::TEXT ) ); + SELECT diag_test_name( 'sys.process_updates() - No updates left - All updates processed' ); + SELECT is_empty( $$ SELECT * FROM sys.updates WHERE update_state <> 'PROCESSED' $$ ); + SELECT diag_test_name( 'sys.process_updates() - No updates left - sys.end_tick() called' ); + SELECT set_eq( $$ SELECT * FROM end_tick_calls $$ , $$ VALUES ( 1 ) $$ ); + DELETE FROM end_tick_calls; + + /* + * Case: + * The update type has an in-base processing function + * + * Expected result: + * 1) _process_externally return value is NULL + * 2) The processing function has been called. + */ + UPDATE sys.updates SET update_state = 'FUTURE' , update_last = 1; + UPDATE sys.update_types + SET updtype_batch_size = NULL , updtype_proc_name = 'process_dummy_update' + WHERE updtype_id = 1; + SELECT diag_test_name( 'sys.process_updates() - In-base processing - Return value' ); + SELECT ok( sys.process_updates( 1 ) = ROW( TRUE , NULL::TEXT ) ); + SELECT diag_test_name( 'sys.process_updates() - In-base processing - Processing function called' ); + SELECT set_eq( $$ SELECT * FROM process_update_calls $$ , $$ VALUES ( 1 ) $$ ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/admin/050-updates/003-core-update-targets/010-presence.sql b/legacyworlds-server-data/db-structure/tests/admin/050-updates/003-core-update-targets/010-presence.sql new file mode 100644 index 0000000..595b988 --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/admin/050-updates/003-core-update-targets/010-presence.sql @@ -0,0 +1,17 @@ +/* + * Check that core update targets have been defined correctly + */ +BEGIN; + SELECT plan( 3 ); + + SELECT diag_test_name( 'Update targets - Definitions present' ); + SELECT is( COUNT(*)::INT , 2 ) FROM sys.update_targets; + + SELECT diag_test_name( 'Update targets - verse.planets_updates exists' ); + SELECT has_table( 'verse' , 'planets_updates' , 'Missing planets update table' ); + + SELECT diag_test_name( 'Update targets - emp.empires_updates exists' ); + SELECT has_table( 'emp' , 'empires_updates' , 'Missing empires update table' ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/admin/050-updates/015-empire-resources/010-process-empire-resources-updates.sql b/legacyworlds-server-data/db-structure/tests/admin/050-updates/015-empire-resources/010-process-empire-resources-updates.sql index 2b4f57a..5e670c7 100644 --- a/legacyworlds-server-data/db-structure/tests/admin/050-updates/015-empire-resources/010-process-empire-resources-updates.sql +++ b/legacyworlds-server-data/db-structure/tests/admin/050-updates/015-empire-resources/010-process-empire-resources-updates.sql @@ -2,6 +2,12 @@ * Test the sys.process_empire_resources_updates() function */ BEGIN; + /* Start by deleting all update types except for the one + * we're interested in. + */ + DELETE FROM sys.update_types + WHERE updtype_name <> 'EmpireResources'; + /* We need: * - three types of resources, * - two empire with resources records, and the corresponding update @@ -35,14 +41,15 @@ BEGIN; /* Make sure update 0 is set to be processed for testEmp1 */ UPDATE sys.updates - SET status = 'PROCESSING' , last_tick = 0; + SET update_state = 'PROCESSING' , update_last = 0; /* Create testEmp2 empire, make sure it will not be processed */ SELECT emp.create_empire( _get_emp_name( 'testEmp2' ) , _get_map_name( 'testPlanet2' ) , 1 ); UPDATE sys.updates _su - SET status = 'PROCESSED' , last_tick = 0 - FROM emp.updates _eu - WHERE _eu.update_id = _su.id AND _eu.empire_id = _get_emp_name( 'testEmp2' ); + SET update_state = 'PROCESSED' , update_last = 0 + FROM emp.empires_updates _eu + WHERE _eu.update_id = _su.update_id + AND _eu.name_id = _get_emp_name( 'testEmp2' ); /****** TESTS BEGIN HERE ******/ SELECT plan( 19 ); diff --git a/legacyworlds-server-data/db-structure/tests/admin/050-updates/105-planet-resource-regeneration/010-process-planet-res-regen-updates.sql b/legacyworlds-server-data/db-structure/tests/admin/050-updates/105-planet-resource-regeneration/010-process-planet-res-regen-updates.sql index 54385b4..72c4188 100644 --- a/legacyworlds-server-data/db-structure/tests/admin/050-updates/105-planet-resource-regeneration/010-process-planet-res-regen-updates.sql +++ b/legacyworlds-server-data/db-structure/tests/admin/050-updates/105-planet-resource-regeneration/010-process-planet-res-regen-updates.sql @@ -2,6 +2,11 @@ * Test for the sys.process_planet_res_regen_updates() function */ BEGIN; + /* Start by deleting all update types except for the one + * we're interested in. + */ + DELETE FROM sys.update_types + WHERE updtype_name <> 'ResourceRegeneration'; /* We need to create a natural resource and a pair of planets */ \i utils/strings.sql @@ -34,23 +39,19 @@ BEGIN; 0.5 , 0.5 ); - /* Insert update records for the planets. Planets 1 and 2 will be processed, + /* Modify update records for the planets. Planets 1 and 2 will be processed, * planet testPlanet3 will not. */ - INSERT INTO sys.updates ( id , gu_type , status , last_tick ) - VALUES ( 1 , 'PLANET_RES_REGEN' , 'PROCESSING' , 0 ); - INSERT INTO verse.updates ( update_id , planet_id ) - VALUES ( 1 , _get_map_name( 'testPlanet1' ) ); - - INSERT INTO sys.updates ( id , gu_type , status , last_tick ) - VALUES ( 2 , 'PLANET_RES_REGEN' , 'PROCESSING' , 0 ); - INSERT INTO verse.updates ( update_id , planet_id ) - VALUES ( 2 , _get_map_name( 'testPlanet2' ) ); - - INSERT INTO sys.updates ( id , gu_type , status , last_tick ) - VALUES ( 3 , 'PLANET_RES_REGEN' , 'PROCESSED' , 0 ); - INSERT INTO verse.updates ( update_id , planet_id ) - VALUES ( 3 , _get_map_name( 'testPlanet3' ) ); + UPDATE sys.updates su + SET update_state = 'PROCESSING' , update_last = 0 + FROM verse.planets_updates vu + WHERE vu.update_id = su.update_id + AND vu.name_id <> _get_map_name( 'testPlanet3' ); + UPDATE sys.updates su + SET update_state = 'PROCESSED' , update_last = 0 + FROM verse.planets_updates vu + WHERE vu.update_id = su.update_id + AND vu.name_id = _get_map_name( 'testPlanet3' ); /* Helper function that gets a provider's current quantity */ CREATE FUNCTION _get_quantity( _planet TEXT ) @@ -65,7 +66,7 @@ BEGIN; SELECT plan( 6 ); - SELECT sys.process_planet_res_regen_updates( 10 ); + SELECT sys.process_res_regen_updates( 10 ); SELECT diag_test_name( 'PLANET_RES_REGEN update - Wrong update identifier (1/3)' ); SELECT ok( _get_quantity( 'testPlanet1' ) = 50 ); SELECT diag_test_name( 'PLANET_RES_REGEN update - Wrong update identifier (2/3)' ); @@ -74,7 +75,7 @@ BEGIN; SELECT ok( _get_quantity( 'testPlanet3' ) = 50 ); - SELECT sys.process_planet_res_regen_updates( 0 ); + SELECT sys.process_res_regen_updates( 0 ); SELECT diag_test_name( 'PLANET_RES_REGEN update - Processed planet with quantity < max.' ); SELECT ok( _get_quantity( 'testPlanet1' ) > 50 ); SELECT diag_test_name( 'PLANET_RES_REGEN update - Processed planet with quantity = max.' ); diff --git a/legacyworlds-server-data/db-structure/tests/user/030-data/145-updates/010-update-targets.sql b/legacyworlds-server-data/db-structure/tests/user/030-data/145-updates/010-update-targets.sql new file mode 100644 index 0000000..70343c3 --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/user/030-data/145-updates/010-update-targets.sql @@ -0,0 +1,28 @@ +/* + * Test privileges on sys.update_targets + */ +BEGIN; + SELECT plan( 4 ); + + SELECT diag_test_name( 'sys.update_targets - No INSERT privilege' ); + SELECT throws_ok( + $$ INSERT INTO sys.update_targets DEFAULT VALUES; $$ , + 42501 ); + + SELECT diag_test_name( 'sys.update_targets - No UPDATE privilege' ); + SELECT throws_ok( + $$ UPDATE sys.update_targets SET updtgt_id = 42; $$ , + 42501 ); + + SELECT diag_test_name( 'sys.update_targets - No SELECT privilege' ); + SELECT throws_ok( + $$ SELECT * FROM sys.update_targets; $$ , + 42501 ); + + SELECT diag_test_name( 'sys.update_targets - No DELETE privilege' ); + SELECT throws_ok( + $$ DELETE FROM sys.update_targets; $$ , + 42501 ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/user/030-data/145-updates/020-update-types.sql b/legacyworlds-server-data/db-structure/tests/user/030-data/145-updates/020-update-types.sql new file mode 100644 index 0000000..fc33d56 --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/user/030-data/145-updates/020-update-types.sql @@ -0,0 +1,28 @@ +/* + * Test privileges on sys.update_types + */ +BEGIN; + SELECT plan( 4 ); + + SELECT diag_test_name( 'sys.update_types - No INSERT privilege' ); + SELECT throws_ok( + $$ INSERT INTO sys.update_types DEFAULT VALUES; $$ , + 42501 ); + + SELECT diag_test_name( 'sys.update_types - No UPDATE privilege' ); + SELECT throws_ok( + $$ UPDATE sys.update_types SET updtgt_id = 42; $$ , + 42501 ); + + SELECT diag_test_name( 'sys.update_types - No SELECT privilege' ); + SELECT throws_ok( + $$ SELECT * FROM sys.update_types; $$ , + 42501 ); + + SELECT diag_test_name( 'sys.update_types - No DELETE privilege' ); + SELECT throws_ok( + $$ DELETE FROM sys.update_types; $$ , + 42501 ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/user/030-data/145-updates/030-updates.sql b/legacyworlds-server-data/db-structure/tests/user/030-data/145-updates/030-updates.sql new file mode 100644 index 0000000..cc86cb3 --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/user/030-data/145-updates/030-updates.sql @@ -0,0 +1,28 @@ +/* + * Test privileges on sys.updates + */ +BEGIN; + SELECT plan( 4 ); + + SELECT diag_test_name( 'sys.updates - No INSERT privilege' ); + SELECT throws_ok( + $$ INSERT INTO sys.updates DEFAULT VALUES; $$ , + 42501 ); + + SELECT diag_test_name( 'sys.updates - No UPDATE privilege' ); + SELECT throws_ok( + $$ UPDATE sys.updates SET updtgt_id = 42; $$ , + 42501 ); + + SELECT diag_test_name( 'sys.updates - No SELECT privilege' ); + SELECT throws_ok( + $$ SELECT * FROM sys.updates; $$ , + 42501 ); + + SELECT diag_test_name( 'sys.updates - No DELETE privilege' ); + SELECT throws_ok( + $$ DELETE FROM sys.updates; $$ , + 42501 ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/010-get-table-pkey.sql b/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/010-get-table-pkey.sql new file mode 100644 index 0000000..ef222fc --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/010-get-table-pkey.sql @@ -0,0 +1,11 @@ +/* + * Test privileges on sys.get_table_pkey() + */ +BEGIN; + SELECT plan( 1 ); + + SELECT diag_test_name( 'sys.get_table_pkey() - No EXECUTE privilege' ); + SELECT throws_ok( $$ SELECT sys.get_table_pkey( 'a' , 'b' ) $$ , 42501 ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/020-tgf-speupd-before-insert.sql b/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/020-tgf-speupd-before-insert.sql new file mode 100644 index 0000000..dae5ca2 --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/020-tgf-speupd-before-insert.sql @@ -0,0 +1,11 @@ +/* + * Test privileges on sys.tgf_speupd_before_insert() + */ +BEGIN; + SELECT plan( 1 ); + + SELECT diag_test_name( 'sys.tgf_speupd_before_insert() - No EXECUTE privilege' ); + SELECT throws_ok( $$ SELECT sys.tgf_speupd_before_insert( ) $$ , 42501 ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/030-tgf-speupd-after-delete.sql b/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/030-tgf-speupd-after-delete.sql new file mode 100644 index 0000000..431a387 --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/030-tgf-speupd-after-delete.sql @@ -0,0 +1,11 @@ +/* + * Test privileges on sys.tgf_speupd_after_delete() + */ +BEGIN; + SELECT plan( 1 ); + + SELECT diag_test_name( 'sys.tgf_speupd_after_delete() - No EXECUTE privilege' ); + SELECT throws_ok( $$ SELECT sys.tgf_speupd_after_delete( ) $$ , 42501 ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/040-insert-missing-updates.sql b/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/040-insert-missing-updates.sql new file mode 100644 index 0000000..2b22f82 --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/040-insert-missing-updates.sql @@ -0,0 +1,11 @@ +/* + * Test privileges on sys.insert_missing_updates() + */ +BEGIN; + SELECT plan( 1 ); + + SELECT diag_test_name( 'sys.insert_missing_updates() - No EXECUTE privilege' ); + SELECT throws_ok( $$ SELECT sys.insert_missing_updates( 1 ) $$ , 42501 ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/050-tgf-insert-missing-updates.sql b/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/050-tgf-insert-missing-updates.sql new file mode 100644 index 0000000..26f9c10 --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/050-tgf-insert-missing-updates.sql @@ -0,0 +1,11 @@ +/* + * Test privileges on sys.tgf_insert_missing_updates() + */ +BEGIN; + SELECT plan( 1 ); + + SELECT diag_test_name( 'sys.tgf_insert_missing_updates() - No EXECUTE privilege' ); + SELECT throws_ok( $$ SELECT sys.tgf_insert_missing_updates( ) $$ , 42501 ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/060-tgf-updtype-after-insert-row.sql b/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/060-tgf-updtype-after-insert-row.sql new file mode 100644 index 0000000..5fce32c --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/060-tgf-updtype-after-insert-row.sql @@ -0,0 +1,11 @@ +/* + * Test privileges on sys.tgf_updtype_after_insert_row() + */ +BEGIN; + SELECT plan( 1 ); + + SELECT diag_test_name( 'sys.tgf_updtype_after_insert_row() - No EXECUTE privilege' ); + SELECT throws_ok( $$ SELECT sys.tgf_updtype_after_insert_row( ) $$ , 42501 ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/070-tgf-check-update-type-proc.sql b/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/070-tgf-check-update-type-proc.sql new file mode 100644 index 0000000..064dbdc --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/070-tgf-check-update-type-proc.sql @@ -0,0 +1,11 @@ +/* + * Test privileges on sys.tgf_check_update_type_proc() + */ +BEGIN; + SELECT plan( 1 ); + + SELECT diag_test_name( 'sys.tgf_check_update_type_proc() - No EXECUTE privilege' ); + SELECT throws_ok( $$ SELECT sys.tgf_check_update_type_proc( ) $$ , 42501 ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/080-tgf-reorder-update-types.sql b/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/080-tgf-reorder-update-types.sql new file mode 100644 index 0000000..bb27646 --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/080-tgf-reorder-update-types.sql @@ -0,0 +1,11 @@ +/* + * Test privileges on sys.tgf_reorder_update_types() + */ +BEGIN; + SELECT plan( 1 ); + + SELECT diag_test_name( 'sys.tgf_reorder_update_types() - No EXECUTE privilege' ); + SELECT throws_ok( $$ SELECT sys.tgf_reorder_update_types( ) $$ , 42501 ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/090-tgf-updtgt-before-insert.sql b/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/090-tgf-updtgt-before-insert.sql new file mode 100644 index 0000000..5b6ea64 --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/090-tgf-updtgt-before-insert.sql @@ -0,0 +1,11 @@ +/* + * Test privileges on sys.tgf_updtgt_before_insert() + */ +BEGIN; + SELECT plan( 1 ); + + SELECT diag_test_name( 'sys.tgf_updtgt_before_insert() - No EXECUTE privilege' ); + SELECT throws_ok( $$ SELECT sys.tgf_updtgt_before_insert( ) $$ , 42501 ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/100-tgf-updtgt-after-insert.sql b/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/100-tgf-updtgt-after-insert.sql new file mode 100644 index 0000000..6da4cfa --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/100-tgf-updtgt-after-insert.sql @@ -0,0 +1,11 @@ +/* + * Test privileges on sys.tgf_updtgt_after_insert() + */ +BEGIN; + SELECT plan( 1 ); + + SELECT diag_test_name( 'sys.tgf_updtgt_after_insert() - No EXECUTE privilege' ); + SELECT throws_ok( $$ SELECT sys.tgf_updtgt_after_insert( ) $$ , 42501 ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/110-tgf-updtgt-after-delete.sql b/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/110-tgf-updtgt-after-delete.sql new file mode 100644 index 0000000..0d1c554 --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/110-tgf-updtgt-after-delete.sql @@ -0,0 +1,11 @@ +/* + * Test privileges on sys.tgf_updtgt_after_delete() + */ +BEGIN; + SELECT plan( 1 ); + + SELECT diag_test_name( 'sys.tgf_updtgt_after_delete() - No EXECUTE privilege' ); + SELECT throws_ok( $$ SELECT sys.tgf_updtgt_after_delete( ) $$ , 42501 ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/120-register-update-type.sql b/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/120-register-update-type.sql new file mode 100644 index 0000000..f25b59e --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/user/040-functions/003-updates/120-register-update-type.sql @@ -0,0 +1,11 @@ +/* + * Test privileges on sys.register_update_type() + */ +BEGIN; + SELECT plan( 1 ); + + SELECT diag_test_name( 'sys.register_update_type() - No EXECUTE privilege' ); + SELECT throws_ok( $$ SELECT sys.register_update_type( '' , '' , '' , 'x' ) $$ , 42501 ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/user/050-updates/000-updates-ctrl/010-start-tick.sql b/legacyworlds-server-data/db-structure/tests/user/050-updates/000-updates-ctrl/010-start-tick.sql new file mode 100644 index 0000000..cb763fb --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/user/050-updates/000-updates-ctrl/010-start-tick.sql @@ -0,0 +1,11 @@ +/* + * Test privileges on sys.start_tick() + */ +BEGIN; + SELECT plan( 1 ); + + SELECT diag_test_name( 'sys.start_tick() - EXECUTE privilege' ); + SELECT lives_ok( 'SELECT sys.start_tick( )' ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/user/050-updates/000-updates-ctrl/020-check-stuck-tick.sql b/legacyworlds-server-data/db-structure/tests/user/050-updates/000-updates-ctrl/020-check-stuck-tick.sql new file mode 100644 index 0000000..5799b95 --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/user/050-updates/000-updates-ctrl/020-check-stuck-tick.sql @@ -0,0 +1,11 @@ +/* + * Test privileges on sys.check_stuck_tick() + */ +BEGIN; + SELECT plan( 1 ); + + SELECT diag_test_name( 'sys.check_stuck_tick() - EXECUTE privilege' ); + SELECT lives_ok( 'SELECT sys.check_stuck_tick( )' ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/user/050-updates/000-updates-ctrl/030-process-updates.sql b/legacyworlds-server-data/db-structure/tests/user/050-updates/000-updates-ctrl/030-process-updates.sql new file mode 100644 index 0000000..5416d6c --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/user/050-updates/000-updates-ctrl/030-process-updates.sql @@ -0,0 +1,11 @@ +/* + * Test privileges on sys.process_updates() + */ +BEGIN; + SELECT plan( 1 ); + + SELECT diag_test_name( 'sys.process_updates() - EXECUTE privilege' ); + SELECT lives_ok( 'SELECT sys.process_updates( 1 )' ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/user/050-updates/105-planet-resource-regeneration/010-process-planet-res-regen-updates.sql b/legacyworlds-server-data/db-structure/tests/user/050-updates/105-planet-resource-regeneration/010-process-planet-res-regen-updates.sql deleted file mode 100644 index b08eb50..0000000 --- a/legacyworlds-server-data/db-structure/tests/user/050-updates/105-planet-resource-regeneration/010-process-planet-res-regen-updates.sql +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Test privileges on sys.process_planet_res_regen_updates() - */ -BEGIN; - SELECT plan( 1 ); - - SELECT diag_test_name( 'sys.process_planet_res_regen_updates() - Privileges' ); - SELECT throws_ok( 'SELECT sys.process_planet_res_regen_updates( 1 )' , 42501 ); - - SELECT * FROM finish( ); -ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/user/050-updates/105-planet-resource-regeneration/010-process-res-regen-updates.sql b/legacyworlds-server-data/db-structure/tests/user/050-updates/105-planet-resource-regeneration/010-process-res-regen-updates.sql new file mode 100644 index 0000000..cb4bcd6 --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/user/050-updates/105-planet-resource-regeneration/010-process-res-regen-updates.sql @@ -0,0 +1,11 @@ +/* + * Test privileges on sys.process_res_regen_updates() + */ +BEGIN; + SELECT plan( 1 ); + + SELECT diag_test_name( 'sys.process_res_regen_updates() - Privileges' ); + SELECT throws_ok( 'SELECT sys.process_res_regen_updates( 1 )' , 42501 ); + + SELECT * FROM finish( ); +ROLLBACK; \ No newline at end of file diff --git a/legacyworlds-server-data/db-structure/tests/utils/common-setup/setup-gu-pmc-get-data-test.sql b/legacyworlds-server-data/db-structure/tests/utils/common-setup/setup-gu-pmc-get-data-test.sql index 97df95f..57d62d3 100644 --- a/legacyworlds-server-data/db-structure/tests/utils/common-setup/setup-gu-pmc-get-data-test.sql +++ b/legacyworlds-server-data/db-structure/tests/utils/common-setup/setup-gu-pmc-get-data-test.sql @@ -21,13 +21,13 @@ * - Finally, we need a planet that matches the criterias but isn't * scheduled for an update at the time. * - * FIXME: for now, locking is NOT tested. I simply have no idea how to do - * it, at least not without creating an extra database and that - * kind of stuff. - * * FIXME: cannot test where the happiness column comes from until values * all use DOUBLE PRECISION. */ + +DELETE FROM sys.update_types + WHERE updtype_name <> 'PlanetMining'; + \i utils/strings.sql \i utils/resources.sql \i utils/accounts.sql @@ -41,17 +41,7 @@ SELECT _create_emp_names( 4 , 'empire' ); INSERT INTO emp.empires ( name_id , cash ) SELECT id , 0 FROM naming.empire_names; -/* Planet #1 */ -INSERT INTO sys.updates( id , gu_type , status , last_tick ) - VALUES ( 1 , 'PLANET_MINING' , 'PROCESSING' , 0 ); -INSERT INTO verse.updates( update_id , planet_id ) - VALUES ( 1 , _get_map_name( 'planet1' ) ); - /* Planet #2 */ -INSERT INTO sys.updates( id , gu_type , status , last_tick ) - VALUES ( 2 , 'PLANET_MINING' , 'PROCESSING' , 0 ); -INSERT INTO verse.updates( update_id , planet_id ) - VALUES ( 2 , _get_map_name( 'planet2' ) ); INSERT INTO verse.resource_providers ( planet_id , resource_name_id , resprov_quantity_max , resprov_quantity , resprov_difficulty , resprov_recovery @@ -64,10 +54,6 @@ INSERT INTO verse.resource_providers ( ); /* Planet #3 */ -INSERT INTO sys.updates( id , gu_type , status , last_tick ) - VALUES ( 3 , 'PLANET_MINING' , 'PROCESSING' , 0 ); -INSERT INTO verse.updates( update_id , planet_id ) - VALUES ( 3 , _get_map_name( 'planet3' ) ); INSERT INTO verse.resource_providers ( planet_id , resource_name_id , resprov_quantity_max , resprov_quantity , resprov_difficulty , resprov_recovery @@ -90,10 +76,6 @@ INSERT INTO emp.mining_settings( empire_id , resource_name_id , empmset_weight ) ); /* Planet #4 */ -INSERT INTO sys.updates( id , gu_type , status , last_tick ) - VALUES ( 4 , 'PLANET_MINING' , 'PROCESSING' , 0 ); -INSERT INTO verse.updates( update_id , planet_id ) - VALUES ( 4 , _get_map_name( 'planet4' ) ); INSERT INTO verse.resource_providers ( planet_id , resource_name_id , resprov_quantity_max , resprov_quantity , resprov_difficulty , resprov_recovery @@ -123,10 +105,6 @@ INSERT INTO emp.planet_mining_settings( ); /* Planet #5 */ -INSERT INTO sys.updates( id , gu_type , status , last_tick ) - VALUES ( 5 , 'PLANET_MINING' , 'PROCESSING' , 0 ); -INSERT INTO verse.updates( update_id , planet_id ) - VALUES ( 5 , _get_map_name( 'planet5' ) ); INSERT INTO verse.planet_happiness ( planet_id , current , target ) VALUES ( _get_map_name( 'planet5' ) , 0.5 , 0.5 ); INSERT INTO emp.planets ( empire_id , planet_id ) @@ -139,10 +117,6 @@ INSERT INTO emp.mining_settings( empire_id , resource_name_id , empmset_weight ) ); /* Planet #6 */ -INSERT INTO sys.updates( id , gu_type , status , last_tick ) - VALUES ( 6 , 'PLANET_MINING' , 'PROCESSED' , 0 ); -INSERT INTO verse.updates( update_id , planet_id ) - VALUES ( 6 , _get_map_name( 'planet6' ) ); INSERT INTO verse.resource_providers ( planet_id , resource_name_id , resprov_quantity_max , resprov_quantity , resprov_difficulty , resprov_recovery @@ -168,3 +142,16 @@ INSERT INTO emp.mining_settings( empire_id , resource_name_id , empmset_weight ) INSERT INTO verse.planet_resources ( planet_id , resource_name_id , pres_income , pres_upkeep ) SELECT p.name_id , r.resource_name_id , 42 , 0 FROM verse.planets p CROSS JOIN defs.resources r; + +/* Modify update records for the planets. Planet 6 is not to be processed. + */ +UPDATE sys.updates su + SET update_state = 'PROCESSING' , update_last = 0 + FROM verse.planets_updates vu + WHERE vu.update_id = su.update_id + AND vu.name_id <> _get_map_name( 'planet6' ); +UPDATE sys.updates su + SET update_state = 'PROCESSED' , update_last = 0 + FROM verse.planets_updates vu + WHERE vu.update_id = su.update_id + AND vu.name_id = _get_map_name( 'planet6' ); diff --git a/legacyworlds-server-data/src/main/java/com/deepclone/lw/sqld/sys/GameUpdateResult.java b/legacyworlds-server-data/src/main/java/com/deepclone/lw/sqld/sys/GameUpdateResult.java new file mode 100644 index 0000000..5388528 --- /dev/null +++ b/legacyworlds-server-data/src/main/java/com/deepclone/lw/sqld/sys/GameUpdateResult.java @@ -0,0 +1,82 @@ +package com.deepclone.lw.sqld.sys; + + +/** + * The result of a game update + * + *

+ * This class is used to transmit the results of the game updates processor. The results may + * indicate: + *

    + *
  • a finished game update cycle that needs no further processing, + *
  • an in-progress update cycle for which further in-base processing is needed, + *
  • an in-progress update cycle where some processing must be done in the server. + *
+ * + * @author E. Benoît + */ +public class GameUpdateResult +{ + + /** Whether the update cycle is finished */ + private final boolean finished; + + /** + * Name of the type of processing that needs to be run on the server, or null if + * the update is finished or was processed in-base + */ + private final String localExecution; + + + /** + * Initialise a "finished" game update result + * + *

+ * This constructor will set {@link #finished} to true and {@link #localExecution} + * to null. + */ + public GameUpdateResult( ) + { + this.finished = true; + this.localExecution = null; + } + + + /** + * Initialise an "incomplete" game update result + * + *

+ * This constructor will set {@link #finished} to false and {@link #localExecution} + * to the parameter's value. + * + * @param localExecution + * the name of the type of processing to be executed on the server, or + * null if the update was processed in-base. + */ + public GameUpdateResult( String localExecution ) + { + this.finished = false; + this.localExecution = localExecution; + } + + + /** + * @return true if the update cycle was completed or false if further + * processing is required. + */ + public boolean isFinished( ) + { + return this.finished; + } + + + /** + * @return Name of the type of processing that needs to be run on the server, or + * null if the update is finished or was processed in-base + */ + public String getLocalExecution( ) + { + return this.localExecution; + } + +} diff --git a/legacyworlds-server-interfaces/src/main/java/com/deepclone/lw/interfaces/game/UpdatesDAO.java b/legacyworlds-server-interfaces/src/main/java/com/deepclone/lw/interfaces/game/UpdatesDAO.java deleted file mode 100644 index 86736b4..0000000 --- a/legacyworlds-server-interfaces/src/main/java/com/deepclone/lw/interfaces/game/UpdatesDAO.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.deepclone.lw.interfaces.game; - - -public interface UpdatesDAO -{ - public boolean processUpdates( long tickId ); -} diff --git a/legacyworlds-server-interfaces/src/main/java/com/deepclone/lw/interfaces/game/updates/GameUpdateProcessor.java b/legacyworlds-server-interfaces/src/main/java/com/deepclone/lw/interfaces/game/updates/GameUpdateProcessor.java new file mode 100644 index 0000000..9008c4f --- /dev/null +++ b/legacyworlds-server-interfaces/src/main/java/com/deepclone/lw/interfaces/game/updates/GameUpdateProcessor.java @@ -0,0 +1,75 @@ +package com.deepclone.lw.interfaces.game.updates; + + +/** + * Game update processor + * + *

+ * The game update processor allows game update cycles to be executed. It also provides the ability + * to terminated an existing update cycle. + * + *

+ * The game update processor includes a locking mechanism which can be used to prevent two + * components (for example the game update ticker task and the administration interface) from + * executing game updates simultaneously. + * + * @author E. Benoît + */ +public interface GameUpdateProcessor +{ + + /** + * Try locking the game update processor + * + *

+ * Test and set the processor's lock. This lock is not re-entrant and should only be called once + * per locking thread. + * + * @return true if the lock was acquired, false it some other + * component had already locked the processor. + */ + public boolean tryLock( ); + + + /** + * Unlock the game update processor + * + *

+ * Reset the processor's lock. This method should be called from the thread which actually + * locked the processor in the first place, although that will not be checked. + * + * @throws IllegalStateException + * if the processor is not locked. + */ + public void unlock( ) + throws IllegalStateException; + + + /** + * End the previous update cycle + * + *

+ * Check if an update cycle had already started. If such is the case, execute all remaining + * update batches. + * + * @return true if an update cycle was processed or if the system is under + * maintenance, false otherwise. + */ + public boolean endPreviousCycle( ); + + + /** + * Execute a full update cycle + * + *

+ * Check for an update cycle that was already started. If one exists, finish it. Otherwise, + * start a new cycle and execute all batches in separate transactions. + * + *

+ * Update cycles will not be processed if the system is under maintenance. In addition, if the + * system enters maintenance mode while updates are being processed, the processing will stop + * after the current batch. + */ + public void executeUpdateCycle( ); + +} \ No newline at end of file diff --git a/legacyworlds-server-interfaces/src/main/java/com/deepclone/lw/interfaces/game/updates/UpdateBatchProcessor.java b/legacyworlds-server-interfaces/src/main/java/com/deepclone/lw/interfaces/game/updates/UpdateBatchProcessor.java new file mode 100644 index 0000000..a295193 --- /dev/null +++ b/legacyworlds-server-interfaces/src/main/java/com/deepclone/lw/interfaces/game/updates/UpdateBatchProcessor.java @@ -0,0 +1,42 @@ +package com.deepclone.lw.interfaces.game.updates; + + +/** + * Update batch processor interface + * + *

+ * This interface must be implemented by all components which provide support for server-side game + * updates processing. The components will be gathered automatically when the server initialises. + * + * @author E. Benoît + * + */ +public interface UpdateBatchProcessor +{ + + /** + * Obtain the component's supported update type + * + *

+ * This method must be implemented to return a string which corresponds to the name of the + * update type in the database's sys.update_types registry table. + * + * @return the update type supported by the component + */ + public String getUpdateType( ); + + + /** + * Process a batch of updates + * + *

+ * This method is called by the main update processor in the transaction during which items from + * the batch will have been marked for processing. It should process the update, but it must not + * modify the update rows in the sys.updates table. + * + * @param tickId + * the identifier of the current game update cycle + */ + public void processBatch( long tickId ); + +} diff --git a/legacyworlds-server-interfaces/src/main/java/com/deepclone/lw/interfaces/game/updates/UpdatesDAO.java b/legacyworlds-server-interfaces/src/main/java/com/deepclone/lw/interfaces/game/updates/UpdatesDAO.java new file mode 100644 index 0000000..7c48c40 --- /dev/null +++ b/legacyworlds-server-interfaces/src/main/java/com/deepclone/lw/interfaces/game/updates/UpdatesDAO.java @@ -0,0 +1,34 @@ +package com.deepclone.lw.interfaces.game.updates; + + +import com.deepclone.lw.sqld.sys.GameUpdateResult; + + + +/** + * Game updates data access + * + *

+ * This interface allows access to the data and stored procedure which constitute the game update + * sub-system. + * + * @author E. Benoît + */ +public interface UpdatesDAO +{ + + /** + * Run a batch of game updates + * + *

+ * Execute a batch of game updates for the current game update cycle. The value returned by this + * method determines whether the caller should call the method again, and if so whether it needs + * to do some in-server processing. + * + * @param tickId + * the identifier of the current game update cycle + * + * @return the results of the game update processor + */ + public GameUpdateResult processUpdates( long tickId ); +} diff --git a/legacyworlds-server-main/pom.xml b/legacyworlds-server-main/pom.xml index 30a4788..1e4af1b 100644 --- a/legacyworlds-server-main/pom.xml +++ b/legacyworlds-server-main/pom.xml @@ -50,6 +50,10 @@ legacyworlds-server-beans-system com.deepclone.lw + + legacyworlds-server-beans-updates + com.deepclone.lw + legacyworlds-server-beans-user com.deepclone.lw diff --git a/legacyworlds-server-main/src/main/resources/lw-server.xml b/legacyworlds-server-main/src/main/resources/lw-server.xml index 40e41af..cf7c8ea 100644 --- a/legacyworlds-server-main/src/main/resources/lw-server.xml +++ b/legacyworlds-server-main/src/main/resources/lw-server.xml @@ -24,4 +24,10 @@ + + + + + diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/updates/DummyBatchProcessor.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/updates/DummyBatchProcessor.java new file mode 100644 index 0000000..bc5c718 --- /dev/null +++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/updates/DummyBatchProcessor.java @@ -0,0 +1,62 @@ +package com.deepclone.lw.beans.updates; + + +import java.util.Collection; +import java.util.HashSet; + +import com.deepclone.lw.interfaces.game.updates.UpdateBatchProcessor; + + + +/** + * A dummy batch processor used in game update tests + * + * @author E. Benoît + */ +class DummyBatchProcessor + implements UpdateBatchProcessor +{ + /** The name that will be returned by {@link #getUpdateType()} */ + private final String updateType; + + /** Ticks for which the {@link #processBatch(long)} was called */ + private final HashSet< Long > ticks = new HashSet< Long >( ); + + + /** + * Set the update type + * + * @param updateType + * the update type + */ + public DummyBatchProcessor( String updateType ) + { + this.updateType = updateType; + } + + + /** + * @return whatever type name was set when the instance was created + */ + @Override + public String getUpdateType( ) + { + return this.updateType; + } + + + /** Adds the specified tick identifier to the set of ticks */ + @Override + public void processBatch( long tickId ) + { + this.ticks.add( tickId ); + } + + + /** @return the set of ticks for which {@link #processBatch(long)} was called */ + Collection< Long > getTicks( ) + { + return this.ticks; + } + +} diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/updates/MockGameUpdateProcessor.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/updates/MockGameUpdateProcessor.java new file mode 100644 index 0000000..d719661 --- /dev/null +++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/updates/MockGameUpdateProcessor.java @@ -0,0 +1,91 @@ +package com.deepclone.lw.beans.updates; + + +import com.deepclone.lw.interfaces.game.updates.GameUpdateProcessor; + + + +/** + * A mock game update processor + * + *

+ * This mock component keeps track of which of its methods have been called. It can also simulate + * runtime errors during tick processing. + * + * @author E. Benoît + * + */ +public class MockGameUpdateProcessor + implements GameUpdateProcessor +{ + private boolean tryLockCalled = false; + private boolean unlockCalled = false; + private boolean endCycleCalled = false; + private boolean executeCalled = false; + + private boolean failExecute = false; + + + public boolean wasTryLockCalled( ) + { + return this.tryLockCalled; + } + + + public boolean wasUnlockCalled( ) + { + return this.unlockCalled; + } + + + public boolean wasEndCycleCalled( ) + { + return this.endCycleCalled; + } + + + public boolean wasExecuteCalled( ) + { + return this.executeCalled; + } + + + public void setFailExecute( boolean failExecute ) + { + this.failExecute = failExecute; + } + + + @Override + public boolean tryLock( ) + { + this.tryLockCalled = true; + return true; + } + + + @Override + public void unlock( ) + { + this.unlockCalled = true; + } + + + @Override + public boolean endPreviousCycle( ) + { + this.endCycleCalled = true; + return false; + } + + + @Override + public void executeUpdateCycle( ) + { + this.executeCalled = true; + if ( this.failExecute ) { + throw new RuntimeException( "fail" ); + } + } + +} diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/updates/MockRegistry.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/updates/MockRegistry.java new file mode 100644 index 0000000..60ac8fc --- /dev/null +++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/updates/MockRegistry.java @@ -0,0 +1,41 @@ +package com.deepclone.lw.beans.updates; + + +import java.util.HashMap; + +import com.deepclone.lw.interfaces.game.updates.UpdateBatchProcessor; + + + +/** + * A mock processor registry + * + * @author E. Benoît + */ +class MockRegistry + implements ServerProcessorRegistry +{ + + /** The map of "processors" to return */ + private final HashMap< String , UpdateBatchProcessor > processors = new HashMap< String , UpdateBatchProcessor >( ); + + + /** + * Add a processor to the registry + * + * @param processor + * the processor to add + */ + public void put( UpdateBatchProcessor processor ) + { + this.processors.put( processor.getUpdateType( ) , processor ); + } + + + @Override + public UpdateBatchProcessor getProcessorFor( String type ) + { + return this.processors.get( type ); + } + +} diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/updates/MockUpdatesDAO.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/updates/MockUpdatesDAO.java new file mode 100644 index 0000000..5cf16d6 --- /dev/null +++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/updates/MockUpdatesDAO.java @@ -0,0 +1,32 @@ +package com.deepclone.lw.beans.updates; + + +import com.deepclone.lw.interfaces.game.updates.UpdatesDAO; +import com.deepclone.lw.sqld.sys.GameUpdateResult; + + + +/** + * Mock updates DAO used in tests + * + * @author E. Benoît + */ +class MockUpdatesDAO + implements UpdatesDAO +{ + /** The index to read at the next call to {@link #processUpdates(long)} */ + public int index = 0; + + /** The values to return as the update type to process */ + public String[] values = new String[] { }; + + + @Override + public GameUpdateResult processUpdates( long tickId ) + { + if ( this.index >= this.values.length ) { + return new GameUpdateResult( ); + } + return new GameUpdateResult( this.values[ this.index++ ] ); + } +} diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/updates/TestGameUpdateProcessorBean.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/updates/TestGameUpdateProcessorBean.java new file mode 100644 index 0000000..d6ff2da --- /dev/null +++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/updates/TestGameUpdateProcessorBean.java @@ -0,0 +1,240 @@ +package com.deepclone.lw.beans.updates; + + +import static org.junit.Assert.*; + +import java.sql.Timestamp; +import java.util.Date; + +import org.junit.Before; +import org.junit.Test; + +import com.deepclone.lw.interfaces.game.updates.UpdateBatchProcessor; +import com.deepclone.lw.interfaces.sys.MaintenanceData; +import com.deepclone.lw.testing.MockLogger; +import com.deepclone.lw.testing.MockSystemStatus; +import com.deepclone.lw.testing.MockTransactionManager; + + + +/** + * Tests for {@link GameUpdateProcessorBean} + * + * @author E. Benoît + */ +public class TestGameUpdateProcessorBean +{ + + /** The mock logger */ + private MockLogger logger; + + /** Mock updates access interface */ + private MockUpdatesDAO updatesDAO; + + /** Mock processor registry */ + private MockRegistry registry; + + /** Mock system status component */ + private MockSystemStatus system; + + /** The instance under test */ + private GameUpdateProcessorBean gup; + + /** + * A fake batch processor which actually enables maintenance mode. + * + * @author E. Benoît + */ + private static class MaintenanceEnabler + implements UpdateBatchProcessor + { + + /** Mock system status component */ + private MockSystemStatus system; + + + /** + * Set the mock system status component + * + * @param system + * the mock system status component + */ + MaintenanceEnabler( MockSystemStatus system ) + { + this.system = system; + } + + + @Override + public String getUpdateType( ) + { + return "maintenance"; + } + + + @Override + public void processBatch( long tickId ) + { + this.system.setMaintenance( new MaintenanceData( new Timestamp( new Date( ).getTime( ) ) , new Timestamp( + new Date( ).getTime( ) ) , "no reason" ) ); + } + + } + + + @Before + public void setUp( ) + { + this.logger = new MockLogger( ); + this.updatesDAO = new MockUpdatesDAO( ); + this.updatesDAO.values = new String[] { + null + }; + this.system = new MockSystemStatus( ); + this.registry = new MockRegistry( ); + this.registry.put( new MaintenanceEnabler( this.system ) ); + + this.gup = new GameUpdateProcessorBean( ); + this.gup.setLogger( this.logger ); + this.gup.setUpdatesDAO( this.updatesDAO ); + this.gup.setRegistry( this.registry ); + this.gup.setTransactionManager( new MockTransactionManager( ) ); + this.gup.setSystemStatus( this.system ); + } + + + /** + * Try locking the processor + */ + @Test + public void testLocking( ) + { + assertTrue( this.gup.tryLock( ) ); + assertFalse( this.gup.tryLock( ) ); + } + + + /** Try unlocking the processor */ + @Test + public void testUnlocking( ) + { + this.gup.tryLock( ); + this.gup.unlock( ); + assertTrue( this.gup.tryLock( ) ); + } + + + /** Try unlocking the processor when it's not locked */ + @Test( expected = IllegalStateException.class ) + public void testUnlockingWithNoLock( ) + { + this.gup.unlock( ); + } + + + /** + * {@link GameUpdateProcessorBean#endPreviousCycle()} returns false when there is + * no "stuck" tick + */ + @Test + public void testEndCycleNoStuckTick( ) + { + assertFalse( this.gup.endPreviousCycle( ) ); + } + + + /** + * {@link GameUpdateProcessorBean#endPreviousCycle()} returns true when maintenance + * mode is enabled, but the update is not processed + */ + @Test + public void testEndCycleMaintenance( ) + { + this.system.setCSTBehaviour( -2 ); + assertTrue( this.gup.endPreviousCycle( ) ); + assertEquals( 0 , this.updatesDAO.index ); + } + + + /** + * {@link GameUpdateProcessorBean#endPreviousCycle()} returns true when there is a + * stuck tick, and the update is processed + */ + @Test + public void testEndCycle( ) + { + this.system.setCSTBehaviour( 0 ); + assertTrue( this.gup.endPreviousCycle( ) ); + assertEquals( 1 , this.updatesDAO.index ); + } + + + /** + * {@link GameUpdateProcessorBean#executeUpdateCycle()} does not start a tick if there was a + * stuck tick, but the previous tick is processed. + */ + @Test + public void testProcessWithStuckTick( ) + { + this.system.setCSTBehaviour( 0 ); + this.gup.executeUpdateCycle( ); + assertFalse( this.system.wasStartTickCalled( ) ); + assertEquals( 1 , this.updatesDAO.index ); + } + + + /** + * {@link GameUpdateProcessorBean#executeUpdateCycle()} does not start a tick if maintenance + * mode was enabled from the start + */ + @Test + public void testProcessWithStuckTickAndMaintenance( ) + { + this.system.setCSTBehaviour( -2 ); + this.gup.executeUpdateCycle( ); + assertFalse( this.system.wasStartTickCalled( ) ); + assertEquals( 0 , this.updatesDAO.index ); + } + + + /** + * {@link GameUpdateProcessorBean#executeUpdateCycle()} does not start a tick if maintenance + * mode is enabled between the check for stuck ticks and the new tick's start. + */ + @Test + public void testProcessWithMaintenance( ) + { + this.system.setSTBehaviour( -2 ); + this.gup.executeUpdateCycle( ); + assertTrue( this.system.wasStartTickCalled( ) ); + assertEquals( 0 , this.updatesDAO.index ); + } + + + /** + * {@link GameUpdateProcessorBean#executeUpdateCycle()} throws a runtime exception if there + * seems to be a tick running when the new tick is supposed to start. + */ + @Test( expected = RuntimeException.class ) + public void testProcessWithBadState( ) + { + this.system.setSTBehaviour( -1 ); + this.gup.executeUpdateCycle( ); + } + + + /** + * {@link GameUpdateProcessorBean#executeUpdateCycle()} stops processing mid-way if maintenance + * mode becomes enabled + */ + @Test + public void testProcessWithMaintenanceEnabledMidWay( ) + { + this.updatesDAO.values = new String[] { + null , "maintenance" , null + }; + this.gup.executeUpdateCycle( ); + assertTrue( this.system.wasStartTickCalled( ) ); + assertEquals( 2 , this.updatesDAO.index ); + } +} diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/updates/TestGameUpdateTaskBean.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/updates/TestGameUpdateTaskBean.java new file mode 100644 index 0000000..1f97148 --- /dev/null +++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/updates/TestGameUpdateTaskBean.java @@ -0,0 +1,93 @@ +package com.deepclone.lw.beans.updates; + + +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; + +import com.deepclone.lw.interfaces.sys.Ticker.Frequency; +import com.deepclone.lw.testing.MockTicker; + + + +/** + * Tests for {@link GameUpdateTaskBean} + * + * @author E. Benoît + */ +public class TestGameUpdateTaskBean +{ + /** The mock ticker */ + private MockTicker ticker; + + /** The mock game update processor */ + private MockGameUpdateProcessor processor; + + /** The game update task */ + private GameUpdateTaskBean gut; + + + /** + * Initialise the mock components and the game update task instance + */ + @Before + public void setUp( ) + { + this.ticker = new MockTicker( Frequency.MINUTE , "Game update" , GameUpdateTaskBean.class ); + this.processor = new MockGameUpdateProcessor( ); + + this.gut = new GameUpdateTaskBean( ); + this.gut.setTicker( this.ticker ); + this.gut.setGameUpdateProcessor( this.processor ); + } + + + /** + * Check the component's initialisation + */ + @Test + public void testInitialisation( ) + { + this.gut.afterPropertiesSet( ); + assertTrue( this.processor.wasTryLockCalled( ) ); + assertTrue( this.processor.wasUnlockCalled( ) ); + assertTrue( this.processor.wasEndCycleCalled( ) ); + assertFalse( this.processor.wasExecuteCalled( ) ); + } + + + /** + * Check the component's run() method + */ + @Test + public void testNormalRun( ) + { + this.gut.run( ); + assertTrue( this.processor.wasTryLockCalled( ) ); + assertTrue( this.processor.wasUnlockCalled( ) ); + assertFalse( this.processor.wasEndCycleCalled( ) ); + assertTrue( this.processor.wasExecuteCalled( ) ); + } + + + /** + * Make sure the component is unlocked even if the execution fails + */ + @Test + public void testFailedRun( ) + { + this.processor.setFailExecute( true ); + try { + this.gut.run( ); + fail( "Mock processor failed to fail" ); + } catch ( RuntimeException e ) { + // EMPTY + } + assertTrue( this.processor.wasTryLockCalled( ) ); + assertTrue( this.processor.wasUnlockCalled( ) ); + assertFalse( this.processor.wasEndCycleCalled( ) ); + assertTrue( this.processor.wasExecuteCalled( ) ); + } + +} diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/updates/TestGameUpdateTransaction.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/updates/TestGameUpdateTransaction.java new file mode 100644 index 0000000..2484435 --- /dev/null +++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/updates/TestGameUpdateTransaction.java @@ -0,0 +1,86 @@ +package com.deepclone.lw.beans.updates; + + +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; + + + +/** + * Tests for {@link GameUpdateTransaction} + * + * @author E. Benoît + */ +public class TestGameUpdateTransaction +{ + + /** The mock updates DAO */ + private MockUpdatesDAO updatesDAO; + + /** The dummy batch processor used in the tests */ + private DummyBatchProcessor processor; + + /** The mock processor registry */ + private MockRegistry registry; + + /** The game update transaction used as a "guinea pig" */ + private GameUpdateTransaction transaction; + + + /** Set up the mock objects and the guinea pig transaction instance */ + @Before + public void setUp( ) + { + this.updatesDAO = new MockUpdatesDAO( ); + this.registry = new MockRegistry( ); + this.registry.put( this.processor = new DummyBatchProcessor( "test" ) ); + this.transaction = new GameUpdateTransaction( this.updatesDAO , this.registry , 1 ); + } + + + /** Test return value when the stored procedure says that no more updates are to be processed */ + @Test + public void testWhenFinished( ) + { + assertTrue( this.transaction.doInTransaction( null ) ); + } + + + /** Test return value when the stored procedure says that more updates are to be processed */ + @Test + public void testWhenHasMore( ) + { + this.updatesDAO.values = new String[] { + null + }; + assertFalse( this.transaction.doInTransaction( null ) ); + } + + + /** Test what happens when the stored procedure indicates the need for local processing */ + @Test + public void testLocalProcessing( ) + { + this.updatesDAO.values = new String[] { + "test" + }; + assertFalse( this.transaction.doInTransaction( null ) ); + assertEquals( 1 , this.processor.getTicks( ).size( ) ); + assertTrue( this.processor.getTicks( ).contains( 1L ) ); + } + + + /** + * Test what happens when the stored procedure indicates the need for local processing but the + * specified processor does not exist + */ + @Test( expected = UnsupportedUpdateException.class ) + public void testMissingProcessor( ) + { + this.updatesDAO.values = new String[] { + "missing processor" + }; + this.transaction.doInTransaction( null ); + } +} diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/updates/TestServerProcessorRegistryBean.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/updates/TestServerProcessorRegistryBean.java new file mode 100644 index 0000000..65fca67 --- /dev/null +++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/beans/updates/TestServerProcessorRegistryBean.java @@ -0,0 +1,62 @@ +package com.deepclone.lw.beans.updates; + + +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.BeanInitializationException; + + + +/** + * Tests for {@link ServerProcessorRegistryBean} + * + * @author E. Benoît + */ +public class TestServerProcessorRegistryBean +{ + /** The "guinea pig" instance */ + private ServerProcessorRegistryBean registry; + + + @Before + public void setUp( ) + { + this.registry = new ServerProcessorRegistryBean( ); + this.registry.postProcessAfterInitialization( new DummyBatchProcessor( "test" ) , "test" ); + } + + + /** The registry returns null when the type has no processor */ + @Test + public void testMissingReturnsNull( ) + { + assertNull( this.registry.getProcessorFor( "does not exist" ) ); + } + + + /** The registry returns the processor when there is one */ + @Test + public void testExistingReturnsProcessor( ) + { + assertNotNull( this.registry.getProcessorFor( "test" ) ); + } + + + /** The registry ignores objects which are not batch processors */ + @Test + public void testIgnoresOtherObjects( ) + { + this.registry.postProcessAfterInitialization( "test" , "test" ); + } + + + /** The registry crashes when two batch processors are defined for the same type */ + @Test( expected = BeanInitializationException.class ) + public void testFailsIfDuplicate( ) + { + this.registry.postProcessAfterInitialization( new DummyBatchProcessor( "test" ) , "test" ); + } + +} diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/testing/MockLogger.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/testing/MockLogger.java new file mode 100644 index 0000000..a047453 --- /dev/null +++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/testing/MockLogger.java @@ -0,0 +1,34 @@ +package com.deepclone.lw.testing; + + +import java.util.HashMap; + +import com.deepclone.lw.interfaces.eventlog.Logger; +import com.deepclone.lw.interfaces.eventlog.SystemLogger; + + + +/** + * A mock Logger component + * + * @author E. Benoît + * + */ +public class MockLogger + implements Logger +{ + /** Map of existing system loggers */ + private final HashMap< String , MockSystemLogger > loggers = new HashMap< String , MockSystemLogger >( ); + + + @Override + public SystemLogger getSystemLogger( String component ) + { + MockSystemLogger logger = this.loggers.get( component ); + if ( logger == null ) { + this.loggers.put( component , logger = new MockSystemLogger( ) ); + } + return logger; + } + +} diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/testing/MockSystemLogger.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/testing/MockSystemLogger.java new file mode 100644 index 0000000..5d15c20 --- /dev/null +++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/testing/MockSystemLogger.java @@ -0,0 +1,62 @@ +package com.deepclone.lw.testing; + + +import com.deepclone.lw.cmd.admin.logs.LogLevel; +import com.deepclone.lw.interfaces.eventlog.SystemLogger; + + + +/** + * A mock logger for components + * + * @author E. Benoît + */ +public class MockSystemLogger + implements SystemLogger +{ + /** How many messages were logged */ + private int counter = 0; + + + /** @return the amount of messages that were logged */ + public int getCounter( ) + { + return counter; + } + + + /** + * Set the message counter + * + * @param counter + * the counter's new value + */ + public void setCounter( int counter ) + { + this.counter = counter; + } + + + @Override + public SystemLogger log( LogLevel level , String message ) + { + this.counter++; + return this; + } + + + @Override + public SystemLogger log( LogLevel level , String message , Throwable exception ) + { + this.counter++; + return this; + } + + + @Override + public SystemLogger flush( ) + { + return this; + } + +} diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/testing/MockSystemStatus.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/testing/MockSystemStatus.java new file mode 100644 index 0000000..52bb8f8 --- /dev/null +++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/testing/MockSystemStatus.java @@ -0,0 +1,148 @@ +package com.deepclone.lw.testing; + + +import com.deepclone.lw.interfaces.sys.MaintenanceData; +import com.deepclone.lw.interfaces.sys.MaintenanceStatusException; +import com.deepclone.lw.interfaces.sys.SystemStatus; +import com.deepclone.lw.interfaces.sys.TickStatusException; + + + +/** + * A mock system status component + * + * @author E. Benoît + */ +public class MockSystemStatus + implements SystemStatus +{ + /** Maintenance data to return when {@link #checkMaintenance()} is called */ + private MaintenanceData maintenance = null; + + /** Value that determines the behaviour of {@link #startTick()} */ + private long stValue = 0; + + /** Value that determines the behaviour of {@link #checkStuckTick()} */ + private long cstValue = -1; + + /** Was {@link #startTick()} called? */ + private boolean startTickCalled = false; + + + /** + * Set the maintenance data to return when {@link #checkMaintenance()} is called + * + * @param maintenance + * the data to return from {@link #checkMaintenance()} + */ + public void setMaintenance( MaintenanceData maintenance ) + { + this.maintenance = maintenance; + } + + + /** + * Set the value that determines the behaviour of {@link #checkStuckTick()} + * + *

    + *
  • -2 will cause a {@link MaintenanceStatusException}, + *
  • -1 will cause it to return null, + *
  • any other value will be returned. + *
+ * + * @param cstValue + * the value + */ + public void setCSTBehaviour( long cstValue ) + { + this.cstValue = cstValue; + } + + + /** + * Set the value that determines the behaviour of {@link #startTick()} + * + *
    + *
  • -2 will cause a {@link MaintenanceStatusException}, + *
  • -1 will cause a {@link TickStatusException}, + *
  • any other value will be returned. + *
+ * + * @param cstValue + * the value + */ + public void setSTBehaviour( long stValue ) + { + this.stValue = stValue; + } + + + /** + * @return true if {@link #startTick()} was called. + */ + public boolean wasStartTickCalled( ) + { + return startTickCalled; + } + + + @Override + public MaintenanceData checkMaintenance( ) + { + return this.maintenance; + } + + + @Override + public void startMaintenance( int adminId , String reason , int duration ) + throws MaintenanceStatusException + { + // EMPTY - ignored + } + + + @Override + public void updateMaintenance( int adminId , int durationFromNow ) + throws MaintenanceStatusException + { + // EMPTY - ignored + } + + + @Override + public void endMaintenance( int adminId ) + throws MaintenanceStatusException + { + // EMPTY - ignored + } + + + @Override + public long startTick( ) + throws TickStatusException , MaintenanceStatusException + { + this.startTickCalled = true; + if ( this.stValue == -1 ) { + throw new TickStatusException( ); + } + if ( this.stValue == -2 ) { + throw new MaintenanceStatusException( ); + } + return this.stValue; + } + + + @Override + public Long checkStuckTick( ) + throws MaintenanceStatusException + { + if ( this.cstValue == -1 ) { + return null; + } + if ( this.cstValue == -2 ) { + throw new MaintenanceStatusException( ); + } + return this.cstValue; + } + +} diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/testing/MockTicker.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/testing/MockTicker.java new file mode 100644 index 0000000..6dc8fe9 --- /dev/null +++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/testing/MockTicker.java @@ -0,0 +1,82 @@ +package com.deepclone.lw.testing; + + +import com.deepclone.lw.interfaces.sys.Ticker; + +import static org.junit.Assert.*; + + + +/** + * A mock ticker component + * + *

+ * This mock component can be used to make sure that another component registers some task as + * expected. + * + * @author E. Benoît + * + */ +public class MockTicker + implements Ticker +{ + /** The task's expected frequency */ + private final Frequency expectedFrequency; + + /** The task's expected name */ + private final String expectedName; + + /** The task's expected type */ + private final Class< ? > expectedType; + + + /** + * Initialise the mock ticker's expectations + * + * @param expectedFrequency + * the task's expected frequency + * @param expectedName + * the task's expected name + * @param expectedType + * the task's expected type + */ + public MockTicker( Frequency expectedFrequency , String expectedName , Class< ? > expectedType ) + { + this.expectedFrequency = expectedFrequency; + this.expectedName = expectedName; + this.expectedType = expectedType; + } + + + @Override + public void registerTask( Frequency frequency , String name , Runnable task ) + { + assertEquals( this.expectedFrequency , frequency ); + assertEquals( this.expectedName , name ); + assertTrue( this.expectedType.isInstance( task ) ); + } + + + @Override + public void pause( ) + throws IllegalStateException + { + // EMPTY - ignored + } + + + @Override + public void unpause( ) + throws IllegalStateException + { + // EMPTY - ignored + } + + + @Override + public boolean isActive( ) + { + return true; + } + +} diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/testing/MockTransactionManager.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/testing/MockTransactionManager.java new file mode 100644 index 0000000..3b68085 --- /dev/null +++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/testing/MockTransactionManager.java @@ -0,0 +1,43 @@ +package com.deepclone.lw.testing; + + +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionStatus; + + + +/** + * A mock transaction manager + * + * @author E. Benoît + */ +public class MockTransactionManager + implements PlatformTransactionManager +{ + + @Override + public TransactionStatus getTransaction( TransactionDefinition definition ) + throws TransactionException + { + return null; + } + + + @Override + public void commit( TransactionStatus status ) + throws TransactionException + { + // EMPTY + } + + + @Override + public void rollback( TransactionStatus status ) + throws TransactionException + { + // EMPTY + } + +} diff --git a/legacyworlds/pom.xml b/legacyworlds/pom.xml index 86d3192..bd3479b 100644 --- a/legacyworlds/pom.xml +++ b/legacyworlds/pom.xml @@ -168,6 +168,11 @@ com.deepclone.lw ${legacyworlds.version.main}.${legacyworlds.version.release}-${legacyworlds.version.build} + + legacyworlds-server-beans-updates + com.deepclone.lw + ${legacyworlds.version.main}.${legacyworlds.version.release}-${legacyworlds.version.build} + legacyworlds-server-beans-user com.deepclone.lw