mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2026-09-21 17:04:00 +00:00
The module no longer presents the interface of the legacy expcore.roles module. Nothing outside this repository depends on it, so rather than carry the legacy action strings and the transform which mapped them onto permission names, call sites now check the clusterio permission name directly. That removes the one invariant which silently broke every check if the lua and typescript transforms drifted, and makes a check in lua greppable against its definition. - player_allowed and player_has_flag become player_has_permission; flags were only permissions with a change trigger, which define_permission_trigger now provides for any permission. - on_role_assigned and on_role_unassigned become one on_player_roles_changed event carrying the assigned and unassigned names. Every consumer registered both for the same handler. It is also raised for connected players when a role is edited on the controller, which the old events never were, and for changes made on the controller to the roles a player holds. - player_outranks and player_outranks_role replace the repeated comparison of highest role indexes, and apply the core.admin bypass consistently, which two of the six call sites did not. - get_role takes a name, clusterio id, or role; get_roles replaces get_roles_ordered. The config views of the roles are gone, with role:get_player_names covering the one use of config.players. - Roles carry a permission group, which the legacy system mapped roles to and the first version of the plugin dropped. A player is moved into the group of their most privileged role which names one. It is edited with the other in game properties. - skip_checks is dropped from assign_player and unassign_player. A player object with index 0 is treated as the server, which is how exp_commands represents rcon. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
430 lines
10 KiB
TypeScript
430 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,
|
|
/**
|
|
* Factorio permission group players are moved into while this is their
|
|
* most privileged role which names one, empty leaves the group alone.
|
|
*/
|
|
public permissionGroup: string = "",
|
|
/** 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()])),
|
|
permission_group: Type.Optional(Type.String()),
|
|
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.permissionGroup) {
|
|
json.permission_group = this.permissionGroup;
|
|
}
|
|
|
|
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.permission_group ?? "",
|
|
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));
|
|
}
|
|
}
|
|
|
|
/*
|
|
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);
|
|
}
|
|
}
|