Skip to content

Server-authoritative dungeon weapon combat

Status

Dungeon weapon fire over ENet uses a server-authoritative decision path. The client predicts only presentation and ammunition UI. The headless server owns fire acceptance, ammunition, reloads, weapon selection, deterministic shot seeds, target selection, damage, health, kills, replay events, and combat statistics.

Offline play keeps the existing local AdvancedWeaponController path. No ENet peer means no combat session and no behavior change. Developer-seed runs always use local development combat so weapon damage and the existing local enemy attack model remain available while server-side enemy attacks are incomplete; non-developer online runs remain fail-closed.

Ownership and flow

  1. The client generates the V3 dungeon locally and sends DungeonCombatStartRequest with its run id, dungeon hash, configuration, requested mode, and loadout.
  2. The server binds the request to the sending ENet peer and the peer's registered hub player id.
  3. The server independently regenerates the V3 dungeon and rejects mismatched run ids or hashes.
  4. The server validates the loadout against its weapon, modification, perk, unlock, and run-rule catalogs. It calculates both weapon slots and assigns stable instance ids (slot-1:<weapon-id> and slot-2:<weapon-id>).
  5. The server returns a random combat session id and the initial authoritative snapshot.
  6. On input, the client predicts muzzle/audio/tracer hooks and local ammo UI, then sends intent only.
  7. The server validates and resolves the shot with AuthoritativeDungeonCombatSession and the shared Pulse, Shotgun, Rail, or Arc behavior.
  8. A reliable result carries accepted/rejected state, authoritative seed, ammo, hits, remaining health, kills, chain order, and penetrations. Periodic snapshots reconcile ammo, active slot, enemy transforms, health, and alive state.

DungeonWeaponFireRequest deliberately has no damage, hit list, critical flag, target id, or random seed. Client ticks are diagnostic only; rate limiting uses the server tick and server time.

Transport contracts

  • Session: DungeonCombatStartRequest / DungeonCombatStartResult
  • Fire: DungeonWeaponFireRequest / DungeonWeaponFireResult
  • Reload: DungeonReloadRequest / DungeonReloadResult
  • Switch: DungeonWeaponSwitchRequest / DungeonWeaponSwitchResult
  • Movement intent: DungeonCombatPoseRequest / DungeonCombatPoseResult
  • Reconciliation: DungeonCombatSnapshot, WeaponAmmoSnapshot, and AuthoritativeCombatEntitySnapshot

Fire, reload, switch, session setup, and their results use reliable ENet RPCs. Pose updates and periodic snapshots use unreliable RPCs because later state supersedes older state.

Server validation

The pure shared combat session checks:

  • server-issued combat session, deterministic run id, peer-bound player id;
  • active run plus alive/not-stunned player state;
  • active weapon id, stable weapon instance id, validated two-slot loadout, server-resolved mods/perks/rules;
  • non-empty and non-repeated shot id, exact next shot/ammo sequence;
  • server fire rate, magazine state, reload state, reserve ammo;
  • finite normalized aim, authoritative origin tolerance, range and faction;
  • canonical-grid walkability for player poses and line of sight through blocking cells;
  • known, alive, hostile entities and server-owned transforms/health;
  • deterministic shotgun spread/pellets, rail penetration ordering, and arc primary aim plus stable chain selection.

Rejected requests return the authoritative ammo sequence/state so prediction can be corrected. Accepted damage is grouped per entity before health is changed, preventing repeated engine callbacks for multi-pellet hits.

Replay and statistics

AuthoritativeDungeonCombatSession.Replay stores a bounded sequence of accepted/rejected fire, hit, kill, reload, and switch events. Statistics exposes accepted shots, rejected shots, resolved hits, authoritative damage, and kills. These records are produced on the server decision path; the client ShotRecorded event is emitted only after an accepted authoritative response when online.

The in-memory buffer is intentionally bounded at 4096 events. Durable replay upload and leaderboard verification can consume these events later; they are not persisted by the ENet adapter yet.

Runtime integration

  • DungeonCombatNetworkSync is an autoload on clients and the headless server.
  • AppBootstrap and ServerBootstrap inject shared services into it.
  • DungeonRun3DController starts a combat session after V3 generation and player/loadout setup.
  • AdvancedWeaponController chooses the authoritative path only while a combat session is active.
  • DungeonEnemy3D registers by stable EnemyInstanceId, pauses local AI online, and reconciles transform/health/death from snapshots. Offline AI and damage remain unchanged.
  • The headless shared session advances simple server-owned enemy movement over walkable canonical cells. It does not instantiate visual scenes or simulate visual projectiles.

Security boundary and current limitations

Combat messages are bound to the sending peer and a random server-issued combat session id. This closes client authority over hits and damage, but it does not make the existing ENet login cryptographically authenticated: HubNetworkSync currently registers the user id supplied by the client. Production deployment still needs Nakama session-token verification (or a signed one-time ENet join ticket) before hub registration. Until then, a client can impersonate another player id even though it cannot forge combat damage.

The server currently has only the new-player progression snapshot. Official normalized rules are supported end to end. Fun/community requests are currently validated only against the server-created new-player snapshot. Account-specific unlocks or mastery are therefore rejected until authoritative progression is loaded server-side; the client must never be used as the source for that snapshot.

Enemy movement is deliberately minimal server simulation rather than the full Godot navigation/encounter AI. Weapon hit and health authority is complete, but enemy attacks, encounter activation, door-aware navigation, player health, status-effect application, durable replay persistence, reconnect/resume, and multi-player dungeon parties remain follow-up server systems.

Reject codes and debugging

Common session rejects are peer_not_registered, unsupported_generation_mode, run_id_mismatch, dungeon_hash_mismatch, unknown_run_mode, unknown_weapon_id, and loadout validation failures. Fire rejects are invalid_combat_session, invalid_run, invalid_player, duplicate_shot_id, run_not_active, player_dead, player_stunned, inactive_weapon, invalid_weapon_instance, invalid_ammo_sequence, weapon_reloading, magazine_empty, invalid_aim, invalid_fire_origin, fire_rate_exceeded, and fire_timing_rejected. Pose rejects include stale_pose, invalid_pose, movement_exceeded, and position_not_walkable; reload/switch can additionally return reload_not_available or invalid_weapon_slot.

For debugging, correlate the client ShotId with the server replay event and ServerTick. First confirm session acceptance and equal dungeon hashes, then compare stable weapon instance id and ammo sequence. A rejection is normal protocol output, not an exception; the response or next snapshot restores authoritative ammo. Malformed JSON is logged with the peer id and ignored without changing combat state.

Manual ENet smoke test

  1. Start a headless server with the documented server-mode arguments.
  2. Connect clients A and B, enter the hub with both so each peer is registered, then start the same V3 seed on both clients. Each currently receives its own single-player combat session.
  3. Confirm two server log entries for Authoritative dungeon combat started, each with its peer, run, and entity count.
  4. On A, fire each weapon and verify muzzle presentation is immediate while enemy health/death changes after the server response. Observe on B that its separate session state is unaffected.
  5. Hold Pulse fire, reload, switch during reload, empty a magazine, and verify HUD ammo converges to snapshots.
  6. Put a wall between player and target and verify the shot is accepted but produces no hit.
  7. Replay the same fire payload or modify session/run/player/instance/sequence/origin/aim in a diagnostic client and verify a rejection code plus no health change.
  8. Disconnect A while B remains connected; verify A's session is removed and B continues. Then disconnect B and verify its session is removed.

Automated coverage is in AuthoritativeDungeonCombatTests and covers request authority, forged scope, duplicate shots, server rate limiting independent of client tick, ammo/reload/state checks, line of sight, movement/switch reconciliation, and all four weapon archetypes.