Skip to content

Engine Views

Thomas Jones edited this page Aug 17, 2026 · 1 revision

minqlxtended.level, Entity, GameClient, Cvar, server, server_static and match_state are live views onto engine memory. Reading an attribute dereferences the struct there and then; assigning writes straight through. They are not snapshots.

Three rules cover all of them:

  • Game thread only. Reading or writing from a @minqlxtended.thread worker risks corruption and crashes. Do slow work off-thread, then hand the engine part to @minqlxtended.next_frame.
  • Almost everything is writable, and writing does whatever the game module does with that value set. You can break things.
  • EngineStateError when there is no level, mostly before the first map. It subclasses RuntimeError.

Attribute names are the C field names in snake_case, and each docstring names the field it came from. help(minqlxtended.level) lists them.

level

The game module's level_locals_t. 107 fields, plus nine on level.round.

minqlxtended.level.time                  # the level clock, milliseconds
minqlxtended.level.num_playing_clients
minqlxtended.level.team_scores[1]        # red team's score
minqlxtended.level.round.round           # which round a round-based gametype is on
minqlxtended.level.pending_vote_caller   # client id, or -1
minqlxtended.level.time_pause_begin = 0  # unpause

Entity

minqlxtended.Entity(n) over g_entities; minqlxtended.entities() walks them.

for e in minqlxtended.entities(etype=minqlxtended.EntityType.ITEM):
    print(e.number, e.classname, e.s.origin)

e = minqlxtended.Entity(512)
e.health = 100
e.nextthink = 0                   # stop it thinking
e.parent.classname                # entity references come back as Entities

74 fields on the entity, 43 on .s (the networked entityState_t, trajectories flattened to pos_* and apos_*) and 13 on .r (the server-shared entityShared_t).

.r.current_origin is where an entity actually is; .s.origin is where clients were last told it was.

Three flag families sit on three different fields, each named after its field:

Enum Field C prefix
ServerFlag .r.sv_flags SVF_*
EntityFlag .flags FL_* (god mode, notarget)
EntityEffect .s.e_flags, ps.e_flags EF_*

Mixing them up is silent. They are all ints, so entity.flags & ServerFlag.BOT is a perfectly good expression that always answers 0.

entities() is lazy and both filters run in C, so breaking out early costs nothing. Freed slots are skipped by default. G_FreeEntity doesn't clear classname, so reading one off a free slot dereferences an abandoned pointer. entities(inuse=False) gets everything.

An Entity holds the number, so it survives a map load, but identity does not: one held across a map change describes whatever occupies that slot now. Re-check inuse or classname before acting on anything held across frames.

Entity references (parent, enemy, activator, target_ent, teammaster, teamchain, the train links) read as an Entity or None, and accept either. The eight callback members (think, touch, die and the rest) read and write as raw addresses; assigning a Python callable is refused. char* members like classname and target are read-only.

Spawning and removing entities

sp = minqlxtended.spawn_points()[0]           # the five spawn classnames, as Entities
sp.s.origin = (0, 0, 128)                     # the next spawn selection sees it
sp.s.angles = (0, 90, 0)

ent = minqlxtended.spawn_entity("info_player_deathmatch", {"origin": (0, 0, 128), "angle": 90})
minqlxtended.remove_entity(ent.number)

spawn_entity(classname, keys=None) runs the engine's own spawn machinery, gametype filters and item rules included. It returns the new Entity, or None when the engine filtered the spawn out, with the reason on the console.

remove_entity(entity_id) frees a slot as G_FreeEntity does. Client slots, the world, never_free entities and already-free slots are refused. Freeing an entity others reference leaves their pointers at a zeroed slot: reads stay safe, the map logic won't be.

link_entity and unlink_entity re-enter or leave the collision and PVS grid, which a moved solid or visible entity needs before clients see it in the new position. Spawn points never need relinking. QL re-scans the spawn classnames by name on every spawn and reads only s.origin, s.angles and spawnflags.

server and server_static

The dedicated server's own state. Eleven fields on server, five on server_static.

minqlxtended.server.state == minqlxtended.ServerState.GAME
minqlxtended.server.server_id            # changes on every map load
minqlxtended.server.restarting           # a map_restart is in flight
minqlxtended.server_static.time          # milliseconds, and it does not reset

level.time restarts from zero every load. server_static.time keeps counting, so use it for anything measured across maps.

Most of both structs is deliberately not exposed: sv.configstrings (use minqlxtended.configstring(index), which is cached and stays in step with the set_configstring hook), the 336 KB PVS table, the BSP model table, svs.clients (use Player.connection) and the challenge array.

sv is derived rather than exported, so it can fail to resolve. The console says so at startup and minqlxtended.server then raises EngineStateError; server_static is unaffected.

match_state

Per-team join locks, pause and timeout state live in a cluster of game-module globals rather than in level_locals_t.

minqlxtended.match_state.team_locked      # by Team.index
minqlxtended.match_state.unpause_time     # level time the timeout ends; 0 if indefinite
minqlxtended.match_state.timeouts_used    # by Team.index

game.is_team_locked(Team.RED)             # -> bool
game.locked_teams                         # -> (Team.RED,)
  • Nothing in the block is cleared between maps. A lock and a running timeout both outlive a map change and a map_restart. Don't reset lock state on new_game.
  • Game.lock and Game.unlock don't format a console command. They write match_state.team_locked through the game module's MP_LockOrUnlockTeam, so clients are still told and a no-op stays silent.
  • Only red and blue gate joining. A lock on free or spectator is recorded and inert, and locked_teams still reports it.
  • Game.lock(team) raises ValueError for a team this gametype doesn't have. Spectator is accepted everywhere. Game.lock() with no team locks red and blue.
  • pause_caller is 0 both for client 0 and for the server, so read paused_by_server first. unpause_time is 0 both when unpaused and during a pause with no timer, so use Game.paused to tell those apart.

This rests on a byte pattern the build is allowed to miss. If it does, reading raises EngineStateError and the server otherwise runs normally. See Internals.

Clone this wiki locally