OPX//77
opx_infinitycharacter

character

Last updated on

Accounts, characters, money, jobs, gangs, metadata and placement — the module every other module reads a player through.

Owns everything that outlives a session: the account row, its characters, their money, jobs, gangs, metadata and position. There is no selection screen: each account is locked on one character and a connection enters on it; an account locked on nothing gets a new, empty character, which the player builds in the game's creator and names in the world (see entry). Players move the lock with /opx.select, /opx.create and /opx.delete. Every gameplay module reads a player through this one, so it is fatal: if it fails, the runtime does not start.

Sideboth
Requiresnone
Optionalspawn (read with OPX.Api.Get when a join is placed)
Contractcharacter v1 — server, client; Player object
Configconfig/character.lua (shared)
Dataopx77_users, opx77_characters, opx77_character_groups, opx77_active_characters

How a player enters

  1. The platform reports a connection. The server holds the readiness gate, moves the player into a private bucket and reads the account's lock.
  2. Locked on a character: it is loaded. Locked on nothing (or on a character that cannot be entered): a new row is created with no name and no body, the lock is set to it, and it is loaded.
  3. The client receives opx:net:character:loaded. The gate is released and the player leaves the private bucket.
  4. When the platform says the body is alive (onPlayerReady), the character is placed. The server first asks spawn; if spawn takes over, the player picks a place and spawn calls PlaceCharacter. Otherwise: the stored position, or DEFAULT_SPAWN when there is none.
  5. A new character is then sent to the game's creator (appearance) and asked for a name (entry). The name is accepted once.

Placement is always a kill + respawn (fade, preload, 5 s grace), never a teleport. Health is restored from metadata.health as a fraction of HEALTH.MAX in config/shared.lua (at least 15 %), then armour from metadata.armor.

Changing character

CommandWhat happens
/opx.select <citizenId>Under CHARACTERS.SWITCH = 'relog' (shipped): the current character is saved (and waited on), the other is loaded and placed, no disconnect. If that fails, or under 'reconnect': the lock moves and the player is disconnected; the next connection enters on the new character.
/opx.createClears the lock and always disconnects; the next connection builds a new character. A new body needs the game's main-menu creator, which cannot be reopened mid-session.
/opx.delete <citizenId> confirmSoft-deletes the character with its clothes, items and vehicles. Without confirm the command only says what it would do. Whoever is playing it is disconnected.

Privacy: never a name to a stranger

In roleplay a character's name is learnt by meeting them. Since PRs #91 and #93 no part of OPX hands a name, citizen id or out-of-character account data to a client that has not earned it. The data is absent from the wire, not hidden on the page.

WhereWhat a stranger gets
Player state bagNothing: no key is written.
Plate over a headHidden for every remote player (no character name, no account gamertag).
ChatLines signed Player #12 / Joueur #12.
HolocallsA name only if the viewer has that person in their contacts; else Unknown caller. A contact offer names its sender.
Inventory give listTo your left · 1.2 m and #12. Out-of-reach ids all answer too_far.
Dealership offerNo seller name; no buyer name for the seller; no balance probe, no presence probe.
Duo emotes"The person next to you invites you".
DoorlockThe state push carries no by.
Eye Show server IDThe server id only.
StaffName tags (opx.admin.self.tags) carry account, character and citizen id, behind the ACL.

Not covered by OPX: Open77.players.nearby returns the account name to any client resource, and the platform chat or scoreboard (if enabled) show account names. Server resources allowed by SERVER.EXPORTS.READ can read names: they must not relay them to players.

Commands

CommandAccessArgumentsEffect
/opx.characterseveryone (alias chars)—Your characters, slot count, and the one you enter on (>). In game only. 2 s cooldown.
/opx.selecteveryone<citizenId>Switches to another of your characters (see Changing character). 3 s cooldown.
/opx.createeveryone—Clears your lock and disconnects you; next connection builds a new character. Refused at the slot limit or ROW_CEILING.
/opx.deleteeveryone<citizenId> confirmSoft-deletes one of your own characters, with its clothes, items and vehicles. Without confirm it only explains. Refused when CHARACTERS.SELF_DELETE is false.
/opx.dutyeveryone (alias duty)—Toggles duty for your primary job. job.noDuty for a defaultDuty = true job. 2 s cooldown.
/opx.playersACL command.opx.players—Loaded characters: player id, citizen id, name, job.
/opx.whereACL command.opx.where[playerId]What the server holds on a player: account, character, gate, life phase, position and bucket, job, gang, balances.
/opx.hereACL command.opx.here—Your position as a ready-to-paste DEFAULT_SPAWN block. In game only.
/opx.moneyACL command.opx.money<playerId|citizenId> <TYPE> <amount>Adds money; negative removes.
/opx.jobACL command.opx.job<playerId|citizenId> <job> [grade]Sets the primary job (grade default 0).
/opx.gangACL command.opx.gang<playerId|citizenId> <gang> [grade]Sets the primary gang (grade default 0).
/opx.groupACL command.opx.group<job|gang> <name>Up to 200 members, online or not, highest grade first.
/opx.saveACL command.opx.save—Saves every loaded character now. Use before a planned restart.

/opx.money, /opx.job and /opx.gang act on loaded characters only: an id that is not in the world answers the usage line.

Configuration

config/character.lua sets OPX.Config.MODULES.character. Shared script — every client receives it, put nothing secret in it.

KeyDefaultEffect
enabledtrueSwitches the module off. The runtime needs it; leave it on.
AUTOSAVE_SECONDS300How often changed characters are written (floor 30). Changed = any mutation or a move of 1 m or more.
HEADING_REPORT_MS5000How often the client may report its heading (floor 1000).
MONEYsee belowStarting money and paychecks.
CHARACTERSsee belowSlots, switching, deletion, names.
PLAYERsee belowNew-character defaults.
DEFAULT_SPAWN{ SET = true, X = -1600.137451, Y = -2340.758057, Z = 43.250328, HEADING = 139.650009 }Where a character with no stored position is placed. SET = false leaves it where the game put it. /opx.here prints this block.
JOBS12 jobsJob definitions; see Job and gang format.
GANGS12 gangs (incl. none)Gang definitions.

ORIGINS (lifepaths) was removed in PR #95: nothing read it.

MONEY

KeyDefaultEffect
STARTING{ EDDIES = 500, BANK = 5000 }What a new character gets. A type missing on an existing character loads as 0.
PAYCHECK_MINUTES10Minutes between paychecks; 0 turns them off.
PAYCHECK_REQUIRES_DUTYtruePay only characters on duty, unless their job has offDutyPay = true.
PAYCHECK_TYPE'BANK'Money type paid into. Unknown: falls back to MONEY.DEFAULT in config/shared.lua, with a warning at start.

CHARACTERS

KeyDefaultEffect
SWITCH'relog'How /opx.select switches: 'relog' in the world, 'reconnect' by disconnecting. Unknown values fall back to 'reconnect'.
DEFAULT_SLOTS3Characters per account.
SLOTS_BY_USER{}Per-account overrides, { [userId] = slots }.
ROW_CEILING60Character rows ever created per account, deleted ones included (floor 5). Keep it well above DEFAULT_SLOTS.
CASCADE_TABLES{}Extra { TABLE, COLUMN } pairs whose rows are hard-deleted with a character. Only for tables the runtime does not own.
SELF_DELETEtrueWhether players may use /opx.delete. Staff deletes are not affected.
NAME{ MIN = 2, MAX = 32 }Length of each half of a name, in characters. Letters (any alphabet, accents included), spaces, ' and -; must start with a letter. Invisible and direction-changing characters are refused (character.nameInvisible): zero-width characters and joiners, bidi controls and isolates, typographic and no-break spaces, line and paragraph separators, variation selectors, the BOM, Hangul fillers, C0/C1 controls. The form and the server use the same check. Keep config/entry.lua NAME the same.

PLAYER

KeyDefaultEffect
STARTING_METADATA{ health = HEALTH.MAX or 100, armor = 0, isDead = false, inLastStand = false }Metadata a new character starts with; also filled into existing characters that lack a key.
DEFAULT_JOB'unemployed'Job of a new character, and the fallback when a job is removed or no longer defined.
DEFAULT_GANG'none'The same for gangs.

Job and gang format

The table key (ncpd, maelstrom) is stored on character rows: add entries freely, never rename one. Grades start at 0 and must be contiguous.

config/character.lua
JOBS = {
  ncpd = {
    label = 'NCPD',
    type = 'leo',          -- free-form category
    defaultDuty = false,   -- true: always on duty, /opx.duty refused
    offDutyPay = true,     -- paid even when off duty
    grades = {
      [0] = { name = 'Cadet', payment = 150 },
      [3] = ...,           -- every level from 0 up must exist
      -- { name, payment, isBoss = true, bankAuth = true }
    },
  },
},
GANGS = {
  maelstrom = {
    label = 'Maelstrom',
    grades = { [0] = { name = 'Chromehead' }, [3] = { name = 'Warlord', isBoss = true, bankAuth = true } },
  },
},
FieldJobsGangsMeaning
labelyesyesDisplay name.
typeyes—Category, copied to PlayerData.job.type.
defaultDutyyes—On duty from the start; no shift to clock into.
offDutyPayyes—Paid while off duty even when PAYCHECK_REQUIRES_DUTY is true.
grades[n].nameyesyesGrade name.
grades[n].paymentyes—Paycheck amount.
grades[n].isBossyesyesCopied to PlayerData.job/gang.isBoss.
grades[n].bankAuthyesyesCopied to bankAuth. Not on the state bag.

unemployed (defaultDuty = true, 25 per paycheck) and the gang none must stay: they are the defaults.

Translating labels. The HUD and the spawn menu show a job's label and a grade's name through the locale keys character.job.<job> and character.grade.<job>.<level> (e.g. character.grade.ncpd.0), with en and fr for every shipped job. A job you add shows its config text until you register a line for it (Locales).

Data

Tables are created with CREATE TABLE IF NOT EXISTS; there are no migrations. Indexes added after a table was created are ensured on every boot through OPX.Storage.EnsureIndex (M.Storage.INDEXES, applied by OPX.Schema.AddIndexes): idx_opx77_characters_seen (deleted_at, last_logged_out, citizen_id, used by the staff "recent characters" view) is added to an existing opx77_characters automatically. A failure there is a logged warning, not a boot failure.

TableHolds
opx77_usersOne row per account: user_id, display_name, created_at, last_seen_at.
opx77_charactersOne row per character: citizen_id, user_id, cid, name, JSON char_info, money, job, gang, position, metadata, appearance, last_logged_out, created_at, updated_at, deleted_at (soft delete). name is VARCHAR(64): the stored copy is cut to 64 characters; the two halves stay whole in char_info.
opx77_character_groupsJob and gang memberships: citizen_id, group_type, group_name, grade, joined_at.
opx77_active_charactersThe lock: which character each account enters on.

Refusal codes

The code is also the locale key shown to the player.

CodeMeaning
entry.noIdentityThe connection has no verified session.
entry.failedThe player could not be brought into the world.
error.notLoggedInNo character is loaded for that identifier.
error.tooFastCooldown.
error.unavailableDatabase failure or the runtime booted degraded. Detail in the server log.
error.badRequestMalformed argument.
character.notFoundNo such character — or it belongs to another account.
character.inUseLoaded on another connection.
character.alreadyPlaying/opx.select on the character you are playing.
character.limitEvery slot is used.
character.rowLimitThe account reached ROW_CEILING.
character.deleteNotAllowedSELF_DELETE is false.
character.badNameA name half fails length or alphabet.
character.nameInvisibleA name holds an invisible or direction-changing character.
character.nameSetThe character is already named.
character.badBody / character.bodySetNot female/male; body already set.
character.searchShortSearch term under 3 bytes.
money.badType / money.badAmountUnknown type; zero, negative, NaN or infinite amount.
money.tooLargeThe balance would pass ±(2^53−1) (9 007 199 254 740 991). Checked after the hooks. A single amount above that is money.badAmount.
money.insufficientRemoval would go below zero.
money.negativeSetMoney below zero on a type that does not allow it.
money.vetoedA hook refused it.
money.offlineThe character is not loaded. Use AddMoneyOffline to credit an offline character.
job.vetoed / gang.vetoedA job:beforeSet / gang:beforeSet hook refused the change.
job.notFound / job.gradeNotFoundUnknown job or grade.
job.noDutyThe job has defaultDuty = true.
job.notMemberNot a member of that job.
gang.notFound / gang.gradeNotFound / gang.notMemberSame for gangs.

Other resources reach SetJob, SetGang, RemovePlayerFromJob, RemovePlayerFromGang and SetJobDuty through the SetJob, SetGang, RemoveJob, RemoveGang and SetDuty server exports, and their own metadata keys (ext.<resource>.<key>) through GetMetadata/SetMetadata.

On this page