Phase 7: Windows client export, preset fix, code fixes
- Fix: export_presets.cfg — platform='Windows Desktop' (not 'Windows'), Windows as [preset.0] - Fix: plugin_manager.gd — removed class_name (autoload conflict), fixed multiline % formatting - Fix: Disabled GDExtension temporarily for clean export - Add: Windows PE32+ x86_64 client binary (109MB) at build/tactical-shooter-windows-x86_64/ - Add: tactical-shooter-windows.zip (portable zip package) - Build: Linux server binary (78MB) rebuilt
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
## GameServer — drives the 128Hz simulation loop.
|
||||
##
|
||||
## Owns a SimulationServer instance, drives its tick loop in _physics_process,
|
||||
## and manages weapon configuration. Acts as the bridge between the GDScript
|
||||
## world (NetworkManager, ServerMain) and the C++ simulation core.
|
||||
##
|
||||
## Architecture:
|
||||
## GameServer (Node, autoload candidate)
|
||||
## ├── SimulationServer (GDExtension C++) — game state + hit detection
|
||||
## │ ├── applies client input via apply_input()
|
||||
## │ └── process_compensated_fire() with lag compensation rewind
|
||||
## └── WeaponManager (optional, on the player node for client-side rate limit)
|
||||
##
|
||||
## Usage (server_main.gd):
|
||||
## func _ready():
|
||||
## var gs = GameServer.new()
|
||||
## add_child(gs)
|
||||
## gs.configure(ServerConfig.tick_rate, ServerConfig.make_movement_dict())
|
||||
##
|
||||
## Standalone / listen-server test:
|
||||
## var gs = get_node("/root/GameServer")
|
||||
## if gs: gs.start_simulation()
|
||||
##
|
||||
extends Node
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Signals
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Emitted after each simulation tick with hit results.
|
||||
signal tick_completed(tick: int)
|
||||
|
||||
## Emitted when a player is damaged (for scoreboard/UI updates).
|
||||
signal player_damaged(victim_entity_id: int, shooter_entity_id: int, damage: float, killed: bool)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
var simulation_server: Object = null
|
||||
var is_running: bool = false
|
||||
|
||||
# Map entity_id → peer_id for broadcasting damage events
|
||||
var entity_to_peer: Dictionary = {} # entity_id (int) → peer_id (int)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _ready() -> void:
|
||||
# Create the SimulationServer
|
||||
simulation_server = SimulationServer.new()
|
||||
simulation_server.set_tick_rate(128)
|
||||
|
||||
# Apply movement config from ServerConfig singleton if available
|
||||
if ServerConfig and ServerConfig.has_method(&"make_movement_dict"):
|
||||
simulation_server.set_movement_config(ServerConfig.make_movement_dict())
|
||||
|
||||
# Configure the default hitscan weapon (Assault Rifle)
|
||||
simulation_server.set_weapon_config({
|
||||
"base_damage": 30.0,
|
||||
"head_multiplier": 4.0,
|
||||
"body_multiplier": 1.0,
|
||||
"arm_multiplier": 0.75,
|
||||
"leg_multiplier": 0.6,
|
||||
"max_range": 500.0,
|
||||
"spread_degrees": 0.5,
|
||||
"fire_rate_hz": 10.0,
|
||||
})
|
||||
|
||||
# Set history depth for lag compensation (64 ticks = ~500ms at 128Hz)
|
||||
simulation_server.set_history_depth(64)
|
||||
|
||||
# Register as singleton so FPSCharacterController can find us
|
||||
Engine.register_singleton("SimulationServer", simulation_server)
|
||||
|
||||
print("[GameServer] SimulationServer created. Tick rate: %d Hz" % simulation_server.get_tick_rate())
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if simulation_server:
|
||||
simulation_server.stop()
|
||||
simulation_server = null
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Start the simulation.
|
||||
func start_simulation() -> void:
|
||||
if is_running:
|
||||
return
|
||||
if simulation_server == null:
|
||||
push_error("[GameServer] SimulationServer is null, cannot start")
|
||||
return
|
||||
|
||||
simulation_server.start()
|
||||
is_running = true
|
||||
print("[GameServer] Simulation started")
|
||||
|
||||
## Stop the simulation.
|
||||
func stop_simulation() -> void:
|
||||
if not is_running:
|
||||
return
|
||||
if simulation_server:
|
||||
simulation_server.stop()
|
||||
is_running = false
|
||||
print("[GameServer] Simulation stopped")
|
||||
|
||||
## Spawn a player entity and map to peer ID.
|
||||
## Returns the entity ID assigned by the simulation server.
|
||||
func spawn_player_entity(peer_id: int, spawn_pos: Vector3) -> int:
|
||||
if simulation_server == null:
|
||||
return -1
|
||||
|
||||
if not is_running:
|
||||
start_simulation()
|
||||
|
||||
var entity_id: int = simulation_server.spawn_entity(spawn_pos)
|
||||
if entity_id < 0 or entity_id >= 65535:
|
||||
push_error("[GameServer] Failed to spawn entity for peer %d" % peer_id)
|
||||
return -1
|
||||
|
||||
entity_to_peer[entity_id] = peer_id
|
||||
print("[GameServer] Spawned entity %d for peer %d at (%.1f, %.1f, %.1f)" % [entity_id, peer_id, spawn_pos.x, spawn_pos.y, spawn_pos.z])
|
||||
return entity_id
|
||||
|
||||
## Despawn a player entity.
|
||||
func despawn_player_entity(entity_id: int) -> void:
|
||||
if simulation_server == null:
|
||||
return
|
||||
|
||||
if entity_id >= 0:
|
||||
simulation_server.despawn_entity(entity_id)
|
||||
|
||||
# Remove from mapping
|
||||
if entity_id in entity_to_peer:
|
||||
entity_to_peer.erase(entity_id)
|
||||
|
||||
## Get the peer ID for a given entity ID.
|
||||
func get_peer_for_entity(entity_id: int) -> int:
|
||||
return entity_to_peer.get(entity_id, -1)
|
||||
|
||||
## Get the underlying SimulationServer reference.
|
||||
func get_simulation_server() -> Object:
|
||||
return simulation_server
|
||||
|
||||
## Configure weapon damage profile.
|
||||
func set_weapon(config: Dictionary) -> void:
|
||||
if simulation_server:
|
||||
simulation_server.set_weapon_config(config)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main loop (128 Hz)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if not is_running or simulation_server == null:
|
||||
return
|
||||
|
||||
# Drive the fixed-timestep simulation loop
|
||||
while simulation_server.can_tick(delta):
|
||||
var snapshot: PackedByteArray = simulation_server.tick()
|
||||
# snapshot is the serialized state — send to network layer in Phase 2
|
||||
# For now, just emit the tick completed signal
|
||||
tick_completed.emit(simulation_server.get_stats().get("tick_count", 0))
|
||||
|
||||
# Check for hit results and emit damage events
|
||||
var hit_result: Dictionary = simulation_server.get_last_hit_result()
|
||||
if hit_result.get("hit", false):
|
||||
var victim_id: int = hit_result.get("entity_id", -1)
|
||||
var damage: float = hit_result.get("damage", 0.0)
|
||||
var killed: bool = hit_result.get("killed", false)
|
||||
# For now we don't know the shooter — this will be wired in Phase 2
|
||||
player_damaged.emit(victim_id, -1, damage, killed)
|
||||
Reference in New Issue
Block a user