[TUTORIAL] How to avoid multiplayer desyncs

Nightinggale

Deity
Joined
Feb 2, 2009
Messages
5,281
I'm writing this because to my knowledge there is no documentation on how the network code works. Many modders have ruined the ability to play on a network (including online) out of ignorance. Everything is written for BTS, but there is a "chapter" for Colonization as well as it's mostly the same, but for some unknown reason with different function names.

The contents is ordered as follows:
  1. Explaining the network engine
  2. DLL features
  3. Python features
  4. Colonization

Overall design
First of all there is less of a client/server setup than first meets the eye. One person starts a game, another joins. When the third person joins, he connects to both existing players. The next player then connect to the 3 existing players and so on. This means firewalls, nat and so on can cause issues and it could matter which order people join (like A can connect to B, but B can't connect to A due to nat, meaning B should join first etc).

When a player joins, the server saves the game and transmits the savegame. This means if the mod has bugs related to savegames, they will affect network games as well.

The game tend to run as much as possible in parallel, meaning the all computers run the same code with the same input at the same time, resulting in the same output and this is mostly done without any network communication. This means the demands for a good internet connection is much lower, it also involves a not insignificant risk that the output is not the same everywhere, hence resulting in desyncs. It also means the speed is set by the slowest computer because everything calculate what the AI will do. Sometimes it could be beneficial to have a high end computer calculate all the AI actions and just transmit the results to an old and slow computer, but this isn't supported.

Synced vs unsynced code
Generally speaking, code can be executed in two modes. Synced, meaning it's executed on all computers in parallel and unsynced, which means it's executed on just one computer. An unsynced function calling a function will make that function unsynced as well. A synced function calling a function will make the called function synced as well. This is true even if both calls the same function.

The problem regarding keeping a game in sync is to maintain the same game state on all computers. Game state in this case is member variables in class instances. It could also be said like this: if it is included in savegames, it needs to be synced.

Unsynced code can access everything in read only mode. This means get functions are ok to use while set functions aren't. Assuming const is used correctly, unsynced code should use const pointers exclusively.

Synced code can use set functions. It's ok to give 50 gold to a player if it's done on all computers. That will not cause the computers to disagree on the amount of gold owned by the player.

The easiest way to identify unsynced code is to say "it's user input". Anything triggered by a mouse click or a pressed key. This event is only known to one computer.

Synced code is more or less everything else. Most noteworthy doTurn() is synced, which includes all the AI code.

Moving unsynced code to synced code
Whenever you need to write data when you are in unsynced code, the approach is to call a function in the exe, which will make the exe call a DLL function where it has copied the arguments. The call from the exe is then synced, meaning it's the job of the exe to figure out precisely how to get network commands like that to execute at the same time and same order on all computers.

Example:
Say we have a city and want to automate citizens. It's just a matter of calling CvCity::setCitizensAutomated(true). The issue is that it should be in sync and the mouse click in unsynced. What you do is call
PHP:
CvMessageControl::getInstance().sendDoTask(iCity, TASK_SET_AUTOMATED_CITIZENS, 0, 0, true, false, false, false);

This will then cause the exe to call CvCity::doTask(TaskTypes eTask, int iData1, int iData2, bool bOption, bool bAlt, bool bShift, bool bCtrl). This function then has a switch case based on eTask, which contains
PHP:
    case TASK_SET_AUTOMATED_CITIZENS:
       setCitizensAutomated(bOption);
That should make the unsynced mouse click perform a synced action.

The complete list of functions to use is listed in CvMessageControl.h
Spoiler :
PHP:
    DllExport static CvMessageControl& getInstance();
   void sendExtendedGame();
   void sendAutoMoves();
   void sendTurnComplete();
   void sendPushOrder(int iCityID, OrderTypes eOrder, int iData, bool bAlt, bool bShift, bool bCtrl);
   void sendPopOrder(int iCity, int iNum);
   DllExport void sendDoTask(int iCityID, TaskTypes eTask, int iData1, int iData2, bool bOption, bool bAlt, bool bShift, bool bCtrl);
   void sendUpdateCivics(const std::vector<CivicTypes>& aeCivics);
   void sendResearch(TechTypes eTech, int iDiscover, bool bShift);
   void sendEspionageSpendingWeightChange(TeamTypes eTargetTeam, int iChange);
   DllExport void sendAdvancedStartAction(AdvancedStartActionTypes eAction, PlayerTypes ePlayer, int iX, int iY, int iData, bool bAdd);
   void sendModNetMessage(int iData1, int iData2, int iData3, int iData4, int iData5);
   void sendConvert(ReligionTypes eReligion);
   void sendEmpireSplit(PlayerTypes ePlayer, int iAreaId);
   void sendFoundReligion(PlayerTypes ePlayer, ReligionTypes eReligion, ReligionTypes eSlotReligion);
   DllExport void sendLaunch(PlayerTypes ePlayer, VictoryTypes eVictory);
   void sendEventTriggered(PlayerTypes ePlayer, EventTypes eEvent, int iEventTriggeredId);
   DllExport void sendJoinGroup(int iUnitID, int iHeadID);
   void sendPushMission(int iUnitID, MissionTypes eMission, int iData1, int iData2, int iFlags, bool bShift);
   void sendAutoMission(int iUnitID);
   void sendDoCommand(int iUnitID, CommandTypes eCommand, int iData1, int iData2, bool bAlt);
   void sendPercentChange(CommerceTypes eCommerce, int iChange);
   void sendChangeVassal(TeamTypes eMasterTeam, bool bVassal, bool bCapitulated);
   void sendChooseElection(int iSelection, int iVoteId);
   void sendDiploVote(int iVoteId, PlayerVoteTypes eChoice);
   DllExport void sendChangeWar(TeamTypes eRivalTeam, bool bWar);
   DllExport void sendPing(int iX, int iY);
Most of them call obviously named functions, like sendDoCommand(int iUnitID calls CvUnit::doCommand

CvMessageControl and CvMessageData cpp/h files should allow making your own packages, hence customizing the variables applies to the package. Useful if you need 8 ints or something.

Python
CvEventManager.py handles some network sync.

PHP:
    def onModNetMessage(self, argsList):
       'Called whenever CyMessageControl().sendModNetMessage() is called - this is all for you modders!'
      
       iData1, iData2, iData3, iData4, iData5 = argsList
      
       print("Modder's net message!")
      
       CvUtil.pyPrint( 'onModNetMessage' )
This vanilla function makes it fairly clear how it works even with vanilla comments (a bit unusual). CyMessageControl().sendModNetMessage() is called unsynced with 5 ints as argument and then onModNetMessage is called synced with the same arguments. You can get pretty far by modding this function alone.

There are also other functions for use in networks. For instance
PHP:
CvUtil.EventEditCityName : ('EditCityName', self.__eventEditCityNameApply, self.__eventEditCityNameBegin),
__eventEditCityNameBegin is called unsynced to open a popup window where the player can write text in it. When the window is closed, __eventEditCityNameApply is called in sync. This is the approach if you want to sync strings, at least unless you want to mod message control. Naturally there is also the ability to send chat messages, but I find it unlikely that anybody will mod that part.

Random
In order to make the game stay in sync, it's important that everything, which happens is the same, even if what happens is random. Because of this, the game isn't completely random. Instead it uses what is known as predictable random (which sounds like an oxymoron). Predictable randomness is actually quite complex when the goal is to make it feel completely random. In theory one computer should make an infinite list of completely random random seeds and distribute that to the other computers. Each time a random number is used, the first seed is used and then thrown away. However due to memory issues involved in having infinite long lists, vanilla is using the second best thing. Whenever a random number is generated, it generates a new random seed based on the old seed, meaning all computers will get a new seed and they will get the same new seed. Luckily vanilla seems to have this right and we don't have to wonder why it works.

How to use this:
To get a random number in synced code, call GC.getGameINLINE().getSorenRandNum(int X) and it will return a number between 0 and (X - 1). The function is also available in python, where it's called CyGame().getSorenRandNum(int X)

It's important that this is only used in code, which is executed in sync because if one computer calls it and not the rest, they no longer agree on the random seed, which in turn can result in stuff like not even agreeing on who wins combat and the game quickly goes way out of sync. If you for some reason need a random number in unsynced code, use regular random for C++ or python

doTurn
Network traffic send during doTurn will be executed locally, but not in a network. This means adding code in doTurn, which is only executed by the local player and then transmitted will cause desyncs.

Difference between BTS and Colonization
The call to make a synced function call. You need to replace
Code:
CvMessageControl::getInstance().sendDoTask
with
Code:
gDLL->sendDoTask

CvMessageControl and CvMessageData cpp/h files are for some reason moved into the exe and can't be modded.

Vanilla functions are in CvDLLUtilityIFaceBase.h.
Spoiler :
PHP:
    virtual void sendPlayerInfo(PlayerTypes eActivePlayer) = 0;
   virtual void sendGameInfo(const CvWString& szGameName, const CvWString& szAdminPassword) = 0;
   virtual void sendPlayerOption(PlayerOptionTypes eOption, bool bValue) = 0;
   virtual void sendExtendedGame() = 0;
   virtual void sendJoinGroup(int iUnitID, int iHeadID) = 0;
   virtual void sendPushMission(int iUnitID, MissionTypes eMission, int iData1, int iData2, int iFlags, bool bShift) = 0;
   virtual void sendAutoMission(int iUnitID) = 0;
   virtual void sendDoCommand(int iUnitID, CommandTypes eCommand, int iData1, int iData2, bool bAlt) = 0;
   virtual void sendPushOrder(int iCityID, OrderTypes eOrder, int iData, bool bAlt, bool bShift, bool bCtrl) = 0;
   virtual void sendPopOrder(int iCity, int iNum) = 0;
   virtual void sendDoTask(int iCity, TaskTypes eTask, int iData1, int iData2, bool bOption, bool bAlt, bool bShift, bool bCtrl) = 0;
   virtual void sendChat(const CvWString& szChatString, ChatTargetTypes eTarget) = 0;
   virtual void sendPing(int iX, int iY) = 0;
   virtual void sendPause(int iPauseID = -1) = 0;
   virtual void sendMPRetire() = 0;
   virtual void sendToggleTradeMessage(PlayerTypes eWho, const TradeData& kTradeData, int iOtherWho, bool bAIOffer, bool bSendToAll = false) = 0;
   virtual void sendClearTableMessage(PlayerTypes eWhoTradingWith) = 0;
   virtual void sendImplementDealMessage(PlayerTypes eOtherWho, CLinkList<TradeData>* pOurList, CLinkList<TradeData>* pTheirList) = 0;
   virtual void sendChangeWar(TeamTypes iRivalTeam, bool bWar) = 0;
   virtual void sendContactCiv(NetContactTypes eContactType, PlayerTypes eWho, int iTransportId) = 0;
   virtual void sendOffer() = 0;
   virtual void sendDiploEvent(PlayerTypes eWhoTradingWith, DiploEventTypes eDiploEvent, int iData1, int iData2) = 0;
   virtual void sendRenegotiate(PlayerTypes eWhoTradingWith) = 0;
   virtual void sendRenegotiateThisItem(PlayerTypes ePlayer2, TradeableItems eItemType, int iData) = 0;
   virtual void sendExitTrade() = 0;
   virtual void sendKillDeal(int iDealID, bool bFromDiplomacy, TeamTypes eEndingTeam) = 0;
   virtual void sendUpdateCivics(CivicTypes* paeCivics) = 0;
   virtual void sendDiplomacy(PlayerTypes ePlayer, CvDiploParameters* pParams) = 0;
   virtual void sendPopup(PlayerTypes ePlayer, CvPopupInfo* pInfo) = 0;
   virtual void sendAdvancedStartAction(AdvancedStartActionTypes eAction, PlayerTypes ePlayer, int iX, int iY, int iData, bool bAdd) = 0;
   virtual void sendPlayerAction(PlayerTypes ePlayer, PlayerActionTypes eAction, int iData1, int iData2, int iData3) = 0;
 
Last edited:
Top Bottom