mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2026-11-11 15:54:52 +09:00
Merge branch 'main' into feature/reports-plugin
This commit is contained in:
103 files changed
+4492
-4129
No files matched your search
@@ -0,0 +1,51 @@
|
||||
name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
# The packages use workspace:^ and catalog: versions, so they only resolve
|
||||
# inside a clusterio checkout with this repository under external_plugins
|
||||
CLUSTERIO_REPOSITORY: clusterio/clusterio
|
||||
CLUSTERIO_REF: master
|
||||
# Node version to use for the tests. This is used by the setup-node action
|
||||
NODE_VERSION: lts/*
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
defaults:
|
||||
run:
|
||||
working-directory: external_plugins/ExpCluster
|
||||
steps:
|
||||
- name: Checkout Clusterio
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ env.CLUSTERIO_REPOSITORY }}
|
||||
ref: ${{ env.CLUSTERIO_REF }}
|
||||
- name: Checkout ExpCluster
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: external_plugins/ExpCluster
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- name: Install and build
|
||||
working-directory: .
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
- name: Run tests
|
||||
run: pnpm --filter "@expcluster/*" run test
|
||||
- name: Run reference checks
|
||||
run: |
|
||||
# pnpm install runs prepare in dependency order, which hides missing
|
||||
# project references. Building from an empty dist one package at a time
|
||||
# will fail unless every cross package import is referenced correctly.
|
||||
for pkg in exp_*/; do
|
||||
rm -rf exp_*/dist
|
||||
echo "::group::${pkg%/}"
|
||||
(cd "$pkg" && pnpm exec tsc --build)
|
||||
echo "::endgroup::"
|
||||
done
|
||||
+1
-1
@@ -9,7 +9,7 @@ For developers wanting to add features please follow these guidelines:
|
||||
- Lua is checked with [emmylua](https://github.com/EmmyLuaLs/emmylua-analyzer-rust), configured by `.emmyrc.json`. Install the EmmyLua extension rather than sumnekolua / luals, and use factoriomod-debug to generate the factorio api types. Note that an inline cast must be written `--[[@as T]]`, the spaced form is not parsed.
|
||||
- Changes should be made on your own fork and merged into `main` through a pull request.
|
||||
- Each pull request should be limited to one feature or a few bug fixes and link to the related issue page.
|
||||
- Pull requests are automatically linted and documentation checked.
|
||||
- Pull requests are automatically linted, then built and tested against the latest clusterio `master`.
|
||||
- Pull requests are manually reviewed to maintain code and language quality.
|
||||
- New features should have the branch names: `feature/feature-name`
|
||||
- Bug fixes should have the branch names: `fix/bug-name`
|
||||
|
||||
@@ -42,13 +42,11 @@ This module is currently not published and therefore can not be installed via `n
|
||||
|
||||
See [Contributing](CONTRIBUTING.md) for how to make pull requests and issues.
|
||||
|
||||
## Translation
|
||||
<a href="https://translate.aperx.org/engage/expcluster/"><img src="https://translate.aperx.org/widget/expcluster/multi-auto.svg" alt="translation status"></a>
|
||||
Contact for access.
|
||||
[translate]: https://translate.aperx.org
|
||||
Details of the scenario can also be seen on [Wiki](https://wiki.aperx.org/game/factorio/scenario).
|
||||
|
||||
## Wiki
|
||||
[wiki]: https://wiki.aperx.org/game/factorio/scenario
|
||||
For the locale, access to [Translation](https://translate.aperx.org).
|
||||
|
||||
<p align="center"><a href="https://translate.aperx.org/engage/expcluster/"><img src="https://translate.aperx.org/widget/expcluster/multi-auto.svg" alt="translation status"></a></p>
|
||||
|
||||
## Releases
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# ExpGaming - Auto Restart
|
||||
|
||||
Restarts hosts, and optionally the controller, that need a restart once nobody is playing on them.
|
||||
|
||||
A host needs a restart after a plugin or clusterio update, and after a config change that only applies on restart. The web UI shows this as "Restart Required". This plugin waits until the host has had no players online for a while and then sends it the same restart request the web UI button does.
|
||||
|
||||
Restarting a host stops every instance on it. Once the host is back, the instances that were running are started again. Instances with auto start enabled are started by the host itself.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Hosts must be run with `--can-restart` so their process monitor starts them again. The systemd units and run scripts written by the installer do this.
|
||||
- `controller.system_metrics_interval` must be above 0. That poll is how the controller learns a host needs a restart.
|
||||
- Installing a plugin on a host does not yet mark it as needing a restart, only updates and config changes do. Until clusterio does, restart the host by hand after an install.
|
||||
|
||||
## Config
|
||||
|
||||
All fields are on the controller.
|
||||
|
||||
| Field | Default | Description |
|
||||
|---|---|---|
|
||||
| `exp_auto_restart.idle_seconds` | 300 | How long a host must have had no players online before it is restarted. |
|
||||
| `exp_auto_restart.scope` | host | With `cluster`, players on any host hold back every restart. |
|
||||
| `exp_auto_restart.start_instances` | true | Start the instances that were running again once the host is back. |
|
||||
| `exp_auto_restart.restart_controller` | false | Also restart the controller when it needs it and no players are online anywhere. |
|
||||
|
||||
Hosts are checked every 10 seconds. A host that refuses the restart, for example because it was not run with `--can-restart`, is logged once and left alone until its process changes.
|
||||
@@ -0,0 +1,295 @@
|
||||
import type {
|
||||
Controller, ControllerPluginContext, HostConnection, HostRecord, InstanceRecord,
|
||||
} from "@clusterio/controller";
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
/** How often hosts are checked for a pending restart */
|
||||
export const CHECK_INTERVAL_MS = 10_000;
|
||||
/** How long to wait for a restarted host to report its instances back before giving up on starting them */
|
||||
export const PENDING_TIMEOUT_MS = 10 * 60_000;
|
||||
|
||||
/** A host that was told to restart and has not reported all of its instances back yet */
|
||||
export interface PendingRestart {
|
||||
requestedAtMs: number;
|
||||
/** Instances that were running when the restart was requested */
|
||||
instanceIds: Set<number>;
|
||||
}
|
||||
|
||||
export class AutoRestart {
|
||||
controller: Controller;
|
||||
logger: lib.Logger;
|
||||
name: string;
|
||||
|
||||
/** When each host, and the cluster as a whole, last became empty of players. Absent while players are online. */
|
||||
idleSince = new Map<number | "cluster", number>();
|
||||
/** Hosts that were told to restart and are waiting for their instances to come back */
|
||||
pending = new Map<number, PendingRestart>();
|
||||
/** Process start time of each host last asked to restart, so the same process is not asked twice */
|
||||
requested = new Map<number, number>();
|
||||
/** Hosts already warned about not being restartable */
|
||||
warned = new Set<number | "controller">();
|
||||
controllerRestarting = false;
|
||||
private checkInterval?: ReturnType<typeof setInterval>;
|
||||
|
||||
constructor(context: ControllerPluginContext) {
|
||||
this.controller = context.controller;
|
||||
this.logger = context.logger;
|
||||
this.name = context.plugin.name;
|
||||
}
|
||||
|
||||
/** Attach to the controller hooks and start checking for restarts. */
|
||||
start() {
|
||||
const hooks = this.controller.hooks;
|
||||
hooks.shutdown.attach(this.name, () => this.stop());
|
||||
hooks.playerEvent.attach(this.name, (instance, event) => this.onPlayerEvent(instance, event));
|
||||
hooks.instanceStatusChanged.attach(this.name, (instance, prev) => this.onInstanceStatusChanged(instance, prev));
|
||||
hooks.hostConnectionEvent.attach(this.name, (connection, event) => this.onHostConnectionEvent(connection, event));
|
||||
|
||||
if (this.controller.config.get("controller.system_metrics_interval") <= 0) {
|
||||
this.logger.warn(
|
||||
"controller.system_metrics_interval is 0, so the controller never learns which hosts need a restart"
|
||||
);
|
||||
}
|
||||
this.refreshIdle();
|
||||
this.checkInterval = setInterval(() => {
|
||||
this.check().catch((err: any) => {
|
||||
this.logger.error(`Unexpected error checking for restarts:\n${err.stack ?? err.message}`);
|
||||
});
|
||||
}, CHECK_INTERVAL_MS);
|
||||
this.checkInterval.unref();
|
||||
}
|
||||
|
||||
stop() {
|
||||
clearInterval(this.checkInterval);
|
||||
}
|
||||
|
||||
onPlayerEvent(instance: InstanceRecord, event: lib.PlayerEvent) {
|
||||
if (event.type === "join" || event.type === "leave") {
|
||||
this.refreshIdle();
|
||||
}
|
||||
}
|
||||
|
||||
onInstanceStatusChanged(instance: InstanceRecord, prev?: lib.InstanceStatus) {
|
||||
this.refreshIdle();
|
||||
// A host reports every instance it has when it connects, which is how
|
||||
// a restarted host announces it is back
|
||||
if (prev === "unknown") {
|
||||
this.instanceReported(instance);
|
||||
}
|
||||
}
|
||||
|
||||
onHostConnectionEvent(connection: HostConnection, event: "connect" | "drop" | "resume" | "close") {
|
||||
if (event === "connect" || event === "close") {
|
||||
this.refreshIdle();
|
||||
}
|
||||
}
|
||||
|
||||
/** Players online per host, counted from the users online on running instances. */
|
||||
onlinePlayers() {
|
||||
const counts = new Map<number, number>();
|
||||
for (const user of this.controller.users.records.values()) {
|
||||
if (user.isDeleted) {
|
||||
continue;
|
||||
}
|
||||
for (const instanceId of user.instances) {
|
||||
const instance = this.controller.instances.get(instanceId);
|
||||
// Nobody is online on an instance that is not running, whatever
|
||||
// the user records say after a host crashed
|
||||
if (!instance || instance.status !== "running") {
|
||||
continue;
|
||||
}
|
||||
const hostId = instance.config.get("instance.assigned_host");
|
||||
if (hostId === null) {
|
||||
continue;
|
||||
}
|
||||
counts.set(hostId, (counts.get(hostId) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
/** Record when connected hosts and the cluster became empty, from the players online now. */
|
||||
refreshIdle(nowMs = Date.now()) {
|
||||
const counts = this.onlinePlayers();
|
||||
for (const host of this.controller.hosts.values()) {
|
||||
if (host.isDeleted || !host.connected) {
|
||||
this.idleSince.delete(host.id);
|
||||
continue;
|
||||
}
|
||||
this.markIdle(host.id, !counts.get(host.id), nowMs);
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
for (const count of counts.values()) {
|
||||
total += count;
|
||||
}
|
||||
this.markIdle("cluster", total === 0, nowMs);
|
||||
}
|
||||
|
||||
private markIdle(key: number | "cluster", idle: boolean, nowMs: number) {
|
||||
if (!idle) {
|
||||
this.idleSince.delete(key);
|
||||
} else if (!this.idleSince.has(key)) {
|
||||
this.idleSince.set(key, nowMs);
|
||||
}
|
||||
}
|
||||
|
||||
/** How long a host has been without players, or undefined while it has some. Uses the whole cluster with scope cluster. */
|
||||
idleMs(hostId: number, nowMs: number) {
|
||||
const scope = this.controller.config.get("exp_auto_restart.scope");
|
||||
const since = this.idleSince.get(scope === "cluster" ? "cluster" : hostId);
|
||||
return since === undefined ? undefined : nowMs - since;
|
||||
}
|
||||
|
||||
/** Restart every host, and the controller if enabled, that needs it and has been empty for long enough. */
|
||||
async check(nowMs = Date.now()) {
|
||||
this.refreshIdle(nowMs);
|
||||
this.expirePending(nowMs);
|
||||
const waitMs = this.controller.config.get("exp_auto_restart.idle_seconds") * 1000;
|
||||
|
||||
for (const system of this.controller.systems.values()) {
|
||||
if (system.id === "controller" || system.isDeleted || !system.restartRequired) {
|
||||
continue;
|
||||
}
|
||||
const hostId = system.id;
|
||||
const host = this.controller.hosts.get(hostId);
|
||||
if (!host || host.isDeleted || !host.connected) {
|
||||
continue;
|
||||
}
|
||||
// Already asked, either waiting for it to come back or it refused
|
||||
if (this.pending.has(hostId) || this.requested.get(hostId) === system.processStartedAtMs) {
|
||||
continue;
|
||||
}
|
||||
if (!system.canRestart) {
|
||||
this.warnOnce(hostId, `Host ${host.name} needs a restart but is not run with --can-restart`);
|
||||
continue;
|
||||
}
|
||||
const idleMs = this.idleMs(hostId, nowMs);
|
||||
if (idleMs === undefined || idleMs < waitMs) {
|
||||
continue;
|
||||
}
|
||||
await this.restartHost(host, system, idleMs, nowMs);
|
||||
}
|
||||
|
||||
if (this.controller.config.get("exp_auto_restart.restart_controller")) {
|
||||
await this.checkController(nowMs, waitMs);
|
||||
}
|
||||
}
|
||||
|
||||
async restartHost(host: Readonly<HostRecord>, system: lib.SystemInfo, idleMs: number, nowMs: number) {
|
||||
const running = [...this.controller.instances.values()].filter(
|
||||
instance => instance.status === "running" && instance.config.get("instance.assigned_host") === host.id
|
||||
);
|
||||
this.requested.set(host.id, system.processStartedAtMs);
|
||||
if (running.length && this.controller.config.get("exp_auto_restart.start_instances")) {
|
||||
this.pending.set(host.id, {
|
||||
requestedAtMs: nowMs,
|
||||
instanceIds: new Set(running.map(instance => instance.id)),
|
||||
});
|
||||
}
|
||||
this.logger.info(
|
||||
`Restarting host ${host.name} after ${Math.round(idleMs / 1000)} s without players, ` +
|
||||
`${running.length} instance(s) running`
|
||||
);
|
||||
|
||||
try {
|
||||
await this.controller.sendTo({ hostId: host.id }, new lib.HostRestartRequest());
|
||||
} catch (err: any) {
|
||||
this.pending.delete(host.id);
|
||||
// The host refusing is final for this process, anything else is retried
|
||||
if (!(err instanceof lib.RequestError)) {
|
||||
this.requested.delete(host.id);
|
||||
}
|
||||
this.logger.error(`Failed to restart host ${host.name}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** An instance on a host that reconnected has reported its status. */
|
||||
instanceReported(instance: InstanceRecord) {
|
||||
const hostId = instance.config.get("instance.assigned_host");
|
||||
if (hostId === null) {
|
||||
return;
|
||||
}
|
||||
const pending = this.pending.get(hostId);
|
||||
if (!pending?.instanceIds.delete(instance.id)) {
|
||||
return;
|
||||
}
|
||||
if (!pending.instanceIds.size) {
|
||||
this.pending.delete(hostId);
|
||||
}
|
||||
|
||||
// The host starts these itself right after reporting them
|
||||
if (instance.status !== "stopped" || instance.config.get("instance.auto_start")) {
|
||||
return;
|
||||
}
|
||||
this.startInstance(instance);
|
||||
}
|
||||
|
||||
async startInstance(instance: InstanceRecord) {
|
||||
const name = instance.config.get("instance.name");
|
||||
this.logger.info(`Starting ${name} again after its host restarted`);
|
||||
try {
|
||||
await this.controller.sendTo({ instanceId: instance.id }, new lib.InstanceStartRequest());
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Failed to start ${name} after its host restarted: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Forget hosts that were restarted but never reported their instances back. */
|
||||
expirePending(nowMs: number) {
|
||||
for (const [hostId, pending] of this.pending) {
|
||||
if (nowMs - pending.requestedAtMs >= PENDING_TIMEOUT_MS) {
|
||||
this.pending.delete(hostId);
|
||||
this.logger.warn(
|
||||
`Host ${hostId} has not reported back since it was restarted, giving up on starting its instances`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async checkController(nowMs: number, waitMs: number) {
|
||||
if (this.controllerRestarting) {
|
||||
return;
|
||||
}
|
||||
const system = this.controller.systems.get("controller");
|
||||
if (!system?.restartRequired) {
|
||||
return;
|
||||
}
|
||||
if (!system.canRestart) {
|
||||
this.warnOnce("controller", "The controller needs a restart but is not run with --can-restart");
|
||||
return;
|
||||
}
|
||||
const since = this.idleSince.get("cluster");
|
||||
if (since === undefined || nowMs - since < waitMs) {
|
||||
return;
|
||||
}
|
||||
const downgrade = await this.controller.checkRestartDowngrade();
|
||||
if (downgrade) {
|
||||
this.warnOnce(
|
||||
"controller",
|
||||
`Not restarting the controller because installed version ${downgrade.installedVersion} ` +
|
||||
`is older than running version ${downgrade.runningVersion}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.controllerRestarting = true;
|
||||
this.logger.info(`Restarting controller after ${Math.round((nowMs - since) / 1000)} s without players`);
|
||||
this.controller.shouldRestart = true;
|
||||
this.controller.stop().catch((err: any) => {
|
||||
this.logger.error(`Failed to restart the controller: ${err.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
private warnOnce(key: number | "controller", message: string) {
|
||||
if (this.warned.has(key)) {
|
||||
return;
|
||||
}
|
||||
this.warned.add(key);
|
||||
this.logger.warn(message);
|
||||
}
|
||||
}
|
||||
|
||||
export default async function (context: ControllerPluginContext) {
|
||||
new AutoRestart(context).start();
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
declare module "@clusterio/lib" {
|
||||
export interface ControllerConfigFields {
|
||||
"exp_auto_restart.idle_seconds": number;
|
||||
"exp_auto_restart.scope": "host" | "cluster";
|
||||
"exp_auto_restart.start_instances": boolean;
|
||||
"exp_auto_restart.restart_controller": boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export const plugin: lib.PluginDeclaration = {
|
||||
name: "exp_auto_restart",
|
||||
title: "ExpGaming - Auto Restart",
|
||||
description: "Clusterio plugin restarting hosts and the controller once no players are online",
|
||||
|
||||
controllerEntrypoint: "./dist/node/controller.js",
|
||||
controllerConfigFields: {
|
||||
"exp_auto_restart.idle_seconds": {
|
||||
title: "Idle Seconds",
|
||||
description: "How long a host must have had no players online before it is restarted",
|
||||
type: "number",
|
||||
initialValue: 300,
|
||||
validator: (value: number) => {
|
||||
if (value < 0) {
|
||||
throw new Error("Idle seconds cannot be negative");
|
||||
}
|
||||
},
|
||||
},
|
||||
"exp_auto_restart.scope": {
|
||||
title: "Scope",
|
||||
description: "Whether players on other hosts also hold back a restart",
|
||||
type: "string",
|
||||
enum: ["host", "cluster"],
|
||||
initialValue: "host",
|
||||
},
|
||||
"exp_auto_restart.start_instances": {
|
||||
title: "Start Instances",
|
||||
description: "Start the instances that were running again once the host is back",
|
||||
type: "boolean",
|
||||
initialValue: true,
|
||||
},
|
||||
"exp_auto_restart.restart_controller": {
|
||||
title: "Restart Controller",
|
||||
description: "Also restart the controller when it needs it and no players are online anywhere",
|
||||
type: "boolean",
|
||||
initialValue: false,
|
||||
},
|
||||
},
|
||||
|
||||
webEntrypoint: "./web",
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@expcluster/auto-restart",
|
||||
"version": "7.0.1",
|
||||
"description": "Clusterio plugin restarting hosts and the controller once no players are online.",
|
||||
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
|
||||
"license": "MIT",
|
||||
"repository": "explosivegaming/ExpCluster",
|
||||
"type": "module",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --env production",
|
||||
"test": "tap --type-strip-only --disable-coverage --allow-empty-coverage test/*.test.js",
|
||||
"coverage": "tap --coverage-report=text test/*.test.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@clusterio/controller": "workspace:^",
|
||||
"@clusterio/lib": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clusterio/controller": "workspace:^",
|
||||
"@clusterio/lib": "workspace:^",
|
||||
"@types/node": "catalog:",
|
||||
"tap": "^21.1.0",
|
||||
"typescript": "catalog:",
|
||||
"webpack": "catalog:",
|
||||
"webpack-cli": "catalog:",
|
||||
"webpack-merge": "catalog:"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"keywords": [
|
||||
"clusterio",
|
||||
"clusterio-plugin",
|
||||
"factorio"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
import t from "tap";
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { Controller, HostRecord, InstanceRecord } from "@clusterio/controller";
|
||||
import entrypoint, { AutoRestart, PENDING_TIMEOUT_MS } from "../dist/node/controller.js";
|
||||
import { plugin as pluginDeclaration } from "../dist/node/index.js";
|
||||
|
||||
// The plugin's config fields must be defined before a ControllerConfig can set them
|
||||
lib.addPluginConfigFields([pluginDeclaration]);
|
||||
|
||||
// Silence the singleton logger to avoid polluting the test output
|
||||
lib.logger.silent = true;
|
||||
t.after(() => {
|
||||
lib.logger.silent = false;
|
||||
});
|
||||
|
||||
/** Build the plugin around a real controller, which is side effect free while not started. */
|
||||
async function startPlugin(t2, { config = {}, canRestart = false } = {}) {
|
||||
const logs = [];
|
||||
const logger = {
|
||||
child: () => logger,
|
||||
info: message => logs.push(["info", message]),
|
||||
warn: message => logs.push(["warn", message]),
|
||||
error: message => logs.push(["error", message]),
|
||||
verbose: () => {},
|
||||
};
|
||||
const controllerConfig = new lib.ControllerConfig("controller", {
|
||||
"controller.database_directory": t2.testdir(),
|
||||
...config,
|
||||
});
|
||||
const controller = new Controller(logger, [], controllerConfig, canRestart);
|
||||
|
||||
// Record what would be sent to hosts and instances instead of sending it
|
||||
const sent = [];
|
||||
controller.sendTo = async (address, message) => {
|
||||
sent.push({ address, message });
|
||||
};
|
||||
|
||||
const plugin = new AutoRestart({ controller, logger, plugin: pluginDeclaration, metrics: undefined });
|
||||
plugin.start();
|
||||
t2.teardown(() => plugin.stop());
|
||||
return { plugin, controller, sent, logs };
|
||||
}
|
||||
|
||||
function addHost(controller, id, connected = true) {
|
||||
controller.hosts.set(new HostRecord(id, `host-${id}`, "2.0.0", new Map(), connected));
|
||||
}
|
||||
|
||||
function addInstance(controller, id, hostId, status = "running", fields = {}) {
|
||||
const config = new lib.InstanceConfig("controller", {
|
||||
"instance.id": id,
|
||||
"instance.name": `instance-${id}`,
|
||||
"instance.assigned_host": hostId,
|
||||
...fields,
|
||||
});
|
||||
const record = new InstanceRecord(config, status);
|
||||
controller.instances.records.set(record);
|
||||
return record;
|
||||
}
|
||||
|
||||
function addSystem(controller, id, { restartRequired = true, canRestart = true, processStartedAtMs = 1000 } = {}) {
|
||||
controller.systems.set(new lib.SystemInfo(
|
||||
id, "box", "v22", "Linux", "x64", "cpu", [], 0, 0, 0, 0,
|
||||
canRestart, restartRequired, 0, processStartedAtMs, Date.now(), false,
|
||||
));
|
||||
}
|
||||
|
||||
const join = (controller, name, instanceId) => controller.users.getOrCreateUser(name).notifyJoin(instanceId);
|
||||
const leave = (controller, name, instanceId) => controller.users.getOrCreateUser(name).notifyLeave(instanceId);
|
||||
const restartsSent = sent => sent.filter(({ message }) => message instanceof lib.HostRestartRequest);
|
||||
const startsSent = sent => sent.filter(({ message }) => message instanceof lib.InstanceStartRequest);
|
||||
|
||||
t.test("entrypoint", async t2 => {
|
||||
const logger = { child: () => logger, info: () => {}, warn: () => {}, error: () => {}, verbose: () => {} };
|
||||
const controllerConfig = new lib.ControllerConfig("controller", { "controller.database_directory": t2.testdir() });
|
||||
const controller = new Controller(logger, [], controllerConfig);
|
||||
await entrypoint({ controller, logger, plugin: pluginDeclaration, metrics: undefined });
|
||||
for (const hook of ["shutdown", "playerEvent", "instanceStatusChanged", "hostConnectionEvent"]) {
|
||||
t2.ok([...controller.hooks[hook].attached].includes("exp_auto_restart"), `attached to ${hook}`);
|
||||
}
|
||||
await controller.hooks.shutdown.invoke();
|
||||
});
|
||||
|
||||
t.test("class AutoRestart", t2 => {
|
||||
t2.test(".onlinePlayers() counts players on running instances per host", async t3 => {
|
||||
const { plugin, controller } = await startPlugin(t3);
|
||||
addHost(controller, 1);
|
||||
addHost(controller, 2);
|
||||
addInstance(controller, 10, 1);
|
||||
addInstance(controller, 11, 1, "stopped");
|
||||
addInstance(controller, 20, 2);
|
||||
join(controller, "alice", 10);
|
||||
join(controller, "bob", 10);
|
||||
join(controller, "carol", 11);
|
||||
join(controller, "dave", 20);
|
||||
|
||||
t3.strictSame(plugin.onlinePlayers(), new Map([[1, 2], [2, 1]]), "players on stopped instances are not counted");
|
||||
});
|
||||
|
||||
t2.test(".refreshIdle() tracks when hosts and the cluster became empty", async t3 => {
|
||||
const { plugin, controller } = await startPlugin(t3);
|
||||
addHost(controller, 1);
|
||||
addHost(controller, 2);
|
||||
addHost(controller, 3, false);
|
||||
addInstance(controller, 10, 1);
|
||||
join(controller, "alice", 10);
|
||||
|
||||
plugin.refreshIdle(1000);
|
||||
t3.notOk(plugin.idleSince.has(1), "a host with players is not idle");
|
||||
t3.strictSame(plugin.idleSince.get(2), 1000, "an empty host is idle from now");
|
||||
t3.notOk(plugin.idleSince.has(3), "a disconnected host is not tracked");
|
||||
t3.notOk(plugin.idleSince.has("cluster"), "the cluster is not idle while anyone is online");
|
||||
|
||||
leave(controller, "alice", 10);
|
||||
plugin.refreshIdle(2000);
|
||||
t3.strictSame(plugin.idleSince.get(1), 2000, "a host becomes idle when its last player leaves");
|
||||
t3.strictSame(plugin.idleSince.get(2), 1000, "an idle host keeps its original time");
|
||||
t3.strictSame(plugin.idleSince.get("cluster"), 2000, "the cluster becomes idle with the last player");
|
||||
|
||||
join(controller, "alice", 10);
|
||||
plugin.refreshIdle(3000);
|
||||
t3.notOk(plugin.idleSince.has(1), "a join clears the idle time");
|
||||
t3.notOk(plugin.idleSince.has("cluster"));
|
||||
});
|
||||
|
||||
t2.test(".check() restarts a host once it has been empty for long enough", async t3 => {
|
||||
const { plugin, controller, sent } = await startPlugin(t3, {
|
||||
config: { "exp_auto_restart.idle_seconds": 60 },
|
||||
});
|
||||
const now = Date.now();
|
||||
addHost(controller, 1);
|
||||
addInstance(controller, 10, 1);
|
||||
addInstance(controller, 11, 1, "stopped");
|
||||
addSystem(controller, 1);
|
||||
join(controller, "alice", 10);
|
||||
|
||||
await plugin.check(now);
|
||||
t3.strictSame(restartsSent(sent).length, 0, "not restarted while a player is online");
|
||||
|
||||
leave(controller, "alice", 10);
|
||||
await plugin.check(now + 10_000);
|
||||
t3.strictSame(restartsSent(sent).length, 0, "not restarted before the idle time has passed");
|
||||
|
||||
await plugin.check(now + 70_000);
|
||||
t3.strictSame(restartsSent(sent).length, 1, "restarted once the idle time has passed");
|
||||
t3.strictSame(restartsSent(sent)[0].address, { hostId: 1 }, "the restart is sent to the host");
|
||||
t3.strictSame([...plugin.pending.get(1).instanceIds], [10], "only the running instances are remembered");
|
||||
|
||||
await plugin.check(now + 80_000);
|
||||
t3.strictSame(restartsSent(sent).length, 1, "not restarted again while waiting for it to come back");
|
||||
});
|
||||
|
||||
t2.test(".check() asks each host process only once", async t3 => {
|
||||
const { plugin, controller, sent } = await startPlugin(t3, {
|
||||
config: { "exp_auto_restart.idle_seconds": 0 },
|
||||
});
|
||||
const now = Date.now();
|
||||
addHost(controller, 1);
|
||||
addSystem(controller, 1, { processStartedAtMs: 1000 });
|
||||
|
||||
await plugin.check(now);
|
||||
t3.strictSame(restartsSent(sent).length, 1, "restarted with no running instances");
|
||||
t3.notOk(plugin.pending.has(1), "nothing to wait for without running instances");
|
||||
|
||||
await plugin.check(now + 10_000);
|
||||
t3.strictSame(restartsSent(sent).length, 1, "the same process is not asked again");
|
||||
|
||||
addSystem(controller, 1, { restartRequired: false, processStartedAtMs: 2000 });
|
||||
await plugin.check(now + 20_000);
|
||||
t3.strictSame(restartsSent(sent).length, 1, "the new process does not need a restart");
|
||||
|
||||
addSystem(controller, 1, { processStartedAtMs: 2000 });
|
||||
await plugin.check(now + 30_000);
|
||||
t3.strictSame(restartsSent(sent).length, 2, "the new process is restarted when it needs it");
|
||||
});
|
||||
|
||||
t2.test(".check() leaves hosts alone that cannot or need not restart", async t3 => {
|
||||
const { plugin, controller, sent, logs } = await startPlugin(t3, {
|
||||
config: { "exp_auto_restart.idle_seconds": 0 },
|
||||
});
|
||||
const now = Date.now();
|
||||
addHost(controller, 1);
|
||||
addSystem(controller, 1, { canRestart: false });
|
||||
addHost(controller, 2);
|
||||
addSystem(controller, 2, { restartRequired: false });
|
||||
addHost(controller, 3, false);
|
||||
addSystem(controller, 3);
|
||||
|
||||
await plugin.check(now);
|
||||
await plugin.check(now + 10_000);
|
||||
t3.strictSame(restartsSent(sent).length, 0, "no host is restarted");
|
||||
const warnings = logs.filter(([level, message]) => level === "warn" && message.includes("--can-restart"));
|
||||
t3.strictSame(warnings.length, 1, "a host without a process monitor is warned about once");
|
||||
});
|
||||
|
||||
t2.test(".check() with scope cluster waits for the whole cluster to be empty", async t3 => {
|
||||
const { plugin, controller, sent } = await startPlugin(t3, {
|
||||
config: { "exp_auto_restart.idle_seconds": 0, "exp_auto_restart.scope": "cluster" },
|
||||
});
|
||||
const now = Date.now();
|
||||
addHost(controller, 1);
|
||||
addSystem(controller, 1);
|
||||
addHost(controller, 2);
|
||||
addInstance(controller, 20, 2);
|
||||
join(controller, "alice", 20);
|
||||
|
||||
await plugin.check(now);
|
||||
t3.strictSame(restartsSent(sent).length, 0, "a player on another host holds back the restart");
|
||||
|
||||
leave(controller, "alice", 20);
|
||||
await plugin.check(now + 10_000);
|
||||
t3.strictSame(restartsSent(sent).length, 1, "restarted once the cluster is empty");
|
||||
});
|
||||
|
||||
t2.test(".restartHost() retries transport errors but not refusals", async t3 => {
|
||||
const { plugin, controller, sent, logs } = await startPlugin(t3, {
|
||||
config: { "exp_auto_restart.idle_seconds": 0 },
|
||||
});
|
||||
const now = Date.now();
|
||||
addHost(controller, 1);
|
||||
addInstance(controller, 10, 1);
|
||||
addSystem(controller, 1);
|
||||
|
||||
controller.sendTo = async () => { throw new lib.RequestError("Cannot restart"); };
|
||||
await plugin.check(now);
|
||||
await plugin.check(now + 10_000);
|
||||
t3.strictSame(logs.filter(([level]) => level === "error").length, 1, "a refusal is logged once");
|
||||
t3.notOk(plugin.pending.has(1), "a refused restart is not waited on");
|
||||
|
||||
addSystem(controller, 1, { processStartedAtMs: 2000 });
|
||||
let attempts = 0;
|
||||
controller.sendTo = async () => { attempts += 1; throw new Error("Connection lost"); };
|
||||
await plugin.check(now + 20_000);
|
||||
await plugin.check(now + 30_000);
|
||||
t3.strictSame(attempts, 2, "a transport error is retried on the next check");
|
||||
});
|
||||
|
||||
t2.test(".instanceReported() starts the instances that were running", async t3 => {
|
||||
const { plugin, controller, sent } = await startPlugin(t3, {
|
||||
config: { "exp_auto_restart.idle_seconds": 0 },
|
||||
});
|
||||
const now = Date.now();
|
||||
addHost(controller, 1);
|
||||
const manual = addInstance(controller, 10, 1);
|
||||
const auto = addInstance(controller, 11, 1, "running", { "instance.auto_start": true });
|
||||
const stopped = addInstance(controller, 12, 1, "stopped");
|
||||
addSystem(controller, 1);
|
||||
await plugin.check(now);
|
||||
t3.strictSame([...plugin.pending.get(1).instanceIds], [10, 11]);
|
||||
|
||||
// The host comes back and reports every instance as stopped
|
||||
for (const instance of [manual, auto, stopped]) {
|
||||
instance.status = "stopped";
|
||||
await plugin.onInstanceStatusChanged(instance, "unknown");
|
||||
}
|
||||
t3.strictSame(startsSent(sent).map(({ address }) => address), [{ instanceId: 10 }], "only the instance the host will not start itself is started");
|
||||
t3.notOk(plugin.pending.has(1), "the host is no longer waited on");
|
||||
|
||||
// An instance reporting in without a pending restart is ignored
|
||||
await plugin.onInstanceStatusChanged(manual, "unknown");
|
||||
t3.strictSame(startsSent(sent).length, 1);
|
||||
});
|
||||
|
||||
t2.test(".restartHost() does not wait on instances when they are not to be started", async t3 => {
|
||||
const { plugin, controller, sent } = await startPlugin(t3, {
|
||||
config: { "exp_auto_restart.idle_seconds": 0, "exp_auto_restart.start_instances": false },
|
||||
});
|
||||
addHost(controller, 1);
|
||||
const instance = addInstance(controller, 10, 1);
|
||||
addSystem(controller, 1);
|
||||
await plugin.check(Date.now());
|
||||
t3.strictSame(restartsSent(sent).length, 1);
|
||||
t3.notOk(plugin.pending.has(1), "nothing is waited on");
|
||||
|
||||
instance.status = "stopped";
|
||||
await plugin.onInstanceStatusChanged(instance, "unknown");
|
||||
t3.strictSame(startsSent(sent).length, 0, "nothing is started");
|
||||
});
|
||||
|
||||
t2.test(".expirePending() gives up on hosts that never come back", async t3 => {
|
||||
const { plugin, controller, logs } = await startPlugin(t3, {
|
||||
config: { "exp_auto_restart.idle_seconds": 0 },
|
||||
});
|
||||
const now = Date.now();
|
||||
addHost(controller, 1);
|
||||
addInstance(controller, 10, 1);
|
||||
addSystem(controller, 1);
|
||||
await plugin.check(now);
|
||||
t3.ok(plugin.pending.has(1));
|
||||
|
||||
await plugin.check(now + PENDING_TIMEOUT_MS - 1);
|
||||
t3.ok(plugin.pending.has(1), "still waited on before the timeout");
|
||||
await plugin.check(now + PENDING_TIMEOUT_MS);
|
||||
t3.notOk(plugin.pending.has(1), "no longer waited on after the timeout");
|
||||
t3.ok(logs.some(([level, message]) => level === "warn" && message.includes("giving up")), "giving up is logged");
|
||||
});
|
||||
|
||||
t2.test(".checkController() restarts the controller when the cluster is empty", async t3 => {
|
||||
const { plugin, controller } = await startPlugin(t3, {
|
||||
config: { "exp_auto_restart.idle_seconds": 60, "exp_auto_restart.restart_controller": true },
|
||||
canRestart: true,
|
||||
});
|
||||
const now = Date.now();
|
||||
let stopped = 0;
|
||||
controller.stop = async () => { stopped += 1; };
|
||||
controller.checkRestartDowngrade = async () => null;
|
||||
addHost(controller, 1);
|
||||
addInstance(controller, 10, 1);
|
||||
addSystem(controller, "controller");
|
||||
join(controller, "alice", 10);
|
||||
|
||||
await plugin.check(now);
|
||||
t3.strictSame(stopped, 0, "not restarted while a player is online");
|
||||
|
||||
leave(controller, "alice", 10);
|
||||
await plugin.check(now + 10_000);
|
||||
t3.strictSame(stopped, 0, "not restarted before the idle time has passed");
|
||||
|
||||
await plugin.check(now + 70_000);
|
||||
t3.strictSame(stopped, 1, "restarted once the cluster has been empty for long enough");
|
||||
t3.ok(controller.shouldRestart, "stopped with the intent to restart");
|
||||
|
||||
await plugin.check(now + 80_000);
|
||||
t3.strictSame(stopped, 1, "not restarted twice");
|
||||
});
|
||||
|
||||
t2.test(".checkController() refuses downgrades and missing process monitors", async t3 => {
|
||||
const { plugin, controller, logs } = await startPlugin(t3, {
|
||||
config: { "exp_auto_restart.idle_seconds": 0, "exp_auto_restart.restart_controller": true },
|
||||
canRestart: true,
|
||||
});
|
||||
let stopped = 0;
|
||||
controller.stop = async () => { stopped += 1; };
|
||||
controller.checkRestartDowngrade = async () => ({ installedVersion: "2.0.0", runningVersion: "2.0.1" });
|
||||
addSystem(controller, "controller");
|
||||
|
||||
await plugin.check(Date.now());
|
||||
t3.strictSame(stopped, 0, "not restarted into an older version");
|
||||
t3.ok(logs.some(([level, message]) => level === "warn" && message.includes("older")), "the downgrade is logged");
|
||||
|
||||
plugin.warned.clear();
|
||||
addSystem(controller, "controller", { canRestart: false });
|
||||
await plugin.check(Date.now());
|
||||
t3.strictSame(stopped, 0, "not restarted without a process monitor");
|
||||
t3.ok(logs.some(([level, message]) => level === "warn" && message.includes("--can-restart")));
|
||||
});
|
||||
|
||||
t2.test(".start() warns when system metrics are disabled", async t3 => {
|
||||
const { logs } = await startPlugin(t3, { config: { "controller.system_metrics_interval": 0 } });
|
||||
t3.ok(logs.some(([level, message]) => level === "warn" && message.includes("system_metrics_interval")));
|
||||
});
|
||||
|
||||
t2.end();
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../tsconfig.browser.json",
|
||||
"include": [ "web/**/*.tsx", "web/**/*.ts", "package.json" ],
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.node.json" },
|
||||
{ "path": "./tsconfig.browser.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../tsconfig.node.json",
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["test/*", "./dist/*"],
|
||||
}
|
||||
Whitespace-only changes.
@@ -0,0 +1,29 @@
|
||||
"use strict";
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const { merge } = require("webpack-merge");
|
||||
|
||||
const common = require("@clusterio/web_ui/webpack.common");
|
||||
|
||||
module.exports = (env = {}) => merge(common(env), {
|
||||
context: __dirname,
|
||||
entry: "./web/index.tsx",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist", "web"),
|
||||
},
|
||||
plugins: [
|
||||
new webpack.container.ModuleFederationPlugin({
|
||||
name: "exp_auto_restart",
|
||||
library: { type: "window", name: "plugin_exp_auto_restart" },
|
||||
exposes: {
|
||||
"./": "./index.ts",
|
||||
"./package.json": "./package.json",
|
||||
"./web": "./web/index.tsx",
|
||||
},
|
||||
shared: {
|
||||
"@clusterio/lib": { import: false },
|
||||
"@clusterio/web_ui": { import: false },
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -4,5 +4,5 @@ export const plugin: lib.PluginDeclaration = {
|
||||
name: "exp_commands",
|
||||
title: "exp_commands",
|
||||
description: "Example Description. Plugin. Change me in index.ts",
|
||||
instanceEntrypoint: "./dist/node/instance",
|
||||
instanceEntrypoint: "./dist/node/instance.js",
|
||||
};
|
||||
@@ -5,6 +5,7 @@
|
||||
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
|
||||
"license": "MIT",
|
||||
"repository": "explosivegaming/ExpCluster",
|
||||
"type": "module",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --env production"
|
||||
|
||||
File renamed without changes.
@@ -1,15 +1,23 @@
|
||||
import { BaseControllerPlugin } from "@clusterio/controller";
|
||||
import { BaseControllerPlugin, type Controller } from "@clusterio/controller";
|
||||
import * as lib from "@clusterio/lib";
|
||||
import * as messages from "./messages";
|
||||
import * as messages from "./messages.js";
|
||||
import * as path from "node:path";
|
||||
|
||||
const loaded = new WeakMap<Controller, ControllerPlugin>();
|
||||
|
||||
export class ControllerPlugin extends BaseControllerPlugin {
|
||||
groups!: lib.SubscribableDatastore<messages.GroupRecord>;
|
||||
roleMappings!: lib.SubscribableDatastore<messages.RoleMappingRecord>;
|
||||
manualAssignments!: lib.SubscribableDatastore<messages.AssignmentRecord>;
|
||||
resolvedAssignments!: lib.SubscribableDatastore<messages.AssignmentRecord>;
|
||||
|
||||
/** The plugin loaded on a controller, for the exp_scenario seed. */
|
||||
static get(controller: Controller) {
|
||||
return loaded.get(controller);
|
||||
}
|
||||
|
||||
async init() {
|
||||
loaded.set(this.controller, this);
|
||||
const databaseDirectory = this.controller.config.get("controller.database_directory");
|
||||
|
||||
this.groups = new lib.SubscribableDatastore(
|
||||
|
||||
+125
-120
@@ -1,5 +1,8 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
import * as messages from "./messages";
|
||||
import * as messages from "./messages.js";
|
||||
|
||||
export * from "./messages.js";
|
||||
export type { ControllerPlugin } from "./controller.js";
|
||||
|
||||
declare module "@clusterio/lib" {
|
||||
export interface InstanceConfigFields {
|
||||
@@ -9,123 +12,6 @@ declare module "@clusterio/lib" {
|
||||
}
|
||||
}
|
||||
|
||||
// Group permissions
|
||||
|
||||
lib.definePermission({
|
||||
name: "exp_groups.group.get",
|
||||
title: "Get Groups",
|
||||
description: "Retrieve a specific Factorio permission group by ID.",
|
||||
grantByDefault: true,
|
||||
});
|
||||
lib.definePermission({
|
||||
name: "exp_groups.group.list",
|
||||
title: "List Groups",
|
||||
description: "List all Factorio permission groups.",
|
||||
grantByDefault: true,
|
||||
});
|
||||
lib.definePermission({
|
||||
name: "exp_groups.group.subscribe",
|
||||
title: "Subscribe to Group Updates",
|
||||
description: "Receive updates when Factorio permission groups change.",
|
||||
grantByDefault: true,
|
||||
});
|
||||
lib.definePermission({
|
||||
name: "exp_groups.group.create",
|
||||
title: "Create Groups",
|
||||
description: "Create new Factorio permission groups.",
|
||||
grantByDefault: false,
|
||||
});
|
||||
lib.definePermission({
|
||||
name: "exp_groups.group.update",
|
||||
title: "Update Groups",
|
||||
description: "Modify existing Factorio permission groups.",
|
||||
grantByDefault: false,
|
||||
});
|
||||
lib.definePermission({
|
||||
name: "exp_groups.group.delete",
|
||||
title: "Delete Groups",
|
||||
description: "Delete Factorio permission groups.",
|
||||
grantByDefault: false,
|
||||
});
|
||||
|
||||
// Assignment permissions
|
||||
|
||||
lib.definePermission({
|
||||
name: "exp_groups.assignment.get",
|
||||
title: "Get Assignments",
|
||||
description: "Retrieve a specific manual group assignment for a player.",
|
||||
grantByDefault: true,
|
||||
});
|
||||
lib.definePermission({
|
||||
name: "exp_groups.assignment.list",
|
||||
title: "List Assignments",
|
||||
description: "List all manual group assignments.",
|
||||
grantByDefault: true,
|
||||
});
|
||||
lib.definePermission({
|
||||
name: "exp_groups.assignment.subscribe",
|
||||
title: "Subscribe to Assignment Updates",
|
||||
description: "Receive updates when manual group assignments change.",
|
||||
grantByDefault: true,
|
||||
});
|
||||
lib.definePermission({
|
||||
name: "exp_groups.assignment.create",
|
||||
title: "Create Assignments",
|
||||
description: "Manually assign players to groups, overriding role mappings.",
|
||||
grantByDefault: false,
|
||||
});
|
||||
lib.definePermission({
|
||||
name: "exp_groups.assignment.update",
|
||||
title: "Update Assignments",
|
||||
description: "Modify existing manual group assignments.",
|
||||
grantByDefault: false,
|
||||
});
|
||||
lib.definePermission({
|
||||
name: "exp_groups.assignment.delete",
|
||||
title: "Delete Assignments",
|
||||
description: "Remove manual group assignments.",
|
||||
grantByDefault: false,
|
||||
});
|
||||
|
||||
// Role mapping permissions
|
||||
|
||||
lib.definePermission({
|
||||
name: "exp_groups.role_mapping.get",
|
||||
title: "Get Role Mappings",
|
||||
description: "Retrieve a specific role mapping rule.",
|
||||
grantByDefault: true,
|
||||
});
|
||||
lib.definePermission({
|
||||
name: "exp_groups.role_mapping.list",
|
||||
title: "List Role Mappings",
|
||||
description: "List all role mapping rules.",
|
||||
grantByDefault: true,
|
||||
});
|
||||
lib.definePermission({
|
||||
name: "exp_groups.role_mapping.subscribe",
|
||||
title: "Subscribe to Role Mapping Updates",
|
||||
description: "Receive updates when role mapping rules change.",
|
||||
grantByDefault: true,
|
||||
});
|
||||
lib.definePermission({
|
||||
name: "exp_groups.role_mapping.create",
|
||||
title: "Create Role Mappings",
|
||||
description: "Create rules that map user roles to Factorio permission groups.",
|
||||
grantByDefault: false,
|
||||
});
|
||||
lib.definePermission({
|
||||
name: "exp_groups.role_mapping.update",
|
||||
title: "Update Role Mappings",
|
||||
description: "Modify existing role mapping rules.",
|
||||
grantByDefault: false,
|
||||
});
|
||||
lib.definePermission({
|
||||
name: "exp_groups.role_mapping.delete",
|
||||
title: "Delete Role Mappings",
|
||||
description: "Delete role mapping rules.",
|
||||
grantByDefault: false,
|
||||
});
|
||||
|
||||
export const plugin: lib.PluginDeclaration = {
|
||||
name: "exp_groups",
|
||||
title: "ExpGaming - Permission Groups",
|
||||
@@ -161,7 +47,126 @@ export const plugin: lib.PluginDeclaration = {
|
||||
messages.RoleMappingListRequest,
|
||||
],
|
||||
|
||||
instanceEntrypoint: "./dist/node/instance",
|
||||
permissions: [
|
||||
// Group permissions
|
||||
|
||||
{
|
||||
name: "exp_groups.group.get",
|
||||
title: "Get Groups",
|
||||
description: "Retrieve a specific Factorio permission group by ID.",
|
||||
grantByDefault: true,
|
||||
},
|
||||
{
|
||||
name: "exp_groups.group.list",
|
||||
title: "List Groups",
|
||||
description: "List all Factorio permission groups.",
|
||||
grantByDefault: true,
|
||||
},
|
||||
{
|
||||
name: "exp_groups.group.subscribe",
|
||||
title: "Subscribe to Group Updates",
|
||||
description: "Receive updates when Factorio permission groups change.",
|
||||
grantByDefault: true,
|
||||
},
|
||||
{
|
||||
name: "exp_groups.group.create",
|
||||
title: "Create Groups",
|
||||
description: "Create new Factorio permission groups.",
|
||||
grantByDefault: false,
|
||||
},
|
||||
{
|
||||
name: "exp_groups.group.update",
|
||||
title: "Update Groups",
|
||||
description: "Modify existing Factorio permission groups.",
|
||||
grantByDefault: false,
|
||||
},
|
||||
{
|
||||
name: "exp_groups.group.delete",
|
||||
title: "Delete Groups",
|
||||
description: "Delete Factorio permission groups.",
|
||||
grantByDefault: false,
|
||||
},
|
||||
|
||||
// Assignment permissions
|
||||
|
||||
{
|
||||
name: "exp_groups.assignment.get",
|
||||
title: "Get Assignments",
|
||||
description: "Retrieve a specific manual group assignment for a player.",
|
||||
grantByDefault: true,
|
||||
},
|
||||
{
|
||||
name: "exp_groups.assignment.list",
|
||||
title: "List Assignments",
|
||||
description: "List all manual group assignments.",
|
||||
grantByDefault: true,
|
||||
},
|
||||
{
|
||||
name: "exp_groups.assignment.subscribe",
|
||||
title: "Subscribe to Assignment Updates",
|
||||
description: "Receive updates when manual group assignments change.",
|
||||
grantByDefault: true,
|
||||
},
|
||||
{
|
||||
name: "exp_groups.assignment.create",
|
||||
title: "Create Assignments",
|
||||
description: "Manually assign players to groups, overriding role mappings.",
|
||||
grantByDefault: false,
|
||||
},
|
||||
{
|
||||
name: "exp_groups.assignment.update",
|
||||
title: "Update Assignments",
|
||||
description: "Modify existing manual group assignments.",
|
||||
grantByDefault: false,
|
||||
},
|
||||
{
|
||||
name: "exp_groups.assignment.delete",
|
||||
title: "Delete Assignments",
|
||||
description: "Remove manual group assignments.",
|
||||
grantByDefault: false,
|
||||
},
|
||||
|
||||
// Role mapping permissions
|
||||
|
||||
{
|
||||
name: "exp_groups.role_mapping.get",
|
||||
title: "Get Role Mappings",
|
||||
description: "Retrieve a specific role mapping rule.",
|
||||
grantByDefault: true,
|
||||
},
|
||||
{
|
||||
name: "exp_groups.role_mapping.list",
|
||||
title: "List Role Mappings",
|
||||
description: "List all role mapping rules.",
|
||||
grantByDefault: true,
|
||||
},
|
||||
{
|
||||
name: "exp_groups.role_mapping.subscribe",
|
||||
title: "Subscribe to Role Mapping Updates",
|
||||
description: "Receive updates when role mapping rules change.",
|
||||
grantByDefault: true,
|
||||
},
|
||||
{
|
||||
name: "exp_groups.role_mapping.create",
|
||||
title: "Create Role Mappings",
|
||||
description: "Create rules that map user roles to Factorio permission groups.",
|
||||
grantByDefault: false,
|
||||
},
|
||||
{
|
||||
name: "exp_groups.role_mapping.update",
|
||||
title: "Update Role Mappings",
|
||||
description: "Modify existing role mapping rules.",
|
||||
grantByDefault: false,
|
||||
},
|
||||
{
|
||||
name: "exp_groups.role_mapping.delete",
|
||||
title: "Delete Role Mappings",
|
||||
description: "Delete role mapping rules.",
|
||||
grantByDefault: false,
|
||||
},
|
||||
],
|
||||
|
||||
instanceEntrypoint: "./dist/node/instance.js",
|
||||
instanceConfigFields: {
|
||||
"exp_groups.sync_mode": {
|
||||
description: "Synchronize permission groups with the controller",
|
||||
@@ -171,7 +176,7 @@ export const plugin: lib.PluginDeclaration = {
|
||||
},
|
||||
},
|
||||
|
||||
controllerEntrypoint: "./dist/node/controller",
|
||||
controllerEntrypoint: "./dist/node/controller.js",
|
||||
controllerConfigFields: {
|
||||
},
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BaseInstancePlugin } from "@clusterio/host";
|
||||
import * as lib from "@clusterio/lib";
|
||||
import * as messages from "./messages";
|
||||
import * as messages from "./messages.js";
|
||||
|
||||
export type IpcGroupUpdated = {
|
||||
group_name: string,
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { Type, Static } from "@sinclair/typebox";
|
||||
|
||||
// Declared here rather than in index.ts because the browser project cannot include index.ts, which re-exports controller.ts
|
||||
declare module "@clusterio/lib" {
|
||||
export interface Permissions {
|
||||
"exp_groups.group.get": never;
|
||||
"exp_groups.group.list": never;
|
||||
"exp_groups.group.subscribe": never;
|
||||
"exp_groups.group.create": never;
|
||||
"exp_groups.group.update": never;
|
||||
"exp_groups.group.delete": never;
|
||||
"exp_groups.assignment.get": never;
|
||||
"exp_groups.assignment.list": never;
|
||||
"exp_groups.assignment.subscribe": never;
|
||||
"exp_groups.assignment.create": never;
|
||||
"exp_groups.assignment.update": never;
|
||||
"exp_groups.assignment.delete": never;
|
||||
"exp_groups.role_mapping.get": never;
|
||||
"exp_groups.role_mapping.list": never;
|
||||
"exp_groups.role_mapping.subscribe": never;
|
||||
"exp_groups.role_mapping.create": never;
|
||||
"exp_groups.role_mapping.update": never;
|
||||
"exp_groups.role_mapping.delete": never;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Data records
|
||||
*/
|
||||
|
||||
@@ -102,11 +102,11 @@ local function encode_group_permissions(group)
|
||||
local allows_action = group.allows_action
|
||||
for input_action_name, input_action in pairs(defines.input_action) do
|
||||
if allows_action(input_action) then
|
||||
whitelist[whitelist_index] = input_action_name
|
||||
whitelist_index = whitelist_index + 1
|
||||
whitelist[whitelist_index] = input_action_name
|
||||
else
|
||||
blacklist[blacklist_index] = input_action_name
|
||||
blacklist_index = blacklist_index + 1
|
||||
blacklist[blacklist_index] = input_action_name
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
|
||||
"license": "MIT",
|
||||
"repository": "explosivegaming/ExpCluster",
|
||||
"type": "module",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --env production"
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useContext, useEffect } from "react";
|
||||
import { Modal, Form, Select } from "antd";
|
||||
|
||||
import { ControlContext, useUsers } from "@clusterio/web_ui";
|
||||
import * as messages from "../../messages";
|
||||
import * as messages from "../../messages.js";
|
||||
import type { WebPlugin } from "..";
|
||||
|
||||
export default function AssignmentForm({ open, setOpen, initial }: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Table, Button, Space, Input, InputRef } from "antd";
|
||||
import { EditOutlined, SearchOutlined } from "@ant-design/icons";
|
||||
|
||||
import { ControlContext, useAccount } from "@clusterio/web_ui";
|
||||
import { AssignmentDeleteRequest, AssignmentRecord } from "../../messages";
|
||||
import { AssignmentDeleteRequest, AssignmentRecord } from "../../messages.js";
|
||||
import type { WebPlugin } from "..";
|
||||
|
||||
import AssignmentForm from "./AssignmentForm";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Modal, Form, Input, Switch } from "antd";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { ControlContext } from "@clusterio/web_ui";
|
||||
import * as messages from "../../messages";
|
||||
import * as messages from "../../messages.js";
|
||||
|
||||
export default function GroupForm({ open, setOpen }: {
|
||||
open: boolean,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Button, Checkbox, Input, Space, Spin, Alert } from "antd";
|
||||
import { ControlContext, useAccount, useDefaultModPack, PageLayout, PageHeader, notifyErrorHandler } from "@clusterio/web_ui";
|
||||
import DeletedConfirm from "./DeleteConfirm";
|
||||
|
||||
import * as messages from "../../messages";
|
||||
import * as messages from "../../messages.js";
|
||||
import type { WebPlugin } from "..";
|
||||
|
||||
const DOMAIN_MAPPING = {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Table } from "antd";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { ControlContext } from "@clusterio/web_ui";
|
||||
import { GroupRecord } from "../../messages";
|
||||
import { GroupRecord } from "../../messages.js";
|
||||
import type { WebPlugin } from "..";
|
||||
|
||||
import GroupForm from "./GroupForm";
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useContext, useEffect } from "react";
|
||||
import { Modal, Form, Select, InputNumber, Switch, Alert } from "antd";
|
||||
|
||||
import { ControlContext, useRoles } from "@clusterio/web_ui";
|
||||
import { RoleMappingRecord, RoleMappingCreateRequest, RoleMappingUpdateRequest } from "../../messages";
|
||||
import { RoleMappingRecord, RoleMappingCreateRequest, RoleMappingUpdateRequest } from "../../messages.js";
|
||||
import type { WebPlugin } from "..";
|
||||
|
||||
export default function RoleMappingForm({ open, setOpen, initial }: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Table, Button, Space, Tag } from "antd";
|
||||
import { EditOutlined } from "@ant-design/icons";
|
||||
|
||||
import { ControlContext, useAccount, useRoles } from "@clusterio/web_ui";
|
||||
import { RoleMappingRecord, RoleMappingDeleteRequest } from "../../messages";
|
||||
import { RoleMappingRecord, RoleMappingDeleteRequest } from "../../messages.js";
|
||||
import type { WebPlugin } from "..";
|
||||
|
||||
import RoleMappingForm from "./RoleMappingForm";
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useCallback, useSyncExternalStore } from "react";
|
||||
import { BaseWebPlugin, PageLayout, PageHeader, useAccount, SectionHeader } from "@clusterio/web_ui";
|
||||
import { Button } from "antd";
|
||||
|
||||
import * as messages from "../messages";
|
||||
import * as messages from "../messages.js";
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
import GroupsTable from "./components/GroupsTable";
|
||||
|
||||
File renamed without changes.
+1
-1
@@ -4,5 +4,5 @@ export const plugin: lib.PluginDeclaration = {
|
||||
name: "exp_gui",
|
||||
title: "exp_gui",
|
||||
description: "Example Description. Plugin. Change me in index.ts",
|
||||
instanceEntrypoint: "./dist/node/instance",
|
||||
instanceEntrypoint: "./dist/node/instance.js",
|
||||
};
|
||||
@@ -5,6 +5,7 @@
|
||||
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
|
||||
"license": "MIT",
|
||||
"repository": "explosivegaming/ExpCluster",
|
||||
"type": "module",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --env production"
|
||||
|
||||
File renamed without changes.
+1
-1
@@ -4,5 +4,5 @@ export const plugin: lib.PluginDeclaration = {
|
||||
name: "exp_legacy",
|
||||
title: "exp_legacy",
|
||||
description: "Example Description. Plugin. Change me in index.ts",
|
||||
instanceEntrypoint: "./dist/node/instance",
|
||||
instanceEntrypoint: "./dist/node/instance.js",
|
||||
};
|
||||
@@ -6,9 +6,6 @@
|
||||
return {
|
||||
"expcore.player_data", -- must be loaded first to register event handlers
|
||||
|
||||
-- Control
|
||||
"modules.control.vlayer",
|
||||
|
||||
--- Data
|
||||
"modules.data.statistics",
|
||||
"modules.data.player-colours",
|
||||
@@ -17,13 +14,5 @@ return {
|
||||
"modules.data.alt-view",
|
||||
"modules.data.tag",
|
||||
"modules.data.language",
|
||||
--"modules.data.toolbar",
|
||||
|
||||
--- GUI
|
||||
"modules.gui.warp-list",
|
||||
"modules.gui.vlayer",
|
||||
|
||||
"modules.graftorio.require", -- graftorio
|
||||
--- Config Files
|
||||
"config.expcore.permission_groups", -- loads some predefined permission groups
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
--- Use this file to add new permission groups to the game;
|
||||
-- start with Permission_Groups.new_group('name');
|
||||
-- then use either :allow_all() or :disallow_all() to set the default for non specified actions;
|
||||
-- then use :allow{} and :disallow{} to specify certain actions to allow/disallow
|
||||
-- @config Permission-Groups
|
||||
|
||||
-- local Event = require("modules/exp_legacy/utils/event") -- @dep utils.event
|
||||
local Groups = require("modules.exp_legacy.expcore.permission_groups") --- @dep expcore.permission_groups
|
||||
|
||||
Groups.new_group("Admin")
|
||||
:allow_all()
|
||||
:disallow{
|
||||
"add_permission_group", -- admin
|
||||
"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",
|
||||
}
|
||||
|
||||
Groups.new_group("Trusted")
|
||||
:allow_all()
|
||||
:disallow{
|
||||
"add_permission_group", -- admin
|
||||
"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",
|
||||
"admin_action", -- trusted
|
||||
}
|
||||
|
||||
Groups.new_group("Standard")
|
||||
:allow_all()
|
||||
:disallow{
|
||||
"add_permission_group", -- admin
|
||||
"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",
|
||||
"admin_action", -- trusted
|
||||
"change_programmable_speaker_alert_parameters", -- standard
|
||||
"drop_item",
|
||||
"open_new_platform_button_from_rocket_silo",
|
||||
"set_rocket_silo_send_to_orbit_automated_mode",
|
||||
}
|
||||
|
||||
Groups.new_group("Guest")
|
||||
:allow_all()
|
||||
:disallow{
|
||||
"add_permission_group", -- admin
|
||||
"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",
|
||||
"admin_action", -- trusted
|
||||
"change_programmable_speaker_alert_parameters", -- standard
|
||||
"drop_item",
|
||||
"open_new_platform_button_from_rocket_silo",
|
||||
"set_rocket_silo_send_to_orbit_automated_mode",
|
||||
"change_programmable_speaker_parameters", -- guest
|
||||
"change_train_stop_station",
|
||||
-- 'deconstruct',
|
||||
"remove_cables",
|
||||
"remove_train_station",
|
||||
"reset_assembling_machine",
|
||||
"rotate_entity",
|
||||
-- 'use_artillery_remote', -- not in 2.0
|
||||
"launch_rocket",
|
||||
"cancel_research",
|
||||
-- 'activate_cut', -- not in 2.0
|
||||
"flush_opened_entity_fluid",
|
||||
"flush_opened_entity_specific_fluid",
|
||||
}
|
||||
|
||||
Groups.new_group("Restricted")
|
||||
:disallow_all()
|
||||
:allow("write_to_console")
|
||||
|
||||
--[[ These events are used until a role system is added to make it easier for our admins
|
||||
|
||||
local trusted_time = 60*60*60*10 -- 10 hour
|
||||
local standard_time = 60*60*60*3 -- 3 hour
|
||||
local function assign_group(player)
|
||||
local current_group_name = player.permission_group and player.permission_group.name or 'None'
|
||||
if player.admin then
|
||||
Permission_Groups.set_player_group(player,'Admin')
|
||||
elseif player.online_time > trusted_time or current_group_name == 'Trusted' then
|
||||
Permission_Groups.set_player_group(player,'Trusted')
|
||||
elseif player.online_time > standard_time or current_group_name == 'Standard' then
|
||||
Permission_Groups.set_player_group(player,'Standard')
|
||||
else
|
||||
Permission_Groups.set_player_group(player,'Guest')
|
||||
end
|
||||
end
|
||||
|
||||
Event.add(defines.events.on_player_joined_game,function(event)
|
||||
local player = game.players[event.player_index]
|
||||
assign_group(player)
|
||||
end)
|
||||
|
||||
Event.add(defines.events.on_player_promoted,function(event)
|
||||
local player = game.players[event.player_index]
|
||||
assign_group(player)
|
||||
end)
|
||||
|
||||
Event.add(defines.events.on_player_demoted,function(event)
|
||||
local player = game.players[event.player_index]
|
||||
assign_group(player)
|
||||
end)
|
||||
|
||||
local check_interval = 60*60*15 -- 15 minutes
|
||||
Event.on_nth_tick(check_interval,function(event)
|
||||
for _,player in pairs(game.connected_players) do
|
||||
assign_group(player)
|
||||
end
|
||||
end)]]
|
||||
@@ -1,355 +0,0 @@
|
||||
--[[-- Core Module - Permission Groups
|
||||
- Permission group making for factorio so you never have to make one by hand again
|
||||
@core Groups
|
||||
@alias Permissions_Groups
|
||||
|
||||
@usage--- Example Group (Allow All)
|
||||
-- here we will create an admin group however we do not want them to use the map editor or mess with the permission groups
|
||||
Permission_Groups.new_group('Admin') -- this defines a new group called "Admin"
|
||||
:allow_all() -- this makes the default to allow any input action unless set other wise
|
||||
:disallow{ -- here we disallow the input action we don't want them to use
|
||||
'add_permission_group',
|
||||
'delete_permission_group',
|
||||
'import_permissions_string',
|
||||
'map_editor_action',
|
||||
'toggle_map_editor'
|
||||
}
|
||||
|
||||
@usage--- Example Group (Disallow All)
|
||||
-- here we will create a group that cant do anything but talk in chat
|
||||
Permission_Groups.new_group('Restricted') -- this defines a new group called "Restricted"
|
||||
:disallow_all() -- this makes the default to disallow any input action unless set other wise
|
||||
:allow('write_to_console') -- here we allow them to chat, {} can be used here if we had more than one action
|
||||
|
||||
]]
|
||||
|
||||
local Event = require("modules/exp_legacy/utils/event")
|
||||
local Async = require("modules/exp_util/async")
|
||||
|
||||
local PermissionsGroups = {
|
||||
groups = {}, -- store for the different groups that are created
|
||||
_prototype = {}, -- stores functions that are used on group instances
|
||||
}
|
||||
|
||||
-- Async function to add players to permission groups
|
||||
local add_to_permission_group_async =
|
||||
Async.register(function(permission_group, player)
|
||||
permission_group.add_player(player)
|
||||
end)
|
||||
PermissionsGroups.add_to_permission_group_async = add_to_permission_group_async
|
||||
|
||||
-- Async function to remove players from permission groups
|
||||
local remove_from_permission_group_async =
|
||||
Async.register(function(permission_group, player)
|
||||
permission_group.remove_player(player)
|
||||
end)
|
||||
PermissionsGroups.remove_from_permission_group_async = remove_from_permission_group_async
|
||||
|
||||
--- Getters.
|
||||
-- Functions that get permission groups
|
||||
-- @section getters
|
||||
|
||||
--[[-- Defines a new permission group that can have it actions set in the config
|
||||
@tparam string name the name of the new group
|
||||
@treturn Permissions_Groups._prototype the new group made with function to allow and disallow actions
|
||||
|
||||
@usage-- Defining a new permission group
|
||||
Groups.new_group('Admin')
|
||||
|
||||
]]
|
||||
function PermissionsGroups.new_group(name)
|
||||
local group = setmetatable({
|
||||
name = name,
|
||||
actions = {},
|
||||
allow_all_actions = true,
|
||||
}, {
|
||||
__index = PermissionsGroups._prototype,
|
||||
})
|
||||
PermissionsGroups.groups[name] = group
|
||||
return group
|
||||
end
|
||||
|
||||
--[[-- Returns the group with the given name, case sensitive
|
||||
@tparam string name the name of the group to get
|
||||
@treturn ?Permissions_Groups._prototype|nil the group with that name or nil if non found
|
||||
|
||||
@usage-- Getting a permision group
|
||||
local admin_group = Groups.get_group_by_name('Admin')
|
||||
|
||||
]]
|
||||
function PermissionsGroups.get_group_by_name(name)
|
||||
return PermissionsGroups.groups[name]
|
||||
end
|
||||
|
||||
--[[-- Returns the group that a player is in
|
||||
@tparam LuaPlayer player the player to get the group of can be name index etc
|
||||
@treturn ?Permissions_Groups._prototype|nil the group with that player or nil if non found
|
||||
|
||||
@usage-- Get your permission group
|
||||
local group = Groups.get_group_from_player(game.player)
|
||||
|
||||
]]
|
||||
function PermissionsGroups.get_group_from_player(player)
|
||||
local group = player.permission_group
|
||||
if group then
|
||||
return PermissionsGroups.groups[group.name]
|
||||
end
|
||||
end
|
||||
|
||||
--- Setters.
|
||||
-- Functions that control all groups
|
||||
-- @section players
|
||||
|
||||
--[[-- Reloads/creates all permission groups and sets them to they configured state
|
||||
|
||||
@usage-- Reload the permission groups, used internally
|
||||
Groups.reload_permissions()
|
||||
|
||||
]]
|
||||
function PermissionsGroups.reload_permissions()
|
||||
for _, group in pairs(PermissionsGroups.groups) do
|
||||
group:create()
|
||||
end
|
||||
end
|
||||
|
||||
--[[-- Sets a player's group to the one given, a player can only have one group at a time
|
||||
@tparam LuaPlayer player the player to effect can be name index etc
|
||||
@tparam string group the name of the group to give to the player
|
||||
@treturn boolean true if the player was added successfully, false other wise
|
||||
|
||||
@usage-- Set your permission group
|
||||
Groups.set_player_group(game.player, 'Admin')
|
||||
|
||||
]]
|
||||
function PermissionsGroups.set_player_group(player, group)
|
||||
group = PermissionsGroups.get_group_by_name(group)
|
||||
if not group or not player then return false end
|
||||
group:add_player(player)
|
||||
return true
|
||||
end
|
||||
|
||||
--- Actions.
|
||||
-- Functions that control group actions
|
||||
-- @section actions
|
||||
|
||||
--[[-- Sets the allow state of an action for this group, used internally but is safe to use else where
|
||||
@tparam ?string|defines.input_action action the action that you want to set the state of
|
||||
@tparam boolean state the state that you want to set it to, true = allow, false = disallow
|
||||
@treturn Permissions_Groups._prototype returns self so function can be chained
|
||||
|
||||
@usage-- Set an action to be disallowed
|
||||
group:set_action('toggle_map_editor', false)
|
||||
|
||||
]]
|
||||
function PermissionsGroups._prototype:set_action(action, state)
|
||||
local input_action = defines.input_action[action] --[[@as defines.input_action?]]
|
||||
if input_action == nil then input_action = action end
|
||||
assert(type(input_action) == "number", tostring(action) .. " is not a valid input action")
|
||||
self.actions[input_action] = state
|
||||
return self
|
||||
end
|
||||
|
||||
--[[-- Sets an action or actions to be allowed for this group even with disallow_all triggered, Do not use in runtime
|
||||
@tparam string|Array<string> actions the action or actions that you want to allow for this group
|
||||
@treturn Permissions_Groups._prototype returns self so function can be chained
|
||||
|
||||
@usage-- Allow some actions
|
||||
group:allow{
|
||||
'write_to_console'
|
||||
}
|
||||
|
||||
]]
|
||||
function PermissionsGroups._prototype:allow(actions)
|
||||
if type(actions) ~= "table" then
|
||||
actions = { actions }
|
||||
end
|
||||
for _, action in pairs(actions) do
|
||||
self:set_action(action, true)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--[[-- Sets an action or actions to be disallowed for this group even with allow_all triggered, Do not use in runtime
|
||||
@tparam string|Array<string> actions the action or actions that you want to disallow for this group
|
||||
@treturn Permissions_Groups._prototype returns self so function can be chained
|
||||
|
||||
@usage-- Disalow some actions
|
||||
group:disallow{
|
||||
'add_permission_group',
|
||||
'delete_permission_group',
|
||||
'import_permissions_string',
|
||||
'map_editor_action',
|
||||
'toggle_map_editor'
|
||||
}
|
||||
|
||||
]]
|
||||
function PermissionsGroups._prototype:disallow(actions)
|
||||
if type(actions) ~= "table" then
|
||||
actions = { actions }
|
||||
end
|
||||
for _, action in pairs(actions) do
|
||||
self:set_action(action, false)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--[[-- Sets the default state for any actions not given to be allowed, useful with :disallow
|
||||
@treturn Permissions_Groups._prototype returns self so function can be chained
|
||||
|
||||
@usage-- Allow all actions unless given by disallow
|
||||
group:allow_all()
|
||||
|
||||
]]
|
||||
function PermissionsGroups._prototype:allow_all()
|
||||
self.allow_all_actions = true
|
||||
return self
|
||||
end
|
||||
|
||||
--[[-- Sets the default state for any action not given to be disallowed, useful with :allow
|
||||
@treturn Permissions_Groups._prototype returns self so function can be chained
|
||||
|
||||
@usage-- Disallow all actions unless given by allow
|
||||
group:disallow_all()
|
||||
|
||||
]]
|
||||
function PermissionsGroups._prototype:disallow_all()
|
||||
self.allow_all_actions = false
|
||||
return self
|
||||
end
|
||||
|
||||
--[[-- Returns if an input action is allowed for this group
|
||||
@tparam ?string|defines.input_action action the action that you want to test for
|
||||
@treturn boolean true if the group is allowed the action, false other wise
|
||||
|
||||
@usage-- Test if a group is allowed an action
|
||||
local allowed = group:is_allowed('write_to_console')
|
||||
|
||||
]]
|
||||
function PermissionsGroups._prototype:is_allowed(action)
|
||||
if type(action) == "string" then
|
||||
action = defines.input_action[action]
|
||||
end
|
||||
local state = self.actions[action]
|
||||
if state == nil then
|
||||
state = self.allow_all_actions
|
||||
end
|
||||
return state
|
||||
end
|
||||
|
||||
--- Players.
|
||||
-- Functions that control group players
|
||||
-- @section players
|
||||
|
||||
--[[-- Creates or updates the permission group with the configured actions, used internally
|
||||
@treturn LuaPermissionGroup the permission group that was created
|
||||
|
||||
@usage-- Create the permission group so players can be added, used internally
|
||||
group:create()
|
||||
|
||||
]]
|
||||
function PermissionsGroups._prototype:create()
|
||||
local group = self:get_raw()
|
||||
if not group then
|
||||
group = game.permissions.create_group(self.name) --[[@as LuaPermissionGroup]]
|
||||
end
|
||||
for _, action in pairs(defines.input_action) do
|
||||
group.set_allows_action(action, self:is_allowed(action))
|
||||
end
|
||||
|
||||
return group
|
||||
end
|
||||
|
||||
--[[-- Returns the LuaPermissionGroup that was created with this group object, used internally
|
||||
@treturn LuaPermissionGroup the raw lua permission group
|
||||
|
||||
@usage-- Get the factorio api permision group, used internally
|
||||
local permission_group = group:get_raw()
|
||||
|
||||
]]
|
||||
function PermissionsGroups._prototype:get_raw()
|
||||
return game.permissions.get_group(self.name)
|
||||
end
|
||||
|
||||
--[[-- Adds a player to this group
|
||||
@tparam LuaPlayer player LuaPlayer the player you want to add to this group can be name or index etc
|
||||
@treturn boolean true if the player was added successfully, false other wise
|
||||
|
||||
@usage-- Add a player to this permission group
|
||||
group:add_player(game.player)
|
||||
|
||||
]]
|
||||
function PermissionsGroups._prototype:add_player(player)
|
||||
local group = self:get_raw()
|
||||
if not group or not player then return false end
|
||||
add_to_permission_group_async(group, player)
|
||||
return true
|
||||
end
|
||||
|
||||
--[[-- Removes a player from this group
|
||||
@tparam LuaPlayer player LuaPlayer the player you want to remove from this group can be name or index etc
|
||||
@treturn boolean true if the player was removed successfully, false other wise
|
||||
|
||||
@usage-- Remove a player from this permission group
|
||||
group:remove_player(game.player)
|
||||
|
||||
]]
|
||||
function PermissionsGroups._prototype:remove_player(player)
|
||||
local group = self:get_raw()
|
||||
if not group or not player then return false end
|
||||
remove_from_permission_group_async(group, player)
|
||||
return true
|
||||
end
|
||||
|
||||
--[[-- Returns all player that are in this group with the option to filter to online/offline only
|
||||
@tparam[opt] boolean online if nil returns all players, if true online players only, if false returns online players only
|
||||
@treturn table a table of players that are in this group; filtered if online param is given
|
||||
|
||||
@usage-- Get all players in this group
|
||||
local online_players = group:get_players()
|
||||
|
||||
@usage-- Get all online players in this group
|
||||
local online_players = group:get_players(true)
|
||||
|
||||
]]
|
||||
function PermissionsGroups._prototype:get_players(online)
|
||||
local players = {}
|
||||
local group = self:get_raw()
|
||||
if group then
|
||||
if online == nil then
|
||||
return group.players
|
||||
else
|
||||
for _, player in pairs(group.players) do
|
||||
if player.connected == online then
|
||||
table.insert(player, player)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return players
|
||||
end
|
||||
|
||||
--[[-- Prints a message to every player in this group
|
||||
@tparam string message the message that you want to send to the players
|
||||
@treturn number the number of players that received the message
|
||||
|
||||
@usage-- Print a message to all players in thie group
|
||||
group:print('Hello, World!')
|
||||
|
||||
]]
|
||||
function PermissionsGroups._prototype:print(message)
|
||||
local players = self:get_players(true)
|
||||
for _, player in pairs(players) do
|
||||
player.print(message)
|
||||
end
|
||||
|
||||
return #players
|
||||
end
|
||||
|
||||
-- when the game starts it will make the permission groups
|
||||
Event.on_init(function()
|
||||
PermissionsGroups.reload_permissions()
|
||||
end)
|
||||
|
||||
return PermissionsGroups
|
||||
@@ -158,37 +158,6 @@ ban-player=Ban player
|
||||
afk-time=__1__% of total map time\nLast moved __2__ ago
|
||||
open-map=__1__ __2__\n__3__\nClick to open map
|
||||
|
||||
[warp-list]
|
||||
main-caption=Warp List [img=info]
|
||||
main-tooltip=Warp List
|
||||
sub-tooltip=Warps can only be used every __1__ seconds and when within __2__ tiles\n__3__\n__4__\n__5__\n__6__\n__7__\n__8__
|
||||
sub-tooltip-current= - __1__ This is your current warp point;
|
||||
sub-tooltip-connected= - __1__ You can travel to this warp point;
|
||||
sub-tooltip-different= - __1__ This warp is on a different network;
|
||||
sub-tooltip-cooldown= - __1__ You are currently on cooldown;
|
||||
sub-tooltip-not_available= - __1__ You are not in range of a warp point;
|
||||
sub-tooltip-bypass= - __1__ Your role allows you to travel here;
|
||||
too-close=Cannot create warp; too close to existing warp point: __1__
|
||||
too-close-to-water=Cannot create warp; too close to water, please move __1__ tiles away from the water body or landfill it
|
||||
too-close-to-entities=Cannot create warp; too close to other entities, please move __1__ tiles away from the entities or remove them
|
||||
last-edit=Last edited by __1__ at __2__\nClick to view on map
|
||||
add-tooltip=Add new warp
|
||||
confirm-tooltip=Save changes
|
||||
cancel-tooltip=Discard changes
|
||||
edit-tooltip=Currently being edited by: __1__
|
||||
edit-tooltip-none=Currently being edited by: Nobody
|
||||
remove-tooltip=Remove warp
|
||||
timer-tooltip=Please wait __1__ seconds before you can warp
|
||||
timer-tooltip-zero=After each warp you will need to wait __1__ seconds before you can warp again
|
||||
goto-tooltip=Go to x __1__ y __2__
|
||||
goto-bypass=Go to x __1__ y __2__, bypass mode
|
||||
goto-bypass-different-network=Go to x __1__ y __2__, bypass mode different network
|
||||
goto-same-warp=You are already on this warp
|
||||
goto-different-network=This warp is on a different network, use power poles to connect it
|
||||
goto-cooldown=You are on cooldown, wait for your cooldown to recharge
|
||||
goto-disabled=You are not on a warp point, walk to a warp point
|
||||
goto-edit=Edit warp icon
|
||||
|
||||
[bonus]
|
||||
description=Get / Set the amount of bonus you receive.
|
||||
arg-amount=Amount to set your bonus to, 0 will disable bonus.
|
||||
@@ -215,42 +184,3 @@ display-fwrsb=Robot Storage
|
||||
display-fwrsb-tooltip=Force worker robots storage bonus
|
||||
display-ffrlm=Robot Lifetime
|
||||
display-ffrlm-tooltip=Force following robots lifetime modifier
|
||||
|
||||
[vlayer]
|
||||
main-tooltip=vlayer
|
||||
description-pbr=Recharge Player Battery upto a portion with vlayer
|
||||
description-vi=Vlayer Info
|
||||
pbr-not-running=vlayer need to be running
|
||||
display-item-solar=[img=entity/solar-panel] Solar Panel
|
||||
display-item-accumulator=[img=entity/accumulator] Accumulator
|
||||
display-current-production=[virtual-signal=signal-P] Current Production
|
||||
display-sustained-production=[virtual-signal=signal-S] Sustained Production
|
||||
display-max-capacity=[virtual-signal=signal-C] Battery Capacity
|
||||
display-current-capacity=[virtual-signal=signal-E] Battery Charge
|
||||
display-remaining-surface-area=[virtual-signal=signal-R] Remaining Surface
|
||||
display-item-solar-tooltip=The amount of Solar Panel
|
||||
display-item-accumulator-tooltip=The amount of Accumulator
|
||||
display-current-production-tooltip=Current Power Production
|
||||
display-sustained-production-tooltip=Sustained Power Production
|
||||
display-max-capacity-tooltip=Battery Max Capacity
|
||||
display-current-capacity-tooltip=Battery Current Charge
|
||||
display-remaining-surface-area-tooltip=Remaining Surface. Insert Landfill to increase
|
||||
steel-chest-detect=No steel chest detected
|
||||
steel-chest-empty=Chest is not emptied
|
||||
control-refresh=Refresh List
|
||||
control-see=See Interface
|
||||
control-build=Build Interface
|
||||
control-remove=Remove Interface
|
||||
result-build=built
|
||||
result-remove=removed
|
||||
result-interface-location=__1__ interface was located at __2__
|
||||
interface-result=__1__ __2__ __4__ interface at __3__
|
||||
result-unable=Unable to build __1__ interface, because of __2__
|
||||
result-multiple=Multiple steel chest detected
|
||||
result-limit=Max limit exceeded
|
||||
result-space=Not enough space
|
||||
control-type-energy=Energy
|
||||
control-type-circuit=Circuit
|
||||
control-type-storage-input=Storage Input
|
||||
control-type-storage-output=Storage Output
|
||||
power-on-space-research=You need to research [technology=__1__] at __2__ level for the space platform vlayer feature.
|
||||
@@ -8,17 +8,17 @@ status=伺服器狀態: __1__
|
||||
github=增加功能或回報錯誤: __1__
|
||||
patreon=支持我們: __1__
|
||||
custom-commands=這裹使用了自制指令,如 /tag 和 /me。可使用 /chelp 查看更多。
|
||||
read-readme=確保你已閱讀相關資訊。按左上 i 圖標可再次查看。
|
||||
read-readme=確保你已閱讀相關資訊。按左上 i 圖標可再次查看
|
||||
softmod=這裹用了自設情境,是一種軟裝模組。
|
||||
redmew=
|
||||
redmew=We don't talk about redmew here; they beat us to 1000 members ;-;
|
||||
lhd=列車必須是左則通行。這是本服務器長久以來的規則。
|
||||
|
||||
# data
|
||||
[join-message]
|
||||
description-add=設定 / 取得你的加入信息。
|
||||
description-remove=移除你的加入信息。
|
||||
arg-message=你的加入信息。
|
||||
greet=[color=0,1,0] 歡迎來到 __1__ 的服務器! 若果你喜歡本服務器可加入 DISCORD: __2__ [/color]
|
||||
arg-message=你的加入信息。
|
||||
greet=[color=0,1,0] 歡迎來到 __1__ 的服務器! 若果你喜歡本服務器可加入 Discord: __2__ [/color]
|
||||
message-set=你的加入信息已更新。
|
||||
message-get=你的加入信息為: __1__
|
||||
message-cleared=你的加入信息已清除。
|
||||
@@ -127,7 +127,7 @@ Locale-tooltip=用戶介面語言
|
||||
# expcore
|
||||
[expcore-data]
|
||||
description-preference=允許寫入資料。
|
||||
description-data=把用戶資料寫入電腦
|
||||
description-data=把用戶資料寫入電腦。
|
||||
arg-option="All", "Statistics", "Settings", "Required" 之中
|
||||
set-preference=你的喜好已設定為 __1__ 。在你重新加入前將不會有任何改動。
|
||||
get-preference=你的喜好為 __1__ 。使用 /set-preference 來更改,使用 /save-data 來獲得一份資料副本。
|
||||
@@ -158,37 +158,6 @@ ban-player=封禁用戶
|
||||
afk-time=__1__% 地圖時間\n上次移動: __2__
|
||||
open-map=__1__ __2__\n__3__\n按下打開地圖
|
||||
|
||||
[warp-list]
|
||||
main-caption=傳送陣清單 [img=info]
|
||||
main-tooltip=傳送陣清單
|
||||
sub-tooltip=傳送陣在 __1__ 秒內只能使用一次。 亦需在 __2__ 格之內使用\n__3__\n__4__\n__5__\n__6__\n__7__\n__8__
|
||||
sub-tooltip-current= - __1__ 你目前的傳送陣;
|
||||
sub-tooltip-connected= - __1__ 你可以到本傳送陣;
|
||||
sub-tooltip-different= - __1__ 該傳送陣未連接;
|
||||
sub-tooltip-cooldown= - __1__ 傳送冷卻中;
|
||||
sub-tooltip-not_available= - __1__ 你不在傳送陣範圍;
|
||||
sub-tooltip-bypass= - __1__ 你可以傳送到這裏;
|
||||
too-close=無法新建傳送陣; 太接近 __1__ 了
|
||||
too-close-to-water=無法新建傳送陣; 太接近水源了, 請移動 __1__ 格又或者填平該地方
|
||||
too-close-to-entities=無法新建傳送陣; 太接近其他東西, 請移動 __1__ 格又或者拆除該東西
|
||||
last-edit=最後由 __1__ 在 __2__ 更改\n按這在地圖上看
|
||||
add-tooltip=新建傳送陣
|
||||
confirm-tooltip=儲存更改
|
||||
cancel-tooltip=放棄更改
|
||||
edit-tooltip=現被 __1__ 修改中
|
||||
edit-tooltip-none=現沒有被人修改
|
||||
remove-tooltip=拆除傳送陣
|
||||
timer-tooltip=請等 __1__ 秒才再次傳送
|
||||
timer-tooltip-zero=你要等 __1__ 秒才可以再次傳送
|
||||
goto-tooltip=到 X __1__ Y __2__
|
||||
goto-bypass=到 X __1__ Y __2__, 繞行模式
|
||||
goto-bypass-different-network=到 X __1__ Y __2__, 繞行模式 (不同網絡)
|
||||
goto-same-warp=你已在該傳送陣
|
||||
goto-different-network=該傳送陣未連接, 你可使用電纜連接
|
||||
goto-cooldown=傳送冷卻中, 請等候冷卻
|
||||
goto-disabled=你不在傳送陣, 請移至任意傳送陣
|
||||
goto-edit=修改傳送陣圖案
|
||||
|
||||
[bonus]
|
||||
description=取得 / 設定 Bonus 量。
|
||||
arg-amount=Bonus 數量, 0 來停用。
|
||||
@@ -216,45 +185,6 @@ display-fwrsb-tooltip=勢力機械人儲存
|
||||
display-ffrlm=機械人生命
|
||||
display-ffrlm-tooltip=勢力戰鬥機械人生命時間
|
||||
|
||||
[vlayer]
|
||||
main-tooltip=vlayer
|
||||
description-pbr=使用 Vlayer 為玩家電池充電
|
||||
description-vi=Vlayer 資訊
|
||||
pbr-not-running=vlayer 沒有運行
|
||||
display-item-solar=[img=entity/solar-panel] 太陽能板
|
||||
display-item-accumulator=[img=entity/accumulator] 蓄電池
|
||||
display-current-production=[virtual-signal=signal-P] 現時產能
|
||||
display-sustained-production=[virtual-signal=signal-S] 持續產能
|
||||
display-max-capacity=[virtual-signal=signal-C] 電池容量
|
||||
display-current-capacity=[virtual-signal=signal-E] 電池電量
|
||||
display-remaining-surface-area=[virtual-signal=signal-R] 餘下土地
|
||||
display-item-solar-tooltip=太陽能板數量
|
||||
display-item-accumulator-tooltip=蓄電池數量
|
||||
display-current-production-tooltip=現時電力產能
|
||||
display-sustained-production-tooltip=持續電力產能
|
||||
display-max-capacity-tooltip=電池最大容量
|
||||
display-current-capacity-tooltip=現時電池電量
|
||||
display-remaining-surface-area-tooltip=餘下土地。 放入堆填來增加
|
||||
steel-chest-detect=沒檢測到鋼箱
|
||||
steel-chest-empty=鋼箱不是空的
|
||||
control-refresh=刷新列表
|
||||
control-see=查看介面
|
||||
control-build=建造介面
|
||||
control-remove=拆除介面
|
||||
result-build=建造了
|
||||
result-remove=拆除了
|
||||
result-interface-location=__1__ 介面在 __2__
|
||||
interface-result=__1__ 在 __2__ __3__ __4__ 介面
|
||||
result-unable=無法建造 __1__ 介面,因 __2__
|
||||
result-multiple=檢測到多個鋼箱
|
||||
result-limit=超出最大限制
|
||||
result-space=空間不足
|
||||
control-type-energy=電力
|
||||
control-type-circuit=回路
|
||||
control-type-storage-input=放入箱
|
||||
control-type-storage-output=提取箱
|
||||
power-on-space-research=你要研究 [technology=__1__] 在 __2__ 級 才可使用太空平台 vlayer 功能。
|
||||
|
||||
[toolbar]
|
||||
main-caption=工具箱
|
||||
main-tooltip=工具箱設定\n選上來設定喜好
|
||||
|
||||
@@ -8,17 +8,17 @@ status=伺服器狀態: __1__
|
||||
github=增加功能或回報錯誤: __1__
|
||||
patreon=支持我們: __1__
|
||||
custom-commands=這裹使用了自制指令,如 /tag 和 /me。可使用 /chelp 查看更多。
|
||||
read-readme=確保你已閱讀相關資訊。按左上 i 圖標可再次查看。
|
||||
read-readme=確保你已閱讀相關資訊。按左上 i 圖標可再次查看
|
||||
softmod=這裹用了自設情境,是一種軟裝模組。
|
||||
redmew=
|
||||
redmew=We don't talk about redmew here; they beat us to 1000 members ;-;
|
||||
lhd=列車必須是左則通行。這是本服務器長久以來的規則。
|
||||
|
||||
# data
|
||||
[join-message]
|
||||
description-add=設定 / 取得你的加入信息。
|
||||
description-remove=移除你的加入信息。
|
||||
arg-message=你的加入信息。
|
||||
greet=[color=0,1,0] 歡迎來到 __1__ 的服務器! 若果你喜歡本服務器可加入 DISCORD: __2__ [/color]
|
||||
arg-message=你的加入信息。
|
||||
greet=[color=0,1,0] 歡迎來到 __1__ 的服務器! 若果你喜歡本服務器可加入 Discord: __2__ [/color]
|
||||
message-set=你的加入信息已更新。
|
||||
message-get=你的加入信息為: __1__
|
||||
message-cleared=你的加入信息已清除。
|
||||
@@ -127,7 +127,7 @@ Locale-tooltip=用戶介面語言
|
||||
# expcore
|
||||
[expcore-data]
|
||||
description-preference=允許寫入資料。
|
||||
description-data=把用戶資料寫入電腦
|
||||
description-data=把用戶資料寫入電腦。
|
||||
arg-option="All", "Statistics", "Settings", "Required" 之中
|
||||
set-preference=你的喜好已設定為 __1__ 。在你重新加入前將不會有任何改動。
|
||||
get-preference=你的喜好為 __1__ 。使用 /set-preference 來更改,使用 /save-data 來獲得一份資料副本。
|
||||
@@ -158,37 +158,6 @@ ban-player=封禁用戶
|
||||
afk-time=__1__% 地圖時間\n上次移動: __2__
|
||||
open-map=__1__ __2__\n__3__\n按下打開地圖
|
||||
|
||||
[warp-list]
|
||||
main-caption=傳送陣清單 [img=info]
|
||||
main-tooltip=傳送陣清單
|
||||
sub-tooltip=傳送陣在 __1__ 秒內只能使用一次。 亦需在 __2__ 格之內使用\n__3__\n__4__\n__5__\n__6__\n__7__\n__8__
|
||||
sub-tooltip-current= - __1__ 你目前的傳送陣;
|
||||
sub-tooltip-connected= - __1__ 你可以到本傳送陣;
|
||||
sub-tooltip-different= - __1__ 該傳送陣未連接;
|
||||
sub-tooltip-cooldown= - __1__ 傳送冷卻中;
|
||||
sub-tooltip-not_available= - __1__ 你不在傳送陣範圍;
|
||||
sub-tooltip-bypass= - __1__ 你可以傳送到這裏;
|
||||
too-close=無法新建傳送陣; 太接近 __1__ 了
|
||||
too-close-to-water=無法新建傳送陣; 太接近水源了, 請移動 __1__ 格又或者填平該地方
|
||||
too-close-to-entities=無法新建傳送陣; 太接近其他東西, 請移動 __1__ 格又或者拆除該東西
|
||||
last-edit=最後由 __1__ 在 __2__ 更改\n按這在地圖上看
|
||||
add-tooltip=新建傳送陣
|
||||
confirm-tooltip=儲存更改
|
||||
cancel-tooltip=放棄更改
|
||||
edit-tooltip=現被 __1__ 修改中
|
||||
edit-tooltip-none=現沒有被人修改
|
||||
remove-tooltip=拆除傳送陣
|
||||
timer-tooltip=請等 __1__ 秒才再次傳送
|
||||
timer-tooltip-zero=你要等 __1__ 秒才可以再次傳送
|
||||
goto-tooltip=到 X __1__ Y __2__
|
||||
goto-bypass=到 X __1__ Y __2__, 繞行模式
|
||||
goto-bypass-different-network=到 X __1__ Y __2__, 繞行模式 (不同網絡)
|
||||
goto-same-warp=你已在該傳送陣
|
||||
goto-different-network=該傳送陣未連接, 你可使用電纜連接
|
||||
goto-cooldown=傳送冷卻中, 請等候冷卻
|
||||
goto-disabled=你不在傳送陣, 請移至任意傳送陣
|
||||
goto-edit=修改傳送陣圖案
|
||||
|
||||
[bonus]
|
||||
description=取得 / 設定 Bonus 量。
|
||||
arg-amount=Bonus 數量, 0 來停用。
|
||||
@@ -216,45 +185,6 @@ display-fwrsb-tooltip=勢力機械人儲存
|
||||
display-ffrlm=機械人生命
|
||||
display-ffrlm-tooltip=勢力戰鬥機械人生命時間
|
||||
|
||||
[vlayer]
|
||||
main-tooltip=vlayer
|
||||
description-pbr=使用 Vlayer 為玩家電池充電
|
||||
description-vi=Vlayer 資訊
|
||||
pbr-not-running=vlayer 沒有運行
|
||||
display-item-solar=[img=entity/solar-panel] 太陽能板
|
||||
display-item-accumulator=[img=entity/accumulator] 蓄電池
|
||||
display-current-production=[virtual-signal=signal-P] 現時產能
|
||||
display-sustained-production=[virtual-signal=signal-S] 持續產能
|
||||
display-max-capacity=[virtual-signal=signal-C] 電池容量
|
||||
display-current-capacity=[virtual-signal=signal-E] 電池電量
|
||||
display-remaining-surface-area=[virtual-signal=signal-R] 餘下土地
|
||||
display-item-solar-tooltip=太陽能板數量
|
||||
display-item-accumulator-tooltip=蓄電池數量
|
||||
display-current-production-tooltip=現時電力產能
|
||||
display-sustained-production-tooltip=持續電力產能
|
||||
display-max-capacity-tooltip=電池最大容量
|
||||
display-current-capacity-tooltip=現時電池電量
|
||||
display-remaining-surface-area-tooltip=餘下土地。 放入堆填來增加
|
||||
steel-chest-detect=沒檢測到鋼箱
|
||||
steel-chest-empty=鋼箱不是空的
|
||||
control-refresh=刷新列表
|
||||
control-see=查看介面
|
||||
control-build=建造介面
|
||||
control-remove=拆除介面
|
||||
result-build=建造了
|
||||
result-remove=拆除了
|
||||
result-interface-location=__1__ 介面在 __2__
|
||||
interface-result=__1__ 在 __2__ __3__ __4__ 介面
|
||||
result-unable=無法建造 __1__ 介面,因 __2__
|
||||
result-multiple=檢測到多個鋼箱
|
||||
result-limit=超出最大限制
|
||||
result-space=空間不足
|
||||
control-type-energy=電力
|
||||
control-type-circuit=回路
|
||||
control-type-storage-input=放入箱
|
||||
control-type-storage-output=提取箱
|
||||
power-on-space-research=你要研究 [technology=__1__] 在 __2__ 級 才可使用太空平台 vlayer 功能。
|
||||
|
||||
[toolbar]
|
||||
main-caption=工具箱
|
||||
main-tooltip=工具箱設定\n選上來設定喜好
|
||||
|
||||
@@ -1,807 +0,0 @@
|
||||
--[[-- Control Module - vlayer
|
||||
- Adds a virtual layer to store power to save space.
|
||||
@control vlayer
|
||||
@alias vlayer
|
||||
]]
|
||||
|
||||
local ExpUtil = require("modules/exp_util")
|
||||
local Storage = require("modules/exp_util/storage")
|
||||
local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event
|
||||
local config = require("modules.exp_legacy.config.vlayer") --- @dep config.vlayer
|
||||
|
||||
local mega = 1000000
|
||||
|
||||
local vlayer = {}
|
||||
local vlayer_data = {
|
||||
entity_interfaces = {
|
||||
energy = {}, --- @type LuaEntity[]
|
||||
circuit = {}, --- @type LuaEntity[]
|
||||
storage_input = {}, --- @type LuaEntity[]
|
||||
storage_output = {}, --- @type LuaEntity[]
|
||||
},
|
||||
properties = {
|
||||
total_surface_area = 0,
|
||||
used_surface_area = 0,
|
||||
total_production = 0,
|
||||
production = 0,
|
||||
discharge = 0,
|
||||
capacity = 0,
|
||||
},
|
||||
storage = {
|
||||
items = {}, --- @type table<string, number>
|
||||
power_items = {}, --- @type table<string, { value: number, count: number }>
|
||||
energy = 0,
|
||||
unallocated = {}, --- @type table<string, number>
|
||||
},
|
||||
surface = table.deep_copy(config.surface),
|
||||
}
|
||||
|
||||
Storage.register(vlayer_data, function(tbl)
|
||||
vlayer_data = tbl
|
||||
end)
|
||||
|
||||
for name, properties in pairs(config.allowed_items) do
|
||||
properties.modded = false
|
||||
|
||||
if properties.power then
|
||||
vlayer_data.storage.power_items[name] = {
|
||||
value = assert(properties.fuel_value) * 1000000,
|
||||
count = 0,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- For all modded items, create a config for them
|
||||
for item_name, properties in pairs(config.modded_items) do
|
||||
local base_properties = config.allowed_items[properties.base_game_equivalent]
|
||||
local m = properties.multiplier
|
||||
|
||||
config.allowed_items[item_name] = {
|
||||
starting_value = properties.starting_value or 0,
|
||||
required_area = base_properties.required_area or 0,
|
||||
surface_area = (base_properties.surface_area or 0) * m,
|
||||
production = (base_properties.production or 0) * m,
|
||||
capacity = (base_properties.capacity or 0) * m,
|
||||
modded = true,
|
||||
}
|
||||
end
|
||||
|
||||
--- Get all items in storage, do not modify
|
||||
-- @treturn table a dictionary of all items stored in the vlayer
|
||||
function vlayer.get_items()
|
||||
return vlayer_data.storage.items
|
||||
end
|
||||
|
||||
--- Get all unallocated items in storage
|
||||
-- @treturn table a dictionary of all unallocated items stored in the vlayer
|
||||
function vlayer.get_unallocated_items()
|
||||
return vlayer_data.storage.unallocated
|
||||
end
|
||||
|
||||
--- Get all allocated items in storage
|
||||
-- @treturn table a dictionary of all allocated items stored in the vlayer
|
||||
function vlayer.get_allocated_items()
|
||||
local r = {}
|
||||
|
||||
for k, v in pairs(vlayer_data.storage.items) do
|
||||
r[k] = v
|
||||
|
||||
if vlayer_data.storage.unallocated[k] then
|
||||
r[k] = r[k] - vlayer_data.storage.unallocated[k]
|
||||
end
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
|
||||
--- Get the actual defecit of land
|
||||
local function get_actual_land_defecit()
|
||||
local n = vlayer_data.properties.total_surface_area - vlayer_data.properties.used_surface_area
|
||||
|
||||
for k, v in pairs(vlayer.get_unallocated_items()) do
|
||||
n = n - (config.allowed_items[k].required_area * v)
|
||||
end
|
||||
|
||||
return n
|
||||
end
|
||||
|
||||
--- Get interface counts
|
||||
-- @treturn table a dictionary of the vlayer interface counts
|
||||
function vlayer.get_interface_counts()
|
||||
local interfaces = vlayer_data.entity_interfaces
|
||||
|
||||
return {
|
||||
energy = #interfaces.energy,
|
||||
circuit = #interfaces.circuit,
|
||||
storage_input = #interfaces.storage_input,
|
||||
storage_output = #interfaces.storage_output,
|
||||
}
|
||||
end
|
||||
|
||||
--- Get interfaces
|
||||
-- @treturn table a dictionary of the vlayer interfaces
|
||||
function vlayer.get_interfaces()
|
||||
local interfaces = vlayer_data.entity_interfaces
|
||||
|
||||
return {
|
||||
energy = interfaces.energy,
|
||||
circuit = interfaces.circuit,
|
||||
storage_input = interfaces.storage_input,
|
||||
storage_output = interfaces.storage_output,
|
||||
}
|
||||
end
|
||||
|
||||
--[[
|
||||
25,200 / 420 s
|
||||
昼 210秒 ソーラー効率100%
|
||||
夕方 84秒 1秒ごとにソーラー発電量が約1.2%ずつ下がり、やがて0%になる
|
||||
夜 42秒 ソーラー発電量が0%になる
|
||||
朝方 84秒 1秒ごとにソーラー発電量が約1.2%ずつ上がり、やがて100%になる
|
||||
|
||||
(surface.dawn) 0.75 18,900 Day 12,600 210s
|
||||
0.00 0 Noon
|
||||
(surface.dusk) 0.25 6,300 Sunset 5,040 84s
|
||||
(surface.evening) 0.45 11,340 Night 2,520 42s
|
||||
(surface.morning) 0.55 13,860 Sunrise 5,040 84s
|
||||
]]
|
||||
|
||||
--- Get the power multiplier based on the surface time
|
||||
local function get_production_multiplier()
|
||||
local mul = vlayer_data.surface.solar_power_multiplier
|
||||
local surface = vlayer_data.surface
|
||||
|
||||
if surface.always_day then
|
||||
-- Surface is always day, so full production is used
|
||||
return mul
|
||||
end
|
||||
|
||||
if surface.darkness then
|
||||
-- We are using a real surface, our config does not contain "darkness"
|
||||
local brightness = 1 - surface.darkness
|
||||
|
||||
if brightness >= surface.min_brightness then
|
||||
return mul * (brightness - surface.min_brightness) / (1 - surface.min_brightness)
|
||||
else
|
||||
return 0
|
||||
end
|
||||
end
|
||||
|
||||
-- Caused by using a set config rather than a surface
|
||||
local tick = game.tick % surface.ticks_per_day
|
||||
local daytime = tick / surface.ticks_per_day
|
||||
surface.daytime = daytime
|
||||
|
||||
if daytime <= surface.dusk then -- Noon to Sunset
|
||||
return mul
|
||||
elseif daytime <= surface.evening then -- Sunset to Night
|
||||
return mul * (1 - ((daytime - surface.dusk) / (surface.evening - surface.dusk)))
|
||||
elseif daytime <= surface.morning then -- Night to Sunrise
|
||||
return 0
|
||||
elseif daytime <= surface.dawn then -- Sunrise to Morning
|
||||
return mul * ((surface.daytime - surface.morning) / (surface.dawn - surface.morning))
|
||||
else -- Morning to Noon
|
||||
return mul
|
||||
end
|
||||
end
|
||||
|
||||
--- Get the sustained power multiplier, this needs improving
|
||||
local function get_sustained_multiplier()
|
||||
local mul = vlayer_data.surface.solar_power_multiplier
|
||||
local surface = vlayer_data.surface
|
||||
|
||||
if surface.always_day then
|
||||
-- Surface is always day, so full production is used
|
||||
return mul
|
||||
end
|
||||
|
||||
-- For nauvis vanilla: 210s + (1/2 x (84s + 84s))
|
||||
local day_duration = 1 - surface.dawn + surface.dusk
|
||||
local sunset_duration = surface.evening - surface.dusk
|
||||
local sunrise_duration = surface.dawn - surface.morning
|
||||
|
||||
return mul * (day_duration + (0.5 * (sunset_duration + sunrise_duration)))
|
||||
end
|
||||
|
||||
--- Internal, Allocate items in the vlayer, this will increase the property values of the vlayer such as production and capacity
|
||||
-- Does not increment item storage, so should not be called before insert_item unless during init
|
||||
-- Does not validate area requirements, so checks must be performed before calling this function
|
||||
-- Accepts negative count for deallocating items
|
||||
-- @tparam string item_name The name of the item to allocate
|
||||
-- @tparam number count The count of the item to allocate
|
||||
function vlayer.allocate_item(item_name, count)
|
||||
local item_properties = config.allowed_items[item_name]
|
||||
assert(item_properties, "Item not allowed in vlayer: " .. tostring(item_name))
|
||||
|
||||
if item_properties.production then
|
||||
local nc = item_properties.production * count
|
||||
vlayer_data.properties.production = vlayer_data.properties.production + nc
|
||||
vlayer_data.properties.total_production = vlayer_data.properties.total_production + nc
|
||||
end
|
||||
|
||||
if item_properties.capacity then
|
||||
vlayer_data.properties.capacity = vlayer_data.properties.capacity + item_properties.capacity * count
|
||||
end
|
||||
|
||||
if item_properties.discharge then
|
||||
vlayer_data.properties.discharge = vlayer_data.properties.discharge + item_properties.discharge * count
|
||||
end
|
||||
|
||||
if item_properties.surface_area then
|
||||
vlayer_data.properties.total_surface_area = vlayer_data.properties.total_surface_area + item_properties.surface_area * count
|
||||
end
|
||||
|
||||
if item_properties.required_area and item_properties.required_area > 0 then
|
||||
vlayer_data.properties.used_surface_area = vlayer_data.properties.used_surface_area + item_properties.required_area * count
|
||||
end
|
||||
end
|
||||
|
||||
function vlayer.unable_alloc_item_pwr_calc(item_name, count)
|
||||
local item_properties = config.allowed_items[item_name]
|
||||
|
||||
if item_properties.production then
|
||||
vlayer_data.properties.total_production = vlayer_data.properties.total_production + item_properties.production * count
|
||||
end
|
||||
end
|
||||
|
||||
-- For all allowed items, setup their starting values, default 0
|
||||
for item_name, properties in pairs(config.allowed_items) do
|
||||
vlayer_data.storage.items[item_name] = properties.starting_value or 0
|
||||
|
||||
if properties.required_area and properties.required_area > 0 then
|
||||
vlayer_data.storage.unallocated[item_name] = 0
|
||||
end
|
||||
|
||||
vlayer.allocate_item(item_name, properties.starting_value)
|
||||
end
|
||||
|
||||
--- Insert an item into the vlayer, this will increment its count in storage and allocate it if possible
|
||||
-- @tparam string item_name The name of the item to insert
|
||||
-- @tparam number count The count of the item to insert
|
||||
function vlayer.insert_item(item_name, count)
|
||||
local item_properties = config.allowed_items[item_name]
|
||||
assert(item_properties, "Item not allowed in vlayer: " .. tostring(item_name))
|
||||
vlayer_data.storage.items[item_name] = vlayer_data.storage.items[item_name] + count
|
||||
|
||||
if not config.unlimited_surface_area and item_properties.required_area and item_properties.required_area > 0 then
|
||||
-- Calculate how many can be allocated
|
||||
local surplus_area = vlayer_data.properties.total_surface_area - vlayer_data.properties.used_surface_area
|
||||
local allocate_count = math.min(count, math.floor(surplus_area / item_properties.required_area))
|
||||
|
||||
if allocate_count > 0 then
|
||||
vlayer.allocate_item(item_name, allocate_count)
|
||||
end
|
||||
|
||||
local unallocated = count - allocate_count
|
||||
vlayer_data.storage.unallocated[item_name] = vlayer_data.storage.unallocated[item_name] + unallocated
|
||||
vlayer.unable_alloc_item_pwr_calc(item_name, unallocated)
|
||||
else
|
||||
vlayer.allocate_item(item_name, count)
|
||||
end
|
||||
end
|
||||
|
||||
--- Remove an item from the vlayer, this will decrement its count in storage and prioritise unallocated items over deallocation
|
||||
-- Can not always fulfil the remove request for items which provide surface area, therefore returns the amount actually removed
|
||||
-- @tparam string item_name The name of the item to remove
|
||||
-- @tparam number count The count of the item to remove
|
||||
-- @treturn number The count of the item actually removed
|
||||
function vlayer.remove_item(item_name, count)
|
||||
local item_properties = config.allowed_items[item_name]
|
||||
assert(item_properties, "Item not allowed in vlayer: " .. tostring(item_name))
|
||||
local remove_unallocated = 0
|
||||
|
||||
if not config.unlimited_surface_area and item_properties.required_area and item_properties.required_area > 0 then
|
||||
-- Remove from the unallocated storage first
|
||||
remove_unallocated = math.min(count, vlayer_data.storage.unallocated[item_name])
|
||||
|
||||
if remove_unallocated > 0 then
|
||||
vlayer_data.storage.items[item_name] = vlayer_data.storage.items[item_name] - count
|
||||
vlayer_data.storage.unallocated[item_name] = vlayer_data.storage.unallocated[item_name] - count
|
||||
end
|
||||
|
||||
-- Check if any more items need to be removed
|
||||
count = count - remove_unallocated
|
||||
|
||||
if count == 0 then
|
||||
return remove_unallocated
|
||||
end
|
||||
end
|
||||
|
||||
-- Calculate the amount to remove based on items in storage
|
||||
local remove_count = math.min(count, vlayer_data.storage.items[item_name])
|
||||
|
||||
if item_properties.surface_area and item_properties.surface_area > 0 then
|
||||
-- If the item provides surface area then it has additional limitations
|
||||
local surplus_area = vlayer_data.properties.total_surface_area - vlayer_data.properties.used_surface_area
|
||||
remove_count = math.min(remove_count, math.floor(surplus_area / item_properties.surface_area))
|
||||
|
||||
if remove_count <= 0 then
|
||||
return remove_unallocated
|
||||
end
|
||||
end
|
||||
|
||||
-- Remove the item from allocated storage
|
||||
vlayer_data.storage.items[item_name] = vlayer_data.storage.items[item_name] - remove_count
|
||||
vlayer.allocate_item(item_name, -remove_count)
|
||||
|
||||
return remove_unallocated + remove_count
|
||||
end
|
||||
|
||||
--- Create a new storage input interface
|
||||
-- @tparam LuaSurface surface The surface to place the interface onto
|
||||
-- @tparam MapPosition position The position on the surface to place the interface at
|
||||
-- @tparam[opt] LuaPlayer player The player to show as the last user of the interface
|
||||
-- @treturn LuaEntity The entity that was created for the interface
|
||||
function vlayer.create_input_interface(surface, position, circuit, last_user)
|
||||
local interface = surface.create_entity{ name = "storage-chest", position = position, force = "neutral" }
|
||||
interface.storage_filter = { name = "deconstruction-planner", quality = "normal" }
|
||||
table.insert(vlayer_data.entity_interfaces.storage_input, interface)
|
||||
|
||||
if last_user then
|
||||
interface.last_user = last_user
|
||||
end
|
||||
|
||||
if circuit then
|
||||
for k, _ in pairs(circuit) do
|
||||
for _, v in pairs(circuit[k]) do
|
||||
interface.connect_neighbour{ wire = defines.wire_type[k], target_entity = v }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
interface.destructible = false
|
||||
interface.minable_flag = false
|
||||
interface.operable = true
|
||||
|
||||
return interface
|
||||
end
|
||||
|
||||
--- Handle all input interfaces, will take their contents and insert it into the vlayer storage
|
||||
local function handle_input_interfaces()
|
||||
for index, interface in pairs(vlayer_data.entity_interfaces.storage_input) do
|
||||
if not interface.valid then
|
||||
vlayer_data.entity_interfaces.storage_input[index] = nil
|
||||
else
|
||||
local inventory = assert(interface.get_inventory(defines.inventory.chest))
|
||||
|
||||
for _, v in pairs(inventory.get_contents()) do
|
||||
if config.allowed_items[v.name] then
|
||||
--[[
|
||||
there are no quality support currently.
|
||||
so instead, using the stats projection value for higher quality.
|
||||
]]
|
||||
local count_deduct
|
||||
local count_add
|
||||
|
||||
if prototypes.quality[v.quality].level == 0 then
|
||||
count_deduct = v.count
|
||||
count_add = v.count
|
||||
|
||||
elseif prototypes.quality[v.quality].level > 0 and v.count >= 10 then
|
||||
local batch = math.floor(v.count / 10)
|
||||
count_deduct = batch * 10
|
||||
count_add = batch * (10 + (prototypes.quality[v.quality].level * 3))
|
||||
end
|
||||
|
||||
if count_deduct and count_add then
|
||||
if config.allowed_items[v.name].modded then
|
||||
local modded_item = assert(config.modded_items[v.name])
|
||||
if config.modded_auto_downgrade then
|
||||
vlayer.insert_item(modded_item.base_game_equivalent, count_add * modded_item.multiplier)
|
||||
else
|
||||
vlayer.insert_item(v.name, count_add)
|
||||
end
|
||||
else
|
||||
if vlayer_data.storage.power_items[v.name] then
|
||||
vlayer_data.storage.power_items[v.name].count = vlayer_data.storage.power_items[v.name].count + count_add
|
||||
else
|
||||
vlayer.insert_item(v.name, count_add)
|
||||
end
|
||||
end
|
||||
|
||||
inventory.remove{ name = v.name, count = count_deduct, quality = v.quality }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Create a new storage output interface
|
||||
-- @tparam LuaSurface surface The surface to place the interface onto
|
||||
-- @tparam MapPosition position The position on the surface to place the interface at
|
||||
-- @tparam[opt] LuaPlayer player The player to show as the last user of the interface
|
||||
-- @treturn LuaEntity The entity that was created for the interface
|
||||
function vlayer.create_output_interface(surface, position, circuit, last_user)
|
||||
local interface = surface.create_entity{ name = "requester-chest", position = position, force = "neutral" }
|
||||
table.insert(vlayer_data.entity_interfaces.storage_output, interface)
|
||||
|
||||
if last_user then
|
||||
interface.last_user = last_user
|
||||
end
|
||||
|
||||
if circuit then
|
||||
for k, _ in pairs(circuit) do
|
||||
for _, v in pairs(circuit[k]) do
|
||||
interface.connect_neighbour{ wire = defines.wire_type[k], target_entity = v }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
interface.destructible = false
|
||||
interface.minable_flag = false
|
||||
interface.operable = true
|
||||
|
||||
return interface
|
||||
end
|
||||
|
||||
--- Handle all output interfaces, will take their requests and remove it from the vlayer storage
|
||||
local function handle_output_interfaces()
|
||||
for index, interface in pairs(vlayer_data.entity_interfaces.storage_output) do
|
||||
if not interface.valid then
|
||||
vlayer_data.entity_interfaces.storage_output[index] = nil
|
||||
else
|
||||
local inventory = assert(interface.get_inventory(defines.inventory.chest))
|
||||
local inventory_request_sections = assert(interface.get_logistic_sections()).sections
|
||||
|
||||
for i = 1, #inventory_request_sections do
|
||||
for _, v in pairs(inventory_request_sections[i].filters) do
|
||||
if v.value and config.allowed_items[v.value.name] then
|
||||
local current_amount = inventory.get_item_count(v.value.name)
|
||||
local request_amount = math.min(assert(v.min) - current_amount, vlayer_data.storage.items[v.value.name])
|
||||
|
||||
if request_amount > 0 and inventory.can_insert{ name = v.value.name, count = request_amount } then
|
||||
local removed_item_count = vlayer.remove_item(v.value.name, request_amount)
|
||||
|
||||
if removed_item_count > 0 then
|
||||
inventory.insert{ name = v.value.name, count = removed_item_count, quality = "normal" }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Handle the unallocated items because more surface area may have been added
|
||||
local function handle_unallocated()
|
||||
-- unallocated cant happen when its unlimited
|
||||
if config.unlimited_surface_area then
|
||||
return
|
||||
end
|
||||
|
||||
-- Get the total unallocated area so items can be allocated in equal amounts
|
||||
local unallocated_area = 0
|
||||
|
||||
for item_name, count in pairs(vlayer_data.storage.unallocated) do
|
||||
local item_properties = config.allowed_items[item_name]
|
||||
unallocated_area = unallocated_area + item_properties.required_area * count
|
||||
end
|
||||
|
||||
if unallocated_area == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
-- Allocate items in an equal distribution
|
||||
local surplus_area = vlayer_data.properties.total_surface_area - vlayer_data.properties.used_surface_area
|
||||
|
||||
for item_name, count in pairs(vlayer_data.storage.unallocated) do
|
||||
local allocation_count = math.min(count, math.floor(count * surplus_area / unallocated_area))
|
||||
|
||||
if allocation_count > 0 then
|
||||
vlayer_data.storage.unallocated[item_name] = vlayer_data.storage.unallocated[item_name] - allocation_count
|
||||
vlayer.allocate_item(item_name, allocation_count)
|
||||
vlayer.unable_alloc_item_pwr_calc(item_name, -allocation_count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Get the statistics for the vlayer
|
||||
function vlayer.get_statistics()
|
||||
local vdp = vlayer_data.properties.production * mega
|
||||
local gdm = get_production_multiplier()
|
||||
local gsm = get_sustained_multiplier()
|
||||
local gald = get_actual_land_defecit()
|
||||
|
||||
return {
|
||||
total_surface_area = vlayer_data.properties.total_surface_area,
|
||||
used_surface_area = vlayer_data.properties.used_surface_area,
|
||||
remaining_surface_area = gald,
|
||||
surface_area = vlayer_data.properties.total_surface_area - gald,
|
||||
production_multiplier = gdm,
|
||||
energy_max = vdp,
|
||||
energy_production = vdp * gdm,
|
||||
energy_total_production = vlayer_data.properties.total_production * gsm * mega,
|
||||
energy_sustained = vdp * gsm,
|
||||
energy_capacity = vlayer_data.properties.capacity * mega,
|
||||
energy_storage = vlayer_data.storage.energy,
|
||||
}
|
||||
end
|
||||
|
||||
--- add or reduce vlayer power
|
||||
--- @param power number
|
||||
--- @return number
|
||||
function vlayer.energy_changed(power)
|
||||
local new_value = vlayer_data.storage.energy + power
|
||||
vlayer_data.storage.energy = new_value
|
||||
return new_value
|
||||
end
|
||||
|
||||
--- Circuit signals used for the statistics
|
||||
function vlayer.get_circuits()
|
||||
return {
|
||||
total_surface_area = "signal-A",
|
||||
used_surface_area = "signal-U",
|
||||
remaining_surface_area = "signal-R",
|
||||
production_multiplier = "signal-M",
|
||||
energy_production = "signal-P",
|
||||
energy_sustained = "signal-S",
|
||||
energy_capacity = "signal-C",
|
||||
energy_storage = "signal-E",
|
||||
}
|
||||
end
|
||||
|
||||
local vlayer_circuits_string = ""
|
||||
|
||||
for key, value in pairs(vlayer.get_circuits()) do
|
||||
vlayer_circuits_string = vlayer_circuits_string .. string.format("[virtual-signal=%s] = %s\n", value, key:gsub("_", " "))
|
||||
end
|
||||
|
||||
--- Create a new circuit interface
|
||||
-- @tparam LuaSurface surface The surface to place the interface onto
|
||||
-- @tparam MapPosition position The position on the surface to place the interface at
|
||||
-- @tparam[opt] LuaPlayer player The player to show as the last user of the interface
|
||||
-- @treturn LuaEntity The entity that was created for the interface
|
||||
function vlayer.create_circuit_interface(surface, position, circuit, last_user)
|
||||
local interface = surface.create_entity{ name = "constant-combinator", position = position, force = "neutral" }
|
||||
table.insert(vlayer_data.entity_interfaces.circuit, interface)
|
||||
|
||||
if last_user then
|
||||
interface.last_user = last_user
|
||||
end
|
||||
|
||||
if circuit then
|
||||
for k, _ in pairs(circuit) do
|
||||
for _, v in pairs(circuit[k]) do
|
||||
interface.connect_neighbour{ wire = defines.wire_type[k], target_entity = v }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
interface.destructible = false
|
||||
interface.minable_flag = false
|
||||
interface.operable = true
|
||||
|
||||
return interface
|
||||
end
|
||||
|
||||
--- Handle all circuit interfaces, updating their signals to match the vlayer statistics
|
||||
local function handle_circuit_interfaces()
|
||||
local stats = vlayer.get_statistics()
|
||||
|
||||
for index, interface in pairs(vlayer_data.entity_interfaces.circuit) do
|
||||
if not interface.valid then
|
||||
vlayer_data.entity_interfaces.circuit[index] = nil
|
||||
else
|
||||
local control = interface.get_or_create_control_behavior() --[[@as LuaConstantCombinatorControlBehavior]]
|
||||
if control.sections_count == 0 then
|
||||
control.add_section()
|
||||
end
|
||||
local circuit_oc = assert(control.sections[1])
|
||||
local signal_index = 1
|
||||
local circuit = vlayer.get_circuits()
|
||||
|
||||
-- Set the virtual signals based on the vlayer stats
|
||||
for stat_name, signal_name in pairs(circuit) do
|
||||
if stat_name:find("energy") then
|
||||
circuit_oc.set_slot(signal_index, { value = { type = "virtual", name = signal_name, quality = "normal" }, min = math.floor(stats[stat_name] / mega) })
|
||||
elseif stat_name == "production_multiplier" then
|
||||
circuit_oc.set_slot(signal_index, { value = { type = "virtual", name = signal_name, quality = "normal" }, min = math.floor(stats[stat_name] * 10000) })
|
||||
else
|
||||
circuit_oc.set_slot(signal_index, { value = { type = "virtual", name = signal_name, quality = "normal" }, min = math.floor(stats[stat_name]) })
|
||||
end
|
||||
|
||||
signal_index = signal_index + 1
|
||||
end
|
||||
|
||||
-- Set the item signals based on stored items
|
||||
for item_name, count in pairs(vlayer_data.storage.items) do
|
||||
if prototypes.item[item_name] and count > 0 then
|
||||
local existing_index = circuit_oc.set_slot(signal_index, { value = { type = "item", name = item_name, quality = "normal" }, min = count })
|
||||
if existing_index then
|
||||
circuit_oc.clear_slot(existing_index)
|
||||
circuit_oc.set_slot(signal_index, { value = { type = "item", name = item_name, quality = "normal" }, min = count })
|
||||
end
|
||||
signal_index = signal_index + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Clear remaining signals to prevent outdated values being present (caused by count > 0 check)
|
||||
for clear_index = signal_index, #circuit do
|
||||
if not circuit_oc.get_slot(clear_index).value then
|
||||
break -- There are no more signals to clear
|
||||
end
|
||||
|
||||
circuit_oc.clear_slot(clear_index)
|
||||
end
|
||||
|
||||
interface.combinator_description = vlayer_circuits_string
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Create a new energy interface
|
||||
-- @tparam LuaSurface surface The surface to place the interface onto
|
||||
-- @tparam MapPosition position The position on the surface to place the interface at
|
||||
-- @tparam[opt] LuaPlayer player The player to show as the last user of the interface
|
||||
-- @treturn LuaEntity The entity that was created for the interface, or nil if it could not be created
|
||||
function vlayer.create_energy_interface(surface, position, last_user)
|
||||
if not surface.can_place_entity{ name = "electric-energy-interface", position = position } then
|
||||
return nil
|
||||
end
|
||||
|
||||
local interface = surface.create_entity{ name = "electric-energy-interface", position = position, force = "neutral" }
|
||||
table.insert(vlayer_data.entity_interfaces.energy, interface)
|
||||
|
||||
if last_user then
|
||||
interface.last_user = last_user
|
||||
end
|
||||
|
||||
interface.destructible = false
|
||||
interface.minable_flag = false
|
||||
interface.operable = false
|
||||
interface.electric_buffer_size = 0
|
||||
interface.power_production = 0
|
||||
interface.power_usage = 0
|
||||
interface.energy = 0
|
||||
|
||||
return interface
|
||||
end
|
||||
|
||||
--- Handle all energy interfaces as well as the energy storage
|
||||
local function handle_energy_interfaces()
|
||||
-- Add the newly produced power
|
||||
local production = vlayer_data.properties.production * mega * (config.update_tick_energy / 60)
|
||||
vlayer_data.storage.energy = vlayer_data.storage.energy + math.floor(production * get_production_multiplier())
|
||||
|
||||
-- Calculate how much power is present in the network, that is storage + all interfaces
|
||||
if #vlayer_data.entity_interfaces.energy > 0 then
|
||||
local available_energy = vlayer_data.storage.energy
|
||||
|
||||
for index, interface in pairs(vlayer_data.entity_interfaces.energy) do
|
||||
if not interface.valid then
|
||||
vlayer_data.entity_interfaces.energy[index] = nil
|
||||
else
|
||||
available_energy = available_energy + interface.energy
|
||||
end
|
||||
end
|
||||
|
||||
-- Distribute the energy between all interfaces
|
||||
local discharge_rate = 2 * (production + vlayer_data.properties.discharge * mega) / #vlayer_data.entity_interfaces.energy
|
||||
local fill_to = math.min(discharge_rate, math.floor(available_energy / #vlayer_data.entity_interfaces.energy))
|
||||
|
||||
for _, interface in pairs(vlayer_data.entity_interfaces.energy) do
|
||||
interface.electric_buffer_size = math.max(discharge_rate, interface.energy) -- prevent energy loss
|
||||
local delta = fill_to - interface.energy -- positive means storage to interface
|
||||
vlayer_data.storage.energy = vlayer_data.storage.energy - delta
|
||||
interface.energy = interface.energy + delta
|
||||
end
|
||||
end
|
||||
|
||||
-- Cap the stored energy to the allowed capacity
|
||||
if not config.unlimited_capacity and vlayer_data.storage.energy > vlayer_data.properties.capacity * mega then
|
||||
vlayer_data.storage.energy = vlayer_data.properties.capacity * mega
|
||||
|
||||
-- burn the trash to produce power
|
||||
elseif vlayer_data.storage.power_items then
|
||||
for k, v in pairs(vlayer_data.storage.power_items) do
|
||||
local max_burning = (vlayer_data.properties.capacity * mega / 2) - vlayer_data.storage.energy
|
||||
|
||||
if v.count > 0 and max_burning > 0 then
|
||||
local to_burn = math.min(v.count, math.floor(max_burning / v.value))
|
||||
|
||||
vlayer_data.storage.energy = vlayer_data.storage.energy + (to_burn * v.value)
|
||||
vlayer_data.storage.power_items[k].count = vlayer_data.storage.power_items[k].count - to_burn
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Remove the entity interface using the given position
|
||||
-- @tparam LuaSurface surface The surface to search for an interface on
|
||||
-- @tparam MapPosition position The position of the item
|
||||
-- @treturn string The type of interface that was removed, or nil if no interface was found
|
||||
-- @treturn MapPosition The position the interface was at, or nil if no interface was found
|
||||
function vlayer.remove_interface(surface, position)
|
||||
local entities = surface.find_entities_filtered{
|
||||
name = { "storage-chest", "requester-chest", "constant-combinator", "electric-energy-interface" },
|
||||
force = "neutral",
|
||||
position = position,
|
||||
radius = 2,
|
||||
limit = 1,
|
||||
}
|
||||
|
||||
-- Get the details which will be returned
|
||||
if #entities == 0 then
|
||||
return nil, nil, nil
|
||||
end
|
||||
|
||||
local interface = entities[1]
|
||||
local name = interface.name
|
||||
local sur = interface.surface
|
||||
local pos = interface.position
|
||||
|
||||
-- Return the type of interface removed and do some clean up
|
||||
if name == "storage-chest" then
|
||||
local inventory = assert(interface.get_inventory(defines.inventory.chest))
|
||||
ExpUtil.transfer_inventory_to_surface{
|
||||
inventory = inventory,
|
||||
surface = interface.surface,
|
||||
name = "iron-chest",
|
||||
allow_creation = true,
|
||||
}
|
||||
table.remove_element(vlayer_data.entity_interfaces.storage_input, interface)
|
||||
interface.destroy()
|
||||
|
||||
return "storage-input", sur, pos
|
||||
elseif name == "requester-chest" then
|
||||
local inventory = assert(interface.get_inventory(defines.inventory.chest))
|
||||
ExpUtil.transfer_inventory_to_surface{
|
||||
inventory = inventory,
|
||||
surface = interface.surface,
|
||||
name = "iron-chest",
|
||||
allow_creation = true,
|
||||
}
|
||||
table.remove_element(vlayer_data.entity_interfaces.storage_output, interface)
|
||||
interface.destroy()
|
||||
|
||||
return "storage-output", sur, pos
|
||||
elseif name == "constant-combinator" then
|
||||
table.remove_element(vlayer_data.entity_interfaces.circuit, interface)
|
||||
interface.destroy()
|
||||
|
||||
return "circuit", sur, pos
|
||||
elseif name == "electric-energy-interface" then
|
||||
vlayer_data.storage.energy = vlayer_data.storage.energy + interface.energy
|
||||
table.remove_element(vlayer_data.entity_interfaces.energy, interface)
|
||||
interface.destroy()
|
||||
|
||||
return "energy", sur, pos
|
||||
end
|
||||
end
|
||||
|
||||
local function on_surface_event()
|
||||
if config.mimic_surface then
|
||||
local surface = game.get_surface(config.mimic_surface)
|
||||
|
||||
if surface then
|
||||
vlayer_data.surface = surface
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if not vlayer_data.surface.index then
|
||||
-- Our fake surface data never has an index, we test for this to avoid unneeded copies from the config
|
||||
vlayer_data.surface = table.deep_copy(config.surface)
|
||||
end
|
||||
end
|
||||
|
||||
--- Handle all storage IO and attempt allocation of unallocated items
|
||||
Event.on_nth_tick(config.update_tick_storage, function(_)
|
||||
handle_input_interfaces()
|
||||
handle_output_interfaces()
|
||||
handle_unallocated()
|
||||
end)
|
||||
|
||||
--- Handle all energy and circuit updates
|
||||
Event.on_nth_tick(config.update_tick_energy, function(_)
|
||||
handle_circuit_interfaces()
|
||||
handle_energy_interfaces()
|
||||
end)
|
||||
|
||||
Event.add(defines.events.on_surface_created, on_surface_event)
|
||||
Event.add(defines.events.on_surface_renamed, on_surface_event)
|
||||
Event.add(defines.events.on_surface_imported, on_surface_event)
|
||||
Event.on_init(on_surface_event) -- Default surface always exists, does not trigger on_surface_created
|
||||
|
||||
return vlayer
|
||||
@@ -1,488 +0,0 @@
|
||||
--[[-- Control Module - Warps
|
||||
- Stores warps for each force.
|
||||
@control Warps
|
||||
@alias Warps
|
||||
|
||||
@usage-- Making a new spawn warp
|
||||
local player = game.player
|
||||
local force = player.force
|
||||
local spawn_id = Warps.add_warp(force.name, player.physical_surface, player.physical_position, player.name, 'Spawn')
|
||||
|
||||
Warps.set_spawn_warp(spawn_id, force)
|
||||
Warps.make_warp_tag(spawn_id)
|
||||
|
||||
@usage-- Making a new warp with a warp area
|
||||
local player = game.player
|
||||
local force = player.force
|
||||
local warp_id = Warps.add_warp(force.name, player.physical_surface, player.physical_position, player.name)
|
||||
|
||||
Warps.make_warp_area(warp_id)
|
||||
Warps.make_warp_tag(warp_id)
|
||||
|
||||
]]
|
||||
|
||||
local Datastore = require("modules.exp_legacy.expcore.datastore") --- @dep expcore.datastore
|
||||
local Storage = require("modules/exp_util/storage")
|
||||
local config = require("modules.exp_legacy.config.gui.warps") --- @dep config.warps
|
||||
|
||||
--- Stores all data for the warp system
|
||||
local WrapData = Datastore.connect("WrapData")
|
||||
WrapData:set_serializer(function(raw_key) return raw_key.warp_id end)
|
||||
|
||||
local Warps = {}
|
||||
|
||||
--- @class ExpScenario_Warps.force_warps: { spawn: string, [number]: string }
|
||||
|
||||
-- Storage lookup table for force name to task ids
|
||||
local force_warps = { _uid = 1 }
|
||||
--- @cast force_warps table<string, ExpScenario_Warps.force_warps>
|
||||
Storage.register(force_warps, function(tbl)
|
||||
force_warps = tbl
|
||||
end)
|
||||
|
||||
-- Create an array of entity names that will be added to the remove filter
|
||||
local remove_warp_area_entity_names = {}
|
||||
for _, entity in pairs(config.entities) do
|
||||
table.insert(remove_warp_area_entity_names, entity[1])
|
||||
end
|
||||
|
||||
-- When a warp is updated change its chat tag and restore the warp order
|
||||
WrapData:on_update(function(warp_id, warp, old_warp)
|
||||
if warp then
|
||||
warp.updates = warp.updates + 1
|
||||
-- Update the map chart tag if there is one
|
||||
if warp.tag then
|
||||
Warps.make_warp_tag(warp_id)
|
||||
end
|
||||
|
||||
-- Check that the name of the warp has been changed
|
||||
if not old_warp or warp.name == old_warp.name then return end
|
||||
|
||||
-- Get the names of all the warp points for this force
|
||||
local force_name = warp.force_name
|
||||
local warp_ids = force_warps[force_name]
|
||||
local spawn_id = warp_ids.spawn
|
||||
|
||||
local warp_names = {} --- @type table<string, string>
|
||||
for _, next_warp_id in pairs(warp_ids) do
|
||||
local next_warp = WrapData:get(next_warp_id)
|
||||
if next_warp_id ~= spawn_id then
|
||||
warp_names[next_warp.name .. next_warp_id] = next_warp_id
|
||||
end
|
||||
end
|
||||
|
||||
-- Sort the warp names in alphabetical order
|
||||
local new_warp_ids = table.get_values(table.key_sort(warp_names))
|
||||
--- @cast new_warp_ids ExpScenario_Warps.force_warps
|
||||
table.insert(new_warp_ids, 1, spawn_id)
|
||||
new_warp_ids.spawn = spawn_id
|
||||
force_warps[force_name] = new_warp_ids
|
||||
end
|
||||
end)
|
||||
|
||||
--- Map Integration.
|
||||
-- functions used to create and alter warps with in the map
|
||||
-- @section mapIntegration
|
||||
|
||||
--[[-- Add or update the chat tag for this warp
|
||||
@tparam string warp_id the uid of the warp you want the chart tag for
|
||||
@treturn boolean true if a new tag was made, false if it was updated
|
||||
|
||||
@usage-- Adding a chart tag for a new warp
|
||||
local tag_added = Warps.make_warp_tag(warp_id)
|
||||
|
||||
]]
|
||||
function Warps.make_warp_tag(warp_id)
|
||||
local warp = WrapData:get(warp_id)
|
||||
local name = warp.name
|
||||
local icon = warp.icon
|
||||
|
||||
-- Edit the existing tag if it is present
|
||||
local tag = warp.tag
|
||||
|
||||
if tag and tag.valid then
|
||||
tag.text = "Warp: " .. name
|
||||
tag.icon = icon
|
||||
return false
|
||||
end
|
||||
|
||||
-- Make a new tag if one did not exist
|
||||
local force = game.forces[warp.force_name]
|
||||
local surface = warp.surface
|
||||
local position = warp.position
|
||||
|
||||
tag = force.add_chart_tag(surface, {
|
||||
position = { position.x + 0.5, position.y + 0.5 },
|
||||
text = "Warp: " .. name,
|
||||
icon = icon,
|
||||
})
|
||||
|
||||
-- Add the tag to this warp, store.update not needed as we dont want it to trigger
|
||||
warp.tag = tag
|
||||
return true
|
||||
end
|
||||
|
||||
--[[-- Remove the chart tag for this warp
|
||||
@tparam string warp_id the uid for the warp that you want to remove the chart tag from
|
||||
@treturn boolean true if the tag was valid and was removed, false if the tag was invalid
|
||||
|
||||
@usage-- Removing the chart tag from a warp
|
||||
local removed = Warps.remove_warp_tag(warp_id)
|
||||
|
||||
]]
|
||||
function Warps.remove_warp_tag(warp_id)
|
||||
local warp = WrapData:get(warp_id)
|
||||
|
||||
-- Check there is a tag to remove
|
||||
local tag = warp.tag
|
||||
if not tag or not tag.valid then
|
||||
warp.tag = nil
|
||||
return false
|
||||
end
|
||||
|
||||
-- Remove the warp chart tag if it is valid
|
||||
tag.destroy()
|
||||
warp.tag = nil
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--[[-- Add a warp area for the warp, purely cosmetic
|
||||
@tparam string warp_id the uid of the warp you want the area for
|
||||
|
||||
@usage-- Adding a warp area for a warp
|
||||
Warps.make_warp_area(warp_id)
|
||||
|
||||
]]
|
||||
function Warps.make_warp_area(warp_id)
|
||||
local warp = WrapData:get(warp_id)
|
||||
local surface = warp.surface
|
||||
local position = warp.position
|
||||
local posx = position.x
|
||||
local posy = position.y
|
||||
|
||||
-- Get the tile that is being replaced, store.update not needed as we don't want it to trigger
|
||||
local old_tile = surface.get_tile(position).name
|
||||
warp.old_tile = old_tile
|
||||
|
||||
-- Add a tile pattern on top of the base
|
||||
local tiles = {}
|
||||
for _, tile in pairs(config.tiles) do
|
||||
table.insert(tiles, { name = tile[1], position = { tile[2] + posx, tile[3] + posy } })
|
||||
end
|
||||
|
||||
surface.set_tiles(tiles)
|
||||
|
||||
-- Add entities to the warp structure
|
||||
for _, entity in pairs(config.entities) do
|
||||
entity = surface.create_entity{
|
||||
name = entity[1],
|
||||
position = { entity[2] + posx, entity[3] + posy },
|
||||
force = "neutral",
|
||||
}
|
||||
entity.destructible = false
|
||||
entity.health = 0
|
||||
entity.minable_flag = false
|
||||
entity.rotatable = false
|
||||
|
||||
-- Save reference of the last power pole
|
||||
if entity.type == "electric-pole" then
|
||||
warp.electric_pole = entity
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[-- Remove the warp area for a warp
|
||||
@tparam string warp_id the uid of the warp that you want to remove the area for
|
||||
|
||||
@usage-- Remove the warp area for a warp
|
||||
Warps.remove_warp_area(warp_id)
|
||||
|
||||
]]
|
||||
function Warps.remove_warp_area(warp_id)
|
||||
local warp = WrapData:get(warp_id)
|
||||
local position = warp.position
|
||||
local surface = warp.surface
|
||||
local radius = config.standard_proximity_radius
|
||||
|
||||
-- Check that a warp area was created previously
|
||||
local old_tile = warp.old_tile
|
||||
if not old_tile then return end
|
||||
|
||||
-- Restore the original tiles before the creation of the warp
|
||||
local tiles = {}
|
||||
|
||||
for _, tile in pairs(config.tiles) do
|
||||
table.insert(tiles, { name = old_tile, position = { tile[2] + position.x, tile[3] + position.y } })
|
||||
end
|
||||
|
||||
surface.set_tiles(tiles)
|
||||
|
||||
local area = {
|
||||
{ position.x - radius, position.y - radius },
|
||||
{ position.x + radius, position.y + radius },
|
||||
}
|
||||
|
||||
-- Remove warp structure entities
|
||||
local entities = surface.find_entities_filtered{ force = "neutral", area = area, name = remove_warp_area_entity_names }
|
||||
for _, entity in pairs(entities) do
|
||||
-- Destroy them, this will leave corpses of the entities that it destroyed.
|
||||
if entity and entity.valid and entity.destructible == false then
|
||||
entity.destructible = true
|
||||
entity.die(entity.force)
|
||||
end
|
||||
end
|
||||
|
||||
-- Rechart map area, useful if warp is not covered by a radar
|
||||
game.forces[warp.force_name].chart(surface, area)
|
||||
end
|
||||
|
||||
--[[-- Set a warp to be the spawn point for a force, force must own this warp
|
||||
@tparam string warp_id the uid of the warp that you want to be the spawn for the force
|
||||
@tparam LuaForce force the force that you want to set the spawn for
|
||||
|
||||
@usage-- Set your forces spawn to a warp
|
||||
Warps.set_spawn_warp(warp_id, game.player.force)
|
||||
|
||||
]]
|
||||
function Warps.set_spawn_warp(warp_id, force)
|
||||
-- Check the force owns this warp
|
||||
local warp = WrapData:get(warp_id)
|
||||
if warp.force_name ~= force.name then
|
||||
return
|
||||
end
|
||||
-- Set this warp as the spawn
|
||||
local warp_ids = force_warps[warp.force_name]
|
||||
if not warp_ids then
|
||||
warp_ids = {}
|
||||
force_warps[warp.force_name] = warp_ids
|
||||
end
|
||||
warp_ids.spawn = warp_id
|
||||
-- Set the forces spawn to this warp
|
||||
force.set_spawn_position(warp.position, warp.surface)
|
||||
end
|
||||
|
||||
--[[-- Teleport a player to a warp point
|
||||
@tparam string warp_id the uid of the warp to send the player to
|
||||
@tparam LuaPlayer player the player to teleport to the warp
|
||||
|
||||
@usage-- Teleport yourself to a warp point
|
||||
Warps.teleport_player(warp_id, game.player)
|
||||
|
||||
]]
|
||||
function Warps.teleport_player(warp_id, player)
|
||||
local warp = WrapData:get(warp_id)
|
||||
local surface = warp.surface
|
||||
local position = {
|
||||
x = warp.position.x + 0.5,
|
||||
y = warp.position.y + 0.5,
|
||||
}
|
||||
|
||||
if player.vehicle then
|
||||
-- Teleport the entity
|
||||
local entity = player.vehicle
|
||||
local goto_position = surface.find_non_colliding_position(entity.name, position, 32, 1)
|
||||
-- Surface teleport can only be done for players and cars at the moment. (with surface as an peramitor it gives this error)
|
||||
if entity.type == "car" then
|
||||
entity.teleport(goto_position, surface)
|
||||
elseif surface.index == entity.surface.index then
|
||||
-- Try teleport the entity
|
||||
if not entity.teleport(goto_position) then
|
||||
player.driving = false
|
||||
-- Need to calculate new goto_position because entities have different collision boxes
|
||||
goto_position = surface.find_non_colliding_position("character", position, 32, 1)
|
||||
player.teleport(goto_position, surface)
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Teleport the player
|
||||
local goto_position = surface.find_non_colliding_position("character", position, 32, 1)
|
||||
if player.driving then player.driving = false end
|
||||
player.teleport(goto_position, surface)
|
||||
end
|
||||
end
|
||||
|
||||
--- Setters.
|
||||
-- functions used to created and alter warps
|
||||
-- @section setters
|
||||
|
||||
--[[-- Add a new warp for a force, the warp must have a surface and a position
|
||||
@tparam string force_name the name of the force to add the warp for
|
||||
@tparam LuaSurface surface the surface that the warp will be on
|
||||
@tparam Concepts.Position position the position that the warp will be on
|
||||
@tparam[opt] string player_name the name of the player that is adding the warp
|
||||
@tparam[opt] string warp_name the name of the warp that is being added, if omited default is used
|
||||
@treturn string the uid of the warp which was created
|
||||
|
||||
@usage-- Adding a new warp for your force at your position
|
||||
local player = game.player
|
||||
local warp_id = Warps.add_warp(player.force.name, player.physical_surface, player.physical_position, player.name)
|
||||
|
||||
]]
|
||||
function Warps.add_warp(force_name, surface, position, player_name, warp_name)
|
||||
-- Get new warp id
|
||||
local warp_id = tostring(force_warps._uid)
|
||||
force_warps._uid = force_warps._uid + 1
|
||||
warp_name = warp_name or "New warp"
|
||||
|
||||
-- Get the existing warps for this force
|
||||
local warp_ids = force_warps[force_name]
|
||||
if not warp_ids then
|
||||
warp_ids = {}
|
||||
force_warps[force_name] = warp_ids
|
||||
end
|
||||
|
||||
-- Insert the warp id into the force warps
|
||||
table.insert(warp_ids, warp_id)
|
||||
|
||||
-- Create the editing table
|
||||
local editing = {}
|
||||
if player_name then
|
||||
editing[player_name] = true
|
||||
end
|
||||
|
||||
-- Add the new warp to the store
|
||||
WrapData:set(warp_id, {
|
||||
warp_id = warp_id,
|
||||
force_name = force_name,
|
||||
name = warp_name,
|
||||
icon = { type = config.default_icon.type, name = config.default_icon.name },
|
||||
surface = surface,
|
||||
position = {
|
||||
x = math.floor(position.x),
|
||||
y = math.floor(position.y),
|
||||
},
|
||||
last_edit_name = player_name or "<server>",
|
||||
last_edit_time = game.tick,
|
||||
currently_editing = editing,
|
||||
updates = 0,
|
||||
})
|
||||
|
||||
return warp_id
|
||||
end
|
||||
|
||||
--[[-- Removes a warp and any data linked to it
|
||||
@tparam string warp_id the uid of the warp that you want to remove
|
||||
|
||||
@usage-- Removing a warp
|
||||
Warps.remove_warp(warp_id)
|
||||
|
||||
]]
|
||||
function Warps.remove_warp(warp_id)
|
||||
local warp = WrapData:get(warp_id)
|
||||
local force_name = warp.force_name
|
||||
Warps.remove_warp_tag(warp_id)
|
||||
Warps.remove_warp_area(warp_id)
|
||||
WrapData:remove(warp_id)
|
||||
table.remove_element(force_warps[force_name], warp_id)
|
||||
end
|
||||
|
||||
--[[-- Update the name and icon for a warp
|
||||
@tparam string warp_id the uid of the warp that you want to update
|
||||
@tparam[opt] string new_name the new name that you want the warp to have
|
||||
@tparam[opt] string new_icon the new icon that you want the warp to have
|
||||
@tparam[opt='server'] string player_name the name of the player that made the edit
|
||||
|
||||
@usage-- Changing the name and icon for a warp
|
||||
Warps.update_warp(warp_id, 'My Warp', 'iron-plate', game.player.name)
|
||||
|
||||
]]
|
||||
function Warps.update_warp(warp_id, new_name, new_icon, player_name)
|
||||
WrapData:update(warp_id, function(_, warp)
|
||||
-- If the icon is not valid then replace with the old icon
|
||||
if new_icon and not new_icon.name or not new_icon.type then
|
||||
new_icon = warp.icon
|
||||
end
|
||||
|
||||
warp.last_edit_name = player_name or "<server>"
|
||||
warp.last_edit_time = game.tick
|
||||
warp.name = new_name or warp.name
|
||||
warp.icon = new_icon or warp.icon
|
||||
end)
|
||||
end
|
||||
|
||||
--[[-- Set the editing state for a player, can be used as a warning or to display a text field
|
||||
@tparam string warp_id the uid of the warp that you want to effect
|
||||
@tparam string player_name the name of the player you want to set the state for
|
||||
@tparam boolean state the new state to set editing to
|
||||
|
||||
@usage-- Setting your editing state to true
|
||||
Warps.set_editing(warp_id, game.player.name, true)
|
||||
|
||||
]]
|
||||
function Warps.set_editing(warp_id, player_name, state)
|
||||
WrapData:update(warp_id, function(_, warp)
|
||||
warp.currently_editing[player_name] = state
|
||||
end)
|
||||
end
|
||||
|
||||
--[[-- Adds an update handler for when a warp is added, removed, or updated
|
||||
@tparam function handler the handler which is called when a warp is updated
|
||||
|
||||
@usage-- Add a game print when a warp is updated
|
||||
Warps.on_update(function(warp)
|
||||
game.print(warp.force_name..' now has the warp: '..warp.name)
|
||||
end)
|
||||
|
||||
]]
|
||||
function Warps.on_update(handler)
|
||||
WrapData:on_update(handler)
|
||||
end
|
||||
|
||||
--- Getters.
|
||||
-- function used to get information about warps
|
||||
-- @section getters
|
||||
|
||||
--[[-- Gets the warp information that is linked with this id
|
||||
@tparam string warp_id the uid of the warp you want to get
|
||||
@treturn table the warp information
|
||||
|
||||
@usage-- Getting warp information outside of on_update
|
||||
local warp = Warps.get_warp(warp_id)
|
||||
|
||||
]]
|
||||
function Warps.get_warp(warp_id)
|
||||
return WrapData:get(warp_id)
|
||||
end
|
||||
|
||||
--[[-- Gets all the warp ids that a force has
|
||||
@tparam string force_name the name of the force that you want the warp ids for
|
||||
@treturn table an array of all the warp ids
|
||||
|
||||
@usage-- Getting the warp ids for a force
|
||||
local warp_ids = Warps.get_force_warp_ids(game.player.force.name)
|
||||
|
||||
]]
|
||||
function Warps.get_force_warp_ids(force_name)
|
||||
return force_warps[force_name] or {}
|
||||
end
|
||||
|
||||
--[[-- Get the id of the spawn warp
|
||||
@tparam string force_name the name of the force that you want to get the spawn warp for
|
||||
@treturn ?string|nil the uid of the spawn warp for this force if there is one
|
||||
|
||||
@usage-- Getting the spawn warp id
|
||||
local spawn_id = Warps.get_spawn_warp_id(game.player.force.name)
|
||||
|
||||
]]
|
||||
function Warps.get_spawn_warp_id(force_name)
|
||||
local warp_ids = force_warps[force_name] or {}
|
||||
return warp_ids.spawn
|
||||
end
|
||||
|
||||
--[[-- Gets the editing state for a player
|
||||
@tparam string warp_id the uid of the warp you want to check
|
||||
@tparam string player_name the name of the player that you want to check
|
||||
@treturn boolean weather the player is currently editing this warp
|
||||
|
||||
@usage-- Check if a player is editing a warp or not
|
||||
local editing = Warps.get_editing(warp_id, game.player.name)
|
||||
|
||||
]]
|
||||
function Warps.get_editing(warp_id, player_name)
|
||||
local warp = WrapData:get(warp_id)
|
||||
return warp.currently_editing[player_name]
|
||||
end
|
||||
|
||||
-- Module return
|
||||
return Warps
|
||||
@@ -1,547 +0,0 @@
|
||||
--[[-- Gui Module - Virtual Layer
|
||||
- Adds a virtual layer to store power to save space.
|
||||
@gui Virtual Layer
|
||||
@alias vlayer_container
|
||||
]]
|
||||
|
||||
local Gui = require("modules/exp_gui")
|
||||
local Roles = require("modules/exp_roles")
|
||||
local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event
|
||||
local format_number = require("util").format_number --- @dep util
|
||||
local config = require("modules.exp_legacy.config.vlayer") --- @dep config.vlayer
|
||||
local vlayer = require("modules.exp_legacy.modules.control.vlayer")
|
||||
|
||||
local Selection = require("modules/exp_util/selection")
|
||||
local SelectArea = Selection.connect("ExpGui_VLayer")
|
||||
|
||||
--- Align an aabb to the grid by expanding it
|
||||
local function aabb_align_expand(aabb)
|
||||
return {
|
||||
left_top = { x = math.floor(aabb.left_top.x), y = math.floor(aabb.left_top.y) },
|
||||
right_bottom = { x = math.ceil(aabb.right_bottom.x), y = math.ceil(aabb.right_bottom.y) },
|
||||
}
|
||||
end
|
||||
|
||||
local vlayer_container --- @type ExpElement
|
||||
local vlayer_gui_control_type --- @type ExpElement
|
||||
local vlayer_gui_control_list --- @type ExpElement
|
||||
|
||||
local vlayer_control_type_list = {
|
||||
[1] = "energy",
|
||||
[2] = "circuit",
|
||||
[3] = "storage_input",
|
||||
[4] = "storage_output",
|
||||
}
|
||||
|
||||
local function pos_to_gps_string(pos, surface_name)
|
||||
return "[gps=" .. string.format("%.1f", pos.x) .. "," .. string.format("%.1f", pos.y) .. "," .. surface_name .. "]"
|
||||
end
|
||||
|
||||
local function format_energy(amount, unit)
|
||||
if amount < 1 then
|
||||
return "0 " .. unit
|
||||
end
|
||||
|
||||
local suffix = ""
|
||||
local suffix_list = {
|
||||
["P"] = 1000000000000000,
|
||||
["T"] = 1000000000000,
|
||||
["G"] = 1000000000,
|
||||
["M"] = 1000000,
|
||||
["k"] = 1000,
|
||||
}
|
||||
|
||||
for letter, limit in pairs(suffix_list) do
|
||||
if math.abs(amount) >= limit then
|
||||
amount = string.format("%.1f", amount / limit)
|
||||
suffix = letter
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local k
|
||||
local formatted = amount
|
||||
|
||||
while true do
|
||||
formatted, k = string.gsub(formatted, "^(-?%d+)(%d%d%d)", "%1,%2")
|
||||
|
||||
if (k == 0) then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return formatted .. " " .. suffix .. unit
|
||||
end
|
||||
|
||||
local ExpUtil = require("modules/exp_util")
|
||||
--- When an area is selected to add protection to the area
|
||||
SelectArea:on_selection(function(event)
|
||||
log(ExpUtil.format_any(event))
|
||||
local area = aabb_align_expand(event.area)
|
||||
local player = game.players[event.player_index]
|
||||
|
||||
if not player then
|
||||
return
|
||||
end
|
||||
|
||||
local container = Gui.get_left_element(vlayer_container, player)
|
||||
local disp = container.frame["vlayer_st_2"].disp.table
|
||||
local target = vlayer_control_type_list[disp[vlayer_gui_control_type.name].selected_index]
|
||||
local entities --- @type LuaEntity[]
|
||||
|
||||
if config.power_on_space and event.surface and event.surface.platform and target == "energy" then
|
||||
if (player.force --[[@as LuaForce]]).technologies[config.power_on_space_research.name].level >= config.power_on_space_research.level then
|
||||
entities = event.surface.find_entities_filtered{ area = area, name = "constant-combinator", force = player.force }
|
||||
else
|
||||
player.print{ "vlayer.power-on-space-research", config.power_on_space_research.name, config.power_on_space_research.level }
|
||||
return
|
||||
end
|
||||
else
|
||||
entities = event.surface.find_entities_filtered{ area = area, name = "steel-chest", force = player.force }
|
||||
end
|
||||
|
||||
if #entities == 0 then
|
||||
player.print{ "vlayer.steel-chest-detect" }
|
||||
return
|
||||
elseif #entities > 1 then
|
||||
player.print{ "vlayer.result-unable", { "vlayer.control-type-" .. target:gsub("_", "-") }, { "vlayer.result-multiple" } }
|
||||
return
|
||||
end
|
||||
|
||||
if not entities[1] then
|
||||
return
|
||||
end
|
||||
|
||||
local e = entities[1]
|
||||
local e_pos = { x = string.format("%.1f", e.position.x), y = string.format("%.1f", e.position.y) }
|
||||
local e_circ = nil -- e.get_wire_connectors{ or_create = false }
|
||||
|
||||
if e.name and e.name == "steel-chest" and (not assert(e.get_inventory(defines.inventory.chest)).is_empty()) then
|
||||
player.print{ "vlayer.steel-chest-empty" }
|
||||
return
|
||||
end
|
||||
|
||||
if (vlayer.get_interface_counts()[target] >= config.interface_limit[target]) then
|
||||
player.print{ "vlayer.result-unable", { "vlayer.control-type-" .. target:gsub("_", "-") }, { "vlayer.result-limit" } }
|
||||
return
|
||||
end
|
||||
|
||||
e.destroy()
|
||||
|
||||
if target == "energy" then
|
||||
if not vlayer.create_energy_interface(event.surface, e_pos, player) then
|
||||
player.print{ "vlayer.result-unable", { "vlayer.control-type-energy" }, { "vlayer.result-space" } }
|
||||
return
|
||||
end
|
||||
elseif target == "circuit" then
|
||||
vlayer.create_circuit_interface(event.surface, e_pos, e_circ, player)
|
||||
elseif target == "storage_input" then
|
||||
vlayer.create_input_interface(event.surface, e_pos, e_circ, player)
|
||||
elseif target == "storage_output" then
|
||||
vlayer.create_output_interface(event.surface, e_pos, e_circ, player)
|
||||
end
|
||||
|
||||
game.print{ "vlayer.interface-result", player.name, pos_to_gps_string(e_pos, event.surface.name), { "vlayer.result-build" }, { "vlayer.control-type-" .. target:gsub("_", "-") } }
|
||||
end)
|
||||
|
||||
--- Display label for the number of solar panels
|
||||
-- @element vlayer_gui_display_item_solar_name
|
||||
local vlayer_gui_display_item_solar_name = Gui.define("vlayer_gui_display_item_solar_name")
|
||||
:draw{
|
||||
type = "label",
|
||||
name = Gui.from_name,
|
||||
caption = { "vlayer.display-item-solar" },
|
||||
style = "heading_2_label",
|
||||
}:style{
|
||||
width = 200,
|
||||
}
|
||||
|
||||
local vlayer_gui_display_item_solar_count = Gui.define("vlayer_gui_display_item_solar_count")
|
||||
:draw{
|
||||
type = "progressbar",
|
||||
name = Gui.from_name,
|
||||
caption = "",
|
||||
value = 0,
|
||||
style = "electric_satisfaction_statistics_progressbar",
|
||||
}:style{
|
||||
width = 200,
|
||||
font = "heading-2",
|
||||
}
|
||||
|
||||
--- Display label for the number of accumulators
|
||||
-- @element vlayer_gui_display_item_accumulator_name
|
||||
local vlayer_gui_display_item_accumulator_name = Gui.define("vlayer_gui_display_item_accumulator_name")
|
||||
:draw{
|
||||
type = "label",
|
||||
name = Gui.from_name,
|
||||
caption = { "vlayer.display-item-accumulator" },
|
||||
style = "heading_2_label",
|
||||
}:style{
|
||||
width = 200,
|
||||
}
|
||||
|
||||
local vlayer_gui_display_item_accumulator_count = Gui.define("vlayer_gui_display_item_accumulator_count")
|
||||
:draw{
|
||||
type = "progressbar",
|
||||
name = Gui.from_name,
|
||||
caption = "",
|
||||
value = 0,
|
||||
style = "electric_satisfaction_statistics_progressbar",
|
||||
}:style{
|
||||
width = 200,
|
||||
font = "heading-2",
|
||||
}
|
||||
|
||||
--- Display label for the surface area
|
||||
-- @element vlayer_gui_display_signal_surface_area_name
|
||||
local vlayer_gui_display_signal_surface_area_name = Gui.define("vlayer_gui_display_signal_surface_area_name")
|
||||
:draw{
|
||||
type = "label",
|
||||
name = Gui.from_name,
|
||||
caption = { "vlayer.display-remaining-surface-area" },
|
||||
tooltip = { "vlayer.display-remaining-surface-area-tooltip" },
|
||||
style = "heading_2_label",
|
||||
}:style{
|
||||
width = 200,
|
||||
}
|
||||
|
||||
local vlayer_gui_display_signal_surface_area_count = Gui.define("vlayer_gui_display_signal_surface_area_count")
|
||||
:draw{
|
||||
type = "progressbar",
|
||||
name = Gui.from_name,
|
||||
caption = "",
|
||||
value = 0,
|
||||
style = "electric_satisfaction_statistics_progressbar",
|
||||
}:style{
|
||||
width = 200,
|
||||
font = "heading-2",
|
||||
}
|
||||
|
||||
--- Display label for the sustained energy production
|
||||
-- @element vlayer_gui_display_signal_sustained_name
|
||||
local vlayer_gui_display_signal_sustained_name = Gui.define("vlayer_gui_display_signal_sustained_name")
|
||||
:draw{
|
||||
type = "label",
|
||||
name = Gui.from_name,
|
||||
caption = { "vlayer.display-sustained-production" },
|
||||
tooltip = { "vlayer.display-sustained-production-tooltip" },
|
||||
style = "heading_2_label",
|
||||
}:style{
|
||||
width = 200,
|
||||
}
|
||||
|
||||
local vlayer_gui_display_signal_sustained_count = Gui.define("vlayer_gui_display_signal_sustained_count")
|
||||
:draw{
|
||||
type = "progressbar",
|
||||
name = Gui.from_name,
|
||||
caption = "",
|
||||
value = 0,
|
||||
style = "electric_satisfaction_statistics_progressbar",
|
||||
}:style{
|
||||
width = 200,
|
||||
font = "heading-2",
|
||||
}
|
||||
|
||||
--- Display label for the current energy production
|
||||
-- @element vlayer_gui_display_signal_production_name
|
||||
local vlayer_gui_display_signal_production_name = Gui.define("vlayer_gui_display_signal_production_name")
|
||||
:draw{
|
||||
type = "label",
|
||||
name = Gui.from_name,
|
||||
caption = { "vlayer.display-current-production" },
|
||||
tooltip = { "vlayer.display-current-production-tooltip" },
|
||||
style = "heading_2_label",
|
||||
}:style{
|
||||
width = 200,
|
||||
}
|
||||
|
||||
local vlayer_gui_display_signal_production_count = Gui.define("vlayer_gui_display_signal_production_count")
|
||||
:draw{
|
||||
type = "progressbar",
|
||||
name = Gui.from_name,
|
||||
caption = "",
|
||||
value = 0,
|
||||
style = "electric_satisfaction_statistics_progressbar",
|
||||
}:style{
|
||||
width = 200,
|
||||
font = "heading-2",
|
||||
}
|
||||
|
||||
--- Display label for the sustained energy capacity
|
||||
-- @element vlayer_gui_display_signal_capacity_name
|
||||
local vlayer_gui_display_signal_capacity_name = Gui.define("vlayer_gui_display_signal_capacity_name")
|
||||
:draw{
|
||||
type = "label",
|
||||
name = Gui.from_name,
|
||||
caption = { "vlayer.display-current-capacity" },
|
||||
tooltip = { "vlayer.display-current-capacity-tooltip" },
|
||||
style = "heading_2_label",
|
||||
}:style{
|
||||
width = 200,
|
||||
}
|
||||
|
||||
local vlayer_gui_display_signal_capacity_count = Gui.define("vlayer_gui_display_signal_capacity_count")
|
||||
:draw{
|
||||
type = "progressbar",
|
||||
name = Gui.from_name,
|
||||
caption = "",
|
||||
value = 0,
|
||||
style = "electric_satisfaction_statistics_progressbar",
|
||||
}:style{
|
||||
width = 200,
|
||||
font = "heading-2",
|
||||
}
|
||||
|
||||
--- A vertical flow containing all the displays labels and their counts
|
||||
-- @element vlayer_display_set
|
||||
local vlayer_display_set = Gui.define("vlayer_display_set")
|
||||
:draw(function(_, parent, name)
|
||||
local vlayer_set = parent.add{ type = "flow", direction = "vertical", name = name }
|
||||
local disp = Gui.elements.scroll_table(vlayer_set, 400, 2, "disp")
|
||||
|
||||
vlayer_gui_display_item_solar_name(disp)
|
||||
vlayer_gui_display_item_solar_count(disp)
|
||||
vlayer_gui_display_item_accumulator_name(disp)
|
||||
vlayer_gui_display_item_accumulator_count(disp)
|
||||
vlayer_gui_display_signal_surface_area_name(disp)
|
||||
vlayer_gui_display_signal_surface_area_count(disp)
|
||||
vlayer_gui_display_signal_sustained_name(disp)
|
||||
vlayer_gui_display_signal_sustained_count(disp)
|
||||
vlayer_gui_display_signal_production_name(disp)
|
||||
vlayer_gui_display_signal_production_count(disp)
|
||||
vlayer_gui_display_signal_capacity_name(disp)
|
||||
vlayer_gui_display_signal_capacity_count(disp)
|
||||
|
||||
return vlayer_set
|
||||
end)
|
||||
|
||||
local function vlayer_gui_list_refresh(player)
|
||||
local container = Gui.get_left_element(vlayer_container, player)
|
||||
local disp = container.frame["vlayer_st_2"].disp.table
|
||||
local target = disp[vlayer_gui_control_type.name].selected_index
|
||||
local full_list = {}
|
||||
|
||||
if target then
|
||||
local interface = vlayer.get_interfaces()[vlayer_control_type_list[target]]
|
||||
|
||||
for i = 1, vlayer.get_interface_counts()[vlayer_control_type_list[target]], 1 do
|
||||
local entity = assert(interface[i])
|
||||
local entity_position = entity.position
|
||||
table.insert(full_list, i .. " X " .. entity_position.x .. " Y " .. entity_position.y)
|
||||
end
|
||||
|
||||
disp[vlayer_gui_control_list.name].items = full_list
|
||||
end
|
||||
end
|
||||
|
||||
--- A drop down list filter by this type
|
||||
-- @element vlayer_gui_control_type
|
||||
vlayer_gui_control_type = Gui.define("vlayer_gui_control_type")
|
||||
:draw{
|
||||
type = "drop-down",
|
||||
name = Gui.from_name,
|
||||
items = { { "vlayer.control-type-energy" }, { "vlayer.control-type-circuit" }, { "vlayer.control-type-storage-input" }, { "vlayer.control-type-storage-output" } },
|
||||
selected_index = 1,
|
||||
}:style{
|
||||
width = 200,
|
||||
}:on_selection_state_changed(function(def, player, element)
|
||||
vlayer_gui_list_refresh(player)
|
||||
end)
|
||||
|
||||
--- A drop down list to see the exact item to remove
|
||||
-- @element vlayer_gui_control_list
|
||||
vlayer_gui_control_list = Gui.define("vlayer_gui_control_list")
|
||||
:draw{
|
||||
type = "drop-down",
|
||||
name = Gui.from_name,
|
||||
}:style{
|
||||
width = 200,
|
||||
}
|
||||
|
||||
--- A button to refresh the remove list
|
||||
-- @element vlayer_gui_control_refresh
|
||||
local vlayer_gui_control_refresh = Gui.define("vlayer_gui_control_refresh")
|
||||
:draw{
|
||||
type = "button",
|
||||
name = Gui.from_name,
|
||||
caption = { "vlayer.control-refresh" },
|
||||
}:style{
|
||||
width = 200,
|
||||
}:on_click(function(def, player, element)
|
||||
vlayer_gui_list_refresh(player)
|
||||
end)
|
||||
|
||||
--- A button to check if the item is the one wanted to remove
|
||||
-- @element vlayer_gui_control_see
|
||||
local vlayer_gui_control_see = Gui.define("vlayer_gui_control_see")
|
||||
:draw{
|
||||
type = "button",
|
||||
name = Gui.from_name,
|
||||
caption = { "vlayer.control-see" },
|
||||
}:style{
|
||||
width = 200,
|
||||
}:on_click(function(def, player, element, event)
|
||||
local target = assert(element.parent)[vlayer_gui_control_type.name].selected_index
|
||||
local n = assert(element.parent)[vlayer_gui_control_list.name].selected_index
|
||||
|
||||
if target and vlayer_control_type_list[target] and n > 0 then
|
||||
local i = vlayer.get_interfaces()
|
||||
local entity = i[vlayer_control_type_list[target]][n]
|
||||
if entity and entity.valid then
|
||||
player.set_controller{ type = defines.controllers.remote, position = entity.position, surface = entity.surface }
|
||||
player.print{ "vlayer.result-interface-location", { "vlayer.control-type-" .. vlayer_control_type_list[target]:gsub("_", "-") }, pos_to_gps_string(entity.position, entity.surface.name) }
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
--- A button used to build the vlayer interface
|
||||
-- @element vlayer_gui_control_build
|
||||
local vlayer_gui_control_build = Gui.define("vlayer_gui_control_build")
|
||||
:draw{
|
||||
type = "button",
|
||||
name = Gui.from_name,
|
||||
caption = { "vlayer.control-build" },
|
||||
}:style{
|
||||
width = 200,
|
||||
}:on_click(function(def, player, element)
|
||||
if SelectArea:stop(player) then
|
||||
player.print{ "exp_util.selection_exit", { "vlayer.main-tooltip" } }
|
||||
else
|
||||
SelectArea:start(player)
|
||||
player.print{ "exp_util.selection_enter", { "vlayer.main-tooltip" } }
|
||||
end
|
||||
|
||||
vlayer_gui_list_refresh(player)
|
||||
end)
|
||||
|
||||
--- A button used to remove the vlayer interface
|
||||
-- @element vlayer_gui_control_remove
|
||||
local vlayer_gui_control_remove = Gui.define("vlayer_gui_control_remove")
|
||||
:draw{
|
||||
type = "button",
|
||||
name = Gui.from_name,
|
||||
caption = { "vlayer.control-remove" },
|
||||
}:style{
|
||||
width = 200,
|
||||
}:on_click(function(def, player, element)
|
||||
local target = assert(element.parent)[vlayer_gui_control_type.name].selected_index
|
||||
local n = assert(element.parent)[vlayer_gui_control_list.name].selected_index
|
||||
|
||||
if target and vlayer_control_type_list[target] and n > 0 then
|
||||
local i = vlayer.get_interfaces()
|
||||
|
||||
if i and i[vlayer_control_type_list[target]] then
|
||||
local entity = assert(i[vlayer_control_type_list[target]][n])
|
||||
local interface_type, interface_surface, interface_position = vlayer.remove_interface(entity.surface, entity.position)
|
||||
|
||||
if interface_type then
|
||||
game.print{ "vlayer.interface-result", player.name, pos_to_gps_string(interface_position, interface_surface.name), { "vlayer.result-remove" }, { "vlayer.control-type-" .. interface_type } }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
vlayer_gui_list_refresh(player)
|
||||
end)
|
||||
|
||||
--- A vertical flow containing all the control buttons
|
||||
-- @element vlayer_control_set
|
||||
local vlayer_control_set = Gui.define("vlayer_control_set")
|
||||
:draw(function(_, parent, name)
|
||||
local player = Gui.get_player(parent)
|
||||
local vlayer_set = parent.add{ type = "flow", direction = "vertical", name = name }
|
||||
local disp = Gui.elements.scroll_table(vlayer_set, 400, 2, "disp")
|
||||
|
||||
vlayer_gui_control_type(disp)
|
||||
vlayer_gui_control_list(disp)
|
||||
vlayer_gui_control_refresh(disp)
|
||||
vlayer_gui_control_see(disp)
|
||||
local b = vlayer_gui_control_build(disp)
|
||||
local r = vlayer_gui_control_remove(disp)
|
||||
local v = Roles.player_has_permission(player, "exp_scenario.gui.vlayer_edit")
|
||||
b.visible = v
|
||||
r.visible = v
|
||||
|
||||
return vlayer_set
|
||||
end)
|
||||
|
||||
--- The main container for the vlayer gui
|
||||
-- @element vlayer_container
|
||||
vlayer_container = Gui.define("vlayer_container")
|
||||
:draw(function(definition, parent)
|
||||
local container = Gui.elements.container(parent, 400)
|
||||
|
||||
vlayer_display_set(container, "vlayer_st_1")
|
||||
vlayer_control_set(container, "vlayer_st_2")
|
||||
|
||||
return container.parent
|
||||
end)
|
||||
|
||||
--- Add the element to the left flow with a toolbar button
|
||||
Gui.add_left_element(vlayer_container, false)
|
||||
Gui.toolbar.create_button{
|
||||
name = "vlayer_toggle",
|
||||
left_element = vlayer_container,
|
||||
sprite = "entity/solar-panel",
|
||||
tooltip = { "vlayer.main-tooltip" },
|
||||
visible = function(player, element)
|
||||
return Roles.player_has_permission(player, "exp_scenario.gui.vlayer")
|
||||
end
|
||||
}
|
||||
|
||||
--- Update the visibly of the buttons based on a players roles
|
||||
local function role_update_event(event)
|
||||
local player = game.players[event.player_index]
|
||||
local visible = Roles.player_has_permission(player, "exp_scenario.gui.vlayer_edit")
|
||||
local container = Gui.get_left_element(vlayer_container, player)
|
||||
local disp = container.frame["vlayer_st_2"].disp.table
|
||||
disp[vlayer_gui_control_build.name].visible = visible
|
||||
disp[vlayer_gui_control_remove.name].visible = visible
|
||||
end
|
||||
|
||||
Event.add(Roles.events.on_player_roles_changed, role_update_event)
|
||||
|
||||
Event.on_nth_tick(config.update_tick_gui, function(_)
|
||||
local stats = vlayer.get_statistics()
|
||||
local items = vlayer.get_items()
|
||||
local items_alloc = vlayer.get_allocated_items()
|
||||
|
||||
local vlayer_display = {
|
||||
[vlayer_gui_display_item_solar_count.name] = {
|
||||
val = (items_alloc["solar-panel"] / math.max(items["solar-panel"], 1)),
|
||||
cap = format_number(items_alloc["solar-panel"], false) .. " / " .. format_number(items["solar-panel"], false),
|
||||
},
|
||||
[vlayer_gui_display_item_accumulator_count.name] = {
|
||||
val = (items_alloc["accumulator"] / math.max(items["accumulator"], 1)),
|
||||
cap = format_number(items_alloc["accumulator"], false) .. " / " .. format_number(items["accumulator"], false),
|
||||
},
|
||||
[vlayer_gui_display_signal_surface_area_count.name] = {
|
||||
val = (stats.total_surface_area / math.max(stats.surface_area, 1)),
|
||||
cap = format_number(stats.remaining_surface_area)
|
||||
},
|
||||
[vlayer_gui_display_signal_sustained_count.name] = {
|
||||
val = (stats.energy_sustained / math.max(stats.energy_total_production, 1)),
|
||||
cap = format_energy(stats.energy_sustained, "W") .. " / " .. format_energy(stats.energy_total_production, "W")
|
||||
},
|
||||
[vlayer_gui_display_signal_production_count.name] = {
|
||||
val = (stats.energy_production / math.max(stats.energy_max, 1)),
|
||||
cap = format_energy(stats.energy_production, "W") .. " / " .. format_energy(stats.energy_max, "W"),
|
||||
},
|
||||
[vlayer_gui_display_signal_capacity_count.name] = {
|
||||
val = (stats.energy_storage / math.max(stats.energy_capacity, 1)),
|
||||
cap = format_energy(stats.energy_storage, "J") .. " / " .. format_energy(stats.energy_capacity, "J"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, player in pairs(game.connected_players) do
|
||||
local container = Gui.get_left_element(vlayer_container, player)
|
||||
local disp = container.frame["vlayer_st_1"].disp.table
|
||||
|
||||
for k, v in pairs(vlayer_display) do
|
||||
disp[k].caption = v.cap
|
||||
|
||||
if v.val then
|
||||
disp[k].value = v.val
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
@@ -1,909 +0,0 @@
|
||||
--[[-- Gui Module - Warp List
|
||||
- Adds a warp list gui which allows players to add and remove warp points
|
||||
@gui Warps-List
|
||||
@alias warp_list
|
||||
]]
|
||||
|
||||
local ExpUtil = require("modules/exp_util")
|
||||
local Gui = require("modules/exp_gui")
|
||||
local Datastore = require("modules.exp_legacy.expcore.datastore") --- @dep expcore.datastore
|
||||
local Storage = require("modules/exp_util/storage")
|
||||
local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event
|
||||
local Roles = require("modules/exp_roles")
|
||||
local Colors = require("modules/exp_util/include/color")
|
||||
local config = require("modules.exp_legacy.config.gui.warps") --- @dep config.gui.warps
|
||||
local Warps = require("modules.exp_legacy.modules.control.warps") --- @dep modules.control.warps
|
||||
|
||||
local format_time = ExpUtil.format_time_factory_locale{ format = "short", hours = true, minutes = true }
|
||||
|
||||
--- Stores all data for the warp gui
|
||||
local WrapGuiData = Datastore.connect("WrapGuiData")
|
||||
WrapGuiData:set_serializer(Datastore.name_serializer)
|
||||
local PlayerInRange = WrapGuiData:combine("PlayerInRange")
|
||||
PlayerInRange:set_default(false)
|
||||
local PlayerCooldown = WrapGuiData:combine("PlayerCooldown")
|
||||
PlayerCooldown:set_default(0)
|
||||
|
||||
--- Table that stores a boolean value of weather to keep the warp gui open
|
||||
local keep_gui_open = {}
|
||||
Storage.register(keep_gui_open, function(tbl)
|
||||
keep_gui_open = tbl
|
||||
end)
|
||||
|
||||
--- Styles used for sprite buttons
|
||||
local Styles = {
|
||||
sprite22 = { height = 22, width = 22, padding = -2 },
|
||||
sprite32 = { height = 32, width = 32, left_margin = 1 },
|
||||
}
|
||||
--- Status icon of a warp
|
||||
local warp_status_icons = {
|
||||
cooldown = "[img=utility/multiplayer_waiting_icon]",
|
||||
not_available = "[img=utility/set_bar_slot]",
|
||||
bypass = "[img=utility/side_menu_bonus_icon]",
|
||||
current = "[img=utility/side_menu_map_icon]",
|
||||
connected = "[img=utility/logistic_network_panel_white]",
|
||||
different = "[img=utility/warning_white]",
|
||||
}
|
||||
|
||||
--- Returns if a player is allowed to edit the given warp
|
||||
--- If a player is allowed to use the edit buttons
|
||||
local function check_player_permissions(player, action, warp)
|
||||
-- Check if the action is allow edit and then check bypass settings
|
||||
if action == "allow_edit_warp" then
|
||||
-- Check if the warp is the spawn then it cant be edited
|
||||
local spawn_id = Warps.get_spawn_warp_id(player.force.name)
|
||||
if spawn_id == warp.warp_id then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Check if the player being the last to edit will override existing permisisons
|
||||
if config.user_can_edit_own_warps and warp.last_edit_name == player.name then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- Check player has permission based on value in the config
|
||||
local action_config = config[action]
|
||||
if action_config == "all" then
|
||||
return true
|
||||
elseif action_config == "admin" then
|
||||
return player.admin
|
||||
elseif action_config == "exp_roles" then
|
||||
return Roles.player_has_permission(player, config["exp_roles_" .. action])
|
||||
end
|
||||
|
||||
-- Return false as all other conditions have not been met
|
||||
return false
|
||||
end
|
||||
|
||||
--- Will add a new warp to the list, checks if the player is too close to an existing one
|
||||
-- @element add_new_warp
|
||||
local add_new_warp = Gui.define("add_new_warp")
|
||||
:draw{
|
||||
type = "sprite-button",
|
||||
sprite = "utility/add",
|
||||
tooltip = { "warp-list.add-tooltip" },
|
||||
style = "shortcut_bar_button",
|
||||
name = Gui.from_name,
|
||||
}
|
||||
:style(Styles.sprite22)
|
||||
:on_click(function(def, player, element)
|
||||
if player.controller_type ~= defines.controllers.character then return end
|
||||
-- Add the new warp
|
||||
local force_name = player.force.name
|
||||
local surface = player.physical_surface
|
||||
local position = player.physical_position
|
||||
|
||||
-- Check if the warp is too close to water
|
||||
local water_tiles = surface.find_tiles_filtered{ collision_mask = "water_tile", radius = config.standard_proximity_radius + 1, position = position }
|
||||
if #water_tiles > 0 then
|
||||
player.print(
|
||||
{ "expcore-commands.command-fail", { "warp-list.too-close-to-water", config.standard_proximity_radius + 1 } },
|
||||
{ color = Colors.orange_red }
|
||||
)
|
||||
if game.player then game.player.play_sound{ path = "utility/wire_pickup" } end
|
||||
for _, tile in pairs(water_tiles) do
|
||||
rendering.draw_sprite{
|
||||
sprite = "utility/rail_path_not_possible",
|
||||
x_scale = 0.5,
|
||||
y_scale = 0.5,
|
||||
target = tile.position,
|
||||
surface = surface,
|
||||
players = { player },
|
||||
time_to_live = 60,
|
||||
}
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
-- Check if there are player entities in the way (has a bigger radius because the enities that can be placed by a player are larger)
|
||||
local entities = surface.find_entities_filtered{
|
||||
radius = config.standard_proximity_radius + 2.5,
|
||||
position = position,
|
||||
collision_mask = {
|
||||
"item", "object", "player", "water_tile",
|
||||
},
|
||||
}
|
||||
-- Remove 1 because that is the current player
|
||||
if #entities > 1 then
|
||||
player.print(
|
||||
{ "expcore-commands.command-fail", { "warp-list.too-close-to-entities", config.standard_proximity_radius + 2.5 } },
|
||||
{ color = Colors.orange_red }
|
||||
)
|
||||
if game.player then game.player.play_sound{ path = "utility/wire_pickup" } end
|
||||
local character = player.character
|
||||
for _, entity in pairs(entities) do
|
||||
if entity ~= character then
|
||||
rendering.draw_sprite{
|
||||
sprite = "utility/rail_path_not_possible",
|
||||
x_scale = 0.5,
|
||||
y_scale = 0.5,
|
||||
target = entity,
|
||||
surface = surface,
|
||||
players = { player },
|
||||
time_to_live = 60,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
-- Create the warp
|
||||
local warp_id = Warps.add_warp(force_name, surface, position, player.name)
|
||||
Warps.make_warp_tag(warp_id)
|
||||
Warps.make_warp_area(warp_id)
|
||||
end)
|
||||
|
||||
--- Warp icon button, this will trigger a warp when the player is able to
|
||||
-- @element warp_icon_button
|
||||
local warp_icon_button = Gui.define("warp_icon_button")
|
||||
:draw(function(def, parent, warp)
|
||||
local warp_position = warp.position
|
||||
|
||||
-- The SpritePath type is not the same as the SignalID type
|
||||
local sprite = warp.icon.type .. "/" .. warp.icon.name
|
||||
if warp.icon.type == "virtual" then
|
||||
sprite = "virtual-signal/" .. warp.icon.name
|
||||
end
|
||||
|
||||
-- Draw the element
|
||||
return parent.add{
|
||||
type = "sprite-button",
|
||||
sprite = sprite,
|
||||
name = def.name,
|
||||
tooltip = { "warp-list.goto-tooltip", warp_position.x, warp_position.y },
|
||||
style = "slot_button",
|
||||
}
|
||||
end)
|
||||
:style(Styles.sprite32)
|
||||
:on_click(function(def, player, element)
|
||||
if element.type == "choose-elem-button" then return end
|
||||
local warp_id = assert(element.parent).caption
|
||||
Warps.teleport_player(warp_id, player)
|
||||
|
||||
-- Reset the warp cooldown if the player does not have unlimited warps
|
||||
if not check_player_permissions(player, "bypass_warp_cooldown") then
|
||||
PlayerCooldown:set(player, config.update_smoothing * config.cooldown_duration)
|
||||
end
|
||||
|
||||
PlayerInRange:set(player, warp_id)
|
||||
end)
|
||||
|
||||
--- The button that is visible when the warp is in edit state
|
||||
-- @element warp_icon_editing
|
||||
local warp_icon_editing = Gui.define("warp_icon_editing")
|
||||
:draw(function(def, parent, warp)
|
||||
return parent.add{
|
||||
name = def.name,
|
||||
type = "choose-elem-button",
|
||||
elem_type = "signal",
|
||||
signal = { type = warp.icon.type, name = warp.icon.name },
|
||||
tooltip = { "warp-list.goto-edit" },
|
||||
}
|
||||
end)
|
||||
:style(Styles.sprite32)
|
||||
|
||||
--- Warp label, visible if the player is not in edit state
|
||||
-- @element warp_label
|
||||
local warp_label = Gui.define("warp_label")
|
||||
:draw(function(def, parent, warp)
|
||||
local last_edit_name = warp.last_edit_name
|
||||
local last_edit_time = warp.last_edit_time
|
||||
-- Draw the element
|
||||
return parent.add{
|
||||
type = "label",
|
||||
caption = warp.name,
|
||||
tooltip = { "warp-list.last-edit", last_edit_name, format_time(last_edit_time) },
|
||||
name = def.name,
|
||||
}
|
||||
end)
|
||||
:style{
|
||||
single_line = true,
|
||||
left_padding = 2,
|
||||
right_padding = 2,
|
||||
horizontally_stretchable = true,
|
||||
}
|
||||
:on_click(function(def, player, element)
|
||||
local warp_id = assert(element.parent).caption
|
||||
local warp = Warps.get_warp(warp_id)
|
||||
player.set_controller{ type = defines.controllers.remote, position = warp.position, surface = warp.surface }
|
||||
end)
|
||||
|
||||
--- Warp status, visible if the player is not in edit state
|
||||
--- This will show if the warp is connected or not
|
||||
-- @element warp_status
|
||||
local warp_status = Gui.define("warp_status")
|
||||
:draw{
|
||||
type = "label",
|
||||
caption = "[img=utility/electricity_icon_unplugged]", -- Temporary icon
|
||||
name = Gui.from_name,
|
||||
}
|
||||
:style{
|
||||
-- When editing mode because textbox is larger the icon would move up.
|
||||
top_padding = 1,
|
||||
single_line = false,
|
||||
}
|
||||
|
||||
--- Warp textfield, visible if the player is in edit state
|
||||
-- @element warp_textfield
|
||||
local warp_textfield = Gui.define("warp_textfield")
|
||||
:draw(function(def, parent, warp)
|
||||
-- Draw the element
|
||||
return parent.add{
|
||||
type = "textfield",
|
||||
text = warp.name,
|
||||
clear_and_focus_on_right_click = true,
|
||||
name = def.name,
|
||||
}
|
||||
end)
|
||||
:style{
|
||||
-- Required fields to make it squashable and strechable.
|
||||
minimal_width = 10,
|
||||
maximal_width = 300,
|
||||
horizontally_squashable = true,
|
||||
horizontally_stretchable = true,
|
||||
-- Other styling
|
||||
height = 22,
|
||||
padding = -2,
|
||||
left_margin = 2,
|
||||
right_margin = 2,
|
||||
}
|
||||
:on_confirmed(function(def, player, element)
|
||||
local warp_id = assert(element.parent).caption
|
||||
local warp_name = element.text
|
||||
local warp_icon = assert(element.parent).parent["icon-" .. warp_id][warp_icon_editing.name].elem_value --[[@as SignalID]]
|
||||
if warp_icon.type == nil then warp_icon.type = "item" end
|
||||
Warps.set_editing(warp_id, player.name)
|
||||
Warps.update_warp(warp_id, warp_name, warp_icon, player.name)
|
||||
end)
|
||||
|
||||
--- Confirms the edit to name or icon of the warp
|
||||
-- @element confirm_edit_button
|
||||
local confirm_edit_button = Gui.define("confirm_edit_button")
|
||||
:draw{
|
||||
type = "sprite-button",
|
||||
sprite = "utility/confirm_slot",
|
||||
tooltip = { "warp-list.confirm-tooltip" },
|
||||
style = "shortcut_bar_button_green",
|
||||
name = Gui.from_name,
|
||||
}
|
||||
:style(Styles.sprite22)
|
||||
:on_click(function(def, player, element)
|
||||
local parent = assert(element.parent)
|
||||
local grandparent = assert(parent.parent)
|
||||
local warp_id = parent.caption --[[@as string]]
|
||||
local name_flow = grandparent["name-" .. warp_id]
|
||||
local icon_flow = grandparent["icon-" .. warp_id]
|
||||
local warp_name = name_flow[warp_textfield.name].text
|
||||
local warp_icon = icon_flow[warp_icon_editing.name].elem_value --[[@as SignalID]]
|
||||
if warp_icon.type == nil then warp_icon.type = "item" end
|
||||
Warps.set_editing(warp_id, player.name)
|
||||
Warps.update_warp(warp_id, warp_name, warp_icon, player.name)
|
||||
end)
|
||||
|
||||
--- Cancels the editing changes of the selected warp name or icon
|
||||
-- @element cancel_edit_button
|
||||
local cancel_edit_button = Gui.define("cancel_edit_button")
|
||||
:draw{
|
||||
type = "sprite-button",
|
||||
sprite = "utility/close_black",
|
||||
tooltip = { "warp-list.cancel-tooltip" },
|
||||
style = "shortcut_bar_button_red",
|
||||
name = Gui.from_name,
|
||||
}
|
||||
:style(Styles.sprite22)
|
||||
:on_click(function(def, player, element)
|
||||
local warp_id = assert(element.parent).caption
|
||||
-- Check if this is the first edit, if so remove the warp.
|
||||
local warp = Warps.get_warp(warp_id)
|
||||
if warp.updates == 1 then
|
||||
Warps.remove_warp(warp_id)
|
||||
return
|
||||
end
|
||||
Warps.set_editing(warp_id, player.name)
|
||||
end)
|
||||
|
||||
--- Removes a warp from the list, including the physical area and map tag
|
||||
-- @element remove_warp_button
|
||||
local remove_warp_button = Gui.define("remove_warp_button")
|
||||
:draw{
|
||||
type = "sprite-button",
|
||||
sprite = "utility/trash",
|
||||
tooltip = { "warp-list.remove-tooltip" },
|
||||
style = "shortcut_bar_button_red",
|
||||
name = Gui.from_name,
|
||||
}
|
||||
:style(Styles.sprite22)
|
||||
:on_click(function(def, player, element)
|
||||
local warp_id = assert(element.parent).caption
|
||||
Warps.remove_warp(warp_id)
|
||||
end)
|
||||
|
||||
--- Opens edit mode for the warp
|
||||
-- @element edit_warp_button
|
||||
local edit_warp_button = Gui.define("edit_warp_button")
|
||||
:draw{
|
||||
type = "sprite-button",
|
||||
sprite = "utility/rename_icon",
|
||||
tooltip = { "warp-list.edit-tooltip-none" },
|
||||
style = "shortcut_bar_button",
|
||||
name = Gui.from_name,
|
||||
}
|
||||
:style(Styles.sprite22)
|
||||
:on_click(function(def, player, element)
|
||||
local warp_id = assert(element.parent).caption
|
||||
Warps.set_editing(warp_id, player.name, true)
|
||||
end)
|
||||
|
||||
local update_all_warp_elements
|
||||
--- Set of three elements which make up each row of the warp table
|
||||
-- @element add_warp_elements
|
||||
local add_warp_elements = Gui.define("add_warp_elements")
|
||||
:draw(function(_, parent, warp)
|
||||
-- Add icon flow, this will contain the warp button and warp icon edit button
|
||||
local icon_flow = parent.add{
|
||||
name = "icon-" .. warp.warp_id,
|
||||
type = "flow",
|
||||
caption = warp.warp_id,
|
||||
}
|
||||
icon_flow.style.padding = 0
|
||||
|
||||
-- Add the button and the icon edit button
|
||||
warp_icon_button(icon_flow, warp)
|
||||
warp_icon_editing(icon_flow, warp)
|
||||
|
||||
-- Add name flow, this will contain the warp label and textbox
|
||||
local name_flow = parent.add{
|
||||
type = "flow",
|
||||
name = "name-" .. warp.warp_id,
|
||||
caption = warp.warp_id,
|
||||
}
|
||||
name_flow.style.padding = 0
|
||||
|
||||
-- Add the label and textfield of the warp
|
||||
warp_status(name_flow)
|
||||
warp_label(name_flow, warp)
|
||||
warp_textfield(name_flow, warp)
|
||||
|
||||
-- Add button flow, this will contain buttons to manage this specific warp
|
||||
local button_flow = parent.add{
|
||||
type = "flow",
|
||||
name = "button-" .. warp.warp_id,
|
||||
caption = warp.warp_id,
|
||||
}
|
||||
button_flow.style.padding = 0
|
||||
|
||||
-- Add both edit state buttons
|
||||
confirm_edit_button(button_flow)
|
||||
cancel_edit_button(button_flow)
|
||||
edit_warp_button(button_flow)
|
||||
remove_warp_button(button_flow)
|
||||
|
||||
return Gui.no_return()
|
||||
end)
|
||||
|
||||
-- Removes the three elements that are added as part of the warp base
|
||||
local function remove_warp_elements(parent, warp_id)
|
||||
Gui.destroy_if_valid(parent["icon-" .. warp_id])
|
||||
Gui.destroy_if_valid(parent["name-" .. warp_id])
|
||||
Gui.destroy_if_valid(parent["button-" .. warp_id])
|
||||
end
|
||||
|
||||
--- This timer controls when a player is able to warp, eg every 60 seconds
|
||||
-- @element warp_timer
|
||||
local warp_timer = Gui.define("warp_timer")
|
||||
:draw{
|
||||
type = "progressbar",
|
||||
name = Gui.from_name,
|
||||
tooltip = { "warp-list.timer-tooltip-zero", config.cooldown_duration },
|
||||
minimum_value = 0,
|
||||
maximum_value = config.cooldown_duration * config.update_smoothing,
|
||||
}
|
||||
:style{
|
||||
horizontally_stretchable = true,
|
||||
color = Colors.light_blue,
|
||||
}
|
||||
|
||||
local warp_list_container
|
||||
|
||||
-- Helper function to style and enable or disable a button element
|
||||
local function update_warp_elements(element, warp, warp_player_is_on, on_cooldown, bypass_warp_proximity)
|
||||
-- Check if button element is valid
|
||||
if not element or not element.valid then return end
|
||||
|
||||
local label_style = assert(element.parent).parent["name-" .. warp.warp_id][warp_label.name].style
|
||||
local warp_status_element = assert(element.parent).parent["name-" .. warp.warp_id][warp_status.name]
|
||||
|
||||
-- If player is not on a warp
|
||||
if not warp_player_is_on then
|
||||
-- If player is allowed to warp without being on a warp. If not then disable the warp location
|
||||
if bypass_warp_proximity then
|
||||
local position = warp.position
|
||||
element.tooltip = { "warp-list.goto-bypass", position.x, position.y }
|
||||
element.enabled = true
|
||||
warp_status_element.tooltip = { "warp-list.goto-bypass", position.x, position.y }
|
||||
warp_status_element.caption = warp_status_icons.bypass
|
||||
label_style.font = "default-semibold"
|
||||
else
|
||||
element.tooltip = { "warp-list.goto-disabled" }
|
||||
element.enabled = false
|
||||
warp_status_element.tooltip = { "warp-list.goto-disabled" }
|
||||
warp_status_element.caption = warp_status_icons.not_available
|
||||
label_style.font = "default"
|
||||
end
|
||||
-- If player is on the warp that is being updated
|
||||
elseif warp_player_is_on.warp_id == warp.warp_id then
|
||||
element.tooltip = { "warp-list.goto-same-warp" }
|
||||
element.enabled = false
|
||||
warp_status_element.tooltip = { "warp-list.goto-same-warp" }
|
||||
warp_status_element.caption = warp_status_icons.current
|
||||
label_style.font = "default"
|
||||
-- If player is on cooldown
|
||||
elseif on_cooldown then
|
||||
element.tooltip = { "warp-list.goto-cooldown" }
|
||||
element.enabled = false
|
||||
warp_status_element.tooltip = { "warp-list.goto-cooldown" }
|
||||
warp_status_element.caption = warp_status_icons.cooldown
|
||||
label_style.font = "default"
|
||||
else
|
||||
-- If the warp the player is standing on is the same as the warp that is being updated
|
||||
local warp_electric_network_id = warp.electric_pole and warp.electric_pole.electric_network_id or -1
|
||||
local player_warp_electric_network_id = warp_player_is_on.electric_pole and warp_player_is_on.electric_pole.electric_network_id or -2
|
||||
if warp_electric_network_id == player_warp_electric_network_id then
|
||||
local position = warp.position
|
||||
element.tooltip = { "warp-list.goto-tooltip", position.x, position.y }
|
||||
element.enabled = true
|
||||
warp_status_element.tooltip = { "warp-list.goto-tooltip", position.x, position.y }
|
||||
warp_status_element.caption = warp_status_icons.connected
|
||||
label_style.font = "default-semibold"
|
||||
-- If the warp is not on the same network but the player is allowed to warp without being on a warp
|
||||
elseif bypass_warp_proximity then
|
||||
local position = warp.position
|
||||
element.tooltip = { "warp-list.goto-bypass-different-network", position.x, position.y }
|
||||
element.enabled = true
|
||||
warp_status_element.tooltip = { "warp-list.goto-bypass-different-network", position.x, position.y }
|
||||
warp_status_element.caption = warp_status_icons.bypass
|
||||
label_style.font = "default-semibold"
|
||||
-- If the warp is on a different network than the one the player is standing on
|
||||
else
|
||||
element.tooltip = { "warp-list.goto-different-network" }
|
||||
element.enabled = false
|
||||
warp_status_element.tooltip = { "warp-list.goto-different-network" }
|
||||
warp_status_element.caption = warp_status_icons.different
|
||||
label_style.font = "default"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Update the warp buttons for a player
|
||||
function update_all_warp_elements(player, timer, warp_id)
|
||||
-- Get the warp table
|
||||
local container = Gui.get_left_element(warp_list_container, player)
|
||||
local scroll_table = container.frame.scroll.table
|
||||
|
||||
-- Check if the player is currently on cooldown
|
||||
timer = timer or PlayerCooldown:get(player)
|
||||
local on_cooldown = timer > 0
|
||||
-- Get the warp the player is on
|
||||
warp_id = warp_id or PlayerInRange:get(player)
|
||||
local warp_player_is_on = warp_id and Warps.get_warp(warp_id) or nil
|
||||
-- Check player permission
|
||||
local bypass_warp_proximity = check_player_permissions(player, "bypass_warp_proximity")
|
||||
|
||||
-- Change the enabled state of the warp buttons
|
||||
local warp_ids = Warps.get_force_warp_ids(player.force.name)
|
||||
for _, next_warp_id in pairs(warp_ids) do
|
||||
local element = scroll_table["icon-" .. next_warp_id][warp_icon_button.name]
|
||||
local next_warp = Warps.get_warp(next_warp_id)
|
||||
update_warp_elements(element, next_warp, warp_player_is_on, on_cooldown, bypass_warp_proximity)
|
||||
end
|
||||
end
|
||||
|
||||
--- Updates a warp for a player
|
||||
local function update_warp(player, warp_table, warp_id)
|
||||
local warp = Warps.get_warp(warp_id)
|
||||
|
||||
-- If the warp no longer exists then remove the warp elements from the warp table
|
||||
if not warp then
|
||||
remove_warp_elements(warp_table, warp_id)
|
||||
return
|
||||
end
|
||||
|
||||
-- Create the warp elements if they do not already exist
|
||||
if not warp_table["icon-" .. warp_id] then
|
||||
add_warp_elements(warp_table, warp)
|
||||
end
|
||||
local icon_flow = warp_table["icon-" .. warp_id]
|
||||
local name_flow = warp_table["name-" .. warp_id]
|
||||
local button_flow = warp_table["button-" .. warp_id]
|
||||
|
||||
-- Create local references to the elements for this warp
|
||||
local warp_icon_element = icon_flow[warp_icon_button.name]
|
||||
local warp_icon_edit_element = icon_flow[warp_icon_editing.name]
|
||||
|
||||
local label_element = name_flow[warp_label.name]
|
||||
local textfield_element = name_flow[warp_textfield.name]
|
||||
|
||||
local cancel_edit_element = button_flow[cancel_edit_button.name]
|
||||
local confirm_edit_element = button_flow[confirm_edit_button.name]
|
||||
|
||||
local edit_warp_element = button_flow[edit_warp_button.name]
|
||||
local remove_warp_element = button_flow[remove_warp_button.name]
|
||||
|
||||
-- Hide the edit button if the player is not allowed to edit the warp
|
||||
local player_allowed_edit = check_player_permissions(player, "allow_edit_warp", warp)
|
||||
local players_editing = table.get_keys(warp.currently_editing)
|
||||
edit_warp_element.visible = player_allowed_edit
|
||||
|
||||
-- Set the tooltip of the edit button
|
||||
if #players_editing > 0 then
|
||||
edit_warp_element.hovered_sprite = "utility/warning_icon"
|
||||
edit_warp_element.tooltip = { "warp-list.edit-tooltip", table.concat(players_editing, ", ") }
|
||||
else
|
||||
edit_warp_element.hovered_sprite = edit_warp_element.sprite
|
||||
edit_warp_element.tooltip = { "warp-list.edit-tooltip-none" }
|
||||
end
|
||||
|
||||
-- Set the visibility of the warp elements based on whether the user is editing or not
|
||||
local player_is_editing = warp.currently_editing[player.name]
|
||||
if player_is_editing then
|
||||
-- Set the icon elements visibility
|
||||
warp_icon_element.visible = false
|
||||
warp_icon_edit_element.visible = true
|
||||
-- Set the name elements visibility
|
||||
label_element.visible = false
|
||||
textfield_element.visible = true
|
||||
textfield_element.focus()
|
||||
warp_table.parent.scroll_to_element(textfield_element, "top-third")
|
||||
-- Set the edit buttons
|
||||
cancel_edit_element.visible = true
|
||||
confirm_edit_element.visible = true
|
||||
-- Set the warp buttons
|
||||
edit_warp_element.visible = false
|
||||
remove_warp_element.visible = false
|
||||
else
|
||||
-- Set the icon elements visibility
|
||||
warp_icon_element.visible = true
|
||||
-- The SpritePath type is not the same as the SignalID type
|
||||
local sprite = warp.icon.type .. "/" .. warp.icon.name
|
||||
if warp.icon.type == "virtual" then
|
||||
sprite = "virtual-signal/" .. warp.icon.name
|
||||
end
|
||||
warp_icon_element.sprite = sprite
|
||||
-- Set icon edit to the warps icon
|
||||
warp_icon_edit_element.elem_value = warp.icon
|
||||
warp_icon_edit_element.visible = false
|
||||
-- Set the name elements visibility
|
||||
label_element.visible = true
|
||||
label_element.caption = warp.name
|
||||
textfield_element.visible = false
|
||||
textfield_element.text = warp.name
|
||||
-- Set the edit buttons
|
||||
cancel_edit_element.visible = false
|
||||
confirm_edit_element.visible = false
|
||||
-- Set the warp buttons
|
||||
edit_warp_element.visible = true and player_allowed_edit
|
||||
remove_warp_element.visible = true and player_allowed_edit
|
||||
end
|
||||
|
||||
local timer = PlayerCooldown:get(player)
|
||||
local current_warp_id = PlayerInRange:get(player)
|
||||
local to_warp = current_warp_id and Warps.get_warp(current_warp_id) or nil
|
||||
local bypass_warp_proximity = check_player_permissions(player, "bypass_warp_proximity")
|
||||
update_warp_elements(warp_icon_element, warp, to_warp, timer > 0, bypass_warp_proximity)
|
||||
end
|
||||
|
||||
-- Update all the warps for a player
|
||||
local function update_all_warps(player, warp_table)
|
||||
local warp_ids = Warps.get_force_warp_ids(player.force.name)
|
||||
warp_table.clear()
|
||||
for _, warp_id in ipairs(warp_ids) do
|
||||
update_warp(player, warp_table, warp_id)
|
||||
end
|
||||
end
|
||||
|
||||
-- Update all warps for all players on a force
|
||||
local function update_all_warp_force(force)
|
||||
local warp_ids = Warps.get_force_warp_ids(force.name)
|
||||
for _, player in pairs(force.connected_players) do
|
||||
local container = Gui.get_left_element(warp_list_container, player)
|
||||
local warp_table = container.frame.scroll.table
|
||||
|
||||
warp_table.clear() -- Needed to re-sort the warps
|
||||
for _, warp_id in ipairs(warp_ids) do
|
||||
update_warp(player, warp_table, warp_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Main warp list container for the left flow
|
||||
-- @element warp_list_container
|
||||
warp_list_container = Gui.define("warp_list_container")
|
||||
:draw(function(def, parent)
|
||||
local player = Gui.get_player(parent)
|
||||
-- Check if user has permission to add warps
|
||||
local allow_add_warp = check_player_permissions(player, "allow_add_warp")
|
||||
|
||||
-- Draw the internal container
|
||||
local container = Gui.elements.container(parent, allow_add_warp and 268 or 220)
|
||||
|
||||
-- Draw the header
|
||||
local header = Gui.elements.header(container, {
|
||||
name = "header",
|
||||
caption = { "warp-list.main-caption" },
|
||||
tooltip = {
|
||||
"warp-list.sub-tooltip",
|
||||
config.cooldown_duration,
|
||||
config.standard_proximity_radius,
|
||||
{ "warp-list.sub-tooltip-current", warp_status_icons.current },
|
||||
{ "warp-list.sub-tooltip-connected", warp_status_icons.connected },
|
||||
{ "warp-list.sub-tooltip-different", warp_status_icons.different },
|
||||
{ "warp-list.sub-tooltip-cooldown", warp_status_icons.cooldown },
|
||||
{ "warp-list.sub-tooltip-not_available", warp_status_icons.not_available },
|
||||
{ "warp-list.sub-tooltip-bypass", warp_status_icons.bypass },
|
||||
},
|
||||
})
|
||||
|
||||
-- Draw the new warp button
|
||||
local add_new_warp_element = add_new_warp(header)
|
||||
add_new_warp_element.visible = allow_add_warp
|
||||
|
||||
-- Draw the scroll table for the warps
|
||||
local scroll_table = Gui.elements.scroll_table(container, 250, 3, "scroll")
|
||||
-- Set the scroll panel to always show the scrollbar (not doing this will result in a changing gui size)
|
||||
assert(scroll_table.parent).vertical_scroll_policy = "always"
|
||||
|
||||
-- Change the style of the scroll table
|
||||
local scroll_table_style = scroll_table.style
|
||||
scroll_table_style.top_cell_padding = 3
|
||||
scroll_table_style.bottom_cell_padding = 3
|
||||
|
||||
-- Draw the warp cooldown progress bar
|
||||
local warp_timer_element = warp_timer(container)
|
||||
|
||||
-- Change the progress of the warp timer
|
||||
local timer = PlayerCooldown:get(player)
|
||||
if timer > 0 then
|
||||
warp_timer_element.tooltip = { "warp-list.timer-tooltip", math.floor(timer / config.update_smoothing) }
|
||||
warp_timer_element.value = 1 - (timer / config.update_smoothing / config.cooldown_duration)
|
||||
else
|
||||
warp_timer_element.tooltip = { "warp-list.timer-tooltip-zero", config.cooldown_duration }
|
||||
warp_timer_element.value = 1
|
||||
end
|
||||
|
||||
-- Add any existing warps
|
||||
update_all_warps(player, scroll_table)
|
||||
|
||||
-- Return the external container
|
||||
return container.parent
|
||||
end)
|
||||
|
||||
--- Add the element to the left flow with a toolbar button
|
||||
Gui.add_left_element(warp_list_container, false)
|
||||
Gui.toolbar.create_button{
|
||||
name = "warp_list_toggle",
|
||||
left_element = warp_list_container,
|
||||
sprite = config.default_icon.type .. "/" .. config.default_icon.name,
|
||||
tooltip = { "warp-list.main-tooltip" },
|
||||
visible = function(player, element)
|
||||
return Roles.player_has_permission(player, "exp_scenario.gui.warp_list")
|
||||
end
|
||||
}:on_click(function(def, player, element)
|
||||
-- Set gui keep open state for player that clicked the button: true if visible, false if invisible
|
||||
keep_gui_open[player.name] = Gui.toolbar.get_button_toggled_state(def, player)
|
||||
end)
|
||||
|
||||
--- When the name of a warp is updated this is triggered
|
||||
Warps.on_update(function(_, warp, old_warp)
|
||||
-- Get the force to update, warp is nil when removed
|
||||
if warp then
|
||||
update_all_warp_force(game.forces[warp.force_name])
|
||||
else
|
||||
update_all_warp_force(game.forces[old_warp.force_name])
|
||||
end
|
||||
end)
|
||||
|
||||
--- When the player leaves or enters range of a warp this is triggered
|
||||
PlayerInRange:on_update(function(player_name, warp_id)
|
||||
local player = game.players[player_name]
|
||||
|
||||
-- Change if the frame is visible based on if the player is in range
|
||||
if not keep_gui_open[player.name] then
|
||||
Gui.toolbar.set_left_element_visible_state(warp_list_container, player, warp_id ~= nil)
|
||||
end
|
||||
|
||||
update_all_warp_elements(player, nil, warp_id)
|
||||
end)
|
||||
|
||||
--- Update the warp cooldown progress bars to match the current cooldown
|
||||
PlayerCooldown:on_update(function(player_name, player_cooldown)
|
||||
-- Get the progress bar element
|
||||
local player = game.players[player_name]
|
||||
local container = Gui.get_left_element(warp_list_container, player)
|
||||
local warp_timer_element = container.frame[warp_timer.name]
|
||||
|
||||
-- Set the progress
|
||||
if player_cooldown and player_cooldown > 0 then
|
||||
warp_timer_element.tooltip = { "warp-list.timer-tooltip", math.floor(player_cooldown / config.update_smoothing) }
|
||||
warp_timer_element.value = 1 - (player_cooldown / config.update_smoothing / config.cooldown_duration)
|
||||
else
|
||||
warp_timer_element.tooltip = { "warp-list.timer-tooltip-zero", config.cooldown_duration }
|
||||
warp_timer_element.value = 1
|
||||
end
|
||||
|
||||
-- Trigger update of buttons if cooldown is now 0
|
||||
if player_cooldown == 0 then
|
||||
update_all_warp_elements(player, player_cooldown, nil)
|
||||
end
|
||||
end)
|
||||
|
||||
--- Handles updating the timer and checking distance from a warp
|
||||
local r2 = config.standard_proximity_radius ^ 2
|
||||
local rs2 = config.spawn_proximity_radius ^ 2
|
||||
local mr2 = config.minimum_distance ^ 2
|
||||
Event.on_nth_tick(math.floor(60 / config.update_smoothing), function()
|
||||
PlayerCooldown:update_all(function(_, player_cooldown)
|
||||
if player_cooldown > 0 then return player_cooldown - 1 end
|
||||
end)
|
||||
|
||||
local force_warps = {}
|
||||
local warps = {}
|
||||
for _, player in pairs(game.connected_players) do
|
||||
local was_in_range = PlayerInRange:get(player)
|
||||
|
||||
-- Get the ids of all the warps on the players force
|
||||
local force_name = player.force.name
|
||||
local warp_ids = force_warps[force_name]
|
||||
if not warp_ids then
|
||||
warp_ids = Warps.get_force_warp_ids(force_name)
|
||||
force_warps[force_name] = warp_ids
|
||||
end
|
||||
|
||||
-- Check if the force has any warps
|
||||
local closest_warp = nil --- @type { warp_id: string, name: string }?
|
||||
local closest_distance = nil
|
||||
if #warp_ids > 0 then
|
||||
local surface = player.surface
|
||||
local pos = player.physical_position
|
||||
local px, py = pos.x, pos.y
|
||||
|
||||
-- Loop over each warp
|
||||
for _, warp_id in ipairs(warp_ids) do
|
||||
-- Check if warp id is cached
|
||||
local warp = warps[warp_id]
|
||||
if not warp then
|
||||
warp = Warps.get_warp(warp_id)
|
||||
warps[warp_id] = warp
|
||||
end
|
||||
|
||||
-- Check if the player is within range
|
||||
local warp_pos = warp.position
|
||||
if warp.surface == surface then
|
||||
local dx, dy = px - warp_pos.x, py - warp_pos.y
|
||||
local dist = (dx * dx) + (dy * dy)
|
||||
if closest_distance == nil or dist < closest_distance then
|
||||
closest_warp = warp
|
||||
closest_distance = dist
|
||||
if dist < r2 then break end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Check the dist to the closest warp
|
||||
local in_range = closest_warp ~= nil and (closest_warp.warp_id == warp_ids.spawn and closest_distance < rs2 or closest_distance < r2)
|
||||
if was_in_range and not in_range then
|
||||
PlayerInRange:set(player, nil)
|
||||
elseif not was_in_range and in_range then
|
||||
--- @cast closest_warp -nil
|
||||
PlayerInRange:set(player, closest_warp.warp_id)
|
||||
end
|
||||
|
||||
-- Change the enabled state of the add warp button
|
||||
local container = Gui.get_left_element(warp_list_container, player)
|
||||
local add_warp_element = container.frame.header[add_new_warp.name]
|
||||
local old_closest_warp_name = add_warp_element.tooltip[2] or closest_warp and closest_warp.name
|
||||
local was_able_to_make_warp = add_warp_element.enabled
|
||||
local can_make_warp = closest_distance == nil or closest_distance > mr2
|
||||
if can_make_warp and not was_able_to_make_warp then
|
||||
add_warp_element.enabled = true
|
||||
add_warp_element.tooltip = { "warp-list.add-tooltip" }
|
||||
elseif not can_make_warp and was_able_to_make_warp or closest_warp and (old_closest_warp_name ~= closest_warp.name) then
|
||||
--- @cast closest_warp -nil
|
||||
add_warp_element.enabled = false
|
||||
add_warp_element.tooltip = { "warp-list.too-close", closest_warp.name }
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
--- When a player is created make sure that there is a spawn warp created
|
||||
Event.add(defines.events.on_player_created, function(event)
|
||||
-- If the force has no spawn then make a spawn warp
|
||||
local player = game.players[event.player_index]
|
||||
local force = player.force --[[@as LuaForce]]
|
||||
local spawn_id = Warps.get_spawn_warp_id(force.name)
|
||||
if not spawn_id then
|
||||
local spawn_position = force.get_spawn_position(player.surface)
|
||||
spawn_id = Warps.add_warp(force.name, player.surface, spawn_position, nil, "Spawn")
|
||||
Warps.set_spawn_warp(spawn_id, force)
|
||||
Warps.make_warp_tag(spawn_id)
|
||||
|
||||
local entities = player.surface.find_entities_filtered{ type = "electric-pole", position = spawn_position, radius = 20, limit = 1 }
|
||||
if entities and entities[1] then
|
||||
local warp = Warps.get_warp(spawn_id)
|
||||
warp.electric_pole = entities[1]
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
--- Update the warps when the player joins
|
||||
Event.add(defines.events.on_player_joined_game, function(event)
|
||||
local player = game.players[event.player_index]
|
||||
local container = Gui.get_left_element(warp_list_container, player)
|
||||
local scroll_table = container.frame.scroll.table
|
||||
update_all_warps(player, scroll_table)
|
||||
end)
|
||||
|
||||
--- Makes sure the right buttons are present when roles change
|
||||
local function role_update_event(event)
|
||||
local player = game.players[event.player_index]
|
||||
local frame = Gui.get_left_element(warp_list_container, player).frame
|
||||
|
||||
-- Check if user has permission to add warps
|
||||
local allow_add_warp = check_player_permissions(player, "allow_add_warp")
|
||||
-- Update container size depending on whether the player is allowed to add warps
|
||||
assert(frame.parent).style.width = allow_add_warp and 268 or 220
|
||||
|
||||
-- Update the warps, in case the user can now edit them
|
||||
local scroll_table = frame.scroll.table
|
||||
update_all_warps(player, scroll_table)
|
||||
|
||||
-- Update the new warp button in case the user can now add them
|
||||
local add_new_warp_element = frame.header[add_new_warp.name]
|
||||
add_new_warp_element.visible = allow_add_warp
|
||||
end
|
||||
|
||||
Event.add(Roles.events.on_player_roles_changed, role_update_event)
|
||||
|
||||
--- When a chart tag is removed or edited make sure it is not one that belongs to a warp
|
||||
local function maintain_tag(event)
|
||||
if not event.player_index then return end
|
||||
local tag = event.tag
|
||||
local force_name = event.force.name
|
||||
local warp_ids = Warps.get_force_warp_ids(force_name)
|
||||
for _, warp_id in pairs(warp_ids) do
|
||||
local warp = Warps.get_warp(warp_id)
|
||||
local warp_tag = warp.tag
|
||||
if not warp_tag or not warp_tag.valid or warp_tag == tag then
|
||||
if event.name == defines.events.on_chart_tag_removed then
|
||||
warp.tag = nil
|
||||
end
|
||||
Warps.make_warp_tag(warp_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Event.add(defines.events.on_chart_tag_modified, maintain_tag)
|
||||
Event.add(defines.events.on_chart_tag_removed, maintain_tag)
|
||||
@@ -5,6 +5,7 @@
|
||||
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
|
||||
"license": "MIT",
|
||||
"repository": "explosivegaming/ExpCluster",
|
||||
"type": "module",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --env production"
|
||||
|
||||
File renamed without changes.
+10
-66
@@ -1,13 +1,20 @@
|
||||
import { BaseControllerPlugin, InstanceRecord } from "@clusterio/controller";
|
||||
import { BaseControllerPlugin, InstanceRecord, type Controller } from "@clusterio/controller";
|
||||
import * as lib from "@clusterio/lib";
|
||||
import * as messages from "./messages";
|
||||
import { SeedRole, seedRoles, flattenSeedPermissions } from "./seed";
|
||||
import * as messages from "./messages.js";
|
||||
import * as path from "node:path";
|
||||
|
||||
const loaded = new WeakMap<Controller, ControllerPlugin>();
|
||||
|
||||
export class ControllerPlugin extends BaseControllerPlugin {
|
||||
roleMeta!: lib.SubscribableDatastore<messages.RoleMetaRecord>;
|
||||
|
||||
/** The plugin loaded on a controller, for the exp_scenario seed. */
|
||||
static get(controller: Controller) {
|
||||
return loaded.get(controller);
|
||||
}
|
||||
|
||||
async init() {
|
||||
loaded.set(this.controller, this);
|
||||
const databaseDirectory = this.controller.config.get("controller.database_directory");
|
||||
|
||||
this.roleMeta = new lib.SubscribableDatastore(
|
||||
@@ -35,7 +42,6 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
|
||||
this.controller.handle(messages.RoleListRequest, this.handleRoleListRequest.bind(this));
|
||||
this.controller.handle(messages.RoleMetaUpdateRequest, this.handleRoleMetaUpdateRequest.bind(this));
|
||||
this.controller.handle(messages.SeedRolesRequest, this.handleSeedRolesRequest.bind(this));
|
||||
|
||||
this.controller.handle(messages.AssignmentListRequest, this.handleAssignmentListRequest.bind(this));
|
||||
this.controller.handle(messages.AssignmentUpdateRequest, this.handleAssignmentUpdateRequest.bind(this));
|
||||
@@ -45,68 +51,6 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
await this.roleMeta.save();
|
||||
}
|
||||
|
||||
/*
|
||||
Seeding
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create the roles the scenario shipped with. Roles which already exist
|
||||
* by name are reused and only gain the seed permissions.
|
||||
*/
|
||||
async handleSeedRolesRequest() {
|
||||
for (const [index, seedRole] of seedRoles.entries()) {
|
||||
const role = this.seedRole(seedRole);
|
||||
if (!role) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.roleMeta.set(new messages.RoleMetaRecord(
|
||||
role.id,
|
||||
index + 1,
|
||||
seedRole.priority ?? 0,
|
||||
seedRole.shortHand,
|
||||
"",
|
||||
seedRole.color,
|
||||
seedRole.autoAssignHours === undefined ? null : seedRole.autoAssignHours * 3600000,
|
||||
seedRole.blockAutoAssign ?? false,
|
||||
));
|
||||
}
|
||||
|
||||
this.logger.info(`Seeded ${seedRoles.length} roles`);
|
||||
}
|
||||
|
||||
/** Find or create the clusterio role for a seed role, returns undefined if it has no role to use. */
|
||||
seedRole(seedRole: SeedRole) {
|
||||
const roles = this.controller.roles;
|
||||
if (seedRole.isAdmin) {
|
||||
return roles.get(lib.Role.DefaultAdminRoleId);
|
||||
}
|
||||
if (seedRole.isDefault) {
|
||||
const defaultRoleId = this.controller.config.get("controller.default_role_id");
|
||||
return defaultRoleId !== null ? roles.get(defaultRoleId) : undefined;
|
||||
}
|
||||
|
||||
const permissions = flattenSeedPermissions(seedRole);
|
||||
for (const permission of permissions) {
|
||||
if (!lib.permissions.has(permission)) {
|
||||
this.logger.warn(`Seed role ${seedRole.name} grants unknown permission ${permission}`);
|
||||
}
|
||||
}
|
||||
|
||||
let role = [...roles.valuesMutable()].find(other => other.name === seedRole.name);
|
||||
if (role) {
|
||||
for (const permission of permissions) {
|
||||
role.permissions.add(permission);
|
||||
}
|
||||
} else {
|
||||
const id = Math.max(5, ...[...roles.keys()].map(other => other + 1));
|
||||
role = new lib.Role(id, seedRole.name, "", permissions);
|
||||
this.logger.info(`Created role ${seedRole.name}`);
|
||||
}
|
||||
roles.set(role);
|
||||
return role;
|
||||
}
|
||||
|
||||
/*
|
||||
Role properties
|
||||
*/
|
||||
|
||||
+6
-4
@@ -1,5 +1,8 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
import * as messages from "./messages";
|
||||
import * as messages from "./messages.js";
|
||||
|
||||
export * from "./messages.js";
|
||||
export type { ControllerPlugin } from "./controller.js";
|
||||
|
||||
declare module "@clusterio/lib" {
|
||||
export interface InstanceConfigFields {
|
||||
@@ -27,13 +30,12 @@ export const plugin: lib.PluginDeclaration = {
|
||||
|
||||
messages.RoleListRequest,
|
||||
messages.RoleMetaUpdateRequest,
|
||||
messages.SeedRolesRequest,
|
||||
|
||||
messages.AssignmentListRequest,
|
||||
messages.AssignmentUpdateRequest,
|
||||
],
|
||||
|
||||
instanceEntrypoint: "./dist/node/instance",
|
||||
instanceEntrypoint: "./dist/node/instance.js",
|
||||
instanceConfigFields: {
|
||||
"exp_roles.sync_mode": {
|
||||
description: "Synchronize in game roles with the controller",
|
||||
@@ -43,7 +45,7 @@ export const plugin: lib.PluginDeclaration = {
|
||||
},
|
||||
},
|
||||
|
||||
controllerEntrypoint: "./dist/node/controller",
|
||||
controllerEntrypoint: "./dist/node/controller.js",
|
||||
controllerConfigFields: {
|
||||
},
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BaseInstancePlugin } from "@clusterio/host";
|
||||
import * as lib from "@clusterio/lib";
|
||||
import * as messages from "./messages";
|
||||
import * as messages from "./messages.js";
|
||||
|
||||
/** Sent by the lua side when roles are changed in game. */
|
||||
export type IpcAssignmentUpdate = {
|
||||
|
||||
@@ -365,18 +365,6 @@ export class RoleMetaUpdateRequest {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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
|
||||
*/
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
[exp-roles]
|
||||
game-message-assign=__1__ has been assigned: __2__ by __3__
|
||||
game-message-unassign=__1__ has been unassigned: __2__ by __3__
|
||||
game-message-assign=__1__ has been assigned to __2__ by __3__
|
||||
game-message-unassign=__1__ has been unassigned from __2__ by __3__
|
||||
@@ -1,3 +1,3 @@
|
||||
[exp-roles]
|
||||
game-message-assign=__1__ 已被 __3__ 指派到身分組 __2__
|
||||
game-message-unassign=__1__ 已被 __3__ 取消指派到身分組 __2__
|
||||
game-message-assign=__1__ 已被 __3__ 指派到身分組 __2__
|
||||
game-message-unassign=__1__ 已被 __3__ 取消指派到身分組 __2__
|
||||
@@ -1,3 +1,3 @@
|
||||
[exp-roles]
|
||||
game-message-assign=__1__ 已被 __3__ 指派到身分組 __2__
|
||||
game-message-unassign=__1__ 已被 __3__ 取消指派到身分組 __2__
|
||||
game-message-assign=__1__ 已被 __3__ 指派到身分組 __2__
|
||||
game-message-unassign=__1__ 已被 __3__ 取消指派到身分組 __2__
|
||||
@@ -5,10 +5,11 @@
|
||||
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
|
||||
"license": "MIT",
|
||||
"repository": "explosivegaming/ExpCluster",
|
||||
"type": "module",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --env production",
|
||||
"test": "tap --disable-coverage --allow-empty-coverage test/*.test.js",
|
||||
"test": "tap --type-strip-only --disable-coverage --allow-empty-coverage test/*.test.js",
|
||||
"coverage": "tap --coverage-report=text test/*.test.js"
|
||||
},
|
||||
"engines": {
|
||||
@@ -26,7 +27,6 @@
|
||||
"@clusterio/host": "workspace:^",
|
||||
"@clusterio/lib": "workspace:^",
|
||||
"@clusterio/web_ui": "workspace:^",
|
||||
"@expcluster/scenario": "workspace:^",
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"antd": "catalog:",
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
"use strict";
|
||||
const t = require("tap");
|
||||
const lib = require("@clusterio/lib");
|
||||
const { Controller } = require("@clusterio/controller");
|
||||
const { ControllerPlugin } = require("../dist/node/controller");
|
||||
const messages = require("../dist/node/messages");
|
||||
const { seedRoles } = require("../dist/node/seed");
|
||||
|
||||
// Importing this defines the exp_scenario permissions the seed grants
|
||||
require("@expcluster/scenario/dist/node/permissions");
|
||||
import t from "tap";
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { Controller } from "@clusterio/controller";
|
||||
import { ControllerPlugin } from "../dist/node/controller.js";
|
||||
import * as messages from "../dist/node/messages.js";
|
||||
|
||||
// The controller validates message classes against the link registry
|
||||
lib.Link.register(messages.RoleUpdatedEvent);
|
||||
lib.Link.register(messages.AssignmentUpdatedEvent);
|
||||
lib.Link.register(messages.RoleListRequest);
|
||||
lib.Link.register(messages.RoleMetaUpdateRequest);
|
||||
lib.Link.register(messages.SeedRolesRequest);
|
||||
lib.Link.register(messages.AssignmentListRequest);
|
||||
lib.Link.register(messages.AssignmentUpdateRequest);
|
||||
|
||||
@@ -229,22 +223,5 @@ t.test("class ControllerPlugin", t2 => {
|
||||
t3.strictSame(none, null, "nothing is replayed when up to date");
|
||||
});
|
||||
|
||||
t2.test(".handleSeedRolesRequest() creates the roles and reuses them by name", async t3 => {
|
||||
const { plugin, controller } = await startPlugin(t3, {
|
||||
roles: [role(0, "Cluster Admin", ["core.admin"]), role(1, "Player")],
|
||||
});
|
||||
|
||||
await plugin.handleSeedRolesRequest();
|
||||
t3.strictSame(controller.roles.size, seedRoles.length, "every seed role exists");
|
||||
|
||||
const moderator = [...controller.roles.values()].find(other => other.name === "Moderator");
|
||||
t3.ok(moderator.permissions.has("exp_scenario.command.jail"), "parent permissions are flattened in");
|
||||
t3.ok(plugin.roleMeta.get(moderator.id), "the role properties are created");
|
||||
t3.strictSame(plugin.roleMeta.get(moderator.id).shortHand, "Mod", "the properties match the seed");
|
||||
|
||||
await plugin.handleSeedRolesRequest();
|
||||
t3.strictSame(controller.roles.size, seedRoles.length, "seeding again reuses the roles");
|
||||
});
|
||||
|
||||
t2.end();
|
||||
});
|
||||
@@ -1,10 +1,9 @@
|
||||
"use strict";
|
||||
const t = require("tap");
|
||||
const lib = require("@clusterio/lib");
|
||||
const { Instance } = require("@clusterio/host");
|
||||
const { InstancePlugin } = require("../dist/node/instance");
|
||||
const { plugin: pluginDeclaration } = require("../dist/node/index");
|
||||
const messages = require("../dist/node/messages");
|
||||
import t from "tap";
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { Instance } from "@clusterio/host";
|
||||
import { InstancePlugin } from "../dist/node/instance.js";
|
||||
import { plugin as pluginDeclaration } from "../dist/node/index.js";
|
||||
import * as messages from "../dist/node/messages.js";
|
||||
|
||||
// The instance validates message classes against the link registry, and the
|
||||
// plugin's config fields must be defined before an InstanceConfig can set them
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use strict";
|
||||
const t = require("tap");
|
||||
const messages = require("../dist/node/messages");
|
||||
const { testMatrix, testRoundTripJsonSerialisable } = require("../../test/common");
|
||||
import t from "tap";
|
||||
import * as messages from "../dist/node/messages.js";
|
||||
import { testMatrix, testRoundTripJsonSerialisable } from "../../test/common.js";
|
||||
|
||||
const fullMeta = new messages.RoleMetaRecord(
|
||||
7, 3, 1, "Mod", "[Mod]", new messages.RoleColor(1, 2, 3), 3600000, true, 12345, false,
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
"use strict";
|
||||
const path = require("node:path");
|
||||
const t = require("tap");
|
||||
const { reportLuaTests } = require("../../test/lua/runner");
|
||||
import path from "node:path";
|
||||
import t from "tap";
|
||||
import { reportLuaTests } from "../../test/lua/runner.js";
|
||||
|
||||
const envFile = path.join(__dirname, "module", "env.lua");
|
||||
const envFile = path.join(import.meta.dirname, "module", "env.lua");
|
||||
|
||||
// Each file runs in its own lua state, and each test in a fresh environment
|
||||
t.test("control.lua", t2 => {
|
||||
for (const file of ["lookup.lua", "players.lua", "assignment.lua", "sync.lua", "holders.lua"]) {
|
||||
t2.test(file, t3 => reportLuaTests(t3, envFile, path.join(__dirname, "module", file)));
|
||||
t2.test(file, t3 => reportLuaTests(t3, envFile, path.join(import.meta.dirname, "module", file)));
|
||||
}
|
||||
t2.end();
|
||||
});
|
||||
@@ -4,8 +4,8 @@ import { Button, Col, ColorPicker, Form, Input, InputNumber, Row, Switch, Toolti
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { ControlContext, SectionHeader, useAccount, notifyErrorHandler } from "@clusterio/web_ui";
|
||||
|
||||
import { RoleColor, RoleMetaRecord, RoleMetaUpdateRequest } from "../../messages";
|
||||
import type { WebPlugin } from "..";
|
||||
import { RoleColor, RoleMetaRecord, RoleMetaUpdateRequest } from "../../messages.js";
|
||||
import { useRoles } from "..";
|
||||
|
||||
const MS_PER_HOUR = 3600000;
|
||||
|
||||
@@ -25,13 +25,13 @@ type RoleFormValues = {
|
||||
* The name, description and permissions of a role are owned by clusterio, this
|
||||
* only covers the properties which only mean something in game.
|
||||
*/
|
||||
export default function RoleProperties(props: { plugin: WebPlugin, role?: lib.Role }) {
|
||||
export default function RoleProperties(props: { role?: lib.Role }) {
|
||||
const control = useContext(ControlContext);
|
||||
const account = useAccount();
|
||||
const [roles] = useRoles();
|
||||
const [form] = Form.useForm<RoleFormValues>();
|
||||
const [applying, setApplying] = useState(false);
|
||||
|
||||
const [roles] = props.plugin.useRoles();
|
||||
const record = props.role ? roles.get(props.role.id) : undefined;
|
||||
const meta = record?.meta;
|
||||
const canUpdate = account.hasPermission("core.role.update");
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { Button, Popconfirm } from "antd";
|
||||
|
||||
import { ControlContext, SectionHeader, useAccount, notifyErrorHandler } from "@clusterio/web_ui";
|
||||
|
||||
import { SeedRolesRequest } from "../../messages";
|
||||
|
||||
/** Button on the roles page which creates the roles the scenario shipped with. */
|
||||
export default function SeedRoles() {
|
||||
const control = useContext(ControlContext);
|
||||
const account = useAccount();
|
||||
const [seeding, setSeeding] = useState(false);
|
||||
|
||||
if (!account.hasPermission("core.role.create")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <SectionHeader
|
||||
title="ExpGaming Roles"
|
||||
extra={<Popconfirm
|
||||
title="Create the ExpGaming roles?"
|
||||
description="Roles which already exist by name are kept and only gain permissions."
|
||||
onConfirm={() => {
|
||||
setSeeding(true);
|
||||
control.send(new SeedRolesRequest())
|
||||
.catch(notifyErrorHandler("Error seeding roles"))
|
||||
.finally(() => setSeeding(false));
|
||||
}}
|
||||
>
|
||||
<Button loading={seeding}>Seed roles</Button>
|
||||
</Popconfirm>}
|
||||
/>;
|
||||
}
|
||||
+13
-17
@@ -1,26 +1,22 @@
|
||||
import React, { useCallback, useSyncExternalStore } from "react";
|
||||
import { BaseWebPlugin } from "@clusterio/web_ui";
|
||||
import { WebPluginContext } from "@clusterio/web_ui";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
import * as messages from "../messages";
|
||||
import * as messages from "../messages.js";
|
||||
|
||||
import RoleProperties from "./components/RoleProperties";
|
||||
import SeedRoles from "./components/SeedRoles";
|
||||
|
||||
export class WebPlugin extends BaseWebPlugin {
|
||||
roles = new lib.MapSubscriber(messages.RoleUpdatedEvent, this.control);
|
||||
let roles: lib.MapSubscriber<messages.RoleUpdatedEvent> | undefined;
|
||||
|
||||
async init() {
|
||||
// The core components pass a role and the plugin, which componentExtra
|
||||
// does not carry in its type
|
||||
this.componentExtra = {
|
||||
RoleViewPage: RoleProperties as React.ComponentType,
|
||||
RolesPage: SeedRoles,
|
||||
};
|
||||
}
|
||||
export function useRoles() {
|
||||
const subscribe = useCallback((cb: () => void) => roles!.subscribe(cb), []);
|
||||
return useSyncExternalStore(subscribe, () => roles!.getSnapshot());
|
||||
}
|
||||
|
||||
useRoles() {
|
||||
const subscribe = useCallback((cb: () => void) => this.roles.subscribe(cb), []);
|
||||
return useSyncExternalStore(subscribe, () => this.roles.getSnapshot());
|
||||
}
|
||||
export default function registerPlugin(context: WebPluginContext) {
|
||||
context.control.hooks.extensionComponents.attach("exp_roles", () => ({
|
||||
RoleViewPage: RoleProperties,
|
||||
}));
|
||||
|
||||
roles = new lib.MapSubscriber(messages.RoleUpdatedEvent, context.control);
|
||||
}
|
||||
File renamed without changes.
@@ -1,6 +1,157 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { BaseControllerPlugin } from "@clusterio/controller";
|
||||
import { RoleMetaRecord } from "@expcluster/roles";
|
||||
import { GroupRecord, GroupPermissions, RoleMappingRecord } from "@expcluster/permission-groups";
|
||||
import { ControllerPlugin as RolesPlugin } from "@expcluster/roles/dist/node/controller.js";
|
||||
import { ControllerPlugin as GroupsPlugin } from "@expcluster/permission-groups/dist/node/controller.js";
|
||||
import * as messages from "./messages.js";
|
||||
import { SeedRole, SeedGroup, seedRoles, seedGroups, flattenSeedPermissions } from "./seed.js";
|
||||
|
||||
export class ControllerPlugin extends BaseControllerPlugin {
|
||||
async init() {
|
||||
this.controller.handle(messages.SeedRequest, this.handleSeedRequest.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the roles and permission groups the scenario shipped with.
|
||||
*
|
||||
* Roles which already exist by name are reused and only gain the seed
|
||||
* permissions, groups which already exist by name are reset to the seed.
|
||||
*/
|
||||
async handleSeedRequest() {
|
||||
const rolesPlugin = RolesPlugin.get(this.controller);
|
||||
const groupsPlugin = GroupsPlugin.get(this.controller);
|
||||
if (!rolesPlugin || !groupsPlugin) {
|
||||
throw new lib.RequestError("Seeding requires the exp_roles and exp_groups plugins");
|
||||
}
|
||||
|
||||
const roleIds = new Map<string, number>();
|
||||
for (const [index, seedRole] of seedRoles.entries()) {
|
||||
const role = this.seedRole(seedRole);
|
||||
if (!role) {
|
||||
continue;
|
||||
}
|
||||
|
||||
roleIds.set(seedRole.name, role.id);
|
||||
rolesPlugin.roleMeta.set(new RoleMetaRecord(
|
||||
role.id,
|
||||
index + 1,
|
||||
seedRole.priority ?? 0,
|
||||
seedRole.shortHand,
|
||||
"",
|
||||
seedRole.color,
|
||||
seedRole.autoAssignHours === undefined ? null : seedRole.autoAssignHours * 3600000,
|
||||
seedRole.blockAutoAssign ?? false,
|
||||
));
|
||||
}
|
||||
|
||||
const groupIds = new Map<string, number>();
|
||||
for (const seedGroup of seedGroups) {
|
||||
groupIds.set(seedGroup.name, this.seedGroup(groupsPlugin, seedGroup).id);
|
||||
}
|
||||
|
||||
this.seedRoleMappings(groupsPlugin, roleIds, groupIds);
|
||||
this.logger.info(`Seeded ${roleIds.size} roles and ${groupIds.size} permission groups`);
|
||||
}
|
||||
|
||||
/** Find or create the clusterio role for a seed role, returns undefined if it has no role to use. */
|
||||
seedRole(seedRole: SeedRole) {
|
||||
const roles = this.controller.roles;
|
||||
if (seedRole.isAdmin) {
|
||||
return roles.get(lib.Role.DefaultAdminRoleId);
|
||||
}
|
||||
if (seedRole.isDefault) {
|
||||
const defaultRoleId = this.controller.config.get("controller.default_role_id");
|
||||
return defaultRoleId !== null ? roles.get(defaultRoleId) : undefined;
|
||||
}
|
||||
|
||||
const permissions = flattenSeedPermissions(seedRole);
|
||||
for (const permission of permissions) {
|
||||
if (!lib.permissions.has(permission)) {
|
||||
this.logger.warn(`Seed role ${seedRole.name} grants unknown permission ${permission}`);
|
||||
}
|
||||
}
|
||||
|
||||
let role = [...roles.valuesMutable()].find(other => other.name === seedRole.name);
|
||||
if (role) {
|
||||
for (const permission of permissions) {
|
||||
role.permissions.add(permission);
|
||||
}
|
||||
} else {
|
||||
const id = Math.max(5, ...[...roles.keys()].map(other => other + 1));
|
||||
role = new lib.Role(id, seedRole.name, "", permissions);
|
||||
this.logger.info(`Created role ${seedRole.name}`);
|
||||
}
|
||||
roles.set(role);
|
||||
return role;
|
||||
}
|
||||
|
||||
/** Find or create the permission group for a seed group. */
|
||||
seedGroup(groupsPlugin: GroupsPlugin, seedGroup: SeedGroup) {
|
||||
const permissions = new GroupPermissions(seedGroup.isBlacklist, [...seedGroup.inputActions]);
|
||||
const existing = [...groupsPlugin.groups.values()].find(other => other.name === seedGroup.name);
|
||||
const group = new GroupRecord(existing?.id ?? newId(groupsPlugin.groups), seedGroup.name, permissions);
|
||||
if (!existing) {
|
||||
this.logger.info(`Created permission group ${seedGroup.name}`);
|
||||
}
|
||||
groupsPlugin.groups.set(group);
|
||||
return group;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map each seed role onto its seed group.
|
||||
*
|
||||
* The mapping with the highest priority decides a player's group, so the
|
||||
* priorities follow how exp_roles picks a player's highest role: role
|
||||
* priority first, then the role order.
|
||||
*/
|
||||
seedRoleMappings(groupsPlugin: GroupsPlugin, roleIds: Map<string, number>, groupIds: Map<string, number>) {
|
||||
// Lowest role first, so the mapping priority rises with the role
|
||||
const ranked = seedRoles
|
||||
.filter(seedRole => seedRole.group !== undefined && roleIds.has(seedRole.name))
|
||||
.reverse()
|
||||
.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
|
||||
const seededRoleIds = new Set(ranked.map(seedRole => roleIds.get(seedRole.name)!));
|
||||
|
||||
// A mapping of a single seed role is reused, every other mapping keeps its priority
|
||||
const existing = new Map<number, RoleMappingRecord>();
|
||||
const taken = new Set<number>();
|
||||
for (const mapping of groupsPlugin.roleMappings.values()) {
|
||||
const [roleId] = mapping.roleIds;
|
||||
if (mapping.roleIds.size === 1 && seededRoleIds.has(roleId)) {
|
||||
existing.set(roleId, mapping);
|
||||
} else {
|
||||
taken.add(mapping.priority);
|
||||
}
|
||||
}
|
||||
|
||||
const mappings = [];
|
||||
let priority = 0;
|
||||
for (const seedRole of ranked) {
|
||||
priority += 1;
|
||||
while (taken.has(priority)) {
|
||||
priority += 1;
|
||||
}
|
||||
|
||||
const roleId = roleIds.get(seedRole.name)!;
|
||||
mappings.push(new RoleMappingRecord(
|
||||
existing.get(roleId)?.id ?? newId(groupsPlugin.roleMappings),
|
||||
new Set([roleId]),
|
||||
groupIds.get(seedRole.group!)!,
|
||||
priority,
|
||||
true,
|
||||
));
|
||||
}
|
||||
|
||||
groupsPlugin.roleMappings.setMany(mappings);
|
||||
return mappings;
|
||||
}
|
||||
}
|
||||
|
||||
function newId(datastore: { has(id: number): boolean }) {
|
||||
let id = Math.random() * 2 ** 31 | 0;
|
||||
while (datastore.has(id)) {
|
||||
id = Math.random() * 2 ** 31 | 0;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
+30
-24
@@ -1,39 +1,45 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
// import * as Messages from "./messages";
|
||||
|
||||
// Defines a permission for every in game action and role flag used by the scenario
|
||||
import "./permissions";
|
||||
|
||||
lib.definePermission({
|
||||
name: "exp_scenario.config.view",
|
||||
title: "View ExpScenario Config",
|
||||
description: "View the config for all submodules of ExpScenario",
|
||||
});
|
||||
|
||||
lib.definePermission({
|
||||
name: "exp_scenario.config.edit",
|
||||
title: "Edit ExpScenario Config",
|
||||
description: "Edit the config for all submodules of ExpScenario",
|
||||
});
|
||||
import * as messages from "./messages.js";
|
||||
import { permissions as scenarioPermissions, type ScenarioPermissionName } from "./permissions.js";
|
||||
|
||||
declare module "@clusterio/lib" {
|
||||
|
||||
// Everything checked in game through exp_roles, plus the permissions checked on the controller
|
||||
export interface Permissions extends Record<ScenarioPermissionName, never> {
|
||||
"exp_scenario.config.view": never;
|
||||
"exp_scenario.config.edit": never;
|
||||
"exp_scenario.seed": never;
|
||||
}
|
||||
}
|
||||
|
||||
export const plugin: lib.PluginDeclaration = {
|
||||
name: "exp_scenario",
|
||||
title: "exp_scenario",
|
||||
description: "Example Description. Plugin. Change me in index.ts",
|
||||
controllerEntrypoint: "./dist/node/controller",
|
||||
instanceEntrypoint: "./dist/node/instance",
|
||||
controllerEntrypoint: "./dist/node/controller.js",
|
||||
instanceEntrypoint: "./dist/node/instance.js",
|
||||
|
||||
/*
|
||||
messages: [
|
||||
messages.SeedRequest,
|
||||
],
|
||||
|
||||
permissions: [
|
||||
...scenarioPermissions,
|
||||
{
|
||||
name: "exp_scenario.config.view",
|
||||
title: "View ExpScenario Config",
|
||||
description: "View the config for all submodules of ExpScenario",
|
||||
},
|
||||
{
|
||||
name: "exp_scenario.config.edit",
|
||||
title: "Edit ExpScenario Config",
|
||||
description: "Edit the config for all submodules of ExpScenario",
|
||||
},
|
||||
{
|
||||
name: "exp_scenario.seed",
|
||||
title: "Seed ExpScenario roles and groups",
|
||||
description: "Create the roles and permission groups the scenario shipped with",
|
||||
},
|
||||
],
|
||||
|
||||
webEntrypoint: "./web",
|
||||
routes: [
|
||||
"/exp_scenario",
|
||||
],
|
||||
*/
|
||||
};
|
||||
@@ -1,95 +1,11 @@
|
||||
import { plainJson, jsonArray, JsonBoolean, JsonNumber, JsonString, StringEnum } from "@clusterio/lib";
|
||||
import { Type, Static } from "@sinclair/typebox";
|
||||
|
||||
export class PluginExampleEvent {
|
||||
declare ["constructor"]: typeof PluginExampleEvent;
|
||||
static type = "event" as const;
|
||||
static src = ["host", "control"] as const;
|
||||
static dst = ["controller", "host", "instance"] as const;
|
||||
/** Create the roles and permission groups the scenario shipped with, see seed.ts. */
|
||||
export class SeedRequest {
|
||||
declare ["constructor"]: typeof SeedRequest;
|
||||
static plugin = "exp_scenario" as const;
|
||||
static permission = "exp_scenario.example.permission.event";
|
||||
|
||||
constructor(
|
||||
public myString: string,
|
||||
public myNumberArray: number[],
|
||||
) {
|
||||
}
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"myString": Type.String(),
|
||||
"myNumberArray": Type.Array(Type.Number()),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof PluginExampleEvent.jsonSchema>) {
|
||||
return new PluginExampleEvent(json.myString, json.myNumberArray);
|
||||
}
|
||||
}
|
||||
|
||||
export class PluginExampleRequest {
|
||||
declare ["constructor"]: typeof PluginExampleRequest;
|
||||
static type = "request" as const;
|
||||
static src = ["host", "control"] as const;
|
||||
static dst = ["controller", "host", "instance"] as const;
|
||||
static plugin = "exp_scenario" as const;
|
||||
static permission = "exp_scenario.example.permission.request";
|
||||
static src = "control" as const;
|
||||
static dst = "controller" as const;
|
||||
static permission = "exp_scenario.seed" as const;
|
||||
|
||||
constructor(
|
||||
public myString: string,
|
||||
public myNumberArray: number[],
|
||||
) {
|
||||
}
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"myString": Type.String(),
|
||||
"myNumberArray": Type.Array(Type.Number()),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof PluginExampleRequest.jsonSchema>) {
|
||||
return new PluginExampleRequest(json.myString, json.myNumberArray);
|
||||
}
|
||||
|
||||
static Response = plainJson(Type.Object({
|
||||
"myResponseString": Type.String(),
|
||||
"myResponseNumbers": Type.Array(Type.Number()),
|
||||
}));
|
||||
}
|
||||
|
||||
export class ExampleSubscribableValue {
|
||||
constructor(
|
||||
public id: string,
|
||||
public updatedAtMs: number,
|
||||
public isDeleted: boolean,
|
||||
) {
|
||||
}
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
id: Type.String(),
|
||||
updatedAtMs: Type.Number(),
|
||||
isDeleted: Type.Boolean(),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(json.id, json.updatedAtMs, json.isDeleted);
|
||||
}
|
||||
}
|
||||
|
||||
export class ExampleSubscribableUpdate {
|
||||
declare ["constructor"]: typeof ExampleSubscribableUpdate;
|
||||
static type = "event" as const;
|
||||
static src = "controller" as const;
|
||||
static dst = "control" as const;
|
||||
static plugin = "exp_scenario" as const;
|
||||
static permission = "exp_scenario.example.permission.subscribe";
|
||||
|
||||
constructor(
|
||||
public updates: ExampleSubscribableValue[],
|
||||
) { }
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"updates": Type.Array(ExampleSubscribableValue.jsonSchema),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(json.updates.map(update => ExampleSubscribableValue.fromJSON(update)));
|
||||
}
|
||||
constructor() {}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ local add_static, add_dynamic = Commands.add_rcon_static, Commands.add_rcon_dyna
|
||||
|
||||
add_static("Gui", require("modules/exp_gui"))
|
||||
|
||||
add_static("Group", require("modules.exp_legacy.expcore.permission_groups"))
|
||||
add_static("Roles", require("modules/exp_roles"))
|
||||
add_static("Datastore", require("modules.exp_legacy.expcore.datastore"))
|
||||
add_static("External", require("modules.exp_legacy.expcore.external"))
|
||||
@@ -3,7 +3,7 @@ Adds a virtual layer to store power to save space.
|
||||
]]
|
||||
|
||||
local Commands = require("modules/exp_commands")
|
||||
local vlayer = require("modules.exp_legacy.modules.control.vlayer")
|
||||
local Vlayer = require("modules/exp_scenario/control/vlayer")
|
||||
|
||||
--- @class ExpCommand_vlayer.commands
|
||||
local commands = {}
|
||||
@@ -14,7 +14,7 @@ commands.vlayer = Commands.new("vlayer-info", { "exp-commands_vlayer.description
|
||||
:register(function(player)
|
||||
local index = 3
|
||||
local response = { "", "exp-commands_vlayer.title" } --- @type LocalisedString
|
||||
for title, value in pairs(vlayer.get_circuits()) do
|
||||
for title, value in pairs(Vlayer.get_circuits()) do
|
||||
response[index] = { "exp-commands_vlayer.result", title, value }
|
||||
index = index + 1
|
||||
end
|
||||
|
||||
@@ -67,6 +67,7 @@ add(require("modules/exp_scenario/control/roles"))
|
||||
add(require("modules/exp_scenario/control/spawn_area"))
|
||||
add(require("modules/exp_scenario/control/spectate"))
|
||||
add(require("modules/exp_scenario/control/station_auto_name"))
|
||||
add(require("modules/exp_scenario/control/vlayer"))
|
||||
|
||||
--- Guis
|
||||
add(require("modules/exp_scenario/gui/autofill"))
|
||||
@@ -83,5 +84,7 @@ add(require("modules/exp_scenario/gui/rocket_info"))
|
||||
add(require("modules/exp_scenario/gui/science_production"))
|
||||
add(require("modules/exp_scenario/gui/surveillance"))
|
||||
add(require("modules/exp_scenario/gui/task_list"))
|
||||
add(require("modules/exp_scenario/gui/vlayer"))
|
||||
add(require("modules/exp_scenario/gui/warp_list"))
|
||||
add(require("modules/exp_scenario/gui/debug/shim"))
|
||||
add(require("modules/exp_scenario/gui/debug/event_view"))
|
||||
@@ -9,9 +9,6 @@ Roles.define_permission_trigger("exp_scenario.player.admin", function(player, st
|
||||
player.admin = state
|
||||
end)
|
||||
|
||||
Roles.define_permission_trigger("exp_scenario.player.spectator", function(player, state)
|
||||
player.spectator = state
|
||||
end)
|
||||
|
||||
return {
|
||||
events = {
|
||||
|
||||
@@ -0,0 +1,673 @@
|
||||
--[[-- Control - Virtual Layer
|
||||
Stores solar panels, accumulators and landfill in a virtual layer which produces power through interface entities
|
||||
]]
|
||||
|
||||
local ExpUtil = require("modules/exp_util")
|
||||
local Storage = require("modules/exp_util/storage")
|
||||
local config = require("modules.exp_legacy.config.vlayer")
|
||||
|
||||
local floor = math.floor
|
||||
local min = math.min
|
||||
local max = math.max
|
||||
local mega = 1000000
|
||||
|
||||
--- @alias ExpScenario_Vlayer.InterfaceType "energy" | "circuit" | "storage_input" | "storage_output"
|
||||
|
||||
--- @class ExpScenario_Vlayer.Interfaces
|
||||
--- @field energy LuaEntity[]
|
||||
--- @field circuit LuaEntity[]
|
||||
--- @field storage_input LuaEntity[]
|
||||
--- @field storage_output LuaEntity[]
|
||||
|
||||
--- @class ExpScenario_Vlayer.Properties
|
||||
--- @field total_surface_area number Tiles provided by landfill
|
||||
--- @field used_surface_area number Tiles taken by allocated items
|
||||
--- @field total_production number MW of every solar panel stored, allocated or not
|
||||
--- @field production number MW of the allocated solar panels
|
||||
--- @field discharge number MW the allocated accumulators can discharge
|
||||
--- @field capacity number MJ the allocated accumulators can hold
|
||||
|
||||
--- @class ExpScenario_Vlayer.PowerItem
|
||||
--- @field value number J released by burning one
|
||||
--- @field count number
|
||||
|
||||
--- @class ExpScenario_Vlayer.Storage
|
||||
--- @field items table<string, number> Every stored item, allocated or not
|
||||
--- @field unallocated table<string, number> Items waiting for surface area
|
||||
--- @field power_items table<string, ExpScenario_Vlayer.PowerItem> Fuel burned when the stored energy is low
|
||||
--- @field energy number J
|
||||
|
||||
--- @class ExpScenario_Vlayer.Statistics
|
||||
--- @field total_surface_area number
|
||||
--- @field used_surface_area number
|
||||
--- @field remaining_surface_area number Surface area left after the unallocated items are counted
|
||||
--- @field surface_area number
|
||||
--- @field production_multiplier number
|
||||
--- @field energy_max number W when the sun is up
|
||||
--- @field energy_production number W right now
|
||||
--- @field energy_total_production number W sustained if every stored solar panel was allocated
|
||||
--- @field energy_sustained number W averaged over a day
|
||||
--- @field energy_capacity number J
|
||||
--- @field energy_storage number J
|
||||
|
||||
--- @class ExpScenario_Vlayer.Data
|
||||
--- @field interfaces ExpScenario_Vlayer.Interfaces
|
||||
--- @field properties ExpScenario_Vlayer.Properties
|
||||
--- @field storage ExpScenario_Vlayer.Storage
|
||||
--- @field surface LuaSurface | table The surface the day cycle is taken from, or a copy of the config settings
|
||||
|
||||
--- @class ExpScenario_Vlayer
|
||||
local Vlayer = {
|
||||
--- @package
|
||||
events = {},
|
||||
--- @package
|
||||
on_nth_tick = {},
|
||||
}
|
||||
|
||||
--- The properties of every allowed item, modded items are derived from their base game equivalent
|
||||
local allowed_items = {} --- @type table<string, Vlayer.ItemProperties>
|
||||
for name, properties in pairs(config.allowed_items) do
|
||||
allowed_items[name] = properties
|
||||
end
|
||||
for name, properties in pairs(config.modded_items) do
|
||||
local base = config.allowed_items[properties.base_game_equivalent]
|
||||
local multiplier = properties.multiplier
|
||||
allowed_items[name] = {
|
||||
starting_value = properties.starting_value or 0,
|
||||
required_area = base.required_area or 0,
|
||||
surface_area = (base.surface_area or 0) * multiplier,
|
||||
production = (base.production or 0) * multiplier,
|
||||
capacity = (base.capacity or 0) * multiplier,
|
||||
}
|
||||
end
|
||||
|
||||
--- @type ExpScenario_Vlayer.Data
|
||||
local vlayer_data = {
|
||||
interfaces = {
|
||||
energy = {},
|
||||
circuit = {},
|
||||
storage_input = {},
|
||||
storage_output = {},
|
||||
},
|
||||
properties = {
|
||||
total_surface_area = 0,
|
||||
used_surface_area = 0,
|
||||
total_production = 0,
|
||||
production = 0,
|
||||
discharge = 0,
|
||||
capacity = 0,
|
||||
},
|
||||
storage = {
|
||||
items = {},
|
||||
unallocated = {},
|
||||
power_items = {},
|
||||
energy = 0,
|
||||
},
|
||||
surface = table.deep_copy(config.surface),
|
||||
}
|
||||
|
||||
Storage.register(vlayer_data, function(tbl)
|
||||
vlayer_data = tbl
|
||||
end)
|
||||
|
||||
--- Interface entities are neutral so players can not mine them, these are the prototypes used for each type
|
||||
local interface_names = {
|
||||
energy = "electric-energy-interface",
|
||||
circuit = "constant-combinator",
|
||||
storage_input = "storage-chest",
|
||||
storage_output = "requester-chest",
|
||||
} --- @type table<ExpScenario_Vlayer.InterfaceType, string>
|
||||
|
||||
local interface_types = {} --- @type table<string, ExpScenario_Vlayer.InterfaceType>
|
||||
for interface_type, name in pairs(interface_names) do
|
||||
interface_types[name] = interface_type
|
||||
end
|
||||
|
||||
--- Statistics exposed on circuit interfaces and the signal each is sent as
|
||||
local circuit_signals = {
|
||||
total_surface_area = "signal-A",
|
||||
used_surface_area = "signal-U",
|
||||
remaining_surface_area = "signal-R",
|
||||
production_multiplier = "signal-M",
|
||||
energy_production = "signal-P",
|
||||
energy_sustained = "signal-S",
|
||||
energy_capacity = "signal-C",
|
||||
energy_storage = "signal-E",
|
||||
} --- @type table<string, string>
|
||||
|
||||
local circuit_description = {}
|
||||
for stat_name, signal_name in pairs(circuit_signals) do
|
||||
circuit_description[#circuit_description + 1] = string.format("[virtual-signal=%s] = %s", signal_name, stat_name:gsub("_", " "))
|
||||
end
|
||||
circuit_description = table.concat(circuit_description, "\n")
|
||||
|
||||
--- Get every stored item keyed by name, allocated or not
|
||||
--- @return table<string, number>
|
||||
function Vlayer.get_items()
|
||||
return vlayer_data.storage.items
|
||||
end
|
||||
|
||||
--- Get the items waiting for surface area keyed by name
|
||||
--- @return table<string, number>
|
||||
function Vlayer.get_unallocated_items()
|
||||
return vlayer_data.storage.unallocated
|
||||
end
|
||||
|
||||
--- Get the items which contribute to the properties keyed by name
|
||||
--- @return table<string, number>
|
||||
function Vlayer.get_allocated_items()
|
||||
local allocated = {}
|
||||
local unallocated = vlayer_data.storage.unallocated
|
||||
for name, count in pairs(vlayer_data.storage.items) do
|
||||
allocated[name] = count - (unallocated[name] or 0)
|
||||
end
|
||||
return allocated
|
||||
end
|
||||
|
||||
--- Get the interface entities of each type, do not modify
|
||||
--- @return ExpScenario_Vlayer.Interfaces
|
||||
function Vlayer.get_interfaces()
|
||||
return vlayer_data.interfaces
|
||||
end
|
||||
|
||||
--- Get the signal each statistic is sent as on circuit interfaces
|
||||
--- @return table<string, string>
|
||||
function Vlayer.get_circuits()
|
||||
return circuit_signals
|
||||
end
|
||||
|
||||
--- Surface area left once the unallocated items have taken what they need
|
||||
--- @return number
|
||||
local function get_remaining_surface_area()
|
||||
local properties = vlayer_data.properties
|
||||
local remaining = properties.total_surface_area - properties.used_surface_area
|
||||
for name, count in pairs(vlayer_data.storage.unallocated) do
|
||||
remaining = remaining - allowed_items[name].required_area * count
|
||||
end
|
||||
return remaining
|
||||
end
|
||||
|
||||
--- Fraction of solar production available right now, from the darkness of a real surface or the config day cycle
|
||||
--- @return number
|
||||
local function get_production_multiplier()
|
||||
local surface = vlayer_data.surface
|
||||
local multiplier = surface.solar_power_multiplier
|
||||
if surface.always_day then
|
||||
return multiplier
|
||||
end
|
||||
|
||||
if surface.darkness then
|
||||
local brightness = 1 - surface.darkness
|
||||
if brightness < surface.min_brightness then
|
||||
return 0
|
||||
end
|
||||
return multiplier * (brightness - surface.min_brightness) / (1 - surface.min_brightness)
|
||||
end
|
||||
|
||||
local daytime = (game.tick % surface.ticks_per_day) / surface.ticks_per_day
|
||||
surface.daytime = daytime
|
||||
if daytime <= surface.dusk then
|
||||
return multiplier
|
||||
elseif daytime <= surface.evening then
|
||||
return multiplier * (1 - (daytime - surface.dusk) / (surface.evening - surface.dusk))
|
||||
elseif daytime <= surface.morning then
|
||||
return 0
|
||||
elseif daytime <= surface.dawn then
|
||||
return multiplier * (daytime - surface.morning) / (surface.dawn - surface.morning)
|
||||
end
|
||||
return multiplier
|
||||
end
|
||||
|
||||
--- Fraction of solar production averaged over a whole day, sunrise and sunset count for half
|
||||
--- @return number
|
||||
local function get_sustained_multiplier()
|
||||
local surface = vlayer_data.surface
|
||||
local multiplier = surface.solar_power_multiplier
|
||||
if surface.always_day then
|
||||
return multiplier
|
||||
end
|
||||
|
||||
local day_duration = 1 - surface.dawn + surface.dusk
|
||||
local sunset_duration = surface.evening - surface.dusk
|
||||
local sunrise_duration = surface.dawn - surface.morning
|
||||
return multiplier * (day_duration + 0.5 * (sunset_duration + sunrise_duration))
|
||||
end
|
||||
|
||||
--- Apply the properties of an item to the layer, a negative count deallocates
|
||||
--- @param name string
|
||||
--- @param count number
|
||||
local function allocate_item(name, count)
|
||||
local item = allowed_items[name]
|
||||
local properties = vlayer_data.properties
|
||||
if item.production then
|
||||
properties.production = properties.production + item.production * count
|
||||
properties.total_production = properties.total_production + item.production * count
|
||||
end
|
||||
if item.capacity then
|
||||
properties.capacity = properties.capacity + item.capacity * count
|
||||
end
|
||||
if item.discharge then
|
||||
properties.discharge = properties.discharge + item.discharge * count
|
||||
end
|
||||
if item.surface_area then
|
||||
properties.total_surface_area = properties.total_surface_area + item.surface_area * count
|
||||
end
|
||||
if item.required_area > 0 then
|
||||
properties.used_surface_area = properties.used_surface_area + item.required_area * count
|
||||
end
|
||||
end
|
||||
|
||||
--- Count the production of items which are stored but not allocated, so the total shows what more surface area would give
|
||||
--- @param name string
|
||||
--- @param count number
|
||||
local function count_unallocated_production(name, count)
|
||||
local item = allowed_items[name]
|
||||
if item.production then
|
||||
vlayer_data.properties.total_production = vlayer_data.properties.total_production + item.production * count
|
||||
end
|
||||
end
|
||||
|
||||
for name, item in pairs(allowed_items) do
|
||||
vlayer_data.storage.items[name] = item.starting_value
|
||||
if item.required_area > 0 then
|
||||
vlayer_data.storage.unallocated[name] = 0
|
||||
end
|
||||
if item.power then
|
||||
vlayer_data.storage.power_items[name] = { value = assert(item.fuel_value) * mega, count = 0 }
|
||||
end
|
||||
allocate_item(name, item.starting_value)
|
||||
end
|
||||
|
||||
--- Store an item, it is allocated straight away when there is surface area for it
|
||||
--- @param name string
|
||||
--- @param count number
|
||||
function Vlayer.insert_item(name, count)
|
||||
local item = assert(allowed_items[name], "Item not allowed in the vlayer: " .. name)
|
||||
local storage = vlayer_data.storage
|
||||
storage.items[name] = storage.items[name] + count
|
||||
|
||||
if config.unlimited_surface_area or item.required_area == 0 then
|
||||
allocate_item(name, count)
|
||||
return
|
||||
end
|
||||
|
||||
local properties = vlayer_data.properties
|
||||
local surplus_area = properties.total_surface_area - properties.used_surface_area
|
||||
local allocated = min(count, floor(surplus_area / item.required_area))
|
||||
if allocated > 0 then
|
||||
allocate_item(name, allocated)
|
||||
end
|
||||
|
||||
local unallocated = count - allocated
|
||||
storage.unallocated[name] = storage.unallocated[name] + unallocated
|
||||
count_unallocated_production(name, unallocated)
|
||||
end
|
||||
|
||||
--- Take an item out, unallocated items go first and landfill can only go while nothing is using its area
|
||||
--- @param name string
|
||||
--- @param count number
|
||||
--- @return number # The count actually removed
|
||||
function Vlayer.remove_item(name, count)
|
||||
local item = assert(allowed_items[name], "Item not allowed in the vlayer: " .. name)
|
||||
local storage = vlayer_data.storage
|
||||
local properties = vlayer_data.properties
|
||||
local removed = 0
|
||||
|
||||
if not config.unlimited_surface_area and item.required_area > 0 then
|
||||
removed = min(count, storage.unallocated[name])
|
||||
storage.items[name] = storage.items[name] - removed
|
||||
storage.unallocated[name] = storage.unallocated[name] - removed
|
||||
count_unallocated_production(name, -removed)
|
||||
count = count - removed
|
||||
if count == 0 then
|
||||
return removed
|
||||
end
|
||||
end
|
||||
|
||||
local deallocate = min(count, storage.items[name])
|
||||
if item.surface_area and item.surface_area > 0 then
|
||||
local surplus_area = properties.total_surface_area - properties.used_surface_area
|
||||
deallocate = min(deallocate, floor(surplus_area / item.surface_area))
|
||||
if deallocate <= 0 then
|
||||
return removed
|
||||
end
|
||||
end
|
||||
|
||||
storage.items[name] = storage.items[name] - deallocate
|
||||
allocate_item(name, -deallocate)
|
||||
return removed + deallocate
|
||||
end
|
||||
|
||||
--- Add or take energy, used by other modules which spend the stored energy
|
||||
--- @param energy number J, negative to take
|
||||
--- @return number # J stored afterwards
|
||||
function Vlayer.energy_changed(energy)
|
||||
local storage = vlayer_data.storage
|
||||
storage.energy = storage.energy + energy
|
||||
return storage.energy
|
||||
end
|
||||
|
||||
--- Get the statistics of the layer
|
||||
--- @return ExpScenario_Vlayer.Statistics
|
||||
function Vlayer.get_statistics()
|
||||
local properties = vlayer_data.properties
|
||||
local production = properties.production * mega
|
||||
local production_multiplier = get_production_multiplier()
|
||||
local sustained_multiplier = get_sustained_multiplier()
|
||||
local remaining_surface_area = get_remaining_surface_area()
|
||||
|
||||
return {
|
||||
total_surface_area = properties.total_surface_area,
|
||||
used_surface_area = properties.used_surface_area,
|
||||
remaining_surface_area = remaining_surface_area,
|
||||
surface_area = properties.total_surface_area - remaining_surface_area,
|
||||
production_multiplier = production_multiplier,
|
||||
energy_max = production,
|
||||
energy_production = production * production_multiplier,
|
||||
energy_total_production = properties.total_production * sustained_multiplier * mega,
|
||||
energy_sustained = production * sustained_multiplier,
|
||||
energy_capacity = properties.capacity * mega,
|
||||
energy_storage = vlayer_data.storage.energy,
|
||||
}
|
||||
end
|
||||
|
||||
--- Make an entity into an interface which players can not mine or damage
|
||||
--- @param interface LuaEntity
|
||||
--- @param interface_type ExpScenario_Vlayer.InterfaceType
|
||||
--- @param player LuaPlayer?
|
||||
local function register_interface(interface, interface_type, player)
|
||||
local interfaces = vlayer_data.interfaces[interface_type]
|
||||
interfaces[#interfaces + 1] = interface
|
||||
interface.last_user = player
|
||||
interface.destructible = false
|
||||
interface.minable_flag = false
|
||||
end
|
||||
|
||||
--- Create an interface which powers the electric network it is placed in
|
||||
--- @param surface LuaSurface
|
||||
--- @param position MapPosition
|
||||
--- @param player LuaPlayer?
|
||||
--- @return LuaEntity? # Nil when the interface does not fit
|
||||
function Vlayer.create_energy_interface(surface, position, player)
|
||||
if not surface.can_place_entity{ name = interface_names.energy, position = position } then
|
||||
return nil
|
||||
end
|
||||
|
||||
local interface = assert(surface.create_entity{ name = interface_names.energy, position = position, force = "neutral" })
|
||||
register_interface(interface, "energy", player)
|
||||
interface.operable = false
|
||||
interface.electric_buffer_size = 0
|
||||
interface.power_production = 0
|
||||
interface.power_usage = 0
|
||||
interface.energy = 0
|
||||
return interface
|
||||
end
|
||||
|
||||
--- Create an interface which outputs the statistics and stored items as signals
|
||||
--- @param surface LuaSurface
|
||||
--- @param position MapPosition
|
||||
--- @param player LuaPlayer?
|
||||
--- @return LuaEntity
|
||||
function Vlayer.create_circuit_interface(surface, position, player)
|
||||
local interface = assert(surface.create_entity{ name = interface_names.circuit, position = position, force = "neutral" })
|
||||
register_interface(interface, "circuit", player)
|
||||
interface.combinator_description = circuit_description
|
||||
return interface
|
||||
end
|
||||
|
||||
--- Create an interface which stores every allowed item put into it
|
||||
--- @param surface LuaSurface
|
||||
--- @param position MapPosition
|
||||
--- @param player LuaPlayer?
|
||||
--- @return LuaEntity
|
||||
function Vlayer.create_input_interface(surface, position, player)
|
||||
local interface = assert(surface.create_entity{ name = interface_names.storage_input, position = position, force = "neutral" })
|
||||
register_interface(interface, "storage_input", player)
|
||||
-- A filter nothing can match stops robots filling it with anything
|
||||
interface.storage_filter = { name = "deconstruction-planner", quality = "normal" }
|
||||
return interface
|
||||
end
|
||||
|
||||
--- Create an interface which takes stored items out to fulfil its requests
|
||||
--- @param surface LuaSurface
|
||||
--- @param position MapPosition
|
||||
--- @param player LuaPlayer?
|
||||
--- @return LuaEntity
|
||||
function Vlayer.create_output_interface(surface, position, player)
|
||||
local interface = assert(surface.create_entity{ name = interface_names.storage_output, position = position, force = "neutral" })
|
||||
register_interface(interface, "storage_output", player)
|
||||
return interface
|
||||
end
|
||||
|
||||
--- Remove an interface, anything it holds is dropped in a chest next to it and stored energy is kept
|
||||
--- @param interface LuaEntity
|
||||
--- @return ExpScenario_Vlayer.InterfaceType
|
||||
function Vlayer.remove_interface(interface)
|
||||
local interface_type = assert(interface_types[interface.name], "Entity is not a vlayer interface: " .. interface.name)
|
||||
if interface_type == "storage_input" or interface_type == "storage_output" then
|
||||
ExpUtil.transfer_inventory_to_surface{
|
||||
inventory = assert(interface.get_inventory(defines.inventory.chest)),
|
||||
surface = interface.surface,
|
||||
name = "iron-chest",
|
||||
allow_creation = true,
|
||||
}
|
||||
elseif interface_type == "energy" then
|
||||
vlayer_data.storage.energy = vlayer_data.storage.energy + interface.energy
|
||||
end
|
||||
|
||||
table.remove_element(vlayer_data.interfaces[interface_type], interface)
|
||||
interface.destroy()
|
||||
return interface_type
|
||||
end
|
||||
|
||||
--- Forget interfaces which no longer exist
|
||||
--- @param interfaces LuaEntity[]
|
||||
local function prune_interfaces(interfaces)
|
||||
for index = #interfaces, 1, -1 do
|
||||
if not interfaces[index].valid then
|
||||
table.remove(interfaces, index)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Store the contents of the input interfaces
|
||||
local function handle_input_interfaces()
|
||||
local interfaces = vlayer_data.interfaces.storage_input
|
||||
prune_interfaces(interfaces)
|
||||
for _, interface in ipairs(interfaces) do
|
||||
local inventory = assert(interface.get_inventory(defines.inventory.chest))
|
||||
for _, item in pairs(inventory.get_contents()) do
|
||||
local name = item.name
|
||||
if allowed_items[name] then
|
||||
-- Quality is not tracked, so higher quality is stored as extra normal items in batches of ten
|
||||
local quality_level = prototypes.quality[item.quality].level
|
||||
local removed, stored = 0, 0
|
||||
if quality_level == 0 then
|
||||
removed, stored = item.count, item.count
|
||||
elseif item.count >= 10 then
|
||||
local batches = floor(item.count / 10)
|
||||
removed, stored = batches * 10, batches * (10 + quality_level * 3)
|
||||
end
|
||||
|
||||
if removed > 0 then
|
||||
local modded = config.modded_items[name]
|
||||
if modded and config.modded_auto_downgrade then
|
||||
Vlayer.insert_item(modded.base_game_equivalent, stored * modded.multiplier)
|
||||
elseif vlayer_data.storage.power_items[name] then
|
||||
local power_item = vlayer_data.storage.power_items[name]
|
||||
power_item.count = power_item.count + stored
|
||||
else
|
||||
Vlayer.insert_item(name, stored)
|
||||
end
|
||||
inventory.remove{ name = name, count = removed, quality = item.quality }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Fill the requests of the output interfaces from storage
|
||||
local function handle_output_interfaces()
|
||||
local interfaces = vlayer_data.interfaces.storage_output
|
||||
prune_interfaces(interfaces)
|
||||
for _, interface in ipairs(interfaces) do
|
||||
local inventory = assert(interface.get_inventory(defines.inventory.chest))
|
||||
for _, section in ipairs(assert(interface.get_logistic_sections()).sections) do
|
||||
for _, filter in pairs(section.filters) do
|
||||
local name = filter.value and filter.value.name
|
||||
if name and allowed_items[name] then
|
||||
local wanted = min(assert(filter.min) - inventory.get_item_count(name), vlayer_data.storage.items[name])
|
||||
if wanted > 0 and inventory.can_insert{ name = name, count = wanted } then
|
||||
local removed = Vlayer.remove_item(name, wanted)
|
||||
if removed > 0 then
|
||||
inventory.insert{ name = name, count = removed, quality = "normal" }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Allocate waiting items in proportion to the surface area that has become available
|
||||
local function handle_unallocated()
|
||||
if config.unlimited_surface_area then return end
|
||||
|
||||
local unallocated_area = 0
|
||||
for name, count in pairs(vlayer_data.storage.unallocated) do
|
||||
unallocated_area = unallocated_area + allowed_items[name].required_area * count
|
||||
end
|
||||
if unallocated_area == 0 then return end
|
||||
|
||||
local properties = vlayer_data.properties
|
||||
local surplus_area = properties.total_surface_area - properties.used_surface_area
|
||||
for name, count in pairs(vlayer_data.storage.unallocated) do
|
||||
local allocated = min(count, floor(count * surplus_area / unallocated_area))
|
||||
if allocated > 0 then
|
||||
vlayer_data.storage.unallocated[name] = count - allocated
|
||||
allocate_item(name, allocated)
|
||||
count_unallocated_production(name, -allocated)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Set the signals of the circuit interfaces to the statistics and stored items
|
||||
local function handle_circuit_interfaces()
|
||||
local interfaces = vlayer_data.interfaces.circuit
|
||||
prune_interfaces(interfaces)
|
||||
if #interfaces == 0 then return end
|
||||
|
||||
local stats = Vlayer.get_statistics()
|
||||
for _, interface in ipairs(interfaces) do
|
||||
local control = interface.get_or_create_control_behavior() --[[@as LuaConstantCombinatorControlBehavior]]
|
||||
if control.sections_count == 0 then
|
||||
control.add_section()
|
||||
end
|
||||
local section = assert(control.sections[1])
|
||||
|
||||
-- Slots are cleared first because an item can not be set in two slots at once
|
||||
for index = section.filters_count, 1, -1 do
|
||||
section.clear_slot(index)
|
||||
end
|
||||
|
||||
local index = 1
|
||||
for stat_name, signal_name in pairs(circuit_signals) do
|
||||
local value = stats[stat_name] --[[@as number]]
|
||||
if stat_name:find("energy") then
|
||||
value = value / mega
|
||||
elseif stat_name == "production_multiplier" then
|
||||
value = value * 10000
|
||||
end
|
||||
section.set_slot(index, { value = { type = "virtual", name = signal_name, quality = "normal" }, min = floor(value) })
|
||||
index = index + 1
|
||||
end
|
||||
|
||||
for name, count in pairs(vlayer_data.storage.items) do
|
||||
if count > 0 and prototypes.item[name] then
|
||||
section.set_slot(index, { value = { type = "item", name = name, quality = "normal" }, min = count })
|
||||
index = index + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Produce energy, share it between the energy interfaces, and burn fuel when storage runs low
|
||||
local function handle_energy_interfaces()
|
||||
local storage = vlayer_data.storage
|
||||
local properties = vlayer_data.properties
|
||||
local production = properties.production * mega * (config.update_tick_energy / 60)
|
||||
storage.energy = storage.energy + floor(production * get_production_multiplier())
|
||||
|
||||
local interfaces = vlayer_data.interfaces.energy
|
||||
prune_interfaces(interfaces)
|
||||
if #interfaces > 0 then
|
||||
local available_energy = storage.energy
|
||||
for _, interface in ipairs(interfaces) do
|
||||
available_energy = available_energy + interface.energy
|
||||
end
|
||||
|
||||
local discharge_rate = 2 * (production + properties.discharge * mega) / #interfaces
|
||||
local fill_to = min(discharge_rate, floor(available_energy / #interfaces))
|
||||
for _, interface in ipairs(interfaces) do
|
||||
interface.electric_buffer_size = max(discharge_rate, interface.energy) -- A smaller buffer would lose energy
|
||||
local delta = fill_to - interface.energy
|
||||
storage.energy = storage.energy - delta
|
||||
interface.energy = interface.energy + delta
|
||||
end
|
||||
end
|
||||
|
||||
local capacity = properties.capacity * mega
|
||||
if not config.unlimited_capacity and storage.energy > capacity then
|
||||
storage.energy = capacity
|
||||
return
|
||||
end
|
||||
|
||||
for _, power_item in pairs(storage.power_items) do
|
||||
local max_burn = capacity / 2 - storage.energy
|
||||
if power_item.count > 0 and max_burn > 0 then
|
||||
local burn = min(power_item.count, floor(max_burn / power_item.value))
|
||||
storage.energy = storage.energy + burn * power_item.value
|
||||
power_item.count = power_item.count - burn
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Take the day cycle from the mimic surface when it exists, otherwise from the config
|
||||
local function update_surface()
|
||||
if config.mimic_surface then
|
||||
local surface = game.get_surface(config.mimic_surface)
|
||||
if surface then
|
||||
vlayer_data.surface = surface
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- The config copy never has an index, so this avoids replacing it every time a surface changes
|
||||
if not vlayer_data.surface.index then
|
||||
vlayer_data.surface = table.deep_copy(config.surface)
|
||||
end
|
||||
end
|
||||
|
||||
--- Move items between the interfaces and storage
|
||||
local function update_storage()
|
||||
handle_input_interfaces()
|
||||
handle_output_interfaces()
|
||||
handle_unallocated()
|
||||
end
|
||||
|
||||
--- Update the signals and energy of the interfaces
|
||||
local function update_energy()
|
||||
handle_circuit_interfaces()
|
||||
handle_energy_interfaces()
|
||||
end
|
||||
|
||||
local e = defines.events
|
||||
|
||||
Vlayer.on_init = update_surface -- The default surface exists before on_surface_created can fire
|
||||
Vlayer.events[e.on_surface_created] = update_surface
|
||||
Vlayer.events[e.on_surface_renamed] = update_surface
|
||||
Vlayer.events[e.on_surface_imported] = update_surface
|
||||
Vlayer.on_nth_tick[config.update_tick_storage] = update_storage
|
||||
Vlayer.on_nth_tick[config.update_tick_energy] = update_energy
|
||||
|
||||
return Vlayer
|
||||
@@ -5,7 +5,7 @@ Adds a gui that allows players to apply various bonuses
|
||||
local Gui = require("modules/exp_gui")
|
||||
local Roles = require("modules/exp_roles")
|
||||
local config = require("modules/exp_legacy/config/bonus")
|
||||
local vlayer = require("modules/exp_legacy/modules/control/vlayer")
|
||||
local Vlayer = require("modules/exp_scenario/control/vlayer")
|
||||
local format_number = require("util").format_number
|
||||
|
||||
--- @class ExpGui_PlayerBonus.elements
|
||||
@@ -486,7 +486,7 @@ end
|
||||
--- Apply periodic bonus to a player
|
||||
--- @param player LuaPlayer
|
||||
local function apply_personal_battery_recharge(player)
|
||||
local available_energy = vlayer.get_statistics()["energy_storage"]
|
||||
local available_energy = Vlayer.get_statistics().energy_storage
|
||||
if available_energy <= 0 then
|
||||
return -- No power to give
|
||||
end
|
||||
@@ -508,7 +508,7 @@ local function apply_personal_battery_recharge(player)
|
||||
local energy_to_give = math.min(math.floor(equipment.max_energy - equipment.energy), available_energy, recharge_amount)
|
||||
equipment.energy = equipment.energy + energy_to_give
|
||||
recharge_amount = recharge_amount - energy_to_give
|
||||
available_energy = vlayer.energy_changed(-energy_to_give)
|
||||
available_energy = Vlayer.energy_changed(-energy_to_give)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -117,7 +117,7 @@ Elements.player_stats_table = Gui.define("player_stats/data_table")
|
||||
width = 135,
|
||||
})
|
||||
labels[stat_name] = Elements.table_label(data_table, {
|
||||
caption = { "readme.data-format", value, metadata.unit or "" },
|
||||
caption = { "exp-gui_readme.data-format", value, metadata.unit or "" },
|
||||
tooltip = metadata.value_tooltip or { "exp-statistics." .. stat_name .. "-tooltip" },
|
||||
width = 105,
|
||||
})
|
||||
@@ -131,7 +131,7 @@ Elements.player_stats_table = Gui.define("player_stats/data_table")
|
||||
width = 135,
|
||||
})
|
||||
labels[stat_name] = Elements.table_label(data_table, {
|
||||
caption = { "readme.data-format", data.default, "" },
|
||||
caption = { "exp-gui_readme.data-format", data.default, "" },
|
||||
tooltip = { "exp-statistics." .. stat_name .. "-tooltip" },
|
||||
width = 105,
|
||||
})
|
||||
|
||||
@@ -189,7 +189,7 @@ define_tab(
|
||||
local player = Gui.get_player(parent)
|
||||
|
||||
local server_details = {
|
||||
name = "ExpGaming S0 - Local",
|
||||
name = config_server_detail["community_name"] .. " S0 - Local",
|
||||
welcome = "Failed to load description: disconnected from external api.",
|
||||
reset_time = "Not Set",
|
||||
}
|
||||
@@ -338,7 +338,7 @@ define_tab(
|
||||
local external_links = Elements.title_table(scroll_pane, 235, { "exp-gui_readme.servers-external" }, 2)
|
||||
for _, key in ipairs{ "discord", "website", "patreon", "status", "github" } do
|
||||
Gui.elements.centered_label(external_links, 110, key:gsub("^%l", string.upper))
|
||||
Gui.elements.centered_label(external_links, 460, { "links." .. key }, { "exp-gui_readme.servers-open-in-browser" })
|
||||
Gui.elements.centered_label(external_links, 460, config_server_detail[key], { "exp-gui_readme.servers-open-in-browser" })
|
||||
end
|
||||
|
||||
return container
|
||||
|
||||
@@ -0,0 +1,531 @@
|
||||
--[[-- Gui - Virtual Layer
|
||||
Shows what the virtual layer holds and lets players build and remove its interfaces
|
||||
]]
|
||||
|
||||
local Gui = require("modules/exp_gui")
|
||||
local Roles = require("modules/exp_roles")
|
||||
local Colors = require("modules/exp_util/include/color")
|
||||
local config = require("modules.exp_legacy.config.vlayer")
|
||||
local Vlayer = require("modules/exp_scenario/control/vlayer")
|
||||
local format_number = require("util").format_number
|
||||
|
||||
local Selection = require("modules/exp_util/selection")
|
||||
local SelectArea = Selection.connect("ExpGui_Vlayer")
|
||||
|
||||
local floor = math.floor
|
||||
local ceil = math.ceil
|
||||
local max = math.max
|
||||
local format_string = string.format
|
||||
|
||||
--- @class ExpGui_Vlayer.elements
|
||||
local Elements = {}
|
||||
|
||||
--- Interface types in the order the type dropdown lists them
|
||||
local interface_types = { "energy", "circuit", "storage_input", "storage_output" } --- @type ExpScenario_Vlayer.InterfaceType[]
|
||||
|
||||
--- Locale key of each interface type
|
||||
local type_captions = {} --- @type table<ExpScenario_Vlayer.InterfaceType, LocalisedString>
|
||||
for _, interface_type in ipairs(interface_types) do
|
||||
type_captions[interface_type] = { "exp-gui_vlayer.type-" .. interface_type:gsub("_", "-") }
|
||||
end
|
||||
|
||||
--- Statistics shown as progress bars, in display order
|
||||
--- @type { name: string, caption: LocalisedString, tooltip: LocalisedString }[]
|
||||
local stats = {}
|
||||
for _, name in ipairs{ "solar", "accumulator", "surface-area", "sustained", "production", "capacity" } do
|
||||
stats[#stats + 1] = {
|
||||
name = name,
|
||||
caption = { "exp-gui_vlayer.caption-stat-" .. name },
|
||||
tooltip = { "exp-gui_vlayer.tooltip-stat-" .. name },
|
||||
}
|
||||
end
|
||||
|
||||
local energy_suffixes = {
|
||||
{ 1e15, "P" }, { 1e12, "T" }, { 1e9, "G" }, { 1e6, "M" }, { 1e3, "k" },
|
||||
}
|
||||
|
||||
--- Format an amount of energy with an SI prefix, util.format_number uses B for giga
|
||||
--- @param amount number
|
||||
--- @param unit string
|
||||
--- @return string
|
||||
local function format_energy(amount, unit)
|
||||
if amount < 1 then
|
||||
return "0 " .. unit
|
||||
end
|
||||
for _, suffix in ipairs(energy_suffixes) do
|
||||
if amount >= suffix[1] then
|
||||
return format_string("%.1f %s%s", amount / suffix[1], suffix[2], unit)
|
||||
end
|
||||
end
|
||||
return format_number(floor(amount), false) .. " " .. unit
|
||||
end
|
||||
|
||||
--- Expand an area to whole tiles
|
||||
--- @param area BoundingBox
|
||||
--- @return BoundingBox
|
||||
local function expand_area(area)
|
||||
return {
|
||||
left_top = { x = floor(area.left_top.x), y = floor(area.left_top.y) },
|
||||
right_bottom = { x = ceil(area.right_bottom.x), y = ceil(area.right_bottom.y) },
|
||||
}
|
||||
end
|
||||
|
||||
--- Rich text link to a position
|
||||
--- @param position MapPosition
|
||||
--- @param surface LuaSurface
|
||||
--- @return string
|
||||
local function gps_string(position, surface)
|
||||
return format_string("[gps=%.1f,%.1f,%s]", position.x, position.y, surface.name)
|
||||
end
|
||||
|
||||
--- Label naming a statistic
|
||||
--- @class ExpGui_Vlayer.elements.stat_label: ExpElement
|
||||
--- @overload fun(parent: LuaGuiElement, caption: LocalisedString, tooltip: LocalisedString): LuaGuiElement
|
||||
Elements.stat_label = Gui.define("vlayer/stat_label")
|
||||
:draw{
|
||||
type = "label",
|
||||
caption = Gui.from_argument(1),
|
||||
tooltip = Gui.from_argument(2),
|
||||
style = "heading_2_label",
|
||||
}
|
||||
:style{
|
||||
width = 200,
|
||||
} --[[@as any]]
|
||||
|
||||
--- Bar showing a statistic as a fraction with the values as its caption
|
||||
--- @class ExpGui_Vlayer.elements.stat_bar: ExpElement
|
||||
--- @overload fun(parent: LuaGuiElement): LuaGuiElement
|
||||
Elements.stat_bar = Gui.define("vlayer/stat_bar")
|
||||
:draw{
|
||||
type = "progressbar",
|
||||
caption = "",
|
||||
value = 0,
|
||||
style = "electric_satisfaction_statistics_progressbar",
|
||||
}
|
||||
:style{
|
||||
width = 200,
|
||||
font = "heading-2",
|
||||
} --[[@as any]]
|
||||
|
||||
--- @class ExpGui_Vlayer.elements.stat_bar.display_data
|
||||
--- @field value number Fraction the bar is filled
|
||||
--- @field caption string
|
||||
|
||||
--- Refresh a bar
|
||||
--- @param stat_bar LuaGuiElement
|
||||
--- @param display_data ExpGui_Vlayer.elements.stat_bar.display_data
|
||||
function Elements.stat_bar.refresh(stat_bar, display_data)
|
||||
stat_bar.value = display_data.value
|
||||
stat_bar.caption = display_data.caption
|
||||
end
|
||||
|
||||
--- Table of a label and bar for each statistic
|
||||
--- @class ExpGui_Vlayer.elements.stats_table: ExpElement
|
||||
--- @field data table<LuaGuiElement, table<string, LuaGuiElement>> Bars keyed by statistic name
|
||||
--- @overload fun(parent: LuaGuiElement): LuaGuiElement
|
||||
Elements.stats_table = Gui.define("vlayer/stats_table")
|
||||
:track_all_elements()
|
||||
:draw(function(def, parent)
|
||||
--- @cast def ExpGui_Vlayer.elements.stats_table
|
||||
local stats_table = Gui.elements.scroll_table(parent, 400, 2, "stats")
|
||||
local stat_bars = {}
|
||||
for _, stat in ipairs(stats) do
|
||||
Elements.stat_label(stats_table, stat.caption, stat.tooltip)
|
||||
stat_bars[stat.name] = Elements.stat_bar(stats_table)
|
||||
end
|
||||
def.data[stats_table] = stat_bars
|
||||
return stats_table
|
||||
end) --[[@as any]]
|
||||
|
||||
--- Calculate the display data of every statistic, the same for every player
|
||||
--- @return table<string, ExpGui_Vlayer.elements.stat_bar.display_data>
|
||||
function Elements.stats_table.calculate_display_data()
|
||||
local statistics = Vlayer.get_statistics()
|
||||
local items = Vlayer.get_items()
|
||||
local allocated = Vlayer.get_allocated_items()
|
||||
return {
|
||||
solar = {
|
||||
value = allocated["solar-panel"] / max(items["solar-panel"], 1),
|
||||
caption = format_number(allocated["solar-panel"], false) .. " / " .. format_number(items["solar-panel"], false),
|
||||
},
|
||||
accumulator = {
|
||||
value = allocated["accumulator"] / max(items["accumulator"], 1),
|
||||
caption = format_number(allocated["accumulator"], false) .. " / " .. format_number(items["accumulator"], false),
|
||||
},
|
||||
["surface-area"] = {
|
||||
value = statistics.total_surface_area / max(statistics.surface_area, 1),
|
||||
caption = format_number(statistics.remaining_surface_area, false),
|
||||
},
|
||||
sustained = {
|
||||
value = statistics.energy_sustained / max(statistics.energy_total_production, 1),
|
||||
caption = format_energy(statistics.energy_sustained, "W") .. " / " .. format_energy(statistics.energy_total_production, "W"),
|
||||
},
|
||||
production = {
|
||||
value = statistics.energy_production / max(statistics.energy_max, 1),
|
||||
caption = format_energy(statistics.energy_production, "W") .. " / " .. format_energy(statistics.energy_max, "W"),
|
||||
},
|
||||
capacity = {
|
||||
value = statistics.energy_storage / max(statistics.energy_capacity, 1),
|
||||
caption = format_energy(statistics.energy_storage, "J") .. " / " .. format_energy(statistics.energy_capacity, "J"),
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
--- Refresh every bar of a table
|
||||
--- @param stats_table LuaGuiElement
|
||||
--- @param display_data table<string, ExpGui_Vlayer.elements.stat_bar.display_data>
|
||||
function Elements.stats_table.refresh(stats_table, display_data)
|
||||
for name, stat_bar in pairs(Elements.stats_table.data[stats_table]) do
|
||||
Elements.stat_bar.refresh(stat_bar, display_data[name])
|
||||
end
|
||||
end
|
||||
|
||||
--- Refresh the tables of every online player
|
||||
function Elements.stats_table.refresh_online()
|
||||
local display_data = Elements.stats_table.calculate_display_data()
|
||||
for _, stats_table in Elements.stats_table:online_elements() do
|
||||
Elements.stats_table.refresh(stats_table, display_data)
|
||||
end
|
||||
end
|
||||
|
||||
--- @class ExpGui_Vlayer.elements.type_dropdown.element_data
|
||||
--- @field controls LuaGuiElement
|
||||
|
||||
--- Dropdown which picks the type of interface to build or list
|
||||
--- @class ExpGui_Vlayer.elements.type_dropdown: ExpElement
|
||||
--- @field data table<LuaGuiElement, ExpGui_Vlayer.elements.type_dropdown.element_data>
|
||||
--- @overload fun(parent: LuaGuiElement, controls: LuaGuiElement): LuaGuiElement
|
||||
Elements.type_dropdown = Gui.define("vlayer/type_dropdown")
|
||||
:draw{
|
||||
type = "drop-down",
|
||||
items = { type_captions.energy, type_captions.circuit, type_captions.storage_input, type_captions.storage_output },
|
||||
selected_index = 1,
|
||||
}
|
||||
:style{
|
||||
width = 200,
|
||||
}
|
||||
:element_data{
|
||||
controls = Gui.from_argument(1),
|
||||
}
|
||||
:on_selection_state_changed(function(def, _, type_dropdown)
|
||||
--- @cast def ExpGui_Vlayer.elements.type_dropdown
|
||||
Elements.controls.refresh_interfaces(def.data[type_dropdown].controls)
|
||||
end) --[[@as any]]
|
||||
|
||||
--- Get the selected interface type
|
||||
--- @param type_dropdown LuaGuiElement
|
||||
--- @return ExpScenario_Vlayer.InterfaceType
|
||||
function Elements.type_dropdown.get_selected(type_dropdown)
|
||||
return (assert(interface_types[type_dropdown.selected_index]))
|
||||
end
|
||||
|
||||
--- Dropdown listing the interfaces of the selected type by position
|
||||
--- @class ExpGui_Vlayer.elements.interface_dropdown: ExpElement
|
||||
--- @overload fun(parent: LuaGuiElement): LuaGuiElement
|
||||
Elements.interface_dropdown = Gui.define("vlayer/interface_dropdown")
|
||||
:draw{
|
||||
type = "drop-down",
|
||||
}
|
||||
:style{
|
||||
width = 200,
|
||||
} --[[@as any]]
|
||||
|
||||
--- Refresh the listed interfaces
|
||||
--- @param interface_dropdown LuaGuiElement
|
||||
--- @param interfaces LuaEntity[]
|
||||
function Elements.interface_dropdown.refresh(interface_dropdown, interfaces)
|
||||
local items = {}
|
||||
for index, interface in ipairs(interfaces) do
|
||||
local position = interface.position
|
||||
items[index] = format_string("%d X %.1f Y %.1f", index, position.x, position.y)
|
||||
end
|
||||
interface_dropdown.items = items
|
||||
end
|
||||
|
||||
--- Get the selected interface
|
||||
--- @param interface_dropdown LuaGuiElement
|
||||
--- @param interfaces LuaEntity[]
|
||||
--- @return LuaEntity?
|
||||
function Elements.interface_dropdown.get_selected(interface_dropdown, interfaces)
|
||||
local interface = interfaces[interface_dropdown.selected_index]
|
||||
if interface and interface.valid then
|
||||
return interface
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
--- @class ExpGui_Vlayer.elements.control_button.element_data
|
||||
--- @field controls LuaGuiElement
|
||||
|
||||
--- Button which refreshes the listed interfaces
|
||||
--- @class ExpGui_Vlayer.elements.refresh_button: ExpElement
|
||||
--- @field data table<LuaGuiElement, ExpGui_Vlayer.elements.control_button.element_data>
|
||||
--- @overload fun(parent: LuaGuiElement, controls: LuaGuiElement): LuaGuiElement
|
||||
Elements.refresh_button = Gui.define("vlayer/refresh_button")
|
||||
:draw{
|
||||
type = "button",
|
||||
caption = { "exp-gui_vlayer.caption-refresh" },
|
||||
}
|
||||
:style{
|
||||
width = 200,
|
||||
}
|
||||
:element_data{
|
||||
controls = Gui.from_argument(1),
|
||||
}
|
||||
:on_click(function(def, _, refresh_button)
|
||||
--- @cast def ExpGui_Vlayer.elements.refresh_button
|
||||
Elements.controls.refresh_interfaces(def.data[refresh_button].controls)
|
||||
end) --[[@as any]]
|
||||
|
||||
--- Button which opens the map at the selected interface
|
||||
--- @class ExpGui_Vlayer.elements.view_button: ExpElement
|
||||
--- @field data table<LuaGuiElement, ExpGui_Vlayer.elements.control_button.element_data>
|
||||
--- @overload fun(parent: LuaGuiElement, controls: LuaGuiElement): LuaGuiElement
|
||||
Elements.view_button = Gui.define("vlayer/view_button")
|
||||
:draw{
|
||||
type = "button",
|
||||
caption = { "exp-gui_vlayer.caption-view" },
|
||||
}
|
||||
:style{
|
||||
width = 200,
|
||||
}
|
||||
:element_data{
|
||||
controls = Gui.from_argument(1),
|
||||
}
|
||||
:on_click(function(def, player, view_button)
|
||||
--- @cast def ExpGui_Vlayer.elements.view_button
|
||||
local controls = def.data[view_button].controls
|
||||
local interface_type, interface = Elements.controls.get_selected_interface(controls)
|
||||
if not interface then return end
|
||||
player.set_controller{ type = defines.controllers.remote, position = interface.position, surface = interface.surface }
|
||||
player.print{ "exp-gui_vlayer.message-located", type_captions[interface_type], gps_string(interface.position, interface.surface) }
|
||||
end) --[[@as any]]
|
||||
|
||||
--- Button which toggles selecting an area to build an interface in
|
||||
--- @class ExpGui_Vlayer.elements.build_button: ExpElement
|
||||
--- @overload fun(parent: LuaGuiElement): LuaGuiElement
|
||||
Elements.build_button = Gui.define("vlayer/build_button")
|
||||
:track_all_elements()
|
||||
:draw{
|
||||
type = "button",
|
||||
caption = { "exp-gui_vlayer.caption-build" },
|
||||
}
|
||||
:style{
|
||||
width = 200,
|
||||
}
|
||||
:on_click(function(_, player)
|
||||
if SelectArea:stop(player) then
|
||||
player.print{ "exp_util.selection_exit", { "exp-gui_vlayer.selection-name" } }
|
||||
else
|
||||
SelectArea:start(player)
|
||||
player.print{ "exp_util.selection_enter", { "exp-gui_vlayer.selection-name" } }
|
||||
end
|
||||
end) --[[@as any]]
|
||||
|
||||
--- Button which removes the selected interface
|
||||
--- @class ExpGui_Vlayer.elements.remove_button: ExpElement
|
||||
--- @field data table<LuaGuiElement, ExpGui_Vlayer.elements.control_button.element_data>
|
||||
--- @overload fun(parent: LuaGuiElement, controls: LuaGuiElement): LuaGuiElement
|
||||
Elements.remove_button = Gui.define("vlayer/remove_button")
|
||||
:track_all_elements()
|
||||
:draw{
|
||||
type = "button",
|
||||
caption = { "exp-gui_vlayer.caption-remove" },
|
||||
}
|
||||
:style{
|
||||
width = 200,
|
||||
}
|
||||
:element_data{
|
||||
controls = Gui.from_argument(1),
|
||||
}
|
||||
:on_click(function(def, player, remove_button)
|
||||
--- @cast def ExpGui_Vlayer.elements.remove_button
|
||||
local controls = def.data[remove_button].controls
|
||||
local _, interface = Elements.controls.get_selected_interface(controls)
|
||||
if not interface then return end
|
||||
local position, surface = interface.position, interface.surface
|
||||
local interface_type = Vlayer.remove_interface(interface)
|
||||
game.print{ "exp-gui_vlayer.message-removed", player.name, type_captions[interface_type], gps_string(position, surface) }
|
||||
Elements.controls.refresh_interfaces_online()
|
||||
end) --[[@as any]]
|
||||
|
||||
--- @class ExpGui_Vlayer.elements.controls.element_data
|
||||
--- @field type_dropdown LuaGuiElement
|
||||
--- @field interface_dropdown LuaGuiElement
|
||||
|
||||
--- Table of the dropdowns and buttons used to manage interfaces
|
||||
--- @class ExpGui_Vlayer.elements.controls: ExpElement
|
||||
--- @field data table<LuaGuiElement, ExpGui_Vlayer.elements.controls.element_data>
|
||||
--- @overload fun(parent: LuaGuiElement): LuaGuiElement
|
||||
Elements.controls = Gui.define("vlayer/controls")
|
||||
:track_all_elements()
|
||||
:draw(function(def, parent)
|
||||
--- @cast def ExpGui_Vlayer.elements.controls
|
||||
local controls = Gui.elements.scroll_table(parent, 400, 2, "controls")
|
||||
local player = Gui.get_player(parent)
|
||||
local can_edit = Roles.player_has_permission(player, "exp_scenario.gui.vlayer_edit")
|
||||
|
||||
local type_dropdown = Elements.type_dropdown(controls, controls)
|
||||
local interface_dropdown = Elements.interface_dropdown(controls)
|
||||
Elements.refresh_button(controls, controls)
|
||||
Elements.view_button(controls, controls)
|
||||
Elements.build_button(controls).visible = can_edit
|
||||
Elements.remove_button(controls, controls).visible = can_edit
|
||||
|
||||
def.data[controls] = {
|
||||
type_dropdown = type_dropdown,
|
||||
interface_dropdown = interface_dropdown,
|
||||
}
|
||||
Elements.controls.refresh_interfaces(controls)
|
||||
return controls
|
||||
end) --[[@as any]]
|
||||
|
||||
--- Get the selected interface type
|
||||
--- @param controls LuaGuiElement
|
||||
--- @return ExpScenario_Vlayer.InterfaceType
|
||||
function Elements.controls.get_selected_type(controls)
|
||||
return Elements.type_dropdown.get_selected(Elements.controls.data[controls].type_dropdown)
|
||||
end
|
||||
|
||||
--- Get the selected interface and its type
|
||||
--- @param controls LuaGuiElement
|
||||
--- @return ExpScenario_Vlayer.InterfaceType, LuaEntity?
|
||||
function Elements.controls.get_selected_interface(controls)
|
||||
local elements = Elements.controls.data[controls]
|
||||
local interface_type = Elements.type_dropdown.get_selected(elements.type_dropdown)
|
||||
local interfaces = Vlayer.get_interfaces()[interface_type]
|
||||
return interface_type, Elements.interface_dropdown.get_selected(elements.interface_dropdown, interfaces)
|
||||
end
|
||||
|
||||
--- Refresh the listed interfaces to match the selected type
|
||||
--- @param controls LuaGuiElement
|
||||
function Elements.controls.refresh_interfaces(controls)
|
||||
local elements = Elements.controls.data[controls]
|
||||
local interface_type = Elements.type_dropdown.get_selected(elements.type_dropdown)
|
||||
Elements.interface_dropdown.refresh(elements.interface_dropdown, Vlayer.get_interfaces()[interface_type])
|
||||
end
|
||||
|
||||
--- Refresh the listed interfaces of every online player, used when an interface is built or removed
|
||||
function Elements.controls.refresh_interfaces_online()
|
||||
for _, controls in Elements.controls:online_elements() do
|
||||
Elements.controls.refresh_interfaces(controls)
|
||||
end
|
||||
end
|
||||
|
||||
--- Refresh whether a player can see the edit buttons
|
||||
--- @param player LuaPlayer
|
||||
function Elements.controls.refresh_permissions(player)
|
||||
local can_edit = Roles.player_has_permission(player, "exp_scenario.gui.vlayer_edit")
|
||||
for _, build_button in Elements.build_button:tracked_elements(player) do
|
||||
build_button.visible = can_edit
|
||||
end
|
||||
for _, remove_button in Elements.remove_button:tracked_elements(player) do
|
||||
remove_button.visible = can_edit
|
||||
end
|
||||
end
|
||||
|
||||
--- Container added to the left gui flow
|
||||
--- @class ExpGui_Vlayer.elements.container: ExpElement
|
||||
--- @overload fun(parent: LuaGuiElement): LuaGuiElement
|
||||
Elements.container = Gui.define("vlayer/container")
|
||||
:draw(function(_, parent)
|
||||
local container = Gui.elements.container(parent, 400)
|
||||
local stats_table = Elements.stats_table(container)
|
||||
Elements.stats_table.refresh(stats_table, Elements.stats_table.calculate_display_data())
|
||||
Elements.controls(container)
|
||||
return Gui.elements.container.get_root_element(container)
|
||||
end) --[[@as any]]
|
||||
|
||||
--- Add the element to the left flow with a toolbar button
|
||||
Gui.add_left_element(Elements.container, false)
|
||||
Gui.toolbar.create_button{
|
||||
name = "toggle_vlayer",
|
||||
left_element = Elements.container,
|
||||
sprite = "entity/solar-panel",
|
||||
tooltip = { "exp-gui_vlayer.tooltip-main" },
|
||||
visible = function(player, element)
|
||||
return Roles.player_has_permission(player, "exp_scenario.gui.vlayer")
|
||||
end
|
||||
}
|
||||
|
||||
--- Replace the one marker entity in a selected area with an interface of the type the player picked
|
||||
--- @param event EventData.on_player_selected_area
|
||||
local function on_area_selected(event)
|
||||
local player = Gui.get_player(event)
|
||||
local surface = event.surface
|
||||
local area = expand_area(event.area)
|
||||
local force = player.force --[[@as LuaForce]]
|
||||
|
||||
local interface_type = "energy" --- @type ExpScenario_Vlayer.InterfaceType
|
||||
for _, controls in Elements.controls:tracked_elements(player) do
|
||||
interface_type = Elements.controls.get_selected_type(controls)
|
||||
end
|
||||
|
||||
-- Platforms have no chests, so a combinator marks the spot once the research allows it
|
||||
local marker_name = "steel-chest"
|
||||
if config.power_on_space and surface.platform and interface_type == "energy" then
|
||||
local research = config.power_on_space_research
|
||||
if force.technologies[research.name].level < research.level then
|
||||
player.print({ "exp-gui_vlayer.message-research", research.name, research.level }, Colors.orange_red)
|
||||
return
|
||||
end
|
||||
marker_name = "constant-combinator"
|
||||
end
|
||||
|
||||
local markers = surface.find_entities_filtered{ area = area, name = marker_name, force = force }
|
||||
if #markers == 0 then
|
||||
player.print({ "exp-gui_vlayer.message-no-chest" }, Colors.orange_red)
|
||||
return
|
||||
elseif #markers > 1 then
|
||||
player.print({ "exp-gui_vlayer.message-multiple", type_captions[interface_type] }, Colors.orange_red)
|
||||
return
|
||||
end
|
||||
|
||||
local marker = assert(markers[1])
|
||||
if marker.name == "steel-chest" and not assert(marker.get_inventory(defines.inventory.chest)).is_empty() then
|
||||
player.print({ "exp-gui_vlayer.message-chest-not-empty" }, Colors.orange_red)
|
||||
return
|
||||
end
|
||||
|
||||
if #Vlayer.get_interfaces()[interface_type] >= config.interface_limit[interface_type] then
|
||||
player.print({ "exp-gui_vlayer.message-limit", type_captions[interface_type] }, Colors.orange_red)
|
||||
return
|
||||
end
|
||||
|
||||
local position = marker.position
|
||||
marker.destroy()
|
||||
|
||||
if interface_type == "energy" then
|
||||
if not Vlayer.create_energy_interface(surface, position, player) then
|
||||
player.print({ "exp-gui_vlayer.message-no-space" }, Colors.orange_red)
|
||||
return
|
||||
end
|
||||
elseif interface_type == "circuit" then
|
||||
Vlayer.create_circuit_interface(surface, position, player)
|
||||
elseif interface_type == "storage_input" then
|
||||
Vlayer.create_input_interface(surface, position, player)
|
||||
else
|
||||
Vlayer.create_output_interface(surface, position, player)
|
||||
end
|
||||
|
||||
game.print{ "exp-gui_vlayer.message-built", player.name, type_captions[interface_type], gps_string(position, surface) }
|
||||
Elements.controls.refresh_interfaces_online()
|
||||
end
|
||||
|
||||
SelectArea:on_selection(on_area_selected)
|
||||
|
||||
--- Refresh the edit buttons as the permissions of a player may have changed
|
||||
--- @param event EventData.ExpRoles.on_player_roles_changed
|
||||
local function refresh_player_permissions(event)
|
||||
Elements.controls.refresh_permissions(Gui.get_player(event))
|
||||
end
|
||||
|
||||
return {
|
||||
elements = Elements,
|
||||
events = {
|
||||
[Roles.events.on_player_roles_changed] = refresh_player_permissions,
|
||||
},
|
||||
on_nth_tick = {
|
||||
[config.update_tick_gui] = Elements.stats_table.refresh_online,
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -31,7 +31,7 @@ description-pollution-enabled=Set polution enabled state for this game.
|
||||
description-game-speed=Set or get the current game speed.
|
||||
arg-state=State to set, default is to toggle the current value.
|
||||
arg-player=Player to toggle cheat mode for.
|
||||
arg-force-friendly-fire=Force to research all technology for.
|
||||
arg-force-friendly-fire=Force to toggle friendly fire.
|
||||
arg-force-research=Force to research all technology for.
|
||||
arg-surface=Surface to toggle always day for.
|
||||
arg-amount=Amount to set, returns the current value if not given.
|
||||
@@ -150,16 +150,16 @@ arg-items-per-second=Target items per second, defaults to output of a single mac
|
||||
not-selecting=Please select an entity with a recipe.
|
||||
item-in=You need __1__ per second of [item=__2__].
|
||||
fluid-in=You need __1__ per second of [fluid=__2__].
|
||||
item-out=This will result in: __1__ [item=__2__] per second.
|
||||
fluid-out=This will result in: __1__ [fluid=__2__] per second.
|
||||
item-out=This will result in: __1__ [item=__2__] per second.
|
||||
fluid-out=This will result in: __1__ [fluid=__2__] per second.
|
||||
machine-count=And you will need __1__ machines (with the same speed as this one) for this.
|
||||
|
||||
[exp-commands_repair]
|
||||
selection-name=Repair
|
||||
description=Repairs entities on your force around you.
|
||||
# Intentional space left on the two lines below so they can be joined together.
|
||||
response-revive=__1__ entities were revived.
|
||||
response-heal=__1__ entities were healed.
|
||||
response-revive=__1__ entities were revived.
|
||||
response-heal=__1__ entities were healed.
|
||||
|
||||
[exp-commands_reports]
|
||||
description-create=Reports a player and notifies moderators
|
||||
@@ -480,6 +480,70 @@ caption-create-footer=Create task
|
||||
caption-edit-footer=Edit task
|
||||
caption-view-footer=Task details
|
||||
|
||||
[exp-gui_vlayer]
|
||||
tooltip-main=Virtual Layer
|
||||
selection-name=vlayer
|
||||
caption-stat-solar=[img=entity/solar-panel] Solar Panels
|
||||
tooltip-stat-solar=Solar panels in use out of those stored
|
||||
caption-stat-accumulator=[img=entity/accumulator] Accumulators
|
||||
tooltip-stat-accumulator=Accumulators in use out of those stored
|
||||
caption-stat-surface-area=[virtual-signal=signal-R] Remaining Surface
|
||||
tooltip-stat-surface-area=Remaining surface area, insert landfill to increase it
|
||||
caption-stat-sustained=[virtual-signal=signal-S] Sustained Production
|
||||
tooltip-stat-sustained=Power production averaged over a day, out of what every stored solar panel could give
|
||||
caption-stat-production=[virtual-signal=signal-P] Current Production
|
||||
tooltip-stat-production=Power production right now, out of the production in full daylight
|
||||
caption-stat-capacity=[virtual-signal=signal-E] Battery Charge
|
||||
tooltip-stat-capacity=Stored energy out of the accumulator capacity
|
||||
type-energy=Energy
|
||||
type-circuit=Circuit
|
||||
type-storage-input=Storage Input
|
||||
type-storage-output=Storage Output
|
||||
caption-refresh=Refresh List
|
||||
caption-view=View Interface
|
||||
caption-build=Build Interface
|
||||
caption-remove=Remove Interface
|
||||
message-located=The __1__ interface is at __2__
|
||||
message-built=__1__ built a __2__ interface at __3__
|
||||
message-removed=__1__ removed a __2__ interface at __3__
|
||||
message-no-chest=No steel chest was selected
|
||||
message-chest-not-empty=The steel chest is not empty
|
||||
message-multiple=Cannot build a __1__ interface, more than one steel chest was selected
|
||||
message-limit=Cannot build a __1__ interface, the limit has been reached
|
||||
message-no-space=Cannot build an energy interface here, there is not enough space
|
||||
message-research=You need to research [technology=__1__] to level __2__ to use the vlayer on a space platform
|
||||
|
||||
[exp-gui_warp-list]
|
||||
caption-main=Warp List [img=info]
|
||||
tooltip-main=Warp List
|
||||
tooltip-sub=Warps can only be used every __1__ seconds and when within __2__ tiles\n__3__\n__4__\n__5__\n__6__\n__7__\n__8__
|
||||
tooltip-sub-current= - __1__ This is your current warp point;
|
||||
tooltip-sub-connected= - __1__ You can travel to this warp point;
|
||||
tooltip-sub-different= - __1__ This warp is on a different network;
|
||||
tooltip-sub-cooldown= - __1__ You are currently on cooldown;
|
||||
tooltip-sub-not-available= - __1__ You are not in range of a warp point;
|
||||
tooltip-sub-bypass= - __1__ Your role allows you to travel here;
|
||||
tooltip-add=Add new warp
|
||||
tooltip-add-too-close=Cannot create warp; too close to existing warp point: __1__
|
||||
add-warning-water=Cannot create warp; too close to water, please move __1__ tiles away from the water body or landfill it
|
||||
add-warning-entities=Cannot create warp; too close to other entities, please move __1__ tiles away from the entities or remove them
|
||||
tooltip-icon=Edit warp icon
|
||||
tooltip-last-edit=Last edited by __1__ at __2__\nClick to view on map
|
||||
tooltip-confirm=Save changes
|
||||
tooltip-discard=Discard changes
|
||||
tooltip-edit=Currently being edited by: __1__
|
||||
tooltip-edit-none=Currently being edited by: Nobody
|
||||
tooltip-remove=Remove warp
|
||||
tooltip-cooldown=Please wait __1__ seconds before you can warp
|
||||
tooltip-cooldown-none=After each warp you will need to wait __1__ seconds before you can warp again
|
||||
tooltip-goto=Go to x __1__ y __2__
|
||||
tooltip-goto-bypass=Go to x __1__ y __2__, bypass mode
|
||||
tooltip-goto-bypass-different-network=Go to x __1__ y __2__, bypass mode different network
|
||||
tooltip-goto-same-warp=You are already on this warp
|
||||
tooltip-goto-different-network=This warp is on a different network, use power poles to connect it
|
||||
tooltip-goto-cooldown=You are on cooldown, wait for your cooldown to recharge
|
||||
tooltip-goto-disabled=You are not on a warp point, walk to a warp point
|
||||
|
||||
[exp_afk-kick]
|
||||
kick-message=All players were kicked because everyone was AFK.
|
||||
|
||||
@@ -490,30 +554,30 @@ reply-online=There are __1__ players online
|
||||
reply-players=There have been __1__ players on this map
|
||||
reply-dev=Cooldude2606 is a dev for this server and makes the scenario and is not a factorio dev.
|
||||
reply-softmod=A softmod is a custom scenario that runs on this server, an example is the player list.
|
||||
reply-clusterio=Clusterio is a factorio server hosting platform allowing internet enabled mods.
|
||||
reply-clusterio=Clusterio is a factorio server hosting platform allowing internet enabled mods.
|
||||
reply-blame=Blame __1__ for what just happened!
|
||||
reply-afk=You're afk? Look at __1__, that player has been afk for: __2__
|
||||
reply-evolution=Current evolution factor is __1__
|
||||
reply-magic=Fear the admin magic (ノ゚∀゚)ノ⌒・*:.。. .。.:*・゜゚・*☆
|
||||
reply-aids=≖ ‿ ≖ Fear the aids of a public server ≖ ‿ ≖
|
||||
reply-riot=(admins) ┬┴┬┴┤ᵒ_ᵒ)├┬┴┬┴ ‹ ‹\(´ω` )/››‹‹\ ( ´)/››‹‹\ ( ´ω`)/›› (rest of server)
|
||||
reply-loops=NO LOOPS; LOOPS ARE BAD; JUST NO LOOPS!!!!!; IF YOU MAKE A LOOP.... IT WILL NOT END WELL!!!!!!!
|
||||
reply-loops=NO LOOPS; LOOPS ARE BAD; JUST NO LOOPS!!!; IF YOU MAKE A LOOP… IT WILL NOT END WELL!!!
|
||||
reply-lenny=( ͡° ͜ʖ ͡°)
|
||||
reply-hodor=Hodor
|
||||
reply-popcorn-1=Heating the oil and waiting for the popping sound...
|
||||
reply-popcorn-1=Heating the oil and waiting for the popping sound…
|
||||
reply-popcorn-2=__1__ your popcorn is finished. Lean backwards and watch the drama unfold.
|
||||
reply-snaps-1=Pouring the glasses and finding the correct song book...
|
||||
reply-snaps-2=Singing a song...🎤🎶
|
||||
reply-snaps-1=Pouring the glasses and finding the correct song book…
|
||||
reply-snaps-2=Singing a song…🎤🎶
|
||||
reply-snaps-3=schkål, my friends!
|
||||
reply-cocktail-1= 🍸 Inintiating mind reading unit... 🍸
|
||||
reply-cocktail-1= 🍸 Inintiating mind reading unit… 🍸
|
||||
reply-cocktail-2= 🍸 Mixing favourite ingredients of __1__ 🍸
|
||||
reply-cocktail-3=🍸 __1__ your cocktail is done.🍸
|
||||
reply-coffee-1= ☕ Boiling the water and grinding the coffee beans... ☕
|
||||
reply-coffee-1= ☕ Boiling the water and grinding the coffee beans… ☕
|
||||
reply-coffee-2= ☕ __1__ we ran out of coffe beans! Have some tea instead. ☕
|
||||
reply-pizza-1= 🍕 Finding nearest pizza supplier... 🍕
|
||||
reply-pizza-1= 🍕 Finding nearest pizza supplier… 🍕
|
||||
reply-pizza-2= 🍕 Figuring out the favourite pizza of __1__ 🍕
|
||||
reply-pizza-3= 🍕 __1__ your pizza is here! 🍕
|
||||
reply-tea-1= ☕ Boiling the water... ☕
|
||||
reply-tea-1= ☕ Boiling the water… ☕
|
||||
reply-tea-2= ☕ __1__ your tea is done! ☕
|
||||
reply-mead-1= Filling the drinking horn
|
||||
reply-mead-2= Skål!
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[exp-commands-authorities_role]
|
||||
deny=你沒有權限運行該項目
|
||||
deny=你沒有權限運行該項目。
|
||||
|
||||
[exp-commands-parse_role]
|
||||
lower-role=用戶組比你現有的更高
|
||||
lower-role-player=用戶有更高權限
|
||||
lower-role=用戶組比你現有的更高。
|
||||
lower-role-player=用戶有更高權限。
|
||||
|
||||
[exp-commands_admin-chat]
|
||||
description=一個只有管理員能看到的對話。
|
||||
@@ -12,31 +12,31 @@ format=[AC] __1__: __2__
|
||||
|
||||
[exp-commands_artillery]
|
||||
selection-name=火炮
|
||||
description=火炮遙控
|
||||
invalid-area=所選地區無效
|
||||
description=火炮遙控。
|
||||
invalid-area=所選地區無效。
|
||||
|
||||
[exp-commands_bot-queue]
|
||||
description=取得或更改建設隊列數量。
|
||||
arg-amount=建設隊列數量
|
||||
arg-amount=建設隊列數量。
|
||||
get=建設隊列為 __1__ 個成功嘗試,和 __2__ 個嘗試。
|
||||
set=__1__ 把建設隊列改成 __2__ 個成功嘗試,和 __3__ 個嘗試。
|
||||
|
||||
[exp-commands_cheat]
|
||||
description-cheat-mode=為你或其他玩家切換作弊模式。
|
||||
description-always-day=啟用永久白天
|
||||
description-friendly-fire=啟用友方傷害
|
||||
description-research-all=啟用所有科技
|
||||
description-clear-pollution=清除污染
|
||||
description-pollution-enabled=啟用污染
|
||||
description-game-speed=設定遊戲速度
|
||||
arg-state=狀態
|
||||
arg-player=用戶
|
||||
arg-force-friendly-fire=勢力
|
||||
arg-force-research=勢力
|
||||
arg-surface=位面
|
||||
arg-amount=數量
|
||||
description-always-day=啟用永久白天。
|
||||
description-friendly-fire=啟用友方傷害。
|
||||
description-research-all=啟用所有科技。
|
||||
description-clear-pollution=清除污染。
|
||||
description-pollution-enabled=啟用污染。
|
||||
description-game-speed=設定遊戲速度。
|
||||
arg-state=要設定的狀態,預設是切換目前值。
|
||||
arg-player=要為其切換作弊模式的玩家。
|
||||
arg-force-friendly-fire=為勢力切換友方傷害。
|
||||
arg-force-research=為勢力研究所有科技。
|
||||
arg-surface=為位面切換永久白晝。
|
||||
arg-amount=要設定的數量,如果未指定,則傳回目前值。
|
||||
cheat-mode=作弊模式現在為 __1__
|
||||
always-day=__1__ 把在 __2__ 的位面的永久白天設置為 __3__ 。
|
||||
always-day=__1__ 把在 __2__ 的位面的永久白晝設置為 __3__ 。
|
||||
friendly-fire=__1__ 已為在 __2__ 的勢力設定成 __3__ 。
|
||||
research-all=__1__ 已為在 __2__ 的勢力啟用所有科技 。
|
||||
clear-pollution=__1__ 已為在 __2__ 的位面清除所有污染 。
|
||||
@@ -46,15 +46,15 @@ game-speed-get=現在遊戲速度為 __1__ 。
|
||||
|
||||
[exp-commands_clear-inventory]
|
||||
description=清除用戶物品。
|
||||
arg-player=用戶
|
||||
arg-player=要清除其物品欄的用戶。
|
||||
|
||||
[exp-commands_connect]
|
||||
description=連接到另一服務器
|
||||
description-player=讓其他用戶連接到另一服務器
|
||||
description-all=將所有玩家連接到另一台服務器
|
||||
arg-player=連接到別的服的用戶
|
||||
arg-server=輸入為地址 / 名稱
|
||||
arg-is-address=輸入為地址
|
||||
description=連接到另一服務器。
|
||||
description-player=讓其他用戶連接到另一服務器。
|
||||
description-all=將所有玩家連接到另一台服務器。
|
||||
arg-player=連接到別的服務器的用戶。
|
||||
arg-server=輸入為地址或名稱。
|
||||
arg-is-address=輸入應該為地址。
|
||||
too-many-matching=該服務器名稱有多過一個結果: __1__
|
||||
wrong-version=該服務器版本不同: __1__
|
||||
same-server=你已連接該服務器: __1__
|
||||
@@ -65,15 +65,15 @@ none-matching=沒找到服務器。
|
||||
description=打開偵錯介面。
|
||||
|
||||
[exp-commands_enemy]
|
||||
description-kill=僅移除蟲子
|
||||
description-remove=移除蟲子及停止產生
|
||||
description-kill=僅移除蟲單位。
|
||||
description-remove=移除蟲單位及停止生產器。
|
||||
arg-surface=位面
|
||||
|
||||
[exp-commands_home]
|
||||
description-home=傳送回家
|
||||
description-set=設定目前位置為家
|
||||
description-get=取得家的位置
|
||||
description-return=傳送回上一個位置
|
||||
description-home=傳送回家。
|
||||
description-set=設定目前位置為家。
|
||||
description-get=取得家的位置。
|
||||
description-return=傳送回上一個位置。
|
||||
no-home=你在 __1__ 上還沒設回程傳送點。
|
||||
no-return=你還沒在 __1__ 上使用過回程。
|
||||
home-set=你的回程傳送點為 __1__ 上, x: __2__ y: __3__
|
||||
@@ -84,7 +84,7 @@ home-get=你的回程傳送點在 __1__ 上, x: __2__ y: __3__
|
||||
description=把用戶送監而禁止其行動。
|
||||
arg-player=用戶
|
||||
arg-reason=原因
|
||||
jailed=__1__ 已被 __2__ ,因 __3__ 而被禁止行動。
|
||||
jailed=__1__ 已被 __2__ ,因 __3__ 而被禁止行動
|
||||
already-jailed=__1__ 已被禁止行動。
|
||||
|
||||
[exp-commands_unjail]
|
||||
@@ -101,18 +101,18 @@ lower-role=用戶組比你現有的更高
|
||||
|
||||
[exp-commands_lawnmower]
|
||||
selection-name=割草機
|
||||
description=清理屍體、裝飾和核洞
|
||||
description=清理屍體、裝飾和核彈洞。
|
||||
arg-range=半徑
|
||||
complete=__1__ 格已完成。
|
||||
|
||||
[exp-commands_locate]
|
||||
description=在地圖上查找用戶
|
||||
description=在地圖上查找用戶的最後位置。
|
||||
arg-player=用戶
|
||||
arg-remote=若在使用地圖查看模式
|
||||
arg-remote=若為 true,則開啟在玩家遠端視圖的位置。
|
||||
response=__1__ 的最後地點是 [gps=__2__,__3__]
|
||||
|
||||
[exp-commands_me]
|
||||
description=在聊天中發送操作訊息
|
||||
description=在聊天中發送操作訊息。
|
||||
arg-action=你想做的動作。
|
||||
response=* __1__ __2__ *
|
||||
|
||||
@@ -121,17 +121,19 @@ description-entity=啟用建築保護
|
||||
description-area=啟用區域保護
|
||||
enter-entity=現在進入建築保護,選擇一個建築來保護,按住 shift 來選擇則可取消。
|
||||
enter-area=現在進入區域保護,選擇一個區域來保護,按住 shift 來選擇則可取消。
|
||||
exit-entity=已進入建築保護
|
||||
exit-area=已進入區域選擇
|
||||
exit-entity=已進入建築保護。
|
||||
exit-area=已進入區域選擇。
|
||||
protected-entities=__1__ 個建築現已受保護。
|
||||
unprotected-entities=__1__ 個建築現不受保護。
|
||||
already-protected=本地區已受保護。
|
||||
protected-area=本地區現已受保護。
|
||||
unprotected-area=本地區現不受保護。
|
||||
repeat-offence=__1__ 在 [gps=__3__,__4__] 拆除了 __2__
|
||||
enter-entity-selection = Entered entity selection, select entites to protect, hold shift to remove protection.
|
||||
enter-area-selection = Entered area selection, select areas to protect, hold shift to remove protection.
|
||||
|
||||
[exp-commands_tag-protection]
|
||||
description=切換管理員標記模式,新標記只能由管理員編輯
|
||||
description=切換管理員標記模式,新標記只能由管理員編輯。
|
||||
exit=你已離開管理員地圖標記模式,所有新地圖標記將不受保護。
|
||||
enter=你已進入管理員地圖標記模式,所有新地圖標記將受保護。
|
||||
create=你放了一個管理員地圖標記。
|
||||
@@ -153,6 +155,7 @@ fluid-in=你每秒需要 __1__ 來做 [fluid=__2__]。
|
||||
item-out=結果: __1__ [item=__2__] 每秒。
|
||||
fluid-out=結果: __1__ [fluid=__2__] 每秒。
|
||||
machines=你需要 __1__ 部同樣的機器。
|
||||
machine-count = And you will need __1__ machines (with the same speed as this one) for this.
|
||||
|
||||
[exp-commands_repair]
|
||||
selection-name=維修
|
||||
@@ -172,11 +175,21 @@ arg-player-clear=用戶
|
||||
arg-reason=原因
|
||||
arg-from-player=用戶
|
||||
player-immune=用戶不能被舉報
|
||||
self-report=你不能舉報自己
|
||||
self-report=你不能舉報自己。
|
||||
response=__1__ 因 __2__ 被舉報了。
|
||||
response-admin=__1__ 因 __3__ 被 __2__ 舉報了。
|
||||
already-reported=你已經舉報過該用戶了,你可以讓管理員清除報告來重寫。
|
||||
not-reported=該用戶沒有被舉報的紀錄。
|
||||
not-reported-by=該用戶沒有被 __1__ 舉報的紀錄。
|
||||
reports-title==該用戶現在的被舉報紀錄:
|
||||
player-title=__1__ 有 __2__ 項被舉報的紀錄:
|
||||
list-element=__1__: __2__
|
||||
removed-all=__1__ 被舉報的所有紀錄已被 __2__ 清除。
|
||||
removed=__1__ 被舉報的一個紀錄已被 __2__ 清除。
|
||||
|
||||
[exp-commands_roles]
|
||||
description-assign=為用戶指配用戶組
|
||||
description-unassign=為用戶取消指配用戶組
|
||||
description-assign=為用戶指配用戶組。
|
||||
description-unassign=為用戶取消指配用戶組。
|
||||
description-get=顯示所有用戶組
|
||||
arg-player-assign=指配
|
||||
arg-player-unassign=取消指配
|
||||
@@ -199,16 +212,16 @@ title=有 [item=__1__] 的用戶:
|
||||
result=\n__1__) __2__ 有 __3__ 個。 (__4__)
|
||||
|
||||
[exp-commands_spectate]
|
||||
description-spectate=切換旁觀者模式
|
||||
description-follow=開始在旁觀者模式中關注玩家
|
||||
description-spectate=切換旁觀者模式。
|
||||
description-follow=開始在旁觀者模式中關注玩家。
|
||||
arg-player=用戶
|
||||
follow-self=你不能追蹤自己
|
||||
follow-self=你不能追蹤自己。
|
||||
|
||||
[exp-commands_surface]
|
||||
selection-name=位面
|
||||
description-items=清除地面上的物品
|
||||
description-blueprints=清除藍圖
|
||||
description-blueprints-surface=清除藍圖
|
||||
description-items=清除地面上的物品。
|
||||
description-blueprints=清除藍圖。
|
||||
description-blueprints-surface=清除本位面上的藍圖。
|
||||
arg-surface=位面
|
||||
items=__1__ 清除了所有在 __2__ 中掉地上的東西。
|
||||
blueprints=__1__ 清除了所有在 __2__ 中在地上的藍圖。
|
||||
@@ -225,9 +238,9 @@ response=__1__ 把 __2__ 個火車設置為自動模式。
|
||||
description-teleport=將一名玩家傳送到另一名玩家。
|
||||
description-bring=將一名玩家傳送到你身邊。
|
||||
description-goto=將你傳送到玩家身邊。
|
||||
description-spawn=傳送到出生點。
|
||||
description-spawn=傳送到出生點
|
||||
arg-player-teleport=傳送的玩家,若沒有目標,你會被傳送到這。
|
||||
arg-player-from=傳送的玩家
|
||||
arg-player-from=傳送的玩家。
|
||||
arg-player-to=傳送的玩家目標
|
||||
unavailable=沒找到空間進行傳送
|
||||
same-player=沒辦法傳送到自己
|
||||
@@ -379,6 +392,7 @@ data-statistics=數據
|
||||
data-required=需要
|
||||
data-misc=雜項
|
||||
data-format=__1____2__
|
||||
welcome-title = Welcome to __1__
|
||||
|
||||
[exp-gui_research-milestones]
|
||||
tooltip-main=研究介面
|
||||
@@ -410,7 +424,7 @@ data-caption-fastest-launch=最快發射
|
||||
data-tooltip-fastest-launch=最快發射時間
|
||||
data-caption-total-rockets=總發射次數
|
||||
data-tooltip-total-rockets=我方總發射次數
|
||||
value-tooltip-total-rockets=__1__% 的火箭發射都在本地圖上。
|
||||
value-tooltip-total-rockets=__1__% 的火箭發射都在這張地圖上
|
||||
data-caption-avg-launch=平均時間
|
||||
data-tooltip-avg-launch=火箭發射平均時間
|
||||
data-caption-avg-launch-n=平均時間 __1__
|
||||
@@ -453,8 +467,8 @@ tooltip-cursor-empty=游標為空
|
||||
caption-main=工作流程 [img=info]
|
||||
tooltip-main=工作流程
|
||||
tooltip-sub=需要完成的工作流程\n- 你可以用富文本來加上圖片 [img=utility/not_enough_repair_packs_icon] 或 [color=blue] 顏色[/color]。
|
||||
caption-no-tasks=沒有工作流程
|
||||
tooltip-no-tasks=按加號加入工作流程
|
||||
caption-no-tasks=沒有工作流程!
|
||||
tooltip-no-tasks=按加號加入工作流程!
|
||||
tooltip-last-edit=最後由 __1__ 在 __2__ 修改
|
||||
tooltip-close=關閉工作流程
|
||||
tooltip-new=加入工作流程
|
||||
@@ -470,41 +484,106 @@ tooltip-edit-none=現沒有被人修改
|
||||
caption-create-footer=加入工作流程
|
||||
caption-edit-footer=修改工作流程
|
||||
caption-view-footer=工作流程細節
|
||||
tooltip-confirm = Save changes (minimum of 5 characters long)
|
||||
|
||||
[exp-gui_warp-list]
|
||||
caption-main=傳送陣清單 [img=info]
|
||||
tooltip-main=傳送陣清單
|
||||
tooltip-sub=傳送陣在 __1__ 秒內只能使用一次。 亦需在 __2__ 格之內使用\n__3__\n__4__\n__5__\n__6__\n__7__\n__8__
|
||||
tooltip-sub-current= - __1__ 你目前的傳送陣;
|
||||
tooltip-sub-connected= - __1__ 你可以到本傳送陣;
|
||||
tooltip-sub-different= - __1__ 該傳送陣未連接;
|
||||
tooltip-sub-cooldown= - __1__ 傳送冷卻中;
|
||||
tooltip-sub-not-available= - __1__ 你不在傳送陣範圍;
|
||||
tooltip-sub-bypass= - __1__ 你可以傳送到這裏;
|
||||
tooltip-add=新建傳送陣
|
||||
tooltip-add-too-close=無法新建傳送陣; 太接近 __1__ 了
|
||||
add-warning-water=無法新建傳送陣; 太接近水源了, 請移動 __1__ 格又或者填平該地方
|
||||
add-warning-entities=無法新建傳送陣; 太接近其他東西, 請移動 __1__ 格又或者拆除該東西
|
||||
tooltip-icon=修改傳送陣圖案
|
||||
tooltip-last-edit=最後由 __1__ 在 __2__ 更改\n按這在地圖上看
|
||||
tooltip-confirm=儲存更改
|
||||
tooltip-discard=放棄更改
|
||||
tooltip-edit=現被 __1__ 修改中
|
||||
tooltip-edit-none=現沒有被人修改
|
||||
tooltip-remove=拆除傳送陣
|
||||
tooltip-cooldown=請等 __1__ 秒才再次傳送
|
||||
tooltip-cooldown-none=你要等 __1__ 秒才可以再次傳送
|
||||
tooltip-goto=到 X __1__ Y __2__
|
||||
tooltip-goto-bypass=到 X __1__ Y __2__, 繞行模式
|
||||
tooltip-goto-bypass-different-network=到 X __1__ Y __2__, 繞行模式 (不同網絡)
|
||||
tooltip-goto-same-warp=你已在該傳送陣
|
||||
tooltip-goto-different-network=該傳送陣未連接, 你可使用電纜連接
|
||||
tooltip-goto-cooldown=傳送冷卻中, 請等候冷卻
|
||||
tooltip-goto-disabled=你不在傳送陣, 請移至任意傳送陣
|
||||
|
||||
[exp-gui_vlayer]
|
||||
tooltip-main=Vlayer 介面
|
||||
selection-name=vlayer
|
||||
caption-stat-solar=[img=entity/solar-panel] 太陽能板
|
||||
tooltip-stat-solar=使用中的太陽能板數量 / 儲存的太陽能板數量
|
||||
caption-stat-accumulator=[img=entity/accumulator] 蓄電池
|
||||
tooltip-stat-accumulator=使用中的蓄電池數量 / 儲存的蓄電池數量
|
||||
caption-stat-surface-area=[virtual-signal=signal-R] 餘下土地
|
||||
tooltip-stat-surface-area=餘下土地。 放入堆填來增加
|
||||
caption-stat-sustained=[virtual-signal=signal-S] 持續產能
|
||||
tooltip-stat-sustained=持續電力產能
|
||||
caption-stat-production=[virtual-signal=signal-P] 現時產能
|
||||
tooltip-stat-production=現時電力產能
|
||||
caption-stat-capacity=[virtual-signal=signal-E] 電池電量
|
||||
tooltip-stat-capacity=現時電池電量 / 電池最大容量
|
||||
type-energy=電力
|
||||
type-circuit=回路
|
||||
type-storage-input=放入箱
|
||||
type-storage-output=提取箱
|
||||
caption-refresh=刷新列表
|
||||
caption-view=查看介面
|
||||
caption-build=建造介面
|
||||
caption-remove=拆除介面
|
||||
message-located=__1__ 介面在 __2__
|
||||
message-built=__1__ 在 __3__ 建造了 __2__ 介面
|
||||
message-removed=__1__ 在 __3__ 拆除了 __2__ 介面
|
||||
message-no-chest=沒檢測到鋼箱
|
||||
message-chest-not-empty=鋼箱不是空的
|
||||
message-multiple=無法建造 __1__ 介面,檢測到多個鋼箱
|
||||
message-limit=無法建造 __1__ 介面,超出最大限制
|
||||
message-no-space=無法在此建造電力介面,空間不足
|
||||
message-research=你要研究 [technology=__1__] 在 __2__ 級 才可使用太空平台 vlayer 功能。
|
||||
|
||||
[exp_afk-kick]
|
||||
kick-message=因地圖中沒有活躍玩家,所以所有人都已被請離。
|
||||
|
||||
[exp_chat-auto-reply]
|
||||
chat-reply=[Compilatron] __1__
|
||||
chat-disallowed=你沒有權限使用這個指令。
|
||||
reply-online=現在有 __1__ 人上線。
|
||||
reply-players=本地圖現在有 __1__ 人曾上線。
|
||||
reply-dev=Cooldude2606 只是本場境的開發者
|
||||
chat-disallowed=You can't use global chat commands
|
||||
reply-online=現在有 __1__ 人在線
|
||||
reply-players=這張地圖上共有__1__名用戶
|
||||
reply-dev=Cooldude2606 is a dev for this server and makes the scenario and is not a factorio dev.
|
||||
reply-softmod=這裹用了自設情境。
|
||||
reply-clusterio=Clusterio is a factorio server hosting platform allowing internet enabled mods.
|
||||
reply-blame=責怪 __1__ 吧。
|
||||
reply-afk=看看 __1__, 他已掛機 __2__ 。
|
||||
reply-evolution=現在敵人進化度為 __1__ 。
|
||||
reply-blame=責怪 __1__ 吧!
|
||||
reply-afk=看看 __1__,他的掛機時長為 __2__
|
||||
reply-evolution=現在敵人進化度為 __1__
|
||||
reply-magic=Fear the admin magic (ノ゚∀゚)ノ⌒・*:.。. .。.:*・゜゚・*☆
|
||||
reply-aids=≖ ‿ ≖ Fear the aids of a public server ≖ ‿ ≖
|
||||
reply-riot=(admins) ┬┴┬┴┤ᵒ_ᵒ)├┬┴┬┴ ‹ ‹\(´ω` )/››‹‹\ ( ´)/››‹‹\ ( ´ω`)/›› (rest of server)
|
||||
reply-loops=架設迴旋處最終後果都不好。
|
||||
reply-loops=NO LOOPS; LOOPS ARE BAD; JUST NO LOOPS!!!; IF YOU MAKE A LOOP… IT WILL NOT END WELL!!!
|
||||
reply-lenny=( ͡° ͜ʖ ͡°)
|
||||
reply-hodor=Hodor
|
||||
reply-popcorn-1=Heating the oil and waiting for the popping sound...
|
||||
reply-popcorn-1=Heating the oil and waiting for the popping sound…
|
||||
reply-popcorn-2=__1__ your popcorn is finished. Lean backwards and watch the drama unfold.
|
||||
reply-snaps-1=Pouring the glasses and finding the correct song book...
|
||||
reply-snaps-2=Singing a song...🎤🎶
|
||||
reply-snaps-1=Pouring the glasses and finding the correct song book…
|
||||
reply-snaps-2=Singing a song…🎤🎶
|
||||
reply-snaps-3=schkål, my friends!
|
||||
reply-cocktail-1= 🍸 Inintiating mind reading unit... 🍸
|
||||
reply-cocktail-1= 🍸 Inintiating mind reading unit… 🍸
|
||||
reply-cocktail-2= 🍸 Mixing favourite ingredients of __1__ 🍸
|
||||
reply-cocktail-3=🍸 __1__ your cocktail is done.🍸
|
||||
reply-coffee-1= ☕ Boiling the water and grinding the coffee beans... ☕
|
||||
reply-coffee-1= ☕ Boiling the water and grinding the coffee beans… ☕
|
||||
reply-coffee-2= ☕ __1__ we ran out of coffe beans! Have some tea instead. ☕
|
||||
reply-pizza-1= 🍕 Finding nearest pizza supplier... 🍕
|
||||
reply-pizza-1= 🍕 Finding nearest pizza supplier… 🍕
|
||||
reply-pizza-2= 🍕 Figuring out the favourite pizza of __1__ 🍕
|
||||
reply-pizza-3= 🍕 __1__ your pizza is here! 🍕
|
||||
reply-tea-1= ☕ Boiling the water... ☕
|
||||
reply-tea-1= ☕ Boiling the water… ☕
|
||||
reply-tea-2= ☕ __1__ your tea is done! ☕
|
||||
reply-mead-1= Filling the drinking horn
|
||||
reply-mead-2= Skål!
|
||||
@@ -536,3 +615,22 @@ chat-jailed=__1__ 因被多次拆除受保護物體而被禁止行動。請等
|
||||
|
||||
[exp_report-jail]
|
||||
chat-jailed=__1__ 因被多次舉報而被禁止行動。請等候管理員作出下一步處理。
|
||||
|
||||
|
||||
[exp-gui_player-list]
|
||||
main-tooltip = Player List
|
||||
open-action-bar = Options
|
||||
close-action-bar = Close Options
|
||||
reason-confirm = Confirm Reason
|
||||
reason-entry = Enter Reason
|
||||
goto-player = Goto player
|
||||
bring-player = Bring player
|
||||
report-player = Report player
|
||||
jail-player = Jail player
|
||||
kick-player = Kick player
|
||||
ban-player = Ban player
|
||||
afk-time = __1__% of total map time\nLast moved __2__ ago
|
||||
open-map = __1__ __2__\n__3__\nClick to open map
|
||||
|
||||
[exp_spectate]
|
||||
follow-label = Following __1__.\nClick here or press escape to stop following.
|
||||
@@ -1,9 +1,9 @@
|
||||
[exp-commands-authorities_role]
|
||||
deny=你沒有權限運行該項目
|
||||
deny=你沒有權限運行該項目。
|
||||
|
||||
[exp-commands-parse_role]
|
||||
lower-role=用戶組比你現有的更高
|
||||
lower-role-player=用戶有更高權限
|
||||
lower-role=用戶組比你現有的更高。
|
||||
lower-role-player=用戶有更高權限。
|
||||
|
||||
[exp-commands_admin-chat]
|
||||
description=一個只有管理員能看到的對話。
|
||||
@@ -12,31 +12,31 @@ format=[AC] __1__: __2__
|
||||
|
||||
[exp-commands_artillery]
|
||||
selection-name=火炮
|
||||
description=火炮遙控
|
||||
invalid-area=所選地區無效
|
||||
description=火炮遙控。
|
||||
invalid-area=所選地區無效。
|
||||
|
||||
[exp-commands_bot-queue]
|
||||
description=取得或更改建設隊列數量。
|
||||
arg-amount=建設隊列數量
|
||||
arg-amount=建設隊列數量。
|
||||
get=建設隊列為 __1__ 個成功嘗試,和 __2__ 個嘗試。
|
||||
set=__1__ 把建設隊列改成 __2__ 個成功嘗試,和 __3__ 個嘗試。
|
||||
|
||||
[exp-commands_cheat]
|
||||
description-cheat-mode=為你或其他玩家切換作弊模式。
|
||||
description-always-day=啟用永久白天
|
||||
description-friendly-fire=啟用友方傷害
|
||||
description-research-all=啟用所有科技
|
||||
description-clear-pollution=清除污染
|
||||
description-pollution-enabled=啟用污染
|
||||
description-game-speed=設定遊戲速度
|
||||
arg-state=狀態
|
||||
arg-player=用戶
|
||||
arg-force-friendly-fire=勢力
|
||||
arg-force-research=勢力
|
||||
arg-surface=位面
|
||||
arg-amount=數量
|
||||
description-always-day=啟用永久白天。
|
||||
description-friendly-fire=啟用友方傷害。
|
||||
description-research-all=啟用所有科技。
|
||||
description-clear-pollution=清除污染。
|
||||
description-pollution-enabled=啟用污染。
|
||||
description-game-speed=設定遊戲速度。
|
||||
arg-state=要設定的狀態,預設是切換目前值。
|
||||
arg-player=要為其切換作弊模式的玩家。
|
||||
arg-force-friendly-fire=為勢力切換友方傷害。
|
||||
arg-force-research=為勢力研究所有科技。
|
||||
arg-surface=為位面切換永久白晝。
|
||||
arg-amount=要設定的數量,如果未指定,則傳回目前值。
|
||||
cheat-mode=作弊模式現在為 __1__
|
||||
always-day=__1__ 把在 __2__ 的位面的永久白天設置為 __3__ 。
|
||||
always-day=__1__ 把在 __2__ 的位面的永久白晝設置為 __3__ 。
|
||||
friendly-fire=__1__ 已為在 __2__ 的勢力設定成 __3__ 。
|
||||
research-all=__1__ 已為在 __2__ 的勢力啟用所有科技 。
|
||||
clear-pollution=__1__ 已為在 __2__ 的位面清除所有污染 。
|
||||
@@ -46,15 +46,15 @@ game-speed-get=現在遊戲速度為 __1__ 。
|
||||
|
||||
[exp-commands_clear-inventory]
|
||||
description=清除用戶物品。
|
||||
arg-player=用戶
|
||||
arg-player=要清除其物品欄的用戶。
|
||||
|
||||
[exp-commands_connect]
|
||||
description=連接到另一服務器
|
||||
description-player=讓其他用戶連接到另一服務器
|
||||
description-all=將所有玩家連接到另一台服務器
|
||||
arg-player=連接到別的服的用戶
|
||||
arg-server=輸入為地址 / 名稱
|
||||
arg-is-address=輸入為地址
|
||||
description=連接到另一服務器。
|
||||
description-player=讓其他用戶連接到另一服務器。
|
||||
description-all=將所有玩家連接到另一台服務器。
|
||||
arg-player=連接到別的服務器的用戶。
|
||||
arg-server=輸入為地址或名稱。
|
||||
arg-is-address=輸入應該為地址。
|
||||
too-many-matching=該服務器名稱有多過一個結果: __1__
|
||||
wrong-version=該服務器版本不同: __1__
|
||||
same-server=你已連接該服務器: __1__
|
||||
@@ -65,15 +65,15 @@ none-matching=沒找到服務器。
|
||||
description=打開偵錯介面。
|
||||
|
||||
[exp-commands_enemy]
|
||||
description-kill=僅移除蟲子
|
||||
description-remove=移除蟲子及停止產生
|
||||
description-kill=僅移除蟲單位。
|
||||
description-remove=移除蟲單位及停止生產器。
|
||||
arg-surface=位面
|
||||
|
||||
[exp-commands_home]
|
||||
description-home=傳送回家
|
||||
description-set=設定目前位置為家
|
||||
description-get=取得家的位置
|
||||
description-return=傳送回上一個位置
|
||||
description-home=傳送回家。
|
||||
description-set=設定目前位置為家。
|
||||
description-get=取得家的位置。
|
||||
description-return=傳送回上一個位置。
|
||||
no-home=你在 __1__ 上還沒設回程傳送點。
|
||||
no-return=你還沒在 __1__ 上使用過回程。
|
||||
home-set=你的回程傳送點為 __1__ 上, x: __2__ y: __3__
|
||||
@@ -84,7 +84,7 @@ home-get=你的回程傳送點在 __1__ 上, x: __2__ y: __3__
|
||||
description=把用戶送監而禁止其行動。
|
||||
arg-player=用戶
|
||||
arg-reason=原因
|
||||
jailed=__1__ 已被 __2__ ,因 __3__ 而被禁止行動。
|
||||
jailed=__1__ 已被 __2__ ,因 __3__ 而被禁止行動
|
||||
already-jailed=__1__ 已被禁止行動。
|
||||
|
||||
[exp-commands_unjail]
|
||||
@@ -101,18 +101,18 @@ lower-role=用戶組比你現有的更高
|
||||
|
||||
[exp-commands_lawnmower]
|
||||
selection-name=割草機
|
||||
description=清理屍體、裝飾和核洞
|
||||
description=清理屍體、裝飾和核彈洞。
|
||||
arg-range=半徑
|
||||
complete=__1__ 格已完成。
|
||||
|
||||
[exp-commands_locate]
|
||||
description=在地圖上查找用戶
|
||||
description=在地圖上查找用戶的最後位置。
|
||||
arg-player=用戶
|
||||
arg-remote=若在使用地圖查看模式
|
||||
arg-remote=若為 true,則開啟在玩家遠端視圖的位置。
|
||||
response=__1__ 的最後地點是 [gps=__2__,__3__]
|
||||
|
||||
[exp-commands_me]
|
||||
description=在聊天中發送操作訊息
|
||||
description=在聊天中發送操作訊息。
|
||||
arg-action=你想做的動作。
|
||||
response=* __1__ __2__ *
|
||||
|
||||
@@ -121,17 +121,19 @@ description-entity=啟用建築保護
|
||||
description-area=啟用區域保護
|
||||
enter-entity=現在進入建築保護,選擇一個建築來保護,按住 shift 來選擇則可取消。
|
||||
enter-area=現在進入區域保護,選擇一個區域來保護,按住 shift 來選擇則可取消。
|
||||
exit-entity=已進入建築保護
|
||||
exit-area=已進入區域選擇
|
||||
exit-entity=已進入建築保護。
|
||||
exit-area=已進入區域選擇。
|
||||
protected-entities=__1__ 個建築現已受保護。
|
||||
unprotected-entities=__1__ 個建築現不受保護。
|
||||
already-protected=本地區已受保護。
|
||||
protected-area=本地區現已受保護。
|
||||
unprotected-area=本地區現不受保護。
|
||||
repeat-offence=__1__ 在 [gps=__3__,__4__] 拆除了 __2__
|
||||
enter-entity-selection = Entered entity selection, select entites to protect, hold shift to remove protection.
|
||||
enter-area-selection = Entered area selection, select areas to protect, hold shift to remove protection.
|
||||
|
||||
[exp-commands_tag-protection]
|
||||
description=切換管理員標記模式,新標記只能由管理員編輯
|
||||
description=切換管理員標記模式,新標記只能由管理員編輯。
|
||||
exit=你已離開管理員地圖標記模式,所有新地圖標記將不受保護。
|
||||
enter=你已進入管理員地圖標記模式,所有新地圖標記將受保護。
|
||||
create=你放了一個管理員地圖標記。
|
||||
@@ -153,6 +155,7 @@ fluid-in=你每秒需要 __1__ 來做 [fluid=__2__]。
|
||||
item-out=結果: __1__ [item=__2__] 每秒。
|
||||
fluid-out=結果: __1__ [fluid=__2__] 每秒。
|
||||
machines=你需要 __1__ 部同樣的機器。
|
||||
machine-count = And you will need __1__ machines (with the same speed as this one) for this.
|
||||
|
||||
[exp-commands_repair]
|
||||
selection-name=維修
|
||||
@@ -172,11 +175,21 @@ arg-player-clear=用戶
|
||||
arg-reason=原因
|
||||
arg-from-player=用戶
|
||||
player-immune=用戶不能被舉報
|
||||
self-report=你不能舉報自己
|
||||
self-report=你不能舉報自己。
|
||||
response=__1__ 因 __2__ 被舉報了。
|
||||
response-admin=__1__ 因 __3__ 被 __2__ 舉報了。
|
||||
already-reported=你已經舉報過該用戶了,你可以讓管理員清除報告來重寫。
|
||||
not-reported=該用戶沒有被舉報的紀錄。
|
||||
not-reported-by=該用戶沒有被 __1__ 舉報的紀錄。
|
||||
reports-title==該用戶現在的被舉報紀錄:
|
||||
player-title=__1__ 有 __2__ 項被舉報的紀錄:
|
||||
list-element=__1__: __2__
|
||||
removed-all=__1__ 被舉報的所有紀錄已被 __2__ 清除。
|
||||
removed=__1__ 被舉報的一個紀錄已被 __2__ 清除。
|
||||
|
||||
[exp-commands_roles]
|
||||
description-assign=為用戶指配用戶組
|
||||
description-unassign=為用戶取消指配用戶組
|
||||
description-assign=為用戶指配用戶組。
|
||||
description-unassign=為用戶取消指配用戶組。
|
||||
description-get=顯示所有用戶組
|
||||
arg-player-assign=指配
|
||||
arg-player-unassign=取消指配
|
||||
@@ -199,16 +212,16 @@ title=有 [item=__1__] 的用戶:
|
||||
result=\n__1__) __2__ 有 __3__ 個。 (__4__)
|
||||
|
||||
[exp-commands_spectate]
|
||||
description-spectate=切換旁觀者模式
|
||||
description-follow=開始在旁觀者模式中關注玩家
|
||||
description-spectate=切換旁觀者模式。
|
||||
description-follow=開始在旁觀者模式中關注玩家。
|
||||
arg-player=用戶
|
||||
follow-self=你不能追蹤自己
|
||||
follow-self=你不能追蹤自己。
|
||||
|
||||
[exp-commands_surface]
|
||||
selection-name=位面
|
||||
description-items=清除地面上的物品
|
||||
description-blueprints=清除藍圖
|
||||
description-blueprints-surface=清除藍圖
|
||||
description-items=清除地面上的物品。
|
||||
description-blueprints=清除藍圖。
|
||||
description-blueprints-surface=清除本位面上的藍圖。
|
||||
arg-surface=位面
|
||||
items=__1__ 清除了所有在 __2__ 中掉地上的東西。
|
||||
blueprints=__1__ 清除了所有在 __2__ 中在地上的藍圖。
|
||||
@@ -225,9 +238,9 @@ response=__1__ 把 __2__ 個火車設置為自動模式。
|
||||
description-teleport=將一名玩家傳送到另一名玩家。
|
||||
description-bring=將一名玩家傳送到你身邊。
|
||||
description-goto=將你傳送到玩家身邊。
|
||||
description-spawn=傳送到出生點。
|
||||
description-spawn=傳送到出生點
|
||||
arg-player-teleport=傳送的玩家,若沒有目標,你會被傳送到這。
|
||||
arg-player-from=傳送的玩家
|
||||
arg-player-from=傳送的玩家。
|
||||
arg-player-to=傳送的玩家目標
|
||||
unavailable=沒找到空間進行傳送
|
||||
same-player=沒辦法傳送到自己
|
||||
@@ -379,6 +392,7 @@ data-statistics=數據
|
||||
data-required=需要
|
||||
data-misc=雜項
|
||||
data-format=__1____2__
|
||||
welcome-title = Welcome to __1__
|
||||
|
||||
[exp-gui_research-milestones]
|
||||
tooltip-main=研究介面
|
||||
@@ -410,7 +424,7 @@ data-caption-fastest-launch=最快發射
|
||||
data-tooltip-fastest-launch=最快發射時間
|
||||
data-caption-total-rockets=總發射次數
|
||||
data-tooltip-total-rockets=我方總發射次數
|
||||
value-tooltip-total-rockets=__1__% 的火箭發射都在本地圖上。
|
||||
value-tooltip-total-rockets=__1__% 的火箭發射都在這張地圖上
|
||||
data-caption-avg-launch=平均時間
|
||||
data-tooltip-avg-launch=火箭發射平均時間
|
||||
data-caption-avg-launch-n=平均時間 __1__
|
||||
@@ -453,8 +467,8 @@ tooltip-cursor-empty=游標為空
|
||||
caption-main=工作流程 [img=info]
|
||||
tooltip-main=工作流程
|
||||
tooltip-sub=需要完成的工作流程\n- 你可以用富文本來加上圖片 [img=utility/not_enough_repair_packs_icon] 或 [color=blue] 顏色[/color]。
|
||||
caption-no-tasks=沒有工作流程
|
||||
tooltip-no-tasks=按加號加入工作流程
|
||||
caption-no-tasks=沒有工作流程!
|
||||
tooltip-no-tasks=按加號加入工作流程!
|
||||
tooltip-last-edit=最後由 __1__ 在 __2__ 修改
|
||||
tooltip-close=關閉工作流程
|
||||
tooltip-new=加入工作流程
|
||||
@@ -470,41 +484,106 @@ tooltip-edit-none=現沒有被人修改
|
||||
caption-create-footer=加入工作流程
|
||||
caption-edit-footer=修改工作流程
|
||||
caption-view-footer=工作流程細節
|
||||
tooltip-confirm = Save changes (minimum of 5 characters long)
|
||||
|
||||
[exp-gui_warp-list]
|
||||
caption-main=傳送陣清單 [img=info]
|
||||
tooltip-main=傳送陣清單
|
||||
tooltip-sub=傳送陣在 __1__ 秒內只能使用一次。 亦需在 __2__ 格之內使用\n__3__\n__4__\n__5__\n__6__\n__7__\n__8__
|
||||
tooltip-sub-current= - __1__ 你目前的傳送陣;
|
||||
tooltip-sub-connected= - __1__ 你可以到本傳送陣;
|
||||
tooltip-sub-different= - __1__ 該傳送陣未連接;
|
||||
tooltip-sub-cooldown= - __1__ 傳送冷卻中;
|
||||
tooltip-sub-not-available= - __1__ 你不在傳送陣範圍;
|
||||
tooltip-sub-bypass= - __1__ 你可以傳送到這裏;
|
||||
tooltip-add=新建傳送陣
|
||||
tooltip-add-too-close=無法新建傳送陣; 太接近 __1__ 了
|
||||
add-warning-water=無法新建傳送陣; 太接近水源了, 請移動 __1__ 格又或者填平該地方
|
||||
add-warning-entities=無法新建傳送陣; 太接近其他東西, 請移動 __1__ 格又或者拆除該東西
|
||||
tooltip-icon=修改傳送陣圖案
|
||||
tooltip-last-edit=最後由 __1__ 在 __2__ 更改\n按這在地圖上看
|
||||
tooltip-confirm=儲存更改
|
||||
tooltip-discard=放棄更改
|
||||
tooltip-edit=現被 __1__ 修改中
|
||||
tooltip-edit-none=現沒有被人修改
|
||||
tooltip-remove=拆除傳送陣
|
||||
tooltip-cooldown=請等 __1__ 秒才再次傳送
|
||||
tooltip-cooldown-none=你要等 __1__ 秒才可以再次傳送
|
||||
tooltip-goto=到 X __1__ Y __2__
|
||||
tooltip-goto-bypass=到 X __1__ Y __2__, 繞行模式
|
||||
tooltip-goto-bypass-different-network=到 X __1__ Y __2__, 繞行模式 (不同網絡)
|
||||
tooltip-goto-same-warp=你已在該傳送陣
|
||||
tooltip-goto-different-network=該傳送陣未連接, 你可使用電纜連接
|
||||
tooltip-goto-cooldown=傳送冷卻中, 請等候冷卻
|
||||
tooltip-goto-disabled=你不在傳送陣, 請移至任意傳送陣
|
||||
|
||||
[exp-gui_vlayer]
|
||||
tooltip-main=Vlayer 介面
|
||||
selection-name=vlayer
|
||||
caption-stat-solar=[img=entity/solar-panel] 太陽能板
|
||||
tooltip-stat-solar=使用中的太陽能板數量 / 儲存的太陽能板數量
|
||||
caption-stat-accumulator=[img=entity/accumulator] 蓄電池
|
||||
tooltip-stat-accumulator=使用中的蓄電池數量 / 儲存的蓄電池數量
|
||||
caption-stat-surface-area=[virtual-signal=signal-R] 餘下土地
|
||||
tooltip-stat-surface-area=餘下土地。 放入堆填來增加
|
||||
caption-stat-sustained=[virtual-signal=signal-S] 持續產能
|
||||
tooltip-stat-sustained=持續電力產能
|
||||
caption-stat-production=[virtual-signal=signal-P] 現時產能
|
||||
tooltip-stat-production=現時電力產能
|
||||
caption-stat-capacity=[virtual-signal=signal-E] 電池電量
|
||||
tooltip-stat-capacity=現時電池電量 / 電池最大容量
|
||||
type-energy=電力
|
||||
type-circuit=回路
|
||||
type-storage-input=放入箱
|
||||
type-storage-output=提取箱
|
||||
caption-refresh=刷新列表
|
||||
caption-view=查看介面
|
||||
caption-build=建造介面
|
||||
caption-remove=拆除介面
|
||||
message-located=__1__ 介面在 __2__
|
||||
message-built=__1__ 在 __3__ 建造了 __2__ 介面
|
||||
message-removed=__1__ 在 __3__ 拆除了 __2__ 介面
|
||||
message-no-chest=沒檢測到鋼箱
|
||||
message-chest-not-empty=鋼箱不是空的
|
||||
message-multiple=無法建造 __1__ 介面,檢測到多個鋼箱
|
||||
message-limit=無法建造 __1__ 介面,超出最大限制
|
||||
message-no-space=無法在此建造電力介面,空間不足
|
||||
message-research=你要研究 [technology=__1__] 在 __2__ 級 才可使用太空平台 vlayer 功能。
|
||||
|
||||
[exp_afk-kick]
|
||||
kick-message=因地圖中沒有活躍玩家,所以所有人都已被請離。
|
||||
|
||||
[exp_chat-auto-reply]
|
||||
chat-reply=[Compilatron] __1__
|
||||
chat-disallowed=你沒有權限使用這個指令。
|
||||
reply-online=現在有 __1__ 人上線。
|
||||
reply-players=本地圖現在有 __1__ 人曾上線。
|
||||
reply-dev=Cooldude2606 只是本場境的開發者
|
||||
chat-disallowed=You can't use global chat commands
|
||||
reply-online=現在有 __1__ 人在線
|
||||
reply-players=這張地圖上共有__1__名用戶
|
||||
reply-dev=Cooldude2606 is a dev for this server and makes the scenario and is not a factorio dev.
|
||||
reply-softmod=這裹用了自設情境。
|
||||
reply-clusterio=Clusterio is a factorio server hosting platform allowing internet enabled mods.
|
||||
reply-blame=責怪 __1__ 吧。
|
||||
reply-afk=看看 __1__, 他已掛機 __2__ 。
|
||||
reply-evolution=現在敵人進化度為 __1__ 。
|
||||
reply-blame=責怪 __1__ 吧!
|
||||
reply-afk=看看 __1__,他的掛機時長為 __2__
|
||||
reply-evolution=現在敵人進化度為 __1__
|
||||
reply-magic=Fear the admin magic (ノ゚∀゚)ノ⌒・*:.。. .。.:*・゜゚・*☆
|
||||
reply-aids=≖ ‿ ≖ Fear the aids of a public server ≖ ‿ ≖
|
||||
reply-riot=(admins) ┬┴┬┴┤ᵒ_ᵒ)├┬┴┬┴ ‹ ‹\(´ω` )/››‹‹\ ( ´)/››‹‹\ ( ´ω`)/›› (rest of server)
|
||||
reply-loops=架設迴旋處最終後果都不好。
|
||||
reply-loops=NO LOOPS; LOOPS ARE BAD; JUST NO LOOPS!!!; IF YOU MAKE A LOOP… IT WILL NOT END WELL!!!
|
||||
reply-lenny=( ͡° ͜ʖ ͡°)
|
||||
reply-hodor=Hodor
|
||||
reply-popcorn-1=Heating the oil and waiting for the popping sound...
|
||||
reply-popcorn-1=Heating the oil and waiting for the popping sound…
|
||||
reply-popcorn-2=__1__ your popcorn is finished. Lean backwards and watch the drama unfold.
|
||||
reply-snaps-1=Pouring the glasses and finding the correct song book...
|
||||
reply-snaps-2=Singing a song...🎤🎶
|
||||
reply-snaps-1=Pouring the glasses and finding the correct song book…
|
||||
reply-snaps-2=Singing a song…🎤🎶
|
||||
reply-snaps-3=schkål, my friends!
|
||||
reply-cocktail-1= 🍸 Inintiating mind reading unit... 🍸
|
||||
reply-cocktail-1= 🍸 Inintiating mind reading unit… 🍸
|
||||
reply-cocktail-2= 🍸 Mixing favourite ingredients of __1__ 🍸
|
||||
reply-cocktail-3=🍸 __1__ your cocktail is done.🍸
|
||||
reply-coffee-1= ☕ Boiling the water and grinding the coffee beans... ☕
|
||||
reply-coffee-1= ☕ Boiling the water and grinding the coffee beans… ☕
|
||||
reply-coffee-2= ☕ __1__ we ran out of coffe beans! Have some tea instead. ☕
|
||||
reply-pizza-1= 🍕 Finding nearest pizza supplier... 🍕
|
||||
reply-pizza-1= 🍕 Finding nearest pizza supplier… 🍕
|
||||
reply-pizza-2= 🍕 Figuring out the favourite pizza of __1__ 🍕
|
||||
reply-pizza-3= 🍕 __1__ your pizza is here! 🍕
|
||||
reply-tea-1= ☕ Boiling the water... ☕
|
||||
reply-tea-1= ☕ Boiling the water… ☕
|
||||
reply-tea-2= ☕ __1__ your tea is done! ☕
|
||||
reply-mead-1= Filling the drinking horn
|
||||
reply-mead-2= Skål!
|
||||
@@ -536,3 +615,22 @@ chat-jailed=__1__ 因被多次拆除受保護物體而被禁止行動。請等
|
||||
|
||||
[exp_report-jail]
|
||||
chat-jailed=__1__ 因被多次舉報而被禁止行動。請等候管理員作出下一步處理。
|
||||
|
||||
|
||||
[exp-gui_player-list]
|
||||
main-tooltip = Player List
|
||||
open-action-bar = Options
|
||||
close-action-bar = Close Options
|
||||
reason-confirm = Confirm Reason
|
||||
reason-entry = Enter Reason
|
||||
goto-player = Goto player
|
||||
bring-player = Bring player
|
||||
report-player = Report player
|
||||
jail-player = Jail player
|
||||
kick-player = Kick player
|
||||
ban-player = Ban player
|
||||
afk-time = __1__% of total map time\nLast moved __2__ ago
|
||||
open-map = __1__ __2__\n__3__\nClick to open map
|
||||
|
||||
[exp_spectate]
|
||||
follow-label = Following __1__.\nClick here or press escape to stop following.
|
||||
@@ -5,17 +5,22 @@
|
||||
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
|
||||
"license": "MIT",
|
||||
"repository": "explosivegaming/ExpCluster",
|
||||
"type": "module",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --env production"
|
||||
"prepare": "tsc --build && webpack-cli --env production",
|
||||
"test": "tap --type-strip-only --disable-coverage --allow-empty-coverage test/*.test.js",
|
||||
"coverage": "tap --coverage-report=text test/*.test.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@clusterio/controller": "workspace:^",
|
||||
"@clusterio/lib": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clusterio/controller": "workspace:^",
|
||||
"@clusterio/lib": "workspace:^",
|
||||
"@clusterio/web_ui": "workspace:^",
|
||||
"@types/node": "catalog:",
|
||||
@@ -23,6 +28,7 @@
|
||||
"antd": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"tap": "^21.1.0",
|
||||
"typescript": "catalog:",
|
||||
"webpack": "catalog:",
|
||||
"webpack-cli": "catalog:",
|
||||
@@ -32,6 +38,8 @@
|
||||
"@expcluster/lib_commands": "workspace:^",
|
||||
"@expcluster/lib_util": "workspace:^",
|
||||
"@expcluster/lib_gui": "workspace:^",
|
||||
"@expcluster/permission-groups": "workspace:^",
|
||||
"@expcluster/roles": "workspace:^",
|
||||
"@sinclair/typebox": "catalog:"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
import type * as lib from "@clusterio/lib";
|
||||
|
||||
/**
|
||||
* Permissions checked by the scenario in game through exp_roles.
|
||||
@@ -7,9 +7,9 @@ import * as lib from "@clusterio/lib";
|
||||
* underscores, see module/commands/_authorities.lua. Everything else is checked
|
||||
* by name at its call site.
|
||||
*/
|
||||
type Definition = [name: string, title: string, description: string, grantByDefault?: boolean];
|
||||
type Definition<Name extends string = string> = readonly [name: Name, title: string, description: string, grantByDefault?: boolean];
|
||||
|
||||
const definitions: Definition[] = [
|
||||
const definitions = [
|
||||
["exp_scenario.bypass.deconstruction_log", "Deconstruction log bypass", "Be excluded from the deconstruction log."],
|
||||
["exp_scenario.bypass.entity_protection", "Bypass entity protection", "Remove entities that the protection filter would block."],
|
||||
["exp_scenario.bypass.nuke_protection", "Bypass nuke protection", "Use nukes without the nuke protection restrictions."],
|
||||
@@ -118,8 +118,12 @@ const definitions: Definition[] = [
|
||||
["exp_scenario.player.admin", "Factorio admin", "Be promoted to Factorio admin while holding a role with this permission."],
|
||||
["exp_scenario.player.instant_respawn", "Instant respawn", "Respawn after two seconds instead of the default delay."],
|
||||
["exp_scenario.player.spectator", "Spectator", "Remove the zoom to world noise effect, as Factorio does for spectators."],
|
||||
];
|
||||
] as const satisfies readonly Definition[];
|
||||
|
||||
for (const [name, title, description, grantByDefault] of definitions) {
|
||||
lib.definePermission({ name, title, description, grantByDefault });
|
||||
}
|
||||
/** Name of a permission defined by the scenario, added to lib.Permissions in index.ts. */
|
||||
export type ScenarioPermissionName = (typeof definitions)[number][0];
|
||||
|
||||
export const permissions = definitions.map((definition: Definition<ScenarioPermissionName>): lib.PermissionDefinition => {
|
||||
const [name, title, description, grantByDefault] = definition;
|
||||
return { name, title, description, grantByDefault };
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { RoleColor } from "./messages";
|
||||
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
|
||||
@@ -19,7 +20,23 @@ export interface SeedRole {
|
||||
isAdmin?: boolean;
|
||||
/** Name of the role whose permissions are also granted, applied recursively. */
|
||||
parent?: string;
|
||||
permissions: 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[] = [
|
||||
@@ -32,6 +49,7 @@ export const seedRoles: SeedRole[] = [
|
||||
},
|
||||
{
|
||||
name: "Senior Administrator",
|
||||
group: "Admin",
|
||||
shortHand: "SAdmin",
|
||||
color: new RoleColor(233, 63, 233),
|
||||
parent: "Administrator",
|
||||
@@ -44,6 +62,7 @@ export const seedRoles: SeedRole[] = [
|
||||
},
|
||||
{
|
||||
name: "Administrator",
|
||||
group: "Admin",
|
||||
shortHand: "Admin",
|
||||
color: new RoleColor(233, 63, 233),
|
||||
parent: "Moderator",
|
||||
@@ -55,6 +74,7 @@ export const seedRoles: SeedRole[] = [
|
||||
},
|
||||
{
|
||||
name: "Moderator",
|
||||
group: "Admin",
|
||||
shortHand: "Mod",
|
||||
color: new RoleColor(0, 170, 0),
|
||||
parent: "Trainee",
|
||||
@@ -89,6 +109,7 @@ export const seedRoles: SeedRole[] = [
|
||||
},
|
||||
{
|
||||
name: "Trainee",
|
||||
group: "Admin",
|
||||
shortHand: "TrMod",
|
||||
color: new RoleColor(0, 170, 0),
|
||||
parent: "Veteran",
|
||||
@@ -120,6 +141,7 @@ export const seedRoles: SeedRole[] = [
|
||||
},
|
||||
{
|
||||
name: "Board Member",
|
||||
group: "Trusted",
|
||||
shortHand: "Board",
|
||||
color: new RoleColor(247, 246, 54),
|
||||
parent: "Sponsor",
|
||||
@@ -133,6 +155,7 @@ export const seedRoles: SeedRole[] = [
|
||||
},
|
||||
{
|
||||
name: "Senior Backer",
|
||||
group: "Trusted",
|
||||
shortHand: "Backer",
|
||||
color: new RoleColor(238, 172, 44),
|
||||
parent: "Sponsor",
|
||||
@@ -140,6 +163,7 @@ export const seedRoles: SeedRole[] = [
|
||||
},
|
||||
{
|
||||
name: "Sponsor",
|
||||
group: "Trusted",
|
||||
shortHand: "Spon",
|
||||
color: new RoleColor(238, 172, 44),
|
||||
parent: "Supporter",
|
||||
@@ -158,6 +182,7 @@ export const seedRoles: SeedRole[] = [
|
||||
},
|
||||
{
|
||||
name: "Supporter",
|
||||
group: "Trusted",
|
||||
shortHand: "Sup",
|
||||
color: new RoleColor(230, 99, 34),
|
||||
parent: "Veteran",
|
||||
@@ -172,6 +197,7 @@ export const seedRoles: SeedRole[] = [
|
||||
},
|
||||
{
|
||||
name: "Partner",
|
||||
group: "Trusted",
|
||||
shortHand: "Part",
|
||||
color: new RoleColor(140, 120, 200),
|
||||
parent: "Veteran",
|
||||
@@ -183,6 +209,7 @@ export const seedRoles: SeedRole[] = [
|
||||
},
|
||||
{
|
||||
name: "Veteran",
|
||||
group: "Trusted",
|
||||
shortHand: "Vet",
|
||||
color: new RoleColor(140, 120, 200),
|
||||
parent: "Member",
|
||||
@@ -196,6 +223,7 @@ export const seedRoles: SeedRole[] = [
|
||||
},
|
||||
{
|
||||
name: "Member",
|
||||
group: "Standard",
|
||||
shortHand: "Mem",
|
||||
color: new RoleColor(24, 172, 188),
|
||||
parent: "Regular",
|
||||
@@ -217,6 +245,7 @@ export const seedRoles: SeedRole[] = [
|
||||
},
|
||||
{
|
||||
name: "Regular",
|
||||
group: "Standard",
|
||||
shortHand: "Reg",
|
||||
color: new RoleColor(79, 155, 163),
|
||||
autoAssignHours: 3,
|
||||
@@ -232,6 +261,7 @@ export const seedRoles: SeedRole[] = [
|
||||
},
|
||||
{
|
||||
name: "Jail",
|
||||
group: "Restricted",
|
||||
shortHand: "Jail",
|
||||
color: new RoleColor(50, 50, 50),
|
||||
priority: 1,
|
||||
@@ -240,6 +270,7 @@ export const seedRoles: SeedRole[] = [
|
||||
},
|
||||
{
|
||||
name: "Guest",
|
||||
group: "Guest",
|
||||
shortHand: "",
|
||||
color: new RoleColor(185, 187, 160),
|
||||
isDefault: true,
|
||||
@@ -247,9 +278,59 @@ export const seedRoles: SeedRole[] = [
|
||||
},
|
||||
];
|
||||
|
||||
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<string>();
|
||||
const permissions = new Set<lib.PermissionName>();
|
||||
const seen = new Set<string>();
|
||||
let current: SeedRole | undefined = role;
|
||||
while (current && !seen.has(current.name)) {
|
||||
@@ -0,0 +1,139 @@
|
||||
import t from "tap";
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { Controller } from "@clusterio/controller";
|
||||
import { ControllerPlugin } from "../dist/node/controller.js";
|
||||
import * as messages from "../dist/node/messages.js";
|
||||
import { seedRoles, seedGroups } from "../dist/node/seed.js";
|
||||
import * as roles from "@expcluster/roles";
|
||||
import * as groups from "@expcluster/permission-groups";
|
||||
import { ControllerPlugin as RolesPlugin } from "@expcluster/roles/dist/node/controller.js";
|
||||
import { ControllerPlugin as GroupsPlugin } from "@expcluster/permission-groups/dist/node/controller.js";
|
||||
import { GroupRecord, GroupPermissions, RoleMappingRecord } from "@expcluster/permission-groups";
|
||||
|
||||
import { plugin } from "../dist/node/index.js";
|
||||
|
||||
// Registering the plugin defines the permissions the seed grants
|
||||
lib.registerPluginPermissions([plugin]);
|
||||
|
||||
// The controller validates message classes against the link registry
|
||||
for (const Message of [messages.SeedRequest, ...roles.plugin.messages, ...groups.plugin.messages]) {
|
||||
lib.Link.register(Message);
|
||||
}
|
||||
|
||||
const logger = { child: () => logger, info: () => {}, warn: () => {}, error: () => {}, verbose: () => {} };
|
||||
|
||||
// Silence the singleton logger to avoid polluting the test output
|
||||
lib.logger.silent = true;
|
||||
t.after(() => {
|
||||
lib.logger.silent = false;
|
||||
});
|
||||
|
||||
/** Build the plugin around a real controller, which is side effect free while not started. */
|
||||
async function startPlugin(t2, { withPlugins = true } = {}) {
|
||||
const controllerConfig = new lib.ControllerConfig("controller", {
|
||||
"controller.database_directory": t2.testdir(),
|
||||
"controller.default_role_id": lib.Role.DefaultPlayerRoleId,
|
||||
});
|
||||
const controller = new Controller(logger, [], controllerConfig);
|
||||
controller.roles.set(new lib.Role(0, "Cluster Admin", "", new Set(["core.admin"])));
|
||||
controller.roles.set(new lib.Role(1, "Player", "", new Set()));
|
||||
|
||||
if (withPlugins) {
|
||||
for (const [name, Plugin] of [["exp_roles", RolesPlugin], ["exp_groups", GroupsPlugin]]) {
|
||||
const plugin = new Plugin({ name }, controller, undefined, logger);
|
||||
await plugin.init();
|
||||
}
|
||||
}
|
||||
|
||||
const plugin = new ControllerPlugin({ name: "exp_scenario" }, controller, undefined, logger);
|
||||
await plugin.init();
|
||||
return { plugin, controller };
|
||||
}
|
||||
|
||||
const byName = (datastore, name) => [...datastore.values()].find(other => other.name === name);
|
||||
|
||||
t.test("class ControllerPlugin", t2 => {
|
||||
t2.test(".handleSeedRequest() throws when the seeded plugins are missing", async t3 => {
|
||||
const { plugin } = await startPlugin(t3, { withPlugins: false });
|
||||
await t3.rejects(
|
||||
plugin.handleSeedRequest(),
|
||||
{ message: "Seeding requires the exp_roles and exp_groups plugins" },
|
||||
"the request is rejected",
|
||||
);
|
||||
});
|
||||
|
||||
t2.test(".handleSeedRequest() creates the roles and reuses them by name", async t3 => {
|
||||
const { plugin, controller } = await startPlugin(t3);
|
||||
const rolesPlugin = RolesPlugin.get(controller);
|
||||
|
||||
await plugin.handleSeedRequest();
|
||||
t3.strictSame(controller.roles.size, seedRoles.length, "every seed role exists");
|
||||
|
||||
const moderator = byName(controller.roles, "Moderator");
|
||||
t3.ok(moderator.permissions.has("exp_scenario.command.jail"), "parent permissions are flattened in");
|
||||
t3.strictSame(rolesPlugin.roleMeta.get(moderator.id).shortHand, "Mod", "the role properties match the seed");
|
||||
|
||||
await plugin.handleSeedRequest();
|
||||
t3.strictSame(controller.roles.size, seedRoles.length, "seeding again reuses the roles");
|
||||
});
|
||||
|
||||
t2.test(".handleSeedRequest() creates the groups and resets them by name", async t3 => {
|
||||
const { plugin, controller } = await startPlugin(t3);
|
||||
const groupsPlugin = GroupsPlugin.get(controller);
|
||||
|
||||
await plugin.handleSeedRequest();
|
||||
t3.strictSame(groupsPlugin.groups.size, seedGroups.length, "every seed group exists");
|
||||
|
||||
const restricted = byName(groupsPlugin.groups, "Restricted");
|
||||
t3.strictSame(restricted.permissions.isBlacklist, false, "the group only allows the listed actions");
|
||||
t3.strictSame(restricted.permissions.permissions, ["write_to_console"], "the actions match the seed");
|
||||
|
||||
const admin = byName(groupsPlugin.groups, "Admin");
|
||||
groupsPlugin.groups.set(new GroupRecord(admin.id, admin.name, new GroupPermissions(true, [])));
|
||||
await plugin.handleSeedRequest();
|
||||
t3.strictSame(groupsPlugin.groups.size, seedGroups.length, "seeding again reuses the groups");
|
||||
t3.ok(
|
||||
byName(groupsPlugin.groups, "Admin").permissions.permissions.includes("toggle_map_editor"),
|
||||
"seeding again resets the actions",
|
||||
);
|
||||
});
|
||||
|
||||
t2.test(".handleSeedRequest() maps each role onto its group by rank", async t3 => {
|
||||
const { plugin, controller } = await startPlugin(t3);
|
||||
const groupsPlugin = GroupsPlugin.get(controller);
|
||||
|
||||
await plugin.handleSeedRequest();
|
||||
const mapped = seedRoles.filter(role => role.group !== undefined);
|
||||
t3.strictSame(groupsPlugin.roleMappings.size, mapped.length, "every role with a group is mapped");
|
||||
|
||||
const mappingOf = name => [...groupsPlugin.roleMappings.values()]
|
||||
.find(mapping => mapping.roleIds.has(byName(controller.roles, name).id));
|
||||
const groupOf = name => groupsPlugin.groups.get(mappingOf(name).groupId).name;
|
||||
t3.strictSame(groupOf("Player"), "Guest", "the default role maps to the guest group");
|
||||
t3.strictSame(groupOf("Jail"), "Restricted", "jail maps to the restricted group");
|
||||
t3.ok(mappingOf("Jail").priority > mappingOf("Senior Administrator").priority, "jail outranks every role");
|
||||
t3.ok(mappingOf("Moderator").priority > mappingOf("Veteran").priority, "the role order sets the rank");
|
||||
t3.ok(mappingOf("Veteran").priority > mappingOf("Player").priority, "the default role ranks lowest");
|
||||
t3.strictSame(mappingOf("Cluster Admin"), undefined, "the admin role keeps the factorio default group");
|
||||
|
||||
const priorities = [...groupsPlugin.roleMappings.values()].map(mapping => mapping.priority);
|
||||
t3.strictSame(priorities.length, new Set(priorities).size, "priorities are unique");
|
||||
});
|
||||
|
||||
t2.test(".handleSeedRequest() reuses mappings and keeps clear of other mappings", async t3 => {
|
||||
const { plugin, controller } = await startPlugin(t3);
|
||||
const groupsPlugin = GroupsPlugin.get(controller);
|
||||
|
||||
groupsPlugin.roleMappings.set(new RoleMappingRecord(99, new Set([0, 1]), 5, 2, true));
|
||||
await plugin.handleSeedRequest();
|
||||
const first = new Map([...groupsPlugin.roleMappings.values()].map(mapping => [mapping.id, mapping.priority]));
|
||||
|
||||
await plugin.handleSeedRequest();
|
||||
const second = new Map([...groupsPlugin.roleMappings.values()].map(mapping => [mapping.id, mapping.priority]));
|
||||
t3.strictSame(second, first, "seeding again reuses the mappings");
|
||||
t3.strictSame(groupsPlugin.roleMappings.get(99).priority, 2, "other mappings are left alone");
|
||||
t3.notOk([...first.values()].filter(priority => priority === 2).length > 1, "seed priorities skip taken ones");
|
||||
});
|
||||
|
||||
t2.end();
|
||||
});
|
||||
@@ -1,10 +1,11 @@
|
||||
"use strict";
|
||||
const t = require("tap");
|
||||
const lib = require("@clusterio/lib");
|
||||
const { seedRoles, flattenSeedPermissions } = require("../dist/node/seed");
|
||||
import t from "tap";
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { seedRoles, seedGroups, flattenSeedPermissions } from "../dist/node/seed.js";
|
||||
|
||||
// Importing this defines the exp_scenario permissions the seed grants
|
||||
require("@expcluster/scenario/dist/node/permissions");
|
||||
import { plugin } from "../dist/node/index.js";
|
||||
|
||||
// Registering the plugin defines the permissions the seed grants
|
||||
lib.registerPluginPermissions([plugin]);
|
||||
|
||||
t.test("seedRoles[] grant only defined permissions", t2 => {
|
||||
for (const role of seedRoles) {
|
||||
@@ -23,6 +24,29 @@ t.test("seedRoles[] are unique with one default and one admin", t2 => {
|
||||
t2.end();
|
||||
});
|
||||
|
||||
t.test("seedRoles[] are placed in defined groups", t2 => {
|
||||
const groupNames = new Set(seedGroups.map(group => group.name));
|
||||
for (const role of seedRoles) {
|
||||
if (role.group !== undefined) {
|
||||
t2.ok(groupNames.has(role.group), `${role.name} is placed in defined group ${role.group}`);
|
||||
}
|
||||
}
|
||||
t2.end();
|
||||
});
|
||||
|
||||
t.test("seedGroups[] are unique", t2 => {
|
||||
const names = seedGroups.map(group => group.name);
|
||||
t2.strictSame(names.length, new Set(names).size, "names are unique");
|
||||
for (const group of seedGroups) {
|
||||
t2.strictSame(
|
||||
group.inputActions.length,
|
||||
new Set(group.inputActions).size,
|
||||
`${group.name} lists each input action once`,
|
||||
);
|
||||
}
|
||||
t2.end();
|
||||
});
|
||||
|
||||
t.test("flattenSeedPermissions() inherits permissions from parent roles", t2 => {
|
||||
const byName = new Map(seedRoles.map(role => [role.name, role]));
|
||||
for (const role of seedRoles) {
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"extends": "../tsconfig.browser.json",
|
||||
"include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ],
|
||||
"include": [ "web/**/*.tsx", "web/**/*.ts", "index.ts", "messages.ts", "permissions.ts", "package.json" ],
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"extends": "../tsconfig.node.json",
|
||||
"references": [
|
||||
{ "path": "../exp_groups/tsconfig.node.json" },
|
||||
{ "path": "../exp_roles/tsconfig.node.json" },
|
||||
],
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["test/*", "./dist/*"],
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { Button, Popconfirm } from "antd";
|
||||
|
||||
import { ControlContext, SectionHeader, useAccount, notifyErrorHandler } from "@clusterio/web_ui";
|
||||
|
||||
import { SeedRequest } from "../../messages.js";
|
||||
|
||||
/** Button on the roles page which creates the roles and permission groups the scenario shipped with. */
|
||||
export default function Seed() {
|
||||
const control = useContext(ControlContext);
|
||||
const account = useAccount();
|
||||
const [seeding, setSeeding] = useState(false);
|
||||
|
||||
if (!account.hasPermission("exp_scenario.seed")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <SectionHeader
|
||||
title="ExpGaming Roles"
|
||||
extra={<Popconfirm
|
||||
title="Create the ExpGaming roles and permission groups?"
|
||||
description="Roles which already exist by name only gain permissions, groups are reset to the defaults."
|
||||
onConfirm={() => {
|
||||
setSeeding(true);
|
||||
control.send(new SeedRequest())
|
||||
.catch(notifyErrorHandler("Error seeding roles and groups"))
|
||||
.finally(() => setSeeding(false));
|
||||
}}
|
||||
>
|
||||
<Button loading={seeding}>Seed roles and groups</Button>
|
||||
</Popconfirm>}
|
||||
/>;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { BaseWebPlugin } from "@clusterio/web_ui";
|
||||
|
||||
import Seed from "./components/Seed";
|
||||
|
||||
export class WebPlugin extends BaseWebPlugin {
|
||||
async init() {
|
||||
this.componentExtra = {
|
||||
RolesPage: Seed,
|
||||
};
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -12,7 +12,7 @@ export const plugin: lib.PluginDeclaration = {
|
||||
title: "ExpGaming - Server UPS",
|
||||
description: "Clusterio plugin providing in game server ups counter",
|
||||
|
||||
instanceEntrypoint: "./dist/node/instance",
|
||||
instanceEntrypoint: "./dist/node/instance.js",
|
||||
instanceConfigFields: {
|
||||
"exp_server_ups.update_interval": {
|
||||
title: "Update Interval",
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[exp_server-ups]
|
||||
description=啟動 UPS 顯示
|
||||
description=啟動 UPS 顯示。
|
||||
@@ -1,2 +1,2 @@
|
||||
[exp_server-ups]
|
||||
description=啟動 UPS 顯示
|
||||
description=啟動 UPS 顯示。
|
||||
@@ -5,6 +5,7 @@
|
||||
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
|
||||
"license": "MIT",
|
||||
"repository": "explosivegaming/ExpCluster",
|
||||
"type": "module",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --env production"
|
||||
|
||||
File renamed without changes.
+1
-1
@@ -4,5 +4,5 @@ export const plugin: lib.PluginDeclaration = {
|
||||
name: "exp_util",
|
||||
title: "ExpGaming Module Utilities",
|
||||
description: "Provides extensions and overrides of base Lua library functions, and provides utility modules for improved module compatibly",
|
||||
instanceEntrypoint: "./dist/node/instance",
|
||||
instanceEntrypoint: "./dist/node/instance.js",
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
[exp_util]
|
||||
time-symbol-days-short=__1__日
|
||||
rich-text-color-tag=[color=__1__,__2__,__3__]__4__[/color]
|
||||
selection_enter=現在進入 __1__ 區域選擇
|
||||
selection_exit=已進入 __1__ 區域選擇
|
||||
selection_enter=現在進入 __1__ 區域選擇。
|
||||
selection_exit=已進入 __1__ 區域選擇。
|
||||
@@ -1,5 +1,5 @@
|
||||
[exp_util]
|
||||
time-symbol-days-short=__1__日
|
||||
rich-text-color-tag=[color=__1__,__2__,__3__]__4__[/color]
|
||||
selection_enter=現在進入 __1__ 區域選擇
|
||||
selection_exit=已進入 __1__ 區域選擇
|
||||
selection_enter=現在進入 __1__ 區域選擇。
|
||||
selection_exit=已進入 __1__ 區域選擇。
|
||||
@@ -5,6 +5,7 @@
|
||||
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
|
||||
"license": "MIT",
|
||||
"repository": "explosivegaming/ExpCluster",
|
||||
"type": "module",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --env production"
|
||||
|
||||
File renamed without changes.
+2
-3
@@ -1,5 +1,4 @@
|
||||
"use strict";
|
||||
const { compile } = require("@clusterio/lib");
|
||||
import { compile } from "@clusterio/lib";
|
||||
|
||||
/**
|
||||
* Generate a flat array of tests from a matrix of inputs.
|
||||
@@ -49,7 +48,7 @@ function testRoundTripJsonSerialisable(t, Class, tests) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export {
|
||||
testMatrix,
|
||||
testRoundTripJsonSerialisable,
|
||||
};
|
||||
Loaded 100 of 103 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user