-
Notifications
You must be signed in to change notification settings - Fork 3
Engine Views
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.threadworker 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.
-
EngineStateErrorwhen there is no level, mostly before the first map. It subclassesRuntimeError.
Attribute names are the C field names in snake_case, and each docstring names the field it
came from. help(minqlxtended.level) lists them.
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 # unpauseminqlxtended.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 Entities74 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.BOTis 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.
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.
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 resetlevel.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.
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 onnew_game. -
Game.lockandGame.unlockdon't format a console command. They writematch_state.team_lockedthrough the game module'sMP_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_teamsstill reports it. -
Game.lock(team)raisesValueErrorfor a team this gametype doesn't have. Spectator is accepted everywhere.Game.lock()with no team locks red and blue. -
pause_calleris0both for client 0 and for the server, so readpaused_by_serverfirst.unpause_timeis0both when unpaused and during a pause with no timer, so useGame.pausedto 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.