I was definitely a bit sloppy in my original implementation of live cell info in the database. Each
cell gets pushed to the database individually, and there are more than 135k cells when all of the
production maps are running. And all of those cells are pushed to the database when the game
server starts up; it takes some time.
06/23/2023 03:15:07: {SystemGo} Drag Spin Exp 2.0.99.19
...
06/23/2023 03:19:18: {SystemGo} RoundEvent ends with round = 1. SignalTime was
06/23/2023 03:15:22, this RoundEvent took 00:03:56.4813030, remaining = -00:03:51.4813030.
Four minutes and nine seconds between the first log message and the end of the first round.
Let’s wrap all of those database calls in a single transaction; the decrease in overhead should
speed things up significantly…
06/23/2023 00:44:45: {SystemGo} Drag Spin Exp 2.0.99.19
...
06/23/2023 00:45:44: {SystemGo} RoundEvent ends with round = 1. SignalTime was
06/23/2023 00:44:59, this RoundEvent took 00:00:44.4428500, remaining = -00:00:39.4428500.
59 seconds. So that’s an easy 4x speedup. Can we do better? How many cells are we putting in the
database, anyway?
5/26/2023 1:36:47 PM: {Unknown} Round 1: Inserting 426095 live cells.
We have a lot of duplicates in the list of cells to update in the first round. Let’s only add them
to the update list if they’re not already there. Simplest way to do that?
06/23/2023 00:38:27: {SystemGo} Drag Spin Exp 2.0.99.19
...
06/23/2023 00:42:06: {SystemGo} RoundEvent ends with round = 1. SignalTime was
06/23/2023 00:41:52, this RoundEvent took 00:00:14.0122200, remaining = -00:00:09.0122200.
3 minutes 39 seconds, more than 3x slower. Seems counterintuitive. Until we look at the Cell
class, which has dozens of fields and parameters; comparisons are certainly taking a
not-insignificant amount of time.
Since we don’t need multiple copies of the same cell in the list, only the most recent update, some
sort of set would be a much better fit. It appears the HashSet isn’t available with this version
of .NET libraries, so we’ll try a Hashtable with an unused boolean value.
internalstaticintSaveLiveCell(Cellcell){intresult=0;lock(lockObjectLiveCellUpdate){if(DragonsSpineMain.GameRound!=DAL.DBWorld.roundToUpdate){try{Stringsptouse="prApp_LiveCell_Update";using(SqlConnectiontempConnection=DataAccess.GetSQLConnection()){Utils.Log("Round "+DragonsSpineMain.GameRound+": Inserting "+DAL.DBWorld.cellsToUpdate.Count+" live cells.",Utils.LogType.Unknown);tempConnection.Open();SqlTransactiontrans=tempConnection.BeginTransaction();foreach(CellcurrCellinDAL.DBWorld.cellsToUpdate.Keys){using(SqlStoredProceduresp=newSqlStoredProcedure(sptouse,tempConnection,trans)){sp.AddParameter("@lastRoundChanged",SqlDbType.Int,4,ParameterDirection.Input,DAL.DBWorld.roundToUpdate);sp.AddParameter("@facet",SqlDbType.Int,4,ParameterDirection.Input,currCell.FacetID);sp.AddParameter("@map",SqlDbType.Int,4,ParameterDirection.Input,currCell.MapID);sp.AddParameter("@xCord",SqlDbType.Int,4,ParameterDirection.Input,currCell.X);sp.AddParameter("@yCord",SqlDbType.Int,4,ParameterDirection.Input,currCell.Y);sp.AddParameter("@zCord",SqlDbType.Int,4,ParameterDirection.Input,currCell.Z);sp.AddParameter("@cellGraphic",SqlDbType.Char,2,ParameterDirection.Input,currCell.CellGraphic);sp.AddParameter("@displayGraphic",SqlDbType.Char,2,ParameterDirection.Input,currCell.DisplayGraphic);sp.ExecuteNonQuery();}}trans.Commit();DAL.DBWorld.cellsToUpdate.Clear();DAL.DBWorld.roundToUpdate=DragonsSpineMain.GameRound;}}catch(Exceptione){Utils.LogException(e);result=-1;}}if(cell!=null){cellsToUpdate[cell]=true;}returnresult;}}
06/23/2023 19:21:36: {SystemGo} Drag Spin Exp 2.0.99.19
...
06/23/2023 19:22:05: {SystemGo} RoundEvent ends with round = 1. SignalTime was
06/23/2023 19:21:50, this RoundEvent took 00:00:15.0461640, remaining = -00:00:10.0461640.
Back down to half a minute, which seems good. Looking at Microsoft’s documentation, though, I see
that
it recommends
using Dictionary instead of Hashtable. That’s an easy one-line change:
Depending on the needs of the moment (quick test? relaxed play? full-speed simulation?), we want to
be able to tweak the round timing a few different ways.
While we’re adding configuration, we might as well add something I’ve been wanting for a while: let
environment variables override the config file. That way we don’t have to touch the files within
the container to alter the behavior of the server. For now we’ll just throw it in main():
For a good amount of flexibility in round timing, it seems like we need four parameters:
The minimum round time.
The maximum round time.
Whether to wait for PC input once the minimum time has elapsed (until the maximum time has elapsed).
Whether to use the minimum or maximum time in a world running with no PCs logged in.
NPCs and effects and other system functions occur every round (or every n rounds) regardless, so
a sufficiently small minimum round time (e.g. 0) will mean iterating the rounds as quickly as they
can be processed.
So we introduce these parameters into the config file:
<appSettings><addkey="APP_VERSION"value="2.0.99.19"/><addkey="CLIENT_VERSION"value="0.0.1"/><addkey="APP_NAME"value="Drag Spin Exp"/><addkey="NEWS"value="^8/1/2015^Fork - This experimental version is a fork of version 2.0.16.5 of Dragon's Spine^^"/><addkey="APP_PROTOCOL"value="Kesmai"/><addkey="MIN_ROUND_TIME"value="0"/><addkey="MAX_ROUND_TIME"value="9000"/><addkey="WAIT_FOR_PCS"value="True"/><addkey="FAST_EMPTY_WORLD"value="False"/>
// If the round event is done; and we're past the minimum round time; and// either: a) we have a pending command from each character in the game (or// WAIT_FOR_PCS is false), or b) there are no PCs logged in and// FAST_EMPTY_WORLD is true; then skip to the next round.intwaitingForPCs=0;if(m_roundTimer.Enabled&&m_masterRoundStartTime.AddMilliseconds(MIN_ROUND_TIME)<DateTime.Now){// We've already reached the minimum time, so check to see if we have to waitif(WAIT_FOR_PCS&&Character.pcList.Count>0){// There are PCs about, and we should wait for themforeach(CharactercinCharacter.pcList){if(c.IsPC&&c.PCState==Globals.ePlayerState.PLAYING&&((c.inputCommandQueueCount()==0&&c.cmdWeight==0)||c.IsFeared||c.Stunned>0)){// Utils.Log("Waiting for PC: " + c.Name, Utils.LogType.SystemGo);waitingForPCs++;}}}if(waitingForPCs!=wasWaitingForPCs){// Log the "waiting for PCs" status, but only if it's changed.Utils.Log("Waiting for "+waitingForPCs+" PCs.",Utils.LogType.SystemGo);wasWaitingForPCs=waitingForPCs;}// If we don't have to wait or everyone's put in commands, fast forward.if((Character.pcList.Count>0||FAST_EMPTY_WORLD)&&waitingForPCs==0){Utils.Log("Removing round interval. Was: "+m_roundTimer.Interval,Utils.LogType.SystemGo);m_roundTimer.Interval=1;// let the round timer immediately fire}}System.Threading.Thread.Sleep(100);
Still feels a bit messy; but let’s see if it works well.
dog fur
~~~~~~~~~~~~
~~. . . . .
~~. v$. . .
~~. . . . .
~~. . . . .
~~. . . . .
The dog savages you with it's teeth!
You have been slain!
You are dead, you can either wait to be resurrected or rest.
->look / this is fine
R bottleHits : 0/36 Hits Taken : 36L bottleExperience : 4996 Stamina : 10
When we run the tests against the server with FAST_EMPTY_WORLD set, there’s an issue,
specifically with the delightful scenario UnderworldEnabled True, online evil low-constitution burnt corpse goes there. It seems like the (lawful) dog has time to roam around the minimal map
before the (evil) test player logs in; instead of the test going as planned, the test player is
dispatched by this local canine crimefighter.
Luckily we can just disable the dog’s spawn zone before starting the server, and our evil test player is safe:
defdisable_spawn_zones()client=connect_to_db(@server_database)result=client.execute("UPDATE [#{@server_database}].[dbo].[SpawnZone] SET [enabled] = 0")affected_rows=result.dofail"Failed to disable spawn zones!"if1!=affected_rowsaffected_rowsend
Ultimately, FAST_EMPTY_WORLD created too much variance in what happens while the test player is
logging in, so I left it turned off for testing. But it will be more useful soon.