Skip to content

Upgrading

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

v1.0.0 is not backwards compatible with minqlx, or with minqlxtended before v1.0.0. The plugins repository is already ported and is the best reference if something here isn't clear.

Events

Changed signatures

These six come out of the game module now, so there is no stats dictionary behind them.

Event Was Now
game_start (data) ()
game_end (data) (aborted)
round_end (data) (round_number, winning_team, time)
kill (victim, killer, data) (victim, killer, mod)
death (victim, killer, data) (victim, killer, mod)
chat (player, msg, channel) (player, msg, channel, recipient)
team_switch_attempt (player, old_team, new_team) (player, old_team, new_team, target)
userinfo (player, changed) (player, changed, infostring)
player_connect (player) (player, is_bot)

round_end's winning_team is a team name or None for a draw, and time is the round's duration in milliseconds, where data["TIME"] was elapsed match time in seconds.

mod keeps the vocabulary data["MOD"] used, so data["MOD"] == "GAUNTLET" becomes mod == "GAUNTLET". For anything else the old dictionary carried, such as scores, rosters and per-player totals, read Game() and Player.expanded_stats.

chat's recipient is the player who was told, None for anything said openly. Test recipient is not None to skip tells. Messages are no longer quote-stripped.

userinfo: a dict returned by a handler is merged with what earlier handlers returned rather than replacing it.

Use player_connect's is_bot flag. player.is_bot reports every bot as human at connect time, and is correct everywhere else.

Behaviour changes

  • kill no longer fires for suicides or world deaths. death always fires; kill only when another player was responsible.
  • Changing team no longer raises death. Use team_switch.
  • team_switch_attempt fires for admin puts, duel-queue promotion, follow-cycling and level exit. Cancelling it to enforce a team lock now blocks admins too. new_team is only what was asked for; use team_switch for what happened.
  • vote_called fires while another vote is running, and for votes the engine rejects. Hook vote_started for the votes that run.
  • vote_ended isn't locale-dependent, and fires for votes abandoned by intermission or a level change, reported as not passed.
  • vote fires whether or not a vote is running.
  • Cancel with Return.STOP_EVENT or Return.STOP_ALL, never a bare False.
  • A handler whose signature doesn't fit its event is refused at registration, not at first dispatch. *args handlers and ones whose extra parameters all have defaults are left alone.
  • @next_frame tasks all run at the start of the next frame, before any @delay callbacks due in the same frame.
  • PlayerDisonnectDispatcher is spelled PlayerDisconnectDispatcher. The event name is unchanged.
  • VoteStartedDispatcher.caller() is gone. Plugin.callvote(vote, display, caller=player) is unchanged.

Constants

Every family is an enum. They are IntEnum or IntFlag, so arithmetic and | still work.

Was Now
WP_RAILGUN Weapon.RAILGUN
PRI_HIGH Priority.HIGH
MOD_ROCKET Mod.ROCKET
ET_ITEM EntityType.ITEM
SS_GAME ServerState.GAME
CVAR_ARCHIVE CvarFlag.ARCHIVE
SVF_BOT ServerFlag.BOT
DAMAGE_RADIUS DamageFlag.RADIUS
TR_LINEAR TrajectoryType.LINEAR
MOVER_POS1 MoverState.POS1
SAY_TEAM SayMode.TEAM
MODELINDEX_QUADDAMAGE ModelIndex.QUADDAMAGE
RET_STOP_ALL Return.STOP_ALL
TEAM_RED Team.RED.index
PRIV_ADMIN Privilege.ADMIN.level
CS_ACTIVE ConnectionState.ACTIVE.index

The configstring-index CS_* constants stay plain ints with their names: CS_* names two different families, and CS_FREE and CS_SERVERINFO are both 0, so no one enum can hold it.

Return is not an IntEnum, so False, True, 0 and 1 match no member.

The lookup tables are gone

TEAMS, WEAPONS, GAMETYPES, GAMETYPES_SHORT, ITEMS, HOLDABLES, OBJECTIVES, ROUND_STATES, CONNECTION_STATES, MEANS_OF_DEATH, TEAM_BASED_GAMETYPES and NONTEAM_BASED_GAMETYPES each named a number the member already knows.

minqlxtended.Team.from_index(n)             # was TEAMS[n]
list(minqlxtended.Team)                     # was TEAMS.values()
minqlxtended.Gametype.from_index(n).title   # was GAMETYPES[n]
minqlxtended.Weapon(n).short                # was WEAPONS[n]
minqlxtended.ModelIndex.QUADDAMAGE          # was ITEMS["quad"]
game.is_team_based                          # was game.type_short in TEAM_BASED_GAMETYPES

from_index() is on Team, Gametype, MeansOfDeath, Objective, RoundState and ConnectionState. Privilege.from_level() and Weapon.from_short() are the same idea where the key isn't an index. All raise ValueError naming the family and listing what it has.

Matching is exact

Team("RED"), GameState("WARMUP") and Game.lock("Red") raise. The string-valued properties compare and format exactly as the strings they replaced, so player.team == "spectator" still works.

  • player.privileges answers Privilege.NONE where it answered None. Test == Privilege.NONE. Don't write if player.privileges:, since every member is a non-empty string and so always truthy. The setter still takes None to clear.
  • weapon_fired hands you a Weapon member. weapon == "rl" silently never matches. Compare against Weapon.ROCKET_LAUNCHER, or ask the member for .short.

Player

Eight get-or-set methods are properties

position, velocity, weapons, ammo, powerups, keys, flight and weapon. What they hand back has _replace, like a namedtuple.

player.weapons = player.weapons._replace(rl=True)     # was player.weapons(rl=True)
player.position = player.position._replace(z=100)     # was player.position(z=100)
player.position = (0, 0, 100)                         # a plain triple works too
player.weapon = minqlxtended.Weapon.ROCKET_LAUNCHER

reset=True becomes NO_WEAPONS, NO_AMMO, NO_POWERUPS and NO_KEYS, so "only a rocket launcher" is NO_WEAPONS._replace(rl=True).

player.powerups is milliseconds in both directions. Where the old method took quad=30, write ._replace(quad=30 * 1000).

Ten per-client calls moved onto Player

send_server_command, client_command, play_sound, play_music, stop_sound, stop_music, configstring_variables, update_configstring, send_configstring and send_configstring_overrides. So player.play_sound(p) instead of self.play_sound(p, player).

player.configstring is the CS_PLAYERS entry describing that player; player.send_configstring sends them any index. update_configstring writes through the server's table so everyone sees it, where the send_* pair reaches one client and is overwritten by the next server-side write.

Other

  • Reading or writing state on an empty slot raises EngineStateError, not AttributeError. Player.state itself still answers None.
  • Player.update() clears the cached userinfo, so ip, country and model stop reporting pre-update values.
  • Player.clan's setter is dirty-checked. If you wrote player.clan = player.clan to force the set_configstring dispatchers, use minqlxtended.set_configstring(minqlxtended.CS_PLAYERS + player.id, player.configstring).
  • Player.qport reads the netchan rather than the userinfo key.
  • Player.ip, ping, is_bot and qport no longer fall back to the worse source.
  • player.holdable warns instead of raising for one it can't name, and reports None.
  • Player and the channels are hashable, so set(players) works.

Plugin and Game

The admin commands are on Game

All twenty-five, with thirteen also on Player. self.lock("red") becomes self.game.lock("red"), self.tempban(player) becomes player.tempban(), and self.slap(player, 50) is self.game.slap(player, 50) or player.slap(50).

Twenty-two engine functions are gone

Every field they wrote is an accessor on Entity, GameClient or level.

player.health = 100          # was set_health(player.id, 100)
player.god = True            # was god(player.id, True)
game.is_training_map = True  # was allow_single_player(True)

minqlxtended.GameClient(n).ps.speed = 10000    # was set_speed(n, 10000)
minqlxtended.GameClient(n).ps.stats[minqlxtended.StatIndex.ARMOR] = 200

Gone: set_stats, set_position, set_velocity, noclip, god, notarget, set_flags, set_health, set_armor, set_speed, set_gravity, set_weapons, set_weapon, set_ammo, set_powerups, set_holdable, set_flight, set_keys, set_invulnerability, set_score, set_privileges, allow_single_player.

Still there, because an accessor can't express them: drop_holdable spawns an entity, player_spawn calls ClientSpawn, slay_with_mod calls G_Damage. player_state() and player_stats() stay as digests spanning three structs in one call, as do the cvar, configstring, demo and client-command families.

EntityFlag has been renamed ServerFlag, and the name reused for FL_*. SVF_* on .r.sv_flags is ServerFlag; EntityFlag is .flags, where god mode and notarget live. EntityFlag.BOT is an AttributeError, but entity.flags & ServerFlag.BOT is a perfectly good expression that always answers 0.

Seven forwarding methods are gone

console, destroy_kamikaze_timers, remove_dropped_items, replace_items, force_weapon_respawn_time (call the module function of the same name), and colored_name, which is Plugin.player(name).name.

Plugin.client_id stayed, but prefer resolve_player/resolve_identifier: client_id hands back a raw slot number without checking who is in it, so one resolved before a thread hop can name whoever took the slot since.

Four names left the package namespace without leaving the framework: minqlxtended.get_configstring is configstring(index, cached=False), minqlxtended.parse_variables is parse_infostring, which raises on a malformed infostring where it used to return what it could, and set_cvar_once/set_cvar_limit_once are on Plugin.

Other

  • Plugin.center_print is broadcast-only. player.center_print(msg) goes to one client.
  • slay kills through G_Damage, so it raises death and kill. If you count deaths or award on kill, you will now see admin slays, carrying Mod.SUICIDE.
  • Game.slap and Game.slay don't announce anything. The broadcast is in essentials.py.
  • Game.red_score and Game.blue_score are gone. They were 1st and 2nd place rather than teams. Use game.team_scores[minqlxtended.Team.RED.index].
  • Game.state reads level.warmup_time rather than parsing g_gameState. Same three GameState members.
  • Game.round_state raises for a state it doesn't know, instead of returning "unknown".
  • Game(cached=...) is gone. The argument had no effect.
  • Plugin.get_cvar() takes a default.
  • Plugin.msg(msg, chat_channel=...) wants a channel object. Default CHAT_CHANNEL.
  • Plugin.play_sound() and play_music() raise ValueError for an empty or wrong-kind path instead of returning False.
  • player_list=[] means "search nobody". An empty list used to search everyone.
  • Command.name is a string. The rest are Command.aliases; Command.names is all.
  • Every channel's reply() takes the same arguments.
  • Plugin.resolve_identifier() returns a named tuple.

Values reaching a command line are checked

The engine escapes nothing, so Game.opsay("hi; quit") used to be two commands, the second stopping the server. These raise ValueError now:

Refuses In
" ; newline Game.opsay, Game.change_map (map and factory), Plugin.play_sound, Plugin.play_music
" Plugin.center_print, Player.center_print, Plugin.send_configstring_to

ChatChannel.reply still substitutes " for ' rather than raising.

Cvars and configstrings

  • minqlxtended.set_cvar() returns the Cvar object instead of a bool, so you can read back what the engine settled on after a latch or clamp. Plugin.set_cvar() is unchanged.
  • set_cvar(name, value, -1) no longer means "force". Pass force=True; flags is a CvarFlag bitfield.
  • Plugin.set_cvar() and set_cvar_limit() don't go through the console, so a value containing a quote no longer truncates or runs as a further command.
  • minqlxtended.require_cvar(name) is new: a RuntimeError naming the cvar if it isn't set by the time it's read.
  • Configstrings are cached. configstring(index) and configstring_variables(index) read a cache kept in step with the set_configstring hook. Pass cached=False for the engine.
  • configstring_variables() returns a read-only mapping. Copy with dict(...), or use update_configstring_variables(index, changes), which re-reads the engine first.
  • apply_variable_changes(variables, changes) returns (variables, dirty) and doesn't modify its argument.
  • Plugin.send_configstring_to(client_id, index, value) was send_big_configstring. It takes any length and chunks into bcs0/bcs1/bcs2 when needed.

Database

  • database.Redis no longer closes the shared connection when a plugin's db is collected. close() closes only a connection that instance opened; Redis.close_shared() is explicit, and with plugin.db as db: is there for a one-off job.
  • redis-py 5.1+ is required, and it wants hiredis 3.0+. With an older hiredis the accelerator installs and is then silently never used. requirements.txt pins both.
  • get_flags(players, flag, default) reads one flag for many players in a single MGET. get_flag is one round-trip per player, up to 64 in one handler on a full server.

Everything else

  • @minqlxtended.thread on a command handler is reported now. Its Return.USAGE never reaches the invoker. Validate on the calling thread, move the slow part into an inner threaded helper. There is no worker pool: @thread starts a fresh thread and Thread.start() blocks the caller until it's running.
  • Work a plugin scheduled stops when the plugin is unloaded, the unload half of a !reload included. A @thread worker can't be cancelled, so a loop that shouldn't outlive its plugin must check self.is_loaded.
  • A plugin that raises partway through construction no longer leaves its hooks behind.
  • starting_weapon_bit(weapon) and toggle_starting_weapon(value, weapon) handle the g_startingWeapons bitfield. The bit is 1 << (weapon - 1). weapon_t starts at 1 and the field reserves no bit for its 0, so shifting by the member itself lands on another weapon.
  • minqlxtended.console_command() defers the seven commands that reload the game module, and calls from a @thread worker. See Console Commands.
  • minqlxtended.redirect_print() nests properly, where the old one ended both blocks.
  • StatsListener is a threading.Thread, and keep_receiving() is now start().
  • minqlxtended.plugins_version() replaces __plugins_version__, and map_titles() replaces the _map_title attributes. Both used to raise AttributeError before the first map load and answer "NOT_SET" and empty strings now.
  • The internals left the package namespace: the handle_* functions, the concrete *Dispatcher classes and the configstring cache's maintenance calls. Reach them as minqlxtended._handlers.handle_frame if you must, but hook through EVENT_DISPATCHERS["name"] or @minqlxtended.hook as before.
  • minqlxtended.EVENT_DISPATCHERS is iterable, with keys(), items() and len(). An unknown event name suggests what you probably meant.
  • @next_frame, @delay and @thread preserve the wrapped function's name and docstring.
  • There are type stubs. python/_minqlxtended.pyi describes the C extension and the package carries py.typed. Point your editor or MYPYPATH at python/.
  • A byte-pattern miss stops the server starting, where it used to warn and fall back to command parsing. minqlxtended.native_events() is gone with the fallback. See Internals.
  • Plugin.plugin(name) returns another loaded plugin's instance or None. Prefer it over self.plugins[name].
  • MAX_CLIENTS and MAX_CONFIGSTRINGS are exported, so stop hardcoding 64 and 1024.
  • Game.is_team_based is new, as is Gametype.is_team_based.
  • minqlxtended.add_event(entity_id, event, event_parm=0) is new, with the EntityEvent enum. It's how to tell clients to play a sound or draw an effect.