Files
bbassieandClaude Opus 5.5 6821d62c10 Keep join messages on the controller and print them from the instance
Join messages were stored in exp_legacy's PlayerData, which nothing
ever synced. The controller now keeps a players.json datastore of
per-player settings keyed by player name, starting with the join
message, and saves it on the save and shutdown hooks. Records with
nothing set are removed.

When a player joins, the instance plugin fetches their record and
prints the message to everyone over rcon in the player's colour, so no
lua is needed to show it. The in-game /set-join-message and
/remove-join-message commands send ipc which the instance forwards to
the controller; /set-join-message without a message asks the instance,
which prints the stored message back to the player.

Greetings is now a feature. Disabling it stops the greeting, the
commands, and the printed join messages. The private greeting with the
community name and Discord link is shown to every joining player, even
those with a join message, since one is a welcome for the player and
the other an announcement to everyone else.

Two controller permissions cover the web interface:
exp_scenario.join_message.set for your own message and .set_other for
anyone's, checked against the requesting user the same way
exp_discord_auth checks unlinking. The seed grants them to Supporter
and Moderator alongside the in-game commands. The user page gets a
Scenario section showing the message with an edit button for users who
may change it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:10:31 +00:00

346 lines
9.4 KiB
TypeScript

import type * as lib from "@clusterio/lib";
import { RoleColor } from "@expcluster/roles";
/**
* A role created by the seed, as the scenario defined it before roles moved to
* the controller. `parent` stands in for the inheritance
* the old config had and is flattened by the seed. The default and admin roles
* already exist, so those entries only provide the in game properties.
*/
export interface SeedRole {
name: string;
shortHand: string;
color: RoleColor | null;
priority?: number;
autoAssignHours?: number;
blockAutoAssign?: boolean;
/** Uses the controller's default role rather than creating one. */
isDefault?: boolean;
/** Uses the controller's admin role rather than creating one. */
isAdmin?: boolean;
/** Name of the role whose permissions are also granted, applied recursively. */
parent?: string;
permissions: lib.PermissionName[];
/**
* Name of the seed group holders are placed in by their highest role.
* Without one the holders stay in Factorio's Default group.
*/
group?: string;
}
/**
* A Factorio permission group created by the seed, as the scenario defined it
* before groups moved to the controller.
*/
export interface SeedGroup {
name: string;
/** When true the input actions are the only ones disallowed, otherwise the only ones allowed. */
isBlacklist: boolean;
inputActions: string[];
}
export const seedRoles: SeedRole[] = [
{
name: "System",
shortHand: "SYS",
color: null,
isAdmin: true,
permissions: [],
},
{
name: "Senior Administrator",
group: "Admin",
shortHand: "SAdmin",
color: new RoleColor(233, 63, 233),
parent: "Administrator",
permissions: [
"exp_scenario.command._rcon",
"exp_scenario.command.debug",
"exp_scenario.command.set_cheat_mode",
"exp_scenario.command.research_all",
],
},
{
name: "Administrator",
group: "Admin",
shortHand: "Admin",
color: new RoleColor(233, 63, 233),
parent: "Moderator",
permissions: [
"exp_scenario.gui.warp_list.bypass_proximity",
"exp_scenario.gui.warp_list.bypass_cooldown",
"exp_scenario.command.connect_all",
],
},
{
name: "Moderator",
group: "Admin",
shortHand: "Mod",
color: new RoleColor(0, 170, 0),
parent: "Trainee",
permissions: [
"exp_scenario.player.instant_respawn",
"exp_scenario.command.assign_role",
"exp_scenario.command.unassign_role",
"exp_scenario.command.repair",
"exp_scenario.command.kill.always",
"exp_scenario.command.tag_clear.always",
"exp_scenario.command.spawn.always",
"exp_scenario.command.clear_reports",
"exp_scenario.join_message.set_other",
"exp_scenario.command.clear_inventory",
"exp_scenario.command.kill_enemies",
"exp_scenario.command.remove_enemies",
"exp_scenario.command.home",
"exp_scenario.command.set_home",
"exp_scenario.command.get_home",
"exp_scenario.command.return",
"exp_scenario.command.connect_player",
"exp_scenario.command.set_bot_queue",
"exp_scenario.command.set_game_speed",
"exp_scenario.command.set_friendly_fire",
"exp_scenario.command.set_always_day",
"exp_scenario.command.set_pollution_enabled",
"exp_scenario.command.clear_pollution",
"exp_scenario.gui.rocket_info.toggle_active",
"exp_scenario.gui.rocket_info.remote_launch",
"exp_scenario.gui.bonus",
"exp_scenario.decon.fast_trees",
],
},
{
name: "Trainee",
group: "Admin",
shortHand: "TrMod",
color: new RoleColor(0, 170, 0),
parent: "Veteran",
permissions: [
"exp_scenario.player.admin",
"exp_scenario.player.spectator",
"exp_scenario.bypass.reports",
"exp_scenario.command.admin_chat",
"exp_scenario.command.goto",
"exp_scenario.command.teleport",
"exp_scenario.command.bring",
"exp_scenario.command.get_reports",
"exp_scenario.command.protect_entity",
"exp_scenario.command.protect_area",
"exp_scenario.command.protect_tag",
"exp_scenario.command.jail",
"exp_scenario.command.unjail",
"exp_scenario.gui.player_list.kick",
"exp_scenario.gui.player_list.ban",
"exp_scenario.command.spectate",
"exp_scenario.command.follow",
"exp_scenario.command.search",
"exp_scenario.command.search_online",
"exp_scenario.command.search_amount",
"exp_scenario.command.search_recent",
"exp_scenario.command.clear_blueprints_surface",
"exp_scenario.gui.playerdata",
],
},
{
name: "Board Member",
group: "Trusted",
shortHand: "Board",
color: new RoleColor(247, 246, 54),
parent: "Sponsor",
permissions: [
"exp_scenario.command.goto",
"exp_scenario.command.repair",
"exp_scenario.command.spectate",
"exp_scenario.command.follow",
"exp_scenario.gui.playerdata",
],
},
{
name: "Senior Backer",
group: "Trusted",
shortHand: "Backer",
color: new RoleColor(238, 172, 44),
parent: "Sponsor",
permissions: [],
},
{
name: "Sponsor",
group: "Trusted",
shortHand: "Spon",
color: new RoleColor(238, 172, 44),
parent: "Supporter",
permissions: [
"exp_scenario.bypass.reports",
"exp_scenario.player.instant_respawn",
"exp_scenario.gui.rocket_info.toggle_active",
"exp_scenario.gui.rocket_info.remote_launch",
"exp_scenario.gui.bonus",
"exp_scenario.command.home",
"exp_scenario.command.set_home",
"exp_scenario.command.get_home",
"exp_scenario.command.return",
"exp_scenario.decon.fast_trees",
],
},
{
name: "Supporter",
group: "Trusted",
shortHand: "Sup",
color: new RoleColor(230, 99, 34),
parent: "Veteran",
permissions: [
"exp_scenario.player.spectator",
"exp_scenario.command.tag_color",
"exp_scenario.command.jail",
"exp_scenario.command.unjail",
"exp_scenario.command.set_join_message",
"exp_scenario.command.remove_join_message",
"exp_scenario.join_message.set",
],
},
{
name: "Partner",
group: "Trusted",
shortHand: "Part",
color: new RoleColor(140, 120, 200),
parent: "Veteran",
permissions: [
"exp_scenario.player.spectator",
"exp_scenario.command.jail",
"exp_scenario.command.unjail",
],
},
{
name: "Veteran",
group: "Trusted",
shortHand: "Vet",
color: new RoleColor(140, 120, 200),
parent: "Member",
autoAssignHours: 10,
permissions: [
"exp_scenario.chat.commands",
"exp_scenario.command.clear_ground_items",
"exp_scenario.command.clear_blueprints",
"exp_scenario.command.set_trains_to_automatic",
],
},
{
name: "Member",
group: "Standard",
shortHand: "Mem",
color: new RoleColor(24, 172, 188),
parent: "Regular",
permissions: [
"exp_scenario.bypass.deconstruction_log",
"exp_scenario.gui.task_list.add",
"exp_scenario.gui.task_list.edit",
"exp_scenario.gui.warp_list.add",
"exp_scenario.gui.warp_list.edit",
"exp_scenario.gui.surveillance",
"exp_scenario.gui.vlayer_edit",
"exp_scenario.gui.tool",
"exp_scenario.command.vlayer_info",
"exp_scenario.command.lawnmower",
"exp_scenario.command.waterfill",
"exp_scenario.command.artillery",
],
},
{
name: "Regular",
group: "Standard",
shortHand: "Reg",
color: new RoleColor(79, 155, 163),
autoAssignHours: 3,
permissions: [
"exp_scenario.command.kill",
"exp_scenario.command.rainbow",
"exp_scenario.command.spawn",
"exp_scenario.command.me",
"exp_scenario.decon.standard",
"exp_scenario.bypass.entity_protection",
"exp_scenario.bypass.nuke_protection",
],
},
{
name: "Jail",
group: "Restricted",
shortHand: "Jail",
color: new RoleColor(50, 50, 50),
priority: 1,
blockAutoAssign: true,
permissions: [],
},
{
name: "Guest",
group: "Guest",
shortHand: "",
color: new RoleColor(185, 187, 160),
isDefault: true,
permissions: [],
},
];
const adminDisallowed = [
"add_permission_group",
"delete_permission_group",
"edit_permission_group",
"import_permissions_string",
"map_editor_action",
"toggle_map_editor",
"change_multiplayer_config",
"set_heat_interface_mode",
"set_heat_interface_temperature",
"set_infinity_container_filter_item",
"set_infinity_container_remove_unfiltered_items",
"set_infinity_pipe_filter",
];
const trustedDisallowed = [
...adminDisallowed,
"admin_action",
];
const standardDisallowed = [
...trustedDisallowed,
"change_programmable_speaker_alert_parameters",
"drop_item",
"open_new_platform_button_from_rocket_silo",
"set_rocket_silo_send_to_orbit_automated_mode",
];
const guestDisallowed = [
...standardDisallowed,
"change_programmable_speaker_parameters",
"change_train_stop_station",
"remove_cables",
"remove_train_station",
"reset_assembling_machine",
"rotate_entity",
"launch_rocket",
"cancel_research",
"flush_opened_entity_fluid",
"flush_opened_entity_specific_fluid",
];
export const seedGroups: SeedGroup[] = [
{ name: "Admin", isBlacklist: true, inputActions: adminDisallowed },
{ name: "Trusted", isBlacklist: true, inputActions: trustedDisallowed },
{ name: "Standard", isBlacklist: true, inputActions: standardDisallowed },
{ name: "Guest", isBlacklist: true, inputActions: guestDisallowed },
{ name: "Restricted", isBlacklist: false, inputActions: ["write_to_console"] },
];
/** The permissions a seed role grants, including those of its parents. */
export function flattenSeedPermissions(role: SeedRole, roles = seedRoles) {
const permissions = new Set<lib.PermissionName>();
const seen = new Set<string>();
let current: SeedRole | undefined = role;
while (current && !seen.has(current.name)) {
seen.add(current.name);
for (const permission of current.permissions) {
permissions.add(permission);
}
current = roles.find(other => other.name === current!.parent);
}
return permissions;
}