Author SHA1 Message Date
Weblateandphidias 6f2471be7b chore(l10n): update Chinese (Traditional Han script) translation
chore(l10n): update Chinese (Simplified Han script) translation

Co-authored-by: phidias <phidias@aperx.org>
Language: Chinese (Simplified Han script)
Language: Chinese (Traditional Han script)
Progress: 100.0% (452 of 452 strings)
Translate-URL: https://translate.aperx.org/projects/expcluster/expscenario/zh_Hans/
Translate-URL: https://translate.aperx.org/projects/expcluster/expscenario/zh_Hant/
Translation: ExpCluster/ExpScenario
2026-09-19 05:47:08 +00:00
phidias 4eeb9b6b36 Merge branch 'explosivegaming:main' into translate 2026-09-19 13:10:26 +09:00
1627fcba8f chore(l10n): update Chinese (Traditional Han script) translation (#45)
chore(l10n): update Chinese (Simplified Han script) translation

chore(l10n): update Chinese (Traditional Han script) translation

chore(l10n): update Chinese (Simplified Han script) translation

chore(l10n): update Chinese (Traditional Han script) translation

chore(l10n): update Chinese (Simplified Han script) translation

chore(l10n): update English translation


Language: Chinese (Simplified Han script)
Language: Chinese (Traditional Han script)
Language: English
Progress: 100.0% (233 of 233 strings)
Progress: 100.0% (452 of 452 strings)
Progress: 95.7% (433 of 452 strings)
Translate-URL: https://translate.aperx.org/projects/expcluster/explegacy/zh_Hans/
Translate-URL: https://translate.aperx.org/projects/expcluster/explegacy/zh_Hant/
Translate-URL: https://translate.aperx.org/projects/expcluster/expscenario/en/
Translate-URL: https://translate.aperx.org/projects/expcluster/expscenario/zh_Hans/
Translate-URL: https://translate.aperx.org/projects/expcluster/expscenario/zh_Hant/
Translation: ExpCluster/ExpLegacy
Translation: ExpCluster/ExpScenario

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: phidias <phidias@aperx.org>
2026-09-18 17:34:39 +09:00
Cooldude2606 06fc1086e3 Merge pull request #476 from bbassie/feature/esm-migration
Migrate to ES modules
2026-09-12 01:18:22 +01:00
bbassieandClaude Fable 5.1 7fd66c6ac7 Migrate to ES modules
Clusterio #984 converted its packages to ESM, so a CommonJS plugin can
no longer import @clusterio/lib and plugin entrypoints must name the
full file. Every package gets "type": "module", entrypoints and relative
imports carry the .js extension, and the webpack configs become .cjs as
webpack-cli loads them with require. tsconfig.node.json moves from
node16 to nodenext to match clusterio, which needs it for the JSON
import attributes in its sources.

The tap tests and the shared test helpers use import and
import.meta.dirname. The test scripts pass --type-strip-only so tap
leaves lib's .ts subpath imports to Node's own type stripping, as its
TypeScript loader compiled them without a default export. The lua
runner resolves fengari from the test file it is given, since a bare
import from the shared test directory does not reach the plugin's
devDependencies.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 01:10:02 +01:00
Cooldude2606 797d7501c7 Merge pull request #475 from bbassie/feature/plugin-permissions
Declare permissions on the plugin declaration
2026-09-12 01:09:26 +01:00
bbassieandClaude Fable 5.1 30f1b5ea75 Declare permissions on the plugin declaration
Clusterio #988 added a permissions array to PluginDeclaration and a
type level Permissions registry, and made checkPermission and the web
UI hasPermission helpers take PermissionName. Move the definePermission
calls of exp_groups and exp_scenario into the declaration and add the
names to the registry so the web pages type check. The browser
tsconfigs include index.ts so the augmentation is visible to the web
bundle, matching the in-repo plugins.

The exp_scenario table keeps its tuple form and derives the name union
from it, so a new row is still one line. Seed role permissions are typed
as PermissionName, so a typo in seed.ts is now a compile error rather
than a warning at seed time. The tests register the declared
permissions through registerPluginPermissions instead of importing
permissions.ts for its side effect.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 23:27:17 +00:00
Cooldude2606 24d2f76a35 Merge pull request #474 from bbassie/feature/node-ci
Add a Node.js CI workflow that builds and tests inside clusterio
2026-09-11 23:43:13 +01:00
Cooldude2606 2048de6244 Small changes to node ci 2026-09-11 23:41:04 +01:00
bbassieandClaude Fable 5.1 5487976a21 Add a Node.js CI workflow that builds and tests inside clusterio
The packages use workspace:^ and catalog: versions, so they only
resolve inside a clusterio checkout with this repository under
external_plugins. The workflow clones clusterio master, checks this
repository out into external_plugins/ExpCluster, runs pnpm install
(which runs every prepare script), then runs the tap suites.

A last step wipes dist and runs tsc --build in each package on its own.
pnpm install builds packages in dependency order, which is why the
missing project references fixed in #473 went unnoticed. On the tree
before #473 this step fails with the same five TS2307 errors.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 17:56:01 +00:00
phidiasandWeblate 23ee68c50a Further locale change (#471)
* chore(l10n): update translation files

Add-on: Cleanup translation files
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.aperx.org/projects/expcluster/expscenario/
Translation: ExpCluster/ExpScenario

* Update readme.lua

* links

* readme

* chore(l10n): update English translation

chore(l10n): update Chinese (Traditional Han script) translation

chore(l10n): update Chinese (Simplified Han script) translation

chore(l10n): update Chinese (Traditional Han script) translation

chore(l10n): update Chinese (Simplified Han script) translation

chore(l10n): update Chinese (Traditional Han script) translation

chore(l10n): update Chinese (Simplified Han script) translation

chore(l10n): update Chinese (Traditional Han script) translation

chore(l10n): update Chinese (Simplified Han script) translation

chore(l10n): update English translation

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: phidias <phidias@aperx.org>
Language: Chinese (Simplified Han script)
Language: Chinese (Traditional Han script)
Language: English
Progress: 100.0% (1 of 1 strings)
Progress: 100.0% (2 of 2 strings)
Progress: 100.0% (233 of 233 strings)
Progress: 100.0% (4 of 4 strings)
Translate-URL: https://translate.aperx.org/projects/expcluster/explegacy/zh_Hans/
Translate-URL: https://translate.aperx.org/projects/expcluster/explegacy/zh_Hant/
Translate-URL: https://translate.aperx.org/projects/expcluster/exprole/en/
Translate-URL: https://translate.aperx.org/projects/expcluster/exprole/zh_Hans/
Translate-URL: https://translate.aperx.org/projects/expcluster/exprole/zh_Hant/
Translate-URL: https://translate.aperx.org/projects/expcluster/expserverups/zh_Hans/
Translate-URL: https://translate.aperx.org/projects/expcluster/expserverups/zh_Hant/
Translate-URL: https://translate.aperx.org/projects/expcluster/exputil/zh_Hans/
Translate-URL: https://translate.aperx.org/projects/expcluster/exputil/zh_Hant/
Translation: ExpCluster/ExpLegacy
Translation: ExpCluster/ExpRole
Translation: ExpCluster/ExpServerUPS
Translation: ExpCluster/ExpUtil

* chore(l10n): update Chinese (Traditional Han script) translation

chore(l10n): update Chinese (Simplified Han script) translation

chore(l10n): update English translation

Co-authored-by: phidias <phidias@aperx.org>
Language: Chinese (Simplified Han script)
Language: Chinese (Traditional Han script)
Language: English
Progress: 100.0% (452 of 452 strings)
Progress: 95.7% (433 of 452 strings)
Translate-URL: https://translate.aperx.org/projects/expcluster/expscenario/en/
Translate-URL: https://translate.aperx.org/projects/expcluster/expscenario/zh_Hans/
Translate-URL: https://translate.aperx.org/projects/expcluster/expscenario/zh_Hant/
Translation: ExpCluster/ExpScenario

* chore(l10n): update Chinese (Traditional Han script) translation

chore(l10n): update Chinese (Simplified Han script) translation

Co-authored-by: phidias <phidias@aperx.org>
Language: Chinese (Simplified Han script)
Language: Chinese (Traditional Han script)
Progress: 95.7% (433 of 452 strings)
Translate-URL: https://translate.aperx.org/projects/expcluster/expscenario/zh_Hans/
Translate-URL: https://translate.aperx.org/projects/expcluster/expscenario/zh_Hant/
Translation: ExpCluster/ExpScenario

---------

Co-authored-by: Weblate <noreply@weblate.org>
2026-09-09 18:43:29 +01:00
Cooldude2606andClaude Fable 5.1 910285ee2d Reference exp_roles and exp_groups from the exp_scenario build (#473)
* Reference exp_roles and exp_groups from the exp_scenario build

exp_scenario imports types from @expcluster/roles and
@expcluster/permission-groups through their dist folders, so building it
on its own before those packages are built fails with TS2307. A plain
pnpm install runs the prepare scripts in dependency order, but a filtered
install or running tsc --build in exp_scenario on a fresh clone does not.
Referencing the node projects of both packages makes tsc build them first.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Export messages and controller types from exp_roles and exp_groups

exp_scenario reached into @expcluster/roles/dist/node/* for the message
classes and the controller plugin type. The package index now re-exports
messages and the ControllerPlugin type, so consumers import from the
package name like they do with @clusterio/lib. The controller export is
type only, which keeps node code out of the web bundle. The test still
requires the controller classes from dist since they cannot be
re-exported at runtime.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 18:42:18 +01:00
bbassieandClaude Fable 5.1 51786c5df9 Export messages and controller types from exp_roles and exp_groups
exp_scenario reached into @expcluster/roles/dist/node/* for the message
classes and the controller plugin type. The package index now re-exports
messages and the ControllerPlugin type, so consumers import from the
package name like they do with @clusterio/lib. The controller export is
type only, which keeps node code out of the web bundle. The test still
requires the controller classes from dist since they cannot be
re-exported at runtime.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 17:31:31 +00:00
bbassieandClaude Fable 5.1 3a9155697a Reference exp_roles and exp_groups from the exp_scenario build
exp_scenario imports types from @expcluster/roles and
@expcluster/permission-groups through their dist folders, so building it
on its own before those packages are built fails with TS2307. A plain
pnpm install runs the prepare scripts in dependency order, but a filtered
install or running tsc --build in exp_scenario on a fresh clone does not.
Referencing the node projects of both packages makes tsc build them first.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 16:58:09 +00:00
BastiaanandClaude Fable 5.1 24efb82d0f Encode group permissions as a 1-based array (#472)
encode_group_permissions filled the whitelist and blacklist starting at
index 0. Factorio's table_to_json only serialises a table as a JSON array
when its keys run 1..n, so the permissions reached the instance plugin as
an object and every GroupUpdateRequest failed schema validation.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 15:47:01 +01:00
Bastiaan 0854a2b7d5 Move the seed into exp_scenario and seed the permission groups (#457)
The seed roles were owned by exp_roles, but they describe the scenario, so
they now live in exp_scenario/seed.ts next to the permissions they grant.
Each seed role names the permission group its holders belong to, and the
five groups the legacy config defined (Admin, Trusted, Standard, Guest,
Restricted) are seeded through exp_groups along with one role mapping per
role. Mapping priorities follow how exp_roles ranks a player's highest
role, so Jail lands in Restricted regardless of other roles.

SeedRolesRequest becomes exp_scenario's SeedRequest behind a new
exp_scenario.seed permission, and the button moves to the scenario's web
plugin, which gets a web entrypoint for it. exp_roles no longer depends on
exp_scenario, and exp_scenario gains controller tests around the seed.

With groups seeded from the controller the legacy expcore.permission_groups
module and its config are removed, along with the Group rcon static.
2026-09-08 11:20:44 +01:00
phidiasandWeblate f805a1e760 fix readme (#470)
* chore(l10n): update translation files

Add-on: Cleanup translation files
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.aperx.org/projects/expcluster/expscenario/
Translation: ExpCluster/ExpScenario

* fix readme

* layout

* chore(l10n): update English translation

chore(l10n): update Chinese (Traditional Han script) translation

chore(l10n): update Chinese (Simplified Han script) translation

chore(l10n): update Chinese (Traditional Han script) translation

chore(l10n): update Chinese (Simplified Han script) translation

chore(l10n): update Chinese (Traditional Han script) translation

chore(l10n): update Chinese (Simplified Han script) translation

chore(l10n): update Chinese (Traditional Han script) translation

chore(l10n): update Chinese (Simplified Han script) translation

chore(l10n): update English translation

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: phidias <phidias@aperx.org>
Language: Chinese (Simplified Han script)
Language: Chinese (Traditional Han script)
Language: English
Progress: 100.0% (1 of 1 strings)
Progress: 100.0% (2 of 2 strings)
Progress: 100.0% (233 of 233 strings)
Progress: 100.0% (4 of 4 strings)
Translate-URL: https://translate.aperx.org/projects/expcluster/explegacy/zh_Hans/
Translate-URL: https://translate.aperx.org/projects/expcluster/explegacy/zh_Hant/
Translate-URL: https://translate.aperx.org/projects/expcluster/exprole/en/
Translate-URL: https://translate.aperx.org/projects/expcluster/exprole/zh_Hans/
Translate-URL: https://translate.aperx.org/projects/expcluster/exprole/zh_Hant/
Translate-URL: https://translate.aperx.org/projects/expcluster/expserverups/zh_Hans/
Translate-URL: https://translate.aperx.org/projects/expcluster/expserverups/zh_Hant/
Translate-URL: https://translate.aperx.org/projects/expcluster/exputil/zh_Hans/
Translate-URL: https://translate.aperx.org/projects/expcluster/exputil/zh_Hant/
Translation: ExpCluster/ExpLegacy
Translation: ExpCluster/ExpRole
Translation: ExpCluster/ExpServerUPS
Translation: ExpCluster/ExpUtil

* Revert "Translate"

* .

---------

Co-authored-by: Weblate <noreply@weblate.org>
2026-09-08 11:05:08 +01:00
phidias 142d296a02 Merge pull request #455 from PHIDIAS0303/patch-1
Localization
2026-09-06 17:05:04 +01:00
Cooldude2606 58c2ad6fb5 Merge pull request #465 from bbassie/fix/rocket-fired-log
Only log a rocket as fired when its ammo count drops
2026-09-06 01:55:34 +01:00
Cooldude2606 bad80ed77a Merge pull request #467 from bbassie/feat/surveillance-cursor
Show the cursor of the followed player in surveillance
2026-09-06 01:47:29 +01:00
bbassie 397033e8a6 Read ammo slots on join so the first shot is logged
Clearing the slots on leave meant nothing was known about a player's ammo
until it changed, so the first shot after rejoining was missed. The slots
are now read when a player joins and when they respawn, since a new
character starts empty and a stale single round would otherwise count as
a shot. The change handler reads the slots the same way and compares them
with what was stored.
2026-09-06 00:45:53 +00:00
Cooldude2606 1de50da0be Adjust button formatting 2026-09-06 01:44:40 +01:00
bbassie 4d976a9e35 Show the cursor as a slot in the surveillance header
The cursor was a line of text under the camera, which read as an
afterthought next to the framed header. It is now a slot button in the
header after the zoom buttons: the item sprite with its count, greyed out
for a ghost, an empty slot for an empty cursor, and the item name in the
tooltip. The window keeps its shape whatever the item is called.
2026-09-06 00:24:28 +00:00
Cooldude2606 e14e27c158 Merge pull request #466 from bbassie/fix/quickbar-pages
Use page and slot for quick bar filters
2026-09-06 01:08:32 +01:00
Cooldude2606 01bc4cd14d Added constant for total page count 2026-09-06 01:07:19 +01:00
Cooldude2606 09fe5d5f8d Merge pull request #464 from bbassie/fix/vehicle-kills
Count kills and damage dealt from vehicles
2026-09-06 00:42:54 +01:00
Cooldude2606 c4cd7d690d Merge pull request #461 from bbassie/feature/remove-warnings
Remove the warning system
2026-09-06 00:32:06 +01:00
Cooldude2606 98545f1828 Merge branch 'main' into feature/remove-warnings 2026-09-06 00:31:28 +01:00
Cooldude2606 42f177a58e Merge pull request #460 from bbassie/feature/scenario-spectate
Move spectate into exp_scenario
2026-09-06 00:27:52 +01:00
Cooldude2606 e1966dec9c Small format changes 2026-09-06 00:27:14 +01:00
Cooldude2606 bbf036b14b Merge pull request #459 from bbassie/feature/scenario-protection
Move protection into exp_scenario
2026-09-06 00:16:20 +01:00
Cooldude2606 702016b3e2 Remove redundant event properties 2026-09-06 00:15:37 +01:00
Cooldude2606 ac7cdbe01a Merge branch 'main' into feature/scenario-protection 2026-09-06 00:13:41 +01:00
Cooldude2606 d5c0c1005f Merge pull request #458 from bbassie/feature/scenario-jail
Move jail into exp_scenario
2026-09-06 00:04:09 +01:00
Cooldude2606 c12f0a8f89 Remove redundant event properties 2026-09-06 00:03:26 +01:00
bbassie 00253de11f Show the cursor of the followed player in surveillance
A label under the camera shows what the player being followed holds in
their cursor, item and count or the ghost item, so that using a
deconstruction planner or similar can be seen. It is hidden while the
camera shows a fixed location.

Closes #427
2026-09-05 20:54:57 +00:00
bbassie f961a8d82e Use page and slot for quick bar filters
Factorio 2.1 changed get_quick_bar_slot and set_quick_bar_slot to take a
page and slot rather than a single index, and to describe slots holding
records, remotes and specific item instances. Saved filters keep their
single index, which is converted on load and save, and only plain item
filters are saved since the other slot types hold data which has no name
to store. The command is enabled again.

Fixes #444
2026-09-05 20:54:21 +00:00
bbassie ce7a6ae1bf Only log a rocket as fired when its ammo count drops
Factorio has no fired event, so the log was written on every ammo
inventory change, which includes loading rockets for the first time and
swapping between rocket types. The last seen name and count of each ammo
slot is now kept per player, and a shot is only logged when a slot holds
the same ammo with one fewer, or empties from a single round. Only the
three configured ammo types are logged, rather than every ammo change.

Fixes #242
2026-09-05 20:53:57 +00:00
bbassie 37eff4b3e9 Count kills and damage dealt from vehicles
The statistics only credited damage and kills when the cause was a
character, so anything done from a car, tank, spidertron or locomotive was
lost. The cause is now resolved to a player through the vehicle's driver,
or passenger when there is no driver, and turrets, remotes and uncrewed
trains still count for nobody.

Fixes #219
2026-09-05 20:53:56 +00:00
bbassie a59b2a8414 Remove the warning system
Warnings were a moderator tool layered on top of reports and had no
callers left outside their own command and the player list button. The
legacy module, its config and locale, the exp_scenario command and its
permissions, the discord alert block and the player list button are all
removed. Reporting from the player list no longer depends on whether the
player could give warnings, anyone can report a player who is not immune.
2026-09-05 19:22:37 +00:00
bbassie 8014cb1e77 Move spectate into exp_scenario
The spectate and follow module moves to exp_scenario/module/control/spectate.lua
in the event_handler shape the other control modules use, and the copy in
exp_legacy is removed. Follow state is a typed record rather than a
positional table, the follow label caption is localised, and the escape
handler marks the record to stop on the next tick instead of poking an
invalid position into it.

The LuaPlayer.close_map call is gone: it does not exist in Factorio 2.0,
where the map is the remote controller and set_controller replaces it.
2026-09-05 19:20:55 +00:00
bbassie 0c52532c05 Move protection into exp_scenario
The entity protection module moves to exp_scenario/module/control/protection.lua
in the event_handler shape the other control modules use, and the copy in
exp_legacy is removed. The config lookups are built as local sets instead of
rewriting the config tables in place, the two mined handlers become one, and
the entity and area key helpers are exposed so the protection command no
longer keeps its own copies. Event ids are exposed as
Protection.on_player_mined_protected and Protection.on_repeat_violation.
2026-09-05 19:19:45 +00:00
bbassie 2e53e4668d Move jail into exp_scenario
With the role system finished the jail module has nothing legacy left in
it, so it moves to exp_scenario/module/control/jail.lua and the copy in
exp_legacy is removed. The module takes LuaPlayer only, requires a reason,
returns false rather than nil when there is nothing to do, and exposes its
event ids as Jail.on_player_jailed and Jail.on_player_unjailed in the same
shape as exp_util's selection events.
2026-09-05 19:18:04 +00:00
Cooldude2606 810269971b fix: bump to 7.0.1 2026-09-05 17:14:37 +01:00
Cooldude2606 0d0f08bcdc Merge pull request #454 from PHIDIAS0303/patch-1
GroupViewPage.tsx double static
2026-09-04 17:24:36 +01:00
phidias 72cd203c25 Update GroupViewPage.tsx 2026-09-05 00:57:13 +09:00
Cooldude2606 f8ccc87edc Merge pull request #453 from bbassie/feature/roles-migration
Move the scenario onto exp_roles and remove the legacy role system
2026-09-01 09:28:34 +01:00
bbassieandClaude Fable 5 08fe6b688e Type the test environments for autocomplete
Building on the class annotations from the smoke test changes:

- The suite is generic over its environment: Framework.suite is
  @generic T : Stubs with the extension returning T, and Suite<T> passes T to
  every test function. A test file declares
  `local Suite = ... --- @type Suite<ExpRoles.TestEnv>` and env autocompletes
  from there.
- The stubs declare their recorded fields, add_player returns Stubs.Player,
  and the server is one.
- ExpRoles.TestEnv : Stubs declares the extension fields, so env.R returns
  ExpRoles.Role and env.Roles is the module.

The typing immediately paid for itself by flagging real drift in the tests,
now fixed: _script_data is package private so the env exposes script_data()
instead, the join event carries its name and tick, the assignment record type
admits is_deleted, admin_state is a file local rather than an undeclared
field, and the event assertions which indexed [1] without a nil check compare
the whole event list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 06:09:40 +00:00
Cooldude2606 59df8bdff3 Manual changes following smoke test 2026-09-01 01:04:09 +01:00
bbassieandClaude Fable 5 574a0a4b52 Address final review nits on the assertions
- Suite.empty(value, name) checks a table has no entries and reports what it
  held, and Suite.throws(fn, message, name) asserts a function errors with the
  given message, so an unrelated error no longer passes as the expected one.
- Scalar comparisons and zero length checks go through eq and empty, and the
  announced messages and print output are compared by content, so a failure
  shows the actual data.
- get_roles() is asserted through sorted names rather than a count.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 10:48:43 +00:00
bbassieandClaude Fable 5 3218351db6 Address review on the suite and instance tests
- The instance tests run against a real Instance with a real InstanceConfig,
  matching the controller tests: the spies are sendTo and the server, which
  record what leaves the instance, so sendRcon passes through the real script
  command gate. The plugin's config fields are registered so the sync mode is
  a real field.
- Shallow eq is gone and eq compares recursively; the suite is created with
  Framework.suite(extend_env), which hands the extension a fresh set of stubs
  per test, so the plugin's env.lua is only the extension.
- add_player assigns continuous indexes itself.
- The lua fixtures are referenced by role name: assignments take names, and
  ids in expectations come from the role object. The name lookup in env.R
  uses a fixture index rather than searching the roles.
- Test names format functions as name() and methods as .name().
- Branch coverage filled in: sort tie breaking, the highest role assertion
  when a player has no roles, has all for the server, the by player default
  from game.player, deleted assignment records on initialise, set_emit_events
  with no argument, printing to a role with no holders, role property update
  validation, the assignment subscription replay, unassignment over the ipc,
  and a refused removal having nothing to roll back.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 21:11:58 +00:00
bbassieandClaude Fable 5 e6552a39be Address review on the stubs and controller tests
- The controller tests run against a real Controller, which is side effect
  free while not started, with real users through its UserManager. The only
  fakes left are two spies which record broadcasts and permission pushes on
  their way through. Since any user update also applies auto assignment, the
  leave event is observed directly rather than through a role change.
- raise_event fills in the name and tick of the event the way factorio does,
  rather than wrapping the payload, and game.players finds players by index
  as well as by name through a metatable.
- names is generic over anything with a name property, so it lives on the
  suite; the save and load helper no longer calls on_load, the test calls the
  handler itself as it does for the other handlers.
- Sent messages and events are compared whole with deep_eq, which pins the
  event shape, the tick, and that the unassign key is absent rather than
  empty.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 16:18:09 +00:00
bbassieandClaude Fable 5 7adcdde9dc Address review on the test framework
- The registry is a Suite, created around the environment factory it runs its
  tests with, so the plugin's env.lua reads as: build environments, hand the
  test files a suite of them. The suite returned there becomes `...` in each
  test file, which is now said where it happens.
- pass and fail are the primitives every other check goes through. eq and
  deep_eq assert rather than compare, failing with both values in the detail.
- Stubs are extended through extend_requires, extend_script, extend_game and
  extend_defines, a recursive merge which raises when a value already exists,
  rather than by mutating the tables. The strict labels carry the factorio
  class names.
- The stubs record registered metatables and can save and load the script
  data the way factorio does: functions are refused and only registered
  metatables survive. env.save_load() uses it, and a new on_load test shows
  role methods and held roles surviving the round trip, which only passes
  because the module registers its metatable.
- The names helper moved out of the shared framework, it is role specific.
- Tests are named after the function or method they cover, on both sides:
  the lua tests read as "role:assign applies locally and is sent", and the
  javascript files wrap their tests in the class they exercise with subtests
  per method. module.test.js is control.test.js, after the file it covers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 22:21:39 +00:00
bbassieandClaude Fable 5 24d36db601 Address review on the test harness
- The generic parts move to test/ in the repository root so other plugins can
  reuse them: the factorio and clusterio stubs, the test framework, the
  fengari runner, and clusterio's testMatrix and round trip helpers. A plugin
  composes them from its own env.lua, which adds its stubs and fixtures.
- Tests are declared with Test.test(name, fn) and every test function
  receives a fresh environment, so nothing carries over between them. A test
  which errors is reported as a failure rather than aborting the file.
- Every stub raises on properties it does not implement, which mirrors the
  game api. game.player is the one property allowed to read as nil.
- Test.deep_eq compares tables recursively with keys checked from both sides.
- The message records round trip through the same testMatrix and
  testRoundTripJsonSerialisable helpers the clusterio tests use, covering
  every optional field combination of the records, events and requests.
- controller.test.js and instance.test.js cover the node side of the plugin
  against faked controller and instance internals: property creation and
  sweeping, record building, broadcasts, subscription replay, assignment
  validation, auto assignment and its blocking role, seeding, the initialise
  payload, sync mode gating, and the rejection rollback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 16:45:51 +00:00
bbassieandClaude Fable 5 59a0e0be13 Build the effective roles in a single pass
The list restarts whenever a higher priority is found, rather than collecting
every role and filtering afterwards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 16:45:51 +00:00
bbassieandClaude Fable 5 e79822b26e Add a test suite for the roles plugin
Run with `pnpm --filter @expcluster/roles test`, using tap as the runner.

The lua module is exercised for real: each test file runs in its own lua
state, created on first use by the helper, with the factorio surface and the
clusterio modules stubbed. The environment is passed to the chunk as an
argument so no globals are involved. Covers lookups and comparisons, the
permission checks, assignment with confirmation, rejection and local only
roles, jail suppression, the sync entry points, and the holder listings,
including a player holding a role in both the synced and the local list.

The javascript side covers the message records round tripping through their
schemas, the indexed permission encoding, and the seed: every permission it
grants must be defined by exp_scenario, and every parent must exist and have
its permissions carried into its children.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 22:02:51 +00:00
bbassieandClaude Fable 5 8a15745cf8 Address review on the role lookups
- player_has_any_permission and player_has_all_permission, matching the
  account checks the web ui has.
- Roles are no longer sorted on the way out of every lookup. get_ordered_roles
  and the public sort_roles cover the two guis and the command which present
  roles in order, and the highest role is found with a single scan.
- get_held_role_ids returns the list and the set it already built, rather than
  a second function rebuilding the set from the list.
- get_player_names collects into a set before listing, so a player holding the
  role in both the synced and the local list is counted once.
- The role metatable is registered directly under the plugin name, dropping
  the storage import.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 22:02:51 +00:00
bbassieandClaude Fable 5 bea8d46c82 Address review on the role api
- Roles are objects and everything done to or with a role is a method on it:
  assign, unassign, has_player, has_permission, is_higher_than,
  is_lower_than, get_players, get_player_names, print. Assignment has one
  entry point, role:assign(player, options), with local_only as an option
  rather than a second function.
- Roles are looked up by clusterio id with get_role; get_role_by_name searches
  the list for the few places, such as configs, which only know a name. The
  name map and the ordered list are gone, get_roles sorts on demand and the
  index field is replaced by the comparison methods.
- Players are LuaPlayer objects only, with nil or index 0 for the server.
- get_higher_roles and get_lower_roles replace print_to_roles_higher and
  print_to_roles_lower, call sites loop over them with role:print.
- Permission groups are removed from roles again, exp_groups owns the mapping
  from roles to groups.
- Seeding is a SeedRolesRequest behind a button on the roles page rather than
  running on first start, and creates only the roles; the player assignments
  are dropped. The seed lists each permission once at the lowest role which
  has it and lets the parent chain carry it up.
- System commands unlock for core.admin rather than a permission of their own.
- Role metatables are registered with Storage.register_metatable so the
  methods survive save and load, which the role records in storage needed.
- The player list auth uses Roles.player_outranks directly, and the event
  carries role ids rather than names.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 15:52:50 +00:00
bbassieandClaude Fable 5 78a533a6f6 Split the seed into the role and assignment halves
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:39:51 +00:00
bbassieandClaude Fable 5 48d06ee996 Seed the legacy roles the first time the plugin runs
When the role properties datastore is empty the plugin has not run before, so
the roles the scenario used to define are created on the controller, along
with the players the config listed. This replaces the role config which was
loaded into every map.

The seed keeps the parent relationships of the old config and flattens them
into the permissions of each role, since clusterio roles do not inherit. The
default and admin roles already exist, so those entries only set the in game
properties; the default role gets its permissions through grantByDefault.

Roles which already exist by name are reused and only gain the seed
permissions, so seeding an existing cluster is safe. Permissions which are
not defined are logged rather than refused, in case a plugin is not loaded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:36:58 +00:00
bbassieandClaude Fable 5 7034f17b5d Move the scenario onto exp_roles and remove the legacy role system
Every call site of expcore.roles now uses exp_roles, and the legacy module,
its config, and the glue which refreshed guis on role events are deleted.
Where a file only renamed the require and the permission strings the change is
mechanical; the rest:

- Jail is now "give the Jail role" and unjail "take it away". The role has a
  higher priority than every other so holding it suppresses them, which is
  what stashing and restoring the roles was for.
- The command role authority derives exp_scenario.command.<name> from the
  command name, and the role parsers use player_outranks rather than comparing
  indexes with their own root check.
- The admin and spectator triggers, and the gui refresh on role changes, live
  in exp_scenario/control/roles.lua; the system commands trigger stays with
  the command authority.
- The player list warn button is keyed on create_warning, the permission
  the command behind it already required, and report on create_report. Both
  were keyed on names no role held, so only root ever saw them.
- The warps and tasks configs say exp_roles where they said expcore.roles.
- The role tables the readme and player list read are replaced by
  get_player_names and get_roles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:36:58 +00:00
bbassieandClaude Fable 5 eb715cc176 Define the scenario permissions by name
With the lua side checking permission names directly there is no transform to
derive them from, so each permission is listed with its name. This was also
the chance to drop the legacy action and flag buckets, which only reflected
how the old config was written:

- exp_scenario.bypass.* for entity protection, nuke protection, the
  deconstruction log, and reports.
- exp_scenario.decon.* for the two deconstruction levels, with descriptions
  which say what they gate.
- exp_scenario.player.* for admin, spectator, instant respawn, and system
  commands.
- exp_scenario.chat.commands, and exp_scenario.gui.player_list.kick and .ban
  for the player list buttons which were never commands.

Commands derive their permission as exp_scenario.command.<name>, so
assign-role, unassign-role, and get-roles get scenario permissions rather
than the core ones they mapped to before. The in game command is bounded by
the lower role check, while core.user.update_roles is not, so granting it to
moderators would have let them change any role from the web ui.

Dropped: defer_role_changes, which priority replaced; command/give-warning,
which no role held and the player list now checks create_warning for; and
command/report, which was never defined. clear-tag/always is renamed to
tag_clear.always to match the command it belongs to. _ipc and _sudo are added
so every command has a definition.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:36:58 +00:00
bbassieandClaude Fable 5 ad9a072661 Redesign the exp_roles lua api around permissions
The module no longer presents the interface of the legacy expcore.roles
module. Nothing outside this repository depends on it, so rather than carry
the legacy action strings and the transform which mapped them onto permission
names, call sites now check the clusterio permission name directly. That
removes the one invariant which silently broke every check if the lua and
typescript transforms drifted, and makes a check in lua greppable against
its definition.

- player_allowed and player_has_flag become player_has_permission; flags were
  only permissions with a change trigger, which define_permission_trigger now
  provides for any permission.
- on_role_assigned and on_role_unassigned become one on_player_roles_changed
  event carrying the assigned and unassigned names. Every consumer registered
  both for the same handler. It is also raised for connected players when a
  role is edited on the controller, which the old events never were, and for
  changes made on the controller to the roles a player holds.
- player_outranks and player_outranks_role replace the repeated comparison of
  highest role indexes, and apply the core.admin bypass consistently, which
  two of the six call sites did not.
- get_role takes a name, clusterio id, or role; get_roles replaces
  get_roles_ordered. The config views of the roles are gone, with
  role:get_player_names covering the one use of config.players.
- Roles carry a permission group, which the legacy system mapped roles to and
  the first version of the plugin dropped. A player is moved into the group of
  their most privileged role which names one. It is edited with the other in
  game properties.
- skip_checks is dropped from assign_player and unassign_player.

A player object with index 0 is treated as the server, which is how
exp_commands represents rcon.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:36:58 +00:00
Cooldude2606 893334b273 Merge pull request #450 from bbassie/feature/emmylua-lint
Migrate the lua lint from luals to emmylua
2026-08-10 12:08:54 +01:00
bbassieandClaude Opus 5 fd38816ff6 Write the factorio typedefs where the config looks for them
`fmtk luals-addon <outdir>` treats its argument as the parent and always
appends `factorio`, so the bundle landed in `$RUNNER_TEMP/factorio/factorio`
while the config pointed at `$RUNNER_TEMP/factorio/library`. The factorio
types were never loaded, which is why ci reported 642 undefined-global
and 1204 type-not-found against a tree that is clean locally.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 09:34:23 +00:00
Cooldude2606 5890155331 Final manual fixups 2026-08-08 23:09:39 +01:00
Cooldude2606 1e861c5c0b Fix issue with MapPosition.energy 2026-08-08 21:58:46 +01:00
bbassieandClaude Opus 5 d495a345d6 Resolve clusterio through CLUSTERIO rather than in ci only
The library path was injected by the workflow, so an editor opened on
this repo could not resolve `modules/clusterio/*`. It now comes from
the environment in both places.

It cannot be written relative to this repo: emmylua does not normalise
`..` in a library path, and an absolute path containing `..` fails the
same way, so the location has to come from outside.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 16:04:16 +00:00
bbassieandClaude Opus 5 619ed4b39f Remove asserts the types already cover
The nil check burn down added guards which turned out redundant once
the surrounding annotations landed. `_has_handlers` is set to true when
the first handler registers, so it is a boolean rather than the literal
false it was inferred as.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:48 +00:00
bbassieandClaude Opus 5 f907007d14 Fix arity and return value mismatches
`assert` returns every argument it is given, so `return assert(x, msg)`
was leaking the message as a second return value.

`get_tile` is documented as taking x and y. The async function class
declared `@operator call` with no parameters, which made every async
call look over supplied.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:48 +00:00
bbassieandClaude Opus 5 2fece14a45 Enable the diagnostics which already pass
These were carried over as disabled from the luals config but report
nothing, so they only ever masked a future regression.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:48 +00:00
bbassieandClaude Opus 5 f5656025ee Clear the last of the nil checks
Guards now cover the statements that follow them, and a couple of reads
are restructured so the narrowing survives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 76ecc37d3f Type the legacy role registry
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 300196bc76 Assert the remaining optional reads in exp_util and commands
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 b5bbf73332 Patch MapPosition and BoundingBox to the named form
The api documents both as a union with a positional array because both
are accepted as input, which makes every read of `.x` or `.left_top`
optional. Everything read back from the game uses the named form, so
the positional variant is removed and we now always write it that way
too.

The spawn area config offsets and the mine depletion search areas were
the only positional writes left, and apply_offset no longer needs its
fallback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 231c3a9ea0 Narrow the gui player lookups
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 3c0b5069fc Assert optional api results across the scenario modules
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 f9ce3c4145 Assert row lookups and narrow force and style reads
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 0b31a70801 Type forward declared handlers and assert element parents
The player list action setter runs during control setup, before any of
the callbacks that use it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 aca8bc79ae Assert the vlayer gui lookups
Dropdowns are siblings of the button that reads them, and the loop
bounds come from the interface count.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 d44381bac1 Assert the vlayer gui lookups
Dropdowns are siblings of the button that reads them, and the loop
bounds come from the interface count.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 7d8de94c2f Assert row lookups and the position shorthand
spawn_area accepts both position forms on purpose, so it asserts that
one of the two is present rather than narrowing the type.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 b75e283fc0 Narrow the remaining position, box and colour reads
Color.struct leaves every channel optional, so the rainbow command
declares its own fully populated colour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 981eed7f34 Assert module inserter row and selector lookups
Rows and selectors are created alongside the machine selector, so they
exist for anything already in the table.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 d25fc42538 Assert vlayer runtime invariants and type its storage
The interfaces are chests and constant combinators, so the inventory,
logistic sections and first circuit section always exist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 7bb874f73f Describe the legacy role type
get_player_roles always returns at least the default or root role, so
get_player_highest_role never returns nil, and callers were right to
treat it that way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 ce3b490554 Assert the debug frames exist
`debug.getinfo` is optional, but these helpers always ask for a frame
which is on the stack.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 bad00a9cef Describe the task list element data by key type
A union of table types makes every lookup optional. Separate index
signatures on one class say the same thing without the nil.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 a4213dadf5 Narrow positions and bounding boxes at the boundary
Values read back from the game always use the named members, so they
are narrowed once where they enter a function rather than at each use.

`LuaControl.force` is a ForceID union, so reading a force only method
off it needs the cast the rest of the repo already uses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 3f46055b70 Enable need-check-nil and start the burn down
`Roles.new_role` returned the result of `error`, which made it nullable,
and `set_permission_group` returned nil for an unknown group, which
broke the config chain with an index error rather than saying so.

Positions and bounding boxes read back from the game always use the
named members, so they are narrowed once where they enter a function.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 1868616a4d Note the emmylua setup for contributors
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:18 +00:00
bbassieandClaude Opus 5 f39da0c585 Type the last of the forward declarations
Locals declared ahead of an assignment inside a callback were inferred
as nil at every use site.

The role event handlers are given a class so the table is not unified
with the other module handler tables.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:18 +00:00
bbassieandClaude Opus 5 5aeafa0e11 Fix child lookups, cache keys and loadstring
`loadstring` was removed in Lua 5.2, `load` takes a string the same way.

Warp gui child lookups are optional, so they use the same assert the
rest of the repo does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:18 +00:00
bbassieandClaude Opus 5 b07052307d Fix stale vlayer signals and index types
`LogisticFilter.signal` became `value` in 2.0, so the clear loop broke
on the first slot and outdated signals were left behind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:18 +00:00
bbassieandClaude Opus 5 4745b6de0b Give the role methods an explicit self
emmylua takes self from the owner table, so `@param self ExpRoles.Role`
only applies with dot syntax. Call sites still use a colon.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:18 +00:00
bbassieandClaude Opus 5 19fd1eabbe Correct container data keys and callable overloads
Autofill stores the whole per entity map against a player.
The readme container is called as a define, so it needs the overload
the other element classes carry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:18 +00:00
bbassieandClaude Opus 5 d78ae40c5d Correct partial table types
`_display_data` is a cache keyed by force name, not a single record.
The auto complete accumulator starts empty, so its fields are optional.
Game commands have no usage beyond their own help text.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:18 +00:00
bbassieandClaude Opus 5 1387b8a2db Fix gui element registration and iter removal
The top, left and relative element tables are keyed by the define, but
the duplicate registration assert looked up `define.name`, so it never
fired.

GuiIter.remove_element indexed `registered_scopes` by player index
instead of the scope elements it had just fetched, so it removed
nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:18 +00:00
bbassieandClaude Opus 5 589b876865 Fix a nil config read and two key types
`config.temp_warning_limit` does not exist, the warnings config defines
`script_warning_limit`, and the locale expects it as `__3__/__4__`, so
the command was printing nil.

map_tags is keyed by the force name joined to the tag number, and
custom_color is the Color union.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:18 +00:00
bbassieandClaude Opus 5 164672602d Make the @as casts actually apply
emmylua only parses the inline cast as `--[[@as T]]`, the spaced form
`--[[ @as T ]]` is treated as a plain comment, so all 149 of them were
doing nothing.

LuaGuiElement.style is a union because a style name can be assigned to
it, so reading it back needs the cast.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:18 +00:00
bbassieandClaude Opus 5 fd2f5a9589 Type the player bonus element data and config
`data` on an element is the GuiData store keyed by element, not the
value being stored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:18 +00:00
bbassieandClaude Opus 5 8ae1144585 Fix logistic network stats for the 2.0 api
`get_contents` and `targeted_items_pickup`/`_deliver` return lists of
ItemWithQualityCount in 2.0, they are no longer name keyed dictionaries,
so the logistorage stats were indexing by array position.

PreferenceEnum is bidirectional, matching what ExpUtil.enum returns.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:18 +00:00
bbassieandClaude Opus 5 2032cadc1b Fix rocket info silo pruning and unit number keys
The prune loop shadowed its own parameter, so `silo_data[unit_number] =
nil` wrote into the entry instead of removing it from the table.

`unit_number` is optional on LuaEntity but always set for a silo. Note
that `--[[ @as ]]` does not narrow for an index expression, only
`@cast` does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:18 +00:00
bbassieandClaude Opus 5 ecf236d831 Describe the Datastore type
The registry and the prototype were both bare tables, so every
`datastore:method()` call resolved to nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:18 +00:00
bbassieandClaude Opus 5 ab89b9feca Type vlayer forward declarations and config entries
The gui element locals are declared at the top and assigned far below,
so emmylua inferred them as nil at every use site.

circuit_oc was reassigned from a control behaviour to a logistic
section, which hid every section method behind the wrong type.

allowed_items entries carry heterogeneous optional properties, now
described by a class.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:18 +00:00
bbassieandClaude Opus 5 0bb92966eb Fix type annotations the linter found to be wrong
Async ids come from `get_function_name`, which returns a string, but
were documented as numbers.

`setmetatable` in clusterio's compat module loses `LibCompat` on the
module return, so the require is cast.

`GuiData.__index` accepts a `DataKey` but emmylua needs that as an
index signature on the class.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:18 +00:00
bbassieandClaude Opus 5 c233a9ab06 Defer need-check-nil and clean up stale suppressions
emmylua deserialises an unknown diagnostic code to a catch all that
matches no checker, so every `invisible`, `nil-check` and
`global-element` suppression was silently doing nothing. Renamed to the
emmylua codes. `name-style-check` has no equivalent and is dropped, it
was already disabled under luals.

The `get_tile` suppressions referenced an api typedef bug from 2024
which no longer reproduces.

need-check-nil is deferred rather than disabled on merit: a third of the
407 findings come from MapPosition and BoundingBox being aliased as
`struct|[double, double]`, so every `entity.position.x` reads as
possibly nil. The rest need per site knowledge of runtime invariants.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:18 +00:00
bbassieandClaude Opus 5 b9cfbc9036 Narrow aabb types and give generic aliases their type argument
The aabb functions all index the named members, so passing the
shorthand [MapPosition, MapPosition] form would error at runtime. The
annotations now say so.

emmylua requires a type argument on a generic alias, luals did not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:18 +00:00
bbassieandClaude Opus 5 90f32699c7 Fix duplicate class and annotation placement lint errors
Classes contributed to by more than one file now carry the `partial`
attribute. trains.lua declared its command as `ExpCommand_Artillery`,
which was a copy paste.

`Color.0` and `MapPosition.0` became `.struct` in fmtk 2.1.6.

emmylua rejects `@type` on a function statement or a `do` block, so
those are moved onto the local or replaced by `@param` and `@return`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:18 +00:00
bbassieandClaude Opus 5 c6da1fd9ac Replace the luals lint config with emmylua
fmtk 2.1.4 onwards generates typedefs targeting emmylua that luals
misreads, and every fmtk changelog since 2.1.5 states luals support is
being dropped. luals 3.18.2 is already the latest release, so there is
nothing to bump.

The `--clusterio-modules` plugin has no emmylua equivalent, as emmylua
has no plugin interface, so it is reimplemented as `workspace.moduleMap`.
Those rules rewrite each file's own module path rather than the require
string, hence the reversed direction.

type.patch.lua is obsolete: `raise_event` now takes `LuaEventType` in
the generated typedefs, and `LuaObject` has become `LuaObject.base`.
exp_scenario/.luacheckrc was already dead, luacheck is never invoked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:18 +00:00
BastiaanandClaude Opus 5 70471c5085 Pin factoriomod-debug to 2.1.3 in the lint workflow (#449)
FMTK 2.1.4 onwards reshapes its generated typedefs for EmmyLua, which
LuaLS misreads: 2.1.4 changed dicts to table<K,V> (game.surfaces.nauvis
now reports undefined-field) and 2.1.5 changed LocalisedString to the
variadic tuple [string, LocalisedString...], which LuaLS 3.18.2 cannot
match against a plain string. Together those account for all 279
findings CI has reported since 2026-07-13; with 2.1.3 the check is
clean.

Upstream FMTK has announced it is dropping sumneko/LuaLS support in
favour of EmmyLua, so this pin is a stopgap until the check is ported
to emmylua_check.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 21:32:43 +01:00
Cooldude2606 a324388bc8 Merge pull request #448 from bbassie/feature/roles-plugin
Add exp_roles plugin and scenario permissions
2026-08-06 15:45:04 +01:00
Cooldude2606 486151723e Fix game properties UI on small screens 2026-08-06 09:55:45 +01:00
bbassieandClaude Opus 5 1943d8743d Left align the role property labels
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 07:19:36 +00:00
bbassieandClaude Opus 5 dcba1eef1c Address review on the roles plugin
- Use the core role permissions rather than defining new ones. Assignments are
  only sent between the controller and an instance, so they are no longer
  addressed to control and need no permission at all.
- Split the datastore reconciliation into ensureRoleMeta, sweepRoleMeta and
  applyAutoAssign, all run on init as well as when roles change. Properties left
  behind by a role deleted while the plugin was not running are now swept up,
  where before they would be inherited by the next role given that id.
- Use a switch in onControllerConfigFieldChanged, matching instance.ts.
- Resolve pending assignments on initialise. A pending role is either confirmed,
  and so now held by synced_players, or it never landed; either way it stops
  being held locally. Roles assigned with assign_player_local are untouched.
- Drop the emit_updates juggling in reject_assignment, sync is already false.
- Send each permission name once and reference it by index, see the benchmark in
  the pull request. Single role updates stay in the plain form.
- Lay the role properties out in columns, name the apply button for the section
  it belongs to, and give every field a tooltip.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 08:04:36 +00:00
phidias 4255522c57 Research Log (#447)
* Update research.lua

* Fix config file references in research milestones
2026-08-04 14:35:26 +01:00
bbassieandClaude Opus 5 fcfa174794 Fix LuaLS findings in the roles module
Checked with the same LuaLS version and factorio library CI uses. The module now
reports no findings; the 279 which remain are all pre-existing and main is
already failing on them.

Most were annotations rather than behaviour. Two are worth noting:

- The role prototype is now its own class which the role inherits, since
  defining methods on a table annotated as the role counted as injecting fields
  into it.
- The role change message is built from a literal locale key per branch. Building
  the key by concatenation gives a plain string where a localised string is
  wanted, which is the same finding the legacy module reports.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 09:39:04 +00:00
bbassieandClaude Opus 5 bbd3b7c1f2 Add exp_roles plugin to sync roles from the controller
Clusterio already stores roles, the permissions they grant, and which user holds
which role, along with a web UI for all three. What it does not have is the
properties a role only needs in game, or a way for an instance to learn about
any of it, since role and user updates are only sent to control connections.

This plugin fills both gaps. The controller keeps a record per role holding the
order, priority, short hand, tag, colour and auto assign threshold, created
automatically for any role which does not have one. It then rebroadcasts roles
and assignments on its own events so instances can follow them.

The lua module presents the same interface the legacy expcore.roles module did,
so the call sites can be moved over without being rewritten. Permission checks
translate the legacy action strings using the same mapping exp_scenario defines.

Two things replace features the legacy system had:

- Priority replaces disallow. Only the roles with the highest priority a player
  holds are considered, so Jail can suppress every other role including the
  default one, without needing to take roles away first.
- Assignments made in game are applied locally and then sent to the controller,
  which keeps assign_player synchronous for callers. A role which should never
  leave this map, such as one earned from time on the map, is assigned with
  assign_player_local instead.

Roles earned from online time across the cluster are granted by the controller
from the threshold on the role, using the online time clusterio already tracks.

Nothing requires this plugin yet; moving the call sites off expcore.roles and
removing the legacy config is left for a follow up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 09:28:40 +00:00
bbassieandClaude Opus 5 e852abc309 Define scenario permissions for legacy role actions
The role system is moving onto clusterio's own roles and permissions, which
requires every in game action to exist as a real permission rather than a bare
string held only by the lua config.

Adds a permission for each of the 104 actions and 7 flags used by the legacy
role config. Names are derived from the legacy action by a deterministic
transform, exported so the lua side can apply the same mapping and keep existing
call sites working. Command descriptions are taken from the existing locale
entries so they match what players already see.

Actions held by the Guest role are marked grantByDefault, since the in game
default role and clusterio's default role are the same concept.

Role management actions have no scenario permission; they map onto
core.user.update_roles and core.role.list instead.

Also corrects the /unjail description, which was a copy of /jail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 09:11:26 +00:00
Cooldude2606 a07be7b7ed Bump version in package jsons 2026-07-14 00:50:00 +01:00
Cooldude2606 50daa81a4a Fix require for /debug 2026-06-29 01:14:39 +01:00
phidias c4f9c97130 remove limit (#446)
* Update surface.lua

* .

* .
2026-06-28 21:53:43 +01:00
Cooldude2606 4fe0e9c0a6 Add AABB.size (#445) 2026-06-27 14:02:49 +01:00
Cooldude2606 27b1a34a83 Merge pull request #443 from Cooldude2606/fix/lint
Fixes many lint issues and other bugs
2026-06-26 15:58:47 +01:00
phidias 1f3689a0ac entity api change (#442)
* Update warps.lua

* .

* .
2026-06-26 15:45:46 +01:00
Cooldude2606 7890540a3e Final lint fixes 2026-06-26 15:40:53 +01:00
Cooldude2606 8b3c437847 Move debug GUI out of legacy 2026-06-26 15:22:06 +01:00
Cooldude2606 95bb8ac304 Move logo out of legacy 2026-06-26 15:09:11 +01:00
Cooldude2606 7be2f8d490 Fix lint errors 2026-06-26 15:02:08 +01:00
Cooldude2606 9019be307c Update to new version of SubscriptionRequest 2026-06-26 14:51:43 +01:00
Cooldude2606 3b6163451c Fix missed use of bonus.initial_value 2026-06-26 14:34:12 +01:00
Cooldude2606 147c586c4b Remove invalid item from file loader 2026-06-26 14:30:35 +01:00
Cooldude2606 f4766cd20e Fix redirect after group creation 2026-06-26 14:21:28 +01:00
Cooldude2606 9755ec8e3a Use /sc rather than /c 2026-06-26 14:20:32 +01:00
Cooldude2606 6b99378fa2 Fix use of MapSubscriber 2026-06-26 14:20:10 +01:00
Cooldude2606 aa75a43aa9 Ignore web dirs for lint 2026-06-26 14:19:15 +01:00
phidiasandCooldude2606 71ab3e7be4 clear blueprint changed to selection (#435)
* Update surface.lua

* Update en.cfg

* Update surface.lua

* Update en.cfg

* Fix description keys for ground items and blueprints

* Update roles.lua

* Update surface.lua

* Update quick_actions.lua

* Update en.cfg

* Update en.cfg

* Update surface.lua

* Update zh-CN.cfg

* Update zh-TW.cfg

* .

* .

* .

* Many fixes and reverts

---------

Co-authored-by: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com>
2026-06-26 13:15:49 +01:00
phidias cb89c08c9f Update vlayer.lua (#440) 2026-06-26 12:43:00 +01:00
Cooldude2606andClaude Opus 4.8 3a46ec4fa4 Migrate player list gui to scenario (#421)
* Migrate player list gui to scenario

Move modules/gui/player-list into exp_scenario/gui/player_list using the
new exp_gui element API, matching the other scenario guis.

- Store the selected player and action on the container's player data
  instead of a datastore, folding the datastore on_update reactivity
  into explicit setter functions
- Switch the actions config to injected accessor functions and the
  exp-gui_player-list locale namespace
- Add the locale section, register in control.lua, and disable the
  legacy module in the file loader

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Align names and call patterns to best practice

Apply the conventions from the rocket info gui review to the player list,
and remove the now migrated legacy module.

- Move row building onto the table def as calculate_row_data, and refresh
  via refresh_player which computes the row data once per player
- Store the selection on the container with private/public accessor
  methods instead of poking element data directly
- Store the selected player as a LuaPlayer rather than a string and update
  the actions config to match
- Use :style{} on the player table and make the action and reason bars
  their own element definitions
- Delete the migrated legacy gui and its file loader entry

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Address player list review feedback

- Rename the player table element data class to element_data
- Remove the redundant offline selection clear; on_player_left_game
  already clears any selection pointing at a player who disconnects
- Move the action bar and reason bar visibility into their own refresh
  methods so refresh_player just delegates to them

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Style and locale fixes

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com>
2026-06-26 12:42:33 +01:00
Cooldude2606 31c18c7851 Style and locale fixes 2026-06-26 12:41:49 +01:00
phidias 3a8d83f981 research changes (#430)
* Add event handlers for research reversed and queued

* Update roles.lua

* Remove bonus inventory settings from research.lua

Removed bonus inventory configuration for mining productivity.

* Update en.cfg

* Update zh-CN.cfg

* Update zh-TW.cfg

* Update research.lua

* Delete exp_scenario/module/commands/research.lua

* Update quick_actions.lua

* Update research.lua

* Update research.lua

* Update research.lua
2026-06-26 11:56:24 +01:00
phidiasandCooldude2606 21779988f0 lawnmower changed to selection (#434)
* Update lawnmower.lua

* Update quick_actions.lua

* Update lawnmower.lua

* update locale

* Update lawnmower.lua

* Fix game.get_player

---------

Co-authored-by: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com>
2026-06-26 11:54:05 +01:00
phidiasandCooldude2606 d548cebc17 repair add quick action, changed to selection (#437)
* Update repair.lua

* Update repair.lua

* Update quick_actions.lua

* Update en.cfg

* Update repair.lua

* Refactor repair command to use area selection

* Update en.cfg

* Fixes

---------

Co-authored-by: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com>
2026-06-26 11:43:09 +01:00
phidiasandCooldude2606 8190b2165d mine depletion (#439)
* Update mine_depletion.lua

* Update miner.lua

* Update mine_depletion.lua

* Compatibility with 2.0 and 2.1

---------

Co-authored-by: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com>
2026-06-26 11:37:00 +01:00
phidiasandCooldude2606 2a1a5c952b home add quick action (#436)
* Update home.lua

* Update quick_actions.lua

---------

Co-authored-by: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com>
2026-06-25 23:15:31 +01:00
phidias aa7fe73bb0 vlayer info add quick action (#438)
* Update vlayer.lua

* Add vlayer command to quick actions
2026-06-25 23:14:14 +01:00
phidias 9863412177 Remove unused properties from vlayer energy calculations (#426)
Removed unused day time, day length, and tick properties from energy calculations.
2026-06-24 22:11:37 +01:00
phidias 7c9d47b6db remove landfill blueprint (#433)
* Delete exp_scenario/module/gui/landfill_blueprint.lua

* Update control.lua

* Update roles.lua

* Update en.cfg

* Update zh-CN.cfg

* Update zh-TW.cfg
2026-06-24 21:34:51 +01:00
phidias 8c351a8411 Update protection.lua (#431) 2026-06-24 21:33:31 +01:00
phidiasandCooldude2606 dc015bc609 bonus changes (#429)
* Delete exp_legacy/module/modules/data/bonus.lua

* Update _file_loader.lua

* Update roles.lua

* Update bonus.lua

* Update player_bonus.lua

* Update bonus.lua

---------

Co-authored-by: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com>
2026-06-24 21:30:46 +01:00
phidias f3a04fd26c remove personal-logistic (#428)
* Delete exp_legacy/module/modules/data/personal-logistic.lua

* Update _file_loader.lua

* Update roles.lua

* Delete exp_legacy/module/config/personal_logistic.lua
2026-06-24 21:25:39 +01:00
bbassieandClaude Opus 4.8 df129b5e9a Address player list review feedback
- Rename the player table element data class to element_data
- Remove the redundant offline selection clear; on_player_left_game
  already clears any selection pointing at a player who disconnects
- Move the action bar and reason bar visibility into their own refresh
  methods so refresh_player just delegates to them

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 18:22:37 +00:00
Cooldude2606 068548889b Merge pull request #420 from bbassie/feature/rocket-info-gui
Migrate rocket info gui to scenario and fix for Space Age
2026-06-22 23:12:24 +01:00
BastiaanandClaude Opus 4.8 817069fbad Bump LuaLS to 3.18.2 in lint workflow (#422)
LuaLS 3.15.0 stack overflows in the cast-type-mismatch diagnostic while
resolving the recursive Commands.Status and Async.Status function type
aliases (functions whose return type is themselves). 3.18.2, the latest
release, resolves these correctly and runs the repository clean.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 23:12:04 +01:00
bbassieandClaude Opus 4.8 4bd5394268 Align names and call patterns to best practice
Apply the conventions from the rocket info gui review to the player list,
and remove the now migrated legacy module.

- Move row building onto the table def as calculate_row_data, and refresh
  via refresh_player which computes the row data once per player
- Store the selection on the container with private/public accessor
  methods instead of poking element data directly
- Store the selected player as a LuaPlayer rather than a string and update
  the actions config to match
- Use :style{} on the player table and make the action and reason bars
  their own element definitions
- Delete the migrated legacy gui and its file loader entry

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 20:10:20 +00:00
Cooldude2606 05cb831b30 Update Locales 2026-06-20 16:43:26 +01:00
Cooldude2606 05b586fb8c Align names and call patterns to best practice 2026-06-20 16:35:44 +01:00
bbassieandClaude Opus 4.8 b1c0f2535b Migrate player list gui to scenario
Move modules/gui/player-list into exp_scenario/gui/player_list using the
new exp_gui element API, matching the other scenario guis.

- Store the selected player and action on the container's player data
  instead of a datastore, folding the datastore on_update reactivity
  into explicit setter functions
- Switch the actions config to injected accessor functions and the
  exp-gui_player-list locale namespace
- Add the locale section, register in control.lua, and disable the
  legacy module in the file loader

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 12:07:03 +00:00
bbassieandClaude Opus 4.8 c669e36bec Remove migrated rocket-info from exp_legacy
The rocket info gui and its control module now live in
exp_scenario/gui/rocket_info, so delete the orphaned legacy files and
drop the disabled file loader entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 20:19:29 +00:00
bbassieandClaude Opus 4.8 8f6d6e1caa Fix milestone lint warning in rocket info gui
Narrow the milestone loop variable with a type check instead of an
@cast, which tripped cast-type-mismatch because the milestones config
mixes the show_milestones flag in with the numbers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 20:05:48 +00:00
bbassieandClaude Opus 4.8 0ead1a51f9 Use gui force data and add_row pattern for rocket info
Address review feedback: store the per-force rocket stats, launch times
and silos on the container's force data instead of a separate storage
registration, and rebuild the stats, milestone and progress tables with
add_row/refresh_row helpers that update label references in place.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 18:53:23 +00:00
bbassieandClaude Opus 4.8 1fcf351566 Migrate rocket info gui to scenario and fix for Space Age
Port modules/gui/rocket-info into exp_scenario/gui/rocket_info using the
new exp_gui element API, matching the other scenario guis. The per-force
stats, launch times and silo tracking from the legacy control module are
folded into the gui module.

- Drop the removed auto/remote launch controls and count launches via
  on_cargo_pod_finished_ascending instead of the old launch event
- Add the exp-gui_rocket-info locale section
- Disable the legacy module in the file loader and re-enable the
  gui/rocket-info role permission

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 00:18:37 +00:00
Cooldude2606 2c7b88bb81 Merge pull request #419 from Cooldude2606/plugin/groups
Add plugin for permission group syncing
2026-05-27 00:41:18 +01:00
Cooldude2606 7318fc3602 Fix lint for global usage 2026-05-27 00:29:12 +01:00
Cooldude2606 b5cae0b0b7 Remove node ci cache 2026-05-27 00:13:56 +01:00
Cooldude2606 bb788f33da Final Cleanup 2026-05-25 20:43:51 +01:00
Cooldude2606 2248f23a8d Added web UI 2026-05-23 03:51:16 +01:00
Cooldude2606 a521cdd901 Change to dev env to fix building 2026-05-22 20:15:36 +01:00
Cooldude2606 230c835103 Add controller plugin 2026-05-21 00:30:17 +01:00
Cooldude2606 dca5086ce3 Add instance plugin 2026-05-20 22:39:44 +01:00
Cooldude2606 7ddfa8e735 Add messages and permissions 2026-05-20 22:39:33 +01:00
Cooldude2606 01af5dbc68 Update deps 2026-05-20 22:04:53 +01:00
Cooldude2606 90d63952d3 Add lua module code 2026-05-20 01:51:09 +01:00
Cooldude2606 b64e109bf1 Plugin Setup exp_groups 2026-05-20 01:48:26 +01:00
Cooldude2606 538714acdb Remove existing plugin 2026-05-19 22:23:47 +01:00
Cooldude2606 73e53a656d Merge pull request #418 from Cooldude2606/gui/readme
Refactor readme gui
2026-05-19 22:21:34 +01:00
Cooldude2606 cfa9cc980a Fix lint warning 2026-05-19 22:21:01 +01:00
Cooldude2606 620161938f Refactor readme gui 2026-05-13 23:31:33 +01:00
phidiasandCooldude2606 1a561f9d1d extra_logging spaces and fix to player_died (#417)
* extra_logging spaces and fix to player_died

* Update extra_logging.lua

* Update extra_logging.lua

* Update extra_logging.lua

* Remove redundant checks

---------

Co-authored-by: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com>
2026-05-13 20:47:02 +01:00
phidiasandCooldude2606 6cbc4e5cfb fix inventory clear (#416)
* Update clear_items function to accept player name

* Update inventory_clear.lua

* Update exp_scenario/module/control/inventory_clear.lua

Co-authored-by: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com>

---------

Co-authored-by: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com>
2026-03-27 13:48:53 +00:00
270 changed files with 13530 additions and 16843 deletions
+34
View File
@@ -0,0 +1,34 @@
{
"$schema": "https://raw.githubusercontent.com/EmmyLuaLs/emmylua-analyzer-rust/refs/heads/main/crates/emmylua_code_analysis/resources/schema.json",
"runtime": {
"version": "Lua5.2",
"requirePattern": [ "?", "?.lua" ]
},
"workspace": {
"$comment-moduleMap": "Replaces the fmtk --clusterio-modules plugin, rewrites each file's own module path rather than the require string",
"$comment-library": "clusterio is outside this repo, set CLUSTERIO to your checkout. The factorio typedefs come from .luarc.json, which the factoriomod-debug extension manages",
"library": [ "$CLUSTERIO/packages/host" ],
"ignoreDir": [ ".github", "web", "dist", "factorio" ],
"ignoreGlobs": [ "**/node_modules/**", "**/dist/**", "**/campaigns/**" ],
"moduleMap": [
{ "pattern": "^([^.]+)\\.module\\.module_exports$", "replace": "modules.$1" },
{ "pattern": "^([^.]+)\\.module\\.(.+)$", "replace": "modules.$1.$2" }
]
},
"doc": {
"privateName": [ "__*" ]
},
"$comment-disable": "Carried over from the luals config, which had these as editor only rather than checked in ci",
"$comment-unnecessary-if": "Disabled by the config fmtk generates for itself, the api union types make it unreliable",
"diagnostics": {
"globals": [ "__DebugAdapter", "__Profiler" ],
"disable": [
"assign-type-mismatch",
"param-type-mismatch",
"preferred-local-alias",
"unnecessary-assert",
"unnecessary-if",
"unused"
]
}
}
+45 -27
View File
@@ -1,76 +1,94 @@
name: FMTK Lint
name: Lua Lint
on:
push:
pull_request:
env:
EMMYLUA_CHECK_VERSION: "0.24.0"
CLUSTERIO: ${{ github.workspace }}/.clusterio
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Checkout clusterio modules
uses: actions/checkout@v4
with:
repository: clusterio/clusterio
path: .clusterio
sparse-checkout: packages/host/modules
sparse-checkout-cone-mode: false
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: "pnpm"
- name: Install FMTK
- name: Generate Factorio typedefs
run: |
pnpm install factoriomod-debug
pnpm exec fmtk luals-addon
jq '.["workspace.library"] += ["${{ github.workspace }}/factorio/library"] | .["runtime.plugin"] = "${{ github.workspace }}/factorio/plugin.lua"' .luarc.json > temp.luarc.json
jq -s '.[0] * .[1].settings' temp.luarc.json ${{ github.workspace }}/factorio/config.json > check.luarc.json
- name: Install LuaLS
pnpm exec fmtk luals-addon "$RUNNER_TEMP"
- name: Install emmylua_check
run: |
wget https://github.com/LuaLS/lua-language-server/releases/download/3.15.0/lua-language-server-3.15.0-linux-x64.tar.gz -q -O lusls.tar.gz
mkdir luals && tar -xf lusls.tar.gz -C luals && rm lusls.tar.gz
curl -fsSL "https://github.com/EmmyLuaLs/emmylua-analyzer-rust/releases/download/${EMMYLUA_CHECK_VERSION}/emmylua_check-linux-x64.tar.gz" \
| tar -xz -C "$RUNNER_TEMP"
- name: Run Lint Report
shell: bash
run: |
./luals/bin/lua-language-server --check=. --logpath=. --configpath=check.luarc.json --checklevel=Information --check_out_path=check.json
# The typedefs are generated per run, clusterio comes from CLUSTERIO above
jq -n --arg lib "$RUNNER_TEMP/factorio/library" \
'{ workspace: { library: [ $lib ], ignoreGlobs: [ ".clusterio/**" ] } }' \
> "$RUNNER_TEMP/ci.emmyrc.json"
# Exits non zero on error severity, so the gate below is used instead
"$RUNNER_TEMP/emmylua_check" . --config ".emmyrc.json,$RUNNER_TEMP/ci.emmyrc.json" \
--output-format json --output check.json || true
# Credit to https://github.com/Krealle/luals-check-action/blob/main/action.yml
# Although some minor fixes were needed
# Format and print the messages (would be nice to format as a table, but that's too complex for jq)
cat check.json | \
jq -r \
'to_entries | map(.key as $file | .value[] |
jq -r --arg root "$PWD/" \
'.[] | (.file | ltrimstr($root)) as $file | .diagnostics[] |
{
file: $file | sub("file://${{ github.workspace }}/./"; ""),
file: $file,
code: .code,
line: .range.start.line,
message: (.message | split("\n")
| map(if startswith("- ") then .[2:] else . end)
| join("\n")),
severity: (if .severity <= 1 then "ERR" else "WARN" end)
}) |
map("**[\(.severity)] \(.code):** \(.file)#L\(.line)<br><code>\(.message)</code>") | .[]' \
> $GITHUB_STEP_SUMMARY
} |
"**[\(.severity)] \(.code):** \(.file)#L\(.line + 1)<br><code>\(.message)</code>"' \
check.json > "$GITHUB_STEP_SUMMARY"
echo ""
# Github Annotations (limited to 10)
cat check.json | \
jq -r \
'to_entries | map(.key as $file | .value[] |
jq -r --arg root "$PWD/" \
'.[] | (.file | ltrimstr($root)) as $file | .diagnostics[] |
{
file: $file | sub("file://${{ github.workspace }}/./"; ""),
file: $file,
title: .code,
line: .range.start.line,
endLine: .range.end.line,
col: .range.start.character,
endColumn: .range.end.character,
message: .message | gsub("\n"; "%0A"; "m"),
message: (.message | gsub("\n"; "%0A"; "m")),
level: (if .severity <= 1 then "error" else "warning" end)
}) |
map("::\(.level) file=\(.file),line=\(.line+1),endLine=\(.line+1),col=\(.col+1),endColumn=\(.endColumn+1),title=\(.title)::\(.message) (\(.title))") | .[]'
} |
"::\(.level) file=\(.file),line=\(.line + 1),endLine=\(.endLine + 1),col=\(.col + 1),endColumn=\(.endColumn + 1),title=\(.title)::\(.message) (\(.title))"' \
check.json
if [[ $(wc -l < check.json) > 1 ]] ; then
# An entry is emitted for every file checked, including clean ones, so
# the gate counts diagnostics and asserts the workspace was actually read
if [[ $(jq 'length' check.json) -lt 100 ]] ; then
echo "::error::Only $(jq 'length' check.json) files were checked, expected the whole workspace"
exit 1
elif [[ $(jq '[.[].diagnostics[]] | length' check.json) -gt 0 ]] ; then
exit 1
else
echo "✅ All checks succeeded!" | tee $GITHUB_STEP_SUMMARY
echo "✅ All checks succeeded!" | tee "$GITHUB_STEP_SUMMARY"
fi
+51
View File
@@ -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
+2
View File
@@ -1,3 +1,5 @@
dist/
node_modules/
.vscode
.luarc.json
.tap/
-103
View File
@@ -1,103 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
"workspace.ignoreDir": [
".vscode", ".github", "luals", "factorio"
],
"completion.requireSeparator": "/",
"runtime.pluginArgs": [
"--clusterio-modules"
],
"doc": {
"regengine": "glob",
"privateName": [ "__*" ],
"packageName": [ "_*" ]
},
"type": {
"checkTableShape": true,
"inferParamType": true
},
"$comment-name-style-check": "Disabled below until config issue fixed: https://github.com/LuaLS/lua-language-server/issues/2643",
"$comment-type-mismatch": "Disabled unless opened due to performance concerns, time reduced to 10s from 400s",
"diagnostics": {
"unusedLocalExclude": [ "_*", "i", "j", "k", "v" ],
"groupFileStatus": {
"ambiguity": "Any",
"await": "None",
"codestyle": "Opened",
"conventions": "Any",
"duplicate": "Any",
"global": "Any",
"luadoc": "Opened",
"redefined": "Opened",
"strict": "Any",
"strong": "Any",
"type-check": "Any",
"unbalanced": "Opened",
"unused": "Opened"
},
"neededFileStatus": {
"no-unknown": "None!",
"spell-check": "None!",
"name-style-check": "None!",
"assign-type-mismatch": "Opened!",
"return-type-mismatch": "Opened!",
"param-type-mismatch": "Opened!"
}
},
"nameStyle.config": {
"local_name_style": [{
"type" : "pattern",
"param": "_?_?(\\S*)",
"$1": "snake_case"
}],
"module_local_name_style": [{
"type" : "pattern",
"param": "_?_?(\\S*)",
"$1": "snake_case"
}, {
"type" : "pattern",
"param": "_?_?(\\S*)",
"$1": "upper_snake_case"
}, {
"type" : "pattern",
"param": "_?_?(\\S*)",
"$1": "pascal_case"
}],
"function_param_name_style": [{
"type" : "pattern",
"param": "_?_?(\\S*)",
"$1": "snake_case"
}],
"function_name_style": [{
"type" : "pattern",
"param": "_?_?(\\S*)",
"$1": "snake_case"
}],
"local_function_name_style": [{
"type" : "pattern",
"param": "_?_?(\\S*)",
"$1": "snake_case"
}],
"table_field_name_style": [{
"type" : "pattern",
"param": "_?_?(\\S*)",
"$1": "snake_case"
}],
"global_variable_name_style": [{
"type" : "pattern",
"param": "_?_?(\\S*)",
"$1": "snake_case"
}, {
"type" : "pattern",
"param": "_?_?(\\S*)",
"$1": "upper_snake_case"
}],
"module_name_style": [ "pascal_case", "snake_case" ],
"require_module_name_style": [ "pascal_case", "snake_case" ],
"class_name_style": "pascal_case",
"const_variable_name_style": "upper_snake_case"
},
"format.defaultConfig": {
"continuation_indent.in_expr": "0"
}
}
+3 -1
View File
@@ -5,9 +5,11 @@ All are welcome to make bug reports, feature requests, and pull requests for our
For developers wanting to add features please follow these guidelines:
- All lua code is documented using ldoc.
- Set the `CLUSTERIO` environment variable to your clusterio development install path, the lint needs it to resolve `modules/clusterio/*`.
- 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`
+9
View File
@@ -42,10 +42,17 @@ 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.
Details of the scenario can also be seen on [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
| Release* | Release Name | Factorio Version** |
|---|---|---|
| [7.0][s7.0] | Clusterio Plugin (Beta) | [2.1.7][f2.1.7] |
| [6.4][s6.4] | Farewell Factorio 1.1 | [1.1.110][f1.1.110] |
| [6.3][s6.3] | Feature Bundle 2: Electric Boogaloo | [1.1.101][f1.1.101] |
| [6.2][s6.2] | Mega Feature Bundle | [1.1.32][f1.1.32] |
@@ -72,6 +79,7 @@ See [Contributing](CONTRIBUTING.md) for how to make pull requests and issues.
\*\* Factorio versions show the version they were made for, often the minimum requirement to run the scenario.
[s7.0]: https://github.com/explosivegaming/scenario/releases/tag/7.0.0
[s6.4]: https://github.com/explosivegaming/scenario/releases/tag/6.4.0
[s6.3]: https://github.com/explosivegaming/scenario/releases/tag/6.3.0
[s6.2]: https://github.com/explosivegaming/scenario/releases/tag/6.2.0
@@ -94,6 +102,7 @@ See [Contributing](CONTRIBUTING.md) for how to make pull requests and issues.
[s1.0]: https://github.com/explosivegaming/scenario/releases/tag/v1.0
[s0.1]: https://github.com/explosivegaming/scenario/releases/tag/v0.1
[f2.1.7]: https://wiki.factorio.com/Version_history/2.1.0#2.1.7
[f1.1.110]: https://wiki.factorio.com/Version_history/1.1.0#1.1.110
[f1.1.101]: https://wiki.factorio.com/Version_history/1.1.0#1.1.101
[f1.1.32]: https://wiki.factorio.com/Version_history/1.1.0#1.1.32
+1 -1
View File
@@ -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",
};
+3 -3
View File
@@ -10,7 +10,7 @@ The default permission authorities controlled by the flags: admin_only, system_o
local Storage = require("modules/exp_util/storage")
local Commands = require("modules/exp_commands") --- @class Commands
local Commands = require("modules/exp_commands") --- @class (partial) Commands
local add, allow, deny = Commands.add_permission_authority, Commands.status.success, Commands.status.unauthorised
local authorities = {}
@@ -28,13 +28,13 @@ end)
--- Allow a player access to system commands, use for debug purposes only
--- @param player_name string? The name of the player to give access to, default is the current player
function Commands.unlock_system_commands(player_name)
system_players[player_name or game.player.name] = true
system_players[player_name or assert(game.player).name] = true
end
--- Remove access from system commands for a player, use for debug purposes only
--- @param player_name string? The name of the player to give access to, default is the current player
function Commands.lock_system_commands(player_name)
system_players[player_name or game.player.name] = nil
system_players[player_name or assert(game.player).name] = nil
end
--- Get a list of all players who have system commands unlocked
+6 -6
View File
@@ -25,18 +25,18 @@ end)
--- @param page_size number The number of requests to show per page
--- @return LocalisedString[][], number
local function format_as_pages(commands, page_size)
local pages = { {} }
local current_page = {}
local pages = { current_page }
local page_length = 0
local current_page = 1
local total = 0
for _, command in pairs(commands) do
total = total + 1
page_length = page_length + 1
if page_length > page_size then
current_page = current_page + 1
pages[current_page] = {}
page_length = 1
current_page = {}
pages[#pages + 1] = current_page
end
local description
@@ -48,7 +48,7 @@ local function format_as_pages(commands, page_size)
end
local aliases = #command.aliases > 0 and { "exp-commands_help.aliases", table.concat(command.aliases, ", ") } or ""
pages[current_page][page_length] = { "exp-commands_help.format", command.name, description, aliases }
current_page[page_length] = { "exp-commands_help.format", command.name, description, aliases }
end
return pages, total
@@ -71,7 +71,7 @@ Commands.new("commands", { "exp-commands_help.description" })
page = as_number
end
keyword = keyword:lower()
keyword = tostring(keyword):lower()
local pages, found
if cache and cache.keyword == keyword then
-- Cached value found, no search is needed
+1 -3
View File
@@ -12,7 +12,7 @@ local ExpUtil = require("modules/exp_util")
local Async = require("modules/exp_util/async")
local Storage = require("modules/exp_util/storage")
local Commands = require("modules/exp_commands") --- @class Commands
local Commands = require("modules/exp_commands") --- @class (partial) Commands
local rcon_env = {} --- @type table<string, any>
local rcon_static = {} --- @type table<string, any>
@@ -21,12 +21,10 @@ setmetatable(rcon_static, { __index = _G })
setmetatable(rcon_env, { __index = rcon_static })
--- Some common static values which can be added now
--- @diagnostic disable: name-style-check
rcon_static.Async = Async
rcon_static.ExpUtil = ExpUtil
rcon_static.Commands = Commands
rcon_static.print = Commands.print
--- @diagnostic enable: name-style-check
--- Some common callback values which are useful when a player uses the command
--- @alias ExpCommand.RconDynamic fun(player: LuaPlayer?): any
+1 -1
View File
@@ -19,7 +19,7 @@ Commands.new("_sudo", { "exp-commands_sudo.description" })
--- @cast command ExpCommand
--- @cast parameter string
--- @diagnostic disable-next-line: invisible
--- @diagnostic disable-next-line: access-invisible
return Commands._event_handler{
name = command.name,
tick = game.tick,
+1 -1
View File
@@ -25,7 +25,7 @@ local Commands = require("modules/exp_commands")
local add, parse = Commands.add_data_type, Commands.parse_input
local valid, invalid = Commands.status.success, Commands.status.invalid_input
local types = {} --- @class Commands.types
local types = {} --- @class (partial) Commands.types
--- A boolean value where true is one of: yes, y, true, 1
types.boolean =
+18 -14
View File
@@ -61,7 +61,7 @@ local Search = require("modules/exp_commands/search")
--- @type LuaPlayer?
local _print_player
--- @class Commands
--- @class (partial) Commands
local Commands = {
color = ExpUtil.color,
format_rich_text_color = ExpUtil.format_rich_text_color,
@@ -84,7 +84,7 @@ local Commands = {
--- Contains the different status values a command can return
Commands.status = {}
--- @class Commands.types: table<string, Commands.InputParser | Commands.InputParserFactory>
--- @class (partial) Commands.types: table<string, Commands.InputParser<any> | Commands.InputParserFactory<any>>
--- Stores all input parsers and validators for different data types
Commands.types = {}
@@ -103,7 +103,7 @@ end
--- @class Commands.Argument
--- @field name string The name of the argument
--- @field description LocalisedString? The description of the argument
--- @field input_parser Commands.InputParser The input parser for the argument
--- @field input_parser Commands.InputParser<any> The input parser for the argument
--- @field optional boolean True when the argument is optional
--- @field default any? The default value of the argument
@@ -159,7 +159,7 @@ Commands.server = setmetatable({
Commands.error("Command does not support rcon usage, requires LuaPlayer." .. key)
error("Command does not support rcon usage, requires LuaPlayer." .. key)
end,
}) --[[ @as LuaPlayer ]]
}) --[[@as LuaPlayer]]
--- Status Returns.
-- Return values used by command callbacks
@@ -170,7 +170,8 @@ Commands.server = setmetatable({
--- @param msg LocalisedString? An optional message to be included when a command completes (only has an effect in command callbacks)
--- @return Commands.Status, LocalisedString # Should be returned directly without modification
function Commands.status.success(msg)
return Commands.status.success, msg == nil and { "exp-commands.success" } or msg
if msg == nil then return Commands.status.success, { "exp-commands.success" } end
return Commands.status.success, msg
end
--- Used to signal an error has occurred in a command, data type parser, or permission authority
@@ -178,7 +179,8 @@ end
--- @param msg LocalisedString? An optional error message to be included in the output, a generic message is used if not provided
--- @return Commands.Status, LocalisedString # Should be returned directly without modification
function Commands.status.error(msg)
return Commands.status.error, { "exp-commands.error", msg == nil and { "exp-commands.error-default" } or msg }
if msg == nil then msg = { "exp-commands.error-default" } end
return Commands.status.error, { "exp-commands.error", msg }
end
--- Used to signal the player is unauthorised to use a command, primarily used by permission authorities but can be used in a command callback
@@ -186,7 +188,8 @@ end
--- @param msg LocalisedString? An optional error message to be included in the output, a generic message is used if not provided
--- @return Commands.Status, LocalisedString # Should be returned directly without modification
function Commands.status.unauthorised(msg)
return Commands.status.unauthorised, { "exp-commands.unauthorized", msg == nil and { "exp-commands.unauthorized-default" } or msg }
if msg == nil then msg = { "exp-commands.unauthorized-default" } end
return Commands.status.unauthorised, { "exp-commands.unauthorized", msg }
end
--- Used to signal the player provided invalid input to an command, primarily used by data type parsers but can be used in a command callback
@@ -194,15 +197,16 @@ end
--- @param msg LocalisedString? An optional error message to be included in the output, a generic message is used if not provided
--- @return Commands.Status, LocalisedString # Should be returned directly without modification
function Commands.status.invalid_input(msg)
return Commands.status.invalid_input, msg == nil and { "exp-commands.invalid-input" } or msg
if msg == nil then return Commands.status.invalid_input, { "exp-commands.invalid-input" } end
return Commands.status.invalid_input, msg
end
--- Used to signal an internal error has occurred, this is reserved for internal use only
--- @param msg LocalisedString A message detailing the error which has occurred, will be logged and outputted
--- @param msg LocalisedString? A message detailing the error which has occurred, will be logged and outputted
--- @return Commands.Status, LocalisedString # Should be returned directly without modification
--- @package
function Commands.status.internal_error(msg)
return Commands.status.internal_error, { "exp-commands.internal-error", msg }
return Commands.status.internal_error, { "exp-commands.internal-error", msg or "" }
end
--- @type table<Commands.Status, string>
@@ -281,7 +285,7 @@ end
--- @alias Commands.InputParserFactory<T> fun(...: any): Commands.InputParser<T>
--- Add a new input parser to the command library, this method validates that it does not already exist
--- @generic T : Commands.InputParser | Commands.InputParserFactory
--- @generic T : Commands.InputParser<any> | Commands.InputParserFactory<any>
--- @param data_type string The name of the data type the input parser reads in and validates, becomes a key of Commands.types
--- @param input_parser T The function used to parse and validate the data type
--- @return T # The function which was provided as the second argument
@@ -295,7 +299,7 @@ function Commands.add_data_type(data_type, input_parser)
end
--- Remove an input parser for a data type, must be the same string that was passed to add_input_parser
--- @param data_type string | Commands.InputParser | Commands.InputParserFactory The data type or input parser you want to remove the input parser for
--- @param data_type string | Commands.InputParser<any> | Commands.InputParserFactory<any> The data type or input parser you want to remove the input parser for
function Commands.remove_data_type(data_type)
Commands.types[data_type] = nil
for k, v in pairs(Commands.types) do
@@ -433,7 +437,7 @@ end
--- Add a new required argument to the command of the given data type
--- @param name string The name of the argument being added
--- @param description LocalisedString? The description of the argument being added
--- @param input_parser Commands.InputParser The input parser to be used for the argument
--- @param input_parser Commands.InputParser<any> The input parser to be used for the argument
--- @return ExpCommand
function Commands._prototype:argument(name, description, input_parser)
assert_command_mutable(self)
@@ -454,7 +458,7 @@ end
--- Add a new optional argument to the command of the given data type
--- @param name string The name of the argument being added
--- @param description LocalisedString? The description of the argument being added
--- @param input_parser Commands.InputParser The input parser to be used for the argument
--- @param input_parser Commands.InputParser<any> The input parser to be used for the argument
--- @return ExpCommand
function Commands._prototype:optional(name, description, input_parser)
assert_command_mutable(self)
+3 -2
View File
@@ -3,7 +3,7 @@ local Search = {}
local Storage = require("modules/exp_util/storage")
--- Setup the storage to contain the pending translations and the completed ones
local pending = {} --- @type { [1]: string, [2]: string }[]
local pending = {} --- @type table<uint, [string, string]>
local translations = {} --- @type table<string, table<string, string>>
Storage.register({
pending,
@@ -50,6 +50,7 @@ function Search.prepare(custom_commands)
name = name,
description = locale_desc,
help_text = locale_desc,
usage = locale_desc,
aliases = {},
}
end
@@ -65,7 +66,7 @@ function Search.on_player_locale_changed(event)
local ids = player.request_translations(required_translations)
assert(ids, "Translation ids was nil")
for i, command_name in ipairs(command_names) do
pending[ids[i]] = { locale, command_name }
pending[assert(ids[i])] = { locale, command_name }
end
end
end
+4 -3
View File
@@ -1,10 +1,11 @@
{
"name": "@expcluster/lib_commands",
"version": "0.1.0",
"version": "7.0.1",
"description": "Clusterio plugin providing a Lua command processing library.",
"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"
@@ -13,10 +14,10 @@
"node": ">=18"
},
"peerDependencies": {
"@clusterio/lib": "catalog:"
"@clusterio/lib": "workspace:^"
},
"devDependencies": {
"@clusterio/lib": "catalog:",
"@clusterio/lib": "workspace:^",
"@types/node": "catalog:",
"typescript": "catalog:",
"webpack": "catalog:",
+375 -223
View File
@@ -1,282 +1,434 @@
import { BaseControllerPlugin } from "@clusterio/controller";
import * as lib from "@clusterio/lib";
import { BaseControllerPlugin, InstanceInfo } from "@clusterio/controller";
import {
PermissionStrings, PermissionStringsUpdate,
PermissionGroup, PermissionGroupUpdate,
InstancePermissionGroups,
PermissionInstanceId,
PermissionGroupEditEvent,
} from "./messages";
import path from "path";
import fs from "fs-extra";
import * as messages from "./messages.js";
import * as path from "node:path";
export class ControllerPlugin extends BaseControllerPlugin {
static permissionGroupsPath = "exp_groups.json";
static userGroupsPath = "exp_user_groups.json";
userToGroup: Map<lib.User["id"], PermissionGroup> = new Map(); // TODO this needs to be per instance
permissionStrings!: Map<PermissionStrings["id"], PermissionStrings>;
permissionGroups!: Map<InstancePermissionGroups["id"], InstancePermissionGroups>;
groups!: lib.SubscribableDatastore<messages.GroupRecord>;
roleMappings!: lib.SubscribableDatastore<messages.RoleMappingRecord>;
manualAssignments!: lib.SubscribableDatastore<messages.AssignmentRecord>;
resolvedAssignments!: lib.SubscribableDatastore<messages.AssignmentRecord>;
async init() {
this.controller.handle(PermissionStringsUpdate, this.handlePermissionStringsUpdate.bind(this));
this.controller.handle(PermissionGroupUpdate, this.handlePermissionGroupUpdate.bind(this));
this.controller.handle(PermissionGroupEditEvent, this.handlePermissionGroupEditEvent.bind(this));
this.controller.subscriptions.handle(PermissionStringsUpdate, this.handlePermissionStringsSubscription.bind(this));
this.controller.subscriptions.handle(PermissionGroupUpdate, this.handlePermissionGroupSubscription.bind(this));
this.controller.subscriptions.handle(PermissionGroupEditEvent);
this.permissionStrings = new Map([["Global", new PermissionStrings("Global", new Set())]]);
this.permissionGroups = new Map([["Global", new InstancePermissionGroups("Global")]]);
await this.loadData();
const databaseDirectory = this.controller.config.get("controller.database_directory");
// Add the default group if missing and add any missing cluster roles
const clusterRoles = [...this.controller.userManager.roles.values()]
for (const instanceGroups of this.permissionGroups.values()) {
const groups = instanceGroups.groups;
const instanceRoles = [...groups.values()].flatMap(group => [...group.roleIds.values()]);
const missingRoles = clusterRoles.filter(role => instanceRoles.includes(role.id));
const defaultGroup = groups.get("Default");
if (defaultGroup) {
for (const role of missingRoles) {
defaultGroup.roleIds.add(role.id)
this.groups = new lib.SubscribableDatastore(
...await new lib.JsonIdDatastoreProvider(
path.join(databaseDirectory, "exp_groups", "groups.json"),
messages.GroupRecord.fromJSON.bind(messages.GroupRecord),
).bootstrap()
);
this.roleMappings = new lib.SubscribableDatastore(
...await new lib.JsonIdDatastoreProvider(
path.join(databaseDirectory, "exp_groups", "role_mappings.json"),
messages.RoleMappingRecord.fromJSON.bind(messages.RoleMappingRecord),
).bootstrap()
);
this.manualAssignments = new lib.SubscribableDatastore(
...await new lib.JsonIdDatastoreProvider(
path.join(databaseDirectory, "exp_groups", "assignments.json"),
messages.AssignmentRecord.fromJSON.bind(messages.AssignmentRecord),
).bootstrap()
);
this.resolvedAssignments = new lib.SubscribableDatastore();
this.controller.subscriptions.handle(messages.GroupUpdatedEvent, this.handleGroupSubscription.bind(this));
this.controller.subscriptions.handle(messages.RoleMappingUpdatedEvent, this.handleRoleMappingSubscription.bind(this));
this.controller.subscriptions.handle(messages.ManualAssignmentUpdatedEvent, this.handleManualAssignmentSubscription.bind(this));
this.controller.subscriptions.handle(messages.ResolvedAssignmentUpdatedEvent, this.handleResolvedAssignmentSubscription.bind(this));
this.groups.on("update", this.groupsUpdated.bind(this));
this.roleMappings.on("update", this.roleMappingsUpdated.bind(this));
this.manualAssignments.on("update", this.manualAssignmentsUpdated.bind(this));
this.resolvedAssignments.on("update", this.resolvedAssignmentsUpdated.bind(this));
this.controller.handle(messages.GroupCreateRequest, this.handleGroupCreateRequest.bind(this));
this.controller.handle(messages.GroupUpdateRequest, this.handleGroupUpdateRequest.bind(this));
this.controller.handle(messages.GroupDeleteRequest, this.handleGroupDeleteRequest.bind(this));
this.controller.handle(messages.GroupGetRequest, this.handleGroupGetRequest.bind(this));
this.controller.handle(messages.GroupListRequest, this.handleGroupListRequest.bind(this));
this.controller.handle(messages.AssignmentCreateRequest, this.handleAssignmentCreateRequest.bind(this));
this.controller.handle(messages.AssignmentUpdateRequest, this.handleAssignmentUpdateRequest.bind(this));
this.controller.handle(messages.AssignmentDeleteRequest, this.handleAssignmentDeleteRequest.bind(this));
this.controller.handle(messages.AssignmentGetRequest, this.handleAssignmentGetRequest.bind(this));
this.controller.handle(messages.AssignmentListRequest, this.handleAssignmentListRequest.bind(this));
this.controller.handle(messages.RoleMappingCreateRequest, this.handleRoleMappingCreateRequest.bind(this));
this.controller.handle(messages.RoleMappingUpdateRequest, this.handleRoleMappingUpdateRequest.bind(this));
this.controller.handle(messages.RoleMappingDeleteRequest, this.handleRoleMappingDeleteRequest.bind(this));
this.controller.handle(messages.RoleMappingGetRequest, this.handleRoleMappingGetRequest.bind(this));
this.controller.handle(messages.RoleMappingListRequest, this.handleRoleMappingListRequest.bind(this));
}
} else {
groups.set("Default", new PermissionGroup(
instanceGroups.instanceId,
"Default",
groups.size,
new Set(missingRoles.map(role => role.id))
));
async onShutdown() {
await Promise.all([
this.groups.save(),
this.manualAssignments.save(),
this.roleMappings.save(),
])
}
/*
Subscriptions
*/
async groupsUpdated(groups: messages.GroupRecord[]) {
this.controller.subscriptions.broadcast(new messages.GroupUpdatedEvent(groups));
// We need to do extra work if the group was deleted
const deletedGroupIds = groups.filter(g => g.isDeleted).map(g => g.id);
if (!deletedGroupIds.length) {
return;
}
// Cascade the delete down to affected role mappings
const mappingsToDelete = [];
for (const mapping of this.roleMappings.values()) {
if (deletedGroupIds.includes(mapping.groupId)) {
mappingsToDelete.push(mapping);
}
}
if (mappingsToDelete.length) {
this.roleMappings.deleteMany(mappingsToDelete);
}
// Cascade the delete down to affected manual assignments
const affectedPlayers = new Set<string>();
const assignmentsToDelete = [];
for (const assignment of this.manualAssignments.values()) {
if (deletedGroupIds.includes(assignment.groupId)) {
assignmentsToDelete.push(assignment);
affectedPlayers.add(assignment.name);
}
}
if (assignmentsToDelete.length) {
this.manualAssignments.deleteMany(assignmentsToDelete);
}
// Find all the affected players who were assigned to this group
for (const resolved of this.resolvedAssignments.values()) {
if (deletedGroupIds.includes(resolved.groupId)) {
affectedPlayers.add(resolved.name);
}
}
if (affectedPlayers.size) {
this.resolvedAssignments.setMany(await this.computeResolvedAssignments([...affectedPlayers]));
}
}
async onControllerConfigFieldChanged(field: string, curr: unknown, prev: unknown) {
if (field === "exp_groups.allow_role_inconsistency") {
// Do something with this.userToGroup
async handleGroupSubscription(request: lib.SubscriptionRequest) {
const groups = [...this.groups.values()]
.filter(group => group.updatedAtMs > request.lastRequestTimeMs);
return groups.length ? new messages.GroupUpdatedEvent(groups) : null;
}
async roleMappingsUpdated(roleMappings: messages.RoleMappingRecord[]) {
this.controller.subscriptions.broadcast(new messages.RoleMappingUpdatedEvent(roleMappings));
// Mappings pointing to a deleted group have already been handled
// But if any are active, then we still must recompute all assignments
let hasActiveGroup = false;
for (const roleMapping of roleMappings) {
const group = this.groups.get(roleMapping.groupId);
if (group && !group.isDeleted) {
hasActiveGroup = true;
break;
}
}
if (!hasActiveGroup) {
return;
}
// Affected players are those without manual assignments
const affectedPlayers = [];
for (const resolved of this.resolvedAssignments.values()) {
if (!this.manualAssignments.has(resolved.name)) {
affectedPlayers.push(resolved.name);
}
}
if (affectedPlayers.length) {
this.resolvedAssignments.setMany(await this.computeResolvedAssignments(affectedPlayers));
}
}
async onInstanceConfigFieldChanged(instance: InstanceInfo, field: string, curr: unknown, prev: unknown) {
this.logger.info(`controller::onInstanceConfigFieldChanged ${instance.id} ${field}`);
if (field === "exp_groups.sync_permission_groups") {
const updates = []
const now = Date.now();
if (curr) {
// Global sync enabled, we dont need the instance config
const instanceGroups = this.permissionGroups.get(instance.id);
if (instanceGroups) {
this.permissionGroups.delete(instance.id);
for (const group of instanceGroups.groups.values()) {
group.updatedAtMs = now;
group.isDeleted = true;
updates.push(group);
async handleRoleMappingSubscription(request: lib.SubscriptionRequest) {
const mappings = [...this.roleMappings.values()]
.filter(mapping => mapping.updatedAtMs > request.lastRequestTimeMs);
return mappings.length ? new messages.RoleMappingUpdatedEvent(mappings) : null;
}
async manualAssignmentsUpdated(assignments: messages.AssignmentRecord[]) {
this.controller.subscriptions.broadcast(new messages.ManualAssignmentUpdatedEvent(assignments));
// Assignments pointing to a deleted group have already been handled
const affectedPlayers: string[] = [];
for (const assignment of assignments) {
const group = this.groups.get(assignment.groupId);
if (!group || group.isDeleted) continue;
affectedPlayers.push(assignment.name);
}
} else {
// Global sync disabled, make a copy of the global config as a base
const global = this.permissionGroups.get("Global")!;
const oldInstanceGroups = this.permissionGroups.get(instance.id);
const instanceGroups = new InstancePermissionGroups(
instance.id, new Map([...global.groups.values()].map(group => [group.name, group.copy(instance.id)]))
)
this.permissionGroups.set(instance.id, instanceGroups);
for (const group of instanceGroups.groups.values()) {
group.updatedAtMs = now;
updates.push(group);
}
// If it has an old config (unexpected) then deal with it
if (oldInstanceGroups) {
for (const group of oldInstanceGroups.groups.values()) {
if (!instanceGroups.groups.has(group.name)) {
group.updatedAtMs = now;
group.isDeleted = true;
updates.push(group);
}
}
}
}
// Send the updates to all instances and controls
if (updates.length) {
this.controller.subscriptions.broadcast(new PermissionGroupUpdate(updates));
}
if (affectedPlayers.length) {
this.resolvedAssignments.setMany(await this.computeResolvedAssignments(affectedPlayers));
}
}
async loadPermissionGroups() {
const file = path.resolve(this.controller.config.get("controller.database_directory"), ControllerPlugin.permissionGroupsPath);
this.logger.verbose(`Loading ${file}`);
try {
const content = await fs.readFile(file, { encoding: "utf8" });
for (const groupRaw of JSON.parse(content)) {
const group = PermissionGroup.fromJSON(groupRaw);
const instanceGroups = this.permissionGroups.get(group.instanceId);
if (instanceGroups) {
instanceGroups.groups.set(group.name, group);
} else {
this.permissionGroups.set(group.instanceId,
new InstancePermissionGroups(group.instanceId, new Map([[group.name, group]]))
async handleManualAssignmentSubscription(request: lib.SubscriptionRequest) {
const assignments = [...this.resolvedAssignments.values()]
.filter(a => a.updatedAtMs > request.lastRequestTimeMs);
return assignments.length ? new messages.ManualAssignmentUpdatedEvent(assignments) : null;
}
resolvedAssignmentsUpdated(assignments: messages.AssignmentRecord[]) {
this.controller.subscriptions.broadcast(
new messages.ResolvedAssignmentUpdatedEvent(assignments),
assignments.map(assignment => assignment.name),
);
}
};
} catch (err: any) {
if (err.code === "ENOENT") {
this.logger.verbose("Creating new permission group database");
return;
}
throw err;
async handleResolvedAssignmentSubscription(request: lib.SubscriptionRequest) {
// Check for any missing assignments to be computed on demand
const filters = request.filters.toJSON();
if (filters.length) {
const missing = filters.filter(name => !this.resolvedAssignments.has(name));
if (missing.length) {
this.resolvedAssignments.setMany(await this.computeResolvedAssignments(missing));
}
}
async savePermissionGroups() {
const file = path.resolve(this.controller.config.get("controller.database_directory"), ControllerPlugin.permissionGroupsPath);
this.logger.verbose(`Writing ${file}`);
await lib.safeOutputFile(file, JSON.stringify(
[...this.permissionGroups.values()].flatMap(instanceGroups => [...instanceGroups.groups.values()])
));
// Filter the assignments
const assignments = (filters.length
? filters.map(name => this.resolvedAssignments.get(name)).filter(Boolean)
: [...this.resolvedAssignments.values()]
).filter(a => a.updatedAtMs > request.lastRequestTimeMs);
return assignments.length ? new messages.ResolvedAssignmentUpdatedEvent(assignments) : null;
}
async loadUserGroups() {
if (!this.controller.config.get("exp_groups.allow_role_inconsistency")) return;
const file = path.resolve(this.controller.config.get("controller.database_directory"), ControllerPlugin.userGroupsPath);
this.logger.verbose(`Loading ${file}`);
try {
const content = await fs.readFile(file, { encoding: "utf8" });
this.userToGroup = new Map(JSON.parse(content));
/*
Groups
*/
} catch (err: any) {
if (err.code === "ENOENT") {
this.logger.verbose("Creating new user group database");
return;
}
throw err;
}
async handleGroupListRequest() {
return [...this.groups.values()];
}
async saveUserGroups() {
if (!this.controller.config.get("exp_groups.allow_role_inconsistency")) return;
const file = path.resolve(this.controller.config.get("controller.database_directory"), ControllerPlugin.userGroupsPath);
this.logger.verbose(`Writing ${file}`);
await lib.safeOutputFile(file, JSON.stringify([...this.permissionGroups.entries()]));
async handleGroupCreateRequest(request: messages.GroupCreateRequest) {
if ([...this.groups.values()].some(g => g.name === request.name)) {
throw new lib.RequestError(`Group with name '${request.name}' already exists`);
}
async loadData() {
await Promise.all([
this.loadPermissionGroups(),
this.loadUserGroups(),
])
let id = Math.random() * 2**31 | 0;
while (this.groups.has(id)) {
id = Math.random() * 2**31 | 0;
}
async onSaveData() {
await Promise.all([
this.savePermissionGroups(),
this.saveUserGroups(),
])
}
addPermisisonGroup(instanceId: PermissionInstanceId, name: string, permissions = new Set<string>(), silent = false) {
const instanceGroups = this.permissionGroups.get(instanceId);
if (!instanceGroups) {
throw new Error("Instance ID does not exist");
}
if (instanceGroups.groups.has(name)) {
return instanceGroups.groups.get(name)!;
}
for (const group of instanceGroups.groups.values()) {
group.order += 1;
}
const group = new PermissionGroup(instanceId, name, 0, new Set(), permissions, Date.now(), false);
instanceGroups.groups.set(group.id, group);
if (!silent) {
this.controller.subscriptions.broadcast(new PermissionGroupUpdate([group]));
}
const group = new messages.GroupRecord(id, request.name, request.permissions);
this.groups.set(group);
return group;
}
removePermissionGroup(instanceId: PermissionInstanceId, name: string, silent = false) {
const instanceGroups = this.permissionGroups.get(instanceId);
if (!instanceGroups) {
throw new Error("Instance ID does not exist");
async handleGroupUpdateRequest(request: messages.GroupUpdateRequest) {
const group = request.group;
if (group.id === undefined || !this.groups.has(group.id)) {
throw new lib.RequestError(`Group with ID ${group.id} does not exist`);
}
const group = instanceGroups.groups.get(name)
this.groups.set(group);
}
async handleGroupDeleteRequest(request: messages.GroupDeleteRequest) {
const { groupId } = request;
const group = this.groups.getMutable(groupId);
if (!group) {
return null;
throw new lib.RequestError(`Group with ID ${groupId} does not exist`);
}
for (const nextGroup of instanceGroups.groups.values()) {
if (nextGroup.order > group.order) {
nextGroup.order -= 1;
this.groups.delete(group);
}
async handleGroupGetRequest(request: messages.GroupGetRequest) {
const group = this.groups.get(request.groupId);
if (!group) {
throw new lib.RequestError(`Group with ID ${request.groupId} does not exist`);
}
instanceGroups.groups.delete(group.id);
group.updatedAtMs = Date.now();
group.isDeleted = true;
if (!silent) {
this.controller.subscriptions.broadcast(new PermissionGroupUpdate([group]));
}
return group;
}
async handlePermissionGroupEditEvent(event: PermissionGroupEditEvent) {
// TODO
/*
Groups
*/
async handleAssignmentListRequest() {
return [...this.manualAssignments.values()];
}
async handlePermissionStringsUpdate(event: PermissionStringsUpdate) {
for (const update of event.updates) {
const global = this.permissionStrings.get("Global")!
this.permissionStrings.set(update.instanceId as number, update)
global.updatedAtMs = Math.max(global.updatedAtMs, update.updatedAtMs)
for (const permission of update.permissions) {
global.permissions.add(permission)
async handleAssignmentCreateRequest(request: messages.AssignmentCreateRequest) {
const { name, groupId } = request;
if (this.manualAssignments.has(name)) {
throw new lib.RequestError(`Assignment for '${name}' already exists`);
}
// TODO maybe check if changes have happened rather than always pushing updates
this.controller.subscriptions.broadcast(new PermissionStringsUpdate([global, update]))
const assignment = new messages.AssignmentRecord(name, groupId);
this.manualAssignments.set(assignment);
return assignment;
}
async handleAssignmentUpdateRequest(request: messages.AssignmentUpdateRequest) {
const assignment = request.assignment;
if (!this.manualAssignments.has(assignment.name)) {
throw new lib.RequestError(`Assignment for '${assignment.name}' does not exist`);
}
this.manualAssignments.set(assignment);
}
async handleAssignmentDeleteRequest(request: messages.AssignmentDeleteRequest) {
const { name } = request;
const assignment = this.manualAssignments.getMutable(name);
if (!assignment) {
throw new lib.RequestError(`Assignment for '${name}' does not exist`);
}
this.manualAssignments.delete(assignment);
}
async handleAssignmentGetRequest(request: messages.AssignmentGetRequest) {
if (request.resolve) {
let assignment = this.resolvedAssignments.get(request.name);
if (!assignment) {
assignment = await this.computeResolvedAssignment(request.name);
this.resolvedAssignments.set(assignment);
}
return assignment;
}
const assignment = this.manualAssignments.get(request.name);
if (!assignment) {
throw new lib.RequestError(`Assignment for '${request.name}' does not exist`);
}
return assignment;
}
/*
Role mappings
*/
async handleRoleMappingListRequest() {
return [...this.roleMappings.values()];
}
async handleRoleMappingCreateRequest(request: messages.RoleMappingCreateRequest) {
let id = Math.random() * 2**31 | 0;
while (this.roleMappings.has(id)) {
id = Math.random() * 2**31 | 0;
}
let priority = request.priority;
const existing = new Set([...this.roleMappings.values()].map(m => m.priority));
while (existing.has(priority)) {
priority++;
}
const roleMapping = new messages.RoleMappingRecord(
id, new Set(request.roleIds), request.groupId, priority, request.enabled,
);
this.roleMappings.set(roleMapping);
return roleMapping;
}
async handleRoleMappingUpdateRequest(request: messages.RoleMappingUpdateRequest) {
const roleMapping = request.roleMapping;
if (roleMapping.id === undefined || !this.roleMappings.has(roleMapping.id)) {
throw new lib.RequestError(`Role mapping with ID ${roleMapping.id} does not exist`);
}
const existing = new Set(
[...this.roleMappings.values()]
.filter(m => m.id !== roleMapping.id)
.map(m => m.priority)
);
let priority = roleMapping.priority;
while (existing.has(priority)) {
priority++;
}
this.roleMappings.set(roleMapping);
}
async handleRoleMappingDeleteRequest(request: messages.RoleMappingDeleteRequest) {
const { id } = request;
const mapping = this.roleMappings.getMutable(id);
if (!mapping) {
throw new lib.RequestError(`Role mapping with ID ${id} does not exist`);
}
this.roleMappings.delete(mapping);
}
async handleRoleMappingGetRequest(request: messages.RoleMappingGetRequest) {
const mapping = this.roleMappings.get(request.id);
if (!mapping) {
throw new lib.RequestError(`Role mapping with ID ${request.id} does not exist`);
}
return mapping;
}
/*
Calculating assignments
*/
async computeResolvedAssignment(playerName: string): Promise<messages.AssignmentRecord> {
// 1) Manual override
const manual = this.manualAssignments.get(playerName);
if (manual) {
return manual;
}
const user = this.controller.users.getByName(playerName);
const userRoles = user?.roleIds ?? new Set<number>();
// 2) Role mappings
let best: messages.RoleMappingRecord | null = null;
for (const mapping of this.roleMappings.values()) {
if (!mapping.enabled) continue;
let matches = true;
for (const roleId of mapping.roleIds) {
if (!userRoles.has(roleId)) {
matches = false;
break;
}
}
async handlePermissionGroupUpdate(event: PermissionGroupUpdate) {
const updates = [];
for (const group of event.updates) {
const groups = this.permissionGroups.get(group.instanceId);
if (!groups) continue;
const existingGroup = groups.groups.get(group.id);
let update
if (!existingGroup) {
update = this.addPermisisonGroup(group.instanceId, group.name, group.permissions, true);
} else if (group.isDeleted) {
update = this.removePermissionGroup(group.instanceId, group.name, true);
} else {
existingGroup.permissions = group.permissions;
existingGroup.updatedAtMs = Date.now();
update = existingGroup;
if (!matches) continue;
if (!best || mapping.priority > best.priority) {
best = mapping;
}
if (update) updates.push(update);
}
this.controller.subscriptions.broadcast(new PermissionGroupUpdate(updates));
}
async handlePermissionStringsSubscription(request: lib.SubscriptionRequest, src: lib.Address) {
const updates = [ ...this.permissionStrings.values() ]
.filter(
value => value.updatedAtMs > request.lastRequestTimeMs,
)
return updates.length ? new PermissionStringsUpdate(updates) : null;
if (best) {
return new messages.AssignmentRecord(playerName, best.groupId);
}
async handlePermissionGroupSubscription(request: lib.SubscriptionRequest, src: lib.Address) {
const updates = [ ...this.permissionGroups.values() ]
.flatMap(instanceGroups => [...instanceGroups.groups.values()])
.filter(
value => value.updatedAtMs > request.lastRequestTimeMs,
)
if (src.type === lib.Address.instance) {
const instanceUpdates = updates.filter(group => group.instanceId === src.id || group.instanceId === "Global");
this.logger.info(JSON.stringify(updates))
this.logger.info(JSON.stringify(instanceUpdates))
return instanceUpdates.length ? new PermissionGroupUpdate(instanceUpdates) : null;
// 3) Default (deleted assignment, assigns to 'Default' in game)
return new messages.AssignmentRecord(playerName, 0, 0, true);
}
return updates.length ? new PermissionGroupUpdate(updates) : null;
async computeResolvedAssignments(playerNames: string[]): Promise<messages.AssignmentRecord[]> {
return Promise.all(playerNames.map(name => this.computeResolvedAssignment(name)));
}
}
+170 -66
View File
@@ -1,84 +1,188 @@
import * as lib from "@clusterio/lib";
import * as Messages from "./messages";
import * as messages from "./messages.js";
lib.definePermission({
name: "exp_groups.create_delete_groups",
title: "Create and delete permission groups",
description: "Create and delete permission groups.",
});
lib.definePermission({
name: "exp_groups.reorder_groups",
title: "Reorder permission groups",
description: "Reorder groups and link them to user roles.",
});
lib.definePermission({
name: "exp_groups.modify_permissions",
title: "Modify permission groups",
description: "Modify game permissions for groups.",
});
lib.definePermission({
name: "exp_groups.assign_players",
title: "Change player group",
description: "Change the permission group of a player",
});
lib.definePermission({
name: "exp_groups.list",
title: "View permission groups",
description: "View permission groups.",
});
lib.definePermission({
name: "exp_groups.list.subscribe",
title: "Subscribe to permission group updates",
description: "Subscribe to permission group updates.",
});
export * from "./messages.js";
export type { ControllerPlugin } from "./controller.js";
declare module "@clusterio/lib" {
export interface ControllerConfigFields {
"exp_groups.allow_role_inconsistency": boolean;
}
export interface InstanceConfigFields {
"exp_groups.sync_permission_groups": boolean;
"exp_groups.sync_mode": "enabled" | "disabled" | "bidirectional"
}
export interface ControllerConfigFields {
}
}
export const plugin: lib.PluginDeclaration = {
name: "exp_groups",
title: "exp_groups",
description: "Create, modify, and link factorio permission groups to clusterio user roles.",
title: "ExpGaming - Permission Groups",
description: "Clusterio plugin providing syncing of permission groups",
controllerEntrypoint: "./dist/node/controller",
controllerConfigFields: {
"exp_groups.allow_role_inconsistency": {
title: "Allow User Role Inconsistency",
description: "When true, users can be assgined to any group regardless of their roles",
type: "boolean",
initialValue: false,
},
},
instanceEntrypoint: "./dist/node/instance",
instanceConfigFields: {
"exp_groups.sync_permission_groups": {
title: "Sync Permission Groups",
description: "When true, the instance cannot deviate from the global group settings and will be hidden from the sellection dropdown.",
type: "boolean",
initialValue: true,
},
},
features: [
"SavePatching",
"ScriptCommands",
],
messages: [
Messages.PermissionGroupEditEvent,
Messages.PermissionStringsUpdate,
Messages.PermissionGroupUpdate,
messages.GroupUpdatedEvent,
messages.ManualAssignmentUpdatedEvent,
messages.ResolvedAssignmentUpdatedEvent,
messages.RoleMappingUpdatedEvent,
messages.GroupCreateRequest,
messages.GroupUpdateRequest,
messages.GroupDeleteRequest,
messages.GroupGetRequest,
messages.GroupListRequest,
messages.AssignmentCreateRequest,
messages.AssignmentUpdateRequest,
messages.AssignmentDeleteRequest,
messages.AssignmentGetRequest,
messages.AssignmentListRequest,
messages.RoleMappingCreateRequest,
messages.RoleMappingUpdateRequest,
messages.RoleMappingDeleteRequest,
messages.RoleMappingGetRequest,
messages.RoleMappingListRequest,
],
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",
type: "string",
enum: ["disabled", "enabled", "bidirectional"],
initialValue: "bidirectional",
},
},
controllerEntrypoint: "./dist/node/controller.js",
controllerConfigFields: {
},
webEntrypoint: "./web",
routes: [
"/exp_groups",
],
"/permission_groups",
"/permission_groups/:id/view",
]
};
+143 -101
View File
@@ -1,129 +1,171 @@
import * as lib from "@clusterio/lib";
import { BaseInstancePlugin } from "@clusterio/host";
import {
PermissionGroup, PermissionGroupEditEvent, PermissionGroupEditType,
PermissionGroupUpdate, PermissionInstanceId, PermissionStrings, PermissionStringsUpdate
} from "./messages";
import * as lib from "@clusterio/lib";
import * as messages from "./messages.js";
const rconBase = "/sc local Groups = package.loaded['modules/exp_groups/module_exports'];"
type EditIPC = {
type: PermissionGroupEditType,
changes: string[],
group: string,
export type IpcGroupUpdated = {
group_name: string,
group_id: number | undefined,
permissions: { is_blacklist: boolean, permissions: string[] | undefined },
};
type CreateIPC = {
group: string,
defiantion: [boolean, string[] | {}]
}
export type IpcGroupDeleted = {
group_name: string,
group_id: number | undefined,
};
type DeleteIPC = {
group: string,
}
export type IpcPlayerAssignments = {
assignments: Record<string, number>,
};
export class InstancePlugin extends BaseInstancePlugin {
permissions: Set<string> = new Set();
permissionGroups = new lib.EventSubscriber(PermissionGroupUpdate, this.instance);
permissionGroupUpdates = new lib.EventSubscriber(PermissionGroupEditEvent, this.instance);
syncId: PermissionInstanceId = this.instance.config.get("exp_groups.sync_permission_groups") ? "Global" : this.instance.id;
// Once only, don't send permissions for these groups
// This is used for groups created on this instance that only need the controller generated id
skipSendingPermissions = new Set<string>();
// This is used for groups updated / deleted on this instance to stop cycles
skipSendingUpdate = new Set<number>();
// Track known online players so that we only apply assignment updates for them
onlinePlayers = new Set<string>();
async init() {
this.instance.server.handle("exp_groups-permission_group_edit", this.handleEditIPC.bind(this));
this.instance.server.handle("exp_groups-permission_group_create", this.handleCreateIPC.bind(this));
this.instance.server.handle("exp_groups-permission_group_delete", this.handleDeleteIPC.bind(this));
}
async onStart() {
// Send the most recent version of the permission string
const permissionsString = await this.sendRcon(rconBase + "rcon.print(Groups.get_actions_json())");
this.permissions = new Set(JSON.parse(permissionsString));
this.instance.sendTo("controller", new PermissionStringsUpdate([
new PermissionStrings(this.instance.id, this.permissions, Date.now())
]));
// Subscribe to get updates for permission groups
this.permissionGroups.subscribe(this.onPermissionGroupsUpdate.bind(this));
this.permissionGroupUpdates.subscribe(this.onPermissionGroupUpdate.bind(this));
}
async onControllerConnectionEvent(event: any) {
this.permissionGroups.handleConnectionEvent(event);
this.instance.handle(messages.GroupUpdatedEvent, this.handleGroupUpdatedEvent.bind(this));
this.instance.handle(messages.ResolvedAssignmentUpdatedEvent, this.handleResolvedAssignmentUpdatedEvent.bind(this));
this.instance.server.handle(`exp_group:group_updated`, this.handleGroupUpdatedIPC.bind(this))
this.instance.server.handle(`exp_group:group_deleted`, this.handleGroupDeletedIPC.bind(this))
this.instance.server.handle(`exp_group:player_assignments`, this.handlePlayerAssignmentsIPC.bind(this))
}
async onInstanceConfigFieldChanged(field: string, curr: unknown, prev: unknown) {
if (field === "exp_groups.sync_permission_groups") {
this.syncId = curr ? "Global" : this.instance.id;
const [snapshot, synced] = this.permissionGroups.getSnapshot();
if (synced && this.instance.status !== "running") await this.syncPermissionGroups(snapshot.values());
switch(field) {
case "exp_groups.sync_mode":
await this.luaSetEmitEvents(curr == "bidirectional")
break;
}
}
async onPermissionGroupsUpdate(event: PermissionGroupUpdate | null, synced: boolean) {
if (!synced || this.instance.status !== "running" || !event?.updates.length) return;
await this.syncPermissionGroups(event.updates);
async onStart() {
// We use Date.now() because we need to manually initialise the groups on the lua side
await this.instance.sendTo("controller", new lib.SubscriptionRequest(
`exp_groups:${messages.GroupUpdatedEvent.name}`, "subscribe", Date.now()
));
const groups = await this.instance.sendTo("controller", new messages.GroupListRequest())
await this.luaSendInitialGroups(groups);
await this.luaSetEmitEvents(this.instance.config.get("exp_groups.sync_mode") == "bidirectional")
}
async syncPermissionGroups(groups: Iterable<PermissionGroup>) {
const updateCommands = [rconBase];
for (const group of groups) {
if (group.instanceId === this.syncId && group.updatedAtMs > (this.permissionGroups.values.get(group.id)?.updatedAtMs ?? 0)) {
if (group.isDeleted) {
updateCommands.push(`Groups.destroy_group('${group.name}')`);
} else if (group.permissions.size < this.permissions.size / 2) {
updateCommands.push(`Groups.get_or_create('${group.name}'):from_json('${JSON.stringify([false, [...this.permissions.values()]])}')`);
async onPlayerEvent(event: lib.PlayerEvent) {
switch(event.type) {
case "join":
this.onlinePlayers.add(event.name);
await this.subscribePlayerAssignment(event.name);
break;
case "leave":
this.onlinePlayers.delete(event.name);
await this.unsubscribePlayerAssignment(event.name);
break;
}
}
async handleGroupUpdatedEvent(event: messages.GroupUpdatedEvent) {
for (const group of event.updates) {
await this.luaSendGroupUpdate(group);
}
}
async handleResolvedAssignmentUpdatedEvent(event: messages.ManualAssignmentUpdatedEvent) {
for (const assignment of event.updates) {
if (this.onlinePlayers.has(assignment.name)) {
await this.luaSendAssignmentUpdate(assignment);
}
}
}
async handleGroupUpdatedIPC(event: IpcGroupUpdated) {
const permissions = new messages.GroupPermissions(
event.permissions.is_blacklist,
event.permissions.permissions ?? [],
)
if (event.group_id === undefined) {
this.skipSendingPermissions.add(event.group_name);
await this.instance.sendTo("controller",
new messages.GroupCreateRequest(event.group_name, permissions),
);
} else {
const inverted = [...this.permissions.values()].filter(permission => !group.permissions.has(permission));
updateCommands.push(`Groups.get_or_create('${group.name}'):from_json('${JSON.stringify([true, inverted])}')`);
}
}
}
await this.sendRcon(updateCommands.join(";"), true);
}
async onPermissionGroupUpdate(event: PermissionGroupEditEvent | null, synced: boolean) {
if (!synced || this.instance.status !== "running" || !event) return;
if (event.src.equals(lib.Address.fromShorthand({ instanceId: this.instance.id }))) return;
const getCmd = `Groups.get_or_create('${event.group}')`;
if (event.type === "add_permissions") {
await this.sendRcon(rconBase + getCmd + `:allow_actions(Groups.json_to_actions('${JSON.stringify(event.changes)}'))`);
} else if (event.type === "remove_permissions") {
await this.sendRcon(rconBase + getCmd + `:disallow_actions(Groups.json_to_actions('${JSON.stringify(event.changes)}'))`);
} else if (event.type === "assign_players") {
await this.sendRcon(rconBase + getCmd + `:add_players(game.json_to_table('${JSON.stringify(event.changes)}'))`);
this.skipSendingUpdate.add(event.group_id);
await this.instance.sendTo("controller", new messages.GroupUpdateRequest(
new messages.GroupRecord(event.group_id, event.group_name, permissions),
));
}
}
async handleEditIPC(event: EditIPC) {
this.logger.info(JSON.stringify(event))
this.instance.sendTo("controller", new PermissionGroupEditEvent(
lib.Address.fromShorthand({ instanceId: this.instance.id }),
event.type, event.group, event.changes
))
async handleGroupDeletedIPC(event: IpcGroupDeleted) {
if (event.group_id === undefined) {
return;
}
this.skipSendingUpdate.add(event.group_id);
await this.instance.sendTo("controller", new messages.GroupDeleteRequest(event.group_id));
}
async handleCreateIPC(event: CreateIPC) {
this.logger.info(JSON.stringify(event))
if (!this.permissionGroups.synced) return;
let [defaultAllow, permissionsRaw] = event.defiantion;
if (!Array.isArray(permissionsRaw)) {
permissionsRaw = [] // lua outputs {} for empty arrays
}
const permissions = [...this.permissions.values()]
.filter(permission => defaultAllow !== (permissionsRaw as String[]).includes(permission));
this.instance.sendTo("controller", new PermissionGroupUpdate([ new PermissionGroup(
this.syncId, event.group, 0, new Set(), new Set(permissions)
) ]));
async handlePlayerAssignmentsIPC(event: IpcPlayerAssignments) {
await Promise.all(
Object.entries(event.assignments).map(([playerName, groupId]) =>
this.instance.sendTo("controller",
new messages.AssignmentUpdateRequest(new messages.AssignmentRecord(playerName, groupId)),
)
)
);
}
async handleDeleteIPC(event: DeleteIPC) {
if (!this.permissionGroups.synced) return;
const group = [...this.permissionGroups.values.values()]
.find(group => group.instanceId === this.syncId && group.name === event.group);
if (group) {
group.updatedAtMs = Date.now();
group.isDeleted = true;
this.instance.sendTo("controller", new PermissionGroupUpdate([ group ]));
async subscribePlayerAssignment(playerName: string) {
await this.instance.sendTo("controller", new lib.SubscriptionRequest(
`exp_groups:${messages.ResolvedAssignmentUpdatedEvent.name}`, "subscribe", 0, lib.SubscriptionFilters.fromShorthand(playerName)
));
}
async unsubscribePlayerAssignment(playerName: string) {
await this.instance.sendTo("controller", new lib.SubscriptionRequest(
`exp_groups:${messages.ResolvedAssignmentUpdatedEvent.name}`, "unsubscribe", 0, lib.SubscriptionFilters.fromShorthand(playerName)
));
}
async luaSendInitialGroups(groups: messages.GroupRecord[]) {
if (this.instance.config.get("exp_groups.sync_mode") === "disabled") {
return;
}
await this.luaSend("initialise_groups", groups);
}
async luaSendGroupUpdate(group: messages.GroupRecord) {
if (this.instance.config.get("exp_groups.sync_mode") === "disabled") {
return;
}
if (this.skipSendingUpdate.has(group.id)) {
this.skipSendingUpdate.delete(group.id);
return;
}
const json = group.toJSON();
if (this.skipSendingPermissions.has(group.name)) {
this.skipSendingPermissions.delete(group.name);
delete (json as any).permissions;
}
await this.luaSend("receive_group_update", json);
}
async luaSendAssignmentUpdate(assignment: messages.AssignmentRecord) {
if (this.instance.config.get("exp_groups.sync_mode") === "disabled") {
return;
}
await this.luaSend("receive_assignment_update", assignment);
}
async luaSetEmitEvents(emitEvents: boolean) {
await this.luaSend("set_emit_events", emitEvents);
}
async luaSend(receiver: string, json: any) {
await this.instance.sendRcon(`/sc exp_groups.${receiver}(helpers.json_to_table[=[${JSON.stringify(json)}]=])`, true)
}
}
+689 -212
View File
@@ -1,239 +1,716 @@
import { User, InstanceDetails, IControllerUser, Link, MessageRequest, StringEnum, PermissionError, Address } from "@clusterio/lib";
import * as lib from "@clusterio/lib";
import { Type, Static } from "@sinclair/typebox";
export const PermissionInstanceIdSchema = Type.Union([InstanceDetails.jsonSchema.properties.id, Type.Literal("Global")])
export type PermissionInstanceId = InstanceDetails["id"] | "Global"
export type GamePermission = string; // todo: maybe enum this?
/**
* Data class for permission groups
*/
export class PermissionGroup {
constructor(
public instanceId: PermissionInstanceId,
public name: string,
/** A lower order assumes a lower permission group */
public order: number = 0,
/** A role will use the highest order group it is apart of */
public roleIds: User["roleIds"] = new Set(),
public permissions: Set<GamePermission> = new Set(),
public updatedAtMs: number = 0,
public isDeleted: boolean = false,
) {
// 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
*/
export class GroupPermissions {
constructor(
public isBlacklist: boolean,
public permissions: string[],
) {}
static jsonSchema = Type.Object({
instanceId: PermissionInstanceIdSchema,
name: Type.String(),
order: Type.Number(),
roleIds: Type.Array(Type.Number()),
is_blacklist: Type.Boolean(),
permissions: Type.Array(Type.String()),
updatedAtMs: Type.Optional(Type.Number()),
isDeleted: Type.Optional(Type.Boolean()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(
json.instanceId,
json.name,
json.order,
new Set(json.roleIds),
new Set(json.permissions),
json.updatedAtMs,
json.isDeleted
);
}
toJSON(): Static<typeof PermissionGroup.jsonSchema> {
toJSON(): Static<typeof GroupPermissions.jsonSchema> {
return {
instanceId: this.instanceId,
name: this.name,
order: this.order,
roleIds: [...this.roleIds.values()],
permissions: [...this.permissions.values()],
updatedAtMs: this.updatedAtMs > 0 ? this.updatedAtMs : undefined,
isDeleted: this.isDeleted ? this.isDeleted : undefined,
}
}
get id() {
return `${this.instanceId}:${this.name}`;
}
copy(newInstanceId: PermissionInstanceId) {
return new PermissionGroup(
newInstanceId,
this.name,
this.order,
new Set(this.roleIds),
new Set(this.permissions),
Date.now(),
false
)
}
}
export class InstancePermissionGroups {
constructor(
public instanceId: PermissionInstanceId,
public groups: Map<PermissionGroup["name"], PermissionGroup> = new Map(),
) {
}
static jsonSchema = Type.Object({
instanceId: PermissionInstanceIdSchema,
permissionsGroups: Type.Array(PermissionGroup.jsonSchema),
});
static fromJSON(json: Static<typeof InstancePermissionGroups.jsonSchema>) {
return new InstancePermissionGroups(
json.instanceId,
new Map(json.permissionsGroups.map(group => [group.name, PermissionGroup.fromJSON(group)])),
);
}
toJSON() {
return {
instanceId: this.instanceId,
permissionsGroups: [...this.groups.values()],
}
}
getUserGroup(user: User) {
const groups = [...user.roleIds.values()].map(roleId =>
// There will always be one and only one group for each role
[...this.groups.values()].find(group => group.roleIds.has(roleId))!
);
return groups.reduce((highest, group) => highest.order > group.order ? highest : group);
}
get id() {
return this.instanceId;
}
}
export class PermissionGroupUpdate {
declare ["constructor"]: typeof PermissionGroupUpdate;
static type = "event" as const;
static src = ["controller", "instance"] as const;
static dst = ["control", "instance", "controller"] as const;
static plugin = "exp_groups" as const;
static permission = "exp_groups.list.subscribe";
constructor(
public updates: PermissionGroup[],
) { }
static jsonSchema = Type.Object({
"updates": Type.Array(PermissionGroup.jsonSchema),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(
json.updates.map(update => PermissionGroup.fromJSON(update))
);
}
}
export type PermissionGroupEditType = "assign_players" | "add_permissions" | "remove_permissions";
export class PermissionGroupEditEvent {
declare ["constructor"]: typeof PermissionGroupEditEvent;
static type = "event" as const;
static src = ["instance", "controller"] as const;
static dst = ["control", "instance", "controller"] as const;
static plugin = "exp_groups" as const;
static permission(user: IControllerUser, message: MessageRequest) {
if (typeof message.data === "object" && message.data !== null) {
const data = message.data as Static<typeof PermissionGroupEditEvent.jsonSchema>;
if (data.type === "add_permissions" || data.type === "remove_permissions") {
user.checkPermission("exp_groups.modify_permissions")
} else if (data.type === "assign_players") {
user.checkPermission("exp_groups.assign_players")
} else {
throw new PermissionError("Permission denied");
}
is_blacklist: this.isBlacklist,
permissions: this.permissions,
};
}
constructor(
public src: Address,
public type: PermissionGroupEditType,
public group: string,
public changes: String[],
) { }
static jsonSchema = Type.Object({
"src": Address.jsonSchema,
"type": StringEnum(["assign_players", "add_permissions", "remove_permissions"]),
"group": Type.String(),
"changes": Type.Array(Type.String()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(Address.fromJSON(json.src), json.type, json.group, json.changes);
}
}
export class PermissionStrings {
constructor(
public instanceId: PermissionInstanceId,
public permissions: Set<GamePermission>,
public updatedAtMs: number = 0,
public isDeleted: boolean = false,
) {
}
static jsonSchema = Type.Object({
instanceId: PermissionInstanceIdSchema,
permissions: Type.Array(Type.String()),
updatedAtMs: Type.Optional(Type.Number()),
isDeleted: Type.Optional(Type.Boolean()),
});
static fromJSON(json: Static<typeof PermissionStrings.jsonSchema>) {
return new PermissionStrings(
json.instanceId,
new Set(json.permissions),
json.updatedAtMs,
json.isDeleted
return new this(
json.is_blacklist,
json.permissions,
);
}
toJSON() {
return {
instanceId: this.instanceId,
permissions: [...this.permissions.values()],
updatedAtMs: this.updatedAtMs > 0 ? this.updatedAtMs : undefined,
isDeleted: this.isDeleted ? this.isDeleted : undefined,
}
}
get id() {
return this.instanceId
}
}
export class PermissionStringsUpdate {
declare ["constructor"]: typeof PermissionStringsUpdate;
static type = "event" as const;
static src = ["instance", "controller"] as const;
static dst = ["controller", "control"] as const;
static plugin = "exp_groups" as const;
static permission = "exp_groups.list.subscribe";
export class GroupRecord {
constructor(
public updates: PermissionStrings[],
) { }
public id: number,
public name: string,
public permissions: GroupPermissions,
public updatedAtMs: number = 0,
public isDeleted: boolean = false,
) {}
static jsonSchema = Type.Object({
"updates": Type.Array(PermissionStrings.jsonSchema),
id: Type.Integer(),
name: Type.String(),
permissions: GroupPermissions.jsonSchema,
updated_at_ms: Type.Optional(Type.Number()),
is_deleted: Type.Optional(Type.Boolean()),
});
toJSON() {
let json: Static<typeof GroupRecord.jsonSchema> = {
id: this.id,
name: this.name,
permissions: this.permissions.toJSON(),
};
if (this.updatedAtMs) {
json.updated_at_ms = this.updatedAtMs;
}
if (this.isDeleted) {
json.is_deleted = true;
}
return json;
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(
json.updates.map(update => PermissionStrings.fromJSON(update))
json.id,
json.name,
GroupPermissions.fromJSON(json.permissions),
json.updated_at_ms ?? 0,
json.is_deleted ?? false,
);
}
}
export class AssignmentRecord {
constructor(
public name: string,
public groupId: number,
public updatedAtMs: number = 0,
public isDeleted: boolean = false,
) {}
get id() {
return this.name;
}
static jsonSchema = Type.Object({
name: Type.String(),
group_id: Type.Integer(),
updated_at_ms: Type.Optional(Type.Number()),
is_deleted: Type.Optional(Type.Boolean()),
});
toJSON(): Static<typeof AssignmentRecord.jsonSchema> {
let json: Static<typeof AssignmentRecord.jsonSchema> = {
name: this.name,
group_id: this.groupId,
};
if (this.updatedAtMs) {
json.updated_at_ms = this.updatedAtMs;
}
if (this.isDeleted) {
json.is_deleted = true;
}
return json;
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(
json.name,
json.group_id,
json.updated_at_ms ?? 0,
json.is_deleted ?? false,
);
}
}
export class RoleMappingRecord {
constructor(
public id: number,
public roleIds: Set<number>,
public groupId: number,
public priority: number,
public enabled: boolean,
public updatedAtMs: number = 0,
public isDeleted: boolean = false,
) {}
static jsonSchema = Type.Object({
id: Type.Integer(),
role_ids: Type.Array(Type.Integer()),
group_id: Type.Integer(),
priority: Type.Number(),
enabled: Type.Boolean(),
updated_at_ms: Type.Optional(Type.Number()),
is_deleted: Type.Optional(Type.Boolean()),
});
toJSON(): Static<typeof RoleMappingRecord.jsonSchema> {
let json: Static<typeof RoleMappingRecord.jsonSchema> = {
id: this.id,
role_ids: [...this.roleIds],
group_id: this.groupId,
priority: this.priority,
enabled: this.enabled,
};
if (this.updatedAtMs) {
json.updated_at_ms = this.updatedAtMs;
}
if (this.isDeleted) {
json.is_deleted = true;
}
return json;
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(
json.id,
new Set(json.role_ids),
json.group_id,
json.priority,
json.enabled,
json.updated_at_ms ?? 0,
json.is_deleted ?? false,
);
}
}
/*
Update events
*/
export class GroupUpdatedEvent {
declare ["constructor"]: typeof GroupUpdatedEvent;
static plugin = "exp_groups" as const;
static type = "event" as const;
static src = "controller" as const;
static dst = ["control", "instance"] as const;
static permission = "exp_groups.group.subscribe" as const;
constructor(
public updates: GroupRecord[],
) {}
static jsonSchema = Type.Object({
updates: Type.Array(GroupRecord.jsonSchema),
});
toJSON() {
return { updates: this.updates.map(group => group.toJSON()) };
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.updates.map(group => GroupRecord.fromJSON(group)));
}
}
export class ManualAssignmentUpdatedEvent {
declare ["constructor"]: typeof ManualAssignmentUpdatedEvent;
static plugin = "exp_groups" as const;
static type = "event" as const;
static src = "controller" as const;
static dst = ["control", "instance"] as const;
static permission = "exp_groups.assignment.subscribe" as const;
constructor(
public updates: AssignmentRecord[],
) {}
static jsonSchema = Type.Object({
updates: Type.Array(AssignmentRecord.jsonSchema),
});
toJSON() {
return { updates: this.updates.map(assignment => assignment.toJSON()) };
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.updates.map(assignment => AssignmentRecord.fromJSON(assignment)));
}
}
export class ResolvedAssignmentUpdatedEvent {
declare ["constructor"]: typeof ResolvedAssignmentUpdatedEvent;
static plugin = "exp_groups" as const;
static type = "event" as const;
static src = "controller" as const;
static dst = ["control", "instance"] as const;
static permission = "exp_groups.assignment.subscribe" as const;
constructor(
public updates: AssignmentRecord[],
) {}
static jsonSchema = Type.Object({
updates: Type.Array(AssignmentRecord.jsonSchema),
});
toJSON() {
return { updates: this.updates.map(assignment => assignment.toJSON()) };
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.updates.map(assignment => AssignmentRecord.fromJSON(assignment)));
}
}
export class RoleMappingUpdatedEvent {
declare ["constructor"]: typeof RoleMappingUpdatedEvent;
static plugin = "exp_groups" as const;
static type = "event" as const;
static src = "controller" as const;
static dst = "control" as const;
static permission = "exp_groups.role_mapping.subscribe" as const;
constructor(
public updates: RoleMappingRecord[],
) {}
static jsonSchema = Type.Object({
updates: Type.Array(RoleMappingRecord.jsonSchema),
});
toJSON() {
return { updates: this.updates.map(roleMapping => roleMapping.toJSON()) };
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.updates.map(roleMapping => RoleMappingRecord.fromJSON(roleMapping)));
}
}
/*
Group requests
*/
export class GroupCreateRequest {
declare ["constructor"]: typeof GroupCreateRequest;
static plugin = "exp_groups" as const;
static type = "request" as const;
static src = ["control", "instance"] as const;
static dst = "controller" as const;
static permission = "exp_groups.group.create" as const;
static Response = GroupRecord;
constructor(
public name: string,
public permissions: GroupPermissions,
) {}
static jsonSchema = Type.Object({
name: Type.String(),
permissions: GroupPermissions.jsonSchema,
});
toJSON() {
return {
name: this.name,
permissions: this.permissions.toJSON(),
};
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(
json.name,
GroupPermissions.fromJSON(json.permissions),
);
}
}
export class GroupUpdateRequest {
declare ["constructor"]: typeof GroupUpdateRequest;
static plugin = "exp_groups" as const;
static type = "request" as const;
static src = ["control", "instance"] as const;
static dst = "controller" as const;
static permission = "exp_groups.group.update" as const;
constructor(
public group: GroupRecord,
) {}
static jsonSchema = Type.Object({
group: GroupRecord.jsonSchema,
});
toJSON() {
return {
group: this.group.toJSON(),
};
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(
GroupRecord.fromJSON(json.group),
);
}
}
export class GroupDeleteRequest {
declare ["constructor"]: typeof GroupDeleteRequest;
static plugin = "exp_groups" as const;
static type = "request" as const;
static src = ["control", "instance"] as const;
static dst = "controller" as const;
static permission = "exp_groups.group.delete" as const;
constructor(
public groupId: number,
) {}
static jsonSchema = Type.Object({
group_id: Type.Integer(),
});
toJSON() {
return {
group_id: this.groupId,
};
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.group_id);
}
}
export class GroupGetRequest {
declare ["constructor"]: typeof GroupGetRequest;
static plugin = "exp_groups" as const;
static type = "request" as const;
static src = ["control", "instance"] as const;
static dst = "controller" as const;
static permission = "exp_groups.group.get" as const;
static Response = GroupRecord;
constructor(
public groupId: number,
) {}
static jsonSchema = Type.Object({
group_id: Type.Integer(),
});
toJSON() {
return {
group_id: this.groupId,
};
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.group_id);
}
}
export class GroupListRequest {
declare ["constructor"]: typeof GroupListRequest;
static plugin = "exp_groups" as const;
static type = "request" as const;
static src = ["control", "instance"] as const;
static dst = "controller" as const;
static permission = "exp_groups.group.list" as const;
static Response = lib.jsonArray(GroupRecord);
constructor() {}
}
/*
Assignment requests
*/
export class AssignmentCreateRequest {
declare ["constructor"]: typeof AssignmentCreateRequest;
static plugin = "exp_groups" as const;
static type = "request" as const;
static src = ["control", "instance"] as const;
static dst = "controller" as const;
static permission = "exp_groups.assignment.create" as const;
static Response = AssignmentRecord;
constructor(
public name: string,
public groupId: number,
) {}
static jsonSchema = Type.Object({
name: Type.String(),
group_id: Type.Integer(),
});
toJSON() {
return {
name: this.name,
group_id: this.groupId,
};
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.name, json.group_id);
}
}
export class AssignmentUpdateRequest {
declare ["constructor"]: typeof AssignmentUpdateRequest;
static plugin = "exp_groups" as const;
static type = "request" as const;
static src = ["control", "instance"] as const;
static dst = "controller" as const;
static permission = "exp_groups.assignment.update" as const;
constructor(
public assignment: AssignmentRecord,
) {}
static jsonSchema = Type.Object({
assignment: AssignmentRecord.jsonSchema,
});
toJSON() {
return {
assignment: this.assignment.toJSON(),
};
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(
AssignmentRecord.fromJSON(json.assignment),
);
}
}
export class AssignmentDeleteRequest {
declare ["constructor"]: typeof AssignmentDeleteRequest;
static plugin = "exp_groups" as const;
static type = "request" as const;
static src = ["control", "instance"] as const;
static dst = "controller" as const;
static permission = "exp_groups.assignment.delete" as const;
constructor(
public name: string,
) {}
static jsonSchema = Type.Object({
name: Type.String(),
});
toJSON() {
return {
name: this.name,
};
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.name);
}
}
export class AssignmentGetRequest {
declare ["constructor"]: typeof AssignmentGetRequest;
static plugin = "exp_groups" as const;
static type = "request" as const;
static src = ["control", "instance"] as const;
static dst = "controller" as const;
static permission = "exp_groups.assignment.get" as const;
static Response = AssignmentRecord;
constructor(
public name: string,
public resolve: boolean = false,
) {}
static jsonSchema = Type.Object({
name: Type.String(),
resolve: Type.Boolean(),
});
toJSON() {
return {
name: this.name,
resolve: this.resolve,
};
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.name, json.resolve);
}
}
export class AssignmentListRequest {
declare ["constructor"]: typeof AssignmentListRequest;
static plugin = "exp_groups" as const;
static type = "request" as const;
static src = ["control", "instance"] as const;
static dst = "controller" as const;
static permission = "exp_groups.assignment.list" as const;
static Response = lib.jsonArray(AssignmentRecord);
constructor() {}
}
/*
Role mapping requests
*/
export class RoleMappingCreateRequest {
declare ["constructor"]: typeof RoleMappingCreateRequest;
static plugin = "exp_groups" as const;
static type = "request" as const;
static src = ["control", "instance"] as const;
static dst = "controller" as const;
static permission = "exp_groups.role_mapping.create" as const;
static Response = RoleMappingRecord;
constructor(
public roleIds: number[],
public groupId: number,
public priority: number,
public enabled: boolean,
) {}
static jsonSchema = Type.Object({
role_ids: Type.Array(Type.Integer()),
group_id: Type.Integer(),
priority: Type.Number(),
enabled: Type.Boolean(),
});
toJSON() {
return {
role_ids: this.roleIds,
group_id: this.groupId,
priority: this.priority,
enabled: this.enabled,
};
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(
json.role_ids,
json.group_id,
json.priority,
json.enabled,
);
}
}
export class RoleMappingUpdateRequest {
declare ["constructor"]: typeof RoleMappingUpdateRequest;
static plugin = "exp_groups" as const;
static type = "request" as const;
static src = ["control", "instance"] as const;
static dst = "controller" as const;
static permission = "exp_groups.role_mapping.update" as const;
constructor(
public roleMapping: RoleMappingRecord,
) {}
static jsonSchema = Type.Object({
role_mapping: RoleMappingRecord.jsonSchema,
});
toJSON() {
return {
role_mapping: this.roleMapping.toJSON(),
};
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(
RoleMappingRecord.fromJSON(json.role_mapping),
);
}
}
export class RoleMappingDeleteRequest {
declare ["constructor"]: typeof RoleMappingDeleteRequest;
static plugin = "exp_groups" as const;
static type = "request" as const;
static src = ["control", "instance"] as const;
static dst = "controller" as const;
static permission = "exp_groups.role_mapping.delete" as const;
constructor(
public id: number,
) {}
static jsonSchema = Type.Object({
id: Type.Integer(),
});
toJSON() {
return {
id: this.id,
};
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.id);
}
}
export class RoleMappingGetRequest {
declare ["constructor"]: typeof RoleMappingGetRequest;
static plugin = "exp_groups" as const;
static type = "request" as const;
static src = ["control", "instance"] as const;
static dst = "controller" as const;
static permission = "exp_groups.role_mapping.get" as const;
static Response = RoleMappingRecord;
constructor(
public id: number,
) {}
static jsonSchema = Type.Object({
id: Type.Integer(),
});
toJSON() {
return {
id: this.id,
};
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.id);
}
}
export class RoleMappingListRequest {
declare ["constructor"]: typeof RoleMappingListRequest;
static plugin = "exp_groups" as const;
static type = "request" as const;
static src = ["control", "instance"] as const;
static dst = "controller" as const;
static permission = "exp_groups.role_mapping.list" as const;
static Response = lib.jsonArray(RoleMappingRecord);
constructor() {}
}
+419 -108
View File
@@ -1,127 +1,438 @@
--[[-- ExpGroups
Adds permission group syncing to clusterio
]]
local clusterio_api = require("modules/clusterio/api")
local Global = require("modules/exp_util/global")
local Groups = require("modules/exp_groups")
local compat = require("modules/clusterio/compat") --[[@as LibCompat]]
local pending_updates = {}
Global.register(pending_updates, function(tbl)
pending_updates = tbl
end)
--- Top level module table, contains event handlers and public methods
--- @class ExpGroups
local ExpGroups = {}
--- @class ExpPermissionGroups.GroupPermissions
--- @field is_blacklist boolean
--- @field permissions string[]?
--- @class ExpPermissionGroups.GroupRecord
--- @field id number
--- @field name string
--- @field permissions ExpPermissionGroups.GroupPermissions?
--- @field is_deleted boolean
--- @class ExpPermissionGroups.AssignmentRecord
--- @field name string
--- @field group_id number
--- @field is_deleted boolean
--- @class ExpPermissionGroups.ScriptData
--- @field factorio_to_clusterio_id table<number, number?>
--- @field clusterio_id_to_group table<number, LuaPermissionGroup?>
--- @field dirty_groups table<number, LuaPermissionGroup>
--- @field dirty_players table<string, LuaPlayer>
--- @field emit_updates boolean
local script_data = {}
local function setup_script_data()
if compat.script_data["exp_groups"] == nil then
--- @type ExpPermissionGroups.ScriptData
compat.script_data["exp_groups"] = {
factorio_to_clusterio_id = {},
clusterio_id_to_group = {},
dirty_groups = {},
dirty_players = {},
emit_updates = false,
}
end
ExpGroups.on_load()
end
--[[
Helper methods
]]
--- Get the default factorio permission group
--- @return LuaPermissionGroup
local function get_default_group()
return assert(game.permissions.get_group("Default"))
end
--- Move all players from one group to another
--- @param group_src LuaPermissionGroup
--- @param group_dst LuaPermissionGroup
local function move_players(group_src, group_dst)
local add_player = group_dst.add_player
for _, player in pairs(group_src.players) do
add_player(player)
end
end
--- Apply an encoded permission definition to a group
--- @param group LuaPermissionGroup
--- @param permissions ExpPermissionGroups.GroupPermissions
local function decode_group_permissions(group, permissions)
-- Construct a hash map for faster lookup
local action_map = {}
for _, input_action_name in pairs(assert(permissions.permissions)) do
action_map[input_action_name] = true
end
-- Apply the whitelist / backlist to the group
local is_blacklist = permissions.is_blacklist
local action_allowed = not is_blacklist
for input_action_name, input_action in pairs(defines.input_action) do
if action_map[input_action_name] then
group.set_allows_action(input_action, action_allowed)
else
group.set_allows_action(input_action, is_blacklist)
end
end
end
--- Encode the permissions of a group in a shorthand format
--- @param group LuaPermissionGroup
--- @return ExpPermissionGroups.GroupPermissions
local function encode_group_permissions(group)
local whitelist = {} --- @type string[]
local blacklist = {} --- @type string[]
local whitelist_index = 0
local blacklist_index = 0
-- Construct the whitelist and blacklist
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_index = whitelist_index + 1
whitelist[whitelist_index] = input_action_name
else
blacklist_index = blacklist_index + 1
blacklist[blacklist_index] = input_action_name
end
end
-- Return the whitelist if it is smaller
if blacklist_index > whitelist_index then
return whitelist_index > 0
and { is_blacklist = false, permissions = whitelist }
or { is_blacklist = false }
end
-- Otherwise return the blacklist as it is smaller
return blacklist_index > 0
and { is_blacklist = true, permissions = blacklist }
or { is_blacklist = true }
end
--[[
State handlers
]]
--- Update the factorio permission group
--- @param group_record ExpPermissionGroups.GroupRecord
local function update_group(group_record)
assert(not group_record.is_deleted)
-- Try find the group by id and then then name
local group = script_data.clusterio_id_to_group[group_record.id]
if not group then
group = game.permissions.get_group(group_record.name)
end
-- Create a new group or update the found group
if not group or not group.valid then
group = assert(game.permissions.create_group(group_record.name))
else
group.name = group_record.name
end
-- Update the permissions for the group
if group_record.permissions then
decode_group_permissions(group, group_record.permissions)
end
-- Update the script data
script_data.factorio_to_clusterio_id[group.group_id] = group_record.id
script_data.clusterio_id_to_group[group_record.id] = group
end
--- Delete the factorio permission group
--- @param group_record ExpPermissionGroups.GroupRecord
local function delete_group(group_record)
assert(group_record.is_deleted)
local default_group = get_default_group()
local group = script_data.clusterio_id_to_group[group_record.id]
if group then
move_players(group, default_group)
group.destroy()
end
end
--- Update an assignment by moving the player to their new group
--- @param assignment_record ExpPermissionGroups.AssignmentRecord
local function update_assignment(assignment_record)
assert(not assignment_record.is_deleted)
local group = script_data.clusterio_id_to_group[assignment_record.group_id]
local player = assert(game.get_player(assignment_record.name))
if group then
group.add_player(player)
end
end
--- Clear an assignment by moving the player to the default group
--- @param assignment_record ExpPermissionGroups.AssignmentRecord
local function delete_assignment(assignment_record)
assert(assignment_record.is_deleted)
local default_group = get_default_group()
local player = assert(game.get_player(assignment_record.name))
default_group.add_player(player)
end
--[[
Public methods
]]
--- Restore local references to persistent script data after load
function ExpGroups.on_load()
script_data = compat.script_data["exp_groups"]
end
--- Enable or disable emitting lua changes back to the instance plugin
--- @param enabled boolean?
function ExpGroups.set_emit_events(enabled)
script_data.emit_updates = enabled ~= false
end
--- Replace local state with expected controller state on startup
--- @param group_records ExpPermissionGroups.GroupRecord[]
function ExpGroups.initialise_groups(group_records)
local _emit_events = script_data.emit_updates
script_data.emit_updates = false
-- Update all the received groups
local seen_clusterio_ids = {} --- @type table<number, boolean>
for _, group_record in pairs(group_records) do
update_group(group_record)
seen_clusterio_ids[group_record.id] = true
end
-- Cleanup the script data, removing stale group ids
local factorio_to_clusterio_id = script_data.factorio_to_clusterio_id
local clusterio_id_to_group = script_data.clusterio_id_to_group
for factorio_id, clusterio_id in pairs(factorio_to_clusterio_id) do
if not seen_clusterio_ids[clusterio_id] then
factorio_to_clusterio_id[factorio_id] = nil
clusterio_id_to_group[clusterio_id] = nil
end
end
-- Remove all other groups
local default_group = get_default_group()
local default_group_id = default_group.group_id
for _, group in pairs(game.permissions.groups) do
if not factorio_to_clusterio_id[group.group_id] and group.group_id ~= default_group_id then
move_players(group, default_group)
group.destroy()
end
end
script_data.emit_updates = _emit_events
end
--- Receive an updated version of a group record
--- @param group_record ExpPermissionGroups.GroupRecord
function ExpGroups.receive_group_update(group_record)
local _emit_events = script_data.emit_updates
script_data.emit_updates = false
if group_record.is_deleted then
delete_group(group_record)
else
update_group(group_record)
end
script_data.emit_updates = _emit_events
end
--- Receive an updated version of a assignment record
--- @param assignment_record ExpPermissionGroups.AssignmentRecord
function ExpGroups.receive_assignment_update(assignment_record)
local _emit_events = script_data.emit_updates
script_data.emit_updates = false
if assignment_record.is_deleted then
delete_assignment(assignment_record)
else
update_assignment(assignment_record)
end
script_data.emit_updates = _emit_events
end
--- Get the current script data for debugging purposes
--- @package
function ExpGroups._script_data()
return script_data
end
--[[
IPC events
]]
--- Emit a group update to the instance plugin
--- @param group LuaPermissionGroup
local function emit_group_update(group)
clusterio_api.send_json("exp_group:group_updated", {
group_name = group.name,
group_id = script_data.factorio_to_clusterio_id[group.group_id],
permissions = encode_group_permissions(group),
})
end
--- Emit a group deletion to the instance plugin
--- @param group_id number
--- @param group_name string
local function emit_group_delete(group_id, group_name)
clusterio_api.send_json("exp_group:group_deleted", {
group_name = group_name,
group_id = script_data.factorio_to_clusterio_id[group_id],
})
end
--- Emit a player assignment to the instance plugin
--- @param assignments table<string, number>
local function emit_player_assignments(assignments)
clusterio_api.send_json("exp_group:player_assignments", {
assignments = assignments
})
end
--[[
IPC event queuing
]]
--- Mark a group as changed
--- @param group LuaPermissionGroup
local function mark_group_dirty(group)
script_data.dirty_groups[group.group_id] = group
end
--- Mark a player as changed
--- @param player LuaPlayer
local function mark_player_dirty(player)
script_data.dirty_players[player.name] = player
end
--- Flush queued group updates to the instance plugin
local function flush_group_updates()
-- Check if updates should be updated
if not script_data.emit_updates then
script_data.dirty_groups = {}
return
end
-- Get all the groups and emit their updates
for _, group in pairs(script_data.dirty_groups) do
if group.valid then
emit_group_update(group)
end
end
script_data.dirty_groups = {}
end
--- Flush queued player updates to the instance plugin
local function flush_player_updates()
-- Check if updates should be updated
if not script_data.emit_updates then
script_data.dirty_players = {}
return
end
-- Construct the update payload
local assignments = {} --- @type table<string, number>
for player_name, player in pairs(script_data.dirty_players) do
local group = player.valid and player.permission_group
if group then
assignments[player_name] = script_data.factorio_to_clusterio_id[group.group_id]
end
end
emit_player_assignments(assignments)
script_data.dirty_players = {}
end
--[[
Factorio events
]]
--- Handle clusterio server startup
local function on_server_startup()
setup_script_data()
end
--- Handle creation of permission groups
--- @param event EventData.on_permission_group_added
local function on_permission_group_added(event)
if not event.player_index then return end
pending_updates[event.group.name] = {
created = true,
sync_all = true,
tick = event.tick,
permissions = {},
players = {},
}
-- Check if updates should be updated
if not script_data.emit_updates then
return
end
mark_group_dirty(event.group)
end
--- Handle deletion of permission groups
--- @param event EventData.on_permission_group_deleted
local function on_permission_group_deleted(event)
if not event.player_index then return end
local existing = pending_updates[event.group_name]
pending_updates[event.group_name] = nil
if not existing or not existing.created then
clusterio_api.send_json("exp_groups-permission_group_delete", {
group = event.group_name,
})
-- Check if updates should be updated
if not script_data.emit_updates then
return
end
emit_group_delete(event.id, event.group_name)
end
--- Handle edits make to permission groups
--- @param event EventData.on_permission_group_edited
local function on_permission_group_edited(event)
if not event.player_index then return end
local pending = pending_updates[event.group.name]
if not pending then
pending = {
tick = event.tick,
permissions = {},
players = {},
}
pending_updates[event.group.name] = pending
-- Check if updates should be updated
if not script_data.emit_updates then
return
end
pending.tick = event.tick
if event.type == "add-permission" then
if not pending.sync_all then
pending.permissions[event.action] = true
end
elseif event.type == "remove-permission" then
if not pending.sync_all then
pending.permissions[event.action] = false
end
elseif event.type == "enable-all" then
pending.sync_all = true
elseif event.type == "disable-all" then
pending.sync_all = true
elseif event.type == "add-player" then
local player = game.get_player(event.other_player_index) --- @cast player -nil
pending.players[player.name] = true
elseif event.type == "remove-player" then
local player = game.get_player(event.other_player_index) --- @cast player -nil
pending.players[player.name] = nil
elseif event.type == "rename" then
pending.created = true
pending.sync_all = true
local old = pending_updates[event.old_name]
if old then pending.players = old.players end
on_permission_group_deleted{
tick = event.tick, player_index = event.player_index, group_name = event.old_name,
}
-- Check if this is a player or group event
if event.type == "add-player" or event.type == "remove-player" then
local player = assert(game.get_player(event.other_player_index))
mark_player_dirty(player)
else
mark_group_dirty(event.group)
end
end
local function send_updates()
local tick = game.tick - 600 -- 10 Seconds
local done = {}
for group_name, pending in pairs(pending_updates) do
if pending.tick < tick then
done[group_name] = true
if pending.sync_all then
clusterio_api.send_json("exp_groups-permission_group_create", {
group = group_name, defiantion = Groups.get_group(group_name):to_json(true),
})
else
if next(pending.players) then
clusterio_api.send_json("exp_groups-permission_group_edit", {
type = "assign_players", group = group_name, changes = table.get_keys(pending.players),
})
end
local add, remove = {}, {}
for permission, state in pairs(pending.permissions) do
if state then
add[#add + 1] = permission
else
remove[#remove + 1] = permission
end
end
if next(add) then
clusterio_api.send_json("exp_groups-permission_group_edit", {
type = "add_permissions", group = group_name, changes = Groups.actions_to_names(add),
})
end
if next(remove) then
clusterio_api.send_json("exp_groups-permission_group_edit", {
type = "remove_permissions", group = group_name, changes = Groups.actions_to_names(remove),
})
end
end
end
end
for group_name in pairs(done) do
pending_updates[group_name] = nil
end
--- Periodically flush queued changes
local function on_nth_tick_flush()
flush_group_updates()
flush_player_updates()
end
return {
events = {
[defines.events.on_permission_group_added] = on_permission_group_added,
[defines.events.on_permission_group_deleted] = on_permission_group_deleted,
[defines.events.on_permission_group_edited] = on_permission_group_edited,
},
on_nth_tick = {
[300] = send_updates,
},
local e = defines.events
local events = {
[clusterio_api.events.on_server_startup] = on_server_startup,
[e.on_multiplayer_init] = on_server_startup,
[e.on_permission_group_added] = on_permission_group_added,
[e.on_permission_group_deleted] = on_permission_group_deleted,
[e.on_permission_group_edited] = on_permission_group_edited,
}
local on_nth_tick = {
[300] = on_nth_tick_flush,
}
ExpGroups.events = events --- @package
ExpGroups.on_nth_tick = on_nth_tick --- @package
return ExpGroups
+11
View File
@@ -0,0 +1,11 @@
--[[
It is best practice to not expose any globals because all modules share a global environment
However, sometimes you need globals, for example to access functions within rcon commands
Therefore, we advise that this should be the only file in your module to expose globals
Typically this would be your control file as shown in the example below
]]
--- @diagnostic disable: global-in-non-module
-- Access using `/sc exp_groups.foo()`
exp_groups = require("modules/exp_groups/control")
+2 -2
View File
@@ -4,9 +4,9 @@
"control.lua"
],
"require": [
"globals.lua"
],
"dependencies": {
"clusterio": "*",
"exp_util": "*"
"clusterio": "*"
}
}
+2 -305
View File
@@ -1,306 +1,3 @@
local Async = require("modules/exp_util/async")
local table_to_json = helpers.table_to_json
local json_to_table = helpers.json_to_table
--- Top level module table, contains event handlers and public methods
local Groups = {}
--- @class ExpGroup : LuaPermissionGroup
--- @field group LuaPermissionGroup The permission group for this group proxy
Groups._prototype = {}
Groups._metatable = {
__index = setmetatable(Groups._prototype, {
--- @type any Annotation required because otherwise it is typed as 'table'
__index = function(self, key)
return self.group[key]
end,
}),
__class = "ExpGroup",
}
local action_to_name = {}
for name, action in pairs(defines.input_action) do
action_to_name[action] = name
end
--- Async Functions
-- These are required to allow bypassing edit_permission_group
--- Add a player to a permission group, requires edit_permission_group
--- @param player LuaPlayer Player to add to the group
--- @param group LuaPermissionGroup Group to add the player to
--- @return boolean # True if successful
local function add_player_to_group(player, group)
return group.add_player(player)
end
--- Add a players to a permission group, requires edit_permission_group
--- @param players LuaPlayer[] Players to add to the group
--- @param group LuaPermissionGroup Group to add the players to
--- @return boolean # True if successful
local function add_players_to_group(players, group)
local add_player = group.add_player
if not add_player(players[1]) then
return false
end
for i = 2, #players do
add_player(players[i])
end
return true
end
-- Async will bypass edit_permission_group but takes at least one tick
local add_player_to_group_async = Async.register(add_player_to_group)
local add_players_to_group_async = Async.register(add_players_to_group)
--- Static methods for gettings, creating and removing permission groups
--- Gets the permission group proxy with the given name or group ID.
--- @param group_name string|uint32 The name or id of the permission group
--- @return ExpGroup?
function Groups.get_group(group_name)
local group = game.permissions.get_group(group_name)
if group == nil then return nil end
return setmetatable({
group = group,
}, Groups._metatable)
end
--- Gets the permission group proxy for a players group
--- @param player LuaPlayer The player to get the group of
--- @return ExpGroup?
function Groups.get_player_group(player)
local group = player.permission_group
if group == nil then return nil end
return setmetatable({
group = group,
}, Groups._metatable)
end
--- Creates a new permission group, requires add_permission_group
--- @param group_name string Name of the group to create
--- @return ExpGroup
function Groups.new_group(group_name)
local group = game.permissions.get_group(group_name)
assert(group == nil, "Group already exists with name: " .. group_name)
group = game.permissions.create_group(group_name)
assert(group ~= nil, "Requires permission add_permission_group")
return setmetatable({
group = group,
}, Groups._metatable)
end
--- Get or create a permisison group, must use the group name not the group id
--- @param group_name string Name of the group to create
--- @return ExpGroup
function Groups.get_or_create(group_name)
local group = game.permissions.get_group(group_name)
if group then
return setmetatable({
group = group,
}, Groups._metatable)
else
group = game.permissions.create_group(group_name)
assert(group ~= nil, "Requires permission add_permission_group")
return setmetatable({
group = group,
}, Groups._metatable)
end
end
--- Destory a permission group, moves all players to default group
--- @param group_name string|uint32 The name or id of the permission group to destroy
--- @param move_to_name string|uint32? The name or id of the permission group to move players to
function Groups.destroy_group(group_name, move_to_name)
local group = game.permissions.get_group(group_name)
if group == nil then return end
local players = group.players
if #players > 0 then
local move_to = game.permissions.get_group(move_to_name or "Default")
for _, player in ipairs(players) do
player.permission_group = move_to
end
end
local success = group.destroy()
assert(success, "Requires permission delete_permission_group")
end
--- Prototype methods for modifying and working with permission groups
--- Add a player to the permission group
--- @param player LuaPlayer The player to add to the group
function Groups._prototype:add_player(player)
if not add_player_to_group(player, self.group) then
add_player_to_group_async(player, self.group)
end
end
--- Add players to the permission group
--- @param players LuaPlayer[] The player to add to the group
function Groups._prototype:add_players(players)
if not add_players_to_group(players, self.group) then
add_players_to_group_async(players, self.group)
end
end
--- Move all players to another group
--- @param other_group ExpGroup The group to move players to, default is the Default group
function Groups._prototype:move_players(other_group)
if not add_players_to_group(self.group.players, other_group.group) then
add_players_to_group_async(self.group.players, other_group.group)
end
end
--- Allow a set of actions for this group
--- @param actions defines.input_action[] Actions to allow
--- @return ExpGroup
function Groups._prototype:allow_actions(actions)
local set_allow = self.group.set_allows_action
for _, action in ipairs(actions) do
set_allow(action, true)
end
return self
end
--- Disallow a set of actions for this group
--- @param actions defines.input_action[] Actions to disallow
--- @return ExpGroup
function Groups._prototype:disallow_actions(actions)
local set_allow = self.group.set_allows_action
for _, action in ipairs(actions) do
set_allow(action, false)
end
return self
end
--- Reset the allowed state of all actions
--- @param allowed boolean? default true for allow all actions, false to disallow all actions
--- @return ExpGroup
function Groups._prototype:reset(allowed)
local set_allow = self.group.set_allows_action
if allowed == nil then allowed = true end
for _, action in pairs(defines.input_action) do
set_allow(action, allowed)
end
return self
end
--- Returns if the group is allowed a given action
--- @param action string|defines.input_action Actions to test
--- @return boolean # True if successful
function Groups._prototype:allows(action)
if type(action) == "string" then
return self.group.allows_action(defines.input_action[action])
end
return self.group.allows_action(action)
end
--- Print a message to all players in the group
function Groups._prototype:print(...)
for _, player in ipairs(self.group.players) do
player.print(...)
end
end
--- Static and Prototype methods for use with IPC
--- Convert an array of strings into an array of action names
--- @param actions_names string[] An array of action names
--- @return defines.input_action[]
local function names_to_actions(actions_names)
--- @type defines.input_action[], number[], number
local actions, invalid, invalid_i = {}, {}, 1
for i, action_name in ipairs(actions_names) do
local action = defines.input_action[action_name] --[[ @as defines.input_action? ]]
if action then
actions[i] = action
else
invalid[invalid_i] = i
invalid_i = invalid_i + 1
end
end
local last = #actions
for _, i in ipairs(invalid) do
actions[i] = actions[last]
last = last - 1
end
return actions
end
--- Get the action names from the action numbers
function Groups.actions_to_names(actions)
local names = {}
for i, action in ipairs(actions) do
names[i] = action_to_name[action]
end
return names
end
--- Get all input actions that are defined
function Groups.get_actions_json()
local rtn, rtn_i = {}, 1
for name in pairs(defines.input_action) do
rtn[rtn_i] = name
rtn_i = rtn_i + 1
end
return table_to_json(rtn)
end
--- Convert a json string array into an array of input actions
--- @param json string A json string representing a string array of actions
--- @return defines.input_action[]
function Groups.json_to_actions(json)
local tbl = json_to_table(json)
assert(tbl, "Invalid Json String")
--- @cast tbl string[]
return names_to_actions(tbl)
end
--- Returns the shortest defination of the allowed actions
-- The first value of the return can be passed to :reset
function Groups._prototype:to_json(raw)
local allow, disallow = {}, {}
local allow_i, disallow_i = 1, 1
local allows = self.group.allows_action
for name, action in pairs(defines.input_action) do
if allows(action) then
allow[allow_i] = name
allow_i = allow_i + 1
else
disallow[disallow_i] = name
disallow_i = disallow_i + 1
end
end
if allow_i >= disallow_i then
return raw and { true, disallow } or table_to_json{ true, disallow }
end
return raw and { false, allow } or table_to_json{ false, allow }
end
--- Restores this group to the state given in a json string
--- @param json string The json string to restore from
function Groups._prototype:from_json(json)
local tbl = json_to_table(json)
assert(tbl and type(tbl[1]) == "boolean" and type(tbl[2]) == "table", "Invalid Json String")
if tbl[1] then
self:reset(true):disallow_actions(names_to_actions(tbl[2]))
return
end
self:reset(false):allow_actions(names_to_actions(tbl[2]))
end
return Groups
-- Access the exports from other modules using require("modules/exp_groups")
return require("modules/exp_groups/control")
+20 -11
View File
@@ -1,41 +1,50 @@
{
"name": "@expcluster/permission_groups",
"private": true,
"version": "0.0.0",
"description": "Example Description. Package. Change me in package.json",
"name": "@expcluster/permission-groups",
"version": "7.0.1",
"description": "Clusterio plugin providing syncing of permission groups",
"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"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@clusterio/lib": "catalog:"
"@clusterio/controller": "workspace:^",
"@clusterio/host": "workspace:^",
"@clusterio/lib": "workspace:^",
"@clusterio/web_ui": "workspace:^"
},
"devDependencies": {
"@clusterio/lib": "catalog:",
"@clusterio/web_ui": "catalog:",
"@types/fs-extra": "^11.0.4",
"@ant-design/icons": "^6.2.3",
"@clusterio/controller": "workspace:^",
"@clusterio/host": "workspace:^",
"@clusterio/lib": "workspace:^",
"@clusterio/web_ui": "workspace:^",
"@types/node": "catalog:",
"@types/react": "catalog:",
"antd": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"react-router-dom": "catalog:",
"typescript": "catalog:",
"webpack": "catalog:",
"webpack-cli": "catalog:",
"webpack-merge": "catalog:"
},
"dependencies": {
"@sinclair/typebox": "catalog:",
"fs-extra": "^11.3.3"
"@sinclair/typebox": "catalog:"
},
"publishConfig": {
"access": "public"
},
"keywords": [
"clusterio",
"clusterio-plugin",
"factorio"
]
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.browser.json" },
{ "path": "./tsconfig.node.json" }
{ "path": "./tsconfig.node.json" },
{ "path": "./tsconfig.browser.json" }
]
}
@@ -0,0 +1,74 @@
import React, { useContext, useEffect } from "react";
import { Modal, Form, Select } from "antd";
import { ControlContext, useUsers } from "@clusterio/web_ui";
import * as messages from "../../messages.js";
import type { WebPlugin } from "..";
export default function AssignmentForm({ open, setOpen, initial }: {
open: boolean,
setOpen: (open: boolean) => void,
initial?: messages.AssignmentRecord,
}) {
const control = useContext(ControlContext);
const plugin = control.plugins.get("exp_groups") as WebPlugin;
const [groups] = plugin.useGroups();
const [users] = useUsers();
const [form] = Form.useForm();
function submit(values: any) {
if (initial) {
control.send(new messages.AssignmentUpdateRequest(
new messages.AssignmentRecord(
values.name,
values.groupId,
)
));
} else {
control.send(new messages.AssignmentCreateRequest(
values.name,
values.groupId,
));
}
setOpen(false);
}
useEffect(() => {
if (open) {
form.resetFields();
form.setFieldsValue(initial);
}
}, [open, initial]);
return <Modal
title={initial ? "Edit Assignment" : "Create Assignment"}
open={open}
onCancel={() => setOpen(false)}
onOk={() => form.submit()}
>
<Form form={form} onFinish={submit}>
<Form.Item name="name" label="Player" rules={[{ required: true }]}>
<Select disabled={Boolean(initial)} showSearch>
{[...users.values()].map(u => (
<Select.Option key={u.name} value={u.name}>
{u.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item name="groupId" label="Group" rules={[{ required: true }]}>
<Select>
{[...groups.values()].map(g => (
<Select.Option key={g.id} value={g.id}>
{g.name}
</Select.Option>
))}
</Select>
</Form.Item>
</Form>
</Modal>;
}
@@ -0,0 +1,111 @@
import React, { useContext, useState, useRef } from "react";
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.js";
import type { WebPlugin } from "..";
import AssignmentForm from "./AssignmentForm";
import DeletedConfirm from "./DeleteConfirm";
const strcmp = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare;
export default function AssignmentsTable() {
const control = useContext(ControlContext);
const plugin = control.plugins.get("exp_groups") as WebPlugin;
const account = useAccount();
const searchInput = useRef<InputRef>(null);
const [assignments, assignmentsSynced] = plugin.useAssignments();
const [groups, groupsSynced] = plugin.useGroups();
const [editing, setEditing] = useState<AssignmentRecord | undefined>();
const [open, setOpen] = useState(false);
const assignmentsArray = [...assignments.values()];
const groupFilters = [...groups.values()]
.filter(group => assignmentsArray.some(assignment => assignment.groupId === group.id))
.map(group => ({ text: group.name, value: group.id }))
return <>
<Table
columns={[
{
title: "Player",
dataIndex: "name",
sorter: (a: AssignmentRecord, b: AssignmentRecord) => strcmp(a.name, b.name),
filterIcon: (filtered: boolean) => (
<SearchOutlined style={{ color: filtered ? "#1677ff" : undefined }} />
),
onFilter: (value, record: AssignmentRecord) => (
record.name.toLowerCase().includes(String(value).toLowerCase())
),
filterDropdownProps: {
onOpenChange: (open: boolean) => open && setTimeout(() => searchInput.current?.select(), 100),
},
filterDropdown: ({ selectedKeys, setSelectedKeys, confirm, clearFilters }) => (
<div style={{ padding: 4 }} onKeyDown={(e) => e.stopPropagation()}>
<Input.Search
allowClear
ref={searchInput}
placeholder={"Search username"}
value={selectedKeys[0]}
onChange={(e) => setSelectedKeys([e.target.value])}
onSearch={() => confirm({ closeDropdown: false })}
onClear={() => {
clearFilters?.({ closeDropdown: false });
confirm({ closeDropdown: true });
}}
/>
</div>
),
},
{
title: "Group",
width: "40%",
filters: groupFilters,
onFilter: (value, record: AssignmentRecord) => (
record.groupId === value
),
render: (_: any, a: any) => (
groups.get(a.groupId)?.name ?? a.groupId
),
},
...(account.hasAnyPermission(
"exp_groups.assignment.update",
"exp_groups.assignment.delete",
) ? [{
title: "Actions",
width: "10%",
render: (_: any, record: AssignmentRecord) => (
<Space>
{account.hasPermission("exp_groups.assignment.update") &&
<Button icon={<EditOutlined />} onClick={() => {
setEditing(record);
setOpen(true);
}} />
}
{account.hasPermission("exp_groups.assignment.delete") && <DeletedConfirm
onConfirm={() => control.send(new AssignmentDeleteRequest(record.name))}
/>}
</Space>
),
}] : []),
]}
dataSource={assignmentsArray}
loading={!assignmentsSynced || !groupsSynced}
rowKey={(a) => a.name}
pagination={{
defaultPageSize: 50,
showSizeChanger: true,
pageSizeOptions: ["10", "20", "50", "100", "200"],
showTotal: (total: number) => `${total} Assignments`,
}}
/>
<AssignmentForm open={open} setOpen={setOpen} initial={editing} />
</>;
}
@@ -0,0 +1,16 @@
import React from "react";
import { Button, Popconfirm } from "antd";
import { DeleteOutlined } from "@ant-design/icons";
import { TooltipPlacement } from "antd/es/tooltip";
export default function DeletedConfirm(props: { onConfirm: () => void, placement?: TooltipPlacement }) {
return <Popconfirm
title="Delete?"
okText="Delete"
okButtonProps={{ danger: true }}
onConfirm={props.onConfirm}
placement={props.placement}
>
<Button danger icon={<DeleteOutlined />} />
</Popconfirm>
}
+45
View File
@@ -0,0 +1,45 @@
import React, { useContext, useEffect } from "react";
import { Modal, Form, Input, Switch } from "antd";
import { useNavigate } from "react-router-dom";
import { ControlContext } from "@clusterio/web_ui";
import * as messages from "../../messages.js";
export default function GroupForm({ open, setOpen }: {
open: boolean,
setOpen: (open: boolean) => void,
}) {
const control = useContext(ControlContext);
const navigate = useNavigate();
const [form] = Form.useForm();
async function submit(values: any) {
const group = await control.send(new messages.GroupCreateRequest(
values.name, new messages.GroupPermissions(Boolean(values.isBlacklist), [])
));
navigate(`/permission_groups/${group.id}/view`);
setOpen(false);
}
useEffect(() => {
if (open) form.resetFields();
}, [open]);
return <Modal
title="Create Group"
open={open}
onCancel={() => setOpen(false)}
onOk={() => form.submit()}
>
<Form form={form} onFinish={submit}>
<Form.Item name="name" label="Name" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="isBlacklist" label="Grant all by default" valuePropName="checked">
<Switch />
</Form.Item>
</Form>
</Modal>;
}
+287
View File
@@ -0,0 +1,287 @@
import React, { useContext, useEffect, useMemo, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
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.js";
import type { WebPlugin } from "..";
const DOMAIN_MAPPING = {
"Admin": ["admin", "cheat", "permission", "infinity", "editor", "spawn"],
"Building & Crafting": ["mining", "build", "craft", "deconstruct", "rotate", "entity"],
"Inventory": ["inventory", "stack", "cursor", "slot", "quick_bar", "item", "equipment"],
"Blueprints": ["blueprint", "copy", "paste", "undo", "redo", "upgrade"],
"Trains": ["train", "station", "schedule", "interrupt", "rolling_stock"],
"Circuit Network": ["combinator", "circuit", "signal", "wire", "cable", "speaker", "display_panel"],
"GUI": ["gui", "open"],
"Pins & Alerts": ["pin", "alert", "tag"],
"Logistics": ["logistic", "filter", "request"],
"Space": ["space", "surface", "planet", "rocket"],
"Toggles": ["toggle", "switch", "swap", "set"],
};
function getDomain(name: string) {
const n = name.toLowerCase();
for (const [domain, keywords] of Object.entries(DOMAIN_MAPPING)) {
if (keywords.some(k => n.includes(k))) return domain;
}
return "Other";
}
export default function GroupViewPage() {
const control = useContext(ControlContext);
const plugin = control.plugins.get("exp_groups") as WebPlugin;
const navigate = useNavigate();
const account = useAccount();
const { id } = useParams();
const groupId = Number(id);
const [groups, synced] = plugin.useGroups();
const group = groups.get(groupId);
const [name, setName] = useState("");
const defaultModPack = useDefaultModPack();
const [definesJson, setDefinesJson] = useState<Record<string, number> | null>(null);
const [permissionState, setPermissionState] = useState<Record<string, boolean>>({});
const [baselineState, setBaselineState] = useState<Record<string, boolean>>({});
const [search, setSearch] = useState("");
const canUpdate = Boolean(account.hasPermission("exp_groups.group.update"));
const canDelete = Boolean(account.hasPermission("exp_groups.group.delete"));
// fetch defines
useEffect(() => {
if (!defaultModPack?.exportManifest?.assets?.defines) return;
if (definesJson) return;
const assetName = defaultModPack.exportManifest.assets.defines;
(async () => {
const response = await fetch(`${staticRoot}${assetName}`);
const json = await response.json();
setDefinesJson(json.input_action ?? {});
})().catch(notifyErrorHandler("Failed to load permissions"));
}, [defaultModPack]);
// initialise permissions
useEffect(() => {
if (!group || !definesJson) return;
if (Object.keys(permissionState).length) return;
const { isBlacklist, permissions } = group.permissions;
const base = Object.fromEntries(
Object.keys(definesJson).map(p => [
p, isBlacklist !== permissions.includes(p)
])
);
setPermissionState(base);
setBaselineState(base);
}, [group, definesJson]);
// sync name
useEffect(() => {
if (group) setName(group.name);
}, [group]);
// derived data
const allPermissions = useMemo(() => (
Object.keys(definesJson ?? {})
), [definesJson]);
const filtered = useMemo(() => allPermissions.filter(p =>
p.toLowerCase().includes(search.toLowerCase())
), [allPermissions, search]);
const grouped = useMemo(() => {
const map = new Map<string, string[]>();
for (const p of filtered) {
const domain = getDomain(p);
if (!map.has(domain)) map.set(domain, []);
map.get(domain)!.push(p);
}
const sorted = [...map.entries()].sort(([a], [b]) => a.localeCompare(b));
for (const [, perms] of sorted) {
perms.sort((a, b) => a.localeCompare(b));
}
return sorted;
}, [filtered]);
const permissionChanged = Object.keys(permissionState).some(
k => permissionState[k] !== baselineState[k]
);
const metaChanged = group ? name !== group.name : false;
const edited = permissionChanged || metaChanged;
function applyChanges() {
if (!group) return;
const whitelist: string[] = [];
const blacklist: string[] = [];
for (const [perm, enabled] of Object.entries(permissionState)) {
if (enabled) whitelist.push(perm);
else blacklist.push(perm);
}
const isBlacklist = blacklist.length < whitelist.length;
const permissions = isBlacklist ? blacklist : whitelist;
control.send(new messages.GroupUpdateRequest(
new messages.GroupRecord(group.id, name, new messages.GroupPermissions(isBlacklist, permissions))
))
.then(() => setBaselineState(permissionState))
.catch(notifyErrorHandler("Error applying changes"));
}
function revertChanges() {
if (!group) return;
setPermissionState(baselineState);
setName(group.name);
}
if (!synced) {
return <PageLayout nav={[{ name: "exp_groups" }, { name: String(groupId) }]}>
<Spin />
</PageLayout>;
}
if (!group) {
return <PageLayout nav={[{ name: "exp_groups" }, { name: String(groupId) }]}>
<PageHeader title="Group not found" />
</PageLayout>;
}
return <PageLayout nav={[{ name: "Permission Groups", path: "/permission_groups" }, { name: group.name }]}>
<PageHeader
title={group.name}
extra={<Space wrap>
{canDelete && <DeletedConfirm placement="bottomRight" onConfirm={async () => {
control.send(new messages.GroupDeleteRequest(group.id));
navigate(`/permission_groups`);
}}/>}
</Space>}
/>
{edited && (
<div style={{
position: "fixed",
bottom: 24,
left: "50%",
transform: "translateX(-50%)",
background: "#2a1912",
borderRadius: 8,
padding: "10px 14px",
display: "flex",
gap: 12,
zIndex: 1000,
}}>
<span>You have unsaved changes</span>
<Space>
<Button onClick={revertChanges}>Revert</Button>
<Button type="primary" onClick={applyChanges}>Apply</Button>
</Space>
</div>
)}
<div style={{ marginBottom: 16, display: "flex", gap: 12 }}>
<label style={{ width: 110 }}>Name</label>
<Input value={name} disabled={!canUpdate} onChange={e => setName(e.target.value)} />
</div>
<h3>Permissions</h3>
{!defaultModPack?.exportManifest?.assets?.defines && (
<Alert
type="warning"
message="Missing export"
description="An export of the default mod pack is required to modify permissions."
style={{ marginBottom: 16 }}
showIcon
/>
)}
{defaultModPack?.exportManifest?.assets?.defines && !definesJson && <Spin />}
{definesJson && <>
<Input.Search
placeholder="Search permissions"
allowClear
onChange={e => setSearch(e.target.value)}
style={{ marginBottom: 16 }}
/>
<Space direction="vertical" style={{ width: "100%" }}>
{grouped.map(([groupName, permissions]) => (
<div key={groupName} style={{ border: "1px solid #424242", padding: 12, borderRadius: 6 }}>
<Space style={{ width: "100%", justifyContent: "space-between" }}>
<strong>{groupName}</strong>
<Space>
<Button
size="small"
disabled={!canUpdate}
onClick={() => {
setPermissionState(prev => {
const next = { ...prev };
for (const p of permissions) next[p] = true;
return next;
});
}}
>
Select all
</Button>
<Button
size="small"
disabled={!canUpdate}
onClick={() => {
setPermissionState(prev => {
const next = { ...prev };
for (const p of permissions) next[p] = false;
return next;
});
}}
>
Clear
</Button>
</Space>
</Space>
<div style={{
marginTop: 12,
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))",
gap: 8,
}}>
{permissions.map(p => (
<Checkbox
key={p}
checked={permissionState[p]}
disabled={!canUpdate}
onChange={e =>
setPermissionState(prev => ({ ...prev, [p]: e.target.checked }))
}
>
{p}
</Checkbox>
))}
</div>
</div>
))}
</Space>
</>}
</PageLayout>;
}
+49
View File
@@ -0,0 +1,49 @@
import React, { useContext, useState } from "react";
import { Table } from "antd";
import { useNavigate } from "react-router-dom";
import { ControlContext } from "@clusterio/web_ui";
import { GroupRecord } from "../../messages.js";
import type { WebPlugin } from "..";
import GroupForm from "./GroupForm";
const strcmp = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare;
export default function GroupsTable() {
const control = useContext(ControlContext);
const plugin = control.plugins.get("exp_groups") as WebPlugin;
const navigate = useNavigate();
const [groups, synced] = plugin.useGroups();
const [open, setOpen] = useState(false);
return <>
<Table
columns={[
{
title: "Name",
dataIndex: "name",
sorter: (a: any, b: GroupRecord) => strcmp(a.name, b.name),
},
{
title: "Permissions",
width: "50%",
render: (_: any, g: GroupRecord) => (
`${g.permissions.permissions.length} ${g.permissions.isBlacklist ? "Disallowed" : "Allowed"}`
),
},
]}
dataSource={[...groups.values()]}
loading={!synced}
rowKey={(g) => g.id}
pagination={false}
onRow={(g) => ({
onClick: () => navigate(`/permission_groups/${g.id}/view`),
})}
/>
<GroupForm open={open} setOpen={setOpen} />
</>;
}
@@ -0,0 +1,106 @@
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.js";
import type { WebPlugin } from "..";
export default function RoleMappingForm({ open, setOpen, initial }: {
open: boolean,
setOpen: (open: boolean) => void,
initial?: RoleMappingRecord,
}) {
const control = useContext(ControlContext);
const plugin = control.plugins.get("exp_groups") as WebPlugin;
const [groups] = plugin.useGroups();
const [roles] = useRoles();
const [form] = Form.useForm();
function submit(values: any) {
if (initial) {
control.send(new RoleMappingUpdateRequest(
new RoleMappingRecord(
initial.id,
new Set(values.roleIds),
values.groupId,
values.priority,
values.enabled,
)
));
} else {
control.send(new RoleMappingCreateRequest(
values.roleIds,
values.groupId,
values.priority,
values.enabled,
));
}
setOpen(false);
}
useEffect(() => {
if (open) {
form.resetFields();
form.setFieldsValue({
...initial,
roleIds: initial ? [...initial.roleIds] : [],
});
}
}, [open, initial]);
return <Modal
title={initial ? "Edit Role Mapping" : "Create Role Mapping"}
open={open}
onCancel={() => setOpen(false)}
onOk={() => form.submit()}
>
<Alert
type="info"
showIcon
message="How role mappings work"
style={{ marginBottom: 16 }}
description={
<ul style={{ paddingLeft: 16, margin: 0 }}>
<li>Manual assignments always take priority.</li>
<li>Mappings are checked in priority order (highest first).</li>
<li>A mapping applies only if the user has <b>all listed roles</b>.</li>
<li>The first matching mapping assigns the group.</li>
<li>If none match, the player is given all permissions.</li>
</ul>
}
/>
<Form form={form} onFinish={submit}>
<Form.Item name="groupId" label="Group" rules={[{ required: true }]}>
<Select>
{[...groups.values()].map(g => (
<Select.Option key={g.id} value={g.id}>
{g.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item name="roleIds" label="Roles" rules={[{ required: true }]}>
<Select mode="multiple">
{[...roles.values()].map(r => (
<Select.Option key={r.id} value={r.id}>
{r.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item name="priority" label="Priority" initialValue={100}>
<InputNumber style={{ width: "100%" }} />
</Form.Item>
<Form.Item name="enabled" label="Enabled" valuePropName="checked" initialValue={true}>
<Switch />
</Form.Item>
</Form>
</Modal>;
}
@@ -0,0 +1,107 @@
import React, { useContext, useState } from "react";
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.js";
import type { WebPlugin } from "..";
import RoleMappingForm from "./RoleMappingForm";
import DeletedConfirm from "./DeleteConfirm";
export default function RoleMappingsTable() {
const control = useContext(ControlContext);
const plugin = control.plugins.get("exp_groups") as WebPlugin;
const account = useAccount();
const [roleMappings, roleMappingsSynced] = plugin.useRoleMappings();
const [groups, groupsSynced] = plugin.useGroups();
const [roles, rolesSynced] = useRoles();
const [editing, setEditing] = useState<RoleMappingRecord | undefined>();
const [open, setOpen] = useState(false);
const roleMappingArray = [...roleMappings.values()];
const roleFilters = [...roles.values()]
.filter(role => roleMappingArray.some(mapping => mapping.roleIds.has(role.id)))
.map(role => ({ text: role.name, value: role.id }));
const groupFilters = [...groups.values()]
.filter(group => roleMappingArray.some(mapping => mapping.groupId === group.id))
.map(group => ({ text: group.name, value: group.id }))
return <>
<Table
columns={[
{
title: "Priority",
dataIndex: "priority",
width: "10%",
},
{
title: "Roles",
filters: roleFilters,
onFilter: (value, record: RoleMappingRecord) => (
record.roleIds.has(value as number)
),
render: (_: any, record: RoleMappingRecord) => (
[...record.roleIds].map(id => <Tag key={id}>{roles.get(id)?.name ?? id}</Tag>)
),
},
{
title: "Group",
width: "30%",
filters: groupFilters,
onFilter: (value, record: RoleMappingRecord) => (
record.groupId === value
),
render: (_: any, record: RoleMappingRecord) => (
groups.get(record.groupId)?.name ?? record.groupId
),
},
{
title: "Enabled",
width: "10%",
filters: [
{ text: "Enabled", value: true },
{ text: "Disabled", value: false },
],
onFilter: (value, record: RoleMappingRecord) => (
record.enabled === value
),
render: (_: any, record: RoleMappingRecord) => (
record.enabled ? "Yes" : "No"
),
},
...(account.hasAnyPermission(
"exp_groups.role_mapping.update",
"exp_groups.role_mapping.delete",
) ? [{
title: "Actions",
width: "10%",
render: (_: any, record: any) => (
<Space>
{account.hasPermission("exp_groups.role_mapping.update") && <Button
icon={<EditOutlined />}
onClick={() => {
setEditing(record);
setOpen(true);
}}
/>}
{account.hasPermission("exp_groups.role_mapping.delete") && <DeletedConfirm
onConfirm={() => control.send(new RoleMappingDeleteRequest(record.id))}
/>}
</Space>
),
}] : []),
]}
dataSource={roleMappingArray.sort((a, b) => b.priority - a.priority)}
loading={!roleMappingsSynced || !groupsSynced || !rolesSynced}
rowKey={(m) => m.id}
pagination={false}
/>
<RoleMappingForm open={open} setOpen={setOpen} initial={editing} />
</>;
}
-129
View File
@@ -1,129 +0,0 @@
import React, { useState } from 'react';
import { Tree } from 'antd';
import type { TreeDataNode, TreeProps } from 'antd';
const defaultData: TreeDataNode[] = [
{
title: "Group 1",
key: "G-1",
icon: false,
children: [
{
title: "Role 1",
key: "R-1"
},
{
title: "Role 2",
key: "R-2"
},
{
title: "Role 3",
key: "R-3"
}
]
},
{
title: "Group 2",
key: "G-2",
icon: false,
children: [
{
title: "Role 4",
key: "R-4"
},
{
title: "Role 5",
key: "R-5"
}
]
},
{
title: "Default",
key: "G-3",
icon: false,
children: [
{
title: "Role 6",
key: "R-6"
}
]
}
];
export function GroupTree() {
const [gData, setGData] = useState(defaultData);
const onDrop: TreeProps['onDrop'] = (info) => {
const dropKey = info.node.key;
const dragKey = info.dragNode.key;
const dropPos = info.node.pos.split('-');
const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]); // the drop position relative to the drop node, inside 0, top -1, bottom 1
const findKey = (
data: TreeDataNode[],
key: React.Key,
callback: (node: TreeDataNode, i: number, data: TreeDataNode[]) => void,
) => {
for (let i = 0; i < data.length; i++) {
if (data[i].key === key) {
return callback(data[i], i, data);
}
if (data[i].children) {
findKey(data[i].children!, key, callback);
}
}
};
const data = [...gData]
// Find dragObject
let dragObj: TreeDataNode;
findKey(data, dragKey, (item, index, arr) => {
arr.splice(index, 1);
dragObj = item;
});
if (!info.dropToGap) {
// Drop on the content
findKey(data, dropKey, (item) => {
item.children = item.children || [];
// where to insert. New item was inserted to the start of the array in this example, but can be anywhere
item.children.unshift(dragObj);
});
} else {
let ar: TreeDataNode[] = [];
let i: number;
findKey(data, dropKey, (_item, index, arr) => {
ar = arr;
i = index;
});
if (dropPosition === -1) {
// Drop on the top of the drop node
ar.splice(i!, 0, dragObj!);
} else {
// Drop on the bottom of the drop node
ar.splice(i! + 1, 0, dragObj!);
}
}
setGData(data)
};
const allowDrop: TreeProps['allowDrop'] = ({dragNode, dropNode, dropPosition}) => {
const dragType = (dragNode.key as string).charAt(0);
const dropType = (dropNode.key as string).charAt(0);
return dropType === dragType && dropPosition != 0 || dragType === "R" && dropType === "G" && dropPosition == 0
}
return (
<Tree
className="draggable-tree"
defaultExpandAll={true}
draggable
blockNode
onDrop={onDrop}
allowDrop={allowDrop}
treeData={gData}
/>
);
};
+83 -57
View File
@@ -1,82 +1,108 @@
import React, {
useContext, useEffect, useState,
useCallback, useSyncExternalStore,
} from "react";
// import {
//
// } from "antd";
import {
BaseWebPlugin, PageLayout, PageHeader, Control, ControlContext, notifyErrorHandler,
useInstances,
} from "@clusterio/web_ui";
import { PermissionGroupUpdate, PermissionInstanceId, PermissionStringsUpdate } from "../messages";
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.js";
import * as lib from "@clusterio/lib";
import { GroupTree } from "./components/groupTree";
import GroupsTable from "./components/GroupsTable";
import AssignmentsTable from "./components/AssignmentsTable";
import RoleMappingsTable from "./components/RoleMappingsTable";
import GroupViewPage from "./components/GroupViewPage";
import GroupForm from "./components/GroupForm";
import RoleMappingForm from "./components/RoleMappingForm";
import AssignmentForm from "./components/AssignmentForm";
function MyTemplatePage() {
const control = useContext(ControlContext);
const plugin = control.plugins.get("exp_groups") as WebPlugin;
const [permissionStrings, permissionStringsSynced] = plugin.usePermissionStrings();
const [permissionGroups, permissionGroupsSynced] = plugin.usePermissionGroups();
const [instances, instancesSync] = useInstances();
let [roles, setRoles] = useState<lib.Role[]>([]);
function ExpGroupsPage() {
const account = useAccount();
useEffect(() => {
control.send(new lib.RoleListRequest()).then(newRoles => {
setRoles(newRoles);
}).catch(notifyErrorHandler("Error fetching role list"));
}, []);
const [groupOpen, setGroupOpen] = useState(false);
const [assignmentOpen, setAssignmentOpen] = useState(false);
const [roleMappingOpen, setRoleMappingOpen] = useState(false);
return <PageLayout nav={[{ name: "exp_groups" }]}>
<PageHeader title="exp_groups" />
Permission Strings: {String(permissionStringsSynced)} {JSON.stringify([...permissionStrings.values()])} <br/>
Permission Groups: {String(permissionGroupsSynced)} {JSON.stringify([...permissionGroups.values()])} <br/>
Instances: {String(instancesSync)} {JSON.stringify([...instances.values()].map(instance => [instance.id, instance.name]))} <br/>
Roles: {JSON.stringify([...roles.values()].map(role => [role.id, role.name]))} <br/>
<GroupTree/>
return <PageLayout nav={[{ name: "Permission Groups" }]}>
<PageHeader title="Permission Groups" />
{account.hasPermission("exp_groups.group.list") && <>
<SectionHeader
title="Groups"
extra={
account.hasPermission("exp_groups.group.create")
? <Button type="primary" onClick={() => setGroupOpen(true)}>Create</Button>
: undefined
}
/>
<GroupsTable />
<GroupForm open={groupOpen} setOpen={setGroupOpen} />
</>}
{account.hasPermission("exp_groups.role_mapping.list") && <>
<SectionHeader
title="Role Mappings"
extra={
account.hasPermission("exp_groups.role_mapping.create")
? <Button type="primary" onClick={() => setRoleMappingOpen(true)}>Create</Button>
: undefined
}
/>
<RoleMappingsTable />
<RoleMappingForm open={roleMappingOpen} setOpen={setRoleMappingOpen} />
</>}
{account.hasPermission("exp_groups.assignment.list") && <>
<SectionHeader
title="Assignments"
extra={
account.hasPermission("exp_groups.assignment.create")
? <Button type="primary" onClick={() => setAssignmentOpen(true)}>Create</Button>
: undefined
}
/>
<AssignmentsTable />
<AssignmentForm open={assignmentOpen} setOpen={setAssignmentOpen} />
</>}
</PageLayout>;
}
export class WebPlugin extends BaseWebPlugin {
permissionStrings = new lib.EventSubscriber(PermissionStringsUpdate, this.control);
permissionGroups = new lib.EventSubscriber(PermissionGroupUpdate, this.control);
groups = new lib.MapSubscriber(messages.GroupUpdatedEvent, this.control);
assignments = new lib.MapSubscriber(messages.ManualAssignmentUpdatedEvent, this.control);
roleMappings = new lib.MapSubscriber(messages.RoleMappingUpdatedEvent, this.control);
async init() {
this.pages = [
{
path: "/exp_groups",
sidebarName: "exp_groups",
permission: "exp_groups.list",
content: <MyTemplatePage/>,
path: "/permission_groups",
sidebarName: "Permission Groups",
permission: (account => account.hasAnyPermission(
"exp_groups.group.list",
"exp_groups.assignment.list",
"exp_groups.role_mapping.list",
)),
content: <ExpGroupsPage />,
},
{
path: "/permission_groups/:id/view",
sidebarPath: "/permission_groups",
permission: "exp_groups.group.get",
content: <GroupViewPage />,
},
];
}
useInstancePermissionStrings(instanceId?: PermissionInstanceId) {
const [permissionStrings, synced] = this.usePermissionStrings();
return [instanceId !== undefined ? permissionStrings.get(instanceId) : undefined, synced] as const;
useGroups() {
const subscribe = useCallback((cb: () => void) => this.groups.subscribe(cb), []);
return useSyncExternalStore(subscribe, () => this.groups.getSnapshot());
}
usePermissionStrings() {
const control = useContext(ControlContext);
const subscribe = useCallback((callback: () => void) => this.permissionStrings.subscribe(callback), [control]);
return useSyncExternalStore(subscribe, () => this.permissionStrings.getSnapshot());
useAssignments() {
const subscribe = useCallback((cb: () => void) => this.assignments.subscribe(cb), []);
return useSyncExternalStore(subscribe, () => this.assignments.getSnapshot());
}
useInstancePermissionGroups(instanceId?: PermissionInstanceId) {
const [permissionGroups, synced] = this.usePermissionGroups();
return [instanceId !== undefined ? [...permissionGroups.values()].filter(group => group.instanceId === instanceId) : undefined, synced] as const;
}
usePermissionGroups() {
const control = useContext(ControlContext);
const subscribe = useCallback((callback: () => void) => this.permissionGroups.subscribe(callback), [control]);
return useSyncExternalStore(subscribe, () => this.permissionGroups.getSnapshot());
useRoleMappings() {
const subscribe = useCallback((cb: () => void) => this.roleMappings.subscribe(cb), []);
return useSyncExternalStore(subscribe, () => this.roleMappings.getSnapshot());
}
}
@@ -26,6 +26,8 @@ module.exports = (env = {}) => merge(common(env), {
"antd": { import: false },
"react": { import: false },
"react-dom": { import: false },
"react-router": { import: false },
"react-router-dom": { import: false },
},
}),
],
+1 -1
View File
@@ -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",
};
+17 -12
View File
@@ -16,7 +16,7 @@ Storage.register(player_elements, function(tbl)
player_elements = tbl
end)
--- @class Gui
--- @class (partial) Gui
local Gui = {
define = ExpElement.new,
from_argument = ExpElement.from_argument,
@@ -46,9 +46,11 @@ end
--- @return LuaPlayer
function Gui.get_player(input)
if type(input) == "table" and not input.player_index then
return assert(game.get_player(input.element.player_index))
--- @cast input { element: LuaGuiElement }
return (assert(game.get_player(input.element.player_index)))
end
return assert(game.get_player(input.player_index))
--- @cast input LuaGuiElement | { player_index: uint }
return (assert(game.get_player(input.player_index)))
end
--- Toggle the enable state of an element
@@ -88,7 +90,7 @@ end
--- @param define ExpElement
--- @param visible Gui.VisibleCallback | boolean | nil
function Gui.add_top_element(define, visible)
assert(Gui.top_elements[define.name] == nil, "Element is already added to the top flow")
assert(Gui.top_elements[define] == nil, "Element is already added to the top flow")
Gui.top_elements[define] = visible or false
end
@@ -96,7 +98,7 @@ end
--- @param define ExpElement
--- @param visible Gui.VisibleCallback | boolean | nil
function Gui.add_left_element(define, visible)
assert(Gui.left_elements[define.name] == nil, "Element is already added to the left flow")
assert(Gui.left_elements[define] == nil, "Element is already added to the left flow")
Gui.left_elements[define] = visible or false
end
@@ -105,7 +107,7 @@ end
--- @param define ExpElement
--- @param visible Gui.VisibleCallback | boolean | nil
function Gui.add_relative_element(define, visible)
assert(Gui.relative_elements[define.name] == nil, "Element is already added to the relative flow")
assert(Gui.relative_elements[define] == nil, "Element is already added to the relative flow")
Gui.relative_elements[define] = visible or false
end
@@ -114,7 +116,8 @@ end
--- @param player LuaPlayer
--- @return LuaGuiElement
function Gui.get_top_element(define, player)
return assert(player_elements[player.index].top[define.name], "Element is not on the top flow")
local elements = assert(player_elements[player.index])
return (assert(elements.top[define.name], "Element is not on the top flow"))
end
--- Register a element define to be drawn to the left flow on join
@@ -122,7 +125,8 @@ end
--- @param player LuaPlayer
--- @return LuaGuiElement
function Gui.get_left_element(define, player)
return assert(player_elements[player.index].left[define.name], "Element is not on the left flow")
local elements = assert(player_elements[player.index])
return (assert(elements.left[define.name], "Element is not on the left flow"))
end
--- Register a element define to be drawn to the relative flow on join
@@ -130,7 +134,8 @@ end
--- @param player LuaPlayer
--- @return LuaGuiElement
function Gui.get_relative_element(define, player)
return assert(player_elements[player.index].relative[define.name], "Element is not on the relative flow")
local elements = assert(player_elements[player.index])
return (assert(elements.relative[define.name], "Element is not on the relative flow"))
end
--- Ensure all the correct elements are visible and exist
@@ -188,9 +193,9 @@ function Gui._ensure_consistency(event)
-- This check isn't needed, but allows the toolbar file to be deleted without modifying any lib code
if Gui.toolbar then
--- @diagnostic disable-next-line invisible
--- @diagnostic disable-next-line: access-invisible
Gui.toolbar._create_elements(player)
--- @diagnostic disable-next-line invisible
--- @diagnostic disable-next-line: access-invisible
Gui.toolbar._ensure_consistency(player)
end
end
@@ -211,7 +216,7 @@ local function on_gui_opened(event)
if visible then
event.element = element
--- @diagnostic disable-next-line invisible
--- @diagnostic disable-next-line: access-invisible
define:raise_event(event)
end
end
+6 -2
View File
@@ -62,6 +62,7 @@ local GuiData = {
--- @field player_data table<uint, any>
--- @field force_data table<uint, any>
--- @field global_data table
--- @field [DataKey] any
-- This class has no prototype methods
-- Same as raw but __index ensures the values exist
@@ -89,13 +90,16 @@ function GuiData._metatable.__index(self, key)
assert(type(key) == "userdata", "Index type '" .. ExpUtil.get_class_name(key) .. "' given to GuiData. Must be of type userdata.")
local object_name = key.object_name --- @diagnostic disable-line assign-type-mismatch
if object_name == "LuaGuiElement" then
--- @cast key LuaGuiElement
local data = self._raw.element_data
local player_elements = data and data[key.player_index]
return player_elements and player_elements[key.index]
elseif object_name == "LuaPlayer" then
--- @cast key LuaPlayer
local data = self._raw.player_data
return data and data[key.index]
elseif object_name == "LuaForce" then
--- @cast key LuaForce
local data = self._raw.force_data
return data and data[key.index]
else
@@ -153,7 +157,7 @@ end
--- @param scope string
--- @return GuiData
function GuiData.get(scope)
return GuiData._scopes[scope] --[[ @as GuiData ]]
return GuiData._scopes[scope] --[[@as GuiData]]
end
--- Used to clean up data from destroyed elements
@@ -168,7 +172,7 @@ local function on_object_destroyed(event)
for _, scope in pairs(registered_scopes) do
local data = scope._raw.element_data
local player_elements = data and data[player_index]
if player_elements then
if data and player_elements then
player_elements[element_index] = nil
if not next(player_elements) then
data[player_index] = nil
+17 -17
View File
@@ -1,4 +1,4 @@
--- @class Gui
--- @class (partial) Gui
local Gui = require("modules/exp_gui")
--- @class Gui.elements
@@ -28,7 +28,7 @@ Elements.aligned_flow = Gui.define("aligned_flow")
vertically_stretchable = vertical_align ~= "center",
horizontally_stretchable = horizontal_align ~= "center",
}
end) --[[ @as any ]]
end) --[[@as any]]
--- A solid horizontal white bar element
--- @class Gui.elements.bar: ExpElement
@@ -48,11 +48,11 @@ Elements.bar = Gui.define("bar")
else
style.horizontally_stretchable = true
end
end) --[[ @as any ]]
end) --[[@as any]]
--- A label which is centered
--- @class Gui.elements.centered_label: ExpElement
--- @overload fun(parent: LuaGuiElement, width: number, caption: LocalisedString, tooltip: LocalisedString?): LuaGuiElement
--- @overload fun(parent: LuaGuiElement, width: number, caption: LocalisedString?, tooltip: LocalisedString?): LuaGuiElement
Elements.centered_label = Gui.define("centered_label")
:draw{
type = "label",
@@ -63,7 +63,7 @@ Elements.centered_label = Gui.define("centered_label")
horizontal_align = "center",
single_line = false,
width = Gui.from_argument(1),
} --[[ @as any ]]
} --[[@as any]]
--- A label which has two white bars on either side of it
--- @class Gui.elements.title_label: ExpElement
@@ -91,7 +91,7 @@ Elements.title_label = Gui.define("title_label")
Elements.bar(flow)
return label
end) --[[ @as any ]]
end) --[[@as any]]
--- A fixed size vertical scroll pane
--- @class Gui.elements.scroll_pane: ExpElement
@@ -109,7 +109,7 @@ Elements.scroll_pane = Gui.define("scroll_pane")
padding = { 1, 3 },
maximal_height = Gui.from_argument(1),
horizontally_stretchable = true,
} --[[ @as any ]]
} --[[@as any]]
--- A fixed size vertical scroll pane containing a table
--- @class Gui.elements.scroll_table: ExpElement
@@ -132,7 +132,7 @@ Elements.scroll_table = Gui.define("scroll_table")
cell_padding = 1,
vertical_align = "center",
horizontally_stretchable = true,
} --[[ @as any ]]
} --[[@as any]]
--- A container frame
--- @class Gui.elements.container: ExpElement
@@ -159,13 +159,13 @@ Elements.container = Gui.define("container")
vertically_stretchable = false,
horizontally_stretchable = false,
minimal_width = Gui.from_argument(1),
} --[[ @as any ]]
} --[[@as any]]
--- Get the root element of a container
--- @param container LuaGuiElement
--- @return LuaGuiElement
function Elements.container.get_root_element(container)
return container.parent
return assert(container.parent)
end
--- A frame within a container
@@ -183,7 +183,7 @@ Elements.subframe_base = Gui.define("container_subframe")
padding = { 3, 6, 0, 6 },
use_header_filler = false,
horizontally_stretchable = true,
} --[[ @as any ]]
} --[[@as any]]
--- @class Gui.elements.header.opts
--- @field name string?
@@ -212,7 +212,7 @@ Elements.header = Gui.define("container_header")
subframe.add{ type = "empty-widget" }.style.horizontally_stretchable = true
return subframe
end) --[[ @as any ]]
end) --[[@as any]]
--- @class Gui.elements.footer.opts
--- @field name string?
@@ -241,7 +241,7 @@ Elements.footer = Gui.define("container_footer")
subframe.add{ type = "empty-widget" }.style.horizontally_stretchable = true
return subframe
end) --[[ @as any ]]
end) --[[@as any]]
--- A button used to destroy its target when clicked, intended for screen frames
--- @class Gui.elements.screen_frame_close: ExpElement
@@ -259,7 +259,7 @@ Elements.screen_frame_close = Gui.define("screen_frame_close")
:on_click(function(def, player, element, event)
--- @cast def Gui.elements.screen_frame_close
Gui.destroy_if_valid(def.data[element])
end) --[[ @as any ]]
end) --[[@as any]]
--- A draggable frame with close button and button flow
--- @class Gui.elements.screen_frame: ExpElement
@@ -313,20 +313,20 @@ Elements.screen_frame = Gui.define("screen_frame")
direction = "vertical",
style = "inside_shallow_frame_packed",
}
end) --[[ @as any ]]
end) --[[@as any]]
--- Get the button flow for a screen frame
--- @param screen_frame LuaGuiElement
--- @return LuaGuiElement
function Elements.screen_frame.get_button_flow(screen_frame)
return assert(Elements.screen_frame.data[screen_frame.parent], "Screen frame has no button flow")
return (assert(Elements.screen_frame.data[assert(screen_frame.parent)], "Screen frame has no button flow"))
end
--- Get the root element of a screen frame
--- @param screen_frame LuaGuiElement
--- @return LuaGuiElement
function Elements.screen_frame.get_root_element(screen_frame)
return screen_frame.parent
return assert(screen_frame.parent)
end
return Elements
+1 -3
View File
@@ -166,7 +166,6 @@ end
function GuiIter.get_tracked_elements(scope, filter)
local class_name = ExpUtil.get_class_name(filter)
if class_name == "nil" then
--- @cast filter nil
return GuiIter.all_elements(scope)
elseif class_name == "LuaPlayer" then
--- @cast filter LuaPlayer
@@ -189,7 +188,6 @@ end
function GuiIter.get_online_elements(scope, filter)
local class_name = ExpUtil.get_class_name(filter)
if class_name == "nil" then
--- @cast filter nil
return GuiIter.filtered_elements(scope, game.connected_players)
elseif class_name == "LuaPlayer" then
--- @cast filter LuaPlayer
@@ -235,7 +233,7 @@ end
function GuiIter.remove_element(scope, player_index, element_index)
local scope_elements = registered_scopes[scope]
if not scope_elements then return end
local player_elements = registered_scopes[player_index]
local player_elements = scope_elements[player_index]
if not player_elements then return end
player_elements[element_index] = nil
end
+6 -3
View File
@@ -43,6 +43,8 @@ ExpElement.events = {}
--- @field _force_data ExpElement.PostDrawCallback?
--- @field _global_data ExpElement.PostDrawCallback?
--- @field _events table<defines.events, ExpElement.EventHandler<EventData>[]>
--- @field _has_handlers boolean
--- @field _track_elements boolean
--- @overload fun(parent: LuaGuiElement, ...: any): LuaGuiElement
ExpElement._prototype = {
_track_elements = false,
@@ -158,7 +160,7 @@ function ExpElement.new(name)
},
}
ExpElement._elements[element_name] = instance --[[ @as ExpElement ]]
ExpElement._elements[element_name] = instance --[[@as ExpElement]]
return setmetatable(instance, ExpElement._metatable)
end
@@ -166,7 +168,7 @@ end
--- @param name string
--- @return ExpElement
function ExpElement.get(name)
return assert(ExpElement._elements[name], "ExpElement is not defined: " .. tostring(name))
return (assert(ExpElement._elements[name], "ExpElement is not defined: " .. tostring(name)))
end
--- Create a new instance of this element definition
@@ -274,6 +276,7 @@ function ExpElement._prototype:draw(definition)
if not next(signals) then
-- If no signals then skip var arg
--- @cast definition LuaGuiElement.add_param
self._draw = function(_, parent)
return parent.add(definition)
end
@@ -459,7 +462,7 @@ function ExpElement._prototype:raise_event(event)
for _, handler in ipairs(handlers) do
-- All gui elements will contain player and element, other events might have these as nil
-- Therefore only the signature of on_event has these values as optional
handler(self, player --[[ @as LuaPlayer ]], event.element, event --[[ @as EventData ]])
handler(self, player --[[@as LuaPlayer]], event.element, event --[[@as EventData]])
end
end
+1 -1
View File
@@ -1,4 +1,4 @@
--- @class Gui
--- @class (partial) Gui
local Gui = require("modules/exp_gui")
--- @class Gui.styles
+18 -16
View File
@@ -1,5 +1,5 @@
--- @class Gui
--- @class (partial) Gui
local Gui = require("modules/exp_gui")
local ExpElement = require("modules/exp_gui/prototype")
local mod_gui = require("mod-gui")
@@ -29,7 +29,7 @@ Toolbar.on_gui_button_toggled = script.generate_event_name()
--- @class _ExpElement._prototype
--- @field on_button_toggled ExpElement.OnEventAdder<EventData.on_gui_button_toggled>
--- @diagnostic disable-next-line: invisible, inject-field
--- @diagnostic disable-next-line: access-invisible, inject-field
function ExpElement._prototype.on_button_toggled(self, handler)
return self:on_event(Toolbar.on_gui_button_toggled, handler)
end
@@ -85,7 +85,7 @@ function Toolbar.set_button_toggled_state(define, player, state, _from_left)
button.style = state and toolbar_button_active_style or toolbar_button_default_style
-- Make the extra required adjustments
local style = button.style
local style = button.style --[[@as LuaStyle]]
style.minimal_width = original_width
style.maximal_height = original_height
if button.type == "sprite-button" then
@@ -335,9 +335,10 @@ Below here is the toolbar settings GUI and its associated functions
--- @param dst LuaGuiElement
local function copy_style(src, dst)
dst.style = src.style.name
dst.style.height = toolbar_button_small
dst.style.width = toolbar_button_small
dst.style.padding = -2
local style = dst.style --[[@as LuaStyle]]
style.height = toolbar_button_small
style.width = toolbar_button_small
style.padding = -2
end
--- Reorder the buttons relative to each other, this will update the datastore
@@ -350,7 +351,7 @@ local function move_toolbar_button(player, item, offset)
-- Swap the position in the list
local list = assert(item.parent)
local other_item = list.children[new_index]
local other_item = assert(list.children[new_index])
list.swap_children(old_index, new_index)
-- Swap the position in the top flow, offset by 1 because of settings button
@@ -365,7 +366,7 @@ local function move_toolbar_button(player, item, offset)
local other_element = Gui.get_left_element(other_left_element, player)
local left_index = element.get_index_in_parent()
local other_index = other_element.get_index_in_parent()
element.parent.swap_children(left_index, other_index)
assert(element.parent).swap_children(left_index, other_index)
end
-- If we are moving in/out of first/last place we need to update the move buttons
@@ -393,7 +394,7 @@ end
--- @param player LuaPlayer
--- @param order Gui.ToolbarOrder
function Toolbar.set_order(player, order)
local list = elements.toolbar_settings.data[player] --[[ @as LuaGuiElement ]]
local list = elements.toolbar_settings.data[player] --[[@as LuaGuiElement]]
local left_flow = Gui.get_left_flow(player)
local top_flow = Gui.get_top_flow(player)
@@ -466,7 +467,7 @@ end
function Toolbar.get_state(player)
-- Get the order of toolbar buttons
local order = {}
local list = elements.toolbar_settings.data[player] --[[ @as LuaGuiElement ]]
local list = elements.toolbar_settings.data[player] --[[@as LuaGuiElement]]
for index, item in pairs(list.children) do
order[index] = { name = item.name, favourite = elements.toolbar_list_item.data[item].set_favourite.state }
end
@@ -487,7 +488,7 @@ end
--- @param player LuaPlayer
function Toolbar._create_elements(player)
-- Add any missing items to the gui
local toolbar_list = elements.toolbar_settings.data[player] --[[ @as LuaGuiElement ]]
local toolbar_list = elements.toolbar_settings.data[player] --[[@as LuaGuiElement]]
local previous_last_index = #toolbar_list.children_names
for define in pairs(Gui.top_elements) do
if define ~= elements.close_toolbar and toolbar_list[define.name] == nil then
@@ -499,7 +500,8 @@ function Toolbar._create_elements(player)
-- Reset the state of the previous last child
local children = toolbar_list.children
if previous_last_index > 0 then
elements.toolbar_list_item.data[children[previous_last_index]].move_item_down.enabled = true
local previous_last = assert(children[previous_last_index])
elements.toolbar_list_item.data[previous_last].move_item_down.enabled = true
end
-- Set the state of the move buttons for the first and last element
@@ -513,7 +515,7 @@ end
--- @param player LuaPlayer
function Toolbar._ensure_consistency(player)
-- Update the toolbar buttons
local list = elements.toolbar_settings.data[player] --[[ @as LuaGuiElement ]]
local list = elements.toolbar_settings.data[player] --[[@as LuaGuiElement]]
for _, button in ipairs(toolbar_buttons) do
-- Update the visible state based on if the player is allowed the button
local element = Gui.get_top_element(button, player)
@@ -619,7 +621,7 @@ elements.move_item_up = Gui.define("move_item_up")
size = toolbar_button_small,
})
:on_click(function(def, player, element)
local item = assert(element.parent.parent)
local item = assert(assert(element.parent).parent)
move_toolbar_button(player, item, -1)
end)
@@ -634,7 +636,7 @@ elements.move_item_down = Gui.define("move_item_down")
size = toolbar_button_small,
})
:on_click(function(def, player, element)
local item = assert(element.parent.parent)
local item = assert(assert(element.parent).parent)
move_toolbar_button(player, item, 1)
end)
@@ -658,7 +660,7 @@ elements.set_favourite = Gui.define("set_favourite")
width = 180,
}
:on_checked_state_changed(function(def, player, element)
local define = ExpElement.get(element.tags.element_name --[[ @as string ]])
local define = ExpElement.get(element.tags.element_name --[[@as string]])
local top_element = Gui.get_top_element(define, player)
local had_visible = Toolbar.has_visible_buttons(player)
top_element.visible = element.state
+4 -3
View File
@@ -1,10 +1,11 @@
{
"name": "@expcluster/lib_gui",
"version": "0.1.0",
"version": "7.0.1",
"description": "Clusterio plugin providing a Lua GUI definition library.",
"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"
@@ -13,10 +14,10 @@
"node": ">=18"
},
"peerDependencies": {
"@clusterio/lib": "catalog:"
"@clusterio/lib": "workspace:^"
},
"devDependencies": {
"@clusterio/lib": "catalog:",
"@clusterio/lib": "workspace:^",
"@types/node": "catalog:",
"typescript": "catalog:",
"webpack": "catalog:",
+1 -1
View File
@@ -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",
};
@@ -16,21 +16,12 @@ return {
"modules.data.quickbar",
"modules.data.alt-view",
"modules.data.tag",
-- 'modules.data.bonus',
"modules.data.personal-logistic",
"modules.data.language",
--"modules.data.toolbar",
--- GUI
"modules.gui.readme",
"modules.gui.rocket-info",
"modules.gui.warp-list",
"modules.gui.player-list",
"modules.gui.vlayer",
"modules.gui._role_updates",
"modules.graftorio.require", -- graftorio
--- Config Files
"config.expcore.permission_groups", -- loads some predefined permission groups
"config.expcore.roles", -- loads some predefined roles
}
+3 -2
View File
@@ -1,4 +1,4 @@
local Roles = require("modules.exp_legacy.expcore.roles")
local Roles = require("modules/exp_roles")
return {
admin_as_active = true, --- @setting admin_as_active When true admins will be treated as active regardless of afk time
@@ -8,6 +8,7 @@ return {
trust_time = 3600 * 60 * 10, --- @setting trust_time The time in ticks that a player must be online for to count as trusted
update_time = 3600 * 30, --- @setting update_time How often in ticks the script checks for active players
custom_active_check = function(player)
return Roles.get_player_highest_role(player).index <= Roles.get_role_from_any("Veteran").index
local veteran = Roles.get_role_by_name("Veteran")
return veteran ~= nil and not Roles.get_player_highest_role(player):is_lower_than(veteran)
end,
}
+15 -216
View File
@@ -3,21 +3,17 @@
return {
--[[
TODO
force bonus
quick health regeneration
Base point is equal to the amount of standard value in each parameter.
CMMS CRS CCS CISB CHB CRDB PBR
STD 30 90 32 20 16 12 60
= 260
STD 20 60 24 10 12 8 40
= 174
MAX 60 180 64 40 32 24 120
= 480
MAX 40 120 48 20 24 16 80
= 348
]]
points = {
base = 260,
base = 174,
increase_percentage_per_role_level = 0.03,
role_name = "Member",
},
@@ -27,8 +23,7 @@ return {
name = "character_mining_speed_modifier",
scale = 1,
cost = 10,
max_value = 6,
initial_value = 3,
max_value = 4,
value_step = 0.5,
is_percentage = true,
},
@@ -36,8 +31,7 @@ return {
name = "character_running_speed_modifier",
scale = 1,
cost = 60,
max_value = 3,
initial_value = 1.5,
max_value = 2,
value_step = 0.25,
is_percentage = true,
},
@@ -45,33 +39,29 @@ return {
name = "character_crafting_speed_modifier",
scale = 1,
cost = 4,
max_value = 16,
initial_value = 8,
max_value = 12,
value_step = 1,
is_percentage = true,
},
{
name = "character_inventory_slots_bonus",
cost = 2,
scale = 10,
max_value = 200,
initial_value = 100,
cost = 2,
max_value = 100,
value_step = 10,
},
{
name = "character_health_bonus",
scale = 50,
cost = 4,
max_value = 400,
initial_value = 200,
max_value = 300,
value_step = 50,
},
{
name = "character_reach_distance_bonus",
cost = 1,
scale = 1,
max_value = 24,
initial_value = 12,
cost = 1,
max_value = 16,
value_step = 2,
combined_bonus = {
"character_resource_reach_distance_bonus",
@@ -80,104 +70,15 @@ return {
},
{
name = "personal_battery_recharge",
initial_value = 6,
max_value = 12,
value_step = 1,
scale = 4,
cost = 40,
max_value = 8,
value_step = 1,
is_special = true,
},
--[[
['character_item_pickup_distance_bonus'] = {
initial_value = 0,
max_value = 20,
value_step = 1,
scale = 1,
cost = 1,
is_percentage = false
},
['character_loot_pickup_distance_bonus'] = {
initial_value = 0,
max_value = 20,
value_step = 1,
scale = 1,
cost = 1,
is_percentage = false
},
['character_item_drop_distance_bonus'] = {
initial_value = 0,
max_value = 20,
value_step = 1,
scale = 1,
cost = 1,
is_percentage = false
}
]]
},
force_bonus = {
--[[
['character_mining_speed_modifier'] = {
initial_value = 0,
max_value = 6,
value_step = 0.5,
scale = 1,
cost = 10,
is_percentage = true
},
['character_running_speed_modifier'] = {
initial_value = 0,
max_value = 3,
value_step = 0.25,
scale = 1,
cost = 40,
is_percentage = true
},
['character_crafting_speed_modifier'] = {
initial_value = 0,
max_value = 16,
value_step = 1,
scale = 1,
cost = 4,
is_percentage = true
},
['character_inventory_slots_bonus'] = {
initial_value = 0,
max_value = 200,
value_step = 10,
scale = 100,
cost = 2,
is_percentage = false
},
['character_health_bonus'] = {
initial_value = 0,
max_value = 400,
value_step = 50,
cost = 4,
is_percentage = false
},
['character_reach_distance_bonus'] = {
initial_value = 0,
max_value = 24,
value_step = 2,
scale = 1,
cost = 1,
is_percentage = false,
combined_bonus = {
'character_resource_reach_distance_bonus',
'character_build_distance_bonus'
}
},
['worker_robots_speed_modifier'] = {
initial_value = 0,
max_value = 0,
value_step = 0,
scale = 1,
cost = 1,
is_percentage = false
},
]]
["worker_robots_battery_modifier"] = {
initial_value = 1,
max_value = 1,
value_step = 1,
scale = 1,
@@ -185,7 +86,6 @@ return {
is_percentage = false,
},
["worker_robots_storage_bonus"] = {
initial_value = 1,
max_value = 1,
value_step = 1,
scale = 1,
@@ -193,114 +93,13 @@ return {
is_percentage = false,
},
["following_robots_lifetime_modifier"] = {
initial_value = 1,
max_value = 1,
value_step = 1,
scale = 1,
cost = 1,
is_percentage = false,
},
--[[
['character_item_pickup_distance_bonus'] = {
initial_value = 0,
max_value = 20,
value_step = 1,
scale = 1,
cost = 1,
is_percentage = false
},
['character_loot_pickup_distance_bonus'] = {
initial_value = 0,
max_value = 20,
value_step = 1,
scale = 1,
cost = 1,
is_percentage = false
},
['character_item_drop_distance_bonus'] = {
initial_value = 0,
max_value = 20,
value_step = 1,
scale = 1,
cost = 1,
is_percentage = false
},
['character_trash_slot_count'] = {
initial_value = 0,
max_value = 0,
value_step = 0,
scale = 1,
cost = 1,
is_percentage = false
},
['mining_drill_productivity_bonus'] = {
initial_value = 0,
max_value = 0,
value_step = 0,
scale = 1,
cost = 1,
is_percentage = false
},
['train_braking_force_bonus'] = {
initial_value = 0,
max_value = 0,
value_step = 0,
scale = 1,
cost = 1,
is_percentage = false
},
['laboratory_speed_modifier'] = {
initial_value = 0,
max_value = 0,
value_step = 0,
scale = 1,
cost = 1,
is_percentage = false
},
['laboratory_productivity_bonus'] = {
initial_value = 0,
max_value = 0,
value_step = 0,
scale = 1,
cost = 1,
is_percentage = false
},
['inserter_stack_size_bonus'] = {
initial_value = 0,
max_value = 0,
value_step = 0,
scale = 1,
cost = 1,
is_percentage = false
},
['stack_inserter_capacity_bonus'] = {
initial_value = 0,
max_value = 0,
value_step = 0,
scale = 1,
cost = 1,
is_percentage = false
},
['artillery_range_modifier'] = {
initial_value = 0,
max_value = 0,
value_step = 0,
scale = 1,
cost = 1,
is_percentage = false
}
]]
},
surface_bonus = {
--[[
['solar_power_multiplier'] = {
initial_value = 1,
max_value = 1000,
value_step = 1,
scale = 1,
cost = 1,
is_percentage = false
}
]]
},
}
+11 -8
View File
@@ -3,6 +3,7 @@
local ExpUtil = require("modules/exp_util")
local Async = require("modules/exp_util/async")
local config_server_detail = require("modules.exp_legacy.config.server_detail") --- @dep config.server_detail
local floor = math.floor
local random = math.random
@@ -24,14 +25,15 @@ local afk_time_units = {
}
return {
--- @type table<string, LocalisedString | fun(player: LuaPlayer, is_command: boolean): LocalisedString>
messages = { --- @setting messages will trigger when ever the word is said
["discord"] = { "info.discord" },
["expgaming"] = { "info.website" },
["website"] = { "info.website" },
["status"] = { "info.status" },
["github"] = { "info.github" },
["patreon"] = { "info.patreon" },
["donate"] = { "info.patreon" },
["discord"] = { "info.discord", config_server_detail["discord"] },
["expgaming"] = { "info.website", config_server_detail["website"] },
["website"] = { "info.website", config_server_detail["website"] },
["status"] = { "info.status", config_server_detail["status"] },
["github"] = { "info.github", config_server_detail["github"] },
["patreon"] = { "info.patreon", config_server_detail["patreon"] },
["donate"] = { "info.patreon", config_server_detail["patreon"] },
["command"] = { "info.custom-commands" },
["commands"] = { "info.custom-commands" },
["softmod"] = { "info.softmod" },
@@ -64,8 +66,9 @@ return {
},
allow_command_prefix_for_messages = true, --- @setting allow_command_prefix_for_messages when true any message trigger will print to all player when prefixed
command_admin_only = false, --- @setting command_admin_only when true will only allow chat commands for admins
command_permission = "command/chat-commands", --- @setting command_permission the permission used to allow command prefixes
command_permission = "exp_scenario.chat.commands", --- @setting command_permission the permission used to allow command prefixes
command_prefix = "!", --- @setting command_prefix prefix used for commands below and to print to all players (if enabled above)
--- @type table<string, LocalisedString | fun(player: LuaPlayer, is_command: boolean): LocalisedString>
commands = { --- @setting commands will trigger only when command prefix is given
["dev"] = { "exp_chat-auto-reply.reply-dev" },
["magic"] = { "exp_chat-auto-reply.reply-magic" },
+6 -5
View File
@@ -1,5 +1,6 @@
--- Config file for the compliatrons including where they spawn and what messages they show
-- @config Compilatron
local config_server_detail = require("modules.exp_legacy.config.server_detail") --- @dep config.server_detail
return {
message_cycle = 60 * 15, --- @setting message_cycle 15 seconds default, how often (in ticks) the messages will cycle
@@ -9,16 +10,16 @@ return {
spawn_surface = "nauvis",
entity_name = "small-biter",
messages = {
{ "info.website" },
{ "info.website", config_server_detail["website"] },
{ "info.read-readme" },
{ "info.discord" },
{ "info.discord", config_server_detail["discord"] },
{ "info.softmod" },
{ "info.redmew" },
{ "info.custom-commands" },
{ "info.status" },
{ "info.status", config_server_detail["status"] },
{ "info.lhd" },
{ "info.github" },
{ "info.patreon" },
{ "info.github", config_server_detail["github"] },
{ "info.patreon", config_server_detail["patreon"] },
},
}
},
@@ -5,7 +5,6 @@ return {
show_playtime = true,
entity_protection = true,
player_reports = true,
player_warnings = true,
player_bans = true,
player_mutes = true,
player_kicks = true,
@@ -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)]]
-368
View File
@@ -1,368 +0,0 @@
--- This is the main config file for the role system; file includes defines for roles and role flags and default values
-- @config Roles
local Roles = require("modules.exp_legacy.expcore.roles") --- @dep expcore.roles
local PlayerData = require("modules.exp_legacy.expcore.player_data") --- @dep expcore.player_data
local Statistics = PlayerData.Statistics
--- Role flags that will run when a player changes roles
Roles.define_flag_trigger("is_admin", function(player, state)
player.admin = state
end)
Roles.define_flag_trigger("is_spectator", function(player, state)
player.spectator = state
end)
Roles.define_flag_trigger("is_jail", function(player, state)
if player.character then
player.character.active = not state
end
end)
--- Admin Roles
Roles.new_role("System", "SYS")
:set_permission_group("Default", true)
:set_flag("is_admin")
:set_flag("is_system")
:set_flag("is_spectator")
:set_flag("report-immune")
:set_flag("instant-respawn")
:set_allow_all()
Roles.new_role("Senior Administrator", "SAdmin")
:set_permission_group("Admin")
:set_custom_color{ r = 233, g = 63, b = 233 }
:set_flag("is_admin")
:set_flag("is_system")
:set_flag("is_spectator")
:set_flag("report-immune")
:set_flag("instant-respawn")
:set_parent("Administrator")
:allow{
"command/_rcon",
"command/debug",
"command/set-cheat-mode",
"command/research-all",
}
Roles.new_role("Administrator", "Admin")
:set_permission_group("Admin")
:set_custom_color{ r = 233, g = 63, b = 233 }
:set_flag("is_admin")
:set_flag("is_spectator")
:set_flag("report-immune")
:set_flag("instant-respawn")
:set_parent("Moderator")
:allow{
"gui/warp-list/bypass-proximity",
"gui/warp-list/bypass-cooldown",
"command/connect-all",
}
Roles.new_role("Moderator", "Mod")
:set_permission_group("Admin")
:set_custom_color{ r = 0, g = 170, b = 0 }
:set_flag("is_admin")
:set_flag("is_spectator")
:set_flag("report-immune")
:set_flag("instant-respawn")
:set_parent("Trainee")
:allow{
"command/assign-role",
"command/unassign-role",
"command/repair",
"command/kill/always",
"command/clear-tag/always",
"command/spawn/always",
"command/clear-reports",
"command/clear-warnings",
"command/clear-script-warnings",
"command/clear-last-warnings",
"command/clear-inventory",
"command/kill-enemies",
"command/remove-enemies",
--'command/bonus',
"command/home",
"command/set-home",
"command/get-home",
"command/return",
"command/connect-player",
"command/set-bot-queue",
"command/set-game-speed",
"command/set-friendly-fire",
"command/set-always-day",
"command/set-pollution-enabled",
"command/clear-pollution",
"gui/rocket-info/toggle-active",
"gui/rocket-info/remote_launch",
"gui/bonus",
"fast-tree-decon",
}
Roles.new_role("Trainee", "TrMod")
:set_permission_group("Admin")
:set_custom_color{ r = 0, g = 170, b = 0 }
:set_flag("is_admin")
:set_flag("is_spectator")
:set_flag("report-immune")
:set_parent("Veteran")
:allow{
"command/admin-chat",
"command/goto",
"command/teleport",
"command/bring",
"command/create-warning",
"command/get-warnings",
"command/get-reports",
"command/protect-entity",
"command/protect-area",
"command/protect-tag",
"command/jail",
"command/unjail",
"command/kick",
"command/ban",
"command/spectate",
"command/follow",
"command/search",
"command/search-online",
"command/search-amount",
"command/search-recent",
"command/clear-blueprints",
"gui/playerdata",
}
--- Trusted Roles
Roles.new_role("Board Member", "Board")
:set_permission_group("Trusted")
:set_custom_color{ r = 247, g = 246, b = 54 }
:set_flag("is_spectator")
:set_flag("report-immune")
:set_flag("instant-respawn")
:set_parent("Sponsor")
:allow{
"command/goto",
"command/repair",
"command/spectate",
"command/follow",
"gui/playerdata",
}
Roles.new_role("Senior Backer", "Backer")
:set_permission_group("Trusted")
:set_custom_color{ r = 238, g = 172, b = 44 }
:set_flag("is_spectator")
:set_flag("report-immune")
:set_flag("instant-respawn")
:set_parent("Sponsor")
:allow{
}
Roles.new_role("Sponsor", "Spon")
:set_permission_group("Trusted")
:set_custom_color{ r = 238, g = 172, b = 44 }
:set_flag("is_spectator")
:set_flag("report-immune")
:set_flag("instant-respawn")
:set_parent("Supporter")
:allow{
"gui/rocket-info/toggle-active",
"gui/rocket-info/remote_launch",
"gui/bonus",
--"command/bonus",
"command/home",
"command/set-home",
"command/get-home",
"command/return",
"fast-tree-decon",
}
Roles.new_role("Supporter", "Sup")
:set_permission_group("Trusted")
:set_custom_color{ r = 230, g = 99, b = 34 }
:set_flag("is_spectator")
:set_parent("Veteran")
:allow{
"command/tag-color",
"command/jail",
"command/unjail",
"command/set-join-message",
"command/remove-join-message",
}
Roles.new_role("Partner", "Part")
:set_permission_group("Trusted")
:set_custom_color{ r = 140, g = 120, b = 200 }
:set_flag("is_spectator")
:set_parent("Veteran")
:allow{
"command/jail",
"command/unjail",
}
local hours10, hours250 = 10 * 216000, 250 * 60
Roles.new_role("Veteran", "Vet")
:set_permission_group("Trusted")
:set_custom_color{ r = 140, g = 120, b = 200 }
:set_parent("Member")
:allow{
"command/chat-commands",
"command/clear-ground-items",
"command/clear-blueprints-radius",
"command/set-trains-to-automatic",
"command/set-auto-research",
}
:set_auto_assign_condition(function(player)
if player.online_time >= hours10 then
return true
else
local stats = Statistics:get(player, {})
local playtime, afk_time, map_count = stats.Playtime or 0, stats.AfkTime or 0, stats.MapsPlayed or 0
return playtime - afk_time >= hours250 and map_count >= 25
end
end)
--- Standard User Roles
Roles.new_role("Member", "Mem")
:set_permission_group("Standard")
:set_custom_color{ r = 24, g = 172, b = 188 }
:set_flag("deconlog-bypass")
:set_parent("Regular")
:allow{
"gui/task-list/add",
"gui/task-list/edit",
"gui/warp-list/add",
"gui/warp-list/edit",
"gui/surveillance",
"gui/vlayer-edit",
"gui/tool",
"command/save-quickbar",
"command/vlayer-info",
"command/personal-logistic",
"command/lawnmower",
"command/waterfill",
"command/artillery",
}
local hours3, hours15 = 3 * 216000, 15 * 60
Roles.new_role("Regular", "Reg")
:set_permission_group("Standard")
:set_custom_color{ r = 79, g = 155, b = 163 }
:set_parent("Guest")
:allow{
"command/kill",
"command/rainbow",
"command/spawn",
"command/me",
"standard-decon",
"bypass-entity-protection",
"bypass-nukeprotect",
}
:set_auto_assign_condition(function(player)
if player.online_time >= hours3 then
return true
else
local stats = Statistics:get(player, {})
local playtime, afk_time, map_count = stats.Playtime or 0, stats.AfkTime or 0, stats.MapsPlayed or 0
return playtime - afk_time >= hours15 and map_count >= 5
end
end)
--- Guest/Default role
local default = Roles.new_role("Guest", "")
:set_permission_group("Guest")
:set_custom_color{ r = 185, g = 187, b = 160 }
:allow{
"command/tag",
"command/tag-clear",
"command/commands",
"command/get-roles",
"command/locate",
"command/create-report",
"command/ratio",
"command/server-ups",
"command/save-data",
"command/data-preference",
"command/connect",
"gui/player-list",
--"gui/rocket-info",
"gui/science-info",
"gui/task-list",
"gui/warp-list",
"gui/readme",
"gui/vlayer",
"gui/research",
"gui/autofill",
"gui/module",
"gui/landfill",
"gui/production",
}
--- Jail role
Roles.new_role("Jail")
:set_permission_group("Restricted")
:set_custom_color{ r = 50, g = 50, b = 50 }
:set_block_auto_assign(true)
:set_flag("defer_role_changes")
:disallow(default.allowed)
--- System defaults which are required to be set
Roles.set_root("System")
Roles.set_default("Guest")
Roles.define_role_order{
"System", -- Best to keep root at top
"Senior Administrator",
"Administrator",
"Moderator",
"Trainee",
"Board Member",
"Senior Backer",
"Sponsor",
"Supporter",
"Partner",
"Veteran",
"Member",
"Regular",
"Jail",
"Guest", -- Default must be last if you want to apply restrictions to other roles
}
Roles.override_player_roles{
["PHIDIAS0303"] = { "Moderator", "Board Member", "Member" },
["aldldl"] = { "Administrator", "Moderator", "Member" },
["arty714"] = { "Senior Administrator", "Moderator", "Member" },
["Cooldude2606"] = { "Senior Administrator", "Moderator", "Member" },
["Drahc_pro"] = { "Administrator", "Moderator", "Member" },
["mark9064"] = { "Administrator", "Moderator", "Member" },
["7h3w1z4rd"] = { "Moderator", "Member" },
["FlipHalfling90"] = { "Moderator", "Member" },
["hamsterbryan"] = { "Moderator", "Member" },
["HunterOfGames"] = { "Moderator", "Member" },
["NextIdea"] = { "Moderator", "Member" },
["TheKernel32"] = { "Moderator", "Member" },
["TheKernel64"] = { "Moderator", "Member" },
["tovernaar123"] = { "Moderator", "Member" },
["UUBlueFire"] = { "Moderator", "Member" },
["AssemblyStorm"] = { "Moderator", "Member" },
["banakeg"] = { "Moderator", "Member" },
["connormkii"] = { "Moderator", "Member" },
["cydes"] = { "Moderator", "Member" },
["darklich14"] = { "Moderator", "Member" },
["facere"] = { "Moderator", "Member" },
["freek18"] = { "Moderator", "Member" },
["Gizan"] = { "Moderator", "Member" },
["LoicB"] = { "Moderator", "Member" },
["M74132"] = { "Moderator", "Member" },
["mafisch3"] = { "Moderator", "Member" },
["maplesyrup01"] = { "Moderator", "Member" },
["ookl"] = { "Moderator", "Member" },
["Phoenix27833"] = { "Moderator", "Member" },
["porelos"] = { "Moderator", "Member" },
["Ruuyji"] = { "Moderator", "Member" },
["samy115"] = { "Moderator", "Member" },
["SilentLog"] = { "Moderator", "Member" },
["Tcheko"] = { "Moderator", "Member" },
["thadius856"] = { "Moderator", "Member" },
["whoami32"] = { "Moderator", "Member" },
["Windbomb"] = { "Moderator", "Member" },
["XenoCyber"] = { "Moderator", "Member" },
}
@@ -7,33 +7,24 @@
local ExpUtil = require("modules/exp_util")
local Gui = require("modules/exp_gui")
local Roles = require("modules.exp_legacy.expcore.roles") --- @dep expcore.roles
local Roles = require("modules/exp_roles")
local Reports = require("modules.exp_legacy.modules.control.reports") --- @dep modules.control.reports
local Warnings = require("modules.exp_legacy.modules.control.warnings") --- @dep modules.control.warnings
local Jail = require("modules.exp_legacy.modules.control.jail") --- @dep modules.control.jail
local Jail = require("modules/exp_scenario/control/jail")
local Colors = require("modules/exp_util/include/color")
local format_player_name = ExpUtil.format_player_name_locale
local SelectedPlayer, SelectedAction
local function set_datastores(player, action)
SelectedPlayer, SelectedAction = player, action
end
-- auth that will only allow when on player's of lower roles
local function auth_lower_role(player, selected_player_name)
local player_highest = Roles.get_player_highest_role(player)
local action_player_highest = Roles.get_player_highest_role(selected_player_name)
if player_highest.index < action_player_highest.index then
return true
end
--- Accessors injected by the gui so the actions can read the selected player and set the selected action
local get_selected_player --- @type fun(player: LuaPlayer): LuaPlayer
local set_selected_action --- @type fun(player: LuaPlayer, action: string?)
local function set_accessors(player_getter, action_setter)
get_selected_player, set_selected_action = player_getter, action_setter
end
-- gets the action player and a coloured name for the action to be used on
local function get_action_player_name(player)
local selected_player_name = SelectedPlayer:get(player)
local selected_player = game.players[selected_player_name]
local function get_action_player(player)
local selected_player = get_selected_player(player) --[[@as LuaPlayer]]
local selected_player_color = format_player_name(selected_player)
return selected_player_name, selected_player_color
return selected_player, selected_player_color
end
-- teleports one player to another
@@ -63,12 +54,11 @@ end
--- Teleports the user to the action player
-- @element goto_player
local goto_player = new_button("utility/export", { "player-list.goto-player" })
local goto_player = new_button("utility/export", { "exp-gui_player-list.goto-player" })
:on_click(function(def, player, element)
local selected_player_name = get_action_player_name(player)
local selected_player = game.players[selected_player_name]
local selected_player = get_action_player(player)
if not player.character or not selected_player.character then
player.print({ "expcore-commands.reject-player-alive" }, Colors.orange_red)
player.print({ "exp-commands-parse.player-alive" }, Colors.orange_red)
else
teleport(player, selected_player)
end
@@ -76,12 +66,11 @@ local goto_player = new_button("utility/export", { "player-list.goto-player" })
--- Teleports the action player to the user
-- @element bring_player
local bring_player = new_button("utility/import", { "player-list.bring-player" })
local bring_player = new_button("utility/import", { "exp-gui_player-list.bring-player" })
:on_click(function(def, player, element)
local selected_player_name = get_action_player_name(player)
local selected_player = game.players[selected_player_name]
local selected_player = get_action_player(player)
if not player.character or not selected_player.character then
player.print({ "expcore-commands.reject-player-alive" }, Colors.orange_red)
player.print({ "exp-commands-parse.player-alive" }, Colors.orange_red)
else
teleport(selected_player, player)
end
@@ -89,118 +78,100 @@ local bring_player = new_button("utility/import", { "player-list.bring-player" }
--- Reports the action player, requires a reason to be given
-- @element report_player
local report_player = new_button("utility/spawn_flag", { "player-list.report-player" })
local report_player = new_button("utility/spawn_flag", { "exp-gui_player-list.report-player" })
:on_click(function(def, player, element)
local selected_player_name = get_action_player_name(player)
if Reports.is_reported(selected_player_name, player.name) then
local selected_player = get_action_player(player)
if Reports.is_reported(selected_player.name, player.name) then
player.print({ "exp-commands_report.already-reported" }, Colors.orange_red)
else
SelectedAction:set(player, "command/report")
set_selected_action(player, "exp_scenario.command.create_report")
end
end)
local function report_player_callback(player, reason)
local selected_player_name, selected_player_color = get_action_player_name(player)
local selected_player, selected_player_color = get_action_player(player)
local by_player_name_color = format_player_name(player)
game.print{ "exp-commands_reports.response", selected_player_color, reason }
Roles.print_to_roles_higher("Trainee", { "exp-commands_reports.response-admin", selected_player_color, by_player_name_color, reason })
Reports.report_player(selected_player_name, player.name, reason)
end
--- Gives the action player a warning, requires a reason
-- @element warn_player
local warn_player = new_button("utility/spawn_flag", { "player-list.warn-player" })
:on_click(function(def, player, element)
SelectedAction:set(player, "command/give-warning")
end)
local function warn_player_callback(player, reason)
local selected_player_name, selected_player_color = get_action_player_name(player)
local by_player_name_color = format_player_name(player)
game.print{ "exp-commands_warnings.create", selected_player_color, by_player_name_color, reason }
Warnings.add_warning(selected_player_name, player.name, reason)
local trainee = Roles.get_role_by_name("Trainee")
for _, role in ipairs(trainee and Roles.get_higher_roles(trainee) or {}) do
role:print{ "exp-commands_reports.response-admin", selected_player_color, by_player_name_color, reason }
end
Reports.report_player(selected_player.name, player.name, reason)
end
--- Jails the action player, requires a reason
-- @element jail_player
local jail_player = new_button("utility/multiplayer_waiting_icon", { "player-list.jail-player" })
local jail_player = new_button("utility/multiplayer_waiting_icon", { "exp-gui_player-list.jail-player" })
:on_click(function(def, player, element)
local selected_player_name, selected_player_color = get_action_player_name(player)
if Jail.is_jailed(selected_player_name) then
local selected_player, selected_player_color = get_action_player(player)
if Jail.is_jailed(selected_player) then
player.print({ "exp-commands_jail.already-jailed", selected_player_color }, Colors.orange_red)
else
SelectedAction:set(player, "command/jail")
set_selected_action(player, "exp_scenario.command.jail")
end
end)
local function jail_player_callback(player, reason)
local selected_player_name, selected_player_color = get_action_player_name(player)
local selected_player, selected_player_color = get_action_player(player)
local by_player_name_color = format_player_name(player)
game.print{ "exp-commands_jail.jailed", selected_player_color, by_player_name_color, reason }
Jail.jail_player(selected_player_name, player.name, reason)
Jail.jail_player(selected_player, player.name, reason)
end
--- Kicks the action player, requires a reason
-- @element kick_player
local kick_player = new_button("utility/warning_icon", { "player-list.kick-player" })
local kick_player = new_button("utility/warning_icon", { "exp-gui_player-list.kick-player" })
:on_click(function(def, player, element)
SelectedAction:set(player, "command/kick")
set_selected_action(player, "exp_scenario.gui.player_list.kick")
end)
local function kick_player_callback(player, reason)
local selected_player = get_action_player_name(player)
local selected_player = get_action_player(player)
game.kick_player(selected_player, reason)
end
--- Bans the action player, requires a reason
-- @element ban_player
local ban_player = new_button("utility/danger_icon", { "player-list.ban-player" })
local ban_player = new_button("utility/danger_icon", { "exp-gui_player-list.ban-player" })
:on_click(function(def, player, element)
SelectedAction:set(player, "command/ban")
set_selected_action(player, "exp_scenario.gui.player_list.ban")
end)
local function ban_player_callback(player, reason)
local selected_player = get_action_player_name(player)
local selected_player = get_action_player(player)
game.ban_player(selected_player, reason)
end
return {
set_datastores = set_datastores,
set_accessors = set_accessors,
buttons = {
["command/teleport"] = {
["exp_scenario.command.teleport"] = {
auth = function(player, selected_player)
return player.name ~= selected_player.name
end, -- cant teleport to your self
goto_player,
bring_player,
},
["command/report"] = {
["exp_scenario.command.create_report"] = {
auth = function(player, selected_player)
if player == selected_player then return false end
if not Roles.player_allowed(player, "command/give-warning") then
return not Roles.player_has_flag(selected_player, "report-immune")
end
end, -- can report any player that isn't immune and you aren't able to give warnings
return not Roles.player_has_permission(selected_player, "exp_scenario.bypass.reports")
end, -- can report any player that isn't immune
reason_callback = report_player_callback,
report_player,
},
["command/give-warning"] = {
auth = auth_lower_role, -- warn a lower user, replaces report
reason_callback = warn_player_callback,
warn_player,
},
["command/jail"] = {
auth = auth_lower_role,
["exp_scenario.command.jail"] = {
auth = Roles.player_outranks,
reason_callback = jail_player_callback,
jail_player,
},
["command/kick"] = {
auth = auth_lower_role,
["exp_scenario.gui.player_list.kick"] = {
auth = Roles.player_outranks,
reason_callback = kick_player_callback,
kick_player,
},
["command/ban"] = {
auth = auth_lower_role,
["exp_scenario.gui.player_list.ban"] = {
auth = Roles.player_outranks,
reason_callback = ban_player_callback,
ban_player,
},
+2 -2
View File
@@ -39,9 +39,9 @@ return {
allow_zoom_to_map = true, --- @setting allow_zoom_to_map false will disable the zoom to map feature
allow_remote_launch = true, --- @setting allow_remote_launch false removes the remote launch button for all players
remote_launch_admins_only = false, --- @setting remote_launch_admins_only true will remove the remote launch button for all non (game) admins
remote_launch_role_permission = "gui/rocket-info/remote_launch", --- @setting remote_launch_role_permission value used by custom permission system to allow or disallow the button
remote_launch_role_permission = "exp_scenario.gui.rocket_info.remote_launch", --- @setting remote_launch_role_permission value used by custom permission system to allow or disallow the button
allow_toggle_active = true, --- @setting allow_toggle_active false removes the remote toggle auto launch button for all players
toggle_active_admins_only = false, --- @setting toggle_active_admins_only true will remove the toggle auto launch button for all non (game) admins
toggle_active_role_permission = "gui/rocket-info/toggle-active", --- @setting toggle_active_role_permission value used by custom permission system to allow or disallow the button
toggle_active_role_permission = "exp_scenario.gui.rocket_info.toggle_active", --- @setting toggle_active_role_permission value used by custom permission system to allow or disallow the button
},
}
+4 -4
View File
@@ -3,11 +3,11 @@
return {
-- Adding tasks
allow_add_task = "all", --- @setting allow_add_task dictates who is allowed to add new tasks; values: all, admin, expcore.roles, none
expcore_roles_allow_add_task = "gui/task-list/add", --- @setting expcore_roles_allow_add_task if expcore.roles is used then this is the required permission
allow_add_task = "all", --- @setting allow_add_task dictates who is allowed to add new tasks; values: all, admin, exp_roles, none
exp_roles_allow_add_task = "exp_scenario.gui.task_list.add", --- @setting exp_roles_allow_add_task if exp_roles is used then this is the required permission
-- Editing tasks
allow_edit_task = "expcore.roles", --- @setting allow_edit_task dictates who is allowed to edit existing tasks; values: all, admin, expcore.roles, none
expcore_roles_allow_edit_task = "gui/task-list/edit", --- @setting expcore_roles_allow_edit_task if expcore.roles is used then this is the required permission
allow_edit_task = "exp_roles", --- @setting allow_edit_task dictates who is allowed to edit existing tasks; values: all, admin, exp_roles, none
exp_roles_allow_edit_task = "exp_scenario.gui.task_list.edit", --- @setting exp_roles_allow_edit_task if exp_roles is used then this is the required permission
user_can_edit_own_tasks = true, --- @settings if true then the user who made the task can edit it regardless of the allow_edit_task setting
}
+8 -8
View File
@@ -8,23 +8,23 @@ return {
default_icon = { type = "item", name = "discharge-defense-equipment" }, --- @setting default_icon the default icon that will be used for warps
-- Warp cooldowns
bypass_warp_cooldown = "expcore.roles", --- @setting bypass_warp_cooldown dictates who the warp cooldown is applied to; values: all, admin, expcore.roles, none
expcore_roles_bypass_warp_cooldown = "gui/warp-list/bypass-cooldown", --- @setting expcore_roles_bypass_warp_cooldown if expcore.roles is used then this is the required permission
bypass_warp_cooldown = "exp_roles", --- @setting bypass_warp_cooldown dictates who the warp cooldown is applied to; values: all, admin, exp_roles, none
exp_roles_bypass_warp_cooldown = "exp_scenario.gui.warp_list.bypass_cooldown", --- @setting exp_roles_bypass_warp_cooldown if exp_roles is used then this is the required permission
cooldown_duration = 60, --- @setting cooldown_duration the duration of the warp cooldown in seconds
-- Warp proximity
bypass_warp_proximity = "expcore.roles", --- @setting bypass_warp_proximity dictates who the warp proximity is applied to; values: all, admin, expcore.roles, none
expcore_roles_bypass_warp_proximity = "gui/warp-list/bypass-proximity", --- @setting expcore_roles_bypass_warp_proximity if expcore.roles is used then this is the required permission
bypass_warp_proximity = "exp_roles", --- @setting bypass_warp_proximity dictates who the warp proximity is applied to; values: all, admin, exp_roles, none
exp_roles_bypass_warp_proximity = "exp_scenario.gui.warp_list.bypass_proximity", --- @setting exp_roles_bypass_warp_proximity if exp_roles is used then this is the required permission
standard_proximity_radius = 4, --- @setting standard_proximity_radius the minimum distance a player is allowed to be to a warp in order to use it
spawn_proximity_radius = 20, --- @setting spawn_proximity_radius the minimum distance a player is allowed to be from they spawn point to use warps
-- Adding warps
allow_add_warp = "expcore.roles", --- @setting allow_add_warp dictates who is allowed to add warps; values: all, admin, expcore.roles, none
expcore_roles_allow_add_warp = "gui/warp-list/add", --- @setting expcore_roles_allow_add_warp if expcore.roles is used then this is the required permission
allow_add_warp = "exp_roles", --- @setting allow_add_warp dictates who is allowed to add warps; values: all, admin, exp_roles, none
exp_roles_allow_add_warp = "exp_scenario.gui.warp_list.add", --- @setting exp_roles_allow_add_warp if exp_roles is used then this is the required permission
-- Editing warps
allow_edit_warp = "expcore.roles", --- @setting allow_edit_warp dictates who is allowed to edit warps; values: all, admin, expcore.roles, none
expcore_roles_allow_edit_warp = "gui/warp-list/edit", --- @setting expcore_roles_allow_edit_warp if expcore.roles is used then this is the required permission
allow_edit_warp = "exp_roles", --- @setting allow_edit_warp dictates who is allowed to edit warps; values: all, admin, exp_roles, none
exp_roles_allow_edit_warp = "exp_scenario.gui.warp_list.edit", --- @setting exp_roles_allow_edit_warp if exp_roles is used then this is the required permission
user_can_edit_own_warps = false, --- @settings user_can_edit_own_warps if true then the user who made the warp can edit it regardless of the allow_edit_warp setting
-- Warp area generation
+1
View File
@@ -4,4 +4,5 @@
return {
fluid = true, --- @setting fluid When true, checks for for fluid pipes when removing miners
chest = true, --- @setting chest When true, checks for for chest when removing miners
beacon = true, --- @setting beacon When true, checks for for beacon when removing miners
}
+1 -1
View File
@@ -25,6 +25,6 @@ return {
},
},
},
ignore_permission = "bypass-nukeprotect", -- @setting ignore_permission The permission that nukeprotect will ignore
ignore_permission = "exp_scenario.bypass.nuke_protection", -- @setting ignore_permission The permission that nukeprotect will ignore
ignore_admins = true, -- @setting ignore_admins Ignore admins, true by default. Allows usage outside of the roles module
}
File diff suppressed because it is too large Load Diff
+3 -5
View File
@@ -1,19 +1,17 @@
return {
ignore_admins = true, --- @setting ignore_admins If admins are ignored by the protection filter
ignore_permission = "bypass-entity-protection", --- @setting ignore_permission Players with this permission will be ignored by the protection filter, leave nil if expcore.roles is not used
ignore_permission = "exp_scenario.bypass.entity_protection", --- @setting ignore_permission Players with this permission will be ignored by the protection filter, leave nil to protect against every player
repeat_count = 5, --- @setting repeat_count Number of protected entities that must be removed within repeat_lifetime in order to trigger repeated removal protection
repeat_lifetime = 3600 * 20, --- @setting repeat_lifetime The length of time, in ticks, that protected removals will be remembered for
refresh_rate = 3600 * 5, --- @setting refresh_rate How often the age of protected removals are checked against repeat_lifetime
always_protected_names = { --- @setting always_protected_names Names of entities which are always protected
},
always_protected_types = { --- @setting always_protected_types Types of entities which are always protected
"boiler", "generator", "offshore-pump", "power-switch", "reactor", "rocket-silo",
"boiler", "generator", "offshore-pump", "reactor", "heat-exchanger", "heat-pipe", "fusion-reactor", "fusion-generator", "power-switch", "rocket-silo",
},
always_trigger_repeat_names = { --- @setting always_trigger_repeat_names Names of entities which always trigger repeated removal protection
},
always_trigger_repeat_types = { --- @setting always_trigger_repeat_types Types of entities which always trigger repeated removal protection
"reactor", "rocket-silo",
"reactor", "fusion-reactor", "rocket-silo",
},
}
-8
View File
@@ -2,14 +2,6 @@
-- @config Repair
return {
disallow = { --- @setting disallow items in this list will never be repaired
["loader"] = true,
["fast-loader"] = true,
["express-loader"] = true,
["electric-energy-interface"] = true,
["infinity-chest"] = true,
},
max_range = 50, --- @setting max_range the max range that can be used with the repair command
allow_blueprint_repair = false, --- @setting allow_blueprint_repair when true will allow blueprints (things not destroyed by biters) to be build instantly using the repair command
allow_ghost_revive = true, --- @setting allow_ghost_revive when true will allow ghosts (things destroyed by biters) to be build instantly using the repair command
allow_heal_entities = true, --- @setting allow_heal_entities when true will heal entities to full health that are within range
+13 -11
View File
@@ -12,16 +12,6 @@ return {
-- this enable 20 more inventory for each mining productivity level up to 4
bonus_inventory = {
enabled = true,
log = {
["base"] = {
["name"] = "mining-productivity-4",
["level"] = 4
},
["space-age"] = {
["name"] = "mining-productivity-3",
["level"] = 3
},
},
res = {
-- Mining Productivity
["mining-productivity"] = true,
@@ -33,7 +23,19 @@ return {
rate = 5,
limit = 20,
},
file_name = "log/research.log",
file = {
name = "log/research.log",
final_res = {
["base"] = {
["name"] = "mining-productivity-4",
["level"] = 4
},
["space-age"] = {
["name"] = "mining-productivity-3",
["level"] = 3
}
}
},
milestone = {
["base"] = {
["automation"] = 600,
@@ -0,0 +1,11 @@
--- Config file for the server detail
-- @config Server
return {
community_name = "Explosive Gaming",
discord = "https://discord.explosivegaming.nl",
website = "https://www.explosivegaming.nl",
status = "https://status.explosivegaming.nl",
github = "https://github.com/explosivegaming/ExpCluster",
patreon = "https://www.patreon.com/ExpGaming"
}
+6 -6
View File
@@ -188,7 +188,7 @@ return {
amount = 4000,
size = { 26, 27 },
-- offset = {-64,-32}
offset = { -64, -64 },
offset = { x = -64, y = -64 },
},
{
enabled = false,
@@ -196,7 +196,7 @@ return {
amount = 4000,
size = { 26, 27 },
-- offset = {-64, 0}
offset = { 64, -64 },
offset = { x = 64, y = -64 },
},
{
enabled = false,
@@ -204,7 +204,7 @@ return {
amount = 4000,
size = { 22, 20 },
-- offset = {-64, 32}
offset = { -64, 64 },
offset = { x = -64, y = 64 },
},
{
enabled = false,
@@ -212,7 +212,7 @@ return {
amount = 4000,
size = { 22, 20 },
-- offset = {-64, -64}
offset = { 64, 64 },
offset = { x = 64, y = 64 },
},
{
enabled = false,
@@ -220,7 +220,7 @@ return {
amount = 4000,
size = { 22, 20 },
-- offset = {-64, -96}
offset = { 0, 64 },
offset = { x = 0, y = 64 },
},
},
},
@@ -233,7 +233,7 @@ return {
num_patches = 4,
amount = 4000000,
-- offset = {-80, -12},
offset = { -12, 64 },
offset = { x = -12, y = 64 },
-- offset_next = {0, 6}
offset_next = { 6, 0 },
},
+14
View File
@@ -35,6 +35,20 @@ return {
storage_output = 1, -- >0 allows for item teleportation (allowed_items only)
},
--- @class Vlayer.ItemProperties
--- @field starting_value number
--- @field required_area number
--- @field production number?
--- @field discharge number?
--- @field capacity number?
--- @field surface_area number?
--- @field fuel_value number?
--- @field power boolean?
--- @field modded boolean? Set when the item is registered from a modded equivalent
--- @field base_game_equivalent string?
--- @field multiplier number?
--- @type table<string, Vlayer.ItemProperties>
allowed_items = { --- @setting allowed_items List of all items allowed in vlayer storage and their properties
--[[
Allowed properties:
-37
View File
@@ -1,37 +0,0 @@
--- Config file for the warning system, this is very similar to reports but is for the use of moderators rather than normal users.
-- @config Warnings
return {
actions = { --- @setting actions what actions are taking at number of warnings
-- if a localized string is used then __1__ will by_player_name and __2__ will be the current warning count (auto inserted)
{ "warnings.received", "" },
{ "warnings.received", "" },
{ "warnings.received", { "warnings.pre-kick" } },
function(player, by_player_name, number_of_warnings)
local str = {
"You received a warning from ",
by_player_name,
". You have ",
number_of_warnings,
" warnings. You were kicked for having too many warnings; you may rejoin if you wish.",
}
game.kick_player(player, table.concat(str, "")) -- Does not support locale strings
-- game.kick_player(player, { "warnings.received", by_player_name, number_of_warnings, { "warnings.kick" } })
end,
{ "warnings.received", { "warnings.pre-pre-ban" } },
{ "warnings.received", { "warnings.pre-ban" } },
function(player, by_player_name, number_of_warnings)
local str = {
"You received a warning from ",
by_player_name,
". You have ",
number_of_warnings,
" warnings. You were banned for having too many warnings; visit https://www.explosivegaming.nl to request a ban appeal.",
}
game.kick_player(player, table.concat(str, "")) -- Does not support locale strings
-- game.ban_player(player, { "warnings.received", by_player_name, number_of_warnings, { "warnings.ban", { "links.website" } } })
end,
},
script_warning_cool_down = 30, --- @setting script_warning_cool_down time for a script warning (given by script) to be removed (in minutes)
script_warning_limit = 5, --- @setting script_warning_limit the number of script warnings (given by script) that are allowed before full warnings are given
}
+16 -2
View File
@@ -149,9 +149,23 @@ PlayerData.Statistics:combine('JoinCount')
local Storage = require("modules/exp_util/storage")
local DatastoreManager = {}
local Datastores = {}
local Datastore = {}
local Datastores = {} --- @type table<string, Datastore>
local Data = {}
--- @class Datastore
--- @field name string
--- @field value_name string
--- @field auto_save boolean
--- @field save_to_disk boolean
--- @field propagate_changes boolean
--- @field serializer function | false
--- @field parent Datastore | false
--- @field children table<string, Datastore>
--- @field metadata table
--- @field events table<string, function[]>
--- @field data table
local Datastore = {}
local copy = table.deep_copy
local trace = debug.traceback
local table_to_json = helpers.table_to_json
+10 -8
View File
@@ -14,7 +14,8 @@ end
]]
local ext, var
local ext --- @type table<string, any>
local var --- @type table<string, any>
local concat = table.concat
local External = {}
@@ -29,12 +30,13 @@ end
]]
function External.valid()
if storage.ext == nil then return false end
if ext == storage.ext and var == ext.var then
local stored = storage.ext
if stored == nil then return false end
if ext == stored and var == ext.var then
return var ~= nil
else
ext = storage.ext
var = ext.var
ext = stored
var = stored.var
return var ~= nil
end
end
@@ -48,7 +50,7 @@ local servers = External.get_servers()
]]
function External.get_servers()
assert(ext, "No external data was found, use External.valid() to ensure external data exists.")
return assert(ext.servers, "No server list was found, please ensure that the external service is running")
return (assert(ext.servers, "No server list was found, please ensure that the external service is running"))
end
--[[-- Gets a table of all the servers filtered by name, key is the server id, value is the server details
@@ -83,7 +85,7 @@ function External.get_current_server()
assert(ext, "No external data was found, use External.valid() to ensure external data exists.")
local servers = assert(ext.servers, "No server list was found, please ensure that the external service is running")
local server_id = assert(ext.current, "No current id was found, please ensure that the external service is running")
return assert(servers[server_id], "No details found for server with id: " .. tostring(server_id))
return (assert(servers[server_id], "No details found for server with id: " .. tostring(server_id)))
end
--[[-- Gets the details of the given server
@@ -97,7 +99,7 @@ local server = External.get_server_details('eu-01')
function External.get_server_details(server_id)
assert(ext, "No external data was found, use External.valid() to ensure external data exists.")
local servers = assert(ext.servers, "No server list was found, please ensure that the external service is running")
return assert(servers[server_id], "No details found for server with id: " .. tostring(server_id))
return (assert(servers[server_id], "No details found for server with id: " .. tostring(server_id)))
end
--[[-- Gets the status of the given server
@@ -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
+1 -1
View File
@@ -55,7 +55,7 @@ PlayerData:set_serializer(Datastore.name_serializer) -- use player name
--- Store and enum for the data saving preference
local DataSavingPreference = PlayerData:combine("DataSavingPreference")
local PreferenceEnum = { "All", "Statistics", "Settings", "Required" }
local PreferenceEnum = { "All", "Statistics", "Settings", "Required" } --- @type table<string | number, string | number>
for k, v in ipairs(PreferenceEnum) do PreferenceEnum[v] = k end
DataSavingPreference:set_default("All")
File diff suppressed because it is too large Load Diff
+256
View File
@@ -0,0 +1,256 @@
# addons
[info]
players-online=There are __1__ players online
total-map-time=This map has been on for __1__
discord=Join us on our discord at: __1__
website=Please visit our website at: __1__
status=Want to check if out servers are down? Visit: __1__
github=Want to help improve our server with some extra features? Help us at: __1__
patreon=Consider supporting our server at: __1__
custom-commands=We use custom commands, such as /tag and /me, use /chelp for more info.
read-readme=Make sure you have read the information gui (It can be found through the info mark on the top left)
softmod=We run a softmod on our servers. A softmod is a custom scenario that runs on this server, an example is the player list.
redmew=We don't talk about redmew here; they beat us to 1000 members ;-;
lhd=All trains must be LHD! This is a long standing rule on our servers, please respect this.
# data
[join-message]
description-add=Sets / Gets your custom join message.
description-remove=Removes you custom join message.
arg-message=Your custom join message.
greet=[color=0,1,0] Welcome to __1__ community server! If you like the server join our discord: __2__ [/color]
message-set=Your join message has been updated.
message-get=Your join message is: __1__
message-cleared=Your join message has been cleared.
[quickbar]
saved=Your quickbar filters have been saved.
[exp-settings]
Colour=Colour
Colour-tooltip=Your player colour
Colour-value-tooltip=Change by using /color
JoinMessage=Join Message
JoinMessage-tooltip=The message displayed when you join
JoinMessage-value-tooltip=Change by using /join-message
QuickbarFilters=Quickbar Filters
QuickbarFilters-tooltip=The filters on your quickbar
QuickbarFilters-value-tooltip=Change by using /save-quickbar
UsesAlt=Alt View
UsesAlt-tooltip=Whether you use alt view when you play
UsesAlt-value-tooltip=Change by pressing __CONTROL__show-info__
UsesServerUps=Server UPS
UsesServerUps-tooltip=Whether the current server UPS is shown
UsesServerUps-value-tooltip=Change by using /server-ups
Tag=Player Tag
Tag-tooltip=The tag shown after your name
Tag-value-tooltip=Change by using /tag
TagColor=Player Tag color
TagColor-tooltip=The color of the tag shown after your name
TagColor-value-tooltip=Change by using /tag-color
Bonus=Player Bonus
Bonus-tooltip=The bonus given to your character
Bonus-value-tooltip=Change by using /bonus
HasEnabledDecon=Quick Tree Decon
ToolbarState=Toolbox
ToolbarState-tooltip=The order and favourites in your toolbox
ToolbarState-value-tooltip=This value is calculated automatically when you leave the game
[exp-statistics]
MapsPlayed=Maps Played
MapsPlayed-tooltip=The number of unique maps you have played on
JoinCount=Join Count
JoinCount-tooltip=The number of times you have joined our servers
Playtime=Playtime
Playtime-tooltip=The amount of time you have spent on our servers
AfkTime=AFK Time
AfkTime-tooltip=The amount of time you have been AFK on our servers
ChatMessages=Messages
ChatMessages-tooltip=The number of messages you have sent in chat
CommandsUsed=Commands
CommandsUsed-tooltip=The number of commands you have used
RocketsLaunched=Rockets Launched
RocketsLaunched-tooltip=The number of rockets launched while you were online
ResearchCompleted=Research Completed
ResearchCompleted-tooltip=The number of research projects completed while you were online
MachinesBuilt=Machines Built
MachinesBuilt-tooltip=The number of machines you have built
MachinesRemoved=Machines Removed
MachinesRemoved-tooltip=The number of machines you have removed
TilesBuilt=Tiles Placed
TilesBuilt-tooltip=The number of tiles you have placed
TilesRemoved=Tiles Removed
TilesRemoved-tooltip=The number of tiles you have removed
TreesDestroyed=Trees Destroyed
TreesDestroyed-tooltip=The number of trees you have destroyed
OreMined=Ore Mined
OreMined-tooltip=The amount of ore you have mined
ItemsCrafted=Items Crafted
ItemsCrafted-tooltip=The number of items you have crafted
ItemsPickedUp=Items Picked Up
ItemsPickedUp-tooltip=The number of items you have picked up
Kills=Kills
Kills-tooltip=The number of biters and biter bases you have squished
Deaths=Deaths
Deaths-tooltip=The number of times you have died
DamageDealt=Damage Dealt
DamageDealt-tooltip=The amount of damage you have dealt to other forces
DistanceTravelled=Distance Travelled
DistanceTravelled-tooltip=The total distance in tiles that you have travelled
CapsulesUsed=Capsules Used
CapsulesUsed-tooltip=The number of capsules you have used
EntityRepaired=Machines Repaired
EntityRepaired-tooltip=The number of machines which you have repaired
DeconstructionPlannerUsed=Decon Planner Used
DeconstructionPlannerUsed-tooltip=The number of times you have used the deconstruction planner
MapTagsMade=Map Tags Created
MapTagsMade-tooltip=The number of map tags you have created
DamageDeathRatio=Damage Death Ratio
DamageDeathRatio-tooltip=Damage over Death
KillDeathRatio=Kill Death Ratio
KillDeathRatio-tooltip=Kill over Death
SessionTime=Session Time
SessionTime-tooltip=Session Time
BuildRatio=Build Ratio
BuildRatio-tooltip=Build over Remove
RocketPerHour=Rocket Per Hour
RocketPerHour-tooltip=Rocket Launched Over Play Time in Hour
TreeKillPerMinute=Tree Kill Per Minute
TreeKillPerMinute-tooltip=Tree Destroyed Per Minute
NetPlayTime=Net Play Time
NetPlayTime-tooltip=Net Play Time
AFKTimeRatio=AFK Time Ratio
AFKTimeRatio-tooltip=AFK Time over Play Time
Locale=Locale
Locale-tooltip=The language of player interface
# expcore
[expcore-data]
description-preference=Allows you to set/get your data saving preference.
description-data=Writes all your player data to a file on your computer.
arg-option=One of "All", "Statistics", "Settings", "Required"
set-preference=You data saving preference has been set to __1__. Existing data will not be effected until you rejoin.
get-preference=You data saving preference is __1__. Use /set-preference to change this. Use /save-data to get a local copy of your data.
get-data=Your player data has been writen to file, location: factorio/script_output/expgaming_player_data.json
data-failed=Your player data has failed to load. Any changes to your data will not be saved.
data-restore=Your player data has been restored and changes will now save when you leave.
preference=Saving Preference
preference-tooltip=Which categories will be saved when you leave the game
preference-value-tooltip=Change by using /set-preference
preference-All=All data will be saved
preference-Statistics=Only statistics, settings, and required data will be saved
preference-Settings=Only settings and required data will be saved
preference-Required=Only required data will be saved
# 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
[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.
set=Your bonus has been set to __1__.
get=Your bonus is __1__.
perm=You dont have enough permission to set more than __1__.
display-fmms=Mining
display-fmms-tooltip=Force manual mining speed
display-frs=Running
display-frs-tooltip=Force running speed
display-fcs=Crafting
display-fcs-tooltip=Force crafting speed
display-fisb=Inventory
display-fisb-tooltip=Force inventory slots bonus
display-fhb=Health
display-fhb-tooltip=Force health bonus
display-frdb=Reach
display-frdb-tooltip=Force reach distance bonus
display-fwrsm=Robot Speed
display-fwrsm-tooltip=Force worker robots speed modifier
display-fwrbm=Robot Battery
display-fwrbm-tooltip=Force worker robots battery modifier
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.
-31
View File
@@ -1,31 +0,0 @@
[links]
discord=https://discord.explosivegaming.nl
website=https://www.explosivegaming.nl
status=https://status.explosivegaming.nl
github=https://github.com/explosivegaming/ExpCluster
patreon=https://www.patreon.com/ExpGaming
[info]
players-online=There are __1__ players online
total-map-time=This map has been on for __1__
discord=Join us on our discord at: https://discord.explosivegaming.nl
website=Please visit our website at: https://www.explosivegaming.nl
status=Want to check if out servers are down? Visit: https://status.explosivegaming.nl
github=Want to help improve our server with some extra features? Help us at: https://github.com/explosivegaming/ExpCluster
patreon=Consider supporting our server at: https://www.patreon.com/ExpGaming
custom-commands=We use custom commands, such as /tag and /me, use /chelp for more info.
read-readme=Make sure you have read the information gui (It can be found through the info mark on the top left)
softmod=We run a softmod on our servers. A softmod is a custom scenario that runs on this server, an example is the player list.
redmew=We don't talk about redmew here; they beat us to 1000 members ;-;
lhd=All trains must be LHD! This is a long standing rule on our servers, please respect this.
[warnings]
received=You received a warning from __1__. You have __2__ warnings. __3__
pre-kick=This is your last warning before you are kicked.
kick=You were kicked for having too many warnings; you may rejoin if you wish.
pre-pre-ban=You are close to receiving a ban; successful ban appeals are unlikely.
pre-ban=This your LAST warning before you are BANNED! Successful ban appeals are unlikely.
ban=You were banned for having too many warnings; visit __1__ to request a ban appeal.
script-warning=You are receiving script warnings; if you recive too many you will receive a permanent warning (__1__/__2__)
script-warning-removed=A script warning has expired (__1__/__2__)
script-warning-limit=__1__ has received a permanent warning from the script.
-114
View File
@@ -1,114 +0,0 @@
[join-message]
description-add=Sets / Gets your custom join message.
description-remove=Removes you custom join message.
arg-message=Your custom join message.
greet=[color=0,1,0] Welcome to explosive gaming community server! If you like the server join our discord: __1__ [/color]
message-set=Your join message has been updated.
message-get=Your join message is: __1__
message-cleared=Your join message has been cleared.
[quickbar]
saved=Your quickbar filters have been saved.
[exp-required]
Warnings=Warnings
Warnings-tooltip=The total number of warnings you have received from staff
Warnings-value-tooltip=The total number of warnings you have received from staff
[exp-settings]
Colour=Colour
Colour-tooltip=Your player colour
Colour-value-tooltip=Change by using /color
JoinMessage=Join Message
JoinMessage-tooltip=The message displayed when you join
JoinMessage-value-tooltip=Change by using /join-message
QuickbarFilters=Quickbar Filters
QuickbarFilters-tooltip=The filters on your quickbar
QuickbarFilters-value-tooltip=Change by using /save-quickbar
UsesAlt=Alt View
UsesAlt-tooltip=Whether you use alt view when you play
UsesAlt-value-tooltip=Change by pressing __CONTROL__show-info__
UsesServerUps=Server UPS
UsesServerUps-tooltip=Whether the current server UPS is shown
UsesServerUps-value-tooltip=Change by using /server-ups
Tag=Player Tag
Tag-tooltip=The tag shown after your name
Tag-value-tooltip=Change by using /tag
TagColor=Player Tag color
TagColor-tooltip=The color of the tag shown after your name
TagColor-value-tooltip=Change by using /tag-color
Bonus=Player Bonus
Bonus-tooltip=The bonus given to your character
Bonus-value-tooltip=Change by using /bonus
HasEnabledDecon=Quick Tree Decon
ToolbarState=Toolbox
ToolbarState-tooltip=The order and favourites in your toolbox
ToolbarState-value-tooltip=This value is calculated automatically when you leave the game
[exp-statistics]
MapsPlayed=Maps Played
MapsPlayed-tooltip=The number of unique maps you have played on
JoinCount=Join Count
JoinCount-tooltip=The number of times you have joined our servers
Playtime=Playtime
Playtime-tooltip=The amount of time you have spent on our servers
AfkTime=AFK Time
AfkTime-tooltip=The amount of time you have been AFK on our servers
ChatMessages=Messages
ChatMessages-tooltip=The number of messages you have sent in chat
CommandsUsed=Commands
CommandsUsed-tooltip=The number of commands you have used
RocketsLaunched=Rockets Launched
RocketsLaunched-tooltip=The number of rockets launched while you were online
ResearchCompleted=Research Completed
ResearchCompleted-tooltip=The number of research projects completed while you were online
MachinesBuilt=Machines Built
MachinesBuilt-tooltip=The number of machines you have built
MachinesRemoved=Machines Removed
MachinesRemoved-tooltip=The number of machines you have removed
TilesBuilt=Tiles Placed
TilesBuilt-tooltip=The number of tiles you have placed
TilesRemoved=Tiles Removed
TilesRemoved-tooltip=The number of tiles you have removed
TreesDestroyed=Trees Destroyed
TreesDestroyed-tooltip=The number of trees you have destroyed
OreMined=Ore Mined
OreMined-tooltip=The amount of ore you have mined
ItemsCrafted=Items Crafted
ItemsCrafted-tooltip=The number of items you have crafted
ItemsPickedUp=Items Picked Up
ItemsPickedUp-tooltip=The number of items you have picked up
Kills=Kills
Kills-tooltip=The number of biters and biter bases you have squished
Deaths=Deaths
Deaths-tooltip=The number of times you have died
DamageDealt=Damage Dealt
DamageDealt-tooltip=The amount of damage you have dealt to other forces
DistanceTravelled=Distance Travelled
DistanceTravelled-tooltip=The total distance in tiles that you have travelled
CapsulesUsed=Capsules Used
CapsulesUsed-tooltip=The number of capsules you have used
EntityRepaired=Machines Repaired
EntityRepaired-tooltip=The number of machines which you have repaired
DeconstructionPlannerUsed=Decon Planner Used
DeconstructionPlannerUsed-tooltip=The number of times you have used the deconstruction planner
MapTagsMade=Map Tags Created
MapTagsMade-tooltip=The number of map tags you have created
DamageDeathRatio=Damage Death Ratio
DamageDeathRatio-tooltip=Damage over Death
KillDeathRatio=Kill Death Ratio
KillDeathRatio-tooltip=Kill over Death
SessionTime=Session Time
SessionTime-tooltip=Session Time
BuildRatio=Build Ratio
BuildRatio-tooltip=Build over Remove
RocketPerHour=Rocket Per Hour
RocketPerHour-tooltip=Rocket Launched Over Play Time in Hour
TreeKillPerMinute=Tree Kill Per Minute
TreeKillPerMinute-tooltip=Tree Destroyed Per Minute
NetPlayTime=Net Play Time
NetPlayTime-tooltip=Net Play Time
AFKTimeRatio=AFK Time Ratio
AFKTimeRatio-tooltip=AFK Time over Play Time
Locale=Locale
Locale-tooltip=The language of player interface
-24
View File
@@ -1,24 +0,0 @@
[expcore-roles]
error-log-format-flag=[ERROR] roleFlag/__1__ :: __2__
error-log-format-assign=[ERROR] rolePromote/__1__ :: __2__
game-message-assign=__1__ has been assigned to __2__ by __3__
game-message-unassign=__1__ has been unassigned from __2__ by __3__
reject-role=Invalid Role Name.
reject-player-role=Player has a higher role.
[expcore-data]
description-preference=Allows you to set/get your data saving preference.
description-data=Writes all your player data to a file on your computer.
arg-option=One of "All", "Statistics", "Settings", "Required"
set-preference=You data saving preference has been set to __1__. Existing data will not be effected until you rejoin.
get-preference=You data saving preference is __1__. Use /set-preference to change this. Use /save-data to get a local copy of your data.
get-data=Your player data has been writen to file, location: factorio/script_output/expgaming_player_data.json
data-failed=Your player data has failed to load. Any changes to your data will not be saved.
data-restore=Your player data has been restored and changes will now save when you leave.
preference=Saving Preference
preference-tooltip=Which categories will be saved when you leave the game
preference-value-tooltip=Change by using /set-preference
preference-All=All data will be saved
preference-Statistics=Only statistics, settings, and required data will be saved
preference-Settings=Only settings and required data will be saved
preference-Required=Only required data will be saved
-227
View File
@@ -1,227 +0,0 @@
[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
warn-player=Warn 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
[rocket-info]
main-tooltip=Rocket Info
launch-tooltip=Launch rocket
launch-tooltip-disabled=Launch rocket (not ready)
toggle-rocket-tooltip=Disable auto launch
toggle-rocket-tooltip-disabled=Enable auto launch
toggle-section-tooltip=Expand Section
toggle-section-collapse-tooltip=Collapse Section
section-caption-stats=Statistics
section-tooltip-stats=Statistics about how rockets are launched
section-caption-milestones=Milestones
section-tooltip-milestones=The times when milestones were met
section-caption-progress=Build Progress
section-tooltip-progress=The progress for your rocket silos
progress-no-silos=You have no rocket silos
data-caption-first-launch=First Launch
data-tooltip-first-launch=The time of the first launch
data-caption-last-launch=Last Launch
data-tooltip-last-launch=The time of the last launch
data-caption-fastest-launch=Fastest Launch
data-tooltip-fastest-launch=The time taken for the fastest launch
data-caption-total-rockets=Total Launched
data-tooltip-total-rockets=Total number of rockets launched by your force
value-tooltip-total-rockets=__1__% of all rockets on this map
data-caption-avg-launch=Average Time
data-tooltip-avg-launch=The average amount of time taken to launch a rocket
data-caption-avg-launch-n=Average Time __1__
data-tooltip-avg-launch-n=The average amount of time taken to launch the last __1__ rockets
data-caption-milestone-n=Milestone __1__
data-tooltip-milestone-n=Time taken to each __1__ rockets
data-caption-milestone-next=N/A
data-tooltip-milestone-next=Not yet achieved
progress-x-pos=X __1__
progress-y-pos=Y __1__
progress-label-tooltip=View on map
progress-launched=Launched
progress-caption=__1__%
progress-tooltip=This silo has launched __1__ rockets
launch-failed=Failed to launch rocket, please wait a few seconds and try again.
[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
[readme]
main-tooltip=Infomation
welcome-tab=Welcome
welcome-tooltip=Welcome to Explosive Gaming
welcome-general=Welcome to Explosive Gaming; we host many factorio servers. While you are here, we ask you to follow our rules. You can find these in the tab above. You can also find our custom commands and links to our other servers. This map has been online for __2__.\nPlease note that our servers reset periodically, the next reset is: __1__
welcome-roles=We run a custom role system to help protect the work of others. As a result you may not be able to use your deconstruction planner yet or drop item on the groud. Roles also give you access to some custom features such as adding tasks to our task list or making new warp points.\nYou have been assigned the roles: __1__
welcome-chat=Chatting can be difficult for new players because it’s different than other games! It’s very simple, the button you need to press is the “GRAVE/TILDE” key (which is located under the “ESC key”) - If you would like to change the key, go to your Controls tab in options.\nThe setting you need to change is “Toggle chat (and Lua console)” you currently have it set to "__CONTROL__toggle-console__"
rules-tab=Rules
rules-tooltip=Rules for our server
rules-general=By playing on our servers you accept the rules below. If you are supected of breaking a rule then you will be questioned by one of our moderators. If required you will be banned from our servers, appeals can be made by contacting an Administrator on our discord.
rules-1=Hacking / cheating / abusing bugs will not be tolerated.
rules-2=Any bugs or exploits found should be reported to our staff members.
rules-3=Be respectful to other players in chat, this includes any other forms of communication.
rules-4=Taking all items from a belt or logistics network is forbidden: sharing resources is mandatory.
rules-5=Spamming of chat, bots, unlimited chests, or concrete is not allowed.
rules-6=Before removing major parts of the factory tell other players why you are removing it.
rules-7=Do not cause unnecessary lag by placing/removing tiles with bots or using active provider chests, if think some thing might cause lag please ask staff first.
rules-8=Do not walk in random directions with no reason, this is to reduce map download times.
rules-9=Do not use speakers on global or with alerts without prior permission, no one wants to hear your music at full volume without knowing where the off switch is.
rules-10=Do not rotate belts, deactivate belts with wires, or cause production to stop without good reason, this goes for inserters and spliters as well.
rules-11=Do not make train roundabouts. Other loops such as RoRo stations are allowed.
rules-12=When using trains, use the same size other players have used, many players use 1-2-1, 2-4-2, or 3-8-3, meaning 1 engine 2 cargo 1 engine.
rules-13=Trains are Left Hand Drive (LHD) only, this means trains drive on the left side of the tracks.
rules-14=Do not beg for roles, advertise other servers, or link to harmful sites.
rules-15=Use common sense, report rule breakers, and Administrators have the final word.
commands-tab=Commands
commands-tooltip=Commands which you are able to use
commands-general=We have lots of custom commands which you are able to use. Below you can find a list of all the commands that you are allowed to use and what they do. If you need more information or want to search for a command you can use our /search-help command.
servers-tab=Servers
servers-tooltip=Links to our other servers and sites
servers-general=This is only one of our servers for factorio, we host many of others as well. Below you can find details about all the servers that we host as well as links to our external services such as discord or github.
servers-factorio=Factorio Servers
servers-1=S1 Public
servers-d1=This is our 48 hour reset server.
servers-2=S2 Off
servers-d2=This server is closed.
servers-3=S3 Weekly
servers-d3=This is our weekly reset server.
servers-4=S4 Monthly
servers-d4=This is our monthly reset server.
servers-5=S5 Modded
servers-d5=This is our modded server, see discord for details.
servers-6=S6 Donator
servers-d6=This is our donator only server, online when requested.
servers-7=S7 Event
servers-d7=This is our event server, we try to run events at least once per week.
servers-8=S8 T̷-̶s̶-̴:̷
servers-d8=N̵o̴ ̶o̷-̶e̵ ̴k̸n̷-̶w̵s̸ ̴w̷h̷a̶-̶ ̷h̴a̴p̷p̴e̷n̷s̸ ̷o̶n̴ ̷t̶h̴-̶s̶ ̷s̷e̶r̸v̸e̴r̷,̶ ̸i̸t̴ ̷m̶-̸g̴h̶t̷ ̸n̸-̶t̵ ̷e̴v̸e̸n̶t̷ ̵-̷x̴i̵s̶t̸.̸
servers-connect-Offline=Server is currently offline
servers-connect-Current=This is your current server
servers-connect-Version=Server is on a different version: __1__
servers-connect-Password=Server requires a password
servers-connect-Modded=Server requires mods to be downloaded
servers-connect-Online=Server is online
servers-external=External Links
servers-open-in-browser=Open in your browser
backers-tab=Backers
backers-tooltip=People who have helped make our server
backers-general=We would like to thank all our staff and backers who have helped our community grow. Our staff have helped to keep our servers safe from trolls and a fun place to play. Our backers have helped us to cover our running costs and provide a great community for us all to enjoy together.
backers-management=Administrators
backers-board=Board Members and Senior Backers
backers-staff=Staff Members
backers-backers=Sponsors and Supporters
backers-active=Active Players
data-tab=Data
data-tooltip=All of your stored player data
data-general=Our servers will save your player data so that we can sync it between servers. All of your data can be found below, if you wish to save a copy locally then use /save-data. If you want to change what data is saved then use /set-preference.
data-settings=Settings
data-statistics=Statistics
data-required=Required
data-misc=Miscellaneous
data-format=__1____2__
[bonus]
description=Get / Set the amount of bonus you receive.
arg-amount=Amount to set your bonus to, 0 will disable bonus.
set=Your bonus has been set to __1__.
get=Your bonus is __1__.
perm=You dont have enough permission to set more than __1__.
display-fmms=Mining
display-fmms-tooltip=Force manual mining speed
display-frs=Running
display-frs-tooltip=Force running speed
display-fcs=Crafting
display-fcs-tooltip=Force crafting speed
display-fisb=Inventory
display-fisb-tooltip=Force inventory slots bonus
display-fhb=Health
display-fhb-tooltip=Force health bonus
display-frdb=Reach
display-frdb-tooltip=Force reach distance bonus
display-fwrsm=Robot Speed
display-fwrsm-tooltip=Force worker robots speed modifier
display-fwrbm=Robot Battery
display-fwrbm-tooltip=Force worker robots battery modifier
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 GUI
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.
enter=Entered vlayer selection mode.
exit=Exited vlayer selection mode.
+264
View File
@@ -0,0 +1,264 @@
# addons
[info]
players-online=現在有 __1__ 人在線
total-map-time=地圖時間為 __1__
discord=加入社群: __1__
website=訪問網站: __1__
status=伺服器狀態: __1__
github=增加功能或回報錯誤: __1__
patreon=支持我們: __1__
custom-commands=這裹使用了自制指令,如 /tag 和 /me。可使用 /chelp 查看更多。
read-readme=確保你已閱讀相關資訊。按左上 i 圖標可再次查看
softmod=這裹用了自設情境,是一種軟裝模組。
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]
message-set=你的加入信息已更新。
message-get=你的加入信息為: __1__
message-cleared=你的加入信息已清除。
[quickbar]
saved=你的工具列已儲存。
[exp-settings]
Colour=顏色
Colour-tooltip=你的人物顏色
Colour-value-tooltip=使用 /color 來更換你的人物顏色
JoinMessage=加入信息
JoinMessage-tooltip=你加入時所顯示的信息
JoinMessage-value-tooltip=使用 /join-message 來更換加入信息
QuickbarFilters=快捷欄
QuickbarFilters-tooltip=快捷欄的選項
QuickbarFilters-value-tooltip=使用 /save-quickbar 來更換快捷欄的選項
UsesAlt=細節模式
UsesAlt-tooltip=你加入時是否使用細節模式
UsesAlt-value-tooltip=按下 __CONTROL__show-info__ 來更改
UsesServerUps=服務器更新數 (SUPS)
UsesServerUps-tooltip=現在是否顯示服務器更新數 (SUPS)
UsesServerUps-value-tooltip=使用 /server-ups 來更換顯示
Tag=你的人物標籤
Tag-tooltip=你的人物標籤
Tag-value-tooltip=使用 /tag 來更換你的人物標籤
TagColor=人物標籤顏色
TagColor-tooltip=你的人物標籤顏色
TagColor-value-tooltip=使用 /tag-color 來更換你的人物標籤顏色
Bonus=人物附加屬性
Bonus-tooltip=你的人物附加屬性
Bonus-value-tooltip=使用 /bonus 來更改
HasEnabledDecon=快速拆除樹木
ToolbarState=工具箱
ToolbarState-tooltip=次序和喜愛項目
ToolbarState-value-tooltip=此值在離開時自動計算
[exp-statistics]
MapsPlayed=地圖遊玩次數
MapsPlayed-tooltip=你在本服務器遊玩過的地圖數
JoinCount=登入次數
JoinCount-tooltip=你在本服務器遊玩的次數
Playtime=遊玩時間
Playtime-tooltip=你在本服務器遊玩的時間
AfkTime=掛機時間
AfkTime-tooltip=你在本服務器掛機的時間
ChatMessages=傳送信息次數
ChatMessages-tooltip=你在本服務器發送信息的次數
CommandsUsed=使用指令次數
CommandsUsed-tooltip=你在本服務器使用指令的次數
RocketsLaunched=火箭發射次數
RocketsLaunched-tooltip=你在本服務器在線時所發射過火箭的次數
ResearchCompleted=研究次數
ResearchCompleted-tooltip=你在本服務器在線時完成研究的次數
MachinesBuilt=機器建造次數
MachinesBuilt-tooltip=你在本服務器建造過機器的次數
MachinesRemoved=機器拆除次數
MachinesRemoved-tooltip=你在本服務器拆除過機器的次數
TilesBuilt=地磚建造次數
TilesBuilt-tooltip=你在本服務器建造過地磚的次數
TilesRemoved=地磚拆除次數
TilesRemoved-tooltip=你在本服務器拆除過地磚的次數
TreesDestroyed=樹木拆除次數
TreesDestroyed-tooltip=你在本服務器拆除過樹木的次數
OreMined=挖礦次數
OreMined-tooltip=你在本服務器挖掘礦的次數
ItemsCrafted=物品合成數
ItemsCrafted-tooltip=你在本服務器合成過物品的次數
ItemsPickedUp=物品拾起數
ItemsPickedUp-tooltip=你在本服務器起過物品的次數
Kills=撃殺數
Kills-tooltip=你在本服務器殺過的敵人數量
Deaths=死亡次數
Deaths-tooltip=你在本服務器死過的數量
DamageDealt=傷害量
DamageDealt-tooltip=你在本伺服器對敵人所造成的傷害量
DistanceTravelled=移動距離
DistanceTravelled-tooltip=你在本服務器的總移動距離
CapsulesUsed=膠囊使用次數
CapsulesUsed-tooltip=你在本服務器使用過膠囊的次數
EntityRepaired=機器維修次數
EntityRepaired-tooltip=你在本服務器維修過機器的次數
DeconstructionPlannerUsed=拆除規劃器使用次數
DeconstructionPlannerUsed-tooltip=你在本服務器使用過拆除規劃器的次數
MapTagsMade=地圖標籤數量
MapTagsMade-tooltip=你在本服務器放過的地圖標籤數量
DamageDeathRatio=傷害死亡比
DamageDeathRatio-tooltip=傷害除以死亡
KillDeathRatio=撃殺死亡比
KillDeathRatio-tooltip=撃殺除以死亡
SessionTime=平均遊玩時間
SessionTime-tooltip=平均遊玩時間
BuildRatio=建築比
BuildRatio-tooltip=建築次數除以拆除次數
RocketPerHour=每小時火箭發射次數
RocketPerHour-tooltip=火箭發射次數除以遊玩時間
TreeKillPerMinute=每分鐘樹木拆除數
TreeKillPerMinute-tooltip=樹木拆除數除以遊玩時間
NetPlayTime=淨遊玩時間
NetPlayTime-tooltip=遊玩時間減掛機時間
AFKTimeRatio=掛機時間比
AFKTimeRatio-tooltip=掛機時間除以遊玩時間
Locale=語言
Locale-tooltip=用戶介面語言
# expcore
[expcore-data]
description-preference=允許寫入資料。
description-data=把用戶資料寫入電腦。
arg-option="All", "Statistics", "Settings", "Required" 之中
set-preference=你的喜好已設定為 __1__ 。在你重新加入前將不會有任何改動。
get-preference=你的喜好為 __1__ 。使用 /set-preference 來更改,使用 /save-data 來獲得一份資料副本。
get-data=你的個人資料已寫入: factorio/script_output/expgaming_player_data.json
data-failed=無法載入你的個人資料,任何改動將不會保存。
data-restore=已載入你的個人資料,所有改動將會被保存。
preference=儲存喜好
preference-tooltip=那種資料將在退出時保存
preference-value-tooltip=使用 /set-preference 來更換儲存喜好
preference-All=所有資料都會被儲存
preference-Statistics=只有數據, 設定和資料會被儲存
preference-Settings=只有設定和資料會被儲存
preference-Required=只有需要的資料會被儲存
# gui
[player-list]
main-tooltip=用戶名單
open-action-bar=選項
close-action-bar=關閉選項
reason-confirm=確認原因
reason-entry=輸入原因
goto-player=傳送到用戶
bring-player=傳送用戶到自己
report-player=舉報用戶
jail-player=監禁用戶
kick-player=踢除用戶
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 來停用。
set=你的 Bonus 已設為 __1__。
get=你的 Bonus 為 __1__。
perm=你沒有足夠權限設多過 __1__。
display-fmms=挖掘速度
display-fmms-tooltip=勢力挖掘速度
display-frs=跑步速度
display-frs-tooltip=勢力跑步速度
display-fcs=合成速度
display-fcs-tooltip=勢力合成速度
display-fisb=儲存位
display-fisb-tooltip=勢力儲存位
display-fhb=生命
display-fhb-tooltip=勢力生命
display-frdb=到達距離
display-frdb-tooltip=勢力到達距離
display-fwrsm=機械人速度
display-fwrsm-tooltip=勢力機械人速度
display-fwrbm=機械人電力
display-fwrbm-tooltip=勢力機械人電力
display-fwrsb=械人儲存
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選上來設定喜好
reset=重設
toggle=啟用喜好
move-up=向上
move-down=向下
-31
View File
@@ -1,31 +0,0 @@
[links]
discord=https://discord.explosivegaming.nl
website=https://www.explosivegaming.nl
status=https://status.explosivegaming.nl
github=https://github.com/explosivegaming/ExpCluster
patreon=https://www.patreon.com/ExpGaming
[info]
players-online=現在有 __1__ 人在線
total-map-time=地圖時間為 __1__
discord=加入社群: https://discord.explosivegaming.nl
website=訪問網站: https://www.explosivegaming.nl
status=伺服器狀態: https://status.explosivegaming.nl
github=增加功能或回報錯誤: https://github.com/explosivegaming/ExpCluster
patreon=支持我們: https://www.patreon.com/ExpGaming
custom-commands=這裹使用了自制指令,如 /tag 和 /me。可使用 /chelp 查看更多。
read-readme=確保你已閱讀相關資訊。按左上 i 圖標可再次查看。
softmod=這裹用了自設情境,是一種軟裝模組。
redmew=
lhd=列車必須是左則通行。這是本服務器長久以來的規則。
[warnings]
received=你已被 __1__ 警告了,現在共有 __2__ 個警告。 __3__
pre-kick=這是在被踢離之前的最後警告。
kick=你已因為被警告太多次而被請離,不過你之後還是可以回來的。
pre-pre-ban=按照你的行為,你有可能會被封禁。
pre-ban=這是在被封禁之前的最後警告。
ban=你已因為被警告太多次而被封禁; 可以到 __1__ 申訴.
script-warning=這是系統發出的自動警告 (__1__/__2__)
script-warning-removed=系統發出的自動警告已失效 (__1__/__2__)
script-warning-limit=__1__ 已被系統發出了一則自動警告。
-114
View File
@@ -1,114 +0,0 @@
[join-message]
description-add=設定 / 取得你的加入信息。
description-remove=移除你的加入信息。
arg-message=你的加入信息。
greet=[color=0,1,0] 歡迎來到 EXP 的服務器! 若果你喜歡本服務器可加入 DISCORD: __1__ [/color]
message-set=你的加入信息已更新。
message-get=你的加入信息為: __1__
message-cleared=你的加入信息已清除。
[quickbar]
saved=你的工具列已儲存。
[exp-required]
Warnings=警告
Warnings-tooltip=你所有收到的警告
Warnings-value-tooltip=你所有收到的警告
[exp-settings]
Colour=顏色
Colour-tooltip=你的人物顏色
Colour-value-tooltip=使用 /color 來更換你的人物顏色
JoinMessage=加入信息
JoinMessage-tooltip=你加入時所顯示的信息
JoinMessage-value-tooltip=使用 /join-message 來更換加入信息
QuickbarFilters=快捷欄
QuickbarFilters-tooltip=快捷欄的選項
QuickbarFilters-value-tooltip=使用 /save-quickbar 來更換快捷欄的選項
UsesAlt=細節模式
UsesAlt-tooltip=你加入時是否使用細節模式
UsesAlt-value-tooltip=按下 __CONTROL__show-info__ 來更改
UsesServerUps=服務器更新數 (SUPS)
UsesServerUps-tooltip=現在是否顯示服務器更新數 (SUPS)
UsesServerUps-value-tooltip=使用 /server-ups 來更換顯示
Tag=你的人物標籤
Tag-tooltip=你的人物標籤
Tag-value-tooltip=使用 /tag 來更換你的人物標籤
TagColor=人物標籤顏色
TagColor-tooltip=你的人物標籤顏色
TagColor-value-tooltip=使用 /tag-color 來更換你的人物標籤顏色
Bonus=人物附加屬性
Bonus-tooltip=你的人物附加屬性
Bonus-value-tooltip=使用 /bonus 來更改
HasEnabledDecon=快速拆除樹木
ToolbarState=工具箱
ToolbarState-tooltip=次序和喜愛項目
ToolbarState-value-tooltip=此值在離開時自動計算
[exp-statistics]
MapsPlayed=地圖遊玩次數
MapsPlayed-tooltip=你在本服務器遊玩過的地圖數
JoinCount=登入次數
JoinCount-tooltip=你在本服務器遊玩的次數
Playtime=遊玩時間
Playtime-tooltip=你在本服務器遊玩的時間
AfkTime=掛機時間
AfkTime-tooltip=你在本服務器掛機的時間
ChatMessages=傳送信息次數
ChatMessages-tooltip=你在本服務器發送信息的次數
CommandsUsed=使用指令次數
CommandsUsed-tooltip=你在本服務器使用指令的次數
RocketsLaunched=火箭發射次數
RocketsLaunched-tooltip=你在本服務器在線時所發射過火箭的次數
ResearchCompleted=研究次數
ResearchCompleted-tooltip=你在本服務器在線時完成研究的次數
MachinesBuilt=機器建造次數
MachinesBuilt-tooltip=你在本服務器建造過機器的次數
MachinesRemoved=機器拆除次數
MachinesRemoved-tooltip=你在本服務器拆除過機器的次數
TilesBuilt=地磚建造次數
TilesBuilt-tooltip=你在本服務器建造過地磚的次數
TilesRemoved=地磚拆除次數
TilesRemoved-tooltip=你在本服務器拆除過地磚的次數
TreesDestroyed=樹木拆除次數
TreesDestroyed-tooltip=你在本服務器拆除過樹木的次數
OreMined=挖礦次數
OreMined-tooltip=你在本服務器挖掘礦的次數
ItemsCrafted=物品合成數
ItemsCrafted-tooltip=你在本服務器合成過物品的次數
ItemsPickedUp=物品拾起數
ItemsPickedUp-tooltip=你在本服務器起過物品的次數
Kills=撃殺數
Kills-tooltip=你在本服務器殺過的敵人數量
Deaths=死亡次數
Deaths-tooltip=你在本服務器死過的數量
DamageDealt=傷害量
DamageDealt-tooltip=你在本伺服器對敵人所造成的傷害量
DistanceTravelled=移動距離
DistanceTravelled-tooltip=你在本服務器的總移動距離
CapsulesUsed=膠囊使用次數
CapsulesUsed-tooltip=你在本服務器使用過膠囊的次數
EntityRepaired=機器維修次數
EntityRepaired-tooltip=你在本服務器維修過機器的次數
DeconstructionPlannerUsed=拆除規劃器使用次數
DeconstructionPlannerUsed-tooltip=你在本服務器使用過拆除規劃器的次數
MapTagsMade=地圖標籤數量
MapTagsMade-tooltip=你在本服務器放過的地圖標籤數量
DamageDeathRatio=傷害死亡比
DamageDeathRatio-tooltip=傷害除以死亡
KillDeathRatio=撃殺死亡比
KillDeathRatio-tooltip=撃殺除以死亡
SessionTime=平均遊玩時間
SessionTime-tooltip=平均遊玩時間
BuildRatio=建築比
BuildRatio-tooltip=建築次數除以拆除次數
RocketPerHour=每小時火箭發射次數
RocketPerHour-tooltip=火箭發射次數除以遊玩時間
TreeKillPerMinute=每分鐘樹木拆除數
TreeKillPerMinute-tooltip=樹木拆除數除以遊玩時間
NetPlayTime=淨遊玩時間
NetPlayTime-tooltip=遊玩時間減掛機時間
AFKTimeRatio=掛機時間比
AFKTimeRatio-tooltip=掛機時間除以遊玩時間
Locale=語言
Locale-tooltip=用戶介面語言
@@ -1,24 +0,0 @@
[expcore-roles]
error-log-format-flag=[ERROR] roleFlag/__1__ :: __2__
error-log-format-assign=[ERROR] rolePromote/__1__ :: __2__
game-message-assign=__1__ 已被 __3__ 指派到身分組 __2__
game-message-unassign=__1__ 已被 __3__ 取消指派到身分組 __2__
reject-role=無效的身分組。
reject-player-role=用戶已有更高的身分組。
[expcore-data]
description-preference=允許寫入資料。
description-data=把用戶資料寫入電腦
arg-option="All", "Statistics", "Settings", "Required" 之中
set-preference=你的喜好已設定為 __1__ 。在你重新加入前將不會有任何改動。
get-preference=你的喜好為 __1__ 。使用 /set-preference 來更改,使用 /save-data 來獲得一份資料副本。
get-data=你的個人資料已寫入: factorio/script_output/expgaming_player_data.json
data-failed=無法載入你的個人資料,任何改動將不會保存。
data-restore=已載入你的個人資料,所有改動將會被保存。
preference=儲存喜好
preference-tooltip=那種資料將在退出時保存
preference-value-tooltip=使用 /set-preference 來更換儲存喜好
preference-All=所有資料都會被儲存
preference-Statistics=只有數據, 設定和資料會被儲存
preference-Settings=只有設定和資料會被儲存
preference-Required=只有需要的資料會被儲存
-235
View File
@@ -1,235 +0,0 @@
[player-list]
main-tooltip=用戶名單
open-action-bar=選項
close-action-bar=關閉選項
reason-confirm=確認原因
reason-entry=輸入原因
goto-player=傳送到用戶
bring-player=傳送用戶到自己
report-player=舉報用戶
warn-player=警告用戶
jail-player=監禁用戶
kick-player=踢除用戶
ban-player=封禁用戶
afk-time=__1__% 地圖時間\n上次移動: __2__
open-map=__1__ __2__\n__3__\n按下打開地圖
[rocket-info]
main-tooltip=火箭資訊
launch-tooltip=發射火箭
launch-tooltip-disabled=發射火箭 (未準備)
toggle-rocket-tooltip=取消自動發射
toggle-rocket-tooltip-disabled=啟用自動發射
toggle-section-tooltip=擴張欄
toggle-section-collapse-tooltip=收縮欄
section-caption-stats=數據
section-tooltip-stats=火箭發射數據
section-caption-milestones=里程碑
section-tooltip-milestones=里程碑達成次數
section-caption-progress=建造程度
section-tooltip-progress=火箭建造程度
progress-no-silos=你沒有火箭發射井
data-caption-first-launch=首次發射
data-tooltip-first-launch=首次發射時間
data-caption-last-launch=最後發射
data-tooltip-last-launch=最後發射時間
data-caption-fastest-launch=最快發射
data-tooltip-fastest-launch=最快發射時間
data-caption-total-rockets=總發射次數
data-tooltip-total-rockets=我方總發射次數
value-tooltip-total-rockets=__1__% 的火箭發射都在本地圖上。
data-caption-avg-launch=平均時間
data-tooltip-avg-launch=火箭發射平均時間
data-caption-avg-launch-n=平均時間 __1__
data-tooltip-avg-launch-n=__1__ 次火箭發射的平均時間
data-caption-milestone-n=里程碑 __1__
data-tooltip-milestone-n=__1__ 次火箭發射的時間
data-caption-milestone-next=N/A
data-tooltip-milestone-next=未達成
progress-x-pos=X __1__
progress-y-pos=Y __1__
progress-label-tooltip=在地圖上看
progress-launched=已發射
progress-caption=__1__ %
progress-tooltip=該火箭發射井發射了 __1__ 次
launch-failed=火箭發射失敗, 請過一會再試。
[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=修改傳送陣圖案
[readme]
main-tooltip=資訊
welcome-tab=歡迎
welcome-tooltip=歡迎來到 Explosive Gaming
welcome-general=歡迎來到 Explosive Gaming; 你在這裏的時候要遵守規則。 你可以在左上方的介面找到更多資訊。 地圖時間 __2__。\n下次地圖重設: __1__
welcome-roles=我們有自訂的身份組來保護其他用戶。 所以你有機會不能使用拆除規劃器或掉東西。 身份組給予你各種權限\n你有以下身份組: __1__
welcome-chat=你可以使用 「重音符/抑音符」 鍵 (ESC 鍵 下方) - 你也可以在選項中修改按鍵。\n你目前設定為 「__CONTROL__toggle-console__」
rules-tab=規則
rules-tooltip=服務器規則
rules-general=你在本服務器遊玩即等同同意以下規則。管理員有機會就相關事項提問。你可能會因為違反規則而被封禁,你可以在 Discord 中提出反對。
rules-1=開掛或濫用漏洞將不會被容忍。
rules-2=當你找到漏洞可以通知職員。
rules-3=請在聊天室中尊重其他人。
rules-4=請不要把所有物品全部取走。
rules-5=請不要濫發信息,大量鋪設地磚,或不為箱子設置上限,浪費資源。
rules-6=當你想大量拆除原有設施前請通知其他用戶。
rules-7=請不要無故使用機器人來大量鋪設或拆除地磚,濫用主動出貨箱。
rules-8=請不要隨意移動來開大量地圖,這有助減少地圖下載時間。
rules-9=請不要無故使用全圖喇叭。
rules-10=請不要無故轉動傳送帶方向,停用傳送帶,或停止生產。
rules-11=請不要用迴旋處。 火車站尾返程則是例外。
rules-12=請跟其他人使用相同的火車組成。常用的有 1-2-1,2-4-2,或 3-8-3,即1個動車組拖2個貨車組再拖不同方向的1個動車組。
rules-13=火車必須是左上右下 (LHD)。
rules-14=禁止要求身份組,發廣告,發惡意連結。
rules-15=使用常識, 舉報違規人員,管理員有最終決定權。
commands-tab=指令
commands-tooltip=你可用的指令
commands-general=這裏有自訂的指令。你可以在下邊找到你可用的指令及其功能介紹。你可以用 /search-help 來取得更多資訊。
servers-tab=伺服器
servers-tooltip=到其他伺服器及網站的連結
servers-general=這是其中一個伺服器。你可以在下方找到其他同樣是由我們運行的伺服器資料。
servers-factorio=異星工廠伺服器
servers-1=S1 公共
servers-d1=這個服務器每兩日重設。
servers-2=S2 -
servers-d2=這個服務器沒在營運。
servers-3=S3 周
servers-d3=這個服務器每週重設。
servers-4=S4 月
servers-d4=這個服務器每月重設。
servers-5=S5 模組
servers-d5=這個服務器運行模組,可在 Discord 中得到更多資訊。
servers-6=S6 Donator
servers-d6=這個服務器為支持者運行,可在 Discord 中得到更多資訊。
servers-7=S7 項目
servers-d7=這個服務器只在有項目期間運行。
servers-8=S8 T̷-̶s̶-̴:̷
servers-d8=N̵o̴ ̶o̷-̶e̵ ̴k̸n̷-̶w̵s̸ ̴w̷h̷a̶-̶ ̷h̴a̴p̷p̴e̷n̷s̸ ̷o̶n̴ ̷t̶h̴-̶s̶ ̷s̷e̶r̸v̸e̴r̷,̶ ̸i̸t̴ ̷m̶-̸g̴h̶t̷ ̸n̸-̶t̵ ̷e̴v̸e̸n̶t̷ ̵-̷x̴i̵s̶t̸。̸
servers-connect-Offline=該服務器目前沒有在運行
servers-connect-Current=你目前所在的伺服器
servers-connect-Version=該服務器運行不同的遊戲版本: __1__
servers-connect-Password=該服務器需要密碼
servers-connect-Modded=該服務器需要下載模組
servers-connect-Online=該服務器在線
servers-external=外部連結
servers-open-in-browser=可在你的瀏覽器中打開
backers-tab=支持者
backers-tooltip=支持者
backers-general=他們為服務器運行提供了不少幫助。
backers-management=系統管理員
backers-board=議員
backers-staff=職員
backers-backers=支持者
backers-active=活躍玩家
data-tab=資料
data-tooltip=所有已存資料
data-general=資料會自動存到各個服務器中。 你也可以用 /save-data 存一份副本。 也可用 /set-preference 來更改喜好。
data-settings=設定
data-statistics=數據
data-required=需要
data-misc=雜項
data-format=__1____2__
[bonus]
description=取得 / 設定 Bonus 量。
arg-amount=Bonus 數量, 0 來停用。
set=你的 Bonus 已設為 __1__。
get=你的 Bonus 為 __1__。
perm=你沒有足夠權限設多過 __1__。
display-fmms=挖掘速度
display-fmms-tooltip=勢力挖掘速度
display-frs=跑步速度
display-frs-tooltip=勢力跑步速度
display-fcs=合成速度
display-fcs-tooltip=勢力合成速度
display-fisb=儲存位
display-fisb-tooltip=勢力儲存位
display-fhb=生命
display-fhb-tooltip=勢力生命
display-frdb=到達距離
display-frdb-tooltip=勢力到達距離
display-fwrsm=機械人速度
display-fwrsm-tooltip=勢力機械人速度
display-fwrbm=機械人電力
display-fwrbm-tooltip=勢力機械人電力
display-fwrsb=械人儲存
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 功能。
enter=現在進入 vlayer 區域選擇
exit=已進入 vlayer 區域選擇
[toolbar]
main-caption=工具箱
main-tooltip=工具箱設定\n選上來設定喜好
reset=重設
toggle=啟用喜好
move-up=向上
move-down=向下
+264
View File
@@ -0,0 +1,264 @@
# addons
[info]
players-online=現在有 __1__ 人在線
total-map-time=地圖時間為 __1__
discord=加入社群: __1__
website=訪問網站: __1__
status=伺服器狀態: __1__
github=增加功能或回報錯誤: __1__
patreon=支持我們: __1__
custom-commands=這裹使用了自制指令,如 /tag 和 /me。可使用 /chelp 查看更多。
read-readme=確保你已閱讀相關資訊。按左上 i 圖標可再次查看
softmod=這裹用了自設情境,是一種軟裝模組。
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]
message-set=你的加入信息已更新。
message-get=你的加入信息為: __1__
message-cleared=你的加入信息已清除。
[quickbar]
saved=你的工具列已儲存。
[exp-settings]
Colour=顏色
Colour-tooltip=你的人物顏色
Colour-value-tooltip=使用 /color 來更換你的人物顏色
JoinMessage=加入信息
JoinMessage-tooltip=你加入時所顯示的信息
JoinMessage-value-tooltip=使用 /join-message 來更換加入信息
QuickbarFilters=快捷欄
QuickbarFilters-tooltip=快捷欄的選項
QuickbarFilters-value-tooltip=使用 /save-quickbar 來更換快捷欄的選項
UsesAlt=細節模式
UsesAlt-tooltip=你加入時是否使用細節模式
UsesAlt-value-tooltip=按下 __CONTROL__show-info__ 來更改
UsesServerUps=服務器更新數 (SUPS)
UsesServerUps-tooltip=現在是否顯示服務器更新數 (SUPS)
UsesServerUps-value-tooltip=使用 /server-ups 來更換顯示
Tag=你的人物標籤
Tag-tooltip=你的人物標籤
Tag-value-tooltip=使用 /tag 來更換你的人物標籤
TagColor=人物標籤顏色
TagColor-tooltip=你的人物標籤顏色
TagColor-value-tooltip=使用 /tag-color 來更換你的人物標籤顏色
Bonus=人物附加屬性
Bonus-tooltip=你的人物附加屬性
Bonus-value-tooltip=使用 /bonus 來更改
HasEnabledDecon=快速拆除樹木
ToolbarState=工具箱
ToolbarState-tooltip=次序和喜愛項目
ToolbarState-value-tooltip=此值在離開時自動計算
[exp-statistics]
MapsPlayed=地圖遊玩次數
MapsPlayed-tooltip=你在本服務器遊玩過的地圖數
JoinCount=登入次數
JoinCount-tooltip=你在本服務器遊玩的次數
Playtime=遊玩時間
Playtime-tooltip=你在本服務器遊玩的時間
AfkTime=掛機時間
AfkTime-tooltip=你在本服務器掛機的時間
ChatMessages=傳送信息次數
ChatMessages-tooltip=你在本服務器發送信息的次數
CommandsUsed=使用指令次數
CommandsUsed-tooltip=你在本服務器使用指令的次數
RocketsLaunched=火箭發射次數
RocketsLaunched-tooltip=你在本服務器在線時所發射過火箭的次數
ResearchCompleted=研究次數
ResearchCompleted-tooltip=你在本服務器在線時完成研究的次數
MachinesBuilt=機器建造次數
MachinesBuilt-tooltip=你在本服務器建造過機器的次數
MachinesRemoved=機器拆除次數
MachinesRemoved-tooltip=你在本服務器拆除過機器的次數
TilesBuilt=地磚建造次數
TilesBuilt-tooltip=你在本服務器建造過地磚的次數
TilesRemoved=地磚拆除次數
TilesRemoved-tooltip=你在本服務器拆除過地磚的次數
TreesDestroyed=樹木拆除次數
TreesDestroyed-tooltip=你在本服務器拆除過樹木的次數
OreMined=挖礦次數
OreMined-tooltip=你在本服務器挖掘礦的次數
ItemsCrafted=物品合成數
ItemsCrafted-tooltip=你在本服務器合成過物品的次數
ItemsPickedUp=物品拾起數
ItemsPickedUp-tooltip=你在本服務器起過物品的次數
Kills=撃殺數
Kills-tooltip=你在本服務器殺過的敵人數量
Deaths=死亡次數
Deaths-tooltip=你在本服務器死過的數量
DamageDealt=傷害量
DamageDealt-tooltip=你在本伺服器對敵人所造成的傷害量
DistanceTravelled=移動距離
DistanceTravelled-tooltip=你在本服務器的總移動距離
CapsulesUsed=膠囊使用次數
CapsulesUsed-tooltip=你在本服務器使用過膠囊的次數
EntityRepaired=機器維修次數
EntityRepaired-tooltip=你在本服務器維修過機器的次數
DeconstructionPlannerUsed=拆除規劃器使用次數
DeconstructionPlannerUsed-tooltip=你在本服務器使用過拆除規劃器的次數
MapTagsMade=地圖標籤數量
MapTagsMade-tooltip=你在本服務器放過的地圖標籤數量
DamageDeathRatio=傷害死亡比
DamageDeathRatio-tooltip=傷害除以死亡
KillDeathRatio=撃殺死亡比
KillDeathRatio-tooltip=撃殺除以死亡
SessionTime=平均遊玩時間
SessionTime-tooltip=平均遊玩時間
BuildRatio=建築比
BuildRatio-tooltip=建築次數除以拆除次數
RocketPerHour=每小時火箭發射次數
RocketPerHour-tooltip=火箭發射次數除以遊玩時間
TreeKillPerMinute=每分鐘樹木拆除數
TreeKillPerMinute-tooltip=樹木拆除數除以遊玩時間
NetPlayTime=淨遊玩時間
NetPlayTime-tooltip=遊玩時間減掛機時間
AFKTimeRatio=掛機時間比
AFKTimeRatio-tooltip=掛機時間除以遊玩時間
Locale=語言
Locale-tooltip=用戶介面語言
# expcore
[expcore-data]
description-preference=允許寫入資料。
description-data=把用戶資料寫入電腦。
arg-option="All", "Statistics", "Settings", "Required" 之中
set-preference=你的喜好已設定為 __1__ 。在你重新加入前將不會有任何改動。
get-preference=你的喜好為 __1__ 。使用 /set-preference 來更改,使用 /save-data 來獲得一份資料副本。
get-data=你的個人資料已寫入: factorio/script_output/expgaming_player_data.json
data-failed=無法載入你的個人資料,任何改動將不會保存。
data-restore=已載入你的個人資料,所有改動將會被保存。
preference=儲存喜好
preference-tooltip=那種資料將在退出時保存
preference-value-tooltip=使用 /set-preference 來更換儲存喜好
preference-All=所有資料都會被儲存
preference-Statistics=只有數據, 設定和資料會被儲存
preference-Settings=只有設定和資料會被儲存
preference-Required=只有需要的資料會被儲存
# gui
[player-list]
main-tooltip=用戶名單
open-action-bar=選項
close-action-bar=關閉選項
reason-confirm=確認原因
reason-entry=輸入原因
goto-player=傳送到用戶
bring-player=傳送用戶到自己
report-player=舉報用戶
jail-player=監禁用戶
kick-player=踢除用戶
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 來停用。
set=你的 Bonus 已設為 __1__。
get=你的 Bonus 為 __1__。
perm=你沒有足夠權限設多過 __1__。
display-fmms=挖掘速度
display-fmms-tooltip=勢力挖掘速度
display-frs=跑步速度
display-frs-tooltip=勢力跑步速度
display-fcs=合成速度
display-fcs-tooltip=勢力合成速度
display-fisb=儲存位
display-fisb-tooltip=勢力儲存位
display-fhb=生命
display-fhb-tooltip=勢力生命
display-frdb=到達距離
display-frdb-tooltip=勢力到達距離
display-fwrsm=機械人速度
display-fwrsm-tooltip=勢力機械人速度
display-fwrbm=機械人電力
display-fwrbm-tooltip=勢力機械人電力
display-fwrsb=械人儲存
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選上來設定喜好
reset=重設
toggle=啟用喜好
move-up=向上
move-down=向下
-31
View File
@@ -1,31 +0,0 @@
[links]
discord=https://discord.explosivegaming.nl
website=https://www.explosivegaming.nl
status=https://status.explosivegaming.nl
github=https://github.com/explosivegaming/ExpCluster
patreon=https://www.patreon.com/ExpGaming
[info]
players-online=現在有 __1__ 人在線
total-map-time=地圖時間為 __1__
discord=加入社群: https://discord.explosivegaming.nl
website=訪問網站: https://www.explosivegaming.nl
status=伺服器狀態: https://status.explosivegaming.nl
github=增加功能或回報錯誤: https://github.com/explosivegaming/ExpCluster
patreon=支持我們: https://www.patreon.com/ExpGaming
custom-commands=這裹使用了自制指令,如 /tag 和 /me。可使用 /chelp 查看更多。
read-readme=確保你已閱讀相關資訊。按左上 i 圖標可再次查看。
softmod=這裹用了自設情境,是一種軟裝模組。
redmew=
lhd=列車必須是左則通行。這是本服務器長久以來的規則。
[warnings]
received=你已被 __1__ 警告了,現在共有 __2__ 個警告。 __3__
pre-kick=這是在被踢離之前的最後警告。
kick=你已因為被警告太多次而被請離,不過你之後還是可以回來的。
pre-pre-ban=按照你的行為,你有可能會被封禁。
pre-ban=這是在被封禁之前的最後警告。
ban=你已因為被警告太多次而被封禁; 可以到 __1__ 申訴.
script-warning=這是系統發出的自動警告 (__1__/__2__)
script-warning-removed=系統發出的自動警告已失效 (__1__/__2__)
script-warning-limit=__1__ 已被系統發出了一則自動警告。
-114
View File
@@ -1,114 +0,0 @@
[join-message]
description-add=設定 / 取得你的加入信息。
description-remove=移除你的加入信息。
arg-message=你的加入信息。
greet=[color=0,1,0] 歡迎來到 EXP 的服務器! 若果你喜歡本服務器可加入 DISCORD: __1__ [/color]
message-set=你的加入信息已更新。
message-get=你的加入信息為: __1__
message-cleared=你的加入信息已清除。
[quickbar]
saved=你的工具列已儲存。
[exp-required]
Warnings=警告
Warnings-tooltip=你所有收到的警告
Warnings-value-tooltip=你所有收到的警告
[exp-settings]
Colour=顏色
Colour-tooltip=你的人物顏色
Colour-value-tooltip=使用 /color 來更換你的人物顏色
JoinMessage=加入信息
JoinMessage-tooltip=你加入時所顯示的信息
JoinMessage-value-tooltip=使用 /join-message 來更換加入信息
QuickbarFilters=快捷欄
QuickbarFilters-tooltip=快捷欄的選項
QuickbarFilters-value-tooltip=使用 /save-quickbar 來更換快捷欄的選項
UsesAlt=細節模式
UsesAlt-tooltip=你加入時是否使用細節模式
UsesAlt-value-tooltip=按下 __CONTROL__show-info__ 來更改
UsesServerUps=服務器更新數 (SUPS)
UsesServerUps-tooltip=現在是否顯示服務器更新數 (SUPS)
UsesServerUps-value-tooltip=使用 /server-ups 來更換顯示
Tag=你的人物標籤
Tag-tooltip=你的人物標籤
Tag-value-tooltip=使用 /tag 來更換你的人物標籤
TagColor=人物標籤顏色
TagColor-tooltip=你的人物標籤顏色
TagColor-value-tooltip=使用 /tag-color 來更換你的人物標籤顏色
Bonus=人物附加屬性
Bonus-tooltip=你的人物附加屬性
Bonus-value-tooltip=使用 /bonus 來更改
HasEnabledDecon=快速拆除樹木
ToolbarState=工具箱
ToolbarState-tooltip=次序和喜愛項目
ToolbarState-value-tooltip=此值在離開時自動計算
[exp-statistics]
MapsPlayed=地圖遊玩次數
MapsPlayed-tooltip=你在本服務器遊玩過的地圖數
JoinCount=登入次數
JoinCount-tooltip=你在本服務器遊玩的次數
Playtime=遊玩時間
Playtime-tooltip=你在本服務器遊玩的時間
AfkTime=掛機時間
AfkTime-tooltip=你在本服務器掛機的時間
ChatMessages=傳送信息次數
ChatMessages-tooltip=你在本服務器發送信息的次數
CommandsUsed=使用指令次數
CommandsUsed-tooltip=你在本服務器使用指令的次數
RocketsLaunched=火箭發射次數
RocketsLaunched-tooltip=你在本服務器在線時所發射過火箭的次數
ResearchCompleted=研究次數
ResearchCompleted-tooltip=你在本服務器在線時完成研究的次數
MachinesBuilt=機器建造次數
MachinesBuilt-tooltip=你在本服務器建造過機器的次數
MachinesRemoved=機器拆除次數
MachinesRemoved-tooltip=你在本服務器拆除過機器的次數
TilesBuilt=地磚建造次數
TilesBuilt-tooltip=你在本服務器建造過地磚的次數
TilesRemoved=地磚拆除次數
TilesRemoved-tooltip=你在本服務器拆除過地磚的次數
TreesDestroyed=樹木拆除次數
TreesDestroyed-tooltip=你在本服務器拆除過樹木的次數
OreMined=挖礦次數
OreMined-tooltip=你在本服務器挖掘礦的次數
ItemsCrafted=物品合成數
ItemsCrafted-tooltip=你在本服務器合成過物品的次數
ItemsPickedUp=物品拾起數
ItemsPickedUp-tooltip=你在本服務器起過物品的次數
Kills=撃殺數
Kills-tooltip=你在本服務器殺過的敵人數量
Deaths=死亡次數
Deaths-tooltip=你在本服務器死過的數量
DamageDealt=傷害量
DamageDealt-tooltip=你在本伺服器對敵人所造成的傷害量
DistanceTravelled=移動距離
DistanceTravelled-tooltip=你在本服務器的總移動距離
CapsulesUsed=膠囊使用次數
CapsulesUsed-tooltip=你在本服務器使用過膠囊的次數
EntityRepaired=機器維修次數
EntityRepaired-tooltip=你在本服務器維修過機器的次數
DeconstructionPlannerUsed=拆除規劃器使用次數
DeconstructionPlannerUsed-tooltip=你在本服務器使用過拆除規劃器的次數
MapTagsMade=地圖標籤數量
MapTagsMade-tooltip=你在本服務器放過的地圖標籤數量
DamageDeathRatio=傷害死亡比
DamageDeathRatio-tooltip=傷害除以死亡
KillDeathRatio=撃殺死亡比
KillDeathRatio-tooltip=撃殺除以死亡
SessionTime=平均遊玩時間
SessionTime-tooltip=平均遊玩時間
BuildRatio=建築比
BuildRatio-tooltip=建築次數除以拆除次數
RocketPerHour=每小時火箭發射次數
RocketPerHour-tooltip=火箭發射次數除以遊玩時間
TreeKillPerMinute=每分鐘樹木拆除數
TreeKillPerMinute-tooltip=樹木拆除數除以遊玩時間
NetPlayTime=淨遊玩時間
NetPlayTime-tooltip=遊玩時間減掛機時間
AFKTimeRatio=掛機時間比
AFKTimeRatio-tooltip=掛機時間除以遊玩時間
Locale=語言
Locale-tooltip=用戶介面語言
@@ -1,24 +0,0 @@
[expcore-roles]
error-log-format-flag=[ERROR] roleFlag/__1__ :: __2__
error-log-format-assign=[ERROR] rolePromote/__1__ :: __2__
game-message-assign=__1__ 已被 __3__ 指派到身分組 __2__
game-message-unassign=__1__ 已被 __3__ 取消指派到身分組 __2__
reject-role=無效的身分組。
reject-player-role=用戶已有更高的身分組。
[expcore-data]
description-preference=允許寫入資料。
description-data=把用戶資料寫入電腦
arg-option="All", "Statistics", "Settings", "Required" 之中
set-preference=你的喜好已設定為 __1__ 。在你重新加入前將不會有任何改動。
get-preference=你的喜好為 __1__ 。使用 /set-preference 來更改,使用 /save-data 來獲得一份資料副本。
get-data=你的個人資料已寫入: factorio/script_output/expgaming_player_data.json
data-failed=無法載入你的個人資料,任何改動將不會保存。
data-restore=已載入你的個人資料,所有改動將會被保存。
preference=儲存喜好
preference-tooltip=那種資料將在退出時保存
preference-value-tooltip=使用 /set-preference 來更換儲存喜好
preference-All=所有資料都會被儲存
preference-Statistics=只有數據, 設定和資料會被儲存
preference-Settings=只有設定和資料會被儲存
preference-Required=只有需要的資料會被儲存
-235
View File
@@ -1,235 +0,0 @@
[player-list]
main-tooltip=用戶名單
open-action-bar=選項
close-action-bar=關閉選項
reason-confirm=確認原因
reason-entry=輸入原因
goto-player=傳送到用戶
bring-player=傳送用戶到自己
report-player=舉報用戶
warn-player=警告用戶
jail-player=監禁用戶
kick-player=踢除用戶
ban-player=封禁用戶
afk-time=__1__% 地圖時間\n上次移動: __2__
open-map=__1__ __2__\n__3__\n按下打開地圖
[rocket-info]
main-tooltip=火箭資訊
launch-tooltip=發射火箭
launch-tooltip-disabled=發射火箭 (未準備)
toggle-rocket-tooltip=取消自動發射
toggle-rocket-tooltip-disabled=啟用自動發射
toggle-section-tooltip=擴張欄
toggle-section-collapse-tooltip=收縮欄
section-caption-stats=數據
section-tooltip-stats=火箭發射數據
section-caption-milestones=里程碑
section-tooltip-milestones=里程碑達成次數
section-caption-progress=建造程度
section-tooltip-progress=火箭建造程度
progress-no-silos=你沒有火箭發射井
data-caption-first-launch=首次發射
data-tooltip-first-launch=首次發射時間
data-caption-last-launch=最後發射
data-tooltip-last-launch=最後發射時間
data-caption-fastest-launch=最快發射
data-tooltip-fastest-launch=最快發射時間
data-caption-total-rockets=總發射次數
data-tooltip-total-rockets=我方總發射次數
value-tooltip-total-rockets=__1__% 的火箭發射都在本地圖上。
data-caption-avg-launch=平均時間
data-tooltip-avg-launch=火箭發射平均時間
data-caption-avg-launch-n=平均時間 __1__
data-tooltip-avg-launch-n=__1__ 次火箭發射的平均時間
data-caption-milestone-n=里程碑 __1__
data-tooltip-milestone-n=__1__ 次火箭發射的時間
data-caption-milestone-next=N/A
data-tooltip-milestone-next=未達成
progress-x-pos=X __1__
progress-y-pos=Y __1__
progress-label-tooltip=在地圖上看
progress-launched=已發射
progress-caption=__1__ %
progress-tooltip=該火箭發射井發射了 __1__ 次
launch-failed=火箭發射失敗, 請過一會再試。
[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=修改傳送陣圖案
[readme]
main-tooltip=資訊
welcome-tab=歡迎
welcome-tooltip=歡迎來到 Explosive Gaming
welcome-general=歡迎來到 Explosive Gaming; 你在這裏的時候要遵守規則。 你可以在左上方的介面找到更多資訊。 地圖時間 __2__。\n下次地圖重設: __1__
welcome-roles=我們有自訂的身份組來保護其他用戶。 所以你有機會不能使用拆除規劃器或掉東西。 身份組給予你各種權限\n你有以下身份組: __1__
welcome-chat=你可以使用 「重音符/抑音符」 鍵 (ESC 鍵 下方) - 你也可以在選項中修改按鍵。\n你目前設定為 「__CONTROL__toggle-console__」
rules-tab=規則
rules-tooltip=服務器規則
rules-general=你在本服務器遊玩即等同同意以下規則。管理員有機會就相關事項提問。你可能會因為違反規則而被封禁,你可以在 Discord 中提出反對。
rules-1=開掛或濫用漏洞將不會被容忍。
rules-2=當你找到漏洞可以通知職員。
rules-3=請在聊天室中尊重其他人。
rules-4=請不要把所有物品全部取走。
rules-5=請不要濫發信息,大量鋪設地磚,或不為箱子設置上限,浪費資源。
rules-6=當你想大量拆除原有設施前請通知其他用戶。
rules-7=請不要無故使用機器人來大量鋪設或拆除地磚,濫用主動出貨箱。
rules-8=請不要隨意移動來開大量地圖,這有助減少地圖下載時間。
rules-9=請不要無故使用全圖喇叭。
rules-10=請不要無故轉動傳送帶方向,停用傳送帶,或停止生產。
rules-11=請不要用迴旋處。 火車站尾返程則是例外。
rules-12=請跟其他人使用相同的火車組成。常用的有 1-2-1,2-4-2,或 3-8-3,即1個動車組拖2個貨車組再拖不同方向的1個動車組。
rules-13=火車必須是左上右下 (LHD)。
rules-14=禁止要求身份組,發廣告,發惡意連結。
rules-15=使用常識, 舉報違規人員,管理員有最終決定權。
commands-tab=指令
commands-tooltip=你可用的指令
commands-general=這裏有自訂的指令。你可以在下邊找到你可用的指令及其功能介紹。你可以用 /search-help 來取得更多資訊。
servers-tab=伺服器
servers-tooltip=到其他伺服器及網站的連結
servers-general=這是其中一個伺服器。你可以在下方找到其他同樣是由我們運行的伺服器資料。
servers-factorio=異星工廠伺服器
servers-1=S1 公共
servers-d1=這個服務器每兩日重設。
servers-2=S2 -
servers-d2=這個服務器沒在營運。
servers-3=S3 周
servers-d3=這個服務器每週重設。
servers-4=S4 月
servers-d4=這個服務器每月重設。
servers-5=S5 模組
servers-d5=這個服務器運行模組,可在 Discord 中得到更多資訊。
servers-6=S6 Donator
servers-d6=這個服務器為支持者運行,可在 Discord 中得到更多資訊。
servers-7=S7 項目
servers-d7=這個服務器只在有項目期間運行。
servers-8=S8 T̷-̶s̶-̴:̷
servers-d8=N̵o̴ ̶o̷-̶e̵ ̴k̸n̷-̶w̵s̸ ̴w̷h̷a̶-̶ ̷h̴a̴p̷p̴e̷n̷s̸ ̷o̶n̴ ̷t̶h̴-̶s̶ ̷s̷e̶r̸v̸e̴r̷,̶ ̸i̸t̴ ̷m̶-̸g̴h̶t̷ ̸n̸-̶t̵ ̷e̴v̸e̸n̶t̷ ̵-̷x̴i̵s̶t̸。̸
servers-connect-Offline=該服務器目前沒有在運行
servers-connect-Current=你目前所在的伺服器
servers-connect-Version=該服務器運行不同的遊戲版本: __1__
servers-connect-Password=該服務器需要密碼
servers-connect-Modded=該服務器需要下載模組
servers-connect-Online=該服務器在線
servers-external=外部連結
servers-open-in-browser=可在你的瀏覽器中打開
backers-tab=支持者
backers-tooltip=支持者
backers-general=他們為服務器運行提供了不少幫助。
backers-management=系統管理員
backers-board=議員
backers-staff=職員
backers-backers=支持者
backers-active=活躍玩家
data-tab=資料
data-tooltip=所有已存資料
data-general=資料會自動存到各個服務器中。 你也可以用 /save-data 存一份副本。 也可用 /set-preference 來更改喜好。
data-settings=設定
data-statistics=數據
data-required=需要
data-misc=雜項
data-format=__1____2__
[bonus]
description=取得 / 設定 Bonus 量。
arg-amount=Bonus 數量, 0 來停用。
set=你的 Bonus 已設為 __1__。
get=你的 Bonus 為 __1__。
perm=你沒有足夠權限設多過 __1__。
display-fmms=挖掘速度
display-fmms-tooltip=勢力挖掘速度
display-frs=跑步速度
display-frs-tooltip=勢力跑步速度
display-fcs=合成速度
display-fcs-tooltip=勢力合成速度
display-fisb=儲存位
display-fisb-tooltip=勢力儲存位
display-fhb=生命
display-fhb-tooltip=勢力生命
display-frdb=到達距離
display-frdb-tooltip=勢力到達距離
display-fwrsm=機械人速度
display-fwrsm-tooltip=勢力機械人速度
display-fwrbm=機械人電力
display-fwrbm-tooltip=勢力機械人電力
display-fwrsb=械人儲存
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 功能。
enter=現在進入 vlayer 區域選擇
exit=已進入 vlayer 區域選擇
[toolbar]
main-caption=工具箱
main-tooltip=工具箱設定\n選上來設定喜好
reset=重設
toggle=啟用喜好
move-up=向上
move-down=向下
+1
View File
@@ -8,6 +8,7 @@
"dependencies": {
"clusterio": "*",
"exp_util": "*",
"exp_roles": "*",
"exp_gui": "*"
}
}
-119
View File
@@ -1,119 +0,0 @@
--[[-- Control Module - Jail
- Adds a way to jail players.
@control Jail
@alias Jail
@usage
-- import the module from the control modules
local Jail = require("modules.exp_legacy.modules.control.jail") --- @dep modules.control.jail
-- This will move 'MrBiter' to the jail role and remove all other roles from them
-- the player name and reason are only so they can be included in the event for user feedback
Jail.jail_player('MrBiter', 'Cooldude2606', 'Likes biters too much')
-- This will give 'MrBiter' all his roles back and remove him from jail
-- again as above the player name is only used in the event for user feedback
Jail.unjail_player('MrBiter', 'Cooldude2606')
]]
local Roles = require("modules.exp_legacy.expcore.roles")
local valid_player = function(p) return type(p) == "userdata" and p or game.get_player(p) end
local assign_roles = Roles.assign_player
local unassign_roles = Roles.unassign_player
local has_role = Roles.player_has_role
local get_roles = Roles.get_player_roles
local Jail = {
old_roles = {},
events = {
--- When a player is assigned to jail
-- @event on_player_jailed
-- @tparam number player_index the index of the player who was jailed
-- @tparam string by_player_name the name of the player who jailed the other player
-- @tparam string reason the reason that the player was jailed
on_player_jailed = script.generate_event_name(),
--- When a player is unassigned from jail
-- @event on_player_unjailed
-- @tparam number player_index the index of the player who was unjailed
-- @tparam string by_player_name the name of the player who unjailed the other player
on_player_unjailed = script.generate_event_name(),
},
}
--- Used to emit the jail related events
-- @tparam number event the name of the event that will be emited
-- @tparam LuaPlayer player the player who is being acted on
-- @tparam string by_player_name the player who is doing the action
-- @tparam string reason the reason for the action (jail)
local function event_emit(event, player, by_player_name, reason)
script.raise_event(event, {
name = event,
tick = game.tick,
player_index = player.index,
by_player_name = by_player_name,
reason = reason,
})
end
--- Jail.
-- Functions related to jail
-- @section jail-functions
--- Checks if the player is currently in jail
-- @tparam LuaPlayer player the player to check if they are in jail
-- @treturn boolean whether the player is currently in jail
function Jail.is_jailed(player)
return has_role(valid_player(player), "Jail")
end
--- Moves a player to jail and removes all other roles
-- @tparam LuaPlayer player the player who will be jailed
-- @tparam string by_player_name the name of the player who is doing the jailing
-- @tparam[opt='Non given.'] string reason the reason that the player is being jailed
-- @treturn boolean wheather the user was jailed successfully
function Jail.jail_player(player, by_player_name, reason)
player = valid_player(player)
if not player then return end
if not by_player_name then return end
reason = reason or "Non given."
if has_role(player, "Jail") then return end
local roles = get_roles(player)
player.walking_state = { walking = false, direction = player.walking_state.direction }
player.riding_state = { acceleration = defines.riding.acceleration.nothing, direction = player.riding_state.direction }
player.mining_state = { mining = false }
player.shooting_state = { state = defines.shooting.not_shooting, position = player.shooting_state.position }
player.picking_state = false
player.repair_state = { repairing = false, position = player.repair_state.position }
unassign_roles(player, roles, by_player_name, nil, true)
assign_roles(player, "Jail", by_player_name, nil, true)
assign_roles(player, roles, by_player_name, nil, true)
event_emit(Jail.events.on_player_jailed, player, by_player_name, reason)
return true
end
--- Moves a player out of jail and restores all roles previously removed
-- @tparam LuaPlayer player the player that will be unjailed
-- @tparam string by_player_name the name of the player that is doing the unjail
-- @treturn boolean whether the player was unjailed successfully
function Jail.unjail_player(player, by_player_name)
player = valid_player(player)
if not player then return end
if not by_player_name then return end
if not has_role(player, "Jail") then return end
unassign_roles(player, "Jail", by_player_name, nil, true)
event_emit(Jail.events.on_player_unjailed, player, by_player_name)
return true
end
return Jail
@@ -1,208 +0,0 @@
--[[-- Control Module - Protection
- Controls protected entities
@control Protection
@alias Protection
]]
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.protection") --- @dep config.protection
local EntityProtection = {
protected_entity_names = table.deep_copy(config.always_protected_names),
protected_entity_types = table.deep_copy(config.always_protected_types),
events = {
--- When a player mines a protected entity
-- @event on_player_mined_protected
-- @tparam number player_index the player index of the player who got mined the entity
-- @tparam LuaEntity entity the entity which was mined
on_player_mined_protected = script.generate_event_name(),
--- When a player repeatedly mines protected entities
-- @event on_repeat_violation
-- @tparam number player_index the player index of the player who got mined the entities
-- @tparam LuaEntity entity the last entity which was mined
on_repeat_violation = script.generate_event_name(),
},
}
-- Convert config tables into lookup tables
for _, config_key in ipairs{ "always_protected_names", "always_protected_types", "always_trigger_repeat_names", "always_trigger_repeat_types" } do
local tbl = config[config_key]
for key, value in ipairs(tbl) do
tbl[key] = nil
tbl[value] = true
end
end
-- Require roles if a permission is assigned in the config
local Roles
if config.ignore_permission then
Roles = require("modules.exp_legacy.expcore.roles") --- @dep expcore.roles
end
----- Storage Variables -----
--- Variables stored in the global table
local protected_entities = {} -- All entities which are protected
local protected_areas = {} -- All areas which are protected
local repeats = {} -- Stores repeat removals by players
Storage.register({
protected_entities = protected_entities,
protected_areas = protected_areas,
repeats = repeats,
}, function(tbl)
protected_entities = tbl.protected_entities
protected_areas = tbl.protected_areas
repeats = tbl.repeats
end)
----- Local Functions -----
--- Functions used internally to search and add to the protected array
--- Get the key used in protected_entities
local function get_entity_key(entity)
return string.format("%i,%i", math.floor(entity.position.x), math.floor(entity.position.y))
end
--- Get the key used in protected_areas
local function get_area_key(area)
return string.format("%i,%i", math.floor(area.left_top.x), math.floor(area.left_top.y))
end
--- Check if an entity is always protected
local function check_always_protected(entity)
return config.always_protected_names[entity.name] or config.always_protected_types[entity.type] or false
end
--- Check if an entity always triggers repeat protection
local function check_always_trigger_repeat(entity)
return config.always_trigger_repeat_names[entity.name] or config.always_trigger_repeat_types[entity.type] or false
end
----- Public Functions -----
--- Functions used to add and remove protected entities
--- Add an entity to the protected list
function EntityProtection.add_entity(entity)
local entities = protected_entities[entity.surface.index]
if not entities then
entities = {}
protected_entities[entity.surface.index] = entities
end
entities[get_entity_key(entity)] = entity
end
--- Remove an entity from the protected list
function EntityProtection.remove_entity(entity)
local entities = protected_entities[entity.surface.index]
if not entities then return end
entities[get_entity_key(entity)] = nil
end
--- Get all protected entities on a surface
function EntityProtection.get_entities(surface)
return protected_entities[surface.index] or {}
end
--- Check if an entity is protected
function EntityProtection.is_entity_protected(entity)
if check_always_protected(entity) then return true end
local entities = protected_entities[entity.surface.index]
if not entities then return false end
return entities[get_entity_key(entity)] == entity
end
--- Add an area to the protected list
function EntityProtection.add_area(surface, area)
local areas = protected_areas[surface.index]
if not areas then
areas = {}
protected_areas[surface.index] = areas
end
areas[get_area_key(area)] = area
end
--- Remove an area from the protected list
function EntityProtection.remove_area(surface, area)
local areas = protected_areas[surface.index]
if not areas then return end
areas[get_area_key(area)] = nil
end
--- Get all protected areas on a surface
function EntityProtection.get_areas(surface)
return protected_areas[surface.index] or {}
end
--- Check if an entity is protected
function EntityProtection.is_position_protected(surface, position)
local areas = protected_areas[surface.index]
if not areas then return false end
for _, area in pairs(areas) do
if area.left_top.x <= position.x and area.left_top.y <= position.y
and area.right_bottom.x >= position.x and area.right_bottom.y >= position.y
then
return true
end
end
return false
end
----- Events -----
--- All events registered by this module
--- Raise events for protected entities
Event.add(defines.events.on_pre_player_mined_item, function(event)
local entity = event.entity
local player = game.players[event.player_index]
-- Check if the player should be ignored
if config.ignore_admins and player.admin then return end
if entity.last_user == nil or entity.last_user.index == player.index then return end
if config.ignore_permission and Roles.player_allowed(player, config.ignore_permission) then return end
-- Check if the entity is protected
if EntityProtection.is_entity_protected(entity)
or EntityProtection.is_position_protected(entity.surface, entity.position)
then
-- Update repeats
local player_repeats = repeats[player.name]
if not player_repeats then
player_repeats = { last = game.tick, count = 0 }
repeats[player.name] = player_repeats
end
player_repeats.last = game.tick
player_repeats.count = player_repeats.count + 1
-- Send events
event.name = EntityProtection.events.on_player_mined_protected
script.raise_event(EntityProtection.events.on_player_mined_protected, event)
if check_always_trigger_repeat(entity) or player_repeats.count >= config.repeat_count then
player_repeats.count = 0 -- Reset to avoid spamming of events
event.name = EntityProtection.events.on_repeat_violation
script.raise_event(EntityProtection.events.on_repeat_violation, event)
end
end
end)
--- Remove old repeats
Event.on_nth_tick(config.refresh_rate, function()
local old = game.tick - config.repeat_lifetime
for player_name, player_repeats in pairs(repeats) do
if player_repeats.last <= old then
repeats[player_name] = nil
end
end
end)
--- When an entity is removed remove it from the protection list
local function event_remove_entity(event)
EntityProtection.remove_entity(event.entity)
end
Event.add(defines.events.on_space_platform_pre_mined, event_remove_entity)
Event.add(defines.events.on_pre_player_mined_item, event_remove_entity)
Event.add(defines.events.on_robot_pre_mined, event_remove_entity)
Event.add(defines.events.on_entity_died, event_remove_entity)
Event.add(defines.events.script_raised_destroy, event_remove_entity)
return EntityProtection
@@ -1,204 +0,0 @@
--[[-- Control Module - Rockets
- Stores rocket stats for each force.
@control Rockets
@alias Rockets
@usage
-- import the module from the control modules
local Rockets = require("modules.exp_legacy.modules.control.rockets") --- @dep modules.control.rockets
-- Some basic information is stored for each silo that has been built
-- the data includes: the tick it was built, the rockets launched from it and more
Rockets.get_silo_data(rocket_silo_entity)
-- Some information is also stored for each force
Rockets.get_stats('player')
-- You can get the rocket data for all silos for a force by using get_silos
Rockets.get_silos('player')
-- You can get the launch time for a rocket, meaning what game tick the 50th rocket was launched
Rockets.get_rocket_time('player', 50)
-- The rolling average will work out the time to launch one rocket based on the last X rockets
Rockets.get_rolling_average('player', 10)
]]
local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event
local Storage = require("modules/exp_util/storage")
local config = require("modules.exp_legacy.config.gui.rockets") --- @dep config.rockets
local largest_rolling_avg = 0
for _, avg_over in pairs(config.stats.rolling_avg) do
if avg_over > largest_rolling_avg then
largest_rolling_avg = avg_over
end
end
local Rockets = {
times = {},
stats = {},
silos = {},
}
local rocket_times = Rockets.times
local rocket_stats = Rockets.stats
local rocket_silos = Rockets.silos
Storage.register({
rocket_times = rocket_times,
rocket_stats = rocket_stats,
rocket_silos = rocket_silos,
}, function(tbl)
Rockets.times = tbl.rocket_times
Rockets.stats = tbl.rocket_stats
Rockets.silos = tbl.rocket_silos
rocket_times = Rockets.times
rocket_stats = Rockets.stats
rocket_silos = Rockets.silos
end)
--- Gets the silo data for a given silo entity
--- @param silo LuaEntity Rocket silo entity
--- @return table # Data table for this silo, contains rockets launch, silo status, and its force
function Rockets.get_silo_data(silo)
return rocket_silos[silo.unit_number]
end
--- Gets the silo entity from its silo name, reverse to get_silo_data
--- @param silo_name string Silo name that is stored in its data
--- @return LuaEntity # Rocket silo entity
function Rockets.get_silo_entity(silo_name)
return rocket_silos[tonumber(silo_name)].entity
end
--- Gets the rocket stats for a force
--- @param force_name string Name of the force to get the stats for
--- @return table # Stats for the force
function Rockets.get_stats(force_name)
return rocket_stats[force_name] or {}
end
--- Gets all the rocket silos that belong to a force
--- @param force_name string Name of the force to get the silos for
--- @return table # Array of silo data that all belong to this force
function Rockets.get_silos(force_name)
local rtn = {}
for _, silo_data in pairs(rocket_silos) do
if silo_data.force == force_name then
table.insert(rtn, silo_data)
end
end
return rtn
end
--- Gets the launch time of a given rocket, due to cleaning not all counts are valid
--- @param force_name string Name of the force to get the count for
--- @param rocket_number number Number of the rocket to get the launch time for
--- @return number? # Game tick that the rocket was launched on
function Rockets.get_rocket_time(force_name, rocket_number)
return rocket_times[force_name] and rocket_times[force_name][rocket_number] or nil
end
--- Gets the number of rockets that a force has launched
--- @param force_name string the name of the force to get the count for
--- @return number # Number of rockets that the force has launched
function Rockets.get_rocket_count(force_name)
local force = game.forces[force_name]
return force.rockets_launched
end
--- Gets the total number of rockets launched by all forces
--- @return number # Total number of rockets launched this game
function Rockets.get_game_rocket_count()
local rtn = 0
for _, force in pairs(game.forces) do
rtn = rtn + force.rockets_launched
end
return rtn
end
--- Gets the rolling average time to launch a rocket
--- @param force_name string Name of the force to get the average for
--- @param count number Distance to get the rolling average over
--- @return number # Number of ticks required to launch one rocket
function Rockets.get_rolling_average(force_name, count)
local force = game.forces[force_name]
local rocket_count = force.rockets_launched
if rocket_count == 0 then return 0 end
local last_launch_time = rocket_times[force_name][rocket_count]
local start_rocket_time = 0
if count < rocket_count then
start_rocket_time = rocket_times[force_name][rocket_count - count + 1]
rocket_count = count
end
return math.floor((last_launch_time - start_rocket_time) / rocket_count)
end
--- When a launch is trigger it will wait for the silo to reset
--- @param event EventData.on_rocket_launch_ordered
Event.add(defines.events.on_rocket_launch_ordered, function(event)
local silo_data = Rockets.get_silo_data(event.rocket_silo)
assert(silo_data, "Rocket silo missing data: " .. tostring(event.rocket_silo))
silo_data.launched = silo_data.launched + 1
silo_data.awaiting_reset = true
end)
--- Event used to update the stats and the hui when a rocket is launched
--- @param event EventData.on_cargo_pod_finished_ascending
Event.add(defines.events.on_cargo_pod_finished_ascending, function(event)
local force = event.cargo_pod.force
local force_name = force.name
local rockets_launched = force.rockets_launched
--- Handles updates to the rocket stats
local stats = rocket_stats[force_name]
if not stats then
rocket_stats[force_name] = {}
stats = rocket_stats[force_name]
end
if rockets_launched == 1 then
stats.first_launch = event.tick
stats.fastest_launch = event.tick
elseif event.tick - stats.last_launch < stats.fastest_launch then
stats.fastest_launch = event.tick - stats.last_launch
end
stats.last_launch = event.tick
--- Appends the new rocket into the array
if not rocket_times[force_name] then
rocket_times[force_name] = {}
end
rocket_times[force_name][rockets_launched] = event.tick
local remove_rocket = rockets_launched - largest_rolling_avg
if remove_rocket > 0 and not table.contains(config.milestones, remove_rocket) then
rocket_times[force_name][remove_rocket] = nil
end
end)
--- Adds a silo to the list when it is built
--- @param event EventData.on_built_entity | EventData.on_robot_built_entity
local function on_built(event)
local entity = event.entity
if entity.valid and entity.name == "rocket-silo" then
rocket_silos[entity.unit_number] = {
name = tostring(entity.unit_number),
force = entity.force.name,
entity = entity,
launched = 0,
awaiting_reset = false,
built = game.tick,
}
end
end
Event.add(defines.events.on_built_entity, on_built)
Event.add(defines.events.on_robot_built_entity, on_built)
return Rockets
@@ -1,173 +0,0 @@
local Storage = require("modules/exp_util/storage")
local Gui = require("modules/exp_gui")
local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event
----- Locals -----
local follow_label -- Gui constructor
local following = {}
local spectating = {}
local Public = {}
----- Storage data -----
Storage.register({
following = following,
spectating = spectating,
}, function(tbl)
following = tbl.following
spectating = tbl.spectating
end)
----- Public Functions -----
--- Test if a player is in spectator mode
-- @tparam LuaPlayer player The player to test the controller type of
-- @treturn boolean Returns true if the player is in spectator mode
function Public.is_spectating(player)
assert(player and player.valid, "Invalid player given to follower")
return player.controller_type == defines.controllers.spectator
end
--- Puts a player into spectator mode while maintaining an association to their character
-- @tparam LuaPlayer player The player that will be placed into spectator mode
-- @treturn boolean Returns false if the player was already in spectator mode
function Public.start_spectate(player)
assert(player and player.valid, "Invalid player given to follower")
if spectating[player.index] or not player.character then return false end
local character = player.character
local opened = player.opened
player.set_controller{ type = defines.controllers.spectator }
player.associate_character(character)
spectating[player.index] = character
if opened then player.opened = opened end -- Maintain opened after controller change
return true
end
--- Return a player from spectator mode back to their character, if their character was killed then respawn them
-- @tparam LuaPlayer player The player that will leave spectator mode
function Public.stop_spectate(player)
assert(player and player.valid, "Invalid player given to follower")
local character = spectating[player.index]
spectating[player.index] = nil
if character and character.valid then
local opened = player.opened
player.teleport(character.position, character.surface)
player.set_controller{ type = defines.controllers.character, character = character }
if opened then player.opened = opened end -- Maintain opened after controller change
else
player.ticks_to_respawn = 300
end
end
--- Test if a player is in follow mode
-- @tparam LuaPlayer player The player to test the follow mode of
-- @treturn boolean Returns true if the player is in follow mode
function Public.is_following(player)
assert(player and player.valid, "Invalid player given to follower")
return following[player.index] ~= nil
end
--- Puts a player into spectator mode and follows an entity as it moves
-- @tparam LuaPlayer player The player that will follow the entity
-- @tparam ?LuaPlayer|LuaEntity entity The player or entity that will be followed
function Public.start_follow(player, entity)
assert(player and player.valid, "Invalid player given to follower")
assert(entity and entity.valid, "Invalid entity given to follower")
local spectate = Public.start_spectate(player)
player.close_map()
follow_label(player.gui.screen, entity)
player.teleport(entity.position, entity.surface)
following[player.index] = { player, entity, entity.position, spectate }
end
--- Returns camera control to the player, will return a player to their character if start_follow placed them into spectator mode
-- @tparam LuaPlayer player The player that will regain control of their camera
function Public.stop_follow(player)
assert(player and player.valid, "Invalid player given to follower")
if following[player.index] and following[player.index][4] and Public.is_spectating(player) then
Public.stop_spectate(player)
end
Gui.destroy_if_valid(player.gui.screen.follow_label)
following[player.index] = nil
end
--- Returns camera control to all players, will return a player to their character if start_follow placed them into spectator mode
function Public.stop_all()
for key, data in pairs(following) do
Public.stop_follow(data[1])
end
end
----- Gui -----
--- Label used to show that the player is following, also used to allow esc to stop following
follow_label = Gui.define("follow-label")
:draw(function(def, parent, target)
Gui.destroy_if_valid(parent.follow_label)
local label = parent.add{
type = "label",
name = "follow_label",
style = "frame_title",
caption = "Following " .. target.name .. ".\nClick here or press esc to stop following.",
}
local player = Gui.get_player(parent)
local res = player.display_resolution
label.location = { 0, res.height - 150 }
label.style.width = res.width
label.style.horizontal_align = "center"
player.opened = label
return label
end)
:on_click(function(def, player, element)
Public.stop_follow(player)
end)
:on_closed(function(def, player, element)
-- Don't call set_controller during on_close as it invalidates the controller
-- Setting an invalid position (as to not equal their current) will call stop_follow on the next tick
following[player.index][3] = {}
end)
----- Events -----
--- Updates the location of the player as well as doing some sanity checks
-- @tparam LuaPlayer player The player to update the position of
-- @tparam ?LuaPlayer|LuaEntity entity The player or entity being followed
local function update_player_location(player, entity, old_position)
if player.character or not entity.valid then
Public.stop_follow(player)
elseif player.position.x ~= old_position.x or player.position.y ~= old_position.y then
Public.stop_follow(player)
else
player.teleport(entity.position, entity.surface)
end
end
--- Updates the locations of all players currently following something
local function update_all()
for _, data in pairs(following) do
update_player_location(data[1], data[2], data[3])
data[3] = data[1].position
end
end
-- Update the location of all players each tick
Event.add(defines.events.on_tick, update_all)
-- Check for player leaving
Event.add(defines.events.on_pre_player_left_game, function(event)
local player = game.players[event.player_index]
Public.stop_follow(player)
for _, data in pairs(following) do
if data[2] == player then
Public.stop_follow(data[1])
end
end
end)
----- Module Return -----
return Public
+23 -28
View File
@@ -14,10 +14,10 @@ local mega = 1000000
local vlayer = {}
local vlayer_data = {
entity_interfaces = {
energy = {},
circuit = {},
storage_input = {},
storage_output = {},
energy = {}, --- @type LuaEntity[]
circuit = {}, --- @type LuaEntity[]
storage_input = {}, --- @type LuaEntity[]
storage_output = {}, --- @type LuaEntity[]
},
properties = {
total_surface_area = 0,
@@ -28,10 +28,10 @@ local vlayer_data = {
capacity = 0,
},
storage = {
items = {},
power_items = {},
items = {}, --- @type table<string, number>
power_items = {}, --- @type table<string, { value: number, count: number }>
energy = 0,
unallocated = {},
unallocated = {}, --- @type table<string, number>
},
surface = table.deep_copy(config.surface),
}
@@ -45,7 +45,7 @@ for name, properties in pairs(config.allowed_items) do
if properties.power then
vlayer_data.storage.power_items[name] = {
value = properties.fuel_value * 1000000,
value = assert(properties.fuel_value) * 1000000,
count = 0,
}
end
@@ -349,7 +349,7 @@ function vlayer.create_input_interface(surface, position, circuit, last_user)
end
interface.destructible = false
interface.minable = false
interface.minable_flag = false
interface.operable = true
return interface
@@ -361,7 +361,7 @@ local function handle_input_interfaces()
if not interface.valid then
vlayer_data.entity_interfaces.storage_input[index] = nil
else
local inventory = interface.get_inventory(defines.inventory.chest)
local inventory = assert(interface.get_inventory(defines.inventory.chest))
for _, v in pairs(inventory.get_contents()) do
if config.allowed_items[v.name] then
@@ -384,8 +384,9 @@ local function handle_input_interfaces()
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(config.modded_items[v.name].base_game_equivalent, count_add * config.modded_items[v.name].multiplier)
vlayer.insert_item(modded_item.base_game_equivalent, count_add * modded_item.multiplier)
else
vlayer.insert_item(v.name, count_add)
end
@@ -427,7 +428,7 @@ function vlayer.create_output_interface(surface, position, circuit, last_user)
end
interface.destructible = false
interface.minable = false
interface.minable_flag = false
interface.operable = true
return interface
@@ -439,14 +440,14 @@ local function handle_output_interfaces()
if not interface.valid then
vlayer_data.entity_interfaces.storage_output[index] = nil
else
local inventory = interface.get_inventory(defines.inventory.chest)
local inventory_request_sections = interface.get_logistic_sections().sections
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(v.min - current_amount, vlayer_data.storage.items[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)
@@ -514,9 +515,6 @@ function vlayer.get_statistics()
energy_sustained = vdp * gsm,
energy_capacity = vlayer_data.properties.capacity * mega,
energy_storage = vlayer_data.storage.energy,
day_time = math.floor(vlayer_data.surface.daytime * vlayer_data.surface.ticks_per_day),
day_length = vlayer_data.surface.ticks_per_day,
tick = game.tick,
}
end
@@ -540,9 +538,6 @@ function vlayer.get_circuits()
energy_sustained = "signal-S",
energy_capacity = "signal-C",
energy_storage = "signal-E",
day_time = "signal-D",
day_length = "signal-L",
tick = "signal-T",
}
end
@@ -574,7 +569,7 @@ function vlayer.create_circuit_interface(surface, position, circuit, last_user)
end
interface.destructible = false
interface.minable = false
interface.minable_flag = false
interface.operable = true
return interface
@@ -588,11 +583,11 @@ local function handle_circuit_interfaces()
if not interface.valid then
vlayer_data.entity_interfaces.circuit[index] = nil
else
local circuit_oc = interface.get_or_create_control_behavior()
if circuit_oc.sections_count == 0 then
circuit_oc.add_section()
local control = interface.get_or_create_control_behavior() --[[@as LuaConstantCombinatorControlBehavior]]
if control.sections_count == 0 then
control.add_section()
end
circuit_oc = circuit_oc.sections[1]
local circuit_oc = assert(control.sections[1])
local signal_index = 1
local circuit = vlayer.get_circuits()
@@ -623,7 +618,7 @@ local function handle_circuit_interfaces()
-- 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).signal then
if not circuit_oc.get_slot(clear_index).value then
break -- There are no more signals to clear
end
@@ -653,7 +648,7 @@ function vlayer.create_energy_interface(surface, position, last_user)
end
interface.destructible = false
interface.minable = false
interface.minable_flag = false
interface.operable = false
interface.electric_buffer_size = 0
interface.power_production = 0
@@ -1,331 +0,0 @@
--[[-- Control Module - Warnings
- Adds a way to give and remove warnings to players.
@control Warnings
@alias Warnings
@usage
-- import the module from the control modules
local Warnings = require("modules.exp_legacy.modules.control.warnings") --- @dep modules.control.warnings
-- This will add a warning to the player
Warnings.add_warning('MrBiter', 'Cooldude2606', 'Killed too many biters')
-- This will remove a warning from a player, second name is just who is doing the action
Warnings.remove_warning('MrBiter', 'Cooldude2606')
-- Script warning as similar to normal warning but are designed to have no effect for a short amount of time
-- this is so it can be used for greifer protection without being too agressive
Warnings.add_script_warning('MrBiter', 'Killed too many biters')
-- Both normal and script warnings can also be cleared, this will remove all warnings
Warnings.clear_warnings('MrBiter', 'Cooldude2606')
]]
local Event = require("modules/exp_legacy/utils/event")
local Storage = require("modules/exp_util/storage")
local config = require("modules.exp_legacy.config.warnings")
local valid_player = function(p) return type(p) == "userdata" and p or game.get_player(p) end
--- Stores the quickbar filters for a player
local PlayerData = require("modules.exp_legacy.expcore.player_data") --- @dep expcore.player_data
local PlayerWarnings = PlayerData.Required:combine("Warnings")
PlayerWarnings:set_metadata{
stringify = function(value)
if not value then return "You have no warnings" end
local count = 0
for _ in pairs(value) do count = count + 1 end
return "You have " .. count .. " warnings"
end,
}
local Warnings = {
user_warnings = PlayerWarnings,
user_script_warnings = {},
events = {
--- When a warning is added to a player
-- @event on_warning_added
-- @tparam number player_index the index of the player who recived the warning
-- @tparam string by_player_name the name of the player who gave the warning
-- @tparam string reason the reason that the player was given a warning
-- @tparam number warning_count the new number of warnings that the player has
on_warning_added = script.generate_event_name(),
--- When a warning is removed from a player
-- @event on_warning_removed
-- @tparam number player_index the index of the player who is having the warning removed
-- @tparam string warning_by_name the name of the player who gave the warning
-- @tparam string removed_by_name the name of the player who is removing the warning
-- @tparam number warning_count the new number of warnings that the player has
-- @tparam number batch_count the number of warnings removed in this batch, always one when not a batch
-- @tparam number batch the index of this event in a batch, always one when not a batch
on_warning_removed = script.generate_event_name(),
--- When a warning is added to a player, by the script
-- @event on_script_warning_added
-- @tparam number player_index the index of the player who recived the warning
-- @tparam string reason the reason that the player was given a warning
-- @tparam number warning_count the new number of warnings that the player has
on_script_warning_added = script.generate_event_name(),
--- When a warning is removed from a player, by the script
-- @event on_script_warning_removed
-- @tparam number player_index the index of the player who is having the warning removed
-- @tparam number warning_count the new number of warnings that the player has
on_script_warning_removed = script.generate_event_name(),
},
}
local user_script_warnings = Warnings.user_script_warnings
Storage.register(user_script_warnings, function(tbl)
Warnings.user_script_warnings = tbl
user_script_warnings = tbl
end)
--- Gets an array of warnings that the player has, always returns a list even if empty
-- @tparam LuaPlayer player the player to get the warning for
-- @treturn table an array of all the warnings on this player, contains tick, by_player_name and reason
function Warnings.get_warnings(player)
return PlayerWarnings:get(player.name, {})
end
--- Gets the number of warnings that a player has on them
-- @tparam LuaPlayer player the player to count the warnings for
-- @treturn number the number of warnings that the player has
function Warnings.count_warnings(player)
local warnings = PlayerWarnings:get(player.name, {})
return #warnings
end
--- Adds a warning to a player, when a warning is added a set action is done based on the number of warnings and the config file
-- @tparam LuaPlayer player the player to add a warning to
-- @tparam string by_player_name the name of the player who is doing the action
-- @tparam[opt='Non given.'] string reason the reason that the player is being warned
-- @treturn number the number of warnings that the player has
function Warnings.add_warning(player, by_player_name, reason)
player = valid_player(player)
if not player then return end
if not by_player_name then return end
reason = reason or "None given."
local warning_count
PlayerWarnings:update(player.name, function(_, warnings)
local warning = {
by_player_name = by_player_name,
reason = reason,
}
if not warnings then
warning_count = 1
return { warning }
else
table.insert(warnings, warning)
warning_count = #warnings
end
end)
script.raise_event(Warnings.events.on_warning_added, {
name = Warnings.events.on_warning_added,
tick = game.tick,
player_index = player.index,
warning_count = warning_count,
by_player_name = by_player_name,
reason = reason,
})
local action = config.actions[warning_count]
if action then
local _type = type(action)
if _type == "function" then
action(player, by_player_name, warning_count)
elseif _type == "table" then
local current = table.deepcopy(action)
table.insert(current, 2, by_player_name)
table.insert(current, 3, warning_count)
player.print(current)
elseif type(action) == "string" then
player.print(action)
end
end
return warning_count
end
--- Event trigger for removing a waring due to it being looped in clear warnings
-- @tparam LuaPlayer player the player who is having a warning removed
-- @tparam string warning_by_name the name of the player who made the warning
-- @tparam string removed_by_name the name of the player who is doing the action
-- @tparam number warning_count the number of warnings that the player how has
-- @tparam number batch the index of this event in a batch, always one when not a batch
-- @tparam number batch_count the number of reports removed in this batch, always one when not a batch
local function warning_removed_event(player, warning_by_name, removed_by_name, warning_count, batch, batch_count)
script.raise_event(Warnings.events.on_warning_removed, {
name = Warnings.events.on_warning_removed,
tick = game.tick,
player_index = player.index,
warning_count = warning_count,
warning_by_name = warning_by_name,
removed_by_name = removed_by_name,
batch_count = batch_count or 1,
batch = batch or 1,
})
end
--- Removes a warning from a player, always removes the earliest warning, fifo
-- @tparam LuaPlayer player the player to remove a warning from
-- @tparam string by_player_name the name of the player who is doing the action
-- @treturn number the number of warnings that the player has
function Warnings.remove_warning(player, by_player_name)
player = valid_player(player)
if not player then return end
if not by_player_name then return end
local warning, warning_count
PlayerWarnings:update(player.name, function(_, warnings)
if not warnings then return end
warning = table.remove(warnings, 1)
warning_count = #warnings
end)
if not warning then return end
warning_removed_event(player, warning.by_player_name, by_player_name, warning_count)
return warning_count
end
--- Removes all warnings from a player, will trigger remove event for each warning
-- @tparam LuaPlayer player the player to clear the warnings from
-- @tparam string by_player_name the name of the player who is doing the action
-- @treturn boolean true when warnings were cleared succesfully
function Warnings.clear_warnings(player, by_player_name)
player = valid_player(player)
if not player then return end
if not by_player_name then return end
local warnings = PlayerWarnings:get(player)
if not warnings then return end
local warning_count = #warnings
for n, warning in pairs(warnings) do
warning_removed_event(player, warning.by_player_name, by_player_name, warning_count - n, n, warning_count)
end
PlayerWarnings:remove(player)
return true
end
--- Gets an array of all the script warnings that a player has
-- @tparam LuaPlayer player the player to get the script warnings of
-- @treturn table a table of all the script warnings a player has, contains tick and reason
function Warnings.get_script_warnings(player)
return user_script_warnings[player.name] or {}
end
--- Gets the number of script warnings that a player has on them
-- @tparam LuaPlayer player the player to count the script warnings of
-- @treturn number the number of script warnings that the player has
function Warnings.count_script_warnings(player)
local warnings = user_script_warnings[player.name] or {}
return #warnings
end
--- Adds a script warning to a player, this may add a full warning if max script warnings is met
-- @tparam LuaPlayer player the player to add a script warning to
-- @tparam[opt='Non given.'] string reason the reason that the player is being warned
-- @treturn number the number of script warnings that the player has
function Warnings.add_script_warning(player, reason)
player = valid_player(player)
if not player then return end
reason = reason or "Non given."
local warnings = user_script_warnings[player.name]
if not warnings then
warnings = {}
user_script_warnings[player.name] = warnings
end
table.insert(warnings, {
tick = game.tick,
reason = reason,
})
local warning_count = #warnings
script.raise_event(Warnings.events.on_script_warning_added, {
name = Warnings.events.on_script_warning_added,
tick = game.tick,
player_index = player.index,
warning_count = warning_count,
reason = reason,
})
if warning_count > config.script_warning_limit then
Warnings.add_warning(player, "<server>", reason)
end
return warning_count
end
--- Script warning removed event trigger due to it being looped in clear script warnings
-- @tparam LuaPlayer player the player who is having a script warning removed
-- @tparam number warning_count the number of warnings that the player has
local function script_warning_removed_event(player, warning_count)
script.raise_event(Warnings.events.on_script_warning_removed, {
name = Warnings.events.on_script_warning_removed,
tick = game.tick,
player_index = player.index,
warning_count = warning_count,
})
end
--- Removes a script warning from a player
-- @tparam LuaPlayer player the player to remove a script warning from
-- @treturn number the number of script warnings that the player has
function Warnings.remove_script_warning(player)
player = valid_player(player)
if not player then return end
local warnings = user_script_warnings[player.name]
if not warnings then return end
table.remove(warnings, 1)
script_warning_removed_event(player)
return #warnings
end
--- Removes all script warnings from a player, emits event for each warning removed
-- @tparam LuaPlayer player the player to clear the script warnings from
function Warnings.clear_script_warnings(player)
player = valid_player(player)
if not player then return end
local warnings = user_script_warnings[player.name]
if not warnings then return end
local warning_count = #warnings
for n, _ in pairs(warnings) do
script_warning_removed_event(player, warning_count - n)
end
user_script_warnings[player.name] = nil
return true
end
-- script warnings are removed after a certain amount of time to make them even more lienient
local script_warning_cool_down = config.script_warning_cool_down * 3600
Event.on_nth_tick(script_warning_cool_down / 4, function()
local cutoff = game.tick - script_warning_cool_down
for player_name, script_warnings in pairs(user_script_warnings) do
if #script_warnings > 0 then
for _, warning in pairs(script_warnings) do
if warning.tick < cutoff then
Warnings.remove_script_warning(player_name)
end
end
end
end
end)
return Warnings
+1 -1
View File
@@ -182,7 +182,7 @@ function Warps.make_warp_area(warp_id)
}
entity.destructible = false
entity.health = 0
entity.minable = false
entity.minable_flag = false
entity.rotatable = false
-- Save reference of the last power pole
-99
View File
@@ -1,99 +0,0 @@
--[[-- Commands Module - Bonus
- Adds a command that allows players to have increased stats
@data Bonus
]]
local Roles = require("modules.exp_legacy.expcore.roles") --- @dep expcore.roles
local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event
local config = require("modules.exp_legacy.config.bonus") --- @dep config.bonuses
local Commands = require("modules/exp_commands")
-- Stores the bonus for the player
local PlayerData = require("modules.exp_legacy.expcore.player_data") --- @dep expcore.player_data
local PlayerBonus = PlayerData.Settings:combine("Bonus")
PlayerBonus:set_default(0)
PlayerBonus:set_metadata{
permission = "command/bonus",
stringify = function(value)
if not value or value == 0 then
return "None set"
end
return value
end,
}
--- Apply a bonus to a player
local function apply_bonus(player, stage)
if not player.character then
return
end
for k, v in pairs(config.player_bonus) do
player[k] = v.value * stage / 10
if v.combined_bonus then
for i = 1, #v.combined_bonus, 1 do
player[v.combined_bonus[i]] = v.value * stage / 10
end
end
end
end
--- When store is updated apply new bonus to the player
PlayerBonus:on_update(function(player_name, player_bonus)
apply_bonus(game.players[player_name], player_bonus or 0)
end)
--- Changes the amount of bonus you receive
Commands.new("bonus", { "bonus.description" })
:optional("amount", { "bonus.arg-amount" }, Commands.types.integer_range(0, 10))
:register(function(player, amount)
--- @cast amount number?
if amount then
PlayerBonus:set(player, amount)
return Commands.status.success{ "bonus.set", amount }
else
return Commands.status.success{ "bonus.get", PlayerBonus:get(player) }
end
end)
--- When a player respawns re-apply bonus
Event.add(defines.events.on_player_respawned, function(event)
local player = game.players[event.player_index]
apply_bonus(player, PlayerBonus:get(player))
end)
--- Remove bonus if a player no longer has access to the command
local function role_update(event)
local player = game.players[event.player_index]
if not Roles.player_allowed(player, "command/bonus") then
apply_bonus(player, 0)
end
end
--- When a player dies allow them to have instant respawn
Event.add(defines.events.on_player_died, function(event)
local player = game.players[event.player_index]
if Roles.player_has_flag(player, "instant-respawn") then
player.ticks_to_respawn = 120
end
end)
Event.add(defines.events.on_player_created, function(event)
if event.player_index ~= 1 then
return
end
for k, v in pairs(config.force_bonus) do
game.players[event.player_index].force[k] = v.value
end
for k, v in pairs(config.surface_bonus) do
game.players[event.player_index].surface[k] = v.value
end
end)
Event.add(Roles.events.on_role_assigned, role_update)
Event.add(Roles.events.on_role_unassigned, role_update)

Some files were not shown because too many files have changed in this diff Show More