Refactored UPS monitor as clusterio plugin (#398)

* Refactor server ups

* Use catalogs

* Move to own plugin

* Use web config

* Remove External.get_server_ups

* Update workspace version requirement

* Remove need for storage

* Add locale

* Fix CI
This commit is contained in:
Cooldude2606
2025-08-08 16:36:22 +01:00
committed by GitHub
parent 69909e3202
commit ce80ae9021
34 changed files with 4620 additions and 167 deletions

View File

@@ -9,13 +9,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: "pnpm"
- name: Install FMTK
run: |
npm install factoriomod-debug
npx fmtk luals-addon
pnpm install factoriomod-debug
pnpm exec fmtk luals-addon
jq '.["workspace.library"] += ["${{ github.workspace }}/factorio/library"] | .["runtime.plugin"] = "${{ github.workspace }}/factorio/plugin.lua"' .luarc.json > temp.luarc.json
jq -s '.[0] * .[1].settings' temp.luarc.json ${{ github.workspace }}/factorio/config.json > check.luarc.json
- name: Install LuaLS

2
.gitignore vendored
View File

@@ -1,5 +1,3 @@
dist/
node_modules/
package-lock.json
pnpm-lock.yaml
.vscode

View File

@@ -595,6 +595,7 @@ end
--- @param parameter string The raw command parameter that was used
--- @param detail any
local function log_command(comment, command, player, parameter, detail)
if player.index == 0 and comment == "Command Ran" then return end
ExpUtil.write_json("log/commands.log", {
comment = comment,
command_name = command.name,

View File

@@ -13,16 +13,16 @@
"node": ">=18"
},
"peerDependencies": {
"@clusterio/lib": "^2.0.0-alpha.19"
"@clusterio/lib": "catalog:"
},
"devDependencies": {
"@clusterio/lib": "^2.0.0-alpha.19",
"@types/node": "^20.14.9",
"typescript": "^5.5.3"
"@clusterio/lib": "catalog:",
"@types/node": "catalog:",
"typescript": "catalog:"
},
"dependencies": {
"@expcluster/lib_util": "workspace:*",
"@sinclair/typebox": "^0.30.4"
"@expcluster/lib_util": "workspace:^",
"@sinclair/typebox": "catalog:"
},
"publishConfig": {
"access": "public"

View File

@@ -5,7 +5,7 @@
"description": "Example Description. Package. Change me in package.json",
"main": "dist/node/index.js",
"scripts": {
"prepare": "tsc --build && webpack-cli --env production"
"$prepare": "tsc --build && webpack-cli --env production"
},
"engines": {
"node": ">=18"

View File

@@ -28,7 +28,7 @@ local GuiIter = {
_scopes = registered_scopes,
}
local function nop() return nil, nil end
local function no_loop() return nil, nil end
--- Get the next valid element
--- @param elements table<uint, LuaGuiElement>
@@ -51,9 +51,9 @@ end
--- @param online boolean?
--- @return uint?, LuaPlayer?, table<uint, LuaGuiElement>?
local function next_valid_player(scope_elements, players, prev_index, online)
local index, player = nil, nil
local index, player = prev_index, nil
while true do
index, player = next(players, prev_index)
index, player = next(players, index)
while player and not player.valid do
scope_elements[player.index] = nil
index, player = next(players, index)
@@ -66,7 +66,7 @@ local function next_valid_player(scope_elements, players, prev_index, online)
if online == nil or player.connected == online then
local player_elements = scope_elements[player.index]
if player_elements and #player_elements > 0 then
if player_elements and next(player_elements) then
return index, player, player_elements
end
end
@@ -78,13 +78,13 @@ end
--- @param player LuaPlayer
--- @return ExpGui_GuiIter.ReturnType
function GuiIter.player_elements(scope, player)
if not player.valid then return nop end
if not player.valid then return no_loop end
local scope_elements = registered_scopes[scope]
if not scope_elements then return nop end
if not scope_elements then return no_loop end
local player_elements = scope_elements[player.index]
if not player_elements then return nop end
if not player_elements then return no_loop end
local element_index, element = nil, nil
return function()
@@ -101,7 +101,7 @@ end
--- @return ExpGui_GuiIter.ReturnType
function GuiIter.filtered_elements(scope, players, online)
local scope_elements = registered_scopes[scope]
if not scope_elements then return nop end
if not scope_elements then return no_loop end
local index, player, player_elements = nil, nil, nil
local element_index, element = nil, nil
@@ -128,7 +128,7 @@ end
--- @return ExpGui_GuiIter.ReturnType
function GuiIter.all_elements(scope)
local scope_elements = registered_scopes[scope]
if not scope_elements then return nop end
if not scope_elements then return no_loop end
local player_index, player_elements, player = nil, nil, nil
local element_index, element = nil, nil
@@ -193,7 +193,7 @@ function GuiIter.get_online_elements(scope, filter)
return GuiIter.filtered_elements(scope, game.connected_players)
elseif class_name == "LuaPlayer" then
--- @cast filter LuaPlayer
if not filter.connected then return nop end
if not filter.connected then return no_loop end
return GuiIter.player_elements(scope, filter)
elseif class_name == "LuaForce" then
--- @cast filter LuaForce

View File

@@ -13,16 +13,16 @@
"node": ">=18"
},
"peerDependencies": {
"@clusterio/lib": "^2.0.0-alpha.19"
"@clusterio/lib": "catalog:"
},
"devDependencies": {
"@clusterio/lib": "^2.0.0-alpha.19",
"@types/node": "^20.14.9",
"typescript": "^5.5.3"
"@clusterio/lib": "catalog:",
"@types/node": "catalog:",
"typescript": "catalog:"
},
"dependencies": {
"@expcluster/lib_util": "workspace:*",
"@sinclair/typebox": "^0.30.4"
"@expcluster/lib_util": "workspace:^",
"@sinclair/typebox": "catalog:"
},
"publishConfig": {
"access": "public"

View File

@@ -51,7 +51,6 @@ return {
"modules.gui.task-list",
"modules.gui.warp-list",
"modules.gui.player-list",
"modules.gui.server-ups",
"modules.gui.bonus",
"modules.gui.vlayer",
"modules.gui.research",

View File

@@ -116,16 +116,6 @@ function External.get_server_status(server_id, raw)
return not raw and server_id == current and "Current" or assert(servers[server_id], "No status found for server with id: " .. tostring(server_id))
end
--[[-- Gets the ups of the current server
@usage-- Get the ups of the current server
local server_ups = External.get_server_ups()
]]
function External.get_server_ups()
assert(var, "No external data was found, use External.valid() to ensure external data exists.")
return assert(var.server_ups, "No server ups was found, please ensure that the external service is running")
end
--[[-- Connect a player to the given server
@tparam LuaPlayer player The player that you want to request to join a different server
@tparam string server_id The internal id of the server to connect to, can also be any address but this will show Unknown Server

View File

@@ -332,10 +332,6 @@ attempt=Attempt
difference=Diff
main-tooltip=Research GUI
[server-ups]
description=Toggle the server UPS display.
no-ext=No external source was found, cannot display server ups.
[tool]
main-tooltip=Tool
apply=Apply

View File

@@ -340,10 +340,6 @@ attempt=用時
difference=差距
main-tooltip=研究介面
[server-ups]
description=啟動 UPS 顯示
no-ext=沒找到外置數據,沒法顯示。
[tool]
main-tooltip=工具
apply=應用

View File

@@ -340,10 +340,6 @@ attempt=用時
difference=差距
main-tooltip=研究介面
[server-ups]
description=啟動 UPS 顯示
no-ext=沒找到外置數據,沒法顯示。
[tool]
main-tooltip=工具
apply=應用

View File

@@ -1,88 +0,0 @@
--[[-- Gui Module - Server UPS
- Adds a server ups counter in the top right and a command to toggle is
@gui server-ups
@alias server_ups
]]
local Gui = require("modules/exp_gui")
local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event
local External = require("modules.exp_legacy.expcore.external") --- @dep expcore.external
local Commands = require("modules/exp_commands")
--- Stores the visible state of server ups
local PlayerData = require("modules.exp_legacy.expcore.player_data") --- @dep expcore.player_data
local UsesServerUps = PlayerData.Settings:combine("UsesServerUps")
UsesServerUps:set_default(false)
UsesServerUps:set_metadata{
permission = "command/server-ups",
stringify = function(value) return value and "Visible" or "Hidden" end,
}
--- Label to show the server ups
-- @element server_ups
local server_ups = Gui.element("server_ups")
:draw{
type = "label",
caption = "SUPS = 60.0",
name = Gui.property_from_name,
}
:style{
font = "default-game",
}
--- Change the visible state when your data loads
UsesServerUps:on_load(function(player_name, visible)
local player = game.players[player_name]
local label = player.gui.screen[server_ups.name]
--- @diagnostic disable-next-line undefined-field
if not External.valid() or not storage.ext.var.server_ups then visible = false end
label.visible = visible or false
end)
--- Toggles if the server ups is visbile
Commands.new("server-ups", { "server-ups.description" })
:add_aliases{ "sups", "ups" }
:register(function(player)
local label = player.gui.screen[server_ups.name]
if not External.valid() then
label.visible = false
return Commands.status.error{ "server-ups.no-ext" }
end
label.visible = not label.visible
UsesServerUps:set(player, label.visible)
end)
-- Set the location of the label
-- 1920x1080: x=1455, y=30 (ui scale 100%)
local function set_location(event)
local player = game.players[event.player_index]
local label = player.gui.screen[server_ups.name]
if not label then
label = server_ups(player.gui.screen)
label.visible = UsesServerUps:get(player)
end
local res = player.display_resolution
local uis = player.display_scale
-- below ups and clock
-- label.location = {x=res.width-423*uis, y=50*uis}
label.location = { x = res.width - 363 * uis, y = 31 * uis }
end
-- Draw the label when the player joins
Event.add(defines.events.on_player_created, set_location)
Event.add(defines.events.on_player_joined_game, set_location)
-- Update the caption for all online players
-- percentage of game speed
Event.on_nth_tick(60, function()
if External.valid() then
local caption = External.get_server_ups() .. " (" .. string.format("%.1f", External.get_server_ups() * 5 / 3) .. "%)"
for _, player in pairs(game.connected_players) do
player.gui.screen[server_ups.name].caption = caption
end
end
end)
-- Update when res or ui scale changes
Event.add(defines.events.on_player_display_resolution_changed, set_location)
Event.add(defines.events.on_player_display_scale_changed, set_location)

View File

@@ -13,17 +13,17 @@
"node": ">=18"
},
"peerDependencies": {
"@clusterio/lib": "^2.0.0-alpha.19"
"@clusterio/lib": "catalog:"
},
"devDependencies": {
"@clusterio/lib": "^2.0.0-alpha.20",
"@types/node": "^20.4.5",
"typescript": "^5.5.3"
"@clusterio/lib": "catalog:",
"@types/node": "catalog:",
"typescript": "catalog:"
},
"dependencies": {
"@expcluster/lib_commands": "workspace:*",
"@expcluster/lib_util": "workspace:*",
"@sinclair/typebox": "^0.30.4"
"@expcluster/lib_commands": "workspace:^",
"@expcluster/lib_util": "workspace:^",
"@sinclair/typebox": "catalog:"
},
"publishConfig": {
"access": "public"

View File

@@ -13,25 +13,25 @@
"node": ">=18"
},
"peerDependencies": {
"@clusterio/lib": "^2.0.0-alpha.19"
"@clusterio/lib": "catalog:"
},
"devDependencies": {
"@clusterio/lib": "^2.0.0-alpha.20",
"@clusterio/web_ui": "^2.0.0-alpha.20.b",
"@types/node": "^20.4.5",
"@types/react": "^18.2.21",
"antd": "^5.13.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^5.5.3",
"webpack": "^5.98.0",
"webpack-cli": "^5.1.4",
"webpack-merge": "^5.9.0"
"@clusterio/lib": "catalog:",
"@clusterio/web_ui": "catalog:",
"@types/node": "catalog:",
"@types/react": "catalog:",
"antd": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"typescript": "catalog:",
"webpack": "catalog:",
"webpack-cli": "catalog:",
"webpack-merge": "catalog:"
},
"dependencies": {
"@expcluster/lib_commands": "workspace:*",
"@expcluster/lib_util": "workspace:*",
"@sinclair/typebox": "^0.30.4"
"@expcluster/lib_commands": "workspace:^",
"@expcluster/lib_util": "workspace:^",
"@sinclair/typebox": "catalog:"
},
"publishConfig": {
"access": "public"

30
exp_server_ups/index.ts Normal file
View File

@@ -0,0 +1,30 @@
import * as lib from "@clusterio/lib";
declare module "@clusterio/lib" {
export interface InstanceConfigFields {
"exp_server_ups.update_interval": number;
"exp_server_ups.average_interval": number;
}
}
export const plugin: lib.PluginDeclaration = {
name: "exp_server_ups",
title: "ExpGaming - Server UPS",
description: "Clusterio plugin providing in game server ups counter",
instanceEntrypoint: "./dist/node/instance",
instanceConfigFields: {
"exp_server_ups.update_interval": {
title: "Update Interval",
description: "Frequency at which updates are exchanged with factorio (ms)",
type: "number",
initialValue: 1000,
},
"exp_server_ups.average_interval": {
title: "Average Interval",
description: "Number of update intervals to average updates per second across",
type: "number",
initialValue: 60
},
},
};

View File

@@ -0,0 +1,48 @@
import * as lib from "@clusterio/lib";
import { BaseInstancePlugin } from "@clusterio/host";
export class InstancePlugin extends BaseInstancePlugin {
private updateInterval?: ReturnType<typeof setInterval>;
private gameTimes: number[] = [];
async onStart() {
this.updateInterval = setInterval(this.updateUps.bind(this), this.instance.config.get("exp_server_ups.update_interval"));
}
async onStop() {
if (this.updateInterval) {
clearInterval(this.updateInterval);
}
}
async onInstanceConfigFieldChanged(field: string, curr: unknown): Promise<void> {
if (field === "exp_server_ups.update_interval") {
await this.onStop();
await this.onStart();
} else if (field === "exp_server_ups.average_interval") {
this.gameTimes.splice(curr as number);
}
}
async updateUps() {
let ups = 0;
const collected = this.gameTimes.length - 1;
if (collected > 0) {
const minTick = this.gameTimes[0];
const maxTick = this.gameTimes[collected];
const interval = this.instance.config.get("exp_server_ups.update_interval") / 1000;
ups = (maxTick - minTick) / (collected * interval);
}
try {
const newGameTime = await this.sendRcon(`/_rcon return exp_server_ups.update(${ups})`);
this.gameTimes.push(Number(newGameTime));
} catch (error: any) {
this.logger.error(`Failed to receive new game time: ${error}`);
}
if (collected > this.instance.config.get("exp_server_ups.average_interval")) {
this.gameTimes.shift();
}
}
}

View File

@@ -0,0 +1,95 @@
--[[-- Gui - Server UPS
Adds a server ups counter in the top right corner and a command to toggle it
]]
local Gui = require("modules/exp_gui")
local ExpUtil = require("modules/exp_util")
local Commands = require("modules/exp_commands")
--- Label to show the server ups, drawn to screen on join
local server_ups = Gui.element("server_ups")
:track_all_elements()
:draw{
type = "label",
name = Gui.property_from_name,
}
:style{
font = "default-game",
}
:player_data(function(def, element)
local player = Gui.get_player(element)
local existing = def.data[player]
if not existing or not existing.valid then
return element -- Only set if no previous
end
end)
--- Update the caption for all online players
--- @param ups number The UPS to be displayed
local function update_server_ups(ups)
local caption = ("%.1f (%.1f%%)"):format(ups, ups * 5 / 3)
for _, element in server_ups:online_elements() do
element.caption = caption
end
end
--- Stores the visible state of server ups element for a player
local PlayerData = require("modules/exp_legacy/expcore/player_data")
local UsesServerUps = PlayerData.Settings:combine("UsesServerUps")
UsesServerUps:set_default(false)
UsesServerUps:set_metadata{
permission = "command/server-ups",
stringify = function(value) return value and "Visible" or "Hidden" end,
}
--- Change the visible state when your data loads
UsesServerUps:on_load(function(player_name, visible)
local player = assert(game.get_player(player_name))
server_ups.data[player].visible = visible or false
end)
--- Toggles if the server ups is visbile
Commands.new("server-ups", { "exp_server-ups.description" })
:add_aliases{ "sups", "ups" }
:register(function(player)
local visible = not UsesServerUps:get(player)
server_ups.data[player].visible = visible
UsesServerUps:set(player, visible)
end)
--- Add an interface which can be called from rcon
Commands.add_rcon_static("exp_server_ups", {
update = function(ups)
ExpUtil.assert_argument_type(ups, "number", 1, "ups")
update_server_ups(ups)
return game.tick
end
})
--- Set the location of the label
local function set_location(event)
local player = game.players[event.player_index]
local element = server_ups.data[player]
if not element then
element = server_ups(player.gui.screen)
element.visible = UsesServerUps:get(player)
end
local uis = player.display_scale
local res = player.display_resolution
element.location = { x = res.width - 363 * uis, y = 31 * uis } -- below ups and clock
end
local e = defines.events
return {
elements = {
server_ups = server_ups,
},
events = {
[e.on_player_created] = set_location,
[e.on_player_joined_game] = set_location,
[e.on_player_display_resolution_changed] = set_location,
[e.on_player_display_scale_changed] = set_location,
},
}

View File

@@ -0,0 +1,2 @@
[exp_server-ups]
description=Toggle the server UPS display.

View File

@@ -0,0 +1,2 @@
[exp_server-ups]
description=啟動 UPS 顯示

View File

@@ -0,0 +1,2 @@
[exp_server-ups]
description=啟動 UPS 顯示

View File

@@ -0,0 +1,14 @@
{
"name": "exp_server_ups",
"load": [
"control.lua"
],
"require": [
],
"dependencies": {
"clusterio": "*",
"exp_commands": "*",
"exp_util": "*",
"exp_gui": "*"
}
}

View File

@@ -0,0 +1,3 @@
-- Access the exports from other modules using require("modules/exp_server_ups")
return require("modules/exp_server_ups/control").elements.server_ups

View File

@@ -0,0 +1,40 @@
{
"name": "@expcluster/server_ups",
"version": "0.1.0",
"description": "Clusterio plugin providing in game server ups counter",
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
"license": "MIT",
"repository": "explosivegaming/ExpCluster",
"main": "dist/node/index.js",
"scripts": {
"prepare": "tsc --build && webpack-cli --env production"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@clusterio/lib": "catalog:"
},
"devDependencies": {
"typescript": "catalog:",
"@types/node": "catalog:",
"@clusterio/lib": "catalog:",
"webpack": "catalog:",
"webpack-cli": "catalog:",
"webpack-merge": "catalog:"
},
"dependencies": {
"@sinclair/typebox": "catalog:",
"@expcluster/lib_util": "workspace:^",
"@expcluster/lib_gui": "workspace:^",
"@expcluster/lib_commands": "workspace:^"
},
"publishConfig": {
"access": "public"
},
"keywords": [
"clusterio",
"clusterio-plugin",
"factorio"
]
}

View File

@@ -0,0 +1,4 @@
{
"extends": "../tsconfig.browser.json",
"include": [ "web/**/*.tsx", "web/**/*.ts", "package.json" ],
}

View File

@@ -0,0 +1,6 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -0,0 +1,5 @@
{
"extends": "../tsconfig.node.json",
"include": ["./**/*.ts"],
"exclude": ["test/*", "./dist/*"],
}

View File

View File

@@ -0,0 +1,29 @@
"use strict";
const path = require("path");
const webpack = require("webpack");
const { merge } = require("webpack-merge");
const common = require("@clusterio/web_ui/webpack.common");
module.exports = (env = {}) => merge(common(env), {
context: __dirname,
entry: "./web/index.tsx",
output: {
path: path.resolve(__dirname, "dist", "web"),
},
plugins: [
new webpack.container.ModuleFederationPlugin({
name: "exp_server_ups",
library: { type: "window", name: "plugin_exp_server_ups" },
exposes: {
"./": "./index.ts",
"./package.json": "./package.json",
"./web": "./web/index.tsx",
},
shared: {
"@clusterio/lib": { import: false },
"@clusterio/web_ui": { import: false },
},
}),
],
});

View File

@@ -13,15 +13,15 @@
"node": ">=18"
},
"peerDependencies": {
"@clusterio/lib": "^2.0.0-alpha.19"
"@clusterio/lib": "catalog:"
},
"devDependencies": {
"@clusterio/lib": "^2.0.0-alpha.20",
"@types/node": "^20.14.9",
"typescript": "^5.5.3"
"@clusterio/lib": "catalog:",
"@types/node": "catalog:",
"typescript": "catalog:"
},
"dependencies": {
"@sinclair/typebox": "^0.30.4"
"@sinclair/typebox": "catalog:"
},
"publishConfig": {
"access": "public"

View File

@@ -7,6 +7,6 @@
"watch": "tsc --build --watch"
},
"devDependencies": {
"typescript": "^5.5.3"
"typescript": "catalog:"
}
}

4266
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +1,16 @@
packages:
- "*"
- "*"
catalog:
"@clusterio/lib": ^2.0.0-alpha.21
"@clusterio/web_ui": ^2.0.0-alpha.21
"@sinclair/typebox": ^0.30.4
"@types/node": ^20.14.9
"@types/react": ^18.2.21
"typescript": ^5.5.3
"antd": ^5.13.0
"react": ^18.2.0
"react-dom": ^18.2.0
"webpack": ^5.98.0
"webpack-cli": ^5.1.4
"webpack-merge": ^5.9.0

View File

@@ -6,6 +6,7 @@
{ "path": "./exp_gui/" },
{ "path": "./exp_legacy/" },
{ "path": "./exp_scenario/" },
{ "path": "./exp_server_ups/" },
{ "path": "./exp_util/" },
],
}