Vicevirtuoso
The Modetta Man
NetSyncTools
version 1
version 1
Overview
NetSyncTools is a utility which is designed to allow custom user interface elements to function properly in multiplayer without desychronizing the game.
Why It's Needed
All Lua elements run locally on each user's machine in a multiplayer game. Otherwise, every player would be seeing identical information! Many actions the player can take, such as selecting one of their cities or units, or opening the Culture Overview, have no actual effect on the game state. Thus, no information about these acts are ever sent to the other players in a multiplayer game. However, many other actions do have an effect on the game state. A player selecting a city to view has no relevance to the other players, but that player choosing the city's production does.
When actions which affect the game state are handled in Lua, specific functions are called from the Game API or the Network API. These functions will actually send game state data to the DLL, which will in turn be sent to all other players.
In mods which include a custom Lua script which is intended to change the state of the game (i.e new Civilizations' UAs), the network synchronization is most often handled by the functions in the script being tied to GameEvents. If the mod is properly coded, functions which run from GameEvents hooks will produce identical results on all machines on the network. (Therefore, this utility is not needed if your script functions are called entirely from GameEvents.)
However, new user interface elements intended to allow the player access to new actions which perform changes to the game state run into an issue. Because there is no call to a Game or Network API, the changes will only apply to the local user's machine. Lua will, essentially, think that adding new units or policies is as trivial as opening up your Military Overview screen! None of this data will be sent to the other users, causing the game state to be different for each player, which causes a desync. The problem is that there is no "generic" Game or Network API function to send custom data to the other players. Every one of these functions has a specific purpose - sometimes very specific. (SendMinorNoUnitSpawning, anyone?) Without being able to define any way to send custom data over the network, what is a modder with dreams of multiplayer compatibility to do?
Enter NetSyncTools!
How It Works
Several months ago (yes, I was very slow in deciding to actually write and test this), I discovered that there was some interesting behavior in Network.SendSellBuilding, and the associated GameEvents.CitySoldBuilding. As it turns out, as long as you have a city ID, you can send whatever arbitrary number you want through SendSellBuilding as its building ID, and CitySoldBuilding will be called with that number as its building ID -- even if that building ID doesn't even exist in the Buildings table!
Testing with some arbitrarily defined large numbers as fake "building IDs", I was successfully able to make multiple UI elements which send their game state changes through the network. Examples can be found in all of my Hyperdimension Neptunia civilizations, with double examples in Planeptune and Ultra Planeptune; the Broken Lords also have an example.
However, randomly assigning hardcoded building ID numbers and just hoping they never conflict with those of other modders is a poor solution at best.
What It Does
NetSyncTools removes the guesswork of assigning these fake building ID numbers by keeping a table of functions and their associated building ID numbers. The modder will register their function to NetSyncTools using a string key, and will call the function again using the same string key.
By default, registering a new function uses up one fake building ID number. However, you can optionally define a number of IDs to reserve for your function. If you do so, you can have your function be passed additional integer data. For example, you could reserve 22 IDs. When you call your NetSynced function, you can pass a variable between 1 and 22 to it. This could, for example, identify a specific other player for the function to use. This part may be confusing, so an example of this will be shown below.
How to Implement
NetSyncTools should be loaded in your mod as an InGameUIAddin. The script will detect multiple copies such that only one will run at a time for all mods.
How to Define and Register Your Functions
LuaEvents.AddNetSyncFunction(fFunction, sKey, iNumbersToReserve)
fFunction is the entire function to be registered to the table, sKey is the string key you wish to use, and iNumbersToReserve is the number of IDs you wish to reserve for your function to use to be able to pass integer data. If you don't need this (i.e. the button is basically just an on/off switch), you do not have to define it.
How to Call Your Functions
LuaEvents.NetSyncCall(sKey, iCity, iData)
sKey is the string key for your function registered above.
iCity is the City ID which will be passed to GameEvents.CitySoldBuilding. If not defined, will default to the capital. Only necessary if your function affects cities in some way.
iData is optional integer data passed to a function. Must have defined iNumbersToReserve in AddNetSyncFunction, and the number must be less than or equal to the value iNumbersToReserve was.
Examples
Spoiler :
Code:
--note: this was copied from one of my own mods; the HDNMod table is defined earlier in the script.
function OnHistySentToCity(iPlayer, iCity)
local pPlayer = Players[iPlayer]
local pCity = pPlayer:GetCityByID(iCity)
local sCity = CompileCityID(pCity)
if (not HDNMod.HistoireCity[iPlayer]) or (HDNMod.HistoireCity[iPlayer] and sCity ~= iCity) then
RemoveHistyFromCity(iPlayer)
DepartHisty(iCity, iPlayer)
end
end
function OnHistyRemoveFromCity(iPlayer, iCity)
if HDNMod.HistoireCity[iPlayer] then
RemoveHistyFromCity(iPlayer)
end
end
Events.LoadScreenClose.Add(function ()
LuaEvents.AddNetSyncFunction(OnHistySentToCity, "VV_NEPTUNE_HISTY", 1)
LuaEvents.AddNetSyncFunction(OnHistyRemoveFromCity, "VV_NEPTUNE_HISTY_REMOVE", 1)
end)
Example using additional integer data to kill a random unit belonging to a specific enemy player:
Spoiler :
Code:
function KillRandomUnit(iPlayer, iCity, iOtherPlayer)
--note: iCity is not used by this function, but must be defined due to how the function will be called!
if iPlayer ~= iOtherPlayer then
local pOtherPlayer = Players[iPlayer]
local tUnits = {}
for pUnit in pOtherPlayer:Units() do
tUnits[#tUnits + 1] = pUnit
end
local pChosenUnit = tUnits[Game.Rand(#tUnits, "") + 1]
if pChosenUnit then
pChosenUnit:Kill(true)
end
end
end
Events.LoadScreenClose.Add(function ()
LuaEvents.AddNetSyncFunction(KillRandomUnit, "VV_KILL_RANDOM_UNIT", 22)
end)
And to invoke this function against, for example, player 4 (assuming you have pPlayer defined as the pointer object for the active player):
LuaEvents.NetSyncCall("VV_KILL_RANDOM_UNIT", pPlayer:GetCapitalCity:GetID(), 4)
Potentially Major Drawback
SendSellBuilding doesn't work if the player sending it has no cities. Any Civ with a "stays alive without any cities" element of their UA, or the use of the Complete Kills option, will render players unable to use any NetSynced functions when they are in such a situation.