Files
Addison Bair 623bc8ced5 StageKit: stage and instrument any scenario on the real engine, with no retail assets
tools/stagekit writes a scenario's map in code (a floor in a box, props, players' starts) and plays it headless, a tick a frame, the same every time. Every player is logged each second and tests check what happened: walking, an obstacle, an elevator ridden up and down. 20 tests, about 2 s, CI-safe.
2026-10-16 16:26:08 +00:00

361 lines
9.4 KiB
C

/*
CACHE.C
The map file StageKit writes: a Halo Custom Edition cache (version 609, which
the port converts itself: port/linux/game/cache_file_formats.c names each
offset used here). Its tags are built where the game reads a Custom Edition
map's tags, so the game's structures' pointers are written as they are; the
structure BSP goes at the top of that room, as every map puts it.
*/
/* ---------- headers */
#include "stagekit.h"
#include <sys/mman.h>
#include "tag_schema.h" /* port/linux/game/tag_schema.h */
#include "cache_file_formats.h" /* port/linux/game/cache_file_formats.h */
/* (the game's flags ask for strict ANSI, which hides these: Linux's values) */
#ifndef MAP_ANONYMOUS
#define MAP_ANONYMOUS 0x20
#endif
#ifndef MAP_FIXED_NOREPLACE
#define MAP_FIXED_NOREPLACE 0x100000
#endif
/* ---------- constants */
enum
{
HEADER_BYTES = 0x800,
TAG_INDEX_BYTES = 0x28,
MAXIMUM_TAGS = 128,
/* a tag handle's salt: any, the same in every reference */
HANDLE_SALT = 0x0E00,
/* the room kept at the top of the tag cache for the structure BSP */
BSP_BYTES = 0x10000,
};
/* ---------- structures */
/* a cache's header (cache_files.c keeps it to itself) */
struct cache_header
{
unsigned long signature;
long version;
unsigned long file_length;
unsigned long unused0;
unsigned long tag_data_offset;
unsigned long tag_data_size;
unsigned long unused1[2];
char name[0x20];
char build[0x20];
short scenario_type;
short pad;
unsigned long checksum;
unsigned char unused2[0x794];
unsigned long footer_signature;
};
typedef char cache_header_size_check[sizeof(struct cache_header) == HEADER_BYTES ? 1 : -1];
struct tag_index
{
void *instances;
long scenario_handle;
unsigned long checksum;
long tag_count;
long vertex_count;
unsigned long vertex_data_offset;
long index_count;
unsigned long index_data_offset;
unsigned long model_data_size;
unsigned long signature;
};
struct tag_instance
{
unsigned long group;
unsigned long parent_groups[2];
long handle;
char *name;
void *address;
long in_resource_map;
long pad;
};
/* ---------- globals */
static unsigned char *tag_cache;
static unsigned long tags_used;
static unsigned long bsp_used;
static struct tag_index *tag_index;
static struct tag_instance *instances;
/* ---------- private code */
/* every reference and tag index the generator left as it was made (zero, which
names the first tag) set to none, by the groups' own schemas (tag_schema_*.c),
so the map needs no correction (tools/map_validate.c) */
static void unset_references_to_none(
struct tag_schema_definition const *definition,
unsigned char *base,
char *empty_name)
{
struct tag_schema_field const *field;
for (field = definition->fields; field->type != _tag_schema_terminator; field++)
{
short index;
for (index = 0; index < field->count; index++)
{
unsigned char *at = base + field->offset + index * field->size;
if (field->type == _tag_schema_reference)
{
struct tag_reference *reference = (struct tag_reference *)at;
unsigned long const *groups = field->definition;
if (!reference->group_tag && !reference->name && !reference->index)
{
reference->group_tag = groups ? groups[0] : NONE;
reference->name = empty_name;
reference->index = NONE;
}
}
else if (field->type == _tag_schema_tag_index && TEST_FLAG(field->flags, _tag_schema_none_bit))
{
if (!*(long *)at)
*(long *)at = NONE;
}
else if (field->type == _tag_schema_block)
{
struct tag_block *block = (struct tag_block *)at;
struct tag_schema_definition const *element = field->definition;
long element_index;
for (element_index = 0; element_index < block->count; element_index++)
unset_references_to_none(element, (unsigned char *)block->address + element_index * element->size,
empty_name);
}
else if (field->type == _tag_schema_struct)
{
unset_references_to_none(field->definition, at, empty_name);
}
}
}
}
/* each tag's references made none, where the schemas describe its group (the
structure BSP, written apart, is left as the generator made it) */
static void tags_finish(
void)
{
char *empty_name = cache_allocate(1);
long index;
for (index = 0; index < tag_index->tag_count; index++)
{
struct tag_instance const *instance = &instances[index];
struct tag_schema_group const *const *list;
for (list = tag_schema_group_lists; *list; list++)
{
struct tag_schema_group const *group;
for (group = *list; group->group_tag; group++)
{
if (group->group_tag == instance->group && instance->address)
unset_references_to_none(group->definition, instance->address, empty_name);
}
}
}
}
/* ---------- public code */
void cache_open(
void)
{
tag_cache = mmap((void *)CUSTOM_EDITION_TAG_CACHE_ADDRESS, CUSTOM_EDITION_TAG_CACHE_BYTES,
PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0);
if (tag_cache != (unsigned char *)CUSTOM_EDITION_TAG_CACHE_ADDRESS)
{
fprintf(stderr, "stagekit: the tag cache's address is taken\n");
exit(1);
}
tag_index = cache_allocate(TAG_INDEX_BYTES);
instances = cache_allocate(MAXIMUM_TAGS * sizeof(struct tag_instance));
tag_index->instances = instances;
tag_index->signature = FOURCC('t', 'a', 'g', 's');
}
void *cache_allocate(
unsigned long size)
{
void *result = tag_cache + tags_used;
tags_used = (tags_used + size + 3) & ~3UL;
if (tags_used > CUSTOM_EDITION_TAG_CACHE_BYTES - BSP_BYTES)
{
fprintf(stderr, "stagekit: the tags do not fit\n");
exit(1);
}
return result;
}
void *cache_block(
struct tag_block *block,
long count,
unsigned long element_size)
{
block->count = count;
block->address = count ? cache_allocate(count * element_size) : NULL;
return block->address;
}
void *cache_block_add(
struct tag_block *block,
unsigned long element_size)
{
unsigned char const *old = block->address;
unsigned char *grown = cache_allocate((block->count + 1) * element_size);
/* (the tags are allocated upwards: a copy, its old elements first) */
memcpy(grown, old, block->count * element_size);
block->address = grown;
block->count++;
return grown + (block->count - 1) * element_size;
}
void *cache_bsp_allocate(
unsigned long size)
{
void *result = tag_cache + CUSTOM_EDITION_TAG_CACHE_BYTES - BSP_BYTES + bsp_used;
bsp_used = (bsp_used + size + 3) & ~3UL;
if (bsp_used > BSP_BYTES)
{
fprintf(stderr, "stagekit: the structure BSP does not fit\n");
exit(1);
}
return result;
}
void *cache_bsp_block(
struct tag_block *block,
long count,
unsigned long element_size)
{
block->count = count;
block->address = count ? cache_bsp_allocate(count * element_size) : NULL;
return block->address;
}
long cache_tag_new(
unsigned long group,
unsigned long parent,
unsigned long grandparent,
char const *name,
unsigned long size,
void **data)
{
long index = tag_index->tag_count++;
struct tag_instance *instance = &instances[index];
char *name_copy = cache_allocate(strlen(name) + 1);
if (index >= (long)MAXIMUM_TAGS)
{
fprintf(stderr, "stagekit: more than %d tags\n", MAXIMUM_TAGS);
exit(1);
}
strcpy(name_copy, name);
instance->group = group;
instance->parent_groups[0] = parent;
instance->parent_groups[1] = grandparent;
instance->handle = ((HANDLE_SALT + index) << 16) | index;
instance->name = name_copy;
instance->address = *data = cache_allocate(size);
return instance->handle;
}
/* (a structure BSP's instance has no data of its own: the scenario's
reference places it) */
void cache_tag_no_data(
long handle)
{
instances[handle & 0xFFFF].address = NULL;
}
void cache_reference(
struct tag_reference *reference,
long handle)
{
struct tag_instance *instance = &instances[handle & 0xFFFF];
reference->group_tag = instance->group;
reference->name = instance->name;
reference->name_length = strlen(instance->name);
reference->index = handle;
}
void cache_no_reference(
struct tag_reference *reference,
unsigned long group)
{
reference->group_tag = group;
reference->name = cache_allocate(1);
reference->name_length = 0;
reference->index = NONE;
}
void cache_set_scenario(
long handle)
{
tag_index->scenario_handle = handle;
}
void cache_write(
char const *path,
long *tag_count)
{
struct cache_header header = { 0 };
unsigned long tag_data_offset = HEADER_BYTES + BSP_BYTES;
FILE *file;
tags_finish();
/* no models' vertices or indices: none, before the tags */
tag_index->vertex_data_offset = tag_data_offset;
tag_index->index_data_offset = 0;
tag_index->model_data_size = 0;
header.signature = FOURCC('h', 'e', 'a', 'd');
header.version = 609;
header.file_length = tag_data_offset + tags_used;
header.tag_data_offset = tag_data_offset;
header.tag_data_size = tags_used;
strcpy(header.name, "void");
/* (the port does not check a Custom Edition cache's build: StageKit's own) */
strcpy(header.build, "stagekit");
header.scenario_type = _scenario_type_solo;
header.footer_signature = FOURCC('f', 'o', 'o', 't');
file = fopen(path, "wb");
if (!file || fwrite(&header, sizeof(header), 1, file) != 1 ||
fwrite(tag_cache + CUSTOM_EDITION_TAG_CACHE_BYTES - BSP_BYTES, BSP_BYTES, 1, file) != 1 ||
fwrite(tag_cache, tags_used, 1, file) != 1 ||
fclose(file))
{
fprintf(stderr, "stagekit: could not write %s\n", path);
exit(1);
}
*tag_count = tag_index->tag_count;
}
/* where the structure BSP is in the file, how big, and where in memory */
void cache_bsp_place(
struct scenario_structure_bsp_reference *reference)
{
reference->file_offset = HEADER_BYTES;
reference->file_size = BSP_BYTES;
reference->base_address = tag_cache + CUSTOM_EDITION_TAG_CACHE_BYTES - BSP_BYTES;
}