Files
factorio-expcluster/exp_statistics/module/events.lua
T
bbassieandClaude Opus 5.5 d1b22dd4e7 Add exp_statistics plugin for player statistics
Player statistics move out of exp_legacy into their own plugin, with the
totals kept on the controller instead of the PlayerData datastore, which
was never synced anywhere.

The lua module tracks the same statistics with the same rules. Gains are
kept per player as unsent in storage and sent over IPC every 5 minutes
for every player with changes, and for one player when they leave. Sent
values move into the totals the controller gave when the player joined,
so get() is the total on every server either way.

Nothing is resent. The instance plugin sends everything left when the
server stops and drops anything unsent when it starts. A leftover can
only come from a save made before it was sent, such as the autosave
loaded after a crash, so dropping it loses a few minutes at worst but
never counts twice.

The controller adds the deltas to a record per player name in
database/exp_statistics/players.json. Instances fetch a player's totals
on join and hand them to lua over rcon. The web UI gets a sortable table
of every player at /exp_statistics, behind exp_statistics.view.

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

189 lines
6.4 KiB
Lua

--[[-- ExpStatistics - Events
Tracks the statistics enabled in config.lua, and sends them to the controller.
]]
local ExpStatistics = require("modules/exp_statistics/control")
local config = ExpStatistics.config
local increment = ExpStatistics.increment
local floor = math.floor
local afk_required = 5 * 3600 -- 5 minutes
local send_interval = 5 * 3600 -- 5 minutes
--- @type table<defines.events, fun(event: EventData)>
local events = {}
--- Add a handler, chained after any other for the same event
--- @param event_id defines.events
--- @param handler fun(event: EventData)
local function add(event_id, handler)
local existing = events[event_id]
if existing then
events[event_id] = function(event)
existing(event)
handler(event)
end
else
events[event_id] = handler
end
end
--- Get the player of an event if they are online
--- @param event { player_index: uint? }
--- @return LuaPlayer?
local function get_online_player(event)
if not event.player_index then return nil end
local player = game.players[event.player_index]
if not player or not player.valid or not player.connected then return nil end
return player
end
--- @param event EventData.on_player_left_game
add(defines.events.on_player_left_game, function(event)
local player = game.players[event.player_index]
ExpStatistics.send_and_forget(player.name)
end)
--- @type table<number, fun()>
local on_nth_tick = {
[send_interval] = ExpStatistics.send_all,
}
if config.MapsPlayed then
--- @param event EventData.on_player_created
add(defines.events.on_player_created, function(event)
increment(game.players[event.player_index], "MapsPlayed")
end)
end
if config.Playtime or config.AfkTime then
on_nth_tick[3600] = function()
if game.tick == 0 then return end
for _, player in pairs(game.connected_players) do
if config.Playtime then increment(player, "Playtime") end
if config.AfkTime and player.afk_time > afk_required then increment(player, "AfkTime") end
end
end
end
if config.DistanceTravelled then
--- @param event EventData.on_player_changed_position
add(defines.events.on_player_changed_position, function(event)
local player = get_online_player(event)
if not player or player.afk_time > afk_required then return end
increment(player, "DistanceTravelled")
end)
end
if config.MachinesRemoved or config.TreesDestroyed or config.OreMined then
--- @param event EventData.on_marked_for_deconstruction | EventData.on_player_mined_entity
local function on_removed(event)
local player = get_online_player(event)
if not player then return end
local entity = event.entity
if not entity.valid then return end
if entity.type == "resource" then
if config.OreMined then increment(player, "OreMined") end
elseif entity.type == "tree" then
if config.TreesDestroyed then increment(player, "TreesDestroyed") end
elseif entity.force == player.force then
if config.MachinesRemoved then increment(player, "MachinesRemoved") end
end
end
add(defines.events.on_marked_for_deconstruction, on_removed)
add(defines.events.on_player_mined_entity, on_removed)
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
if config.DamageDealt then
--- @param event EventData.on_entity_damaged
add(defines.events.on_entity_damaged, function(event)
local player = get_cause_player(event.cause)
if not player or not is_hostile(event.entity, player) then return end
increment(player, "DamageDealt", floor(event.final_damage_amount))
end)
end
if config.Kills then
--- @param event EventData.on_entity_died
add(defines.events.on_entity_died, function(event)
local player = get_cause_player(event.cause)
if not player or not is_hostile(event.entity, player) then return end
increment(player, "Kills")
end)
end
--- Increment a statistic for every online player on a force
--- @param force LuaForce?
--- @param statistic string
local function increment_force(force, statistic)
if not force or not force.valid then return end
for _, player in pairs(force.connected_players) do
increment(player, statistic)
end
end
if config.RocketsLaunched then
--- @param event EventData.on_rocket_launched
add(defines.events.on_rocket_launched, function(event)
local silo = event.rocket_silo
if not silo or not silo.valid then return end
increment_force(silo.force --[[@as LuaForce]], "RocketsLaunched")
end)
end
if config.ResearchCompleted then
--- @param event EventData.on_research_finished
add(defines.events.on_research_finished, function(event)
local research = event.research
if event.by_script or not research or not research.valid then return end
increment_force(research.force, "ResearchCompleted")
end)
end
for statistic, event_id in pairs(config.counters) do
add(event_id, function(event)
local player = get_online_player(event)
if player then increment(player, statistic) end
end)
end
return {
events = events,
on_nth_tick = on_nth_tick,
}