RIM-116 & Phalanx
Replication pitfalls
Most of what went wrong on the test server was not logic but replication: a value that differed between machines, a message that went nowhere, a reference that was not there yet.
Replicate the duration, not the deadline
A lockout that replicated an absolute server timestamp showed 184 seconds on a client instead of 15, because
GetWorldTime() is machine-local and the server's clock was minutes ahead of the client's. There is no shared
global clock to fall back on: Replication.Time() does not exist in this engine build – verified in
Core/generated/Replication/Replication.c. Replicate how long, and let each machine add it to its own
clock.
[RplProp(onRplName: "OnLockChanged")]
protected int m_iLockDuration; // replicated: how LONG, never until when
protected int m_iLockEndsLocal; // not replicated: computed per machine
protected void OnLockChanged()
{
m_iLockEndsLocal = 0;
if (m_iLockDuration > 0)
m_iLockEndsLocal = NowInSeconds() + m_iLockDuration;
Redraw();
}
A client may only send an Rpc through an entity it owns
Calling an Rpc on a component of a foreign entity fails silently. A repair action in the Game Master did nothing at
all for weeks for this reason. The way through is a modded SCR_PlayerController, which every client owns:
send the RplComponent.Id() of the target and resolve it on the server.
modded class SCR_PlayerController
{
void MyAD_AskRepair(IEntity target)
{
RplComponent rpl = RplComponent.Cast(target.FindComponent(RplComponent));
if (rpl)
Rpc(MyAD_RpcAsk_Repair, rpl.Id());
}
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
protected void MyAD_RpcAsk_Repair(RplId id)
{
IEntity target = IEntity.Cast(Replication.FindItem(id));
if (target)
MyAD_RepairNow(target); // runs on the server, where it counts
}
}
The reaction to a replicated field does not run on the authority
onRplName fires on the machines that receive a value, not on the one that wrote it. On a dedicated
server that is fine; on a listen server the host never sees its own change. Call the same update method from both the
setter and the callback.
void SetState(int state)
{
m_iState = state;
Replication.BumpMe();
OnStateChanged(); // the authority does not get the callback - call it yourself
}
protected void OnStateChanged() // also the onRplName callback on every proxy
{
Refresh();
}
A prefab's children have no parent yet at init time
References to moving parts that are children in the prefab cannot be resolved in OnPostInit:
GetParent() is still null. Resolve them lazily on first use instead.
protected IEntity Part()
{
if (!m_Part)
m_Part = GetOwner().FindChild("Part"); // not in OnPostInit - the hierarchy is not set up yet
return m_Part;
}
Random numbers: one digit at a time
A random code drawn from one big range (Math.RandomInt(10^(n-1), 10^n)) can overflow the engine's
integer: with eight digits the screen showed a negative code. A single digit never needs more than the range
0 to 9, so nothing can overflow, and the leading digit keeps the length.
string code = Math.RandomInt(1, 10).ToString(); // leading digit: never zero
for (int i = 1; i < digits; i++)
code = code + Math.RandomInt(0, 10).ToString();
Let the server keep the answer
Whatever has to stay secret – a target value, the solution to something the player has to work out – is computed and compared only on the authority. Replicate what the player needs to see, never the answer. A client that knows the solution does not need to go looking.