Files
factorio-scenario-ExpCluster/exp_scenario/module/control/deconstruction_log.lua
T
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

219 lines
6.6 KiB
Lua

--[[-- Control - Deconstruction Log
Log certain actions into a file when events are triggered
]]
local ExpUtil = require("modules/exp_util")
local Storage = require("modules/exp_util/storage")
local Roles = require("modules/exp_roles")
local config = require("modules.exp_legacy.config.deconlog")
local seconds_time_format = ExpUtil.format_time_factory{ format = "short", hours = true, minutes = true, seconds = true }
local format_number = require("util").format_number
local write_file = helpers.write_file
local format_string = string.format
local concat = table.concat
local filepath = "log/deconstruction.log"
--- Clear the log file
local function clear_log()
helpers.remove_path(filepath)
end
--- Add a new line to the log
--- @param player LuaPlayer
--- @param action string
--- @param ... string
local function add_log_line(player, action, ...)
local text = concat({
seconds_time_format(game.tick),
player.name,
action,
...
}, ",")
write_file(filepath, text .. "\n", true, 0)
end
--- Convert a position to a string
--- @param pos MapPosition
--- @return string
local function format_position(pos)
return format_string("%.1f,%.1f", pos.x, pos.y)
end
--- Convert an area to a string
--- @param area BoundingBox.struct
--- @return string
local function format_area(area)
return format_string("%.1f,%.1f,%.1f,%.1f", area.left_top.x, area.left_top.y, area.right_bottom.x, area.right_bottom.y)
end
--- Convert an entity to a string
--- @param entity LuaEntity
--- @return string
local function format_entity(entity)
return format_string("%s,%.1f,%.1f,%s,%s", entity.name, entity.position.x, entity.position.y, entity.direction, entity.orientation)
end
--- Concert a position into a gps tag
--- @param pos MapPosition
--- @param surface_name string
--- @return string
local function format_position_gps(pos, surface_name)
return format_string("[gps=%.1f,%.1f,%s]", pos.x, pos.y, surface_name)
end
--- Print a message to all players who match the value of admin
--- @param message LocalisedString
local function admin_print(message)
for _, player in ipairs(game.connected_players) do
if player.admin then
player.print(message)
end
end
end
--- Check if a log should be created for a player
--- @param event { player_index: number }
--- @return LuaPlayer?
local function get_log_player(event)
local player = assert(game.get_player(event.player_index))
if Roles.player_has_permission(player, "exp_scenario.bypass.deconstruction_log") then
return nil
end
return player
end
--- Log when an area is deconstructed
--- @param event EventData.on_player_deconstructed_area
local function on_player_deconstructed_area(event)
local player = get_log_player(event)
if not player then return end
--- Don't log when a player clears a deconstruction
if event.alt then
return
end
local area = event.area
local surface_name = event.surface.name
local items = event.surface.find_entities_filtered{ area = area, force = player.force }
if #items > 250 then
admin_print{
"exp_deconstruction-log.chat-admin",
player.name,
format_position_gps(area.left_top, surface_name),
format_position_gps(area.right_bottom, surface_name),
format_number(#items, false),
}
end
add_log_line(player, "deconstructed_area", surface_name, format_area(area))
end
--- Log when an entity is built
--- @param event EventData.on_built_entity
local function on_built_entity(event)
local player = get_log_player(event)
if not player then return end
add_log_line(player, "built_entity", format_entity(event.entity))
end
--- Log when an entity is mined
--- @param event EventData.on_player_mined_entity
local function on_player_mined_entity(event)
local player = get_log_player(event)
if not player then return end
add_log_line(player, "mined_entity", format_entity(event.entity))
end
--- Ammo which is logged when fired
local logged_ammo = {
["rocket"] = config.fired_rocket,
["explosive-rocket"] = config.fired_explosive_rocket,
["atomic-bomb"] = config.fired_nuke,
}
--- @class ExpScenario_DeconstructionLog.AmmoSlot
--- @field name string
--- @field count number
--- The last seen contents of each ammo slot, keyed by player index then slot index
local ammo_slots = {} --- @type table<uint, table<uint, ExpScenario_DeconstructionLog.AmmoSlot>>
Storage.register(ammo_slots, function(tbl)
ammo_slots = tbl
end)
--- Log a shot, there is no fired event so a slot losing one of the same ammo is taken as a shot
--- @param event EventData.on_player_ammo_inventory_changed
local function on_player_ammo_inventory_changed(event)
local player = get_log_player(event)
if not player or not player.character then return end
local slots = ammo_slots[player.index]
if not slots then
slots = {}
ammo_slots[player.index] = slots
end
local character_ammo = assert(player.get_inventory(defines.inventory.character_ammo))
for index = 1, #character_ammo do
local stack = character_ammo[index --[[@as uint]]]
local previous = slots[index]
local fired = nil --- @type string?
if stack.valid_for_read then
if previous and previous.name == stack.name and previous.count == stack.count + 1 then
fired = stack.name
end
slots[index] = { name = stack.name, count = stack.count }
else
if previous and previous.count == 1 then
fired = previous.name
end
slots[index] = nil
end
if fired and logged_ammo[fired] then
add_log_line(player, "shot-" .. fired, format_position(player.physical_position), format_position(player.shooting_state.position))
end
end
end
--- Forget the ammo of a player who left, their slots are read again on the next change
--- @param event EventData.on_player_left_game
local function on_player_left_game(event)
ammo_slots[event.player_index] = nil
end
local e = defines.events
local events = {
[e.on_multiplayer_init] = clear_log,
}
if config.decon_area then
events[e.on_player_deconstructed_area] = on_player_deconstructed_area
end
if config.built_entity then
events[e.on_built_entity] = on_built_entity
end
if config.mined_entity then
events[e.on_player_mined_entity] = on_player_mined_entity
end
if config.fired_rocket or config.fired_explosive_rocket or config.fired_nuke then
events[e.on_player_ammo_inventory_changed] = on_player_ammo_inventory_changed
events[e.on_player_left_game] = on_player_left_game
end
return {
events = events,
}