NPC Action Model
Overview
In Gothic 2 Online, remote NPCs are synchronized between the server and clients using an action-based model.
Instead of simulating the full NPC logic on the server, the server delegates execution of actions (such as animations, attacks etc.) to clients.
This approach was chosen because the server cannot fully emulate the Gothic game’s internal logic — many native game systems only exist on the client side.
To handle this, G2O uses a deterministic action queue processed on clients, while maintaining server authority through the host feedback mechanism.
How It Works
Each Remote NPC on the server maintains an action queue.
When a scripter uses functions like playAni, npcAttackMelee etc. the corresponding actions are enqueued.
These actions are then:
- Batched and sent to all clients currently streaming the NPC (every X milliseconds, for optimization).
- Executed client-side in the exact order they were queued.
- Validated by the host player, who sends the results of each action back to the server.
- Once the server receives the confirmation, the action is removed (popped) from the queue.
Host Player Assignment
Each remote NPC must have one host player, responsible for simulating its logic and reporting results to the server.
When determining which client becomes the host, the server uses the nearest player selection algorithm:
Host Selection Logic
function findHost(npc, streamedPlayers):
npcPosition = npc.getPosition()
nearestPlayer = null
nearestDistance = Infinity
for playerID in streamedPlayers:
player = getPlayerByID(playerID)
distance = npcPosition.distanceTo(player.getPosition())
if nearestPlayer == null or distance < nearestDistance:
nearestPlayer = player
nearestDistance = distance
return nearestPlayer
💡 In future versions, host selection may also consider the number of NPCs currently synchronized by each player.
Determinism and Action Execution
All NPC actions must be deterministic — meaning they must produce the exact same behavior and visual outcome across all clients.
This ensures consistent world state and prevents desynchronization between players.
Actions are executed as follows:
- The server queues the action.
- The clients process it in order.
- The host player reports back the final state to the server.
- The server updates the NPC’s authoritative state.
Custom NPC Actions
Scripters can define their own actions by extending the NpcAction class.
Each custom action must be registered in the same order on both server and client, as IDs are automatically assigned in sequence.
Example
Client-side:
// For more information check: https://docs.gothic-online.com/0.3.4/script-reference/client-classes/npc/NpcAction/
class CustomAniAction extends NpcAction {
animation = null
constructor(animation) {
this.animation = animation
}
function onInit(npc_id) {
return true
}
function onUpdate(npc_id) {
playAni(npc_id, this.animation)
return true
}
function getName() {
return "CustomAniAction"
}
function onDeserialize(packet) {
return this(packet.readString())
}
}
Server-side:
// For more information check: https://docs.gothic-online.com/0.3.4/script-reference/server-classes/npc/NpcAction/
class CustomAniAction extends NpcAction {
animation = null
constructor(animation) {
this.animation = animation
}
function onSerialize(packet) {
packet.writeString(this.animation)
}
}
Known Issues and Best Practices
🧩 Stacked Action Queues
If actions are queued for NPCs not currently hosted by any player, they cannot be processed immediately.
These actions remain in the queue until a player enters the NPC’s streaming range — at which point all queued actions are executed at once.
This can result in:
- Thousands of actions piling up.
- Noticeable lag or glitching when the NPC becomes active again.
Solution:
Always check whether an NPC is currently hosted before queuing actions.
Use getNpcHostPlayer to determine if an NPC has an assigned host:
local host = getNpcHostPlayer(npc);
if (host) {
// Safe to queue action
playAni(npc, "T_STAND_2_SIT");
}