Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/games/warcraft-3/pooled-entity-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The reference is id Software's [Quake II edict definition](https://github.com/id

## Lifetime

Each pool has `MAX_ENTITIES` slots, with slot zero reserved. This lets persistent records such as map-placed destructables cover the server's full entity capacity; sparse ability pools use only the records their active lifecycles need. Allocation zeroes the selected record. Exhaustion logs the pool name and fails through `gi.error`; it never provides a shared writable fallback. `G_FreeAncientRoot(ent)` releases that record and clears its member. `G_PoolsReleaseEdict()` releases every attached record during entity teardown. Game initialization and map replacement reset all pools and detach all edict pool pointers.
Pools have 2,048 slots (2,047 usable, slot zero reserved), except the destructable pool at 8,192: every map-placed destructable owns a persistent record, and retail campaign maps peak near 5,000 (NightElf04, UndeadX01). All other pools peak at 256 or fewer. Each game shutdown logs `WC3 pool peak <name>=N/cap` for pools that were used; the map audit logs retain it, so rerun `make audit-wc3-maps` after adding a pool or raising content density. Allocation zeroes the selected record. Exhaustion logs the pool name and fails through `gi.error`; it never provides a shared writable fallback. `G_FreeAncientRoot(ent)` releases that record and clears its member. `G_PoolsReleaseEdict()` releases every attached record during entity teardown. Game initialization and map replacement reset all pools and detach all edict pool pointers.

Construction state exists only while construction is in progress, including paused Human construction. Completion and stop release it after worker cleanup; `ent->construction != NULL` is the active-state check. There is no separate `active` flag.

Expand Down
9 changes: 6 additions & 3 deletions games/warcraft-3/game/g_local.h
Original file line number Diff line number Diff line change
Expand Up @@ -3526,10 +3526,13 @@ extern struct level_locals level;
/* Sparse lifecycles live in pools (Quake's gclient_t pattern). The edict holds
* a null pointer until the behavior starts. Movement, attacks, and health stay
* inline because almost every unit touches them. */
/* A pool must be able to represent every entity the server can allocate. In
* particular, map-placed destructables each own a persistent pool record. */
#define LIFECYCLE_POOL_CAP MAX_ENTITIES
/* Most pools peak in the hundreds across the retail campaigns (measured with
* G_PoolsReportPeaks). Map-placed destructables each own a persistent record
* and peak near 5,000 (NightElf04, UndeadX01), so that pool is sized separately. */
#define DESTRUCTABLE_POOL_CAP 8192
#define LIFECYCLE_POOL_CAP 2048
void G_PoolsReset(void);
void G_PoolsReportPeaks(void);
void G_PoolsReleaseEdict(edict_t *ent);
void G_FreeConstruction(edict_t *ent);
construction_t *G_AllocConstruction(void);
Expand Down
1 change: 1 addition & 0 deletions games/warcraft-3/game/g_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,7 @@ static void G_ShutdownGame(void) {
if (g_edicts == NULL) {
return;
}
G_PoolsReportPeaks();
G_ResetSelectionSoundState();
G_CommandErrorReset();
UI_ResetHud();
Expand Down
2 changes: 2 additions & 0 deletions games/warcraft-3/game/g_model.c
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ void G_NormalizeModelFilename(cstring_t authored, string_t out, size_t out_size)
}

int G_RegisterModel(cstring_t filename) {
/* Units with a missile weapon but no authored Missileart have no model. */
if (!filename || !filename[0]) return 0;
int index = gi.ModelIndex(filename);
if (index > 0 && index < G_MAX_MODELS && !g_models[index].filename[0])
strncpy(g_models[index].filename, filename, MAX_PATHLEN - 1);
Expand Down
161 changes: 104 additions & 57 deletions games/warcraft-3/game/g_pool.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@
#include <stdio.h>
#include <string.h>

#define POOL_CAP LIFECYCLE_POOL_CAP

#define DEFINE_POOL(member, Name, type) \
#define DEFINE_POOL(member, Name, type, POOL_CAP) \
static struct { type data; uint16_t next; bool live; } member##_slots[POOL_CAP]; \
static uint16_t member##_free; \
static bool member##_ready; \
static uint16_t member##_live, member##_peak; \
type *G_Alloc##Name(void) { \
if (!member##_ready) { \
member##_free = 1; \
Expand All @@ -23,6 +22,7 @@ type *G_Alloc##Name(void) { \
member##_free = member##_slots[i].next; \
memset(&member##_slots[i], 0, sizeof(member##_slots[i])); \
member##_slots[i].live = true; \
if (++member##_live > member##_peak) member##_peak = member##_live; \
return &member##_slots[i].data; \
} \
void G_Free##Name(edict_t *ent) { \
Expand All @@ -31,38 +31,71 @@ void G_Free##Name(edict_t *ent) { \
uint16_t i = (uint16_t)(((unsigned char *)ent->member - (unsigned char *)member##_slots) / sizeof(member##_slots[0])); \
assert(i > 0 && i < POOL_CAP && member##_slots[i].live); \
member##_slots[i].live = false; \
member##_live--; \
member##_slots[i].next = member##_free; \
member##_free = i; \
ent->member = NULL; \
}

DEFINE_POOL(construction, Construction, construction_t)
DEFINE_POOL(research, Research, research_t)
DEFINE_POOL(rally, Rally, rally_t)
DEFINE_POOL(food, Food, food_t)
DEFINE_POOL(buildwork, Buildwork, buildwork_t)
DEFINE_POOL(revival, Revival, revival_t)
DEFINE_POOL(sacrifice, Sacrifice, sacrifice_t)
DEFINE_POOL(unsummon, Unsummon, unsummon_t)
DEFINE_POOL(shadowmeld, ShadowMeld, shadowMeld_t)
DEFINE_POOL(militia, Militia, militia_t)
DEFINE_POOL(polymorph, Polymorph, polymorph_t)
DEFINE_POOL(raven, Raven, raven_t)
DEFINE_POOL(blight_growth, BlightGrowth, blightGrowth_t)
DEFINE_POOL(ensnare, Ensnare, ensnare_t)
DEFINE_POOL(ancient_root, AncientRoot, ancientRoot_t)
DEFINE_POOL(goldmine, GoldMine, goldMine_t)
DEFINE_POOL(mineoverlay, MineOverlay, mineOverlay_t)
DEFINE_POOL(acolyte_mine, AcolyteMine, acolyteMine_t)
DEFINE_POOL(item, Item, item_t)
DEFINE_POOL(destructable, Destructable, destructable_t)
DEFINE_POOL(cargo, Cargo, cargo_t)
DEFINE_POOL(stock, Stock, stock_t)
DEFINE_POOL(waygate, Waygate, waygate_t)
DEFINE_POOL(artillery, Artillery, artillery_t)
DEFINE_POOL(avatar, Avatar, avatar_t)
DEFINE_POOL(sleep, Sleep, sleep_t)
DEFINE_POOL(channel, Channel, channel_t)
DEFINE_POOL(construction, Construction, construction_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(research, Research, research_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(rally, Rally, rally_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(food, Food, food_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(buildwork, Buildwork, buildwork_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(revival, Revival, revival_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(sacrifice, Sacrifice, sacrifice_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(unsummon, Unsummon, unsummon_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(shadowmeld, ShadowMeld, shadowMeld_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(militia, Militia, militia_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(polymorph, Polymorph, polymorph_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(raven, Raven, raven_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(blight_growth, BlightGrowth, blightGrowth_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(ensnare, Ensnare, ensnare_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(ancient_root, AncientRoot, ancientRoot_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(goldmine, GoldMine, goldMine_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(mineoverlay, MineOverlay, mineOverlay_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(acolyte_mine, AcolyteMine, acolyteMine_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(item, Item, item_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(destructable, Destructable, destructable_t, DESTRUCTABLE_POOL_CAP)
DEFINE_POOL(cargo, Cargo, cargo_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(stock, Stock, stock_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(waygate, Waygate, waygate_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(artillery, Artillery, artillery_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(avatar, Avatar, avatar_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(sleep, Sleep, sleep_t, LIFECYCLE_POOL_CAP)
DEFINE_POOL(channel, Channel, channel_t, LIFECYCLE_POOL_CAP)

void G_PoolsReportPeaks(void) {
#define REPORT_POOL(member) if (member##_peak) fprintf(stderr, "WC3 pool peak %s=%u/%d\n", #member, member##_peak, (int)(sizeof(member##_slots) / sizeof(member##_slots[0])) - 1)
REPORT_POOL(construction);
REPORT_POOL(research);
REPORT_POOL(rally);
REPORT_POOL(food);
REPORT_POOL(buildwork);
REPORT_POOL(revival);
REPORT_POOL(sacrifice);
REPORT_POOL(unsummon);
REPORT_POOL(shadowmeld);
REPORT_POOL(militia);
REPORT_POOL(polymorph);
REPORT_POOL(raven);
REPORT_POOL(blight_growth);
REPORT_POOL(ensnare);
REPORT_POOL(ancient_root);
REPORT_POOL(goldmine);
REPORT_POOL(mineoverlay);
REPORT_POOL(acolyte_mine);
REPORT_POOL(item);
REPORT_POOL(destructable);
REPORT_POOL(cargo);
REPORT_POOL(stock);
REPORT_POOL(waygate);
REPORT_POOL(artillery);
REPORT_POOL(avatar);
REPORT_POOL(sleep);
REPORT_POOL(channel);
#undef REPORT_POOL
}

void G_PoolsReset(void) {
if (g_edicts) {
Expand Down Expand Up @@ -96,33 +129,33 @@ void G_PoolsReset(void) {
g_edicts[i].channel = NULL;
}
}
construction_ready = false; memset(construction_slots, 0, sizeof(construction_slots)); construction_free = 0;
research_ready = false; memset(research_slots, 0, sizeof(research_slots)); research_free = 0;
rally_ready = false; memset(rally_slots, 0, sizeof(rally_slots)); rally_free = 0;
food_ready = false; memset(food_slots, 0, sizeof(food_slots)); food_free = 0;
buildwork_ready = false; memset(buildwork_slots, 0, sizeof(buildwork_slots)); buildwork_free = 0;
revival_ready = false; memset(revival_slots, 0, sizeof(revival_slots)); revival_free = 0;
sacrifice_ready = false; memset(sacrifice_slots, 0, sizeof(sacrifice_slots)); sacrifice_free = 0;
unsummon_ready = false; memset(unsummon_slots, 0, sizeof(unsummon_slots)); unsummon_free = 0;
shadowmeld_ready = false; memset(shadowmeld_slots, 0, sizeof(shadowmeld_slots)); shadowmeld_free = 0;
militia_ready = false; memset(militia_slots, 0, sizeof(militia_slots)); militia_free = 0;
polymorph_ready = false; memset(polymorph_slots, 0, sizeof(polymorph_slots)); polymorph_free = 0;
raven_ready = false; memset(raven_slots, 0, sizeof(raven_slots)); raven_free = 0;
blight_growth_ready = false; memset(blight_growth_slots, 0, sizeof(blight_growth_slots)); blight_growth_free = 0;
ensnare_ready = false; memset(ensnare_slots, 0, sizeof(ensnare_slots)); ensnare_free = 0;
ancient_root_ready = false; memset(ancient_root_slots, 0, sizeof(ancient_root_slots)); ancient_root_free = 0;
goldmine_ready = false; memset(goldmine_slots, 0, sizeof(goldmine_slots)); goldmine_free = 0;
mineoverlay_ready = false; memset(mineoverlay_slots, 0, sizeof(mineoverlay_slots)); mineoverlay_free = 0;
acolyte_mine_ready = false; memset(acolyte_mine_slots, 0, sizeof(acolyte_mine_slots)); acolyte_mine_free = 0;
item_ready = false; memset(item_slots, 0, sizeof(item_slots)); item_free = 0;
destructable_ready = false; memset(destructable_slots, 0, sizeof(destructable_slots)); destructable_free = 0;
cargo_ready = false; memset(cargo_slots, 0, sizeof(cargo_slots)); cargo_free = 0;
stock_ready = false; memset(stock_slots, 0, sizeof(stock_slots)); stock_free = 0;
waygate_ready = false; memset(waygate_slots, 0, sizeof(waygate_slots)); waygate_free = 0;
artillery_ready = false; memset(artillery_slots, 0, sizeof(artillery_slots)); artillery_free = 0;
avatar_ready = false; memset(avatar_slots, 0, sizeof(avatar_slots)); avatar_free = 0;
sleep_ready = false; memset(sleep_slots, 0, sizeof(sleep_slots)); sleep_free = 0;
channel_ready = false; memset(channel_slots, 0, sizeof(channel_slots)); channel_free = 0;
construction_ready = false; construction_live = construction_peak = 0; memset(construction_slots, 0, sizeof(construction_slots)); construction_free = 0;
research_ready = false; research_live = research_peak = 0; memset(research_slots, 0, sizeof(research_slots)); research_free = 0;
rally_ready = false; rally_live = rally_peak = 0; memset(rally_slots, 0, sizeof(rally_slots)); rally_free = 0;
food_ready = false; food_live = food_peak = 0; memset(food_slots, 0, sizeof(food_slots)); food_free = 0;
buildwork_ready = false; buildwork_live = buildwork_peak = 0; memset(buildwork_slots, 0, sizeof(buildwork_slots)); buildwork_free = 0;
revival_ready = false; revival_live = revival_peak = 0; memset(revival_slots, 0, sizeof(revival_slots)); revival_free = 0;
sacrifice_ready = false; sacrifice_live = sacrifice_peak = 0; memset(sacrifice_slots, 0, sizeof(sacrifice_slots)); sacrifice_free = 0;
unsummon_ready = false; unsummon_live = unsummon_peak = 0; memset(unsummon_slots, 0, sizeof(unsummon_slots)); unsummon_free = 0;
shadowmeld_ready = false; shadowmeld_live = shadowmeld_peak = 0; memset(shadowmeld_slots, 0, sizeof(shadowmeld_slots)); shadowmeld_free = 0;
militia_ready = false; militia_live = militia_peak = 0; memset(militia_slots, 0, sizeof(militia_slots)); militia_free = 0;
polymorph_ready = false; polymorph_live = polymorph_peak = 0; memset(polymorph_slots, 0, sizeof(polymorph_slots)); polymorph_free = 0;
raven_ready = false; raven_live = raven_peak = 0; memset(raven_slots, 0, sizeof(raven_slots)); raven_free = 0;
blight_growth_ready = false; blight_growth_live = blight_growth_peak = 0; memset(blight_growth_slots, 0, sizeof(blight_growth_slots)); blight_growth_free = 0;
ensnare_ready = false; ensnare_live = ensnare_peak = 0; memset(ensnare_slots, 0, sizeof(ensnare_slots)); ensnare_free = 0;
ancient_root_ready = false; ancient_root_live = ancient_root_peak = 0; memset(ancient_root_slots, 0, sizeof(ancient_root_slots)); ancient_root_free = 0;
goldmine_ready = false; goldmine_live = goldmine_peak = 0; memset(goldmine_slots, 0, sizeof(goldmine_slots)); goldmine_free = 0;
mineoverlay_ready = false; mineoverlay_live = mineoverlay_peak = 0; memset(mineoverlay_slots, 0, sizeof(mineoverlay_slots)); mineoverlay_free = 0;
acolyte_mine_ready = false; acolyte_mine_live = acolyte_mine_peak = 0; memset(acolyte_mine_slots, 0, sizeof(acolyte_mine_slots)); acolyte_mine_free = 0;
item_ready = false; item_live = item_peak = 0; memset(item_slots, 0, sizeof(item_slots)); item_free = 0;
destructable_ready = false; destructable_live = destructable_peak = 0; memset(destructable_slots, 0, sizeof(destructable_slots)); destructable_free = 0;
cargo_ready = false; cargo_live = cargo_peak = 0; memset(cargo_slots, 0, sizeof(cargo_slots)); cargo_free = 0;
stock_ready = false; stock_live = stock_peak = 0; memset(stock_slots, 0, sizeof(stock_slots)); stock_free = 0;
waygate_ready = false; waygate_live = waygate_peak = 0; memset(waygate_slots, 0, sizeof(waygate_slots)); waygate_free = 0;
artillery_ready = false; artillery_live = artillery_peak = 0; memset(artillery_slots, 0, sizeof(artillery_slots)); artillery_free = 0;
avatar_ready = false; avatar_live = avatar_peak = 0; memset(avatar_slots, 0, sizeof(avatar_slots)); avatar_free = 0;
sleep_ready = false; sleep_live = sleep_peak = 0; memset(sleep_slots, 0, sizeof(sleep_slots)); sleep_free = 0;
channel_ready = false; channel_live = channel_peak = 0; memset(channel_slots, 0, sizeof(channel_slots)); channel_free = 0;
}

void G_PoolsReleaseEdict(edict_t *ent) {
Expand Down Expand Up @@ -236,4 +269,18 @@ TEST(wc3_pools, release_reuses_zeroed_owned_state) {
G_PoolsReset();
T_NULL(unit->ancient_root);
}

TEST(wc3_pools, destructable_pool_holds_more_than_the_default_cap) {
reset_entities();
edict_t *first = G_Spawn();
first->destructable = G_AllocDestructable();
FOR_LOOP(i, DESTRUCTABLE_POOL_CAP - 2) T_ASSERT(G_AllocDestructable() != NULL);
G_PoolsReset();
T_NULL(first->destructable);
}

TEST(wc3_pools, register_model_ignores_missing_names) {
T_EQ(G_RegisterModel(NULL), 0);
T_EQ(G_RegisterModel(""), 0);
}
#endif
2 changes: 1 addition & 1 deletion games/warcraft-3/game/g_save.c
Original file line number Diff line number Diff line change
Expand Up @@ -1977,7 +1977,7 @@ static bool WritePool(FILE *f, savePool_t const *pool) {

static bool ReadPool(FILE *f, savePool_t const *pool) {
uint32_t count;
if (!LoadBytes(f, &count, sizeof(count)) || count >= LIFECYCLE_POOL_CAP || count > globals.num_edicts) return false;
if (!LoadBytes(f, &count, sizeof(count)) || count >= DESTRUCTABLE_POOL_CAP || count > globals.num_edicts) return false;
FOR_LOOP(n, count) {
uint32_t index;
if (!LoadBytes(f, &index, sizeof(index)) || index >= globals.num_edicts ||
Expand Down
Loading