Removed unused parts of Godot for export

This commit is contained in:
2026-09-16 12:06:47 +02:00
parent 3668d51dd2
commit b6f8fd3504
7 changed files with 231 additions and 57 deletions
+16 -15
View File
@@ -74,7 +74,14 @@ class _MinHeap:
var parent := (index - 1) >> 1
if _keys[parent] <= _keys[index]:
break
_swap(parent, index)
# Swaps are inlined: the heap is the hottest loop in A*, and a
# GDScript call per level dominated the profile.
var key_swap := _keys[parent]
_keys[parent] = _keys[index]
_keys[index] = key_swap
var value_swap := _values[parent]
_values[parent] = _values[index]
_values[index] = value_swap
index = parent
func pop() -> Vector2i:
@@ -84,11 +91,7 @@ class _MinHeap:
_values[0] = _values[last]
_keys.resize(last)
_values.resize(last)
if not _keys.is_empty():
_sift_down(0)
return value
func _sift_down(index: int) -> void:
var index := 0
var count := _keys.size()
while true:
var smallest := index
@@ -100,13 +103,11 @@ class _MinHeap:
smallest = right
if smallest == index:
break
_swap(index, smallest)
var key_swap := _keys[index]
_keys[index] = _keys[smallest]
_keys[smallest] = key_swap
var value_swap := _values[index]
_values[index] = _values[smallest]
_values[smallest] = value_swap
index = smallest
func _swap(a: int, b: int) -> void:
var key := _keys[a]
_keys[a] = _keys[b]
_keys[b] = key
var value := _values[a]
_values[a] = _values[b]
_values[b] = value
return value
+20 -2
View File
@@ -39,6 +39,10 @@ var identity_civ: Dictionary = {}
var _configured: bool = false
var _profile_elapsed: float = 0.0
## Set when the state changed since the last broadcast. Snapshots are expensive,
## so several changes in one frame are coalesced into a single broadcast in
## _process instead of one per changed.emit().
var _state_dirty: bool = false
func _ready() -> void:
@@ -51,6 +55,9 @@ func _ready() -> void:
func _process(delta: float) -> void:
if _configured and state != null:
state.advance_movement(delta / GameClock.SECONDS_PER_HOUR)
if _state_dirty:
_state_dirty = false
_broadcast_state()
if profile_interval <= 0.0:
return
_profile_elapsed += delta
@@ -139,7 +146,7 @@ func configure_game(setup: Dictionary) -> void:
state = GameState.new()
state.map_config = map_config
state.configure(_load_civs(setup.get("civilisations", [])), int(setup.get("seed", 0)))
state.changed.connect(_broadcast_state)
state.changed.connect(_on_state_changed)
_configured = true
for peer_id in Network.get_peer_ids():
_assign_civ(int(peer_id), str(peer_names.get(int(peer_id), "")))
@@ -284,12 +291,23 @@ func _on_hour() -> void:
state.tick_hour()
func _on_state_changed() -> void:
_state_dirty = true
func _broadcast_state() -> void:
if not Network.is_server() or state == null:
return
Profiler.begin("server.broadcast_state")
# The world data is identical for everyone, so serialise it once and only
# add each viewer's explored/visible/stats on top.
var shared := state.serialize_shared()
var stats_cache := {}
for peer_id in Network.get_peer_ids():
Network.send_game_state(peer_id, state.snapshot(int(peer_civ.get(peer_id, -1))))
var civ := int(peer_civ.get(peer_id, -1))
if not stats_cache.has(civ):
stats_cache[civ] = state.viewer_stats(civ)
Network.send_game_state(peer_id, state.viewer_snapshot(shared, civ, stats_cache[civ]))
Profiler.end("server.broadcast_state")
+118 -40
View File
@@ -112,11 +112,28 @@ var _max_step_length: float = 1.0
var _next_unit_id: int = 1
var _next_city_id: int = 1
var _city_names_used: Dictionary = {}
## Id -> unit/city dictionaries, so orders and training resolve in constant time
## instead of scanning the arrays (which grow as units are trained).
var _unit_by_id: Dictionary = {}
var _city_by_id: Dictionary = {}
## City lookup keyed by tile, and the tiles adjacent to a city keyed by the
## owning civilisation. Both are derived caches rebuilt when cities change;
## they keep per-tile economy queries off the O(cities) path.
var _city_by_coords: Dictionary = {}
var _city_adjacent: Dictionary = {}
## Territory grouped by owner, so per-civ economy and visibility queries only
## walk their own tiles instead of scanning every claimed tile. Rebuilt
## alongside `territory` in _recompute_territory.
var _territory_by_civ: Dictionary = {}
## A tile's GDP per capita depends only on terrain, city placement and
## ownership, all fixed once the world is generated, so it is memoised instead
## of being recomputed on every economy query.
var _gdp_per_capita_cache: Dictionary = {}
## Serialised territory and per-viewer explored sets. Both change rarely (only
## on configure/claiming, and when a unit reveals new ground), so the wire
## arrays are built once and reused instead of rebuilt for every snapshot.
var _serialized_territory: Array = []
var _serialized_explored: Dictionary = {}
## Terrain neighbourhoods, memoised because pathfinding asks for each tile's
## neighbours dozens of times. Cleared on configure; never mutated by callers.
var _neighbour_cache: Dictionary = {}
@@ -177,9 +194,15 @@ func configure(civs: Array, world_seed: int = 0) -> void:
visible.clear()
_next_unit_id = 1
_next_city_id = 1
_unit_by_id.clear()
_city_by_id.clear()
_city_names_used.clear()
_city_by_coords.clear()
_city_adjacent.clear()
_territory_by_civ.clear()
_gdp_per_capita_cache.clear()
_serialized_territory = []
_serialized_explored.clear()
_neighbour_cache.clear()
total_hours = 0
_configured = true
@@ -377,17 +400,11 @@ func tick_hour() -> void:
func find_unit(unit_id: int) -> Dictionary:
for unit in units:
if int(unit["id"]) == unit_id:
return unit
return {}
return _unit_by_id.get(unit_id, {})
func find_city(city_id: int) -> Dictionary:
for city in cities:
if int(city["id"]) == city_id:
return city
return {}
return _city_by_id.get(city_id, {})
func unit_proto(unit: Dictionary) -> ProtoUnit:
@@ -434,14 +451,26 @@ func get_tile_info(coords: Vector2i) -> Dictionary:
func get_player_population(civ: int) -> int:
var total := 0.0
for coords in territory.keys():
if int(territory[coords]) == civ:
total += float(tile_population.get(coords, 0.0))
return roundi(total)
return int(_player_aggregates(civ)["population"])
## Population and GDP of a civilisation in a single pass over its tiles. The
## snapshot viewer stats need both, and two separate scans doubled the cost.
func _player_aggregates(civ: int) -> Dictionary:
var population := 0.0
var base_gdp := 0.0
for coords in _territory_by_civ.get(civ, []):
var pop := float(tile_population.get(coords, 0.0))
population += pop
base_gdp += pop * get_tile_gdp_per_capita(coords)
var modifiers := get_civ_modifiers(civ)
var gdp := base_gdp * (1.0 + float(modifiers.get(ProtoBuilding.EFFECT_GDP, 0.0)))
return {"population": roundi(population), "gdp": gdp}
func get_tile_gdp_per_capita(coords: Vector2i) -> float:
if _gdp_per_capita_cache.has(coords):
return _gdp_per_capita_cache[coords]
var owner := civ_at(coords)
if owner < 0:
return 0.0
@@ -451,6 +480,7 @@ func get_tile_gdp_per_capita(coords: Vector2i) -> float:
per_capita *= economy.capital_tile_gdp_multiplier if city["is_capital"] else economy.city_tile_gdp_multiplier
elif _is_next_to_city(coords, owner):
per_capita *= 1.0 + economy.city_surrounding_gdp_bonus
_gdp_per_capita_cache[coords] = per_capita
return per_capita
@@ -460,18 +490,15 @@ func get_tile_gdp(coords: Vector2i) -> float:
func get_player_gdp(civ: int) -> float:
Profiler.begin("state.player_gdp")
var total := 0.0
for coords in territory.keys():
if int(territory[coords]) == civ:
total += get_tile_gdp(coords)
var modifiers := get_civ_modifiers(civ)
total *= 1.0 + float(modifiers.get(ProtoBuilding.EFFECT_GDP, 0.0))
var total := float(_player_aggregates(civ)["gdp"])
Profiler.end("state.player_gdp")
return total
func get_player_upkeep(civ: int) -> float:
var total := get_building_upkeep(civ)
## Total upkeep per game hour. Callers that already know the civilisation's GDP
## (the economy tick and viewer stats) pass it in to avoid a second full scan.
func get_player_upkeep(civ: int, gdp: float = -1.0) -> float:
var total := get_building_upkeep(civ, gdp)
for unit in units:
if int(unit["civ"]) != civ:
continue
@@ -483,9 +510,10 @@ func get_player_upkeep(civ: int) -> float:
## Budget upkeep of every building a civilisation owns, per game hour. Upkeep
## is a fraction of GDP so it keeps pace with the economy as it grows.
func get_building_upkeep(civ: int) -> float:
func get_building_upkeep(civ: int, gdp: float = -1.0) -> float:
var total := 0.0
var gdp := get_player_gdp(civ)
if gdp < 0.0:
gdp = get_player_gdp(civ)
for city in cities:
if int(city["civ"]) != civ:
continue
@@ -691,7 +719,7 @@ func _spawn_city(site: Dictionary) -> void:
var coords: Vector2i = site["coords"]
var is_capital: bool = site["is_capital"]
var description := _city_description_for(civ, is_capital, coords)
cities.append({
var city := {
"id": _next_city_id,
"civ": civ_index,
"coords": coords,
@@ -700,7 +728,9 @@ func _spawn_city(site: Dictionary) -> void:
"population": description.population,
"improvements": description.improvements.duplicate(),
"buildings": {},
})
}
cities.append(city)
_city_by_id[_next_city_id] = city
_next_city_id += 1
@@ -744,7 +774,7 @@ func _spawn_unit(center: Vector2i, civ_index: int, proto: ProtoUnit) -> void:
if index < 0:
proto_units.append(proto)
index = proto_units.size() - 1
units.append({
var unit := {
"id": _next_unit_id,
"civ": civ_index,
"proto": index,
@@ -754,7 +784,9 @@ func _spawn_unit(center: Vector2i, civ_index: int, proto: ProtoUnit) -> void:
"path": [],
"path_index": 0,
"progress_hours": 0.0,
})
}
units.append(unit)
_unit_by_id[_next_unit_id] = unit
_next_unit_id += 1
@@ -762,6 +794,7 @@ func _spawn_unit(center: Vector2i, civ_index: int, proto: ProtoUnit) -> void:
## landmass, via a multi-source breadth-first search seeded from every city.
func _recompute_territory() -> void:
territory.clear()
_gdp_per_capita_cache.clear()
var owner_by_cell: Dictionary = {}
var frontier: Array[Vector2i] = []
for city in cities:
@@ -781,6 +814,13 @@ func _recompute_territory() -> void:
territory[neighbour] = owner
next.append(neighbour)
frontier = next
_territory_by_civ.clear()
for coords in territory.keys():
var owner := int(territory[coords])
if not _territory_by_civ.has(owner):
_territory_by_civ[owner] = []
_territory_by_civ[owner].append(coords)
_serialized_territory = _serialize_cells(territory)
func _generate_population() -> void:
@@ -828,7 +868,7 @@ func _tick_economy() -> void:
var modifiers := get_civ_modifiers(civ)
var income := economy.budget_income(gdp, hours) \
* (1.0 + float(modifiers.get(ProtoBuilding.EFFECT_BUDGET_INCOME, 0.0)))
var upkeep := get_player_upkeep(civ) * hours
var upkeep := get_player_upkeep(civ, gdp) * hours
budgets[civ] = float(budgets[civ]) + income - upkeep
@@ -867,8 +907,9 @@ func _refresh_visibility() -> void:
visible.clear()
for i in civilisations.size():
visible[i] = {}
for coords in territory.keys():
_reveal(int(territory[coords]), coords, TERRITORY_VISION)
for civ in _territory_by_civ.keys():
for coords in _territory_by_civ[civ]:
_reveal(int(civ), coords, TERRITORY_VISION)
for unit in units:
_reveal(int(unit["civ"]), unit["coords"], UNIT_VISION)
Profiler.end("state.refresh_visibility")
@@ -888,9 +929,26 @@ func _reveal(civ: int, center: Vector2i, radius: int) -> void:
explored[civ] = {}
if not visible.has(civ):
visible[civ] = {}
var explored_cells: Dictionary = explored[civ]
var visible_cells: Dictionary = visible[civ]
var discovered := false
for coords in _cells_in_radius(center, radius):
explored[civ][coords] = true
visible[civ][coords] = true
if not explored_cells.has(coords):
explored_cells[coords] = true
discovered = true
visible_cells[coords] = true
# Only new ground changes the serialised explored set; visibility is not
# cached because it changes every time a unit moves.
if discovered:
_serialized_explored.erase(civ)
## Serialised explored set for one viewer, built once and reused until a unit
## reveals a tile the civilisation had not seen before.
func _cached_explored(civ: int) -> Array:
if not _serialized_explored.has(civ):
_serialized_explored[civ] = _serialize_cells(explored.get(civ, {}))
return _serialized_explored[civ]
func _cells_in_radius(center: Vector2i, radius: int) -> Array[Vector2i]:
@@ -961,10 +1019,16 @@ func _compute_max_step_length() -> float:
## that cannot travel over RPC is included. When a viewer civilisation is given,
## its explored/visible sets are included.
func snapshot(viewer_civ: int = -1) -> Dictionary:
return viewer_snapshot(serialize_shared(), viewer_civ)
## Everything in a snapshot that is identical for every viewer. The server
## builds this once per broadcast and layers the cheap per-viewer fields on top,
## instead of reserialising the whole world for every connected peer.
func serialize_shared() -> Dictionary:
Profiler.begin("state.snapshot")
var result := {
"seed": seed,
"viewer": viewer_civ,
"total_hours": total_hours,
"civs": _serialize_civs(),
"protos": _serialize_protos(),
@@ -973,22 +1037,35 @@ func snapshot(viewer_civ: int = -1) -> Dictionary:
"technologies": _serialize_technology_defs(),
"cities": _serialize_cities(),
"units": _serialize_units(),
"territory": _serialize_cells(territory),
"territory": _serialized_territory,
"population": _serialize_population(),
"budgets": _serialize_budgets(),
"research": _serialize_civ_floats(research),
"culture": _serialize_civ_floats(culture),
"government": _serialize_civ_ints(government),
"researched": _serialize_civ_researched(researched),
"viewer_stats": _viewer_stats(viewer_civ),
"training": training.duplicate(true),
"explored": _serialize_cells(explored.get(viewer_civ, {})),
"visible": _serialize_cells(visible.get(viewer_civ, {})),
}
Profiler.end("state.snapshot")
return result
## Adds the fields that differ per viewer to a shared snapshot. `stats` may be
## supplied by the caller to reuse a viewer_stats computed for another peer on
## the same civilisation.
func viewer_snapshot(shared: Dictionary, viewer_civ: int, stats: Dictionary = {}) -> Dictionary:
var result := shared.duplicate()
result["viewer"] = viewer_civ
result["viewer_stats"] = stats if not stats.is_empty() else viewer_stats(viewer_civ)
result["explored"] = _cached_explored(viewer_civ)
result["visible"] = _serialize_cells(visible.get(viewer_civ, {}))
return result
func viewer_stats(viewer_civ: int) -> Dictionary:
return _viewer_stats(viewer_civ)
func _viewer_stats(viewer_civ: int) -> Dictionary:
Profiler.begin("state.viewer_stats")
if viewer_civ < 0:
@@ -997,14 +1074,15 @@ func _viewer_stats(viewer_civ: int) -> Dictionary:
"population": 0, "gdp": 0.0, "gdp_per_capita": 0.0, "budget": 0.0,
"upkeep": 0.0, "research": 0.0, "culture": 0.0, "government": 0,
}
var population := get_player_population(viewer_civ)
var gdp := get_player_gdp(viewer_civ)
var aggregates := _player_aggregates(viewer_civ)
var population := int(aggregates["population"])
var gdp := float(aggregates["gdp"])
var result := {
"population": population,
"gdp": gdp,
"gdp_per_capita": gdp / population if population > 0 else 0.0,
"budget": get_budget(viewer_civ),
"upkeep": get_player_upkeep(viewer_civ),
"upkeep": get_player_upkeep(viewer_civ, gdp),
"research": get_research(viewer_civ),
"culture": get_culture(viewer_civ),
"government": get_government(viewer_civ),