204 Commits
Author SHA1 Message Date
phidias a524e1f128 Merge branch 'explosivegaming:main' into aperx2 2026-09-06 12:59:32 +09: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
phidias fb4690c338 Merge branch 'explosivegaming:main' into aperx2 2026-09-06 01:27:01 +09:00
Cooldude2606 810269971b fix: bump to 7.0.1 2026-09-05 17:14:37 +01:00
phidias 799a7e5ef6 Merge branch 'explosivegaming:main' into aperx2 2026-09-05 01:45:29 +09: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
phidias deb0a12be9 . 2026-09-03 17:07:07 +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
phidias bae09285f6 Merge branch 'explosivegaming:main' into aperx 2026-08-10 20:35:30 +09: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
phidias 6eee2be630 Merge branch 'explosivegaming:main' into aperx 2026-08-07 16:53:56 +09: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
phidias 0cf4a83d0e . 2026-07-23 02:03:27 +09:00
phidias d36d698089 . 2026-07-22 22:46:31 +09:00
phidias fce815bc11 . 2026-07-22 22:01:35 +09:00
phidias b23d57e88f . 2026-07-22 20:14:31 +09:00
phidias f72c7a7deb . 2026-07-21 13:43:53 +09:00
phidias 2cad1386db . 2026-07-21 13:15:42 +09:00
phidias ba5f39a471 . 2026-07-21 13:07:32 +09:00
phidias dddadf2b08 . 2026-07-21 03:28:20 +09:00
phidias 4c4404c0f7 . 2026-07-21 03:27:24 +09:00
phidias ed6d2526a9 . 2026-07-19 13:31:29 +09:00
phidias 3e27d9ed24 Merge branch 'aperx' of https://github.com/PHIDIAS0303/ExpCluster into aperx 2026-07-19 13:22:02 +09:00
phidias ca656d5bd1 .. 2026-07-19 13:21:57 +09:00
phidias da85e3dbdb Merge branch 'explosivegaming:main' into aperx 2026-07-14 16:55:45 +09:00
Cooldude2606 a07be7b7ed Bump version in package jsons 2026-07-14 00:50:00 +01:00
phidias a1716a9aea Merge branch 'explosivegaming:main' into aperx 2026-06-29 17:08:43 +09: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
phidias 889f1cefec Merge branch 'explosivegaming:main' into aperx 2026-06-27 23:32:37 +09:00
Cooldude2606 4fe0e9c0a6 Add AABB.size (#445) 2026-06-27 14:02:49 +01:00
phidias 195b280e1c Merge branch 'explosivegaming:main' into aperx 2026-06-27 00:04:37 +09: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
phidias 4fab391d38 Merge pull request #26 from PHIDIAS0303/main
Main
2026-06-26 22:06:12 +09:00
phidias 595f78f6bd Merge branch 'aperx' into main 2026-06-26 22:05:34 +09: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 d732f5759d Merge pull request #25 from PHIDIAS0303/main
Main
2026-06-26 20:20:34 +09:00
phidias 3b9b160006 Merge branch 'aperx' into main 2026-06-26 20:20:22 +09: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
phidias 5a1c53885e Merge branch 'explosivegaming:main' into aperx 2026-06-26 14:06:50 +09: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 8be8ab0eab Merge pull request #23 from PHIDIAS0303/main
Main
2026-06-25 13:41:44 +09:00
phidias e5cf203a68 Merge branch 'aperx' into main 2026-06-25 13:41:34 +09: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
phidias 34bdd1a2e4 . 2026-06-23 22:46:10 +09:00
phidias ba88f048ac Update addons.cfg 2026-06-23 21:53:56 +09:00
phidias 615bfe74e7 Update addons.cfg 2026-06-23 21:53:45 +09:00
phidias e0b410d067 Update addons.cfg 2026-06-23 21:53:14 +09:00
phidias bd207228ce Update addons.cfg 2026-06-23 21:52:54 +09:00
phidias 28d5ba3b8d Update join_messages.lua 2026-06-23 21:50:57 +09:00
phidias 05b8689c33 Update inventory_clear.lua 2026-06-23 21:50:46 +09:00
phidias 3f310e7073 Update discord_alerts.lua 2026-06-23 21:50:35 +09:00
phidias 4224673b56 Update compilatron.lua 2026-06-23 21:50:10 +09:00
phidias 21cbea0bed Update bonus.lua 2026-06-23 21:45:47 +09:00
phidias 4bfd9c3b36 Update advanced_start.lua 2026-06-23 21:44:43 +09:00
phidias 08328dbb8f Update permission_groups.lua 2026-06-23 21:43:05 +09:00
phidias 7a29c2a5ae Refactor roles and permissions in roles.lua 2026-06-23 20:44:51 +09: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
245 changed files with 12292 additions and 15274 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"
]
}
}
+52 -34
View File
@@ -1,76 +1,94 @@
name: FMTK Lint name: Lua Lint
on: on:
push: push:
pull_request: pull_request:
env:
EMMYLUA_CHECK_VERSION: "0.24.0"
CLUSTERIO: ${{ github.workspace }}/.clusterio
jobs: jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - 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 - uses: pnpm/action-setup@v4
with: with:
version: 10 version: 10
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: lts/* node-version: lts/*
cache: "pnpm" - name: Generate Factorio typedefs
- name: Install FMTK
run: | run: |
pnpm install factoriomod-debug pnpm install factoriomod-debug
pnpm exec fmtk luals-addon pnpm exec fmtk luals-addon "$RUNNER_TEMP"
jq '.["workspace.library"] += ["${{ github.workspace }}/factorio/library"] | .["runtime.plugin"] = "${{ github.workspace }}/factorio/plugin.lua"' .luarc.json > temp.luarc.json - name: Install emmylua_check
jq -s '.[0] * .[1].settings' temp.luarc.json ${{ github.workspace }}/factorio/config.json > check.luarc.json
- name: Install LuaLS
run: | 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 curl -fsSL "https://github.com/EmmyLuaLs/emmylua-analyzer-rust/releases/download/${EMMYLUA_CHECK_VERSION}/emmylua_check-linux-x64.tar.gz" \
mkdir luals && tar -xf lusls.tar.gz -C luals && rm lusls.tar.gz | tar -xz -C "$RUNNER_TEMP"
- name: Run Lint Report - name: Run Lint Report
shell: bash shell: bash
run: | 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 # Credit to https://github.com/Krealle/luals-check-action/blob/main/action.yml
# Although some minor fixes were needed # 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) # 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 --arg root "$PWD/" \
jq -r \ '.[] | (.file | ltrimstr($root)) as $file | .diagnostics[] |
'to_entries | map(.key as $file | .value[] |
{ {
file: $file | sub("file://${{ github.workspace }}/./"; ""), file: $file,
code: .code, code: .code,
line: .range.start.line, line: .range.start.line,
message: (.message | split("\n") message: (.message | split("\n")
| map(if startswith("- ") then .[2:] else . end) | map(if startswith("- ") then .[2:] else . end)
| join("\n")), | join("\n")),
severity: (if .severity <= 1 then "ERR" else "WARN" end) severity: (if .severity <= 1 then "ERR" else "WARN" end)
}) | } |
map("**[\(.severity)] \(.code):** \(.file)#L\(.line)<br><code>\(.message)</code>") | .[]' \ "**[\(.severity)] \(.code):** \(.file)#L\(.line + 1)<br><code>\(.message)</code>"' \
> $GITHUB_STEP_SUMMARY check.json > "$GITHUB_STEP_SUMMARY"
echo "" echo ""
# Github Annotations (limited to 10) # Github Annotations (limited to 10)
cat check.json | \ jq -r --arg root "$PWD/" \
jq -r \ '.[] | (.file | ltrimstr($root)) as $file | .diagnostics[] |
'to_entries | map(.key as $file | .value[] | {
{ file: $file,
file: $file | sub("file://${{ github.workspace }}/./"; ""), title: .code,
title: .code, line: .range.start.line,
line: .range.start.line, endLine: .range.end.line,
endLine: .range.end.line, col: .range.start.character,
col: .range.start.character, endColumn: .range.end.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)
level: (if .severity <= 1 then "error" else "warning" end) } |
}) | "::\(.level) file=\(.file),line=\(.line + 1),endLine=\(.endLine + 1),col=\(.col + 1),endColumn=\(.endColumn + 1),title=\(.title)::\(.message) (\(.title))"' \
map("::\(.level) file=\(.file),line=\(.line+1),endLine=\(.line+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 exit 1
else else
echo "✅ All checks succeeded!" | tee $GITHUB_STEP_SUMMARY echo "✅ All checks succeeded!" | tee "$GITHUB_STEP_SUMMARY"
fi fi
+2
View File
@@ -1,3 +1,5 @@
dist/ dist/
node_modules/ node_modules/
.vscode .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"
}
}
+2
View File
@@ -5,6 +5,8 @@ All are welcome to make bug reports, feature requests, and pull requests for our
For developers wanting to add features please follow these guidelines: For developers wanting to add features please follow these guidelines:
- All lua code is documented using ldoc. - 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. - 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. - 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 and documentation checked.
+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 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 add, allow, deny = Commands.add_permission_authority, Commands.status.success, Commands.status.unauthorised
local authorities = {} local authorities = {}
@@ -28,13 +28,13 @@ end)
--- Allow a player access to system commands, use for debug purposes only --- 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 --- @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) 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 end
--- Remove access from system commands for a player, use for debug purposes only --- 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 --- @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) 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 end
--- Get a list of all players who have system commands unlocked --- 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 --- @param page_size number The number of requests to show per page
--- @return LocalisedString[][], number --- @return LocalisedString[][], number
local function format_as_pages(commands, page_size) local function format_as_pages(commands, page_size)
local pages = { {} } local current_page = {}
local pages = { current_page }
local page_length = 0 local page_length = 0
local current_page = 1
local total = 0 local total = 0
for _, command in pairs(commands) do for _, command in pairs(commands) do
total = total + 1 total = total + 1
page_length = page_length + 1 page_length = page_length + 1
if page_length > page_size then if page_length > page_size then
current_page = current_page + 1
pages[current_page] = {}
page_length = 1 page_length = 1
current_page = {}
pages[#pages + 1] = current_page
end end
local description local description
@@ -48,7 +48,7 @@ local function format_as_pages(commands, page_size)
end end
local aliases = #command.aliases > 0 and { "exp-commands_help.aliases", table.concat(command.aliases, ", ") } or "" 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 end
return pages, total return pages, total
@@ -71,7 +71,7 @@ Commands.new("commands", { "exp-commands_help.description" })
page = as_number page = as_number
end end
keyword = keyword:lower() keyword = tostring(keyword):lower()
local pages, found local pages, found
if cache and cache.keyword == keyword then if cache and cache.keyword == keyword then
-- Cached value found, no search is needed -- 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 Async = require("modules/exp_util/async")
local Storage = require("modules/exp_util/storage") 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_env = {} --- @type table<string, any>
local rcon_static = {} --- @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 }) setmetatable(rcon_env, { __index = rcon_static })
--- Some common static values which can be added now --- Some common static values which can be added now
--- @diagnostic disable: name-style-check
rcon_static.Async = Async rcon_static.Async = Async
rcon_static.ExpUtil = ExpUtil rcon_static.ExpUtil = ExpUtil
rcon_static.Commands = Commands rcon_static.Commands = Commands
rcon_static.print = Commands.print rcon_static.print = Commands.print
--- @diagnostic enable: name-style-check
--- Some common callback values which are useful when a player uses the command --- Some common callback values which are useful when a player uses the command
--- @alias ExpCommand.RconDynamic fun(player: LuaPlayer?): any --- @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 command ExpCommand
--- @cast parameter string --- @cast parameter string
--- @diagnostic disable-next-line: invisible --- @diagnostic disable-next-line: access-invisible
return Commands._event_handler{ return Commands._event_handler{
name = command.name, name = command.name,
tick = game.tick, 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 add, parse = Commands.add_data_type, Commands.parse_input
local valid, invalid = Commands.status.success, Commands.status.invalid_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 --- A boolean value where true is one of: yes, y, true, 1
types.boolean = types.boolean =
+18 -14
View File
@@ -61,7 +61,7 @@ local Search = require("modules/exp_commands/search")
--- @type LuaPlayer? --- @type LuaPlayer?
local _print_player local _print_player
--- @class Commands --- @class (partial) Commands
local Commands = { local Commands = {
color = ExpUtil.color, color = ExpUtil.color,
format_rich_text_color = ExpUtil.format_rich_text_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 --- Contains the different status values a command can return
Commands.status = {} 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 --- Stores all input parsers and validators for different data types
Commands.types = {} Commands.types = {}
@@ -103,7 +103,7 @@ end
--- @class Commands.Argument --- @class Commands.Argument
--- @field name string The name of the argument --- @field name string The name of the argument
--- @field description LocalisedString? The description 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 optional boolean True when the argument is optional
--- @field default any? The default value of the argument --- @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) Commands.error("Command does not support rcon usage, requires LuaPlayer." .. key)
error("Command does not support rcon usage, requires LuaPlayer." .. key) error("Command does not support rcon usage, requires LuaPlayer." .. key)
end, end,
}) --[[ @as LuaPlayer ]] }) --[[@as LuaPlayer]]
--- Status Returns. --- Status Returns.
-- Return values used by command callbacks -- 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) --- @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 --- @return Commands.Status, LocalisedString # Should be returned directly without modification
function Commands.status.success(msg) 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 end
--- Used to signal an error has occurred in a command, data type parser, or permission authority --- 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 --- @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 --- @return Commands.Status, LocalisedString # Should be returned directly without modification
function Commands.status.error(msg) 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 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 --- 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 --- @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 --- @return Commands.Status, LocalisedString # Should be returned directly without modification
function Commands.status.unauthorised(msg) 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 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 --- 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 --- @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 --- @return Commands.Status, LocalisedString # Should be returned directly without modification
function Commands.status.invalid_input(msg) 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 end
--- Used to signal an internal error has occurred, this is reserved for internal use only --- 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 --- @return Commands.Status, LocalisedString # Should be returned directly without modification
--- @package --- @package
function Commands.status.internal_error(msg) 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 end
--- @type table<Commands.Status, string> --- @type table<Commands.Status, string>
@@ -281,7 +285,7 @@ end
--- @alias Commands.InputParserFactory<T> fun(...: any): Commands.InputParser<T> --- @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 --- 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 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 --- @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 --- @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 end
--- Remove an input parser for a data type, must be the same string that was passed to add_input_parser --- 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) function Commands.remove_data_type(data_type)
Commands.types[data_type] = nil Commands.types[data_type] = nil
for k, v in pairs(Commands.types) do 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 --- Add a new required argument to the command of the given data type
--- @param name string The name of the argument being added --- @param name string The name of the argument being added
--- @param description LocalisedString? The description 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 --- @return ExpCommand
function Commands._prototype:argument(name, description, input_parser) function Commands._prototype:argument(name, description, input_parser)
assert_command_mutable(self) assert_command_mutable(self)
@@ -454,7 +458,7 @@ end
--- Add a new optional argument to the command of the given data type --- Add a new optional argument to the command of the given data type
--- @param name string The name of the argument being added --- @param name string The name of the argument being added
--- @param description LocalisedString? The description 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 --- @return ExpCommand
function Commands._prototype:optional(name, description, input_parser) function Commands._prototype:optional(name, description, input_parser)
assert_command_mutable(self) assert_command_mutable(self)
+3 -2
View File
@@ -3,7 +3,7 @@ local Search = {}
local Storage = require("modules/exp_util/storage") local Storage = require("modules/exp_util/storage")
--- Setup the storage to contain the pending translations and the completed ones --- 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>> local translations = {} --- @type table<string, table<string, string>>
Storage.register({ Storage.register({
pending, pending,
@@ -50,6 +50,7 @@ function Search.prepare(custom_commands)
name = name, name = name,
description = locale_desc, description = locale_desc,
help_text = locale_desc, help_text = locale_desc,
usage = locale_desc,
aliases = {}, aliases = {},
} }
end end
@@ -65,7 +66,7 @@ function Search.on_player_locale_changed(event)
local ids = player.request_translations(required_translations) local ids = player.request_translations(required_translations)
assert(ids, "Translation ids was nil") assert(ids, "Translation ids was nil")
for i, command_name in ipairs(command_names) do 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 end
end end
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@expcluster/lib_commands", "name": "@expcluster/lib_commands",
"version": "0.1.0", "version": "7.0.1",
"description": "Clusterio plugin providing a Lua command processing library.", "description": "Clusterio plugin providing a Lua command processing library.",
"author": "Cooldude2606 <https://github.com/Cooldude2606>", "author": "Cooldude2606 <https://github.com/Cooldude2606>",
"license": "MIT", "license": "MIT",
@@ -13,10 +13,10 @@
"node": ">=18" "node": ">=18"
}, },
"peerDependencies": { "peerDependencies": {
"@clusterio/lib": "catalog:" "@clusterio/lib": "workspace:^"
}, },
"devDependencies": { "devDependencies": {
"@clusterio/lib": "catalog:", "@clusterio/lib": "workspace:^",
"@types/node": "catalog:", "@types/node": "catalog:",
"typescript": "catalog:", "typescript": "catalog:",
"webpack": "catalog:", "webpack": "catalog:",
+410 -258
View File
@@ -1,282 +1,434 @@
import { BaseControllerPlugin } from "@clusterio/controller";
import * as lib from "@clusterio/lib"; import * as lib from "@clusterio/lib";
import { BaseControllerPlugin, InstanceInfo } from "@clusterio/controller"; import * as messages from "./messages";
import * as path from "node:path";
import {
PermissionStrings, PermissionStringsUpdate,
PermissionGroup, PermissionGroupUpdate,
InstancePermissionGroups,
PermissionInstanceId,
PermissionGroupEditEvent,
} from "./messages";
import path from "path";
import fs from "fs-extra";
export class ControllerPlugin extends BaseControllerPlugin { export class ControllerPlugin extends BaseControllerPlugin {
static permissionGroupsPath = "exp_groups.json"; groups!: lib.SubscribableDatastore<messages.GroupRecord>;
static userGroupsPath = "exp_user_groups.json"; roleMappings!: lib.SubscribableDatastore<messages.RoleMappingRecord>;
manualAssignments!: lib.SubscribableDatastore<messages.AssignmentRecord>;
resolvedAssignments!: lib.SubscribableDatastore<messages.AssignmentRecord>;
userToGroup: Map<lib.User["id"], PermissionGroup> = new Map(); // TODO this needs to be per instance async init() {
permissionStrings!: Map<PermissionStrings["id"], PermissionStrings>; const databaseDirectory = this.controller.config.get("controller.database_directory");
permissionGroups!: Map<InstancePermissionGroups["id"], InstancePermissionGroups>;
async init() { this.groups = new lib.SubscribableDatastore(
this.controller.handle(PermissionStringsUpdate, this.handlePermissionStringsUpdate.bind(this)); ...await new lib.JsonIdDatastoreProvider(
this.controller.handle(PermissionGroupUpdate, this.handlePermissionGroupUpdate.bind(this)); path.join(databaseDirectory, "exp_groups", "groups.json"),
this.controller.handle(PermissionGroupEditEvent, this.handlePermissionGroupEditEvent.bind(this)); messages.GroupRecord.fromJSON.bind(messages.GroupRecord),
this.controller.subscriptions.handle(PermissionStringsUpdate, this.handlePermissionStringsSubscription.bind(this)); ).bootstrap()
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();
// Add the default group if missing and add any missing cluster roles this.roleMappings = new lib.SubscribableDatastore(
const clusterRoles = [...this.controller.userManager.roles.values()] ...await new lib.JsonIdDatastoreProvider(
for (const instanceGroups of this.permissionGroups.values()) { path.join(databaseDirectory, "exp_groups", "role_mappings.json"),
const groups = instanceGroups.groups; messages.RoleMappingRecord.fromJSON.bind(messages.RoleMappingRecord),
const instanceRoles = [...groups.values()].flatMap(group => [...group.roleIds.values()]); ).bootstrap()
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)
}
} else {
groups.set("Default", new PermissionGroup(
instanceGroups.instanceId,
"Default",
groups.size,
new Set(missingRoles.map(role => role.id))
));
}
}
}
async onControllerConfigFieldChanged(field: string, curr: unknown, prev: unknown) { this.manualAssignments = new lib.SubscribableDatastore(
if (field === "exp_groups.allow_role_inconsistency") { ...await new lib.JsonIdDatastoreProvider(
// Do something with this.userToGroup path.join(databaseDirectory, "exp_groups", "assignments.json"),
} messages.AssignmentRecord.fromJSON.bind(messages.AssignmentRecord),
} ).bootstrap()
);
async onInstanceConfigFieldChanged(instance: InstanceInfo, field: string, curr: unknown, prev: unknown) { this.resolvedAssignments = new lib.SubscribableDatastore();
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);
}
}
} 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));
}
}
}
async loadPermissionGroups() { this.controller.subscriptions.handle(messages.GroupUpdatedEvent, this.handleGroupSubscription.bind(this));
const file = path.resolve(this.controller.config.get("controller.database_directory"), ControllerPlugin.permissionGroupsPath); this.controller.subscriptions.handle(messages.RoleMappingUpdatedEvent, this.handleRoleMappingSubscription.bind(this));
this.logger.verbose(`Loading ${file}`); this.controller.subscriptions.handle(messages.ManualAssignmentUpdatedEvent, this.handleManualAssignmentSubscription.bind(this));
try { this.controller.subscriptions.handle(messages.ResolvedAssignmentUpdatedEvent, this.handleResolvedAssignmentSubscription.bind(this));
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]]))
);
}
};
} catch (err: any) { this.groups.on("update", this.groupsUpdated.bind(this));
if (err.code === "ENOENT") { this.roleMappings.on("update", this.roleMappingsUpdated.bind(this));
this.logger.verbose("Creating new permission group database"); this.manualAssignments.on("update", this.manualAssignmentsUpdated.bind(this));
return; this.resolvedAssignments.on("update", this.resolvedAssignmentsUpdated.bind(this));
}
throw err;
}
}
async savePermissionGroups() { this.controller.handle(messages.GroupCreateRequest, this.handleGroupCreateRequest.bind(this));
const file = path.resolve(this.controller.config.get("controller.database_directory"), ControllerPlugin.permissionGroupsPath); this.controller.handle(messages.GroupUpdateRequest, this.handleGroupUpdateRequest.bind(this));
this.logger.verbose(`Writing ${file}`); this.controller.handle(messages.GroupDeleteRequest, this.handleGroupDeleteRequest.bind(this));
await lib.safeOutputFile(file, JSON.stringify( this.controller.handle(messages.GroupGetRequest, this.handleGroupGetRequest.bind(this));
[...this.permissionGroups.values()].flatMap(instanceGroups => [...instanceGroups.groups.values()]) this.controller.handle(messages.GroupListRequest, this.handleGroupListRequest.bind(this));
));
}
async loadUserGroups() { this.controller.handle(messages.AssignmentCreateRequest, this.handleAssignmentCreateRequest.bind(this));
if (!this.controller.config.get("exp_groups.allow_role_inconsistency")) return; this.controller.handle(messages.AssignmentUpdateRequest, this.handleAssignmentUpdateRequest.bind(this));
const file = path.resolve(this.controller.config.get("controller.database_directory"), ControllerPlugin.userGroupsPath); this.controller.handle(messages.AssignmentDeleteRequest, this.handleAssignmentDeleteRequest.bind(this));
this.logger.verbose(`Loading ${file}`); this.controller.handle(messages.AssignmentGetRequest, this.handleAssignmentGetRequest.bind(this));
try { this.controller.handle(messages.AssignmentListRequest, this.handleAssignmentListRequest.bind(this));
const content = await fs.readFile(file, { encoding: "utf8" });
this.userToGroup = new Map(JSON.parse(content));
} catch (err: any) { this.controller.handle(messages.RoleMappingCreateRequest, this.handleRoleMappingCreateRequest.bind(this));
if (err.code === "ENOENT") { this.controller.handle(messages.RoleMappingUpdateRequest, this.handleRoleMappingUpdateRequest.bind(this));
this.logger.verbose("Creating new user group database"); this.controller.handle(messages.RoleMappingDeleteRequest, this.handleRoleMappingDeleteRequest.bind(this));
return; this.controller.handle(messages.RoleMappingGetRequest, this.handleRoleMappingGetRequest.bind(this));
} this.controller.handle(messages.RoleMappingListRequest, this.handleRoleMappingListRequest.bind(this));
throw err; }
}
}
async saveUserGroups() { async onShutdown() {
if (!this.controller.config.get("exp_groups.allow_role_inconsistency")) return; await Promise.all([
const file = path.resolve(this.controller.config.get("controller.database_directory"), ControllerPlugin.userGroupsPath); this.groups.save(),
this.logger.verbose(`Writing ${file}`); this.manualAssignments.save(),
await lib.safeOutputFile(file, JSON.stringify([...this.permissionGroups.entries()])); this.roleMappings.save(),
} ])
}
async loadData() { /*
await Promise.all([ Subscriptions
this.loadPermissionGroups(), */
this.loadUserGroups(),
])
}
async onSaveData() { async groupsUpdated(groups: messages.GroupRecord[]) {
await Promise.all([ this.controller.subscriptions.broadcast(new messages.GroupUpdatedEvent(groups));
this.savePermissionGroups(),
this.saveUserGroups(),
])
}
addPermisisonGroup(instanceId: PermissionInstanceId, name: string, permissions = new Set<string>(), silent = false) { // We need to do extra work if the group was deleted
const instanceGroups = this.permissionGroups.get(instanceId); const deletedGroupIds = groups.filter(g => g.isDeleted).map(g => g.id);
if (!instanceGroups) { if (!deletedGroupIds.length) {
throw new Error("Instance ID does not exist"); return;
} }
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]));
}
return group;
}
removePermissionGroup(instanceId: PermissionInstanceId, name: string, silent = false) { // Cascade the delete down to affected role mappings
const instanceGroups = this.permissionGroups.get(instanceId); const mappingsToDelete = [];
if (!instanceGroups) { for (const mapping of this.roleMappings.values()) {
throw new Error("Instance ID does not exist"); if (deletedGroupIds.includes(mapping.groupId)) {
} mappingsToDelete.push(mapping);
const group = instanceGroups.groups.get(name) }
if (!group) { }
return null; if (mappingsToDelete.length) {
} this.roleMappings.deleteMany(mappingsToDelete);
for (const nextGroup of instanceGroups.groups.values()) { }
if (nextGroup.order > group.order) {
nextGroup.order -= 1;
}
}
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) { // Cascade the delete down to affected manual assignments
// TODO 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);
}
async handlePermissionStringsUpdate(event: PermissionStringsUpdate) { // Find all the affected players who were assigned to this group
for (const update of event.updates) { for (const resolved of this.resolvedAssignments.values()) {
const global = this.permissionStrings.get("Global")! if (deletedGroupIds.includes(resolved.groupId)) {
this.permissionStrings.set(update.instanceId as number, update) affectedPlayers.add(resolved.name);
global.updatedAtMs = Math.max(global.updatedAtMs, update.updatedAtMs) }
for (const permission of update.permissions) { }
global.permissions.add(permission) if (affectedPlayers.size) {
} this.resolvedAssignments.setMany(await this.computeResolvedAssignments([...affectedPlayers]));
// TODO maybe check if changes have happened rather than always pushing updates }
this.controller.subscriptions.broadcast(new PermissionStringsUpdate([global, update])) }
}
}
async handlePermissionGroupUpdate(event: PermissionGroupUpdate) { async handleGroupSubscription(request: lib.SubscriptionRequest) {
const updates = []; const groups = [...this.groups.values()]
for (const group of event.updates) { .filter(group => group.updatedAtMs > request.lastRequestTimeMs);
const groups = this.permissionGroups.get(group.instanceId); return groups.length ? new messages.GroupUpdatedEvent(groups) : null;
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 (update) updates.push(update);
}
this.controller.subscriptions.broadcast(new PermissionGroupUpdate(updates));
}
async handlePermissionStringsSubscription(request: lib.SubscriptionRequest, src: lib.Address) { async roleMappingsUpdated(roleMappings: messages.RoleMappingRecord[]) {
const updates = [ ...this.permissionStrings.values() ] this.controller.subscriptions.broadcast(new messages.RoleMappingUpdatedEvent(roleMappings));
.filter(
value => value.updatedAtMs > request.lastRequestTimeMs,
)
return updates.length ? new PermissionStringsUpdate(updates) : null;
}
async handlePermissionGroupSubscription(request: lib.SubscriptionRequest, src: lib.Address) { // Mappings pointing to a deleted group have already been handled
const updates = [ ...this.permissionGroups.values() ] // But if any are active, then we still must recompute all assignments
.flatMap(instanceGroups => [...instanceGroups.groups.values()]) let hasActiveGroup = false;
.filter( for (const roleMapping of roleMappings) {
value => value.updatedAtMs > request.lastRequestTimeMs, const group = this.groups.get(roleMapping.groupId);
) if (group && !group.isDeleted) {
if (src.type === lib.Address.instance) { hasActiveGroup = true;
const instanceUpdates = updates.filter(group => group.instanceId === src.id || group.instanceId === "Global"); break;
this.logger.info(JSON.stringify(updates)) }
this.logger.info(JSON.stringify(instanceUpdates)) }
return instanceUpdates.length ? new PermissionGroupUpdate(instanceUpdates) : null; if (!hasActiveGroup) {
} return;
return updates.length ? new PermissionGroupUpdate(updates) : null; }
}
// 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 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);
}
if (affectedPlayers.length) {
this.resolvedAssignments.setMany(await this.computeResolvedAssignments(affectedPlayers));
}
}
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),
);
}
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));
}
}
// 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;
}
/*
Groups
*/
async handleGroupListRequest() {
return [...this.groups.values()];
}
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`);
}
let id = Math.random() * 2**31 | 0;
while (this.groups.has(id)) {
id = Math.random() * 2**31 | 0;
}
const group = new messages.GroupRecord(id, request.name, request.permissions);
this.groups.set(group);
return group;
}
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`);
}
this.groups.set(group);
}
async handleGroupDeleteRequest(request: messages.GroupDeleteRequest) {
const { groupId } = request;
const group = this.groups.getMutable(groupId);
if (!group) {
throw new lib.RequestError(`Group with ID ${groupId} does not exist`);
}
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`);
}
return group;
}
/*
Groups
*/
async handleAssignmentListRequest() {
return [...this.manualAssignments.values()];
}
async handleAssignmentCreateRequest(request: messages.AssignmentCreateRequest) {
const { name, groupId } = request;
if (this.manualAssignments.has(name)) {
throw new lib.RequestError(`Assignment for '${name}' already exists`);
}
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;
}
}
if (!matches) continue;
if (!best || mapping.priority > best.priority) {
best = mapping;
}
}
if (best) {
return new messages.AssignmentRecord(playerName, best.groupId);
}
// 3) Default (deleted assignment, assigns to 'Default' in game)
return new messages.AssignmentRecord(playerName, 0, 0, true);
}
async computeResolvedAssignments(playerNames: string[]): Promise<messages.AssignmentRecord[]> {
return Promise.all(playerNames.map(name => this.computeResolvedAssignment(name)));
}
} }
+163 -64
View File
@@ -1,84 +1,183 @@
import * as lib from "@clusterio/lib"; import * as lib from "@clusterio/lib";
import * as Messages from "./messages"; import * as messages from "./messages";
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.",
});
declare module "@clusterio/lib" { declare module "@clusterio/lib" {
export interface ControllerConfigFields {
"exp_groups.allow_role_inconsistency": boolean;
}
export interface InstanceConfigFields { export interface InstanceConfigFields {
"exp_groups.sync_permission_groups": boolean; "exp_groups.sync_mode": "enabled" | "disabled" | "bidirectional"
}
export interface ControllerConfigFields {
} }
} }
// Group permissions
lib.definePermission({
name: "exp_groups.group.get",
title: "Get Groups",
description: "Retrieve a specific Factorio permission group by ID.",
grantByDefault: true,
});
lib.definePermission({
name: "exp_groups.group.list",
title: "List Groups",
description: "List all Factorio permission groups.",
grantByDefault: true,
});
lib.definePermission({
name: "exp_groups.group.subscribe",
title: "Subscribe to Group Updates",
description: "Receive updates when Factorio permission groups change.",
grantByDefault: true,
});
lib.definePermission({
name: "exp_groups.group.create",
title: "Create Groups",
description: "Create new Factorio permission groups.",
grantByDefault: false,
});
lib.definePermission({
name: "exp_groups.group.update",
title: "Update Groups",
description: "Modify existing Factorio permission groups.",
grantByDefault: false,
});
lib.definePermission({
name: "exp_groups.group.delete",
title: "Delete Groups",
description: "Delete Factorio permission groups.",
grantByDefault: false,
});
// Assignment permissions
lib.definePermission({
name: "exp_groups.assignment.get",
title: "Get Assignments",
description: "Retrieve a specific manual group assignment for a player.",
grantByDefault: true,
});
lib.definePermission({
name: "exp_groups.assignment.list",
title: "List Assignments",
description: "List all manual group assignments.",
grantByDefault: true,
});
lib.definePermission({
name: "exp_groups.assignment.subscribe",
title: "Subscribe to Assignment Updates",
description: "Receive updates when manual group assignments change.",
grantByDefault: true,
});
lib.definePermission({
name: "exp_groups.assignment.create",
title: "Create Assignments",
description: "Manually assign players to groups, overriding role mappings.",
grantByDefault: false,
});
lib.definePermission({
name: "exp_groups.assignment.update",
title: "Update Assignments",
description: "Modify existing manual group assignments.",
grantByDefault: false,
});
lib.definePermission({
name: "exp_groups.assignment.delete",
title: "Delete Assignments",
description: "Remove manual group assignments.",
grantByDefault: false,
});
// Role mapping permissions
lib.definePermission({
name: "exp_groups.role_mapping.get",
title: "Get Role Mappings",
description: "Retrieve a specific role mapping rule.",
grantByDefault: true,
});
lib.definePermission({
name: "exp_groups.role_mapping.list",
title: "List Role Mappings",
description: "List all role mapping rules.",
grantByDefault: true,
});
lib.definePermission({
name: "exp_groups.role_mapping.subscribe",
title: "Subscribe to Role Mapping Updates",
description: "Receive updates when role mapping rules change.",
grantByDefault: true,
});
lib.definePermission({
name: "exp_groups.role_mapping.create",
title: "Create Role Mappings",
description: "Create rules that map user roles to Factorio permission groups.",
grantByDefault: false,
});
lib.definePermission({
name: "exp_groups.role_mapping.update",
title: "Update Role Mappings",
description: "Modify existing role mapping rules.",
grantByDefault: false,
});
lib.definePermission({
name: "exp_groups.role_mapping.delete",
title: "Delete Role Mappings",
description: "Delete role mapping rules.",
grantByDefault: false,
});
export const plugin: lib.PluginDeclaration = { export const plugin: lib.PluginDeclaration = {
name: "exp_groups", name: "exp_groups",
title: "exp_groups", title: "ExpGaming - Permission Groups",
description: "Create, modify, and link factorio permission groups to clusterio user roles.", description: "Clusterio plugin providing syncing of permission groups",
controllerEntrypoint: "./dist/node/controller", features: [
controllerConfigFields: { "SavePatching",
"exp_groups.allow_role_inconsistency": { "ScriptCommands",
title: "Allow User Role Inconsistency", ],
description: "When true, users can be assgined to any group regardless of their roles",
type: "boolean", messages: [
initialValue: false, 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,
],
instanceEntrypoint: "./dist/node/instance", instanceEntrypoint: "./dist/node/instance",
instanceConfigFields: { instanceConfigFields: {
"exp_groups.sync_permission_groups": { "exp_groups.sync_mode": {
title: "Sync Permission Groups", description: "Synchronize permission groups with the controller",
description: "When true, the instance cannot deviate from the global group settings and will be hidden from the sellection dropdown.", type: "string",
type: "boolean", enum: ["disabled", "enabled", "bidirectional"],
initialValue: true, initialValue: "bidirectional",
}, },
}, },
messages: [ controllerEntrypoint: "./dist/node/controller",
Messages.PermissionGroupEditEvent, controllerConfigFields: {
Messages.PermissionStringsUpdate, },
Messages.PermissionGroupUpdate,
],
webEntrypoint: "./web", webEntrypoint: "./web",
routes: [ routes: [
"/exp_groups", "/permission_groups",
], "/permission_groups/:id/view",
]
}; };
+152 -110
View File
@@ -1,129 +1,171 @@
import * as lib from "@clusterio/lib";
import { BaseInstancePlugin } from "@clusterio/host"; import { BaseInstancePlugin } from "@clusterio/host";
import { import * as lib from "@clusterio/lib";
PermissionGroup, PermissionGroupEditEvent, PermissionGroupEditType, import * as messages from "./messages";
PermissionGroupUpdate, PermissionInstanceId, PermissionStrings, PermissionStringsUpdate
} from "./messages";
const rconBase = "/sc local Groups = package.loaded['modules/exp_groups/module_exports'];" export type IpcGroupUpdated = {
group_name: string,
type EditIPC = { group_id: number | undefined,
type: PermissionGroupEditType, permissions: { is_blacklist: boolean, permissions: string[] | undefined },
changes: string[],
group: string,
}; };
type CreateIPC = { export type IpcGroupDeleted = {
group: string, group_name: string,
defiantion: [boolean, string[] | {}] group_id: number | undefined,
} };
type DeleteIPC = { export type IpcPlayerAssignments = {
group: string, assignments: Record<string, number>,
} };
export class InstancePlugin extends BaseInstancePlugin { export class InstancePlugin extends BaseInstancePlugin {
permissions: Set<string> = new Set(); // Once only, don't send permissions for these groups
permissionGroups = new lib.EventSubscriber(PermissionGroupUpdate, this.instance); // This is used for groups created on this instance that only need the controller generated id
permissionGroupUpdates = new lib.EventSubscriber(PermissionGroupEditEvent, this.instance); skipSendingPermissions = new Set<string>();
syncId: PermissionInstanceId = this.instance.config.get("exp_groups.sync_permission_groups") ? "Global" : this.instance.id; // 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() { async init() {
this.instance.server.handle("exp_groups-permission_group_edit", this.handleEditIPC.bind(this)); this.instance.handle(messages.GroupUpdatedEvent, this.handleGroupUpdatedEvent.bind(this));
this.instance.server.handle("exp_groups-permission_group_create", this.handleCreateIPC.bind(this)); this.instance.handle(messages.ResolvedAssignmentUpdatedEvent, this.handleResolvedAssignmentUpdatedEvent.bind(this));
this.instance.server.handle("exp_groups-permission_group_delete", this.handleDeleteIPC.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 onStart() { async onInstanceConfigFieldChanged(field: string, curr: unknown, prev: unknown) {
// Send the most recent version of the permission string switch(field) {
const permissionsString = await this.sendRcon(rconBase + "rcon.print(Groups.get_actions_json())"); case "exp_groups.sync_mode":
this.permissions = new Set(JSON.parse(permissionsString)); await this.luaSetEmitEvents(curr == "bidirectional")
this.instance.sendTo("controller", new PermissionStringsUpdate([ break;
new PermissionStrings(this.instance.id, this.permissions, Date.now()) }
])); }
// Subscribe to get updates for permission groups async onStart() {
this.permissionGroups.subscribe(this.onPermissionGroupsUpdate.bind(this)); // We use Date.now() because we need to manually initialise the groups on the lua side
this.permissionGroupUpdates.subscribe(this.onPermissionGroupUpdate.bind(this)); 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 onControllerConnectionEvent(event: any) { async onPlayerEvent(event: lib.PlayerEvent) {
this.permissionGroups.handleConnectionEvent(event); 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 onInstanceConfigFieldChanged(field: string, curr: unknown, prev: unknown) { async handleGroupUpdatedEvent(event: messages.GroupUpdatedEvent) {
if (field === "exp_groups.sync_permission_groups") { for (const group of event.updates) {
this.syncId = curr ? "Global" : this.instance.id; await this.luaSendGroupUpdate(group);
const [snapshot, synced] = this.permissionGroups.getSnapshot(); }
if (synced && this.instance.status !== "running") await this.syncPermissionGroups(snapshot.values()); }
}
}
async onPermissionGroupsUpdate(event: PermissionGroupUpdate | null, synced: boolean) { async handleResolvedAssignmentUpdatedEvent(event: messages.ManualAssignmentUpdatedEvent) {
if (!synced || this.instance.status !== "running" || !event?.updates.length) return; for (const assignment of event.updates) {
await this.syncPermissionGroups(event.updates); if (this.onlinePlayers.has(assignment.name)) {
} await this.luaSendAssignmentUpdate(assignment);
}
}
}
async syncPermissionGroups(groups: Iterable<PermissionGroup>) { async handleGroupUpdatedIPC(event: IpcGroupUpdated) {
const updateCommands = [rconBase]; const permissions = new messages.GroupPermissions(
for (const group of groups) { event.permissions.is_blacklist,
if (group.instanceId === this.syncId && group.updatedAtMs > (this.permissionGroups.values.get(group.id)?.updatedAtMs ?? 0)) { event.permissions.permissions ?? [],
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()]])}')`);
} 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 (event.group_id === undefined) {
if (!synced || this.instance.status !== "running" || !event) return; this.skipSendingPermissions.add(event.group_name);
if (event.src.equals(lib.Address.fromShorthand({ instanceId: this.instance.id }))) return; await this.instance.sendTo("controller",
const getCmd = `Groups.get_or_create('${event.group}')`; new messages.GroupCreateRequest(event.group_name, permissions),
if (event.type === "add_permissions") { );
await this.sendRcon(rconBase + getCmd + `:allow_actions(Groups.json_to_actions('${JSON.stringify(event.changes)}'))`); } else {
} else if (event.type === "remove_permissions") { this.skipSendingUpdate.add(event.group_id);
await this.sendRcon(rconBase + getCmd + `:disallow_actions(Groups.json_to_actions('${JSON.stringify(event.changes)}'))`); await this.instance.sendTo("controller", new messages.GroupUpdateRequest(
} else if (event.type === "assign_players") { new messages.GroupRecord(event.group_id, event.group_name, permissions),
await this.sendRcon(rconBase + getCmd + `:add_players(game.json_to_table('${JSON.stringify(event.changes)}'))`); ));
} }
} }
async handleEditIPC(event: EditIPC) { async handleGroupDeletedIPC(event: IpcGroupDeleted) {
this.logger.info(JSON.stringify(event)) if (event.group_id === undefined) {
this.instance.sendTo("controller", new PermissionGroupEditEvent( return;
lib.Address.fromShorthand({ instanceId: this.instance.id }), }
event.type, event.group, event.changes this.skipSendingUpdate.add(event.group_id);
)) await this.instance.sendTo("controller", new messages.GroupDeleteRequest(event.group_id));
} }
async handleCreateIPC(event: CreateIPC) { async handlePlayerAssignmentsIPC(event: IpcPlayerAssignments) {
this.logger.info(JSON.stringify(event)) await Promise.all(
if (!this.permissionGroups.synced) return; Object.entries(event.assignments).map(([playerName, groupId]) =>
let [defaultAllow, permissionsRaw] = event.defiantion; this.instance.sendTo("controller",
if (!Array.isArray(permissionsRaw)) { new messages.AssignmentUpdateRequest(new messages.AssignmentRecord(playerName, groupId)),
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 handleDeleteIPC(event: DeleteIPC) { async subscribePlayerAssignment(playerName: string) {
if (!this.permissionGroups.synced) return; await this.instance.sendTo("controller", new lib.SubscriptionRequest(
const group = [...this.permissionGroups.values.values()] `exp_groups:${messages.ResolvedAssignmentUpdatedEvent.name}`, "subscribe", 0, lib.SubscriptionFilters.fromShorthand(playerName)
.find(group => group.instanceId === this.syncId && group.name === event.group); ));
if (group) { }
group.updatedAtMs = Date.now();
group.isDeleted = true; async unsubscribePlayerAssignment(playerName: string) {
this.instance.sendTo("controller", new PermissionGroupUpdate([ group ])); 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)
}
} }
+655 -202
View File
@@ -1,239 +1,692 @@
import { User, InstanceDetails, IControllerUser, Link, MessageRequest, StringEnum, PermissionError, Address } from "@clusterio/lib"; import * as lib from "@clusterio/lib";
import { Type, Static } from "@sinclair/typebox"; import { Type, Static } from "@sinclair/typebox";
export const PermissionInstanceIdSchema = Type.Union([InstanceDetails.jsonSchema.properties.id, Type.Literal("Global")]) /*
export type PermissionInstanceId = InstanceDetails["id"] | "Global" Data records
export type GamePermission = string; // todo: maybe enum this? */
/** export class GroupPermissions {
* Data class for permission groups constructor(
*/ public isBlacklist: boolean,
export class PermissionGroup { public permissions: string[],
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,
) {
}
static jsonSchema = Type.Object({ static jsonSchema = Type.Object({
instanceId: PermissionInstanceIdSchema, is_blacklist: Type.Boolean(),
name: Type.String(), permissions: Type.Array(Type.String()),
order: Type.Number(), });
roleIds: Type.Array(Type.Number()),
permissions: Type.Array(Type.String()),
updatedAtMs: Type.Optional(Type.Number()),
isDeleted: Type.Optional(Type.Boolean()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) { toJSON(): Static<typeof GroupPermissions.jsonSchema> {
return new this( return {
json.instanceId, is_blacklist: this.isBlacklist,
json.name, permissions: this.permissions,
json.order, };
new Set(json.roleIds), }
new Set(json.permissions),
json.updatedAtMs,
json.isDeleted
);
}
toJSON(): Static<typeof PermissionGroup.jsonSchema> { static fromJSON(json: Static<typeof this.jsonSchema>) {
return { return new this(
instanceId: this.instanceId, json.is_blacklist,
name: this.name, json.permissions,
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 { export class GroupRecord {
constructor( constructor(
public instanceId: PermissionInstanceId, public id: number,
public groups: Map<PermissionGroup["name"], PermissionGroup> = new Map(), public name: string,
) { public permissions: GroupPermissions,
} public updatedAtMs: number = 0,
public isDeleted: boolean = false,
) {}
static jsonSchema = Type.Object({ static jsonSchema = Type.Object({
instanceId: PermissionInstanceIdSchema, id: Type.Integer(),
permissionsGroups: Type.Array(PermissionGroup.jsonSchema), name: Type.String(),
}); permissions: GroupPermissions.jsonSchema,
updated_at_ms: Type.Optional(Type.Number()),
is_deleted: Type.Optional(Type.Boolean()),
});
static fromJSON(json: Static<typeof InstancePermissionGroups.jsonSchema>) { toJSON() {
return new InstancePermissionGroups( let json: Static<typeof GroupRecord.jsonSchema> = {
json.instanceId, id: this.id,
new Map(json.permissionsGroups.map(group => [group.name, PermissionGroup.fromJSON(group)])), name: this.name,
); permissions: this.permissions.toJSON(),
} };
toJSON() { if (this.updatedAtMs) {
return { json.updated_at_ms = this.updatedAtMs;
instanceId: this.instanceId, }
permissionsGroups: [...this.groups.values()],
}
}
getUserGroup(user: User) { if (this.isDeleted) {
const groups = [...user.roleIds.values()].map(roleId => json.is_deleted = true;
// 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 json;
return this.instanceId; }
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(
json.id,
json.name,
GroupPermissions.fromJSON(json.permissions),
json.updated_at_ms ?? 0,
json.is_deleted ?? false,
);
}
} }
export class PermissionGroupUpdate { export class AssignmentRecord {
declare ["constructor"]: typeof PermissionGroupUpdate; constructor(
static type = "event" as const; public name: string,
static src = ["controller", "instance"] as const; public groupId: number,
static dst = ["control", "instance", "controller"] as const; public updatedAtMs: number = 0,
static plugin = "exp_groups" as const; public isDeleted: boolean = false,
static permission = "exp_groups.list.subscribe"; ) {}
constructor( get id() {
public updates: PermissionGroup[], return this.name;
) { } }
static jsonSchema = Type.Object({ static jsonSchema = Type.Object({
"updates": Type.Array(PermissionGroup.jsonSchema), name: Type.String(),
}); group_id: Type.Integer(),
updated_at_ms: Type.Optional(Type.Number()),
is_deleted: Type.Optional(Type.Boolean()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) { toJSON(): Static<typeof AssignmentRecord.jsonSchema> {
return new this( let json: Static<typeof AssignmentRecord.jsonSchema> = {
json.updates.map(update => PermissionGroup.fromJSON(update)) 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 type PermissionGroupEditType = "assign_players" | "add_permissions" | "remove_permissions"; 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,
) {}
export class PermissionGroupEditEvent { static jsonSchema = Type.Object({
declare ["constructor"]: typeof PermissionGroupEditEvent; id: Type.Integer(),
static type = "event" as const; role_ids: Type.Array(Type.Integer()),
static src = ["instance", "controller"] as const; group_id: Type.Integer(),
static dst = ["control", "instance", "controller"] as const; priority: Type.Number(),
static plugin = "exp_groups" as const; enabled: Type.Boolean(),
updated_at_ms: Type.Optional(Type.Number()),
is_deleted: Type.Optional(Type.Boolean()),
});
static permission(user: IControllerUser, message: MessageRequest) { toJSON(): Static<typeof RoleMappingRecord.jsonSchema> {
if (typeof message.data === "object" && message.data !== null) { let json: Static<typeof RoleMappingRecord.jsonSchema> = {
const data = message.data as Static<typeof PermissionGroupEditEvent.jsonSchema>; id: this.id,
if (data.type === "add_permissions" || data.type === "remove_permissions") { role_ids: [...this.roleIds],
user.checkPermission("exp_groups.modify_permissions") group_id: this.groupId,
} else if (data.type === "assign_players") { priority: this.priority,
user.checkPermission("exp_groups.assign_players") enabled: this.enabled,
} else { };
throw new PermissionError("Permission denied");
}
};
}
constructor( if (this.updatedAtMs) {
public src: Address, json.updated_at_ms = this.updatedAtMs;
public type: PermissionGroupEditType, }
public group: string,
public changes: String[],
) { }
static jsonSchema = Type.Object({ if (this.isDeleted) {
"src": Address.jsonSchema, json.is_deleted = true;
"type": StringEnum(["assign_players", "add_permissions", "remove_permissions"]), }
"group": Type.String(),
"changes": Type.Array(Type.String()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) { return json;
return new this(Address.fromJSON(json.src), json.type, json.group, json.changes); }
}
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,
);
}
} }
export class PermissionStrings { /*
constructor( Update events
public instanceId: PermissionInstanceId, */
public permissions: Set<GamePermission>,
public updatedAtMs: number = 0,
public isDeleted: boolean = false,
) {
}
static jsonSchema = Type.Object({ export class GroupUpdatedEvent {
instanceId: PermissionInstanceIdSchema, declare ["constructor"]: typeof GroupUpdatedEvent;
permissions: Type.Array(Type.String()), static plugin = "exp_groups" as const;
updatedAtMs: Type.Optional(Type.Number()), static type = "event" as const;
isDeleted: Type.Optional(Type.Boolean()), static src = "controller" as const;
}); static dst = ["control", "instance"] as const;
static permission = "exp_groups.group.subscribe" as const;
static fromJSON(json: Static<typeof PermissionStrings.jsonSchema>) { constructor(
return new PermissionStrings( public updates: GroupRecord[],
json.instanceId, ) {}
new Set(json.permissions),
json.updatedAtMs,
json.isDeleted
);
}
toJSON() { static jsonSchema = Type.Object({
return { updates: Type.Array(GroupRecord.jsonSchema),
instanceId: this.instanceId, });
permissions: [...this.permissions.values()],
updatedAtMs: this.updatedAtMs > 0 ? this.updatedAtMs : undefined,
isDeleted: this.isDeleted ? this.isDeleted : undefined,
}
}
get id() { toJSON() {
return this.instanceId 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 PermissionStringsUpdate { export class ManualAssignmentUpdatedEvent {
declare ["constructor"]: typeof PermissionStringsUpdate; declare ["constructor"]: typeof ManualAssignmentUpdatedEvent;
static type = "event" as const; static plugin = "exp_groups" as const;
static src = ["instance", "controller"] as const; static type = "event" as const;
static dst = ["controller", "control"] as const; static src = "controller" as const;
static plugin = "exp_groups" as const; static dst = ["control", "instance"] as const;
static permission = "exp_groups.list.subscribe"; static permission = "exp_groups.assignment.subscribe" as const;
constructor( constructor(
public updates: PermissionStrings[], public updates: AssignmentRecord[],
) { } ) {}
static jsonSchema = Type.Object({ static jsonSchema = Type.Object({
"updates": Type.Array(PermissionStrings.jsonSchema), updates: Type.Array(AssignmentRecord.jsonSchema),
}); });
static fromJSON(json: Static<typeof this.jsonSchema>) { toJSON() {
return new this( return { updates: this.updates.map(assignment => assignment.toJSON()) };
json.updates.map(update => PermissionStrings.fromJSON(update)) }
);
} 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 clusterio_api = require("modules/clusterio/api")
local Global = require("modules/exp_util/global") local compat = require("modules/clusterio/compat") --[[@as LibCompat]]
local Groups = require("modules/exp_groups")
local pending_updates = {} --- Top level module table, contains event handlers and public methods
Global.register(pending_updates, function(tbl) --- @class ExpGroups
pending_updates = tbl local ExpGroups = {}
end)
--- @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[whitelist_index] = input_action_name
whitelist_index = whitelist_index + 1
else
blacklist[blacklist_index] = input_action_name
blacklist_index = blacklist_index + 1
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) local function on_permission_group_added(event)
if not event.player_index then return end -- Check if updates should be updated
pending_updates[event.group.name] = { if not script_data.emit_updates then
created = true, return
sync_all = true, end
tick = event.tick,
permissions = {}, mark_group_dirty(event.group)
players = {},
}
end end
--- Handle deletion of permission groups
--- @param event EventData.on_permission_group_deleted
local function on_permission_group_deleted(event) local function on_permission_group_deleted(event)
if not event.player_index then return end -- Check if updates should be updated
local existing = pending_updates[event.group_name] if not script_data.emit_updates then
pending_updates[event.group_name] = nil return
if not existing or not existing.created then
clusterio_api.send_json("exp_groups-permission_group_delete", {
group = event.group_name,
})
end end
emit_group_delete(event.id, event.group_name)
end end
--- Handle edits make to permission groups
--- @param event EventData.on_permission_group_edited
local function on_permission_group_edited(event) local function on_permission_group_edited(event)
if not event.player_index then return end -- Check if updates should be updated
local pending = pending_updates[event.group.name] if not script_data.emit_updates then
if not pending then return
pending = {
tick = event.tick,
permissions = {},
players = {},
}
pending_updates[event.group.name] = pending
end end
pending.tick = event.tick
if event.type == "add-permission" then -- Check if this is a player or group event
if not pending.sync_all then if event.type == "add-player" or event.type == "remove-player" then
pending.permissions[event.action] = true local player = assert(game.get_player(event.other_player_index))
end mark_player_dirty(player)
elseif event.type == "remove-permission" then else
if not pending.sync_all then mark_group_dirty(event.group)
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,
}
end end
end end
local function send_updates() --- Periodically flush queued changes
local tick = game.tick - 600 -- 10 Seconds local function on_nth_tick_flush()
local done = {} flush_group_updates()
for group_name, pending in pairs(pending_updates) do flush_player_updates()
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
end end
return { local e = defines.events
events = {
[defines.events.on_permission_group_added] = on_permission_group_added, local events = {
[defines.events.on_permission_group_deleted] = on_permission_group_deleted, [clusterio_api.events.on_server_startup] = on_server_startup,
[defines.events.on_permission_group_edited] = on_permission_group_edited, [e.on_multiplayer_init] = on_server_startup,
}, [e.on_permission_group_added] = on_permission_group_added,
on_nth_tick = { [e.on_permission_group_deleted] = on_permission_group_deleted,
[300] = send_updates, [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" "control.lua"
], ],
"require": [ "require": [
"globals.lua"
], ],
"dependencies": { "dependencies": {
"clusterio": "*", "clusterio": "*"
"exp_util": "*"
} }
} }
+2 -305
View File
@@ -1,306 +1,3 @@
local Async = require("modules/exp_util/async")
local table_to_json = helpers.table_to_json -- Access the exports from other modules using require("modules/exp_groups")
local json_to_table = helpers.json_to_table return require("modules/exp_groups/control")
--- 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
+19 -11
View File
@@ -1,41 +1,49 @@
{ {
"name": "@expcluster/permission_groups", "name": "@expcluster/permission-groups",
"private": true, "version": "7.0.1",
"version": "0.0.0", "description": "Clusterio plugin providing syncing of permission groups",
"description": "Example Description. Package. Change me in package.json", "author": "Cooldude2606 <https://github.com/Cooldude2606>",
"license": "MIT",
"repository": "explosivegaming/ExpCluster",
"main": "dist/node/index.js", "main": "dist/node/index.js",
"scripts": { "scripts": {
"$prepare": "tsc --build && webpack-cli --env production" "prepare": "tsc --build && webpack-cli --env production"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
"peerDependencies": { "peerDependencies": {
"@clusterio/lib": "catalog:" "@clusterio/controller": "workspace:^",
"@clusterio/host": "workspace:^",
"@clusterio/lib": "workspace:^",
"@clusterio/web_ui": "workspace:^"
}, },
"devDependencies": { "devDependencies": {
"@clusterio/lib": "catalog:", "@ant-design/icons": "^6.2.3",
"@clusterio/web_ui": "catalog:", "@clusterio/controller": "workspace:^",
"@types/fs-extra": "^11.0.4", "@clusterio/host": "workspace:^",
"@clusterio/lib": "workspace:^",
"@clusterio/web_ui": "workspace:^",
"@types/node": "catalog:", "@types/node": "catalog:",
"@types/react": "catalog:", "@types/react": "catalog:",
"antd": "catalog:", "antd": "catalog:",
"react": "catalog:", "react": "catalog:",
"react-dom": "catalog:", "react-dom": "catalog:",
"react-router-dom": "catalog:",
"typescript": "catalog:", "typescript": "catalog:",
"webpack": "catalog:", "webpack": "catalog:",
"webpack-cli": "catalog:", "webpack-cli": "catalog:",
"webpack-merge": "catalog:" "webpack-merge": "catalog:"
}, },
"dependencies": { "dependencies": {
"@sinclair/typebox": "catalog:", "@sinclair/typebox": "catalog:"
"fs-extra": "^11.3.3"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
"keywords": [ "keywords": [
"clusterio", "clusterio",
"clusterio-plugin",
"factorio" "factorio"
] ]
} }
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"files": [], "files": [],
"references": [ "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";
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";
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";
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";
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";
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";
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";
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, { import React, { useState, useCallback, useSyncExternalStore } from "react";
useContext, useEffect, useState, import { BaseWebPlugin, PageLayout, PageHeader, useAccount, SectionHeader } from "@clusterio/web_ui";
useCallback, useSyncExternalStore, import { Button } from "antd";
} from "react";
// import {
//
// } from "antd";
import {
BaseWebPlugin, PageLayout, PageHeader, Control, ControlContext, notifyErrorHandler,
useInstances,
} from "@clusterio/web_ui";
import { PermissionGroupUpdate, PermissionInstanceId, PermissionStringsUpdate } from "../messages";
import * as messages from "../messages";
import * as lib from "@clusterio/lib"; 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(() => { const [groupOpen, setGroupOpen] = useState(false);
control.send(new lib.RoleListRequest()).then(newRoles => { const [assignmentOpen, setAssignmentOpen] = useState(false);
setRoles(newRoles); const [roleMappingOpen, setRoleMappingOpen] = useState(false);
}).catch(notifyErrorHandler("Error fetching role list"));
}, []);
return <PageLayout nav={[{ name: "exp_groups" }]}> return <PageLayout nav={[{ name: "Permission Groups" }]}>
<PageHeader title="exp_groups" /> <PageHeader title="Permission Groups" />
Permission Strings: {String(permissionStringsSynced)} {JSON.stringify([...permissionStrings.values()])} <br/>
Permission Groups: {String(permissionGroupsSynced)} {JSON.stringify([...permissionGroups.values()])} <br/> {account.hasPermission("exp_groups.group.list") && <>
Instances: {String(instancesSync)} {JSON.stringify([...instances.values()].map(instance => [instance.id, instance.name]))} <br/> <SectionHeader
Roles: {JSON.stringify([...roles.values()].map(role => [role.id, role.name]))} <br/> title="Groups"
<GroupTree/> 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>; </PageLayout>;
} }
export class WebPlugin extends BaseWebPlugin { export class WebPlugin extends BaseWebPlugin {
permissionStrings = new lib.EventSubscriber(PermissionStringsUpdate, this.control); groups = new lib.MapSubscriber(messages.GroupUpdatedEvent, this.control);
permissionGroups = new lib.EventSubscriber(PermissionGroupUpdate, this.control); assignments = new lib.MapSubscriber(messages.ManualAssignmentUpdatedEvent, this.control);
roleMappings = new lib.MapSubscriber(messages.RoleMappingUpdatedEvent, this.control);
async init() { async init() {
this.pages = [ this.pages = [
{ {
path: "/exp_groups", path: "/permission_groups",
sidebarName: "exp_groups", sidebarName: "Permission Groups",
permission: "exp_groups.list", permission: (account => account.hasAnyPermission(
content: <MyTemplatePage/>, "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) { useGroups() {
const [permissionStrings, synced] = this.usePermissionStrings(); const subscribe = useCallback((cb: () => void) => this.groups.subscribe(cb), []);
return [instanceId !== undefined ? permissionStrings.get(instanceId) : undefined, synced] as const; return useSyncExternalStore(subscribe, () => this.groups.getSnapshot());
} }
usePermissionStrings() { useAssignments() {
const control = useContext(ControlContext); const subscribe = useCallback((cb: () => void) => this.assignments.subscribe(cb), []);
const subscribe = useCallback((callback: () => void) => this.permissionStrings.subscribe(callback), [control]); return useSyncExternalStore(subscribe, () => this.assignments.getSnapshot());
return useSyncExternalStore(subscribe, () => this.permissionStrings.getSnapshot());
} }
useInstancePermissionGroups(instanceId?: PermissionInstanceId) { useRoleMappings() {
const [permissionGroups, synced] = this.usePermissionGroups(); const subscribe = useCallback((cb: () => void) => this.roleMappings.subscribe(cb), []);
return [instanceId !== undefined ? [...permissionGroups.values()].filter(group => group.instanceId === instanceId) : undefined, synced] as const; return useSyncExternalStore(subscribe, () => this.roleMappings.getSnapshot());
}
usePermissionGroups() {
const control = useContext(ControlContext);
const subscribe = useCallback((callback: () => void) => this.permissionGroups.subscribe(callback), [control]);
return useSyncExternalStore(subscribe, () => this.permissionGroups.getSnapshot());
} }
} }
+2
View File
@@ -26,6 +26,8 @@ module.exports = (env = {}) => merge(common(env), {
"antd": { import: false }, "antd": { import: false },
"react": { import: false }, "react": { import: false },
"react-dom": { import: false }, "react-dom": { import: false },
"react-router": { import: false },
"react-router-dom": { import: false },
}, },
}), }),
], ],
+17 -12
View File
@@ -16,7 +16,7 @@ Storage.register(player_elements, function(tbl)
player_elements = tbl player_elements = tbl
end) end)
--- @class Gui --- @class (partial) Gui
local Gui = { local Gui = {
define = ExpElement.new, define = ExpElement.new,
from_argument = ExpElement.from_argument, from_argument = ExpElement.from_argument,
@@ -46,9 +46,11 @@ end
--- @return LuaPlayer --- @return LuaPlayer
function Gui.get_player(input) function Gui.get_player(input)
if type(input) == "table" and not input.player_index then 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 end
return assert(game.get_player(input.player_index)) --- @cast input LuaGuiElement | { player_index: uint }
return (assert(game.get_player(input.player_index)))
end end
--- Toggle the enable state of an element --- Toggle the enable state of an element
@@ -88,7 +90,7 @@ end
--- @param define ExpElement --- @param define ExpElement
--- @param visible Gui.VisibleCallback | boolean | nil --- @param visible Gui.VisibleCallback | boolean | nil
function Gui.add_top_element(define, visible) 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 Gui.top_elements[define] = visible or false
end end
@@ -96,7 +98,7 @@ end
--- @param define ExpElement --- @param define ExpElement
--- @param visible Gui.VisibleCallback | boolean | nil --- @param visible Gui.VisibleCallback | boolean | nil
function Gui.add_left_element(define, visible) 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 Gui.left_elements[define] = visible or false
end end
@@ -105,7 +107,7 @@ end
--- @param define ExpElement --- @param define ExpElement
--- @param visible Gui.VisibleCallback | boolean | nil --- @param visible Gui.VisibleCallback | boolean | nil
function Gui.add_relative_element(define, visible) 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 Gui.relative_elements[define] = visible or false
end end
@@ -114,7 +116,8 @@ end
--- @param player LuaPlayer --- @param player LuaPlayer
--- @return LuaGuiElement --- @return LuaGuiElement
function Gui.get_top_element(define, player) 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 end
--- Register a element define to be drawn to the left flow on join --- Register a element define to be drawn to the left flow on join
@@ -122,7 +125,8 @@ end
--- @param player LuaPlayer --- @param player LuaPlayer
--- @return LuaGuiElement --- @return LuaGuiElement
function Gui.get_left_element(define, player) 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 end
--- Register a element define to be drawn to the relative flow on join --- Register a element define to be drawn to the relative flow on join
@@ -130,7 +134,8 @@ end
--- @param player LuaPlayer --- @param player LuaPlayer
--- @return LuaGuiElement --- @return LuaGuiElement
function Gui.get_relative_element(define, player) 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 end
--- Ensure all the correct elements are visible and exist --- 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 -- This check isn't needed, but allows the toolbar file to be deleted without modifying any lib code
if Gui.toolbar then if Gui.toolbar then
--- @diagnostic disable-next-line invisible --- @diagnostic disable-next-line: access-invisible
Gui.toolbar._create_elements(player) Gui.toolbar._create_elements(player)
--- @diagnostic disable-next-line invisible --- @diagnostic disable-next-line: access-invisible
Gui.toolbar._ensure_consistency(player) Gui.toolbar._ensure_consistency(player)
end end
end end
@@ -211,7 +216,7 @@ local function on_gui_opened(event)
if visible then if visible then
event.element = element event.element = element
--- @diagnostic disable-next-line invisible --- @diagnostic disable-next-line: access-invisible
define:raise_event(event) define:raise_event(event)
end end
end end
+6 -2
View File
@@ -62,6 +62,7 @@ local GuiData = {
--- @field player_data table<uint, any> --- @field player_data table<uint, any>
--- @field force_data table<uint, any> --- @field force_data table<uint, any>
--- @field global_data table --- @field global_data table
--- @field [DataKey] any
-- This class has no prototype methods -- This class has no prototype methods
-- Same as raw but __index ensures the values exist -- 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.") 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 local object_name = key.object_name --- @diagnostic disable-line assign-type-mismatch
if object_name == "LuaGuiElement" then if object_name == "LuaGuiElement" then
--- @cast key LuaGuiElement
local data = self._raw.element_data local data = self._raw.element_data
local player_elements = data and data[key.player_index] local player_elements = data and data[key.player_index]
return player_elements and player_elements[key.index] return player_elements and player_elements[key.index]
elseif object_name == "LuaPlayer" then elseif object_name == "LuaPlayer" then
--- @cast key LuaPlayer
local data = self._raw.player_data local data = self._raw.player_data
return data and data[key.index] return data and data[key.index]
elseif object_name == "LuaForce" then elseif object_name == "LuaForce" then
--- @cast key LuaForce
local data = self._raw.force_data local data = self._raw.force_data
return data and data[key.index] return data and data[key.index]
else else
@@ -153,7 +157,7 @@ end
--- @param scope string --- @param scope string
--- @return GuiData --- @return GuiData
function GuiData.get(scope) function GuiData.get(scope)
return GuiData._scopes[scope] --[[ @as GuiData ]] return GuiData._scopes[scope] --[[@as GuiData]]
end end
--- Used to clean up data from destroyed elements --- 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 for _, scope in pairs(registered_scopes) do
local data = scope._raw.element_data local data = scope._raw.element_data
local player_elements = data and data[player_index] local player_elements = data and data[player_index]
if player_elements then if data and player_elements then
player_elements[element_index] = nil player_elements[element_index] = nil
if not next(player_elements) then if not next(player_elements) then
data[player_index] = nil data[player_index] = nil
+17 -17
View File
@@ -1,4 +1,4 @@
--- @class Gui --- @class (partial) Gui
local Gui = require("modules/exp_gui") local Gui = require("modules/exp_gui")
--- @class Gui.elements --- @class Gui.elements
@@ -28,7 +28,7 @@ Elements.aligned_flow = Gui.define("aligned_flow")
vertically_stretchable = vertical_align ~= "center", vertically_stretchable = vertical_align ~= "center",
horizontally_stretchable = horizontal_align ~= "center", horizontally_stretchable = horizontal_align ~= "center",
} }
end) --[[ @as any ]] end) --[[@as any]]
--- A solid horizontal white bar element --- A solid horizontal white bar element
--- @class Gui.elements.bar: ExpElement --- @class Gui.elements.bar: ExpElement
@@ -48,11 +48,11 @@ Elements.bar = Gui.define("bar")
else else
style.horizontally_stretchable = true style.horizontally_stretchable = true
end end
end) --[[ @as any ]] end) --[[@as any]]
--- A label which is centered --- A label which is centered
--- @class Gui.elements.centered_label: ExpElement --- @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") Elements.centered_label = Gui.define("centered_label")
:draw{ :draw{
type = "label", type = "label",
@@ -63,7 +63,7 @@ Elements.centered_label = Gui.define("centered_label")
horizontal_align = "center", horizontal_align = "center",
single_line = false, single_line = false,
width = Gui.from_argument(1), width = Gui.from_argument(1),
} --[[ @as any ]] } --[[@as any]]
--- A label which has two white bars on either side of it --- A label which has two white bars on either side of it
--- @class Gui.elements.title_label: ExpElement --- @class Gui.elements.title_label: ExpElement
@@ -91,7 +91,7 @@ Elements.title_label = Gui.define("title_label")
Elements.bar(flow) Elements.bar(flow)
return label return label
end) --[[ @as any ]] end) --[[@as any]]
--- A fixed size vertical scroll pane --- A fixed size vertical scroll pane
--- @class Gui.elements.scroll_pane: ExpElement --- @class Gui.elements.scroll_pane: ExpElement
@@ -109,7 +109,7 @@ Elements.scroll_pane = Gui.define("scroll_pane")
padding = { 1, 3 }, padding = { 1, 3 },
maximal_height = Gui.from_argument(1), maximal_height = Gui.from_argument(1),
horizontally_stretchable = true, horizontally_stretchable = true,
} --[[ @as any ]] } --[[@as any]]
--- A fixed size vertical scroll pane containing a table --- A fixed size vertical scroll pane containing a table
--- @class Gui.elements.scroll_table: ExpElement --- @class Gui.elements.scroll_table: ExpElement
@@ -132,7 +132,7 @@ Elements.scroll_table = Gui.define("scroll_table")
cell_padding = 1, cell_padding = 1,
vertical_align = "center", vertical_align = "center",
horizontally_stretchable = true, horizontally_stretchable = true,
} --[[ @as any ]] } --[[@as any]]
--- A container frame --- A container frame
--- @class Gui.elements.container: ExpElement --- @class Gui.elements.container: ExpElement
@@ -159,13 +159,13 @@ Elements.container = Gui.define("container")
vertically_stretchable = false, vertically_stretchable = false,
horizontally_stretchable = false, horizontally_stretchable = false,
minimal_width = Gui.from_argument(1), minimal_width = Gui.from_argument(1),
} --[[ @as any ]] } --[[@as any]]
--- Get the root element of a container --- Get the root element of a container
--- @param container LuaGuiElement --- @param container LuaGuiElement
--- @return LuaGuiElement --- @return LuaGuiElement
function Elements.container.get_root_element(container) function Elements.container.get_root_element(container)
return container.parent return assert(container.parent)
end end
--- A frame within a container --- A frame within a container
@@ -183,7 +183,7 @@ Elements.subframe_base = Gui.define("container_subframe")
padding = { 3, 6, 0, 6 }, padding = { 3, 6, 0, 6 },
use_header_filler = false, use_header_filler = false,
horizontally_stretchable = true, horizontally_stretchable = true,
} --[[ @as any ]] } --[[@as any]]
--- @class Gui.elements.header.opts --- @class Gui.elements.header.opts
--- @field name string? --- @field name string?
@@ -212,7 +212,7 @@ Elements.header = Gui.define("container_header")
subframe.add{ type = "empty-widget" }.style.horizontally_stretchable = true subframe.add{ type = "empty-widget" }.style.horizontally_stretchable = true
return subframe return subframe
end) --[[ @as any ]] end) --[[@as any]]
--- @class Gui.elements.footer.opts --- @class Gui.elements.footer.opts
--- @field name string? --- @field name string?
@@ -241,7 +241,7 @@ Elements.footer = Gui.define("container_footer")
subframe.add{ type = "empty-widget" }.style.horizontally_stretchable = true subframe.add{ type = "empty-widget" }.style.horizontally_stretchable = true
return subframe return subframe
end) --[[ @as any ]] end) --[[@as any]]
--- A button used to destroy its target when clicked, intended for screen frames --- A button used to destroy its target when clicked, intended for screen frames
--- @class Gui.elements.screen_frame_close: ExpElement --- @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) :on_click(function(def, player, element, event)
--- @cast def Gui.elements.screen_frame_close --- @cast def Gui.elements.screen_frame_close
Gui.destroy_if_valid(def.data[element]) Gui.destroy_if_valid(def.data[element])
end) --[[ @as any ]] end) --[[@as any]]
--- A draggable frame with close button and button flow --- A draggable frame with close button and button flow
--- @class Gui.elements.screen_frame: ExpElement --- @class Gui.elements.screen_frame: ExpElement
@@ -313,20 +313,20 @@ Elements.screen_frame = Gui.define("screen_frame")
direction = "vertical", direction = "vertical",
style = "inside_shallow_frame_packed", style = "inside_shallow_frame_packed",
} }
end) --[[ @as any ]] end) --[[@as any]]
--- Get the button flow for a screen frame --- Get the button flow for a screen frame
--- @param screen_frame LuaGuiElement --- @param screen_frame LuaGuiElement
--- @return LuaGuiElement --- @return LuaGuiElement
function Elements.screen_frame.get_button_flow(screen_frame) 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 end
--- Get the root element of a screen frame --- Get the root element of a screen frame
--- @param screen_frame LuaGuiElement --- @param screen_frame LuaGuiElement
--- @return LuaGuiElement --- @return LuaGuiElement
function Elements.screen_frame.get_root_element(screen_frame) function Elements.screen_frame.get_root_element(screen_frame)
return screen_frame.parent return assert(screen_frame.parent)
end end
return Elements return Elements
+1 -3
View File
@@ -166,7 +166,6 @@ end
function GuiIter.get_tracked_elements(scope, filter) function GuiIter.get_tracked_elements(scope, filter)
local class_name = ExpUtil.get_class_name(filter) local class_name = ExpUtil.get_class_name(filter)
if class_name == "nil" then if class_name == "nil" then
--- @cast filter nil
return GuiIter.all_elements(scope) return GuiIter.all_elements(scope)
elseif class_name == "LuaPlayer" then elseif class_name == "LuaPlayer" then
--- @cast filter LuaPlayer --- @cast filter LuaPlayer
@@ -189,7 +188,6 @@ end
function GuiIter.get_online_elements(scope, filter) function GuiIter.get_online_elements(scope, filter)
local class_name = ExpUtil.get_class_name(filter) local class_name = ExpUtil.get_class_name(filter)
if class_name == "nil" then if class_name == "nil" then
--- @cast filter nil
return GuiIter.filtered_elements(scope, game.connected_players) return GuiIter.filtered_elements(scope, game.connected_players)
elseif class_name == "LuaPlayer" then elseif class_name == "LuaPlayer" then
--- @cast filter LuaPlayer --- @cast filter LuaPlayer
@@ -235,7 +233,7 @@ end
function GuiIter.remove_element(scope, player_index, element_index) function GuiIter.remove_element(scope, player_index, element_index)
local scope_elements = registered_scopes[scope] local scope_elements = registered_scopes[scope]
if not scope_elements then return end 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 if not player_elements then return end
player_elements[element_index] = nil player_elements[element_index] = nil
end end
+6 -3
View File
@@ -43,6 +43,8 @@ ExpElement.events = {}
--- @field _force_data ExpElement.PostDrawCallback? --- @field _force_data ExpElement.PostDrawCallback?
--- @field _global_data ExpElement.PostDrawCallback? --- @field _global_data ExpElement.PostDrawCallback?
--- @field _events table<defines.events, ExpElement.EventHandler<EventData>[]> --- @field _events table<defines.events, ExpElement.EventHandler<EventData>[]>
--- @field _has_handlers boolean
--- @field _track_elements boolean
--- @overload fun(parent: LuaGuiElement, ...: any): LuaGuiElement --- @overload fun(parent: LuaGuiElement, ...: any): LuaGuiElement
ExpElement._prototype = { ExpElement._prototype = {
_track_elements = false, _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) return setmetatable(instance, ExpElement._metatable)
end end
@@ -166,7 +168,7 @@ end
--- @param name string --- @param name string
--- @return ExpElement --- @return ExpElement
function ExpElement.get(name) 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 end
--- Create a new instance of this element definition --- Create a new instance of this element definition
@@ -274,6 +276,7 @@ function ExpElement._prototype:draw(definition)
if not next(signals) then if not next(signals) then
-- If no signals then skip var arg -- If no signals then skip var arg
--- @cast definition LuaGuiElement.add_param
self._draw = function(_, parent) self._draw = function(_, parent)
return parent.add(definition) return parent.add(definition)
end end
@@ -459,7 +462,7 @@ function ExpElement._prototype:raise_event(event)
for _, handler in ipairs(handlers) do for _, handler in ipairs(handlers) do
-- All gui elements will contain player and element, other events might have these as nil -- 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 -- 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
end end
+1 -1
View File
@@ -1,4 +1,4 @@
--- @class Gui --- @class (partial) Gui
local Gui = require("modules/exp_gui") local Gui = require("modules/exp_gui")
--- @class Gui.styles --- @class Gui.styles
+18 -16
View File
@@ -1,5 +1,5 @@
--- @class Gui --- @class (partial) Gui
local Gui = require("modules/exp_gui") local Gui = require("modules/exp_gui")
local ExpElement = require("modules/exp_gui/prototype") local ExpElement = require("modules/exp_gui/prototype")
local mod_gui = require("mod-gui") local mod_gui = require("mod-gui")
@@ -29,7 +29,7 @@ Toolbar.on_gui_button_toggled = script.generate_event_name()
--- @class _ExpElement._prototype --- @class _ExpElement._prototype
--- @field on_button_toggled ExpElement.OnEventAdder<EventData.on_gui_button_toggled> --- @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) function ExpElement._prototype.on_button_toggled(self, handler)
return self:on_event(Toolbar.on_gui_button_toggled, handler) return self:on_event(Toolbar.on_gui_button_toggled, handler)
end 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 button.style = state and toolbar_button_active_style or toolbar_button_default_style
-- Make the extra required adjustments -- Make the extra required adjustments
local style = button.style local style = button.style --[[@as LuaStyle]]
style.minimal_width = original_width style.minimal_width = original_width
style.maximal_height = original_height style.maximal_height = original_height
if button.type == "sprite-button" then if button.type == "sprite-button" then
@@ -335,9 +335,10 @@ Below here is the toolbar settings GUI and its associated functions
--- @param dst LuaGuiElement --- @param dst LuaGuiElement
local function copy_style(src, dst) local function copy_style(src, dst)
dst.style = src.style.name dst.style = src.style.name
dst.style.height = toolbar_button_small local style = dst.style --[[@as LuaStyle]]
dst.style.width = toolbar_button_small style.height = toolbar_button_small
dst.style.padding = -2 style.width = toolbar_button_small
style.padding = -2
end end
--- Reorder the buttons relative to each other, this will update the datastore --- 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 -- Swap the position in the list
local list = assert(item.parent) 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) list.swap_children(old_index, new_index)
-- Swap the position in the top flow, offset by 1 because of settings button -- 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 other_element = Gui.get_left_element(other_left_element, player)
local left_index = element.get_index_in_parent() local left_index = element.get_index_in_parent()
local other_index = other_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 end
-- If we are moving in/out of first/last place we need to update the move buttons -- 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 player LuaPlayer
--- @param order Gui.ToolbarOrder --- @param order Gui.ToolbarOrder
function Toolbar.set_order(player, order) 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 left_flow = Gui.get_left_flow(player)
local top_flow = Gui.get_top_flow(player) local top_flow = Gui.get_top_flow(player)
@@ -466,7 +467,7 @@ end
function Toolbar.get_state(player) function Toolbar.get_state(player)
-- Get the order of toolbar buttons -- Get the order of toolbar buttons
local order = {} 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 for index, item in pairs(list.children) do
order[index] = { name = item.name, favourite = elements.toolbar_list_item.data[item].set_favourite.state } order[index] = { name = item.name, favourite = elements.toolbar_list_item.data[item].set_favourite.state }
end end
@@ -487,7 +488,7 @@ end
--- @param player LuaPlayer --- @param player LuaPlayer
function Toolbar._create_elements(player) function Toolbar._create_elements(player)
-- Add any missing items to the gui -- 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 local previous_last_index = #toolbar_list.children_names
for define in pairs(Gui.top_elements) do for define in pairs(Gui.top_elements) do
if define ~= elements.close_toolbar and toolbar_list[define.name] == nil then 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 -- Reset the state of the previous last child
local children = toolbar_list.children local children = toolbar_list.children
if previous_last_index > 0 then 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 end
-- Set the state of the move buttons for the first and last element -- Set the state of the move buttons for the first and last element
@@ -513,7 +515,7 @@ end
--- @param player LuaPlayer --- @param player LuaPlayer
function Toolbar._ensure_consistency(player) function Toolbar._ensure_consistency(player)
-- Update the toolbar buttons -- 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 for _, button in ipairs(toolbar_buttons) do
-- Update the visible state based on if the player is allowed the button -- Update the visible state based on if the player is allowed the button
local element = Gui.get_top_element(button, player) 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, size = toolbar_button_small,
}) })
:on_click(function(def, player, element) :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) move_toolbar_button(player, item, -1)
end) end)
@@ -634,7 +636,7 @@ elements.move_item_down = Gui.define("move_item_down")
size = toolbar_button_small, size = toolbar_button_small,
}) })
:on_click(function(def, player, element) :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) move_toolbar_button(player, item, 1)
end) end)
@@ -658,7 +660,7 @@ elements.set_favourite = Gui.define("set_favourite")
width = 180, width = 180,
} }
:on_checked_state_changed(function(def, player, element) :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 top_element = Gui.get_top_element(define, player)
local had_visible = Toolbar.has_visible_buttons(player) local had_visible = Toolbar.has_visible_buttons(player)
top_element.visible = element.state top_element.visible = element.state
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@expcluster/lib_gui", "name": "@expcluster/lib_gui",
"version": "0.1.0", "version": "7.0.1",
"description": "Clusterio plugin providing a Lua GUI definition library.", "description": "Clusterio plugin providing a Lua GUI definition library.",
"author": "Cooldude2606 <https://github.com/Cooldude2606>", "author": "Cooldude2606 <https://github.com/Cooldude2606>",
"license": "MIT", "license": "MIT",
@@ -13,10 +13,10 @@
"node": ">=18" "node": ">=18"
}, },
"peerDependencies": { "peerDependencies": {
"@clusterio/lib": "catalog:" "@clusterio/lib": "workspace:^"
}, },
"devDependencies": { "devDependencies": {
"@clusterio/lib": "catalog:", "@clusterio/lib": "workspace:^",
"@types/node": "catalog:", "@types/node": "catalog:",
"typescript": "catalog:", "typescript": "catalog:",
"webpack": "catalog:", "webpack": "catalog:",
@@ -16,21 +16,14 @@ return {
"modules.data.quickbar", "modules.data.quickbar",
"modules.data.alt-view", "modules.data.alt-view",
"modules.data.tag", "modules.data.tag",
-- 'modules.data.bonus',
"modules.data.personal-logistic",
"modules.data.language", "modules.data.language",
--"modules.data.toolbar", --"modules.data.toolbar",
--- GUI --- GUI
"modules.gui.readme",
"modules.gui.rocket-info",
"modules.gui.warp-list", "modules.gui.warp-list",
"modules.gui.player-list",
"modules.gui.vlayer", "modules.gui.vlayer",
"modules.gui._role_updates",
"modules.graftorio.require", -- graftorio "modules.graftorio.require", -- graftorio
--- Config Files --- Config Files
"config.expcore.permission_groups", -- loads some predefined permission groups "config.expcore.permission_groups", -- loads some predefined permission groups
"config.expcore.roles", -- loads some predefined roles
} }
+21 -26
View File
@@ -80,33 +80,28 @@ return {
enemy_expansion = false, --- @setting enemy_expansion a catch all for in case the map settings file fails to load enemy_expansion = false, --- @setting enemy_expansion a catch all for in case the map settings file fails to load
chart_radius = 10 * 32, --- @setting chart_radius the number of tiles that will be charted when the map starts chart_radius = 10 * 32, --- @setting chart_radius the number of tiles that will be charted when the map starts
items = { --- @setting items items and there condition for being given items = { --- @setting items items and there condition for being given
-- ['item-name'] = function(amount_made, production_stats, player) return <Number> end -- 0 means no items given ["iron-ore"] = cutoff_time(20 * minutes, 80, 0),
-- Plates ["stone"] = cutoff_time(20 * minutes, 20, 0),
["iron-plate"] = scale_amount_made(100, 10, 10), ["coal"] = cutoff_time(20 * minutes, 20, 0),
["copper-plate"] = scale_amount_made(100, 0, 8), -- ["burner-mining-drill"]=cutoff_time(20 * minutes, 8, 0),
["steel-plate"] = scale_amount_made(100, 0, 4), -- ["stone-furnace"]=cutoff_time(20 * minutes, 8, 0),
-- Secondary Items ["submachine-gun"] = 1,
["electronic-circuit"] = scale_amount_made(1000, 0, 6), ["piercing-rounds-magazine"] = 20,
["iron-gear-wheel"] = scale_amount_made(1000, 0, 6), ["construction-robot"] = 10,
-- Starting Items ["modular-armor"] = 1,
["burner-mining-drill"] = cutoff_time(10 * minutes, 4, 0), ["solar-panel-equipment"] = 16,
["stone-furnace"] = cutoff_time(10 * minutes, 4, 0), ["belt-immunity-equipment"] = 1,
-- Armor ["battery-equipment"] = 2,
["light-armor"] = cutoff_amount_made_unless(5, 0, 1, "heavy-armor", 5), ["personal-roboport-equipment"] = 1
["heavy-armor"] = cutoff_amount_made(5, 0, 1),
-- Weapon
["pistol"] = cutoff_amount_made_unless(0, 1, 1, "submachine-gun", 5),
["submachine-gun"] = cutoff_amount_made(5, 0, 1),
-- Ammo
["firearm-magazine"] = cutoff_amount_made_unless(100, 10, 0, "piercing-rounds-magazine", 100),
["piercing-rounds-magazine"] = cutoff_amount_made(100, 0, 10),
--[[ --[[
['construction-robot'] = 10, ["construction-robot"] = 50,
['modular-armor'] = 1, ["spidertron"] = 1,
['solar-panel-equipment'] = 16, ["spidertron-remote"] = 1,
['belt-immunity-equipment'] = 1, ["modular-armor"] = 1,
['battery-equipment'] = 2, ["solar-panel-equipment"] = 60,
['personal-roboport-equipment'] = 1 ["belt-immunity-equipment"] = 1,
["battery-equipment"] = 6,
["personal-roboport-equipment"] = 5
]] ]]
} }
} }
+3 -2
View File
@@ -1,4 +1,4 @@
local Roles = require("modules.exp_legacy.expcore.roles") local Roles = require("modules/exp_roles")
return { return {
admin_as_active = true, --- @setting admin_as_active When true admins will be treated as active regardless of afk time 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 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 update_time = 3600 * 30, --- @setting update_time How often in ticks the script checks for active players
custom_active_check = function(player) 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, end,
} }
+15 -216
View File
@@ -3,21 +3,17 @@
return { return {
--[[ --[[
TODO
force bonus
quick health regeneration
Base point is equal to the amount of standard value in each parameter. Base point is equal to the amount of standard value in each parameter.
CMMS CRS CCS CISB CHB CRDB PBR CMMS CRS CCS CISB CHB CRDB PBR
STD 30 90 32 20 16 12 60 STD 20 60 24 10 12 8 40
= 260 = 174
MAX 60 180 64 40 32 24 120 MAX 40 120 48 20 24 16 80
= 480 = 348
]] ]]
points = { points = {
base = 260, base = 174,
increase_percentage_per_role_level = 0.03, increase_percentage_per_role_level = 0.03,
role_name = "Member", role_name = "Member",
}, },
@@ -27,8 +23,7 @@ return {
name = "character_mining_speed_modifier", name = "character_mining_speed_modifier",
scale = 1, scale = 1,
cost = 10, cost = 10,
max_value = 6, max_value = 4,
initial_value = 3,
value_step = 0.5, value_step = 0.5,
is_percentage = true, is_percentage = true,
}, },
@@ -36,8 +31,7 @@ return {
name = "character_running_speed_modifier", name = "character_running_speed_modifier",
scale = 1, scale = 1,
cost = 60, cost = 60,
max_value = 3, max_value = 2,
initial_value = 1.5,
value_step = 0.25, value_step = 0.25,
is_percentage = true, is_percentage = true,
}, },
@@ -45,33 +39,29 @@ return {
name = "character_crafting_speed_modifier", name = "character_crafting_speed_modifier",
scale = 1, scale = 1,
cost = 4, cost = 4,
max_value = 16, max_value = 12,
initial_value = 8,
value_step = 1, value_step = 1,
is_percentage = true, is_percentage = true,
}, },
{ {
name = "character_inventory_slots_bonus", name = "character_inventory_slots_bonus",
cost = 2,
scale = 10, scale = 10,
max_value = 200, cost = 2,
initial_value = 100, max_value = 100,
value_step = 10, value_step = 10,
}, },
{ {
name = "character_health_bonus", name = "character_health_bonus",
scale = 50, scale = 50,
cost = 4, cost = 4,
max_value = 400, max_value = 300,
initial_value = 200,
value_step = 50, value_step = 50,
}, },
{ {
name = "character_reach_distance_bonus", name = "character_reach_distance_bonus",
cost = 1,
scale = 1, scale = 1,
max_value = 24, cost = 1,
initial_value = 12, max_value = 16,
value_step = 2, value_step = 2,
combined_bonus = { combined_bonus = {
"character_resource_reach_distance_bonus", "character_resource_reach_distance_bonus",
@@ -80,104 +70,15 @@ return {
}, },
{ {
name = "personal_battery_recharge", name = "personal_battery_recharge",
initial_value = 6,
max_value = 12,
value_step = 1,
scale = 4, scale = 4,
cost = 40, cost = 40,
max_value = 8,
value_step = 1,
is_special = true, 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 = { 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"] = { ["worker_robots_battery_modifier"] = {
initial_value = 1,
max_value = 1, max_value = 1,
value_step = 1, value_step = 1,
scale = 1, scale = 1,
@@ -185,7 +86,6 @@ return {
is_percentage = false, is_percentage = false,
}, },
["worker_robots_storage_bonus"] = { ["worker_robots_storage_bonus"] = {
initial_value = 1,
max_value = 1, max_value = 1,
value_step = 1, value_step = 1,
scale = 1, scale = 1,
@@ -193,114 +93,13 @@ return {
is_percentage = false, is_percentage = false,
}, },
["following_robots_lifetime_modifier"] = { ["following_robots_lifetime_modifier"] = {
initial_value = 1,
max_value = 1, max_value = 1,
value_step = 1, value_step = 1,
scale = 1, scale = 1,
cost = 1, cost = 1,
is_percentage = false, 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 = { surface_bonus = {
--[[
['solar_power_multiplier'] = {
initial_value = 1,
max_value = 1000,
value_step = 1,
scale = 1,
cost = 1,
is_percentage = false
}
]]
}, },
} }
+3 -1
View File
@@ -24,6 +24,7 @@ local afk_time_units = {
} }
return { return {
--- @type table<string, LocalisedString | fun(player: LuaPlayer, is_command: boolean): LocalisedString>
messages = { --- @setting messages will trigger when ever the word is said messages = { --- @setting messages will trigger when ever the word is said
["discord"] = { "info.discord" }, ["discord"] = { "info.discord" },
["expgaming"] = { "info.website" }, ["expgaming"] = { "info.website" },
@@ -64,8 +65,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 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_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) 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 commands = { --- @setting commands will trigger only when command prefix is given
["dev"] = { "exp_chat-auto-reply.reply-dev" }, ["dev"] = { "exp_chat-auto-reply.reply-dev" },
["magic"] = { "exp_chat-auto-reply.reply-magic" }, ["magic"] = { "exp_chat-auto-reply.reply-magic" },
+1 -1
View File
@@ -7,7 +7,7 @@ return {
["Spawn"] = { ["Spawn"] = {
spawn_position = { x = 0, y = 0 }, spawn_position = { x = 0, y = 0 },
spawn_surface = "nauvis", spawn_surface = "nauvis",
entity_name = "small-biter", entity_name = "behemoth-biter",
messages = { messages = {
{ "info.website" }, { "info.website" },
{ "info.read-readme" }, { "info.read-readme" },
+12 -12
View File
@@ -5,21 +5,21 @@ return {
show_playtime = true, show_playtime = true,
entity_protection = true, entity_protection = true,
player_reports = true, player_reports = true,
player_warnings = true,
player_bans = true, player_bans = true,
player_mutes = true, player_mutes = true,
player_kicks = true, player_kicks = true,
player_promotes = false, player_promotes = false,
player_jail = true, player_jail = true,
["config"] = true, ['config']=false,
["purge"] = true, ['purge']=false,
["c"] = true, ['c']=false,
["command"] = true, ['sc']=false,
["silent-command"] = true, ['command']=false,
["measured-command"] = true, ['silent-command']=false,
["banlist"] = true, ['measured-command']=false,
["permissions"] = true, ['banlist']=false,
["editor"] = true, ['permissions']=false,
["cheat"] = true, ['editor']=false,
["open"] = false, ['cheat']=false,
['open']=false
} }
@@ -9,92 +9,83 @@ local Groups = require("modules.exp_legacy.expcore.permission_groups") --- @dep
Groups.new_group("Admin") Groups.new_group("Admin")
:allow_all() :allow_all()
Groups.new_group("Mod")
:allow_all()
:disallow{ :disallow{
"add_permission_group", -- admin "add_permission_group",
"delete_permission_group", "delete_permission_group",
"edit_permission_group", "edit_permission_group",
"import_permissions_string", "import_permissions_string",
"map_editor_action", "map_editor_action",
"toggle_map_editor", "toggle_map_editor",
"change_multiplayer_config", "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") Groups.new_group("Trusted")
:allow_all() :allow_all()
:disallow{ :disallow{
"add_permission_group", -- admin "add_permission_group", -- mod
"delete_permission_group", "delete_permission_group",
"edit_permission_group", "edit_permission_group",
"import_permissions_string", "import_permissions_string",
"map_editor_action", "map_editor_action",
"toggle_map_editor", "toggle_map_editor",
"change_multiplayer_config", "change_multiplayer_config",
"set_heat_interface_mode", "set_heat_interface_mode", -- trusted
"set_heat_interface_temperature", "set_heat_interface_temperature",
"set_infinity_container_filter_item", "set_infinity_container_filter_item",
"set_infinity_container_remove_unfiltered_items", "set_infinity_container_remove_unfiltered_items",
"set_infinity_pipe_filter", "set_infinity_pipe_filter",
"admin_action", -- trusted "admin_action",
} }
Groups.new_group("Standard") Groups.new_group("Standard")
:allow_all() :allow_all()
:disallow{ :disallow{
"add_permission_group", -- admin "add_permission_group", -- mod
"delete_permission_group", "delete_permission_group",
"edit_permission_group", "edit_permission_group",
"import_permissions_string", "import_permissions_string",
"map_editor_action", "map_editor_action",
"toggle_map_editor", "toggle_map_editor",
"change_multiplayer_config", "change_multiplayer_config",
"set_heat_interface_mode", "set_heat_interface_mode", -- trusted
"set_heat_interface_temperature", "set_heat_interface_temperature",
"set_infinity_container_filter_item", "set_infinity_container_filter_item",
"set_infinity_container_remove_unfiltered_items", "set_infinity_container_remove_unfiltered_items",
"set_infinity_pipe_filter", "set_infinity_pipe_filter",
"admin_action", -- trusted "admin_action",
"change_programmable_speaker_alert_parameters", -- standard "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") Groups.new_group("Guest")
:allow_all() :allow_all()
:disallow{ :disallow{
"add_permission_group", -- admin "add_permission_group", -- mod
"delete_permission_group", "delete_permission_group",
"edit_permission_group", "edit_permission_group",
"import_permissions_string", "import_permissions_string",
"map_editor_action", "map_editor_action",
"toggle_map_editor", "toggle_map_editor",
"change_multiplayer_config", "change_multiplayer_config",
"set_heat_interface_mode", "set_heat_interface_mode", -- trusted
"set_heat_interface_temperature", "set_heat_interface_temperature",
"set_infinity_container_filter_item", "set_infinity_container_filter_item",
"set_infinity_container_remove_unfiltered_items", "set_infinity_container_remove_unfiltered_items",
"set_infinity_pipe_filter", "set_infinity_pipe_filter",
"admin_action", -- trusted "admin_action",
"change_programmable_speaker_alert_parameters", -- standard "change_programmable_speaker_alert_parameters", -- standard
"change_programmable_speaker_parameters", -- guest
"drop_item", "drop_item",
"open_new_platform_button_from_rocket_silo", "open_new_platform_button_from_rocket_silo",
"set_rocket_silo_send_to_orbit_automated_mode", "set_rocket_silo_send_to_orbit_automated_mode",
"change_programmable_speaker_parameters", -- guest
"change_train_stop_station", "change_train_stop_station",
-- 'deconstruct',
"remove_cables", "remove_cables",
"remove_train_station", "remove_train_station",
"reset_assembling_machine", "reset_assembling_machine",
"rotate_entity", "rotate_entity",
-- 'use_artillery_remote', -- not in 2.0
"launch_rocket",
"cancel_research", "cancel_research",
-- 'activate_cut', -- not in 2.0
"flush_opened_entity_fluid", "flush_opened_entity_fluid",
"flush_opened_entity_specific_fluid", "flush_opened_entity_specific_fluid",
} }
+155 -172
View File
@@ -14,7 +14,7 @@ Roles.define_flag_trigger("is_spectator", function(player, state)
end) end)
Roles.define_flag_trigger("is_jail", function(player, state) Roles.define_flag_trigger("is_jail", function(player, state)
if player.character then if player.character then
player.character.active = not state player.character.disabled_by_script = not state
end end
end) end)
@@ -37,12 +37,7 @@ Roles.new_role("Senior Administrator", "SAdmin")
:set_flag("report-immune") :set_flag("report-immune")
:set_flag("instant-respawn") :set_flag("instant-respawn")
:set_parent("Administrator") :set_parent("Administrator")
:allow{ :set_allow_all()
"command/_rcon",
"command/debug",
"command/set-cheat-mode",
"command/research-all",
}
Roles.new_role("Administrator", "Admin") Roles.new_role("Administrator", "Admin")
:set_permission_group("Admin") :set_permission_group("Admin")
@@ -51,6 +46,21 @@ Roles.new_role("Administrator", "Admin")
:set_flag("is_spectator") :set_flag("is_spectator")
:set_flag("report-immune") :set_flag("report-immune")
:set_flag("instant-respawn") :set_flag("instant-respawn")
:set_parent("Senior Moderator")
:allow{
"command/_rcon",
"command/debug",
"command/set-cheat-mode",
"command/research-all"
}
Roles.new_role("Senior Moderator", "SMod")
:set_permission_group("Mod")
: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("Moderator") :set_parent("Moderator")
:allow{ :allow{
"gui/warp-list/bypass-proximity", "gui/warp-list/bypass-proximity",
@@ -59,17 +69,25 @@ Roles.new_role("Administrator", "Admin")
} }
Roles.new_role("Moderator", "Mod") Roles.new_role("Moderator", "Mod")
:set_permission_group("Admin") :set_permission_group("Mod")
:set_custom_color{ r = 0, g = 170, b = 0 } :set_custom_color{ r = 0, g = 170, b = 0 }
:set_flag("is_admin") :set_flag("is_admin")
:set_flag("is_spectator") :set_flag("is_spectator")
:set_flag("report-immune") :set_flag("report-immune")
:set_flag("instant-respawn") :set_flag("instant-respawn")
:set_parent("Trainee") :set_parent("Trainee Moderator")
Roles.new_role("Trainee Moderator", "TrMod")
:set_permission_group("Mod")
:set_custom_color{ r = 0, g = 170, b = 0 }
:set_flag("is_admin")
:set_flag("is_spectator")
:set_flag("report-immune")
:set_parent("Board Member")
:allow{ :allow{
"command/assign-role", "command/assign-role",
"command/unassign-role", "command/unassign-role",
"command/repair", "command/admin-chat",
"command/kill/always", "command/kill/always",
"command/clear-tag/always", "command/clear-tag/always",
"command/spawn/always", "command/spawn/always",
@@ -80,11 +98,6 @@ Roles.new_role("Moderator", "Mod")
"command/clear-inventory", "command/clear-inventory",
"command/kill-enemies", "command/kill-enemies",
"command/remove-enemies", "command/remove-enemies",
--'command/bonus',
"command/home",
"command/set-home",
"command/get-home",
"command/return",
"command/connect-player", "command/connect-player",
"command/set-bot-queue", "command/set-bot-queue",
"command/set-game-speed", "command/set-game-speed",
@@ -92,42 +105,14 @@ Roles.new_role("Moderator", "Mod")
"command/set-always-day", "command/set-always-day",
"command/set-pollution-enabled", "command/set-pollution-enabled",
"command/clear-pollution", "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/create-warning",
"command/get-warnings", "command/get-warnings",
"command/get-reports", "command/get-reports",
"command/protect-entity", "command/protect-entity",
"command/protect-area", "command/protect-area",
"command/protect-tag", "command/protect-tag",
"command/jail",
"command/unjail",
"command/kick", "command/kick",
"command/ban", "command/ban",
"command/spectate",
"command/follow",
"command/search",
"command/search-online",
"command/search-amount",
"command/search-recent",
"command/clear-blueprints",
"gui/playerdata",
} }
--- Trusted Roles --- Trusted Roles
@@ -137,81 +122,86 @@ Roles.new_role("Board Member", "Board")
:set_flag("is_spectator") :set_flag("is_spectator")
:set_flag("report-immune") :set_flag("report-immune")
:set_flag("instant-respawn") :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") :set_parent("Supporter")
:allow{ :allow{
"gui/rocket-info/toggle-active", "gui/playerdata",
"gui/rocket-info/remote_launch", "command/teleport",
"gui/bonus", "command/bring",
--"command/bonus", "command/search",
"command/home", "command/search-online",
"command/set-home", "command/search-amount",
"command/get-home", "command/search-recent",
"command/return", "command/spectate",
"fast-tree-decon", "command/follow",
"command/clear-blueprints-surface",
} }
Roles.new_role("Supporter", "Sup") Roles.new_role("Supporter", "Sup")
:set_permission_group("Trusted") :set_permission_group("Trusted")
:set_custom_color{ r = 230, g = 99, b = 34 } :set_custom_color{ r = 230, g = 99, b = 34 }
:set_flag("is_spectator") :set_flag("is_spectator")
:set_parent("Veteran") :set_parent("Partner")
:allow{
"command/tag-color",
"command/jail",
"command/unjail",
"command/set-join-message",
"command/remove-join-message",
}
Roles.new_role("Partner", "Part") Roles.new_role("Partner", "Part")
:set_permission_group("Trusted") :set_permission_group("Trusted")
:set_custom_color{ r = 140, g = 120, b = 200 } :set_custom_color{ r = 140, g = 120, b = 200 }
:set_flag("is_spectator") :set_flag("is_spectator")
:set_parent("Senior Member")
Roles.new_role("Senior Member", "SMem")
:set_permission_group("Trusted")
:set_custom_color{ r = 140, g = 120, b = 200 }
:set_flag("is_spectator")
:set_parent("Member")
Roles.new_role("Member", "Mem")
:set_permission_group("Trusted")
:set_custom_color{ r = 24, g = 172, b = 188 }
:set_flag("deconlog-bypass")
:set_parent("Veteran") :set_parent("Veteran")
:allow{ :allow{
"fast-tree-decon",
"gui/bonus",
--"command/tag-color",
"command/set-join-message",
"command/remove-join-message",
"command/home",
"command/set-home",
"command/get-home",
"command/return",
"command/goto",
"command/jail", "command/jail",
"command/unjail", "command/unjail",
"command/repair",
"command/spectate",
"command/follow",
} }
local hours10, hours250 = 10 * 216000, 250 * 60 local hours6, hours250 = 6 * 216000, 250 * 60
Roles.new_role("Veteran", "Vet") Roles.new_role("Veteran", "Vet")
:set_permission_group("Trusted") :set_permission_group("Trusted")
:set_custom_color{ r = 140, g = 120, b = 200 } :set_custom_color{ r = 140, g = 120, b = 200 }
:set_parent("Member") :set_parent("Regular")
:allow{ :allow{
"gui/surveillance",
"gui/warp-list/add",
"gui/warp-list/edit",
"gui/tool",
"gui/vlayer-edit",
"gui/rocket-info/toggle-active",
"gui/rocket-info/remote_launch",
"command/chat-commands", "command/chat-commands",
"command/clear-ground-items", "command/clear-ground-items",
"command/clear-blueprints-radius", "command/clear-blueprints",
"command/set-trains-to-automatic", "command/set-trains-to-automatic",
"command/set-auto-research", "command/lawnmower",
"command/waterfill",
"command/artillery",
"command/save-quickbar",
"command/personal-logistic",
} }
:set_auto_assign_condition(function(player) :set_auto_assign_condition(function(player)
if player.online_time >= hours10 then if player.online_time >= hours6 then
return true return true
else else
local stats = Statistics:get(player, {}) local stats = Statistics:get(player, {})
@@ -221,43 +211,25 @@ Roles.new_role("Veteran", "Vet")
end) end)
--- Standard User Roles --- Standard User Roles
Roles.new_role("Member", "Mem") local hours1, hours15 = 1 * 216000, 15 * 60
: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") Roles.new_role("Regular", "Reg")
:set_permission_group("Standard") :set_permission_group("Standard")
:set_custom_color{ r = 79, g = 155, b = 163 } :set_custom_color{ r = 79, g = 155, b = 163 }
:set_parent("Guest") :set_parent("Guest")
:allow{ :allow{
"standard-decon",
"bypass-entity-protection",
"bypass-nukeprotect",
"gui/task-list/add",
"gui/task-list/edit",
"command/kill", "command/kill",
"command/rainbow", "command/rainbow",
"command/spawn", "command/spawn",
"command/me", "command/me",
"standard-decon", "command/vlayer-info",
"bypass-entity-protection",
"bypass-nukeprotect",
} }
:set_auto_assign_condition(function(player) :set_auto_assign_condition(function(player)
if player.online_time >= hours3 then if player.online_time >= hours1 then
return true return true
else else
local stats = Statistics:get(player, {}) local stats = Statistics:get(player, {})
@@ -271,8 +243,19 @@ local default = Roles.new_role("Guest", "")
:set_permission_group("Guest") :set_permission_group("Guest")
:set_custom_color{ r = 185, g = 187, b = 160 } :set_custom_color{ r = 185, g = 187, b = 160 }
:allow{ :allow{
"command/tag", "gui/player-list",
"command/tag-clear", "gui/rocket-info",
"gui/science-info",
"gui/task-list",
"gui/warp-list",
"gui/readme",
"gui/vlayer",
"gui/research",
"gui/autofill",
"gui/module",
"gui/production",
-- "command/tag",
-- "command/tag-clear",
"command/commands", "command/commands",
"command/get-roles", "command/get-roles",
"command/locate", "command/locate",
@@ -282,18 +265,6 @@ local default = Roles.new_role("Guest", "")
"command/save-data", "command/save-data",
"command/data-preference", "command/data-preference",
"command/connect", "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 --- Jail role
@@ -312,57 +283,69 @@ Roles.define_role_order{
"System", -- Best to keep root at top "System", -- Best to keep root at top
"Senior Administrator", "Senior Administrator",
"Administrator", "Administrator",
"Senior Moderator",
"Moderator", "Moderator",
"Trainee", "Trainee Moderator",
"Board Member", "Board Member",
"Senior Backer",
"Sponsor",
"Supporter", "Supporter",
"Partner", "Partner",
"Veteran", "Senior Member",
"Member", "Member",
"Veteran",
"Regular", "Regular",
"Jail", "Jail",
"Guest", -- Default must be last if you want to apply restrictions to other roles "Guest", -- Default must be last if you want to apply restrictions to other roles
} }
Roles.override_player_roles{ Roles.override_player_roles{
["PHIDIAS0303"] = { "Moderator", "Board Member", "Member" }, ["PHIDIAS0303"] = { "Senior Administrator" },
["aldldl"] = { "Administrator", "Moderator", "Member" }, ["7h3w1z4rd"] = { "Moderator" },
["arty714"] = { "Senior Administrator", "Moderator", "Member" }, ["aldldl"] = { "Moderator" },
["Cooldude2606"] = { "Senior Administrator", "Moderator", "Member" }, ["arty714"] = { "Moderator" },
["Drahc_pro"] = { "Administrator", "Moderator", "Member" }, ["AssemblyStorm"] = { "Moderator" },
["mark9064"] = { "Administrator", "Moderator", "Member" }, ["banakeg"] = { "Moderator" },
["7h3w1z4rd"] = { "Moderator", "Member" }, ["connormkii"] = { "Moderator" },
["FlipHalfling90"] = { "Moderator", "Member" }, ["cydes"] = { "Moderator" },
["hamsterbryan"] = { "Moderator", "Member" }, ["Cooldude2606"] = { "Moderator" },
["HunterOfGames"] = { "Moderator", "Member" }, ["darklich14"] = { "Moderator" },
["NextIdea"] = { "Moderator", "Member" }, ["Drahc_pro"] = { "Moderator" },
["TheKernel32"] = { "Moderator", "Member" }, ["facere"] = { "Moderator" },
["TheKernel64"] = { "Moderator", "Member" }, ["freek18"] = { "Moderator" },
["tovernaar123"] = { "Moderator", "Member" }, ["FlipHalfling90"] = { "Moderator" },
["UUBlueFire"] = { "Moderator", "Member" }, ["Gizan"] = { "Moderator" },
["AssemblyStorm"] = { "Moderator", "Member" }, ["hamsterbryan"] = { "Moderator" },
["banakeg"] = { "Moderator", "Member" }, ["hidden_relic"] = { "Moderator" },
["connormkii"] = { "Moderator", "Member" }, ["Hova"] = { "Moderator" },
["cydes"] = { "Moderator", "Member" }, ["HunterOfGames"] = { "Moderator" },
["darklich14"] = { "Moderator", "Member" }, ["LoicB"] = { "Moderator" },
["facere"] = { "Moderator", "Member" }, ["M74132"] = { "Moderator" },
["freek18"] = { "Moderator", "Member" }, ["MadKatz"] = { "Moderator" },
["Gizan"] = { "Moderator", "Member" }, ["mafisch3"] = { "Moderator" },
["LoicB"] = { "Moderator", "Member" }, ["maplesyrup01"] = { "Moderator" },
["M74132"] = { "Moderator", "Member" }, ["mark9064"] = { "Moderator" },
["mafisch3"] = { "Moderator", "Member" }, ["nallaj"] = { "Moderator" },
["maplesyrup01"] = { "Moderator", "Member" }, ["NextIdea"] = { "Moderator" },
["ookl"] = { "Moderator", "Member" }, ["oof2win2"] = { "Moderator" },
["Phoenix27833"] = { "Moderator", "Member" }, ["ookl"] = { "Moderator" },
["porelos"] = { "Moderator", "Member" }, ["Phoenix27833"] = { "Moderator" },
["Ruuyji"] = { "Moderator", "Member" }, ["porelos"] = { "Moderator" },
["samy115"] = { "Moderator", "Member" }, ["Ruuyji"] = { "Moderator" },
["SilentLog"] = { "Moderator", "Member" }, ["samy115"] = { "Moderator" },
["Tcheko"] = { "Moderator", "Member" }, ["SilentLog"] = { "Moderator" },
["thadius856"] = { "Moderator", "Member" }, ["Tcheko"] = { "Moderator" },
["whoami32"] = { "Moderator", "Member" }, ["thadius856"] = { "Moderator" },
["Windbomb"] = { "Moderator", "Member" }, ["TheKernel32"] = { "Moderator" },
["XenoCyber"] = { "Moderator", "Member" }, ["TheKernel64"] = { "Moderator" },
["tovernaar123"] = { "Moderator" },
["UUBlueFire"] = { "Moderator" },
["whoami32"] = { "Moderator" },
["Windbomb"] = { "Moderator" },
["Windsinger"] = { "Moderator" },
["XenoCyber"] = { "Moderator" },
["Athenaa"] = { "Trainee Moderator" },
["hihicome3"] = { "Trainee Moderator" },
["JamesJung"] = { "Trainee Moderator" },
["konohaScarlet_"] = { "Trainee Moderator" },
["majoro"] = { "Trainee Moderator" },
["rongli"] = { "Trainee Moderator" },
} }
@@ -7,33 +7,24 @@
local ExpUtil = require("modules/exp_util") local ExpUtil = require("modules/exp_util")
local Gui = require("modules/exp_gui") 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 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_scenario/control/jail")
local Jail = require("modules.exp_legacy.modules.control.jail") --- @dep modules.control.jail
local Colors = require("modules/exp_util/include/color") local Colors = require("modules/exp_util/include/color")
local format_player_name = ExpUtil.format_player_name_locale local format_player_name = ExpUtil.format_player_name_locale
local SelectedPlayer, SelectedAction --- Accessors injected by the gui so the actions can read the selected player and set the selected action
local function set_datastores(player, action) local get_selected_player --- @type fun(player: LuaPlayer): LuaPlayer
SelectedPlayer, SelectedAction = player, action local set_selected_action --- @type fun(player: LuaPlayer, action: string?)
end local function set_accessors(player_getter, action_setter)
get_selected_player, set_selected_action = player_getter, action_setter
-- 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
end end
-- gets the action player and a coloured name for the action to be used on -- gets the action player and a coloured name for the action to be used on
local function get_action_player_name(player) local function get_action_player(player)
local selected_player_name = SelectedPlayer:get(player) local selected_player = get_selected_player(player) --[[@as LuaPlayer]]
local selected_player = game.players[selected_player_name]
local selected_player_color = format_player_name(selected_player) local selected_player_color = format_player_name(selected_player)
return selected_player_name, selected_player_color return selected_player, selected_player_color
end end
-- teleports one player to another -- teleports one player to another
@@ -63,12 +54,11 @@ end
--- Teleports the user to the action player --- Teleports the user to the action player
-- @element goto_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) :on_click(function(def, player, element)
local selected_player_name = get_action_player_name(player) local selected_player = get_action_player(player)
local selected_player = game.players[selected_player_name]
if not player.character or not selected_player.character then 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 else
teleport(player, selected_player) teleport(player, selected_player)
end end
@@ -76,12 +66,11 @@ local goto_player = new_button("utility/export", { "player-list.goto-player" })
--- Teleports the action player to the user --- Teleports the action player to the user
-- @element bring_player -- @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) :on_click(function(def, player, element)
local selected_player_name = get_action_player_name(player) local selected_player = get_action_player(player)
local selected_player = game.players[selected_player_name]
if not player.character or not selected_player.character then 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 else
teleport(selected_player, player) teleport(selected_player, player)
end 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 --- Reports the action player, requires a reason to be given
-- @element report_player -- @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) :on_click(function(def, player, element)
local selected_player_name = get_action_player_name(player) local selected_player = get_action_player(player)
if Reports.is_reported(selected_player_name, player.name) then if Reports.is_reported(selected_player.name, player.name) then
player.print({ "exp-commands_report.already-reported" }, Colors.orange_red) player.print({ "exp-commands_report.already-reported" }, Colors.orange_red)
else else
SelectedAction:set(player, "command/report") set_selected_action(player, "exp_scenario.command.create_report")
end end
end) end)
local function report_player_callback(player, reason) 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) local by_player_name_color = format_player_name(player)
game.print{ "exp-commands_reports.response", selected_player_color, reason } 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 }) local trainee = Roles.get_role_by_name("Trainee")
Reports.report_player(selected_player_name, player.name, reason) for _, role in ipairs(trainee and Roles.get_higher_roles(trainee) or {}) do
end role:print{ "exp-commands_reports.response-admin", selected_player_color, by_player_name_color, reason }
end
--- Gives the action player a warning, requires a reason Reports.report_player(selected_player.name, player.name, 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)
end end
--- Jails the action player, requires a reason --- Jails the action player, requires a reason
-- @element jail_player -- @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) :on_click(function(def, player, element)
local selected_player_name, selected_player_color = get_action_player_name(player) local selected_player, selected_player_color = get_action_player(player)
if Jail.is_jailed(selected_player_name) then if Jail.is_jailed(selected_player) then
player.print({ "exp-commands_jail.already-jailed", selected_player_color }, Colors.orange_red) player.print({ "exp-commands_jail.already-jailed", selected_player_color }, Colors.orange_red)
else else
SelectedAction:set(player, "command/jail") set_selected_action(player, "exp_scenario.command.jail")
end end
end) end)
local function jail_player_callback(player, reason) 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) local by_player_name_color = format_player_name(player)
game.print{ "exp-commands_jail.jailed", selected_player_color, by_player_name_color, reason } 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 end
--- Kicks the action player, requires a reason --- Kicks the action player, requires a reason
-- @element kick_player -- @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) :on_click(function(def, player, element)
SelectedAction:set(player, "command/kick") set_selected_action(player, "exp_scenario.gui.player_list.kick")
end) end)
local function kick_player_callback(player, reason) 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) game.kick_player(selected_player, reason)
end end
--- Bans the action player, requires a reason --- Bans the action player, requires a reason
-- @element ban_player -- @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) :on_click(function(def, player, element)
SelectedAction:set(player, "command/ban") set_selected_action(player, "exp_scenario.gui.player_list.ban")
end) end)
local function ban_player_callback(player, reason) 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) game.ban_player(selected_player, reason)
end end
return { return {
set_datastores = set_datastores, set_accessors = set_accessors,
buttons = { buttons = {
["command/teleport"] = { ["exp_scenario.command.teleport"] = {
auth = function(player, selected_player) auth = function(player, selected_player)
return player.name ~= selected_player.name return player.name ~= selected_player.name
end, -- cant teleport to your self end, -- cant teleport to your self
goto_player, goto_player,
bring_player, bring_player,
}, },
["command/report"] = { ["exp_scenario.command.create_report"] = {
auth = function(player, selected_player) auth = function(player, selected_player)
if player == selected_player then return false end if player == selected_player then return false end
if not Roles.player_allowed(player, "command/give-warning") then return not Roles.player_has_permission(selected_player, "exp_scenario.bypass.reports")
return not Roles.player_has_flag(selected_player, "report-immune") end, -- can report any player that isn't immune
end
end, -- can report any player that isn't immune and you aren't able to give warnings
reason_callback = report_player_callback, reason_callback = report_player_callback,
report_player, report_player,
}, },
["command/give-warning"] = { ["exp_scenario.command.jail"] = {
auth = auth_lower_role, -- warn a lower user, replaces report auth = Roles.player_outranks,
reason_callback = warn_player_callback,
warn_player,
},
["command/jail"] = {
auth = auth_lower_role,
reason_callback = jail_player_callback, reason_callback = jail_player_callback,
jail_player, jail_player,
}, },
["command/kick"] = { ["exp_scenario.gui.player_list.kick"] = {
auth = auth_lower_role, auth = Roles.player_outranks,
reason_callback = kick_player_callback, reason_callback = kick_player_callback,
kick_player, kick_player,
}, },
["command/ban"] = { ["exp_scenario.gui.player_list.ban"] = {
auth = auth_lower_role, auth = Roles.player_outranks,
reason_callback = ban_player_callback, reason_callback = ban_player_callback,
ban_player, 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_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 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_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 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_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 { return {
-- Adding tasks -- Adding tasks
allow_add_task = "all", --- @setting allow_add_task dictates who is allowed to add new tasks; values: all, admin, expcore.roles, none allow_add_task = "all", --- @setting allow_add_task dictates who is allowed to add new tasks; values: all, admin, exp_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 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 -- 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 allow_edit_task = "exp_roles", --- @setting allow_edit_task dictates who is allowed to edit existing tasks; values: all, admin, exp_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 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 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 default_icon = { type = "item", name = "discharge-defense-equipment" }, --- @setting default_icon the default icon that will be used for warps
-- Warp cooldowns -- 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 bypass_warp_cooldown = "exp_roles", --- @setting bypass_warp_cooldown dictates who the warp cooldown is applied to; values: all, admin, exp_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 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 cooldown_duration = 60, --- @setting cooldown_duration the duration of the warp cooldown in seconds
-- Warp proximity -- 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 bypass_warp_proximity = "exp_roles", --- @setting bypass_warp_proximity dictates who the warp proximity is applied to; values: all, admin, exp_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 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 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 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 -- Adding warps
allow_add_warp = "expcore.roles", --- @setting allow_add_warp dictates who is allowed to add warps; values: all, admin, expcore.roles, none allow_add_warp = "exp_roles", --- @setting allow_add_warp dictates who is allowed to add warps; values: all, admin, exp_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 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 -- Editing warps
allow_edit_warp = "expcore.roles", --- @setting allow_edit_warp dictates who is allowed to edit warps; values: all, admin, expcore.roles, none allow_edit_warp = "exp_roles", --- @setting allow_edit_warp dictates who is allowed to edit warps; values: all, admin, exp_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 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 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 -- Warp area generation
+1 -1
View File
@@ -4,6 +4,6 @@
local events = defines.events local events = defines.events
return { return {
events.on_player_banned, events.on_player_banned,
events.on_player_kicked, -- events.on_player_kicked,
-- events.on_player_left_game -- events.on_player_left_game
} }
@@ -1,8 +1,2 @@
return { return {
Cooldude2606 = "Lua lets you set metatables on numbers, did you know that? Cooldude2606 knows this.",
samy115 = "Tremble in fear as the banhammer is now here, its owner: samy115",
XenoCyber = '"Fire Fire Fire" oops wrong game, have no fear XenoCyber is here',
HunterOfGames = "Unable to support HunterOfGames. You must construct additional miners.",
ookl = 'ookl says: "Pineapples are amazing, hello everyone!"',
arty714 = "Arty\'s Potato made it!",
} }
+1
View File
@@ -4,4 +4,5 @@
return { return {
fluid = true, --- @setting fluid When true, checks for for fluid pipes when removing miners 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 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 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 { return {
ignore_admins = true, --- @setting ignore_admins If admins are ignored by the protection filter 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_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 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 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_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 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_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 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 -- @config Repair
return { 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_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_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 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 -- this enable 20 more inventory for each mining productivity level up to 4
bonus_inventory = { bonus_inventory = {
enabled = true, enabled = true,
log = {
["base"] = {
["name"] = "mining-productivity-4",
["level"] = 4
},
["space-age"] = {
["name"] = "mining-productivity-3",
["level"] = 3
},
},
res = { res = {
-- Mining Productivity -- Mining Productivity
["mining-productivity"] = true, ["mining-productivity"] = true,
@@ -33,7 +23,19 @@ return {
rate = 5, rate = 5,
limit = 20, 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 = { milestone = {
["base"] = { ["base"] = {
["automation"] = 600, ["automation"] = 600,
+45 -146
View File
@@ -4,10 +4,10 @@
return { return {
spawn_area = { --- @setting spawn_area Settings relating to the whole spawn area spawn_area = { --- @setting spawn_area Settings relating to the whole spawn area
-- Enable predefined patches: 128, else: 32 -- Enable predefined patches: 128, else: 32
deconstruction_radius = 20, -- @setting deconstruction_radius All entities within this radius will be removed deconstruction_radius = 12, -- @setting deconstruction_radius All entities within this radius will be removed
tile_radius = 20, tile_radius = 14,
deconstruction_tile = "concrete", --- @setting deconstruction_tile Tile to be placed in the deconstruction radius, use nil for map gen deconstruction_tile = "refined-concrete", --- @setting deconstruction_tile Tile to be placed in the deconstruction radius, use nil for map gen
landfill_radius = 50, --- @setting pattern_radius All water within this radius will be land filled landfill_radius = 16, --- @setting pattern_radius All water within this radius will be land filled
}, },
turrets = { --- @setting turrets Settings relating to adding turrets to spawn turrets = { --- @setting turrets Settings relating to adding turrets to spawn
enabled = true, --- @setting enabled Whether turrets will be added to spawn enabled = true, --- @setting enabled Whether turrets will be added to spawn
@@ -15,10 +15,10 @@ return {
refill_time = 60 * 60 * 5, --- @setting refill_time The time in ticks between each refill of the turrets, only change if having lag issues refill_time = 60 * 60 * 5, --- @setting refill_time The time in ticks between each refill of the turrets, only change if having lag issues
offset = { x = 0, y = 0 }, --- @setting offset The position offset to apply to turrets offset = { x = 0, y = 0 }, --- @setting offset The position offset to apply to turrets
locations = { --- @setting locations The locations of all turrets, this list can change during runtime locations = { --- @setting locations The locations of all turrets, this list can change during runtime
{ -3, -3 }, { -2, -2 },
{ 3, -3 }, { 2, -2 },
{ -3, 3 }, { -2, 2 },
{ 3, 3 }, { 2, 2 },
}, },
}, },
afk_belts = { --- @setting afk_belts Settings relating to adding afk belts to spawn afk_belts = { --- @setting afk_belts Settings relating to adding afk belts to spawn
@@ -27,8 +27,10 @@ return {
protected = true, --- @setting protected Whether belts will be protected from player interaction protected = true, --- @setting protected Whether belts will be protected from player interaction
offset = { x = 0, y = 0 }, --- @setting offset The position offset to apply to afk belts offset = { x = 0, y = 0 }, --- @setting offset The position offset to apply to afk belts
locations = { --- @setting locations The locations to spawn afk belts at, given as the top left position locations = { --- @setting locations The locations to spawn afk belts at, given as the top left position
{ -5, -5 }, { 5, -5 }, { 4, 4 },
{ -5, 5 }, { 5, 5 }, { 4, -4 },
{ -4, 4 },
{ -4, -4 }
}, },
}, },
water = { --- @setting water Settings relating to adding water to spawn water = { --- @setting water Settings relating to adding water to spawn
@@ -36,147 +38,44 @@ return {
water_tile = "water-mud", --- @setting water_tile The tile to be used as the water tile water_tile = "water-mud", --- @setting water_tile The tile to be used as the water tile
offset = { x = 0, y = 0 }, --- @setting offset The position offset to apply to water tiles offset = { x = 0, y = 0 }, --- @setting offset The position offset to apply to water tiles
locations = { --- @setting locations The location of the water tiles {x,y} locations = { --- @setting locations The location of the water tiles {x,y}
-- Each is a 3x3 with the closest tile to 0,0 removed { 5, 6 }, { 5, 7 }, { 6, 5 }, { 6, 6 }, { 6, 7 }, { 7, 5 }, { 7, 6 }, { 7, 7 },
{ 7, 8 }, { 7, 9 }, { 8, 7 }, { 8, 8 }, { 8, 9 }, { 9, 7 }, { 9, 8 }, { 9, 9 }, -- Bottom Right { 5, -7 }, { 5, -8 }, { 6, -6 }, { 6, -7 }, { 6, -8 }, { 7, -6 }, { 7, -7 }, { 7, -8 },
{ 7, -9 }, { 7, -10 }, { 8, -8 }, { 8, -9 }, { 8, -10 }, { 9, -8 }, { 9, -9 }, { 9, -10 }, -- Top Right { -6, 6 }, { -6, 7 }, { -7, 5 }, { -7, 6 }, { -7, 7 }, { -8, 5 }, { -8, 6 }, { -8, 7 },
{ -8, -9 }, { -8, -10 }, { -9, -8 }, { -9, -9 }, { -9, -10 }, { -10, -8 }, { -10, -9 }, { -10, -10 }, -- Top Left { -6, -7 }, { -6, -8 }, { -7, -6 }, { -7, -7 }, { -7, -8 }, { -8, -6 }, { -8, -7 }, { -8, -8 },
{ -8, 8 }, { -8, 9 }, { -9, 7 }, { -9, 8 }, { -9, 9 }, { -10, 7 }, { -10, 8 }, { -10, 9 }, -- Bottom Left
}, },
}, },
entities = { --- @setting entities Settings relating to adding entities to spawn entities = { --- @setting entities Settings relating to adding entities to spawn
enabled = true, --- @setting enabled Whether entities will be added to spawn enabled = true, --- @setting enabled Whether entities will be added to spawn
protected = true, --- @setting protected Whether entities will be protected from player interaction protected = true, --- @setting protected Whether entities will be protected from player interaction
operable = true, --- @setting operable Whether entities can be opened by players, must be true if chests are used operable = true, --- @setting operable Whether entities can be opened by players, must be true if chests are used
offset = { x = 0, y = -2 }, --- @setting offset The position offset to apply to entities offset = { x = 0, y = 0 }, --- @setting offset The position offset to apply to entities
locations = { --- @setting locations The location and names of entities {name,x,y} locations = { --- @setting locations The location and names of entities {name,x,y}
{ "stone-wall", -10, -5 }, { "stone-wall", -10, -4 }, { "stone-wall", -10, -3 }, { "stone-wall", -10, -2 }, { "stone-wall", -10, -1 }, { "stone-wall", -10, 0 }, { "stone-wall", -10, 3 }, { "stone-wall", -10, 4 }, { "stone-wall", -10, 5 }, { "steel-chest", 1, 3 }, { "steel-chest", 1, 4 }, { "steel-chest", 1, 5 },
{ "stone-wall", -10, 6 }, { "stone-wall", -10, 7 }, { "stone-wall", -10, 8 }, { "small-lamp", -8, -4 }, { "small-lamp", -8, -1 }, { "iron-chest", -8, 0 }, { "iron-chest", -8, 3 }, { "small-lamp", -8, 4 }, { "steel-chest", 3, 1 }, { "steel-chest", 4, 1 }, { "steel-chest", 5, 1 },
{ "small-lamp", -8, 7 }, { "stone-wall", -7, -8 }, { "small-electric-pole", -7, -2 }, { "iron-chest", -7, 0 }, { "iron-chest", -7, 3 }, { "small-electric-pole", -7, 5 }, { "stone-wall", -7, 11 }, { "stone-wall", -6, -8 }, { "small-lamp", -6, -6 }, { "steel-chest", 1, -4 }, { "steel-chest", 1, -5 }, { "steel-chest", 1, -6 },
{ "iron-chest", -6, 0 }, { "iron-chest", -6, 3 }, { "small-lamp", -6, 9 }, { "stone-wall", -6, 11 }, { "stone-wall", -5, -8 }, { "small-lamp", -5, -1 }, { "iron-chest", -5, 0 }, { "iron-chest", -5, 3 }, { "small-lamp", -5, 4 }, { "stone-wall", -5, 11 }, { "steel-chest", 3, -2 }, { "steel-chest", 4, -2 }, { "steel-chest", 5, -2 },
{ "stone-wall", -4, -8 }, { "small-electric-pole", -4, -5 }, { "iron-chest", -4, 0 }, { "iron-chest", -4, 3 }, { "small-electric-pole", -4, 8 }, { "stone-wall", -4, 11 }, { "stone-wall", -3, -8 }, { "small-lamp", -3, -6 }, { "small-lamp", -3, -3 }, { "small-lamp", -3, 6 }, { "steel-chest", -2, 3 }, { "steel-chest", -2, 4 }, { "steel-chest", -2, 5 },
{ "small-lamp", -3, 9 }, { "stone-wall", -3, 11 }, { "stone-wall", -2, -8 }, { "iron-chest", -2, -6 }, { "iron-chest", -2, -5 }, { "iron-chest", -2, -4 }, { "iron-chest", -2, -3 }, { "iron-chest", -2, -2 }, { "iron-chest", -2, 5 }, { "iron-chest", -2, 6 }, { "steel-chest", -4, 1 }, { "steel-chest", -5, 1 }, { "steel-chest", -6, 1 },
{ "iron-chest", -2, 7 }, { "iron-chest", -2, 8 }, { "iron-chest", -2, 9 }, { "stone-wall", -2, 11 }, { "stone-wall", 1, -8 }, { "iron-chest", 1, -6 }, { "steel-chest", -2, -4 }, { "steel-chest", -2, -5 }, { "steel-chest", -2, -6 },
{ "iron-chest", 1, -5 }, { "iron-chest", 1, -4 }, { "iron-chest", 1, -3 }, { "iron-chest", 1, -2 }, { "iron-chest", 1, 5 }, { "iron-chest", 1, 6 }, { "iron-chest", 1, 7 }, { "iron-chest", 1, 8 }, { "iron-chest", 1, 9 }, { "stone-wall", 1, 11 }, { "steel-chest", -4, -2 }, { "steel-chest", -5, -2 }, { "steel-chest", -6, -2 },
{ "stone-wall", 2, -8 }, { "small-lamp", 2, -6 }, { "small-lamp", 2, -3 }, { "small-lamp", 2, 6 }, { "small-lamp", 2, 9 }, { "stone-wall", 2, 11 }, { "stone-wall", 3, -8 }, { "small-electric-pole", 3, -5 }, { "iron-chest", 3, 0 }, { "iron-chest", 3, 3 }, { "medium-electric-pole", 2, 3 }, { "medium-electric-pole", 2, -4 }, { "medium-electric-pole", -3, 3 }, { "medium-electric-pole", -3, -4 },
{ "small-electric-pole", 3, 8 }, { "stone-wall", 3, 11 }, { "stone-wall", 4, -8 }, { "small-lamp", 4, -1 }, { "iron-chest", 4, 0 }, { "iron-chest", 4, 3 }, { "small-lamp", 4, 4 }, { "stone-wall", 4, 11 }, { "stone-wall", 5, -8 }, { "small-lamp", 5, -6 }, { "small-lamp", 2, 4 }, { "small-lamp", 2, -5 }, { "small-lamp", -3, 4 }, { "small-lamp", -3, -5 },
{ "iron-chest", 5, 0 }, { "iron-chest", 5, 3 }, { "small-lamp", 5, 9 }, { "stone-wall", 5, 11 }, { "stone-wall", 6, -8 }, { "small-electric-pole", 6, -2 }, { "iron-chest", 6, 0 }, { "iron-chest", 6, 3 }, { "small-electric-pole", 6, 5 }, { "stone-wall", 6, 11 }, { "small-lamp", 4, 2 }, { "small-lamp", 4, -3 }, { "small-lamp", -5, 2 }, { "small-lamp", -5, -3 },
{ "small-lamp", 7, -4 }, { "small-lamp", 7, -1 }, { "iron-chest", 7, 0 }, { "iron-chest", 7, 3 }, { "small-lamp", 7, 4 }, { "small-lamp", 7, 7 }, { "stone-wall", 9, -5 }, { "stone-wall", 1, 7 }, { "stone-wall", 2, 7 }, { "stone-wall", 3, 7 }, { "stone-wall", 4, 7 },
{ "stone-wall", 9, -4 }, { "stone-wall", 9, -3 }, { "stone-wall", 9, -2 }, { "stone-wall", 9, -1 }, { "stone-wall", 9, 0 }, { "stone-wall", 9, 3 }, { "stone-wall", 9, 4 }, { "stone-wall", 9, 5 }, { "stone-wall", 9, 6 }, { "stone-wall", 9, 7 }, { "stone-wall", 7, 1 }, { "stone-wall", 7, 2 }, { "stone-wall", 7, 3 }, { "stone-wall", 7, 4 },
{ "stone-wall", 9, 8 }, { "stone-wall", 1, -8 }, { "stone-wall", 2, -8 }, { "stone-wall", 3, -8 }, { "stone-wall", 4, -8 },
{ "stone-wall", 7, -2 }, { "stone-wall", 7, -3 }, { "stone-wall", 7, -4 }, { "stone-wall", 7, -5 },
{ "stone-wall", -2, 7 }, { "stone-wall", -3, 7 }, { "stone-wall", -4, 7 }, { "stone-wall", -5, 7 },
{ "stone-wall", -8, 1 }, { "stone-wall", -8, 2 }, { "stone-wall", -8, 3 }, { "stone-wall", -8, 4 },
{ "stone-wall", -2, -8 }, { "stone-wall", -3, -8 }, { "stone-wall", -4, -8 }, { "stone-wall", -5, -8 },
{ "stone-wall", -8, -2 }, { "stone-wall", -8, -3 }, { "stone-wall", -8, -4 }, { "stone-wall", -8, -5 },
}, },
}, },
pattern = { pattern = {
enabled = true, --- @setting enabled Whether pattern tiles will be added to spawn enabled = false, --- @setting enabled Whether pattern tiles will be added to spawn
pattern_tile = "stone-path", --- @setting pattern_tile The tile to be used for the pattern pattern_tile = "refined-concrete", --- @setting pattern_tile The tile to be used for the pattern
offset = { x = 0, y = -2 }, --- @setting offset The position offset to apply to pattern tiles offset = { x = 0, y = 0 }, --- @setting offset The position offset to apply to pattern tiles
locations = { --- @setting locations The location of the pattern tiles {x,y} locations = { --- @setting locations The location of the pattern tiles {x,y}
{ -49, -3 }, { -49, -2 }, { -49, 1 }, { -49, 2 }, { -49, 5 }, { -49, 6 }, { -48, -4 }, { -48, -3 }, { -48, -2 }, { -48, 1 }, { -48, 2 }, { -48, 5 }, { -48, 6 }, { -48, 7 }, { -47, -7 }, { -47, -6 }, { -47, -5 }, { -47, -4 }, { -47, -3 }, { -47, -2 }, { -47, 5 }, { -47, 6 }, { -47, 7 }, { -47, 8 }, { -47, 9 }, { -47, 10 }, { -46, -8 }, { -46, -7 }, { -46, -6 }, { -46, -5 },
{ -46, -4 }, { -46, -3 }, { -46, -2 }, { -46, -1 }, { -46, 4 }, { -46, 5 }, { -46, 6 }, { -46, 7 }, { -46, 8 }, { -46, 9 }, { -46, 10 }, { -46, 11 }, { -45, -17 }, { -45, -16 }, { -45, -15 }, { -45, -14 }, { -45, -13 }, { -45, -12 }, { -45, -9 }, { -45, -8 }, { -45, -7 }, { -45, -2 }, { -45, -1 }, { -45, 0 }, { -45, 1 }, { -45, 2 }, { -45, 3 }, { -45, 4 }, { -45, 5 }, { -45, 10 },
{ -45, 11 }, { -45, 12 }, { -45, 15 }, { -45, 16 }, { -45, 17 }, { -45, 18 }, { -45, 19 }, { -45, 20 }, { -44, -18 }, { -44, -17 }, { -44, -16 }, { -44, -15 }, { -44, -14 }, { -44, -13 }, { -44, -12 }, { -44, -9 }, { -44, -8 }, { -44, -1 }, { -44, 0 }, { -44, 1 }, { -44, 2 }, { -44, 3 }, { -44, 4 }, { -44, 11 }, { -44, 12 }, { -44, 15 }, { -44, 16 }, { -44, 17 }, { -44, 18 }, { -44, 19 },
{ -44, 20 }, { -44, 21 }, { -43, -19 }, { -43, -18 }, { -43, -17 }, { -43, -1 }, { -43, 0 }, { -43, 1 }, { -43, 2 }, { -43, 3 }, { -43, 4 }, { -43, 20 }, { -43, 21 }, { -43, 22 }, { -42, -19 }, { -42, -18 }, { -42, -1 }, { -42, 0 }, { -42, 1 }, { -42, 2 }, { -42, 3 }, { -42, 4 }, { -42, 21 }, { -42, 22 }, { -41, -25 }, { -41, -24 }, { -41, -19 }, { -41, -18 }, { -41, -13 }, { -41, -12 },
{ -41, -11 }, { -41, -10 }, { -41, -5 }, { -41, -4 }, { -41, 7 }, { -41, 8 }, { -41, 13 }, { -41, 14 }, { -41, 15 }, { -41, 16 }, { -41, 21 }, { -41, 22 }, { -41, 27 }, { -41, 28 }, { -40, -26 }, { -40, -25 }, { -40, -24 }, { -40, -20 }, { -40, -19 }, { -40, -18 }, { -40, -13 }, { -40, -12 }, { -40, -11 }, { -40, -10 }, { -40, -5 }, { -40, -4 }, { -40, 7 }, { -40, 8 }, { -40, 13 }, { -40, 14 },
{ -40, 15 }, { -40, 16 }, { -40, 21 }, { -40, 22 }, { -40, 23 }, { -40, 27 }, { -40, 28 }, { -40, 29 }, { -39, -27 }, { -39, -26 }, { -39, -25 }, { -39, -24 }, { -39, -21 }, { -39, -20 }, { -39, -19 }, { -39, -13 }, { -39, -12 }, { -39, -5 }, { -39, -4 }, { -39, -3 }, { -39, -2 }, { -39, -1 }, { -39, 0 }, { -39, 1 }, { -39, 2 }, { -39, 3 }, { -39, 4 }, { -39, 5 }, { -39, 6 }, { -39, 7 },
{ -39, 8 }, { -39, 15 }, { -39, 16 }, { -39, 22 }, { -39, 23 }, { -39, 24 }, { -39, 27 }, { -39, 28 }, { -39, 29 }, { -39, 30 }, { -38, -27 }, { -38, -26 }, { -38, -25 }, { -38, -24 }, { -38, -21 }, { -38, -20 }, { -38, -13 }, { -38, -12 }, { -38, -5 }, { -38, -4 }, { -38, -3 }, { -38, -2 }, { -38, -1 }, { -38, 0 }, { -38, 1 }, { -38, 2 }, { -38, 3 }, { -38, 4 }, { -38, 5 }, { -38, 6 },
{ -38, 7 }, { -38, 8 }, { -38, 15 }, { -38, 16 }, { -38, 23 }, { -38, 24 }, { -38, 27 }, { -38, 28 }, { -38, 29 }, { -38, 30 }, { -37, -17 }, { -37, -16 }, { -37, -13 }, { -37, -12 }, { -37, -11 }, { -37, -10 }, { -37, -4 }, { -37, -3 }, { -37, -2 }, { -37, -1 }, { -37, 0 }, { -37, 3 }, { -37, 4 }, { -37, 5 }, { -37, 6 }, { -37, 7 }, { -37, 13 }, { -37, 14 }, { -37, 15 }, { -37, 16 },
{ -37, 19 }, { -37, 20 }, { -36, -17 }, { -36, -16 }, { -36, -13 }, { -36, -12 }, { -36, -11 }, { -36, -10 }, { -36, -9 }, { -36, -3 }, { -36, -2 }, { -36, -1 }, { -36, 0 }, { -36, 3 }, { -36, 4 }, { -36, 5 }, { -36, 6 }, { -36, 12 }, { -36, 13 }, { -36, 14 }, { -36, 15 }, { -36, 16 }, { -36, 19 }, { -36, 20 }, { -35, -29 }, { -35, -28 }, { -35, -23 }, { -35, -22 }, { -35, -17 }, { -35, -16 },
{ -35, -12 }, { -35, -11 }, { -35, -10 }, { -35, -9 }, { -35, -8 }, { -35, 11 }, { -35, 12 }, { -35, 13 }, { -35, 14 }, { -35, 15 }, { -35, 19 }, { -35, 20 }, { -35, 25 }, { -35, 26 }, { -35, 31 }, { -35, 32 }, { -34, -30 }, { -34, -29 }, { -34, -28 }, { -34, -23 }, { -34, -22 }, { -34, -17 }, { -34, -16 }, { -34, -15 }, { -34, -11 }, { -34, -10 }, { -34, -9 }, { -34, -8 }, { -34, 11 }, { -34, 12 },
{ -34, 13 }, { -34, 14 }, { -34, 18 }, { -34, 19 }, { -34, 20 }, { -34, 25 }, { -34, 26 }, { -34, 31 }, { -34, 32 }, { -34, 33 }, { -33, -31 }, { -33, -30 }, { -33, -29 }, { -33, -28 }, { -33, -23 }, { -33, -22 }, { -33, -16 }, { -33, -15 }, { -33, -14 }, { -33, -5 }, { -33, -4 }, { -33, -1 }, { -33, 0 }, { -33, 3 }, { -33, 4 }, { -33, 7 }, { -33, 8 }, { -33, 17 }, { -33, 18 }, { -33, 19 },
{ -33, 25 }, { -33, 26 }, { -33, 31 }, { -33, 32 }, { -33, 33 }, { -33, 34 }, { -32, -32 }, { -32, -31 }, { -32, -30 }, { -32, -29 }, { -32, -28 }, { -32, -27 }, { -32, -23 }, { -32, -22 }, { -32, -21 }, { -32, -15 }, { -32, -14 }, { -32, -6 }, { -32, -5 }, { -32, -4 }, { -32, -1 }, { -32, 0 }, { -32, 3 }, { -32, 4 }, { -32, 7 }, { -32, 8 }, { -32, 9 }, { -32, 17 }, { -32, 18 }, { -32, 24 },
{ -32, 25 }, { -32, 26 }, { -32, 30 }, { -32, 31 }, { -32, 32 }, { -32, 33 }, { -32, 34 }, { -32, 35 }, { -31, -33 }, { -31, -32 }, { -31, -31 }, { -31, -30 }, { -31, -29 }, { -31, -28 }, { -31, -27 }, { -31, -26 }, { -31, -22 }, { -31, -21 }, { -31, -20 }, { -31, -19 }, { -31, -18 }, { -31, -11 }, { -31, -10 }, { -31, -9 }, { -31, -8 }, { -31, -7 }, { -31, -6 }, { -31, -5 }, { -31, -1 }, { -31, 0 },
{ -31, 1 }, { -31, 2 }, { -31, 3 }, { -31, 4 }, { -31, 8 }, { -31, 9 }, { -31, 10 }, { -31, 11 }, { -31, 12 }, { -31, 13 }, { -31, 14 }, { -31, 21 }, { -31, 22 }, { -31, 23 }, { -31, 24 }, { -31, 25 }, { -31, 29 }, { -31, 30 }, { -31, 31 }, { -31, 32 }, { -31, 33 }, { -31, 34 }, { -31, 35 }, { -31, 36 }, { -30, -33 }, { -30, -32 }, { -30, -31 }, { -30, -30 }, { -30, -29 }, { -30, -28 },
{ -30, -27 }, { -30, -26 }, { -30, -21 }, { -30, -20 }, { -30, -19 }, { -30, -18 }, { -30, -11 }, { -30, -10 }, { -30, -9 }, { -30, -8 }, { -30, -7 }, { -30, -6 }, { -30, -1 }, { -30, 0 }, { -30, 1 }, { -30, 2 }, { -30, 3 }, { -30, 4 }, { -30, 9 }, { -30, 10 }, { -30, 11 }, { -30, 12 }, { -30, 13 }, { -30, 14 }, { -30, 21 }, { -30, 22 }, { -30, 23 }, { -30, 24 }, { -30, 29 }, { -30, 30 },
{ -30, 31 }, { -30, 32 }, { -30, 33 }, { -30, 34 }, { -30, 35 }, { -30, 36 }, { -29, -37 }, { -29, -36 }, { -29, -30 }, { -29, -29 }, { -29, -28 }, { -29, -27 }, { -29, -26 }, { -29, -15 }, { -29, -14 }, { -29, -10 }, { -29, -9 }, { -29, -8 }, { -29, -7 }, { -29, 10 }, { -29, 11 }, { -29, 12 }, { -29, 13 }, { -29, 17 }, { -29, 18 }, { -29, 29 }, { -29, 30 }, { -29, 31 }, { -29, 32 }, { -29, 33 },
{ -29, 39 }, { -29, 40 }, { -28, -38 }, { -28, -37 }, { -28, -36 }, { -28, -29 }, { -28, -28 }, { -28, -27 }, { -28, -26 }, { -28, -16 }, { -28, -15 }, { -28, -14 }, { -28, -9 }, { -28, -8 }, { -28, 11 }, { -28, 12 }, { -28, 17 }, { -28, 18 }, { -28, 19 }, { -28, 29 }, { -28, 30 }, { -28, 31 }, { -28, 32 }, { -28, 39 }, { -28, 40 }, { -28, 41 }, { -27, -39 }, { -27, -38 }, { -27, -37 }, { -27, -36 },
{ -27, -23 }, { -27, -22 }, { -27, -19 }, { -27, -18 }, { -27, -17 }, { -27, -16 }, { -27, -15 }, { -27, -5 }, { -27, -4 }, { -27, -1 }, { -27, 0 }, { -27, 1 }, { -27, 2 }, { -27, 3 }, { -27, 4 }, { -27, 7 }, { -27, 8 }, { -27, 18 }, { -27, 19 }, { -27, 20 }, { -27, 21 }, { -27, 22 }, { -27, 25 }, { -27, 26 }, { -27, 39 }, { -27, 40 }, { -27, 41 }, { -27, 42 }, { -26, -39 }, { -26, -38 },
{ -26, -37 }, { -26, -36 }, { -26, -24 }, { -26, -23 }, { -26, -22 }, { -26, -19 }, { -26, -18 }, { -26, -17 }, { -26, -16 }, { -26, -6 }, { -26, -5 }, { -26, -4 }, { -26, -1 }, { -26, 0 }, { -26, 1 }, { -26, 2 }, { -26, 3 }, { -26, 4 }, { -26, 7 }, { -26, 8 }, { -26, 9 }, { -26, 19 }, { -26, 20 }, { -26, 21 }, { -26, 22 }, { -26, 25 }, { -26, 26 }, { -26, 27 }, { -26, 39 }, { -26, 40 },
{ -26, 41 }, { -26, 42 }, { -25, -33 }, { -25, -32 }, { -25, -31 }, { -25, -30 }, { -25, -25 }, { -25, -24 }, { -25, -23 }, { -25, -22 }, { -25, -19 }, { -25, -18 }, { -25, -17 }, { -25, -9 }, { -25, -8 }, { -25, -7 }, { -25, -6 }, { -25, -5 }, { -25, -4 }, { -25, -1 }, { -25, 0 }, { -25, 1 }, { -25, 2 }, { -25, 3 }, { -25, 4 }, { -25, 7 }, { -25, 8 }, { -25, 9 }, { -25, 10 }, { -25, 11 },
{ -25, 12 }, { -25, 20 }, { -25, 21 }, { -25, 22 }, { -25, 25 }, { -25, 26 }, { -25, 27 }, { -25, 28 }, { -25, 33 }, { -25, 34 }, { -25, 35 }, { -25, 36 }, { -24, -33 }, { -24, -32 }, { -24, -31 }, { -24, -30 }, { -24, -29 }, { -24, -25 }, { -24, -24 }, { -24, -23 }, { -24, -22 }, { -24, -19 }, { -24, -18 }, { -24, -9 }, { -24, -8 }, { -24, -7 }, { -24, -6 }, { -24, -5 }, { -24, -4 }, { -24, -1 },
{ -24, 0 }, { -24, 1 }, { -24, 2 }, { -24, 3 }, { -24, 4 }, { -24, 7 }, { -24, 8 }, { -24, 9 }, { -24, 10 }, { -24, 11 }, { -24, 12 }, { -24, 21 }, { -24, 22 }, { -24, 25 }, { -24, 26 }, { -24, 27 }, { -24, 28 }, { -24, 32 }, { -24, 33 }, { -24, 34 }, { -24, 35 }, { -24, 36 }, { -23, -37 }, { -23, -36 }, { -23, -30 }, { -23, -29 }, { -23, -28 }, { -23, -19 }, { -23, -18 }, { -23, -15 },
{ -23, -14 }, { -23, -9 }, { -23, -8 }, { -23, -7 }, { -23, -6 }, { -23, -5 }, { -23, 0 }, { -23, 1 }, { -23, 2 }, { -23, 3 }, { -23, 8 }, { -23, 9 }, { -23, 10 }, { -23, 11 }, { -23, 12 }, { -23, 17 }, { -23, 18 }, { -23, 21 }, { -23, 22 }, { -23, 31 }, { -23, 32 }, { -23, 33 }, { -23, 39 }, { -23, 40 }, { -22, -38 }, { -22, -37 }, { -22, -36 }, { -22, -29 }, { -22, -28 }, { -22, -19 },
{ -22, -18 }, { -22, -15 }, { -22, -14 }, { -22, -13 }, { -22, -9 }, { -22, -8 }, { -22, -7 }, { -22, -6 }, { -22, 1 }, { -22, 2 }, { -22, 9 }, { -22, 10 }, { -22, 11 }, { -22, 12 }, { -22, 16 }, { -22, 17 }, { -22, 18 }, { -22, 21 }, { -22, 22 }, { -22, 31 }, { -22, 32 }, { -22, 39 }, { -22, 40 }, { -22, 41 }, { -21, -41 }, { -21, -40 }, { -21, -39 }, { -21, -38 }, { -21, -37 }, { -21, -29 },
{ -21, -28 }, { -21, -25 }, { -21, -24 }, { -21, -23 }, { -21, -22 }, { -21, -21 }, { -21, -20 }, { -21, -19 }, { -21, -18 }, { -21, -15 }, { -21, -14 }, { -21, -13 }, { -21, -12 }, { -21, -3 }, { -21, -2 }, { -21, 5 }, { -21, 6 }, { -21, 15 }, { -21, 16 }, { -21, 17 }, { -21, 18 }, { -21, 21 }, { -21, 22 }, { -21, 23 }, { -21, 24 }, { -21, 25 }, { -21, 26 }, { -21, 27 }, { -21, 28 }, { -21, 31 },
{ -21, 32 }, { -21, 40 }, { -21, 41 }, { -21, 42 }, { -21, 43 }, { -21, 44 }, { -20, -42 }, { -20, -41 }, { -20, -40 }, { -20, -39 }, { -20, -38 }, { -20, -29 }, { -20, -28 }, { -20, -25 }, { -20, -24 }, { -20, -23 }, { -20, -22 }, { -20, -21 }, { -20, -20 }, { -20, -19 }, { -20, -18 }, { -20, -15 }, { -20, -14 }, { -20, -13 }, { -20, -12 }, { -20, -3 }, { -20, -2 }, { -20, -1 }, { -20, 4 }, { -20, 5 },
{ -20, 6 }, { -20, 15 }, { -20, 16 }, { -20, 17 }, { -20, 18 }, { -20, 21 }, { -20, 22 }, { -20, 23 }, { -20, 24 }, { -20, 25 }, { -20, 26 }, { -20, 27 }, { -20, 28 }, { -20, 31 }, { -20, 32 }, { -20, 41 }, { -20, 42 }, { -20, 43 }, { -20, 44 }, { -20, 45 }, { -19, -43 }, { -19, -42 }, { -19, -41 }, { -19, -35 }, { -19, -34 }, { -19, -33 }, { -19, -32 }, { -19, -25 }, { -19, -24 }, { -19, -23 },
{ -19, -15 }, { -19, -14 }, { -19, -13 }, { -19, -9 }, { -19, -8 }, { -19, -7 }, { -19, -6 }, { -19, -2 }, { -19, -1 }, { -19, 0 }, { -19, 1 }, { -19, 2 }, { -19, 3 }, { -19, 4 }, { -19, 5 }, { -19, 9 }, { -19, 10 }, { -19, 11 }, { -19, 12 }, { -19, 16 }, { -19, 17 }, { -19, 18 }, { -19, 26 }, { -19, 27 }, { -19, 28 }, { -19, 35 }, { -19, 36 }, { -19, 37 }, { -19, 38 }, { -19, 44 },
{ -19, 45 }, { -19, 46 }, { -18, -43 }, { -18, -42 }, { -18, -35 }, { -18, -34 }, { -18, -33 }, { -18, -32 }, { -18, -31 }, { -18, -26 }, { -18, -25 }, { -18, -24 }, { -18, -15 }, { -18, -14 }, { -18, -10 }, { -18, -9 }, { -18, -8 }, { -18, -7 }, { -18, -6 }, { -18, -1 }, { -18, 0 }, { -18, 1 }, { -18, 2 }, { -18, 3 }, { -18, 4 }, { -18, 9 }, { -18, 10 }, { -18, 11 }, { -18, 12 }, { -18, 13 },
{ -18, 17 }, { -18, 18 }, { -18, 27 }, { -18, 28 }, { -18, 29 }, { -18, 34 }, { -18, 35 }, { -18, 36 }, { -18, 37 }, { -18, 38 }, { -18, 45 }, { -18, 46 }, { -17, -43 }, { -17, -42 }, { -17, -32 }, { -17, -31 }, { -17, -30 }, { -17, -27 }, { -17, -26 }, { -17, -25 }, { -17, -21 }, { -17, -20 }, { -17, -19 }, { -17, -18 }, { -17, -17 }, { -17, -16 }, { -17, -15 }, { -17, -14 }, { -17, -11 }, { -17, -10 },
{ -17, -9 }, { -17, -8 }, { -17, -7 }, { -17, -6 }, { -17, 0 }, { -17, 1 }, { -17, 2 }, { -17, 3 }, { -17, 9 }, { -17, 10 }, { -17, 11 }, { -17, 12 }, { -17, 13 }, { -17, 14 }, { -17, 17 }, { -17, 18 }, { -17, 19 }, { -17, 20 }, { -17, 21 }, { -17, 22 }, { -17, 23 }, { -17, 24 }, { -17, 28 }, { -17, 29 }, { -17, 30 }, { -17, 33 }, { -17, 34 }, { -17, 35 }, { -17, 45 }, { -17, 46 },
{ -16, -43 }, { -16, -42 }, { -16, -31 }, { -16, -30 }, { -16, -27 }, { -16, -26 }, { -16, -21 }, { -16, -20 }, { -16, -19 }, { -16, -18 }, { -16, -17 }, { -16, -16 }, { -16, -15 }, { -16, -14 }, { -16, -11 }, { -16, -10 }, { -16, -9 }, { -16, -8 }, { -16, -7 }, { -16, -6 }, { -16, 1 }, { -16, 2 }, { -16, 9 }, { -16, 10 }, { -16, 11 }, { -16, 12 }, { -16, 13 }, { -16, 14 }, { -16, 17 }, { -16, 18 },
{ -16, 19 }, { -16, 20 }, { -16, 21 }, { -16, 22 }, { -16, 23 }, { -16, 24 }, { -16, 29 }, { -16, 30 }, { -16, 33 }, { -16, 34 }, { -16, 45 }, { -16, 46 }, { -15, -43 }, { -15, -42 }, { -15, -39 }, { -15, -38 }, { -15, -37 }, { -15, -36 }, { -15, -35 }, { -15, -34 }, { -15, -20 }, { -15, -19 }, { -15, -18 }, { -15, -17 }, { -15, -10 }, { -15, -9 }, { -15, -8 }, { -15, -7 }, { -15, -3 }, { -15, -2 },
{ -15, 1 }, { -15, 2 }, { -15, 5 }, { -15, 6 }, { -15, 10 }, { -15, 11 }, { -15, 12 }, { -15, 13 }, { -15, 20 }, { -15, 21 }, { -15, 22 }, { -15, 23 }, { -15, 37 }, { -15, 38 }, { -15, 39 }, { -15, 40 }, { -15, 41 }, { -15, 42 }, { -15, 45 }, { -15, 46 }, { -14, -43 }, { -14, -42 }, { -14, -39 }, { -14, -38 }, { -14, -37 }, { -14, -36 }, { -14, -35 }, { -14, -34 }, { -14, -33 }, { -14, -19 },
{ -14, -18 }, { -14, -9 }, { -14, -8 }, { -14, -4 }, { -14, -3 }, { -14, -2 }, { -14, 1 }, { -14, 2 }, { -14, 5 }, { -14, 6 }, { -14, 7 }, { -14, 11 }, { -14, 12 }, { -14, 21 }, { -14, 22 }, { -14, 36 }, { -14, 37 }, { -14, 38 }, { -14, 39 }, { -14, 40 }, { -14, 41 }, { -14, 42 }, { -14, 45 }, { -14, 46 }, { -13, -39 }, { -13, -38 }, { -13, -35 }, { -13, -34 }, { -13, -33 }, { -13, -32 },
{ -13, -29 }, { -13, -28 }, { -13, -15 }, { -13, -14 }, { -13, -5 }, { -13, -4 }, { -13, -3 }, { -13, -2 }, { -13, 5 }, { -13, 6 }, { -13, 7 }, { -13, 8 }, { -13, 17 }, { -13, 18 }, { -13, 31 }, { -13, 32 }, { -13, 35 }, { -13, 36 }, { -13, 37 }, { -13, 38 }, { -13, 41 }, { -13, 42 }, { -12, -39 }, { -12, -38 }, { -12, -35 }, { -12, -34 }, { -12, -33 }, { -12, -32 }, { -12, -29 }, { -12, -28 },
{ -12, -27 }, { -12, -16 }, { -12, -15 }, { -12, -14 }, { -12, -13 }, { -12, -5 }, { -12, -4 }, { -12, -3 }, { -12, -2 }, { -12, 5 }, { -12, 6 }, { -12, 7 }, { -12, 8 }, { -12, 16 }, { -12, 17 }, { -12, 18 }, { -12, 19 }, { -12, 30 }, { -12, 31 }, { -12, 32 }, { -12, 35 }, { -12, 36 }, { -12, 37 }, { -12, 38 }, { -12, 41 }, { -12, 42 }, { -11, -43 }, { -11, -42 }, { -11, -34 }, { -11, -33 },
{ -11, -32 }, { -11, -29 }, { -11, -28 }, { -11, -27 }, { -11, -26 }, { -11, -23 }, { -11, -22 }, { -11, -21 }, { -11, -20 }, { -11, -17 }, { -11, -16 }, { -11, -15 }, { -11, -14 }, { -11, -13 }, { -11, -12 }, { -11, -9 }, { -11, -8 }, { -11, 1 }, { -11, 2 }, { -11, 11 }, { -11, 12 }, { -11, 15 }, { -11, 16 }, { -11, 17 }, { -11, 18 }, { -11, 19 }, { -11, 20 }, { -11, 23 }, { -11, 24 }, { -11, 25 },
{ -11, 26 }, { -11, 29 }, { -11, 30 }, { -11, 31 }, { -11, 32 }, { -11, 35 }, { -11, 36 }, { -11, 37 }, { -11, 45 }, { -11, 46 }, { -10, -44 }, { -10, -43 }, { -10, -42 }, { -10, -33 }, { -10, -32 }, { -10, -29 }, { -10, -28 }, { -10, -27 }, { -10, -26 }, { -10, -23 }, { -10, -22 }, { -10, -21 }, { -10, -20 }, { -10, -17 }, { -10, -16 }, { -10, -15 }, { -10, -14 }, { -10, -13 }, { -10, -12 }, { -10, -9 },
{ -10, -8 }, { -10, -7 }, { -10, 0 }, { -10, 1 }, { -10, 2 }, { -10, 3 }, { -10, 10 }, { -10, 11 }, { -10, 12 }, { -10, 15 }, { -10, 16 }, { -10, 17 }, { -10, 18 }, { -10, 19 }, { -10, 20 }, { -10, 23 }, { -10, 24 }, { -10, 25 }, { -10, 26 }, { -10, 29 }, { -10, 30 }, { -10, 31 }, { -10, 32 }, { -10, 35 }, { -10, 36 }, { -10, 45 }, { -10, 46 }, { -10, 47 }, { -9, -45 }, { -9, -44 },
{ -9, -43 }, { -9, -29 }, { -9, -28 }, { -9, -27 }, { -9, -23 }, { -9, -22 }, { -9, -21 }, { -9, -20 }, { -9, -17 }, { -9, -16 }, { -9, -15 }, { -9, -14 }, { -9, -13 }, { -9, -8 }, { -9, -7 }, { -9, -6 }, { -9, -5 }, { -9, -1 }, { -9, 0 }, { -9, 1 }, { -9, 2 }, { -9, 3 }, { -9, 4 }, { -9, 8 }, { -9, 9 }, { -9, 10 }, { -9, 11 }, { -9, 16 }, { -9, 17 }, { -9, 18 },
{ -9, 19 }, { -9, 20 }, { -9, 23 }, { -9, 24 }, { -9, 25 }, { -9, 26 }, { -9, 30 }, { -9, 31 }, { -9, 32 }, { -9, 46 }, { -9, 47 }, { -9, 48 }, { -8, -45 }, { -8, -44 }, { -8, -30 }, { -8, -29 }, { -8, -28 }, { -8, -24 }, { -8, -23 }, { -8, -22 }, { -8, -21 }, { -8, -20 }, { -8, -17 }, { -8, -16 }, { -8, -15 }, { -8, -14 }, { -8, -7 }, { -8, -6 }, { -8, -5 }, { -8, -4 },
{ -8, -1 }, { -8, 0 }, { -8, 1 }, { -8, 2 }, { -8, 3 }, { -8, 4 }, { -8, 7 }, { -8, 8 }, { -8, 9 }, { -8, 10 }, { -8, 17 }, { -8, 18 }, { -8, 19 }, { -8, 20 }, { -8, 23 }, { -8, 24 }, { -8, 25 }, { -8, 26 }, { -8, 27 }, { -8, 31 }, { -8, 32 }, { -8, 33 }, { -8, 47 }, { -8, 48 }, { -7, -45 }, { -7, -44 }, { -7, -39 }, { -7, -38 }, { -7, -37 }, { -7, -36 },
{ -7, -31 }, { -7, -30 }, { -7, -29 }, { -7, -25 }, { -7, -24 }, { -7, -23 }, { -7, -22 }, { -7, -21 }, { -7, -11 }, { -7, -10 }, { -7, -7 }, { -7, -6 }, { -7, -5 }, { -7, -4 }, { -7, 7 }, { -7, 8 }, { -7, 9 }, { -7, 10 }, { -7, 13 }, { -7, 14 }, { -7, 24 }, { -7, 25 }, { -7, 26 }, { -7, 27 }, { -7, 28 }, { -7, 32 }, { -7, 33 }, { -7, 34 }, { -7, 39 }, { -7, 40 },
{ -7, 41 }, { -7, 42 }, { -7, 47 }, { -7, 48 }, { -6, -46 }, { -6, -45 }, { -6, -44 }, { -6, -39 }, { -6, -38 }, { -6, -37 }, { -6, -36 }, { -6, -35 }, { -6, -31 }, { -6, -30 }, { -6, -25 }, { -6, -24 }, { -6, -23 }, { -6, -22 }, { -6, -12 }, { -6, -11 }, { -6, -10 }, { -6, -6 }, { -6, -5 }, { -6, 8 }, { -6, 9 }, { -6, 13 }, { -6, 14 }, { -6, 15 }, { -6, 25 }, { -6, 26 },
{ -6, 27 }, { -6, 28 }, { -6, 33 }, { -6, 34 }, { -6, 38 }, { -6, 39 }, { -6, 40 }, { -6, 41 }, { -6, 42 }, { -6, 47 }, { -6, 48 }, { -6, 49 }, { -5, -47 }, { -5, -46 }, { -5, -45 }, { -5, -44 }, { -5, -37 }, { -5, -36 }, { -5, -35 }, { -5, -34 }, { -5, -19 }, { -5, -18 }, { -5, -13 }, { -5, -12 }, { -5, -11 }, { -5, -10 }, { -5, -1 }, { -5, 0 }, { -5, 1 }, { -5, 2 },
{ -5, 3 }, { -5, 4 }, { -5, 13 }, { -5, 14 }, { -5, 15 }, { -5, 16 }, { -5, 21 }, { -5, 22 }, { -5, 37 }, { -5, 38 }, { -5, 39 }, { -5, 40 }, { -5, 47 }, { -5, 48 }, { -5, 49 }, { -5, 50 }, { -4, -47 }, { -4, -46 }, { -4, -45 }, { -4, -44 }, { -4, -43 }, { -4, -37 }, { -4, -36 }, { -4, -35 }, { -4, -34 }, { -4, -19 }, { -4, -18 }, { -4, -17 }, { -4, -13 }, { -4, -12 },
{ -4, -11 }, { -4, -10 }, { -4, -2 }, { -4, -1 }, { -4, 0 }, { -4, 1 }, { -4, 2 }, { -4, 3 }, { -4, 4 }, { -4, 5 }, { -4, 13 }, { -4, 14 }, { -4, 15 }, { -4, 16 }, { -4, 20 }, { -4, 21 }, { -4, 22 }, { -4, 37 }, { -4, 38 }, { -4, 39 }, { -4, 40 }, { -4, 46 }, { -4, 47 }, { -4, 48 }, { -4, 49 }, { -4, 50 }, { -3, -44 }, { -3, -43 }, { -3, -42 }, { -3, -41 },
{ -3, -40 }, { -3, -37 }, { -3, -36 }, { -3, -35 }, { -3, -34 }, { -3, -31 }, { -3, -30 }, { -3, -29 }, { -3, -28 }, { -3, -25 }, { -3, -24 }, { -3, -23 }, { -3, -22 }, { -3, -18 }, { -3, -17 }, { -3, -16 }, { -3, -7 }, { -3, -6 }, { -3, -3 }, { -3, -2 }, { -3, -1 }, { -3, 0 }, { -3, 3 }, { -3, 4 }, { -3, 5 }, { -3, 6 }, { -3, 9 }, { -3, 10 }, { -3, 19 }, { -3, 20 },
{ -3, 21 }, { -3, 25 }, { -3, 26 }, { -3, 27 }, { -3, 28 }, { -3, 31 }, { -3, 32 }, { -3, 33 }, { -3, 34 }, { -3, 37 }, { -3, 38 }, { -3, 39 }, { -3, 40 }, { -3, 43 }, { -3, 44 }, { -3, 45 }, { -3, 46 }, { -3, 47 }, { -2, -43 }, { -2, -42 }, { -2, -41 }, { -2, -40 }, { -2, -37 }, { -2, -36 }, { -2, -35 }, { -2, -34 }, { -2, -31 }, { -2, -30 }, { -2, -29 }, { -2, -28 },
{ -2, -25 }, { -2, -24 }, { -2, -23 }, { -2, -22 }, { -2, -21 }, { -2, -17 }, { -2, -16 }, { -2, -15 }, { -2, -8 }, { -2, -7 }, { -2, -6 }, { -2, -3 }, { -2, -2 }, { -2, -1 }, { -2, 0 }, { -2, 3 }, { -2, 4 }, { -2, 5 }, { -2, 6 }, { -2, 9 }, { -2, 10 }, { -2, 11 }, { -2, 18 }, { -2, 19 }, { -2, 20 }, { -2, 24 }, { -2, 25 }, { -2, 26 }, { -2, 27 }, { -2, 28 },
{ -2, 31 }, { -2, 32 }, { -2, 33 }, { -2, 34 }, { -2, 37 }, { -2, 38 }, { -2, 39 }, { -2, 40 }, { -2, 43 }, { -2, 44 }, { -2, 45 }, { -2, 46 }, { -1, -47 }, { -1, -46 }, { -1, -43 }, { -1, -42 }, { -1, -41 }, { -1, -40 }, { -1, -37 }, { -1, -36 }, { -1, -29 }, { -1, -28 }, { -1, -25 }, { -1, -24 }, { -1, -23 }, { -1, -22 }, { -1, -21 }, { -1, -20 }, { -1, -17 }, { -1, -16 },
{ -1, -15 }, { -1, -14 }, { -1, -13 }, { -1, -12 }, { -1, -9 }, { -1, -8 }, { -1, -7 }, { -1, -6 }, { -1, -3 }, { -1, -2 }, { -1, 5 }, { -1, 6 }, { -1, 9 }, { -1, 10 }, { -1, 11 }, { -1, 12 }, { -1, 15 }, { -1, 16 }, { -1, 17 }, { -1, 18 }, { -1, 19 }, { -1, 20 }, { -1, 23 }, { -1, 24 }, { -1, 25 }, { -1, 26 }, { -1, 27 }, { -1, 28 }, { -1, 31 }, { -1, 32 },
{ -1, 39 }, { -1, 40 }, { -1, 43 }, { -1, 44 }, { -1, 45 }, { -1, 46 }, { -1, 49 }, { -1, 50 }, { 0, -47 }, { 0, -46 }, { 0, -43 }, { 0, -42 }, { 0, -41 }, { 0, -40 }, { 0, -37 }, { 0, -36 }, { 0, -29 }, { 0, -28 }, { 0, -25 }, { 0, -24 }, { 0, -23 }, { 0, -22 }, { 0, -21 }, { 0, -20 }, { 0, -17 }, { 0, -16 }, { 0, -15 }, { 0, -14 }, { 0, -13 }, { 0, -12 },
{ 0, -9 }, { 0, -8 }, { 0, -7 }, { 0, -6 }, { 0, -3 }, { 0, -2 }, { 0, 5 }, { 0, 6 }, { 0, 9 }, { 0, 10 }, { 0, 11 }, { 0, 12 }, { 0, 15 }, { 0, 16 }, { 0, 17 }, { 0, 18 }, { 0, 19 }, { 0, 20 }, { 0, 23 }, { 0, 24 }, { 0, 25 }, { 0, 26 }, { 0, 27 }, { 0, 28 }, { 0, 31 }, { 0, 32 }, { 0, 39 }, { 0, 40 }, { 0, 43 }, { 0, 44 },
{ 0, 45 }, { 0, 46 }, { 0, 49 }, { 0, 50 }, { 1, -43 }, { 1, -42 }, { 1, -41 }, { 1, -40 }, { 1, -37 }, { 1, -36 }, { 1, -35 }, { 1, -34 }, { 1, -31 }, { 1, -30 }, { 1, -29 }, { 1, -28 }, { 1, -25 }, { 1, -24 }, { 1, -23 }, { 1, -22 }, { 1, -21 }, { 1, -17 }, { 1, -16 }, { 1, -15 }, { 1, -8 }, { 1, -7 }, { 1, -6 }, { 1, -3 }, { 1, -2 }, { 1, -1 },
{ 1, 0 }, { 1, 3 }, { 1, 4 }, { 1, 5 }, { 1, 6 }, { 1, 9 }, { 1, 10 }, { 1, 11 }, { 1, 18 }, { 1, 19 }, { 1, 20 }, { 1, 24 }, { 1, 25 }, { 1, 26 }, { 1, 27 }, { 1, 28 }, { 1, 31 }, { 1, 32 }, { 1, 33 }, { 1, 34 }, { 1, 37 }, { 1, 38 }, { 1, 39 }, { 1, 40 }, { 1, 43 }, { 1, 44 }, { 1, 45 }, { 1, 46 }, { 2, -44 }, { 2, -43 },
{ 2, -42 }, { 2, -41 }, { 2, -40 }, { 2, -37 }, { 2, -36 }, { 2, -35 }, { 2, -34 }, { 2, -31 }, { 2, -30 }, { 2, -29 }, { 2, -28 }, { 2, -25 }, { 2, -24 }, { 2, -23 }, { 2, -22 }, { 2, -18 }, { 2, -17 }, { 2, -16 }, { 2, -7 }, { 2, -6 }, { 2, -3 }, { 2, -2 }, { 2, -1 }, { 2, 0 }, { 2, 3 }, { 2, 4 }, { 2, 5 }, { 2, 6 }, { 2, 9 }, { 2, 10 },
{ 2, 19 }, { 2, 20 }, { 2, 21 }, { 2, 25 }, { 2, 26 }, { 2, 27 }, { 2, 28 }, { 2, 31 }, { 2, 32 }, { 2, 33 }, { 2, 34 }, { 2, 37 }, { 2, 38 }, { 2, 39 }, { 2, 40 }, { 2, 43 }, { 2, 44 }, { 2, 45 }, { 2, 46 }, { 2, 47 }, { 3, -47 }, { 3, -46 }, { 3, -45 }, { 3, -44 }, { 3, -43 }, { 3, -37 }, { 3, -36 }, { 3, -35 }, { 3, -34 }, { 3, -19 },
{ 3, -18 }, { 3, -17 }, { 3, -13 }, { 3, -12 }, { 3, -11 }, { 3, -10 }, { 3, -2 }, { 3, -1 }, { 3, 0 }, { 3, 1 }, { 3, 2 }, { 3, 3 }, { 3, 4 }, { 3, 5 }, { 3, 13 }, { 3, 14 }, { 3, 15 }, { 3, 16 }, { 3, 20 }, { 3, 21 }, { 3, 22 }, { 3, 37 }, { 3, 38 }, { 3, 39 }, { 3, 40 }, { 3, 46 }, { 3, 47 }, { 3, 48 }, { 3, 49 }, { 3, 50 },
{ 4, -47 }, { 4, -46 }, { 4, -45 }, { 4, -44 }, { 4, -37 }, { 4, -36 }, { 4, -35 }, { 4, -34 }, { 4, -19 }, { 4, -18 }, { 4, -13 }, { 4, -12 }, { 4, -11 }, { 4, -10 }, { 4, -1 }, { 4, 0 }, { 4, 1 }, { 4, 2 }, { 4, 3 }, { 4, 4 }, { 4, 13 }, { 4, 14 }, { 4, 15 }, { 4, 16 }, { 4, 21 }, { 4, 22 }, { 4, 37 }, { 4, 38 }, { 4, 39 }, { 4, 40 },
{ 4, 47 }, { 4, 48 }, { 4, 49 }, { 4, 50 }, { 5, -46 }, { 5, -45 }, { 5, -44 }, { 5, -39 }, { 5, -38 }, { 5, -37 }, { 5, -36 }, { 5, -35 }, { 5, -31 }, { 5, -30 }, { 5, -25 }, { 5, -24 }, { 5, -23 }, { 5, -22 }, { 5, -12 }, { 5, -11 }, { 5, -10 }, { 5, -6 }, { 5, -5 }, { 5, 8 }, { 5, 9 }, { 5, 13 }, { 5, 14 }, { 5, 15 }, { 5, 25 }, { 5, 26 },
{ 5, 27 }, { 5, 28 }, { 5, 33 }, { 5, 34 }, { 5, 38 }, { 5, 39 }, { 5, 40 }, { 5, 41 }, { 5, 42 }, { 5, 47 }, { 5, 48 }, { 5, 49 }, { 6, -45 }, { 6, -44 }, { 6, -39 }, { 6, -38 }, { 6, -37 }, { 6, -36 }, { 6, -31 }, { 6, -30 }, { 6, -29 }, { 6, -25 }, { 6, -24 }, { 6, -23 }, { 6, -22 }, { 6, -21 }, { 6, -11 }, { 6, -10 }, { 6, -7 }, { 6, -6 },
{ 6, -5 }, { 6, -4 }, { 6, 7 }, { 6, 8 }, { 6, 9 }, { 6, 10 }, { 6, 13 }, { 6, 14 }, { 6, 24 }, { 6, 25 }, { 6, 26 }, { 6, 27 }, { 6, 28 }, { 6, 32 }, { 6, 33 }, { 6, 34 }, { 6, 39 }, { 6, 40 }, { 6, 41 }, { 6, 42 }, { 6, 47 }, { 6, 48 }, { 7, -45 }, { 7, -44 }, { 7, -30 }, { 7, -29 }, { 7, -28 }, { 7, -24 }, { 7, -23 }, { 7, -22 },
{ 7, -21 }, { 7, -20 }, { 7, -17 }, { 7, -16 }, { 7, -15 }, { 7, -14 }, { 7, -7 }, { 7, -6 }, { 7, -5 }, { 7, -4 }, { 7, -1 }, { 7, 0 }, { 7, 1 }, { 7, 2 }, { 7, 3 }, { 7, 4 }, { 7, 7 }, { 7, 8 }, { 7, 9 }, { 7, 10 }, { 7, 17 }, { 7, 18 }, { 7, 19 }, { 7, 20 }, { 7, 23 }, { 7, 24 }, { 7, 25 }, { 7, 26 }, { 7, 27 }, { 7, 31 },
{ 7, 32 }, { 7, 33 }, { 7, 47 }, { 7, 48 }, { 8, -45 }, { 8, -44 }, { 8, -43 }, { 8, -29 }, { 8, -28 }, { 8, -27 }, { 8, -23 }, { 8, -22 }, { 8, -21 }, { 8, -20 }, { 8, -17 }, { 8, -16 }, { 8, -15 }, { 8, -14 }, { 8, -13 }, { 8, -8 }, { 8, -7 }, { 8, -6 }, { 8, -5 }, { 8, -1 }, { 8, 0 }, { 8, 1 }, { 8, 2 }, { 8, 3 }, { 8, 4 }, { 8, 8 },
{ 8, 9 }, { 8, 10 }, { 8, 11 }, { 8, 16 }, { 8, 17 }, { 8, 18 }, { 8, 19 }, { 8, 20 }, { 8, 23 }, { 8, 24 }, { 8, 25 }, { 8, 26 }, { 8, 30 }, { 8, 31 }, { 8, 32 }, { 8, 46 }, { 8, 47 }, { 8, 48 }, { 9, -44 }, { 9, -43 }, { 9, -42 }, { 9, -33 }, { 9, -32 }, { 9, -29 }, { 9, -28 }, { 9, -27 }, { 9, -26 }, { 9, -23 }, { 9, -22 }, { 9, -21 },
{ 9, -20 }, { 9, -17 }, { 9, -16 }, { 9, -15 }, { 9, -14 }, { 9, -13 }, { 9, -12 }, { 9, -9 }, { 9, -8 }, { 9, -7 }, { 9, 0 }, { 9, 1 }, { 9, 2 }, { 9, 3 }, { 9, 10 }, { 9, 11 }, { 9, 12 }, { 9, 15 }, { 9, 16 }, { 9, 17 }, { 9, 18 }, { 9, 19 }, { 9, 20 }, { 9, 23 }, { 9, 24 }, { 9, 25 }, { 9, 26 }, { 9, 29 }, { 9, 30 }, { 9, 31 },
{ 9, 32 }, { 9, 35 }, { 9, 36 }, { 9, 45 }, { 9, 46 }, { 9, 47 }, { 10, -43 }, { 10, -42 }, { 10, -34 }, { 10, -33 }, { 10, -32 }, { 10, -29 }, { 10, -28 }, { 10, -27 }, { 10, -26 }, { 10, -23 }, { 10, -22 }, { 10, -21 }, { 10, -20 }, { 10, -17 }, { 10, -16 }, { 10, -15 }, { 10, -14 }, { 10, -13 }, { 10, -12 }, { 10, -9 }, { 10, -8 }, { 10, 1 }, { 10, 2 }, { 10, 11 },
{ 10, 12 }, { 10, 15 }, { 10, 16 }, { 10, 17 }, { 10, 18 }, { 10, 19 }, { 10, 20 }, { 10, 23 }, { 10, 24 }, { 10, 25 }, { 10, 26 }, { 10, 29 }, { 10, 30 }, { 10, 31 }, { 10, 32 }, { 10, 35 }, { 10, 36 }, { 10, 37 }, { 10, 45 }, { 10, 46 }, { 11, -39 }, { 11, -38 }, { 11, -35 }, { 11, -34 }, { 11, -33 }, { 11, -32 }, { 11, -29 }, { 11, -28 }, { 11, -27 }, { 11, -16 },
{ 11, -15 }, { 11, -14 }, { 11, -13 }, { 11, -5 }, { 11, -4 }, { 11, -3 }, { 11, -2 }, { 11, 5 }, { 11, 6 }, { 11, 7 }, { 11, 8 }, { 11, 16 }, { 11, 17 }, { 11, 18 }, { 11, 19 }, { 11, 30 }, { 11, 31 }, { 11, 32 }, { 11, 35 }, { 11, 36 }, { 11, 37 }, { 11, 38 }, { 11, 41 }, { 11, 42 }, { 12, -39 }, { 12, -38 }, { 12, -35 }, { 12, -34 }, { 12, -33 }, { 12, -32 },
{ 12, -29 }, { 12, -28 }, { 12, -15 }, { 12, -14 }, { 12, -5 }, { 12, -4 }, { 12, -3 }, { 12, -2 }, { 12, 5 }, { 12, 6 }, { 12, 7 }, { 12, 8 }, { 12, 17 }, { 12, 18 }, { 12, 31 }, { 12, 32 }, { 12, 35 }, { 12, 36 }, { 12, 37 }, { 12, 38 }, { 12, 41 }, { 12, 42 }, { 13, -43 }, { 13, -42 }, { 13, -39 }, { 13, -38 }, { 13, -37 }, { 13, -36 }, { 13, -35 }, { 13, -34 },
{ 13, -33 }, { 13, -19 }, { 13, -18 }, { 13, -9 }, { 13, -8 }, { 13, -4 }, { 13, -3 }, { 13, -2 }, { 13, 1 }, { 13, 2 }, { 13, 5 }, { 13, 6 }, { 13, 7 }, { 13, 11 }, { 13, 12 }, { 13, 21 }, { 13, 22 }, { 13, 36 }, { 13, 37 }, { 13, 38 }, { 13, 39 }, { 13, 40 }, { 13, 41 }, { 13, 42 }, { 13, 45 }, { 13, 46 }, { 14, -43 }, { 14, -42 }, { 14, -39 }, { 14, -38 },
{ 14, -37 }, { 14, -36 }, { 14, -35 }, { 14, -34 }, { 14, -20 }, { 14, -19 }, { 14, -18 }, { 14, -17 }, { 14, -10 }, { 14, -9 }, { 14, -8 }, { 14, -7 }, { 14, -3 }, { 14, -2 }, { 14, 1 }, { 14, 2 }, { 14, 5 }, { 14, 6 }, { 14, 10 }, { 14, 11 }, { 14, 12 }, { 14, 13 }, { 14, 20 }, { 14, 21 }, { 14, 22 }, { 14, 23 }, { 14, 37 }, { 14, 38 }, { 14, 39 }, { 14, 40 },
{ 14, 41 }, { 14, 42 }, { 14, 45 }, { 14, 46 }, { 15, -43 }, { 15, -42 }, { 15, -31 }, { 15, -30 }, { 15, -27 }, { 15, -26 }, { 15, -21 }, { 15, -20 }, { 15, -19 }, { 15, -18 }, { 15, -17 }, { 15, -16 }, { 15, -15 }, { 15, -14 }, { 15, -11 }, { 15, -10 }, { 15, -9 }, { 15, -8 }, { 15, -7 }, { 15, -6 }, { 15, 1 }, { 15, 2 }, { 15, 9 }, { 15, 10 }, { 15, 11 }, { 15, 12 },
{ 15, 13 }, { 15, 14 }, { 15, 17 }, { 15, 18 }, { 15, 19 }, { 15, 20 }, { 15, 21 }, { 15, 22 }, { 15, 23 }, { 15, 24 }, { 15, 29 }, { 15, 30 }, { 15, 33 }, { 15, 34 }, { 15, 45 }, { 15, 46 }, { 16, -43 }, { 16, -42 }, { 16, -32 }, { 16, -31 }, { 16, -30 }, { 16, -27 }, { 16, -26 }, { 16, -25 }, { 16, -21 }, { 16, -20 }, { 16, -19 }, { 16, -18 }, { 16, -17 }, { 16, -16 },
{ 16, -15 }, { 16, -14 }, { 16, -11 }, { 16, -10 }, { 16, -9 }, { 16, -8 }, { 16, -7 }, { 16, -6 }, { 16, 0 }, { 16, 1 }, { 16, 2 }, { 16, 3 }, { 16, 9 }, { 16, 10 }, { 16, 11 }, { 16, 12 }, { 16, 13 }, { 16, 14 }, { 16, 17 }, { 16, 18 }, { 16, 19 }, { 16, 20 }, { 16, 21 }, { 16, 22 }, { 16, 23 }, { 16, 24 }, { 16, 28 }, { 16, 29 }, { 16, 30 }, { 16, 33 },
{ 16, 34 }, { 16, 35 }, { 16, 45 }, { 16, 46 }, { 17, -43 }, { 17, -42 }, { 17, -35 }, { 17, -34 }, { 17, -33 }, { 17, -32 }, { 17, -31 }, { 17, -26 }, { 17, -25 }, { 17, -24 }, { 17, -15 }, { 17, -14 }, { 17, -10 }, { 17, -9 }, { 17, -8 }, { 17, -7 }, { 17, -6 }, { 17, -1 }, { 17, 0 }, { 17, 1 }, { 17, 2 }, { 17, 3 }, { 17, 4 }, { 17, 9 }, { 17, 10 }, { 17, 11 },
{ 17, 12 }, { 17, 13 }, { 17, 17 }, { 17, 18 }, { 17, 27 }, { 17, 28 }, { 17, 29 }, { 17, 34 }, { 17, 35 }, { 17, 36 }, { 17, 37 }, { 17, 38 }, { 17, 45 }, { 17, 46 }, { 18, -43 }, { 18, -42 }, { 18, -41 }, { 18, -35 }, { 18, -34 }, { 18, -33 }, { 18, -32 }, { 18, -25 }, { 18, -24 }, { 18, -23 }, { 18, -15 }, { 18, -14 }, { 18, -13 }, { 18, -9 }, { 18, -8 }, { 18, -7 },
{ 18, -6 }, { 18, -2 }, { 18, -1 }, { 18, 0 }, { 18, 1 }, { 18, 2 }, { 18, 3 }, { 18, 4 }, { 18, 5 }, { 18, 9 }, { 18, 10 }, { 18, 11 }, { 18, 12 }, { 18, 16 }, { 18, 17 }, { 18, 18 }, { 18, 26 }, { 18, 27 }, { 18, 28 }, { 18, 35 }, { 18, 36 }, { 18, 37 }, { 18, 38 }, { 18, 44 }, { 18, 45 }, { 18, 46 }, { 19, -42 }, { 19, -41 }, { 19, -40 }, { 19, -39 },
{ 19, -38 }, { 19, -29 }, { 19, -28 }, { 19, -25 }, { 19, -24 }, { 19, -23 }, { 19, -22 }, { 19, -21 }, { 19, -20 }, { 19, -19 }, { 19, -18 }, { 19, -15 }, { 19, -14 }, { 19, -13 }, { 19, -12 }, { 19, -3 }, { 19, -2 }, { 19, -1 }, { 19, 4 }, { 19, 5 }, { 19, 6 }, { 19, 15 }, { 19, 16 }, { 19, 17 }, { 19, 18 }, { 19, 21 }, { 19, 22 }, { 19, 23 }, { 19, 24 }, { 19, 25 },
{ 19, 26 }, { 19, 27 }, { 19, 28 }, { 19, 31 }, { 19, 32 }, { 19, 41 }, { 19, 42 }, { 19, 43 }, { 19, 44 }, { 19, 45 }, { 20, -41 }, { 20, -40 }, { 20, -39 }, { 20, -38 }, { 20, -37 }, { 20, -29 }, { 20, -28 }, { 20, -25 }, { 20, -24 }, { 20, -23 }, { 20, -22 }, { 20, -21 }, { 20, -20 }, { 20, -19 }, { 20, -18 }, { 20, -15 }, { 20, -14 }, { 20, -13 }, { 20, -12 }, { 20, -3 },
{ 20, -2 }, { 20, 5 }, { 20, 6 }, { 20, 15 }, { 20, 16 }, { 20, 17 }, { 20, 18 }, { 20, 21 }, { 20, 22 }, { 20, 23 }, { 20, 24 }, { 20, 25 }, { 20, 26 }, { 20, 27 }, { 20, 28 }, { 20, 31 }, { 20, 32 }, { 20, 40 }, { 20, 41 }, { 20, 42 }, { 20, 43 }, { 20, 44 }, { 21, -38 }, { 21, -37 }, { 21, -36 }, { 21, -29 }, { 21, -28 }, { 21, -19 }, { 21, -18 }, { 21, -15 },
{ 21, -14 }, { 21, -13 }, { 21, -9 }, { 21, -8 }, { 21, -7 }, { 21, -6 }, { 21, 1 }, { 21, 2 }, { 21, 9 }, { 21, 10 }, { 21, 11 }, { 21, 12 }, { 21, 16 }, { 21, 17 }, { 21, 18 }, { 21, 21 }, { 21, 22 }, { 21, 31 }, { 21, 32 }, { 21, 39 }, { 21, 40 }, { 21, 41 }, { 22, -37 }, { 22, -36 }, { 22, -30 }, { 22, -29 }, { 22, -28 }, { 22, -19 }, { 22, -18 }, { 22, -15 },
{ 22, -14 }, { 22, -9 }, { 22, -8 }, { 22, -7 }, { 22, -6 }, { 22, -5 }, { 22, 0 }, { 22, 1 }, { 22, 2 }, { 22, 3 }, { 22, 8 }, { 22, 9 }, { 22, 10 }, { 22, 11 }, { 22, 12 }, { 22, 17 }, { 22, 18 }, { 22, 21 }, { 22, 22 }, { 22, 31 }, { 22, 32 }, { 22, 33 }, { 22, 39 }, { 22, 40 }, { 23, -33 }, { 23, -32 }, { 23, -31 }, { 23, -30 }, { 23, -29 }, { 23, -25 },
{ 23, -24 }, { 23, -23 }, { 23, -22 }, { 23, -19 }, { 23, -18 }, { 23, -9 }, { 23, -8 }, { 23, -7 }, { 23, -6 }, { 23, -5 }, { 23, -4 }, { 23, -1 }, { 23, 0 }, { 23, 1 }, { 23, 2 }, { 23, 3 }, { 23, 4 }, { 23, 7 }, { 23, 8 }, { 23, 9 }, { 23, 10 }, { 23, 11 }, { 23, 12 }, { 23, 21 }, { 23, 22 }, { 23, 25 }, { 23, 26 }, { 23, 27 }, { 23, 28 }, { 23, 32 },
{ 23, 33 }, { 23, 34 }, { 23, 35 }, { 23, 36 }, { 24, -33 }, { 24, -32 }, { 24, -31 }, { 24, -30 }, { 24, -25 }, { 24, -24 }, { 24, -23 }, { 24, -22 }, { 24, -19 }, { 24, -18 }, { 24, -17 }, { 24, -9 }, { 24, -8 }, { 24, -7 }, { 24, -6 }, { 24, -5 }, { 24, -4 }, { 24, -1 }, { 24, 0 }, { 24, 1 }, { 24, 2 }, { 24, 3 }, { 24, 4 }, { 24, 7 }, { 24, 8 }, { 24, 9 },
{ 24, 10 }, { 24, 11 }, { 24, 12 }, { 24, 20 }, { 24, 21 }, { 24, 22 }, { 24, 25 }, { 24, 26 }, { 24, 27 }, { 24, 28 }, { 24, 33 }, { 24, 34 }, { 24, 35 }, { 24, 36 }, { 25, -39 }, { 25, -38 }, { 25, -37 }, { 25, -36 }, { 25, -24 }, { 25, -23 }, { 25, -22 }, { 25, -19 }, { 25, -18 }, { 25, -17 }, { 25, -16 }, { 25, -6 }, { 25, -5 }, { 25, -4 }, { 25, -1 }, { 25, 0 },
{ 25, 1 }, { 25, 2 }, { 25, 3 }, { 25, 4 }, { 25, 7 }, { 25, 8 }, { 25, 9 }, { 25, 19 }, { 25, 20 }, { 25, 21 }, { 25, 22 }, { 25, 25 }, { 25, 26 }, { 25, 27 }, { 25, 39 }, { 25, 40 }, { 25, 41 }, { 25, 42 }, { 26, -39 }, { 26, -38 }, { 26, -37 }, { 26, -36 }, { 26, -23 }, { 26, -22 }, { 26, -19 }, { 26, -18 }, { 26, -17 }, { 26, -16 }, { 26, -15 }, { 26, -5 },
{ 26, -4 }, { 26, -1 }, { 26, 0 }, { 26, 1 }, { 26, 2 }, { 26, 3 }, { 26, 4 }, { 26, 7 }, { 26, 8 }, { 26, 18 }, { 26, 19 }, { 26, 20 }, { 26, 21 }, { 26, 22 }, { 26, 25 }, { 26, 26 }, { 26, 39 }, { 26, 40 }, { 26, 41 }, { 26, 42 }, { 27, -38 }, { 27, -37 }, { 27, -36 }, { 27, -29 }, { 27, -28 }, { 27, -27 }, { 27, -26 }, { 27, -16 }, { 27, -15 }, { 27, -14 },
{ 27, -9 }, { 27, -8 }, { 27, 11 }, { 27, 12 }, { 27, 17 }, { 27, 18 }, { 27, 19 }, { 27, 29 }, { 27, 30 }, { 27, 31 }, { 27, 32 }, { 27, 39 }, { 27, 40 }, { 27, 41 }, { 28, -37 }, { 28, -36 }, { 28, -30 }, { 28, -29 }, { 28, -28 }, { 28, -27 }, { 28, -26 }, { 28, -15 }, { 28, -14 }, { 28, -10 }, { 28, -9 }, { 28, -8 }, { 28, -7 }, { 28, 10 }, { 28, 11 }, { 28, 12 },
{ 28, 13 }, { 28, 17 }, { 28, 18 }, { 28, 29 }, { 28, 30 }, { 28, 31 }, { 28, 32 }, { 28, 33 }, { 28, 39 }, { 28, 40 }, { 29, -33 }, { 29, -32 }, { 29, -31 }, { 29, -30 }, { 29, -29 }, { 29, -28 }, { 29, -27 }, { 29, -26 }, { 29, -21 }, { 29, -20 }, { 29, -19 }, { 29, -18 }, { 29, -11 }, { 29, -10 }, { 29, -9 }, { 29, -8 }, { 29, -7 }, { 29, -6 }, { 29, -1 }, { 29, 0 },
{ 29, 1 }, { 29, 2 }, { 29, 3 }, { 29, 4 }, { 29, 9 }, { 29, 10 }, { 29, 11 }, { 29, 12 }, { 29, 13 }, { 29, 14 }, { 29, 21 }, { 29, 22 }, { 29, 23 }, { 29, 24 }, { 29, 29 }, { 29, 30 }, { 29, 31 }, { 29, 32 }, { 29, 33 }, { 29, 34 }, { 29, 35 }, { 29, 36 }, { 30, -33 }, { 30, -32 }, { 30, -31 }, { 30, -30 }, { 30, -29 }, { 30, -28 }, { 30, -27 }, { 30, -26 },
{ 30, -22 }, { 30, -21 }, { 30, -20 }, { 30, -19 }, { 30, -18 }, { 30, -11 }, { 30, -10 }, { 30, -9 }, { 30, -8 }, { 30, -7 }, { 30, -6 }, { 30, -5 }, { 30, -1 }, { 30, 0 }, { 30, 1 }, { 30, 2 }, { 30, 3 }, { 30, 4 }, { 30, 8 }, { 30, 9 }, { 30, 10 }, { 30, 11 }, { 30, 12 }, { 30, 13 }, { 30, 14 }, { 30, 21 }, { 30, 22 }, { 30, 23 }, { 30, 24 }, { 30, 25 },
{ 30, 29 }, { 30, 30 }, { 30, 31 }, { 30, 32 }, { 30, 33 }, { 30, 34 }, { 30, 35 }, { 30, 36 }, { 31, -32 }, { 31, -31 }, { 31, -30 }, { 31, -29 }, { 31, -28 }, { 31, -27 }, { 31, -23 }, { 31, -22 }, { 31, -21 }, { 31, -15 }, { 31, -14 }, { 31, -6 }, { 31, -5 }, { 31, -4 }, { 31, -1 }, { 31, 0 }, { 31, 3 }, { 31, 4 }, { 31, 7 }, { 31, 8 }, { 31, 9 }, { 31, 17 },
{ 31, 18 }, { 31, 24 }, { 31, 25 }, { 31, 26 }, { 31, 30 }, { 31, 31 }, { 31, 32 }, { 31, 33 }, { 31, 34 }, { 31, 35 }, { 32, -31 }, { 32, -30 }, { 32, -29 }, { 32, -28 }, { 32, -23 }, { 32, -22 }, { 32, -16 }, { 32, -15 }, { 32, -14 }, { 32, -5 }, { 32, -4 }, { 32, -1 }, { 32, 0 }, { 32, 3 }, { 32, 4 }, { 32, 7 }, { 32, 8 }, { 32, 17 }, { 32, 18 }, { 32, 19 },
{ 32, 25 }, { 32, 26 }, { 32, 31 }, { 32, 32 }, { 32, 33 }, { 32, 34 }, { 33, -30 }, { 33, -29 }, { 33, -28 }, { 33, -23 }, { 33, -22 }, { 33, -17 }, { 33, -16 }, { 33, -15 }, { 33, -11 }, { 33, -10 }, { 33, -9 }, { 33, -8 }, { 33, 11 }, { 33, 12 }, { 33, 13 }, { 33, 14 }, { 33, 18 }, { 33, 19 }, { 33, 20 }, { 33, 25 }, { 33, 26 }, { 33, 31 }, { 33, 32 }, { 33, 33 },
{ 34, -29 }, { 34, -28 }, { 34, -23 }, { 34, -22 }, { 34, -17 }, { 34, -16 }, { 34, -12 }, { 34, -11 }, { 34, -10 }, { 34, -9 }, { 34, -8 }, { 34, 11 }, { 34, 12 }, { 34, 13 }, { 34, 14 }, { 34, 15 }, { 34, 19 }, { 34, 20 }, { 34, 25 }, { 34, 26 }, { 34, 31 }, { 34, 32 }, { 35, -17 }, { 35, -16 }, { 35, -13 }, { 35, -12 }, { 35, -11 }, { 35, -10 }, { 35, -9 }, { 35, -3 },
{ 35, -2 }, { 35, -1 }, { 35, 0 }, { 35, 3 }, { 35, 4 }, { 35, 5 }, { 35, 6 }, { 35, 12 }, { 35, 13 }, { 35, 14 }, { 35, 15 }, { 35, 16 }, { 35, 19 }, { 35, 20 }, { 36, -17 }, { 36, -16 }, { 36, -13 }, { 36, -12 }, { 36, -11 }, { 36, -10 }, { 36, -4 }, { 36, -3 }, { 36, -2 }, { 36, -1 }, { 36, 0 }, { 36, 3 }, { 36, 4 }, { 36, 5 }, { 36, 6 }, { 36, 7 },
{ 36, 13 }, { 36, 14 }, { 36, 15 }, { 36, 16 }, { 36, 19 }, { 36, 20 }, { 37, -27 }, { 37, -26 }, { 37, -25 }, { 37, -24 }, { 37, -21 }, { 37, -20 }, { 37, -13 }, { 37, -12 }, { 37, -5 }, { 37, -4 }, { 37, -3 }, { 37, -2 }, { 37, -1 }, { 37, 0 }, { 37, 1 }, { 37, 2 }, { 37, 3 }, { 37, 4 }, { 37, 5 }, { 37, 6 }, { 37, 7 }, { 37, 8 }, { 37, 15 }, { 37, 16 },
{ 37, 23 }, { 37, 24 }, { 37, 27 }, { 37, 28 }, { 37, 29 }, { 37, 30 }, { 38, -27 }, { 38, -26 }, { 38, -25 }, { 38, -24 }, { 38, -21 }, { 38, -20 }, { 38, -19 }, { 38, -13 }, { 38, -12 }, { 38, -5 }, { 38, -4 }, { 38, -3 }, { 38, -2 }, { 38, -1 }, { 38, 0 }, { 38, 1 }, { 38, 2 }, { 38, 3 }, { 38, 4 }, { 38, 5 }, { 38, 6 }, { 38, 7 }, { 38, 8 }, { 38, 15 },
{ 38, 16 }, { 38, 22 }, { 38, 23 }, { 38, 24 }, { 38, 27 }, { 38, 28 }, { 38, 29 }, { 38, 30 }, { 39, -26 }, { 39, -25 }, { 39, -24 }, { 39, -20 }, { 39, -19 }, { 39, -18 }, { 39, -13 }, { 39, -12 }, { 39, -11 }, { 39, -10 }, { 39, -5 }, { 39, -4 }, { 39, 7 }, { 39, 8 }, { 39, 13 }, { 39, 14 }, { 39, 15 }, { 39, 16 }, { 39, 21 }, { 39, 22 }, { 39, 23 }, { 39, 27 },
{ 39, 28 }, { 39, 29 }, { 40, -25 }, { 40, -24 }, { 40, -19 }, { 40, -18 }, { 40, -13 }, { 40, -12 }, { 40, -11 }, { 40, -10 }, { 40, -5 }, { 40, -4 }, { 40, 7 }, { 40, 8 }, { 40, 13 }, { 40, 14 }, { 40, 15 }, { 40, 16 }, { 40, 21 }, { 40, 22 }, { 40, 27 }, { 40, 28 }, { 41, -19 }, { 41, -18 }, { 41, -1 }, { 41, 0 }, { 41, 1 }, { 41, 2 }, { 41, 3 }, { 41, 4 },
{ 41, 21 }, { 41, 22 }, { 42, -19 }, { 42, -18 }, { 42, -17 }, { 42, -1 }, { 42, 0 }, { 42, 1 }, { 42, 2 }, { 42, 3 }, { 42, 4 }, { 42, 20 }, { 42, 21 }, { 42, 22 }, { 43, -18 }, { 43, -17 }, { 43, -16 }, { 43, -15 }, { 43, -14 }, { 43, -13 }, { 43, -12 }, { 43, -9 }, { 43, -8 }, { 43, -1 }, { 43, 0 }, { 43, 1 }, { 43, 2 }, { 43, 3 }, { 43, 4 }, { 43, 11 },
{ 43, 12 }, { 43, 15 }, { 43, 16 }, { 43, 17 }, { 43, 18 }, { 43, 19 }, { 43, 20 }, { 43, 21 }, { 44, -17 }, { 44, -16 }, { 44, -15 }, { 44, -14 }, { 44, -13 }, { 44, -12 }, { 44, -9 }, { 44, -8 }, { 44, -7 }, { 44, -2 }, { 44, -1 }, { 44, 0 }, { 44, 1 }, { 44, 2 }, { 44, 3 }, { 44, 4 }, { 44, 5 }, { 44, 10 }, { 44, 11 }, { 44, 12 }, { 44, 15 }, { 44, 16 },
{ 44, 17 }, { 44, 18 }, { 44, 19 }, { 44, 20 }, { 45, -8 }, { 45, -7 }, { 45, -6 }, { 45, -5 }, { 45, -4 }, { 45, -3 }, { 45, -2 }, { 45, -1 }, { 45, 4 }, { 45, 5 }, { 45, 6 }, { 45, 7 }, { 45, 8 }, { 45, 9 }, { 45, 10 }, { 45, 11 }, { 46, -7 }, { 46, -6 }, { 46, -5 }, { 46, -4 }, { 46, -3 }, { 46, -2 }, { 46, 5 }, { 46, 6 }, { 46, 7 }, { 46, 8 },
{ 46, 9 }, { 46, 10 }, { 47, -4 }, { 47, -3 }, { 47, -2 }, { 47, 1 }, { 47, 2 }, { 47, 5 }, { 47, 6 }, { 47, 7 }, { 48, -3 }, { 48, -2 }, { 48, 1 }, { 48, 2 }, { 48, 5 }, { 48, 6 },
}, },
}, },
resource_tiles = { resource_tiles = {
@@ -188,7 +87,7 @@ return {
amount = 4000, amount = 4000,
size = { 26, 27 }, size = { 26, 27 },
-- offset = {-64,-32} -- offset = {-64,-32}
offset = { -64, -64 }, offset = { x = -64, y = -64 },
}, },
{ {
enabled = false, enabled = false,
@@ -196,7 +95,7 @@ return {
amount = 4000, amount = 4000,
size = { 26, 27 }, size = { 26, 27 },
-- offset = {-64, 0} -- offset = {-64, 0}
offset = { 64, -64 }, offset = { x = 64, y = -64 },
}, },
{ {
enabled = false, enabled = false,
@@ -204,7 +103,7 @@ return {
amount = 4000, amount = 4000,
size = { 22, 20 }, size = { 22, 20 },
-- offset = {-64, 32} -- offset = {-64, 32}
offset = { -64, 64 }, offset = { x = -64, y = 64 },
}, },
{ {
enabled = false, enabled = false,
@@ -212,7 +111,7 @@ return {
amount = 4000, amount = 4000,
size = { 22, 20 }, size = { 22, 20 },
-- offset = {-64, -64} -- offset = {-64, -64}
offset = { 64, 64 }, offset = { x = 64, y = 64 },
}, },
{ {
enabled = false, enabled = false,
@@ -220,7 +119,7 @@ return {
amount = 4000, amount = 4000,
size = { 22, 20 }, size = { 22, 20 },
-- offset = {-64, -96} -- offset = {-64, -96}
offset = { 0, 64 }, offset = { x = 0, y = 64 },
}, },
}, },
}, },
@@ -233,7 +132,7 @@ return {
num_patches = 4, num_patches = 4,
amount = 4000000, amount = 4000000,
-- offset = {-80, -12}, -- offset = {-80, -12},
offset = { -12, 64 }, offset = { x = -12, y = 64 },
-- offset_next = {0, 6} -- offset_next = {0, 6}
offset_next = { 6, 0 }, offset_next = { 6, 0 },
}, },
+14
View File
@@ -35,6 +35,20 @@ return {
storage_output = 1, -- >0 allows for item teleportation (allowed_items only) 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_items = { --- @setting allowed_items List of all items allowed in vlayer storage and their properties
--[[ --[[
Allowed 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 Storage = require("modules/exp_util/storage")
local DatastoreManager = {} local DatastoreManager = {}
local Datastores = {} local Datastores = {} --- @type table<string, Datastore>
local Datastore = {}
local Data = {} 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 copy = table.deep_copy
local trace = debug.traceback local trace = debug.traceback
local table_to_json = helpers.table_to_json 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 concat = table.concat
local External = {} local External = {}
@@ -29,12 +30,13 @@ end
]] ]]
function External.valid() function External.valid()
if storage.ext == nil then return false end local stored = storage.ext
if ext == storage.ext and var == ext.var then if stored == nil then return false end
if ext == stored and var == ext.var then
return var ~= nil return var ~= nil
else else
ext = storage.ext ext = stored
var = ext.var var = stored.var
return var ~= nil return var ~= nil
end end
end end
@@ -48,7 +50,7 @@ local servers = External.get_servers()
]] ]]
function External.get_servers() function External.get_servers()
assert(ext, "No external data was found, use External.valid() to ensure external data exists.") 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 end
--[[-- Gets a table of all the servers filtered by name, key is the server id, value is the server details --[[-- 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.") 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 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") 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 end
--[[-- Gets the details of the given server --[[-- 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) function External.get_server_details(server_id)
assert(ext, "No external data was found, use External.valid() to ensure external data exists.") 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 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 end
--[[-- Gets the status of the given server --[[-- Gets the status of the given server
@@ -142,7 +142,7 @@ group:set_action('toggle_map_editor', false)
]] ]]
function PermissionsGroups._prototype:set_action(action, state) function PermissionsGroups._prototype:set_action(action, state)
local input_action = defines.input_action[action] --[[ @as defines.input_action? ]] local input_action = defines.input_action[action] --[[@as defines.input_action?]]
if input_action == nil then input_action = action end if input_action == nil then input_action = action end
assert(type(input_action) == "number", tostring(action) .. " is not a valid input action") assert(type(input_action) == "number", tostring(action) .. " is not a valid input action")
self.actions[input_action] = state self.actions[input_action] = state
@@ -348,8 +348,10 @@ function PermissionsGroups._prototype:print(message)
end end
-- when the game starts it will make the permission groups -- when the game starts it will make the permission groups
--[[
Event.on_init(function() Event.on_init(function()
PermissionsGroups.reload_permissions() PermissionsGroups.reload_permissions()
end) end)
]]
return PermissionsGroups 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 --- Store and enum for the data saving preference
local DataSavingPreference = PlayerData:combine("DataSavingPreference") 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 for k, v in ipairs(PreferenceEnum) do PreferenceEnum[v] = k end
DataSavingPreference:set_default("All") DataSavingPreference:set_default("All")
File diff suppressed because it is too large Load Diff
+10 -21
View File
@@ -1,31 +1,20 @@
[links] [links]
discord=https://discord.explosivegaming.nl discord=https://discord.aperx.org
website=https://www.explosivegaming.nl website=https://aperx.org
status=https://status.explosivegaming.nl status=https://aperx.org
github=https://github.com/explosivegaming/ExpCluster github=https://github.com/PHIDIAS0303/ExpCluster
patreon=https://www.patreon.com/ExpGaming patreon=https://aperx.org
[info] [info]
players-online=There are __1__ players online players-online=There are __1__ players online
total-map-time=This map has been on for __1__ total-map-time=This map has been on for __1__
discord=Join us on our discord at: https://discord.explosivegaming.nl discord=Join us on our discord at: https://discord.aperx.org
website=Please visit our website at: https://www.explosivegaming.nl website=Please visit our website at: https://aperx.org
status=Want to check if out servers are down? Visit: https://status.explosivegaming.nl status=Want to check if out servers are down? Visit: https://aperx.org
github=Want to help improve our server with some extra features? Help us at: https://github.com/explosivegaming/ExpCluster github=Want to help improve our server with some extra features? Help us at: https://github.com/PHIDIAS0303/ExpCluster
patreon=Consider supporting our server at: https://www.patreon.com/ExpGaming patreon=Consider supporting our server at: https://aperx.org
custom-commands=We use custom commands, such as /tag and /me, use /chelp for more info. 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) 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. 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 ;-; 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. 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.
-5
View File
@@ -10,11 +10,6 @@ message-cleared=Your join message has been cleared.
[quickbar] [quickbar]
saved=Your quickbar filters have been saved. 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] [exp-settings]
Colour=Colour Colour=Colour
Colour-tooltip=Your player colour Colour-tooltip=Your player colour
-8
View File
@@ -1,11 +1,3 @@
[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] [expcore-data]
description-preference=Allows you to set/get your data saving preference. description-preference=Allows you to set/get your data saving preference.
description-data=Writes all your player data to a file on your computer. description-data=Writes all your player data to a file on your computer.
-114
View File
@@ -7,53 +7,12 @@ reason-entry=Enter Reason
goto-player=Goto player goto-player=Goto player
bring-player=Bring player bring-player=Bring player
report-player=Report player report-player=Report player
warn-player=Warn player
jail-player=Jail player jail-player=Jail player
kick-player=Kick player kick-player=Kick player
ban-player=Ban player ban-player=Ban player
afk-time=__1__% of total map time\nLast moved __2__ ago afk-time=__1__% of total map time\nLast moved __2__ ago
open-map=__1__ __2__\n__3__\nClick to open map 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] [warp-list]
main-caption=Warp List [img=info] main-caption=Warp List [img=info]
main-tooltip=Warp List main-tooltip=Warp List
@@ -85,79 +44,6 @@ 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-disabled=You are not on a warp point, walk to a warp point
goto-edit=Edit warp icon 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] [bonus]
description=Get / Set the amount of bonus you receive. description=Get / Set the amount of bonus you receive.
arg-amount=Amount to set your bonus to, 0 will disable bonus. arg-amount=Amount to set your bonus to, 0 will disable bonus.
+10 -21
View File
@@ -1,31 +1,20 @@
[links] [links]
discord=https://discord.explosivegaming.nl discord=https://discord.aperx.org
website=https://www.explosivegaming.nl website=https://aperx.org
status=https://status.explosivegaming.nl status=https://aperx.org
github=https://github.com/explosivegaming/ExpCluster github=https://github.com/PHIDIAS0303/ExpCluster
patreon=https://www.patreon.com/ExpGaming patreon=https://aperx.org
[info] [info]
players-online=現在有 __1__ 人在線 players-online=現在有 __1__ 人在線
total-map-time=地圖時間為 __1__ total-map-time=地圖時間為 __1__
discord=加入社群: https://discord.explosivegaming.nl discord=加入社群: https://discord.aperx.org
website=訪問網站: https://www.explosivegaming.nl website=訪問網站: https://aperx.org
status=伺服器狀態: https://status.explosivegaming.nl status=伺服器狀態: https://aperx.org
github=增加功能或回報錯誤: https://github.com/explosivegaming/ExpCluster github=增加功能或回報錯誤: https://github.com/PHIDIAS0303/ExpCluster
patreon=支持我們: https://www.patreon.com/ExpGaming patreon=支持我們: https://aperx.org
custom-commands=這裹使用了自制指令,如 /tag 和 /me。可使用 /chelp 查看更多。 custom-commands=這裹使用了自制指令,如 /tag 和 /me。可使用 /chelp 查看更多。
read-readme=確保你已閱讀相關資訊。按左上 i 圖標可再次查看。 read-readme=確保你已閱讀相關資訊。按左上 i 圖標可再次查看。
softmod=這裹用了自設情境,是一種軟裝模組。 softmod=這裹用了自設情境,是一種軟裝模組。
redmew= redmew=
lhd=列車必須是左則通行。這是本服務器長久以來的規則。 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__ 已被系統發出了一則自動警告。
-5
View File
@@ -10,11 +10,6 @@ message-cleared=你的加入信息已清除。
[quickbar] [quickbar]
saved=你的工具列已儲存。 saved=你的工具列已儲存。
[exp-required]
Warnings=警告
Warnings-tooltip=你所有收到的警告
Warnings-value-tooltip=你所有收到的警告
[exp-settings] [exp-settings]
Colour=顏色 Colour=顏色
Colour-tooltip=你的人物顏色 Colour-tooltip=你的人物顏色
@@ -1,11 +1,3 @@
[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] [expcore-data]
description-preference=允許寫入資料。 description-preference=允許寫入資料。
description-data=把用戶資料寫入電腦 description-data=把用戶資料寫入電腦
-114
View File
@@ -7,53 +7,12 @@ reason-entry=輸入原因
goto-player=傳送到用戶 goto-player=傳送到用戶
bring-player=傳送用戶到自己 bring-player=傳送用戶到自己
report-player=舉報用戶 report-player=舉報用戶
warn-player=警告用戶
jail-player=監禁用戶 jail-player=監禁用戶
kick-player=踢除用戶 kick-player=踢除用戶
ban-player=封禁用戶 ban-player=封禁用戶
afk-time=__1__% 地圖時間\n上次移動: __2__ afk-time=__1__% 地圖時間\n上次移動: __2__
open-map=__1__ __2__\n__3__\n按下打開地圖 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] [warp-list]
main-caption=傳送陣清單 [img=info] main-caption=傳送陣清單 [img=info]
main-tooltip=傳送陣清單 main-tooltip=傳送陣清單
@@ -85,79 +44,6 @@ goto-cooldown=傳送冷卻中, 請等候冷卻
goto-disabled=你不在傳送陣, 請移至任意傳送陣 goto-disabled=你不在傳送陣, 請移至任意傳送陣
goto-edit=修改傳送陣圖案 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] [bonus]
description=取得 / 設定 Bonus 量。 description=取得 / 設定 Bonus 量。
arg-amount=Bonus 數量, 0 來停用。 arg-amount=Bonus 數量, 0 來停用。
+10 -21
View File
@@ -1,31 +1,20 @@
[links] [links]
discord=https://discord.explosivegaming.nl discord=https://discord.aperx.org
website=https://www.explosivegaming.nl website=https://aperx.org
status=https://status.explosivegaming.nl status=https://aperx.org
github=https://github.com/explosivegaming/ExpCluster github=https://github.com/PHIDIAS0303/ExpCluster
patreon=https://www.patreon.com/ExpGaming patreon=https://aperx.org
[info] [info]
players-online=現在有 __1__ 人在線 players-online=現在有 __1__ 人在線
total-map-time=地圖時間為 __1__ total-map-time=地圖時間為 __1__
discord=加入社群: https://discord.explosivegaming.nl discord=加入社群: https://discord.aperx.org
website=訪問網站: https://www.explosivegaming.nl website=訪問網站: https://aperx.org
status=伺服器狀態: https://status.explosivegaming.nl status=伺服器狀態: https://aperx.org
github=增加功能或回報錯誤: https://github.com/explosivegaming/ExpCluster github=增加功能或回報錯誤: https://github.com/PHIDIAS0303/ExpCluster
patreon=支持我們: https://www.patreon.com/ExpGaming patreon=支持我們: https://aperx.org
custom-commands=這裹使用了自制指令,如 /tag 和 /me。可使用 /chelp 查看更多。 custom-commands=這裹使用了自制指令,如 /tag 和 /me。可使用 /chelp 查看更多。
read-readme=確保你已閱讀相關資訊。按左上 i 圖標可再次查看。 read-readme=確保你已閱讀相關資訊。按左上 i 圖標可再次查看。
softmod=這裹用了自設情境,是一種軟裝模組。 softmod=這裹用了自設情境,是一種軟裝模組。
redmew= redmew=
lhd=列車必須是左則通行。這是本服務器長久以來的規則。 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__ 已被系統發出了一則自動警告。
-5
View File
@@ -10,11 +10,6 @@ message-cleared=你的加入信息已清除。
[quickbar] [quickbar]
saved=你的工具列已儲存。 saved=你的工具列已儲存。
[exp-required]
Warnings=警告
Warnings-tooltip=你所有收到的警告
Warnings-value-tooltip=你所有收到的警告
[exp-settings] [exp-settings]
Colour=顏色 Colour=顏色
Colour-tooltip=你的人物顏色 Colour-tooltip=你的人物顏色
@@ -1,11 +1,3 @@
[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] [expcore-data]
description-preference=允許寫入資料。 description-preference=允許寫入資料。
description-data=把用戶資料寫入電腦 description-data=把用戶資料寫入電腦
-114
View File
@@ -7,53 +7,12 @@ reason-entry=輸入原因
goto-player=傳送到用戶 goto-player=傳送到用戶
bring-player=傳送用戶到自己 bring-player=傳送用戶到自己
report-player=舉報用戶 report-player=舉報用戶
warn-player=警告用戶
jail-player=監禁用戶 jail-player=監禁用戶
kick-player=踢除用戶 kick-player=踢除用戶
ban-player=封禁用戶 ban-player=封禁用戶
afk-time=__1__% 地圖時間\n上次移動: __2__ afk-time=__1__% 地圖時間\n上次移動: __2__
open-map=__1__ __2__\n__3__\n按下打開地圖 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] [warp-list]
main-caption=傳送陣清單 [img=info] main-caption=傳送陣清單 [img=info]
main-tooltip=傳送陣清單 main-tooltip=傳送陣清單
@@ -85,79 +44,6 @@ goto-cooldown=傳送冷卻中, 請等候冷卻
goto-disabled=你不在傳送陣, 請移至任意傳送陣 goto-disabled=你不在傳送陣, 請移至任意傳送陣
goto-edit=修改傳送陣圖案 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] [bonus]
description=取得 / 設定 Bonus 量。 description=取得 / 設定 Bonus 量。
arg-amount=Bonus 數量, 0 來停用。 arg-amount=Bonus 數量, 0 來停用。
+1
View File
@@ -8,6 +8,7 @@
"dependencies": { "dependencies": {
"clusterio": "*", "clusterio": "*",
"exp_util": "*", "exp_util": "*",
"exp_roles": "*",
"exp_gui": "*" "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 = {}
local vlayer_data = { local vlayer_data = {
entity_interfaces = { entity_interfaces = {
energy = {}, energy = {}, --- @type LuaEntity[]
circuit = {}, circuit = {}, --- @type LuaEntity[]
storage_input = {}, storage_input = {}, --- @type LuaEntity[]
storage_output = {}, storage_output = {}, --- @type LuaEntity[]
}, },
properties = { properties = {
total_surface_area = 0, total_surface_area = 0,
@@ -28,10 +28,10 @@ local vlayer_data = {
capacity = 0, capacity = 0,
}, },
storage = { storage = {
items = {}, items = {}, --- @type table<string, number>
power_items = {}, power_items = {}, --- @type table<string, { value: number, count: number }>
energy = 0, energy = 0,
unallocated = {}, unallocated = {}, --- @type table<string, number>
}, },
surface = table.deep_copy(config.surface), surface = table.deep_copy(config.surface),
} }
@@ -45,7 +45,7 @@ for name, properties in pairs(config.allowed_items) do
if properties.power then if properties.power then
vlayer_data.storage.power_items[name] = { vlayer_data.storage.power_items[name] = {
value = properties.fuel_value * 1000000, value = assert(properties.fuel_value) * 1000000,
count = 0, count = 0,
} }
end end
@@ -349,7 +349,7 @@ function vlayer.create_input_interface(surface, position, circuit, last_user)
end end
interface.destructible = false interface.destructible = false
interface.minable = false interface.minable_flag = false
interface.operable = true interface.operable = true
return interface return interface
@@ -361,7 +361,7 @@ local function handle_input_interfaces()
if not interface.valid then if not interface.valid then
vlayer_data.entity_interfaces.storage_input[index] = nil vlayer_data.entity_interfaces.storage_input[index] = nil
else 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 for _, v in pairs(inventory.get_contents()) do
if config.allowed_items[v.name] then if config.allowed_items[v.name] then
@@ -384,8 +384,9 @@ local function handle_input_interfaces()
if count_deduct and count_add then if count_deduct and count_add then
if config.allowed_items[v.name].modded then if config.allowed_items[v.name].modded then
local modded_item = assert(config.modded_items[v.name])
if config.modded_auto_downgrade then 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 else
vlayer.insert_item(v.name, count_add) vlayer.insert_item(v.name, count_add)
end end
@@ -427,7 +428,7 @@ function vlayer.create_output_interface(surface, position, circuit, last_user)
end end
interface.destructible = false interface.destructible = false
interface.minable = false interface.minable_flag = false
interface.operable = true interface.operable = true
return interface return interface
@@ -439,14 +440,14 @@ local function handle_output_interfaces()
if not interface.valid then if not interface.valid then
vlayer_data.entity_interfaces.storage_output[index] = nil vlayer_data.entity_interfaces.storage_output[index] = nil
else else
local inventory = interface.get_inventory(defines.inventory.chest) local inventory = assert(interface.get_inventory(defines.inventory.chest))
local inventory_request_sections = interface.get_logistic_sections().sections local inventory_request_sections = assert(interface.get_logistic_sections()).sections
for i = 1, #inventory_request_sections do for i = 1, #inventory_request_sections do
for _, v in pairs(inventory_request_sections[i].filters) do for _, v in pairs(inventory_request_sections[i].filters) do
if v.value and config.allowed_items[v.value.name] then if v.value and config.allowed_items[v.value.name] then
local current_amount = inventory.get_item_count(v.value.name) 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 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) 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_sustained = vdp * gsm,
energy_capacity = vlayer_data.properties.capacity * mega, energy_capacity = vlayer_data.properties.capacity * mega,
energy_storage = vlayer_data.storage.energy, 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 end
@@ -540,9 +538,6 @@ function vlayer.get_circuits()
energy_sustained = "signal-S", energy_sustained = "signal-S",
energy_capacity = "signal-C", energy_capacity = "signal-C",
energy_storage = "signal-E", energy_storage = "signal-E",
day_time = "signal-D",
day_length = "signal-L",
tick = "signal-T",
} }
end end
@@ -574,7 +569,7 @@ function vlayer.create_circuit_interface(surface, position, circuit, last_user)
end end
interface.destructible = false interface.destructible = false
interface.minable = false interface.minable_flag = false
interface.operable = true interface.operable = true
return interface return interface
@@ -588,11 +583,11 @@ local function handle_circuit_interfaces()
if not interface.valid then if not interface.valid then
vlayer_data.entity_interfaces.circuit[index] = nil vlayer_data.entity_interfaces.circuit[index] = nil
else else
local circuit_oc = interface.get_or_create_control_behavior() local control = interface.get_or_create_control_behavior() --[[@as LuaConstantCombinatorControlBehavior]]
if circuit_oc.sections_count == 0 then if control.sections_count == 0 then
circuit_oc.add_section() control.add_section()
end end
circuit_oc = circuit_oc.sections[1] local circuit_oc = assert(control.sections[1])
local signal_index = 1 local signal_index = 1
local circuit = vlayer.get_circuits() 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) -- Clear remaining signals to prevent outdated values being present (caused by count > 0 check)
for clear_index = signal_index, #circuit do 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 break -- There are no more signals to clear
end end
@@ -653,7 +648,7 @@ function vlayer.create_energy_interface(surface, position, last_user)
end end
interface.destructible = false interface.destructible = false
interface.minable = false interface.minable_flag = false
interface.operable = false interface.operable = false
interface.electric_buffer_size = 0 interface.electric_buffer_size = 0
interface.power_production = 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.destructible = false
entity.health = 0 entity.health = 0
entity.minable = false entity.minable_flag = false
entity.rotatable = false entity.rotatable = false
-- Save reference of the last power pole -- 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)
+1 -1
View File
@@ -8,7 +8,7 @@ local Commands = require("modules/exp_commands")
local PlayerData = require("modules.exp_legacy.expcore.player_data") --- @dep expcore.player_data local PlayerData = require("modules.exp_legacy.expcore.player_data") --- @dep expcore.player_data
local CustomMessages = PlayerData.Settings:combine("JoinMessage") local CustomMessages = PlayerData.Settings:combine("JoinMessage")
CustomMessages:set_metadata{ CustomMessages:set_metadata{
permission = "command/join-message", permission = "exp_scenario.command.set_join_message",
} }
--- When a players data loads show their message --- When a players data loads show their message
@@ -5,6 +5,10 @@ local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event
local PlayerData = require("modules.exp_legacy.expcore.player_data") --- @dep expcore.player_data local PlayerData = require("modules.exp_legacy.expcore.player_data") --- @dep expcore.player_data
local LocalLanguage = PlayerData.Statistics:combine("LocalLanguage") local LocalLanguage = PlayerData.Statistics:combine("LocalLanguage")
LocalLanguage:set_default("Unknown") LocalLanguage:set_default("Unknown")
LocalLanguage:set_metadata{
name = { "exp-statistics.Locale" },
tooltip = { "exp-statistics.Locale-tooltip" },
}
local function set_locale(event) local function set_locale(event)
local player = game.players[event.player_index] local player = game.players[event.player_index]
@@ -1,89 +0,0 @@
local Commands = require("modules/exp_commands")
local config = require("modules.exp_legacy.config.personal_logistic") --- @dep config.personal-logistic
---@param target LuaEntity | LuaPlayer
---@param amount number
local function pl(target, amount)
local c
local s
--- @cast target any Remove cast once implemented
error("Needs updating to use 2.0 logistics")
if target.object_name == "LuaPlayer" then
c = target.clear_personal_logistic_slot
s = target.set_personal_logistic_slot
else
c = target.clear_vehicle_logistic_slot
s = target.set_vehicle_logistic_slot
end
for _, v in pairs(config.request) do
c(config.start + v.key)
end
if (amount < 0) then
return
end
local stats = target.force.get_item_production_statistics(target.surface)
for k, v in pairs(config.request) do
local v_min = math.ceil(v.min * amount)
local v_max = math.ceil(v.max * amount)
if v.stack ~= nil and v.stack ~= 1 and v.type ~= "weapon" then
v_min = math.floor(v_min / v.stack) * v.stack
v_max = math.ceil(v_max / v.stack) * v.stack
end
if v.upgrade_of == nil then
if v.type ~= nil then
if stats.get_input_count(k) < config.production_required[v.type] then
if v_min > 0 then
if v_min == v_max then
v_min = math.floor((v_max * 0.5) / v.stack) * v.stack
end
else
v_min = 0
end
end
end
s(config.start + v.key, { name = k, min = v_min, max = v_max })
else
if v.type ~= nil then
if stats.get_input_count(k) >= config.production_required[v.type] then
s(config.start + v.key, { name = k, min = v_min, max = v_max })
local vuo = v.upgrade_of
while (vuo ~= nil) do
s(config.start + config.request[vuo].key, { name = vuo, min = 0, max = 0 })
vuo = config.request[vuo].upgrade_of
end
else
s(config.start + v.key, { name = k, min = 0, max = v_max })
end
end
end
end
end
Commands.new("personal-logistic", "Set Personal Logistic (-1 to cancel all) (Select spidertron to edit spidertron)")
:argument("amount", "", Commands.types.integer_range(-1, 10))
:add_aliases{ "pl" }
:add_flags{ "disabled" } -- Remove once implemented above
:register(function(player, amount)
--- @cast amount number
if player.force.technologies["logistic-robotics"].researched then
if player.selected ~= nil then
if player.selected.name == "spidertron" then
pl(player.selected, amount / 10)
end
else
pl(player, amount / 10)
end
else
return Commands.status.error("Personal Logistic not researched")
end
end)
+16 -19
View File
@@ -10,7 +10,7 @@ local config = require("modules.exp_legacy.config.preset_player_quickbar") --- @
local PlayerData = require("modules.exp_legacy.expcore.player_data") --- @dep expcore.player_data local PlayerData = require("modules.exp_legacy.expcore.player_data") --- @dep expcore.player_data
local PlayerFilters = PlayerData.Settings:combine("QuickbarFilters") local PlayerFilters = PlayerData.Settings:combine("QuickbarFilters")
PlayerFilters:set_metadata{ PlayerFilters:set_metadata{
permission = "command/save-quickbar", permission = "exp_scenario.command.save_quickbar",
stringify = function(value) stringify = function(value)
if not value then return "No filters set" end if not value then return "No filters set" end
local count = 0 local count = 0
@@ -20,39 +20,36 @@ PlayerFilters:set_metadata{
end, end,
} }
--- Filters are stored by a single index across the ten pages of ten slots
local total_page_count = 10
local slots_per_page = 10
--- Loads your quickbar preset --- Loads your quickbar preset
PlayerFilters:on_load(function(player_name, filters) PlayerFilters:on_load(function(player_name, filters)
if not filters then filters = config[player_name] end if not filters then filters = config[player_name] end
if not filters then return end if not filters then return end
local player = game.players[player_name] local player = game.players[player_name]
for i, item_name in pairs(filters) do for index, item_name in pairs(filters) do
if item_name ~= nil and item_name ~= "" then if item_name ~= nil and item_name ~= "" then
player.set_quick_bar_slot(i, item_name) local page = math.ceil(index / slots_per_page)
local slot = (index - 1) % slots_per_page + 1
player.set_quick_bar_slot(page, slot, item_name)
end end
end end
end) end)
local ignored_items = { --- Saves your quickbar preset, only plain item filters can be saved
["blueprint"] = true,
["blueprint-book"] = true,
["deconstruction-planner"] = true,
["spidertron-remote"] = true,
["upgrade-planner"] = true,
}
--- Saves your quickbar preset to the script-output folder
Commands.new("save-quickbar", "Saves your Quickbar preset items to file") Commands.new("save-quickbar", "Saves your Quickbar preset items to file")
:add_aliases{ "save-toolbar" } :add_aliases{ "save-toolbar" }
:register(function(player) :register(function(player)
local filters = {} local filters = {}
for i = 1, 100 do for page = 1, total_page_count do
local slot = player.get_quick_bar_slot(i) for slot = 1, slots_per_page do
-- Need to filter out blueprint and blueprint books because the slot is a LuaItemPrototype and does not contain a way to export blueprint data -- Records, remotes and specific item instances hold data which can not be saved by name
if slot ~= nil then local quick_bar_slot = player.get_quick_bar_slot(page, slot)
local ignored = ignored_items[slot.name] if quick_bar_slot and quick_bar_slot.type == "filter" then
if ignored ~= true then filters[(page - 1) * slots_per_page + slot] = assert(quick_bar_slot.filter).name
filters[i] = slot.name
end end
end end
end end
+39 -12
View File
@@ -113,16 +113,47 @@ if config.MachinesRemoved or config.TreesDestroyed or config.OreMined then
Event.add(defines.events.on_player_mined_entity, on_event) Event.add(defines.events.on_player_mined_entity, on_event)
end end
--- Vehicles which credit their driver, or failing that their passenger, with what they hit
local crewed_vehicles = { car = true, ["spider-vehicle"] = true, locomotive = true }
--- Get the connected player behind the cause of damage or a death, nil for turrets, trains without a crew and the like
--- @param cause LuaEntity?
--- @return LuaPlayer?
local function get_cause_player(cause)
if not cause or not cause.valid then return nil end
local occupant = cause --- @type LuaEntity | LuaPlayer
if crewed_vehicles[cause.type] then
local crew = cause.get_driver() or cause.get_passenger()
if not crew then return nil end
occupant = crew
end
local player --- @type LuaPlayer?
if occupant.object_name == "LuaPlayer" then
player = occupant --[[@as LuaPlayer]]
elseif occupant.type == "character" then
player = occupant.player
end
if not player or not player.valid or not player.connected then return nil end
return player
end
--- Check that damage or a death counts against an entity, it must be hostile to the player
--- @param entity LuaEntity
--- @param player LuaPlayer
--- @return boolean
local function is_hostile(entity, player)
return entity.valid and entity.force ~= player.force and entity.force.name ~= "neutral"
end
--- Add DamageDealt if it is enabled --- Add DamageDealt if it is enabled
if config.DamageDealt then if config.DamageDealt then
local stat = Statistics:combine("DamageDealt") local stat = Statistics:combine("DamageDealt")
Event.add(defines.events.on_entity_damaged, function(event) Event.add(defines.events.on_entity_damaged, function(event)
local character = event.cause -- Check character is valid local player = get_cause_player(event.cause)
if not character or not character.valid or character.type ~= "character" then return end if not player or not is_hostile(event.entity, player) then return end
local player = character.player -- Check player is valid
if not player.valid or not player.connected then return end
local entity = event.entity -- Check entity is valid
if not entity.valid or entity.force == player.force or entity.force.name == "neutral" then return end
stat:increment(player, floor(event.final_damage_amount)) stat:increment(player, floor(event.final_damage_amount))
end) end)
end end
@@ -131,12 +162,8 @@ end
if config.Kills then if config.Kills then
local stat = Statistics:combine("Kills") local stat = Statistics:combine("Kills")
Event.add(defines.events.on_entity_died, function(event) Event.add(defines.events.on_entity_died, function(event)
local character = event.cause -- Check character is valid local player = get_cause_player(event.cause)
if not character or not character.valid or character.type ~= "character" then return end if not player or not is_hostile(event.entity, player) then return end
local player = character.player -- Check player is valid
if not player or not player.valid or not player.connected then return end
local entity = event.entity -- Check entity is valid
if not entity.valid or entity.force == player.force or entity.force.name == "neutral" then return end
stat:increment(player) stat:increment(player)
end) end)
end end
+4 -4
View File
@@ -4,17 +4,17 @@
]] ]]
local Commands = require("modules/exp_commands") local Commands = require("modules/exp_commands")
local Roles = require("modules.exp_legacy.expcore.roles") --- @dep expcore.roles local Roles = require("modules/exp_roles")
--- Stores the tag for a player --- Stores the tag for a player
local PlayerData = require("modules.exp_legacy.expcore.player_data") --- @dep expcore.player_data local PlayerData = require("modules.exp_legacy.expcore.player_data") --- @dep expcore.player_data
local PlayerTags = PlayerData.Settings:combine("Tag") local PlayerTags = PlayerData.Settings:combine("Tag")
local PlayerTagColors = PlayerData.Settings:combine("TagColor") local PlayerTagColors = PlayerData.Settings:combine("TagColor")
PlayerTags:set_metadata{ PlayerTags:set_metadata{
permission = "command/tag", permission = "exp_scenario.command.tag",
} }
PlayerTagColors:set_metadata{ PlayerTagColors:set_metadata{
permission = "command/tag-color", permission = "exp_scenario.command.tag_color",
} }
local set_tag = function(player, tag, color) local set_tag = function(player, tag, color)
@@ -72,7 +72,7 @@ Commands.new("tag-clear", "Clears your tag. Or another player if you are admin."
if other_player == player then if other_player == player then
-- No player given so removes your tag -- No player given so removes your tag
PlayerTags:remove(other_player) PlayerTags:remove(other_player)
elseif Roles.player_allowed(player, "command/clear-tag/always") then elseif Roles.player_has_permission(player, "exp_scenario.command.tag_clear.always") then
-- Player given and user is admin so clears that player's tag -- Player given and user is admin so clears that player's tag
PlayerTags:remove(other_player) PlayerTags:remove(other_player)
else else
+1 -1
View File
@@ -18,7 +18,7 @@ ToolbarState:on_load(function(player_name, value)
local decompressed = helpers.json_to_table(assert(helpers.decode_string(value), "Failed String Decode")) local decompressed = helpers.json_to_table(assert(helpers.decode_string(value), "Failed String Decode"))
local player = assert(game.get_player(player_name)) local player = assert(game.get_player(player_name))
Gui.toolbar.set_state(player, decompressed --[[ @as Gui.ToolbarState ]]) Gui.toolbar.set_state(player, decompressed --[[@as Gui.ToolbarState]])
return nil -- We don't save the state, use Gui.toolbar.get_state return nil -- We don't save the state, use Gui.toolbar.get_state
end) end)
@@ -126,19 +126,19 @@ lib.collect_loginet = function()
end end
if config.modules.logistorage then if config.modules.logistorage then
for name, v in pairs(network.get_contents()) do for _, item in pairs(network.get_contents()) do
stats.items[name] = (stats.items[name] or 0) + v stats.items[item.name] = (stats.items[item.name] or 0) + item.count
end end
-- pickups and deliveries of items -- pickups and deliveries of items
for _, point_list in pairs{ network.provider_points, network.requester_points, network.storage_points } do for _, point_list in pairs{ network.provider_points, network.requester_points, network.storage_points } do
for _, point in pairs(point_list) do for _, point in pairs(point_list) do
for name, qty in pairs(point.targeted_items_pickup) do for _, item in pairs(point.targeted_items_pickup) do
stats.pickups[name] = (stats.pickups[name] or 0) + qty stats.pickups[item.name] = (stats.pickups[item.name] or 0) + item.count
end end
for name, qty in pairs(point.targeted_items_deliver) do for _, item in pairs(point.targeted_items_deliver) do
stats.deliveries[name] = (stats.deliveries[name] or 0) + qty stats.deliveries[item.name] = (stats.deliveries[item.name] or 0) + item.count
end end
end end
end end

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