mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2026-09-21 17:04:00 +00:00
- Roles are objects and everything done to or with a role is a method on it: assign, unassign, has_player, has_permission, is_higher_than, is_lower_than, get_players, get_player_names, print. Assignment has one entry point, role:assign(player, options), with local_only as an option rather than a second function. - Roles are looked up by clusterio id with get_role; get_role_by_name searches the list for the few places, such as configs, which only know a name. The name map and the ordered list are gone, get_roles sorts on demand and the index field is replaced by the comparison methods. - Players are LuaPlayer objects only, with nil or index 0 for the server. - get_higher_roles and get_lower_roles replace print_to_roles_higher and print_to_roles_lower, call sites loop over them with role:print. - Permission groups are removed from roles again, exp_groups owns the mapping from roles to groups. - Seeding is a SeedRolesRequest behind a button on the roles page rather than running on first start, and creates only the roles; the player assignments are dropped. The seed lists each permission once at the lowest role which has it and lets the parent chain carry it up. - System commands unlock for core.admin rather than a permission of their own. - Role metatables are registered with Storage.register_metatable so the methods survive save and load, which the role records in storage needed. - The player list auth uses Roles.player_outranks directly, and the event carries role ids rather than names. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
431 lines
10 KiB
TypeScript
431 lines
10 KiB
TypeScript
import * as lib from "@clusterio/lib";
|
|
import { Type, Static } from "@sinclair/typebox";
|
|
|
|
/*
|
|
Data records
|
|
*/
|
|
|
|
/** Colour used for a role's tag and name in game. */
|
|
export class RoleColor {
|
|
constructor(
|
|
public r: number,
|
|
public g: number,
|
|
public b: number,
|
|
) {}
|
|
|
|
static jsonSchema = Type.Object({
|
|
r: Type.Number(),
|
|
g: Type.Number(),
|
|
b: Type.Number(),
|
|
});
|
|
|
|
toJSON(): Static<typeof RoleColor.jsonSchema> {
|
|
return { r: this.r, g: this.g, b: this.b };
|
|
}
|
|
|
|
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
|
return new this(json.r, json.g, json.b);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* In game properties of a role which clusterio's own role does not carry.
|
|
*
|
|
* Stored on the controller keyed by the clusterio role id, and created
|
|
* automatically for any role which does not have one yet.
|
|
*/
|
|
export class RoleMetaRecord {
|
|
constructor(
|
|
/** Id of the clusterio role these properties belong to. */
|
|
public id: number,
|
|
/** Position in the role order, a lower value is a more privileged role. */
|
|
public order: number,
|
|
/**
|
|
* Only the roles with the highest priority a player holds are considered,
|
|
* which allows a role such as Jail to suppress all others.
|
|
*/
|
|
public priority: number = 0,
|
|
/** Short form of the role name, used where space is limited. */
|
|
public shortHand: string = "",
|
|
/** Tag shown next to the names of players with this role. */
|
|
public tag: string = "",
|
|
/** Colour used for the role in game, null uses the default colour. */
|
|
public color: RoleColor | null = null,
|
|
/** Online time after which this role is granted, null never grants it. */
|
|
public autoAssignOnlineTimeMs: number | null = null,
|
|
/** When true holders of this role are never granted roles automatically. */
|
|
public blockAutoAssign: boolean = false,
|
|
public updatedAtMs: number = 0,
|
|
public isDeleted: boolean = false,
|
|
) {}
|
|
|
|
static jsonSchema = Type.Object({
|
|
id: Type.Integer(),
|
|
order: Type.Number(),
|
|
priority: Type.Optional(Type.Number()),
|
|
short_hand: Type.Optional(Type.String()),
|
|
tag: Type.Optional(Type.String()),
|
|
color: Type.Optional(Type.Union([RoleColor.jsonSchema, Type.Null()])),
|
|
auto_assign_online_time_ms: Type.Optional(Type.Union([Type.Number(), Type.Null()])),
|
|
block_auto_assign: Type.Optional(Type.Boolean()),
|
|
updated_at_ms: Type.Optional(Type.Number()),
|
|
is_deleted: Type.Optional(Type.Boolean()),
|
|
});
|
|
|
|
toJSON() {
|
|
const json: Static<typeof RoleMetaRecord.jsonSchema> = {
|
|
id: this.id,
|
|
order: this.order,
|
|
};
|
|
|
|
if (this.priority) {
|
|
json.priority = this.priority;
|
|
}
|
|
|
|
if (this.shortHand) {
|
|
json.short_hand = this.shortHand;
|
|
}
|
|
|
|
if (this.tag) {
|
|
json.tag = this.tag;
|
|
}
|
|
|
|
if (this.color) {
|
|
json.color = this.color.toJSON();
|
|
}
|
|
|
|
if (this.autoAssignOnlineTimeMs !== null) {
|
|
json.auto_assign_online_time_ms = this.autoAssignOnlineTimeMs;
|
|
}
|
|
|
|
if (this.blockAutoAssign) {
|
|
json.block_auto_assign = true;
|
|
}
|
|
|
|
if (this.updatedAtMs) {
|
|
json.updated_at_ms = this.updatedAtMs;
|
|
}
|
|
|
|
if (this.isDeleted) {
|
|
json.is_deleted = true;
|
|
}
|
|
|
|
return json;
|
|
}
|
|
|
|
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
|
return new this(
|
|
json.id,
|
|
json.order,
|
|
json.priority ?? 0,
|
|
json.short_hand ?? "",
|
|
json.tag ?? "",
|
|
json.color ? RoleColor.fromJSON(json.color) : null,
|
|
json.auto_assign_online_time_ms ?? null,
|
|
json.block_auto_assign ?? false,
|
|
json.updated_at_ms ?? 0,
|
|
json.is_deleted ?? false,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A clusterio role combined with its in game properties.
|
|
*
|
|
* This is the form sent to instances, because instances can not subscribe to
|
|
* the core role updates which are only sent to control connections.
|
|
*/
|
|
export class RoleRecord {
|
|
constructor(
|
|
public id: number,
|
|
public name: string,
|
|
public permissions: string[],
|
|
public meta: RoleMetaRecord,
|
|
/** True for the role every player holds, taken from controller.default_role_id. */
|
|
public isDefault: boolean = false,
|
|
public updatedAtMs: number = 0,
|
|
public isDeleted: boolean = false,
|
|
) {}
|
|
|
|
static jsonSchema = Type.Object({
|
|
id: Type.Integer(),
|
|
name: Type.String(),
|
|
permissions: Type.Array(Type.String()),
|
|
meta: RoleMetaRecord.jsonSchema,
|
|
is_default: Type.Optional(Type.Boolean()),
|
|
updated_at_ms: Type.Optional(Type.Number()),
|
|
is_deleted: Type.Optional(Type.Boolean()),
|
|
});
|
|
|
|
toJSON() {
|
|
const json: Static<typeof RoleRecord.jsonSchema> = {
|
|
id: this.id,
|
|
name: this.name,
|
|
permissions: this.permissions,
|
|
meta: this.meta.toJSON(),
|
|
};
|
|
|
|
if (this.isDefault) {
|
|
json.is_default = true;
|
|
}
|
|
|
|
if (this.updatedAtMs) {
|
|
json.updated_at_ms = this.updatedAtMs;
|
|
}
|
|
|
|
if (this.isDeleted) {
|
|
json.is_deleted = true;
|
|
}
|
|
|
|
return json;
|
|
}
|
|
|
|
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
|
return new this(
|
|
json.id,
|
|
json.name,
|
|
json.permissions,
|
|
RoleMetaRecord.fromJSON(json.meta),
|
|
json.is_default ?? false,
|
|
json.updated_at_ms ?? 0,
|
|
json.is_deleted ?? false,
|
|
);
|
|
}
|
|
}
|
|
|
|
/** The roles a single player holds, mirroring the roles of their cluster user. */
|
|
export class AssignmentRecord {
|
|
constructor(
|
|
public name: string,
|
|
public roleIds: Set<number>,
|
|
public updatedAtMs: number = 0,
|
|
public isDeleted: boolean = false,
|
|
) {}
|
|
|
|
get id() {
|
|
return this.name;
|
|
}
|
|
|
|
static jsonSchema = Type.Object({
|
|
name: Type.String(),
|
|
role_ids: Type.Array(Type.Integer()),
|
|
updated_at_ms: Type.Optional(Type.Number()),
|
|
is_deleted: Type.Optional(Type.Boolean()),
|
|
});
|
|
|
|
toJSON() {
|
|
const json: Static<typeof AssignmentRecord.jsonSchema> = {
|
|
name: this.name,
|
|
role_ids: [...this.roleIds],
|
|
};
|
|
|
|
if (this.updatedAtMs) {
|
|
json.updated_at_ms = this.updatedAtMs;
|
|
}
|
|
|
|
if (this.isDeleted) {
|
|
json.is_deleted = true;
|
|
}
|
|
|
|
return json;
|
|
}
|
|
|
|
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
|
return new this(
|
|
json.name,
|
|
new Set(json.role_ids),
|
|
json.updated_at_ms ?? 0,
|
|
json.is_deleted ?? false,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Encode roles for the lua initialise call.
|
|
*
|
|
* Roles repeat the same permission names over and over, so each name is sent
|
|
* once and referenced by index. At 15 roles this is 17 KiB rather than 38 KiB
|
|
* and decodes in 0.37 ms rather than 0.50 ms; at 60 roles it is 30 KiB rather
|
|
* than 122 KiB and 0.87 ms rather than 1.52 ms.
|
|
*
|
|
* Single role updates stay in the plain form, they have nothing to share.
|
|
*/
|
|
export function encodeRolesForLua(roles: RoleRecord[]) {
|
|
const names: string[] = [];
|
|
const indexes = new Map<string, number>();
|
|
|
|
const encoded = roles.map(role => {
|
|
const permissions = role.permissions.map(permission => {
|
|
let index = indexes.get(permission);
|
|
if (index === undefined) {
|
|
index = names.length;
|
|
names.push(permission);
|
|
indexes.set(permission, index);
|
|
}
|
|
return index;
|
|
});
|
|
|
|
return { ...role.toJSON(), permissions };
|
|
});
|
|
|
|
return { permission_names: names, roles: encoded };
|
|
}
|
|
|
|
/*
|
|
Update events
|
|
*/
|
|
|
|
export class RoleUpdatedEvent {
|
|
declare ["constructor"]: typeof RoleUpdatedEvent;
|
|
static plugin = "exp_roles" as const;
|
|
static type = "event" as const;
|
|
static src = "controller" as const;
|
|
static dst = ["control", "instance"] as const;
|
|
static permission = "core.role.subscribe" as const;
|
|
|
|
constructor(
|
|
public updates: RoleRecord[],
|
|
) {}
|
|
|
|
static jsonSchema = Type.Object({
|
|
updates: Type.Array(RoleRecord.jsonSchema),
|
|
});
|
|
|
|
toJSON() {
|
|
return { updates: this.updates.map(role => role.toJSON()) };
|
|
}
|
|
|
|
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
|
return new this(json.updates.map(role => RoleRecord.fromJSON(role)));
|
|
}
|
|
}
|
|
|
|
export class AssignmentUpdatedEvent {
|
|
declare ["constructor"]: typeof AssignmentUpdatedEvent;
|
|
static plugin = "exp_roles" as const;
|
|
static type = "event" as const;
|
|
static src = "controller" as const;
|
|
static dst = "instance" as const;
|
|
|
|
constructor(
|
|
public updates: AssignmentRecord[],
|
|
) {}
|
|
|
|
static jsonSchema = Type.Object({
|
|
updates: Type.Array(AssignmentRecord.jsonSchema),
|
|
});
|
|
|
|
toJSON() {
|
|
return { updates: this.updates.map(assignment => assignment.toJSON()) };
|
|
}
|
|
|
|
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
|
return new this(json.updates.map(assignment => AssignmentRecord.fromJSON(assignment)));
|
|
}
|
|
}
|
|
|
|
/*
|
|
Role requests
|
|
*/
|
|
|
|
export class RoleListRequest {
|
|
declare ["constructor"]: typeof RoleListRequest;
|
|
static plugin = "exp_roles" as const;
|
|
static type = "request" as const;
|
|
static src = ["control", "instance"] as const;
|
|
static dst = "controller" as const;
|
|
static permission = "core.role.list" as const;
|
|
static Response = lib.jsonArray(RoleRecord);
|
|
|
|
constructor() {}
|
|
}
|
|
|
|
export class RoleMetaUpdateRequest {
|
|
declare ["constructor"]: typeof RoleMetaUpdateRequest;
|
|
static plugin = "exp_roles" as const;
|
|
static type = "request" as const;
|
|
static src = "control" as const;
|
|
static dst = "controller" as const;
|
|
static permission = "core.role.update" as const;
|
|
|
|
constructor(
|
|
public meta: RoleMetaRecord,
|
|
) {}
|
|
|
|
static jsonSchema = Type.Object({
|
|
meta: RoleMetaRecord.jsonSchema,
|
|
});
|
|
|
|
toJSON() {
|
|
return { meta: this.meta.toJSON() };
|
|
}
|
|
|
|
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
|
return new this(RoleMetaRecord.fromJSON(json.meta));
|
|
}
|
|
}
|
|
|
|
/** Create the roles the scenario shipped with, see seed.ts. */
|
|
export class SeedRolesRequest {
|
|
declare ["constructor"]: typeof SeedRolesRequest;
|
|
static plugin = "exp_roles" as const;
|
|
static type = "request" as const;
|
|
static src = "control" as const;
|
|
static dst = "controller" as const;
|
|
static permission = "core.role.create" as const;
|
|
|
|
constructor() {}
|
|
}
|
|
|
|
/*
|
|
Assignment requests
|
|
*/
|
|
|
|
export class AssignmentListRequest {
|
|
declare ["constructor"]: typeof AssignmentListRequest;
|
|
static plugin = "exp_roles" as const;
|
|
static type = "request" as const;
|
|
static src = "instance" as const;
|
|
static dst = "controller" as const;
|
|
static Response = lib.jsonArray(AssignmentRecord);
|
|
|
|
constructor() {}
|
|
}
|
|
|
|
/**
|
|
* Add or remove roles for a player, sent by an instance when a role is changed
|
|
* in game. Only accepted when the instance is configured as bidirectional.
|
|
*/
|
|
export class AssignmentUpdateRequest {
|
|
declare ["constructor"]: typeof AssignmentUpdateRequest;
|
|
static plugin = "exp_roles" as const;
|
|
static type = "request" as const;
|
|
static src = "instance" as const;
|
|
static dst = "controller" as const;
|
|
static permission = "core.user.update_roles" as const;
|
|
|
|
constructor(
|
|
public name: string,
|
|
public assign: number[],
|
|
public unassign: number[],
|
|
) {}
|
|
|
|
static jsonSchema = Type.Object({
|
|
name: Type.String(),
|
|
assign: Type.Array(Type.Integer()),
|
|
unassign: Type.Array(Type.Integer()),
|
|
});
|
|
|
|
toJSON() {
|
|
return {
|
|
name: this.name,
|
|
assign: this.assign,
|
|
unassign: this.unassign,
|
|
};
|
|
}
|
|
|
|
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
|
return new this(json.name, json.assign, json.unassign);
|
|
}
|
|
}
|