1
0
mirror of https://github.com/ComfyFactory/ComfyFactorio.git synced 2025-01-10 00:43:27 +02:00
ComfyFactorio/utils/datastore/statistics.lua
2024-05-27 20:30:03 +02:00

516 lines
15 KiB
Lua

-- created by Gerkiz for ComfyFactorio
local Global = require 'utils.global'
local Token = require 'utils.token'
local Task = require 'utils.task'
local Server = require 'utils.server'
local Event = require 'utils.event'
local set_timeout_in_ticks = Task.set_timeout_in_ticks
local statistics_dataset = 'statistics'
local set_data = Server.set_data
local try_get_data = Server.try_get_data
local e = defines.events
local floor = math.floor
local events = {
map_tags_made = e.on_chart_tag_added,
chat_messages = e.on_console_chat,
commands_used = e.on_console_command,
machines_built = e.on_built_entity,
items_picked_up = e.on_picked_up_item,
tiles_built = e.on_player_built_tile,
join_count = e.on_player_joined_game,
deaths = e.on_player_died,
entities_repaired = e.on_player_repaired_entity,
items_crafted = e.on_player_crafted_item,
capsules_used = e.on_player_used_capsule,
tiles_removed = e.on_player_mined_tile,
deconstructer_planner_used = e.on_player_deconstructed_area
}
local settings = {
required_only_time_to_save_time = 10 * 3600,
afk_time = 5 * 3600,
nth_tick = 5 * 3600
}
local Public = {
events = {
on_player_removed = Event.generate_event_name('on_player_removed')
}
}
local normalized_names = {
['map_tags_made'] = {name = 'Map-tags created', tooltip = "Tags that you've created in minimap."},
['chat_messages'] = {name = 'Messages', tooltip = 'Messages sent in chat.'},
['commands_used'] = {name = 'Commands', tooltip = 'Commands used in console.'},
['machines_built'] = {name = 'Entities built', tooltip = 'Entities built by the player.'},
['items_picked_up'] = {name = 'Items picked-up', tooltip = 'Items picked-up by the player.'},
['tiles_built'] = {name = 'Tiles placed', tooltip = 'Tiles placed by the player.'},
['join_count'] = {name = 'Join count', tooltip = 'How many times the player has joined the game.'},
['deaths'] = {name = 'Deaths', tooltip = 'How many times the player has died.'},
['entities_repaired'] = {name = 'Entities repaired', tooltip = 'How many entities the player has repaired.'},
['items_crafted'] = {name = 'Items crafted', tooltip = 'How many items the player has crafted.'},
['capsules_used'] = {name = 'Capsules used', tooltip = 'How many capsules the player has used.'},
['tiles_removed'] = {name = 'Tiles removed', tooltip = 'How many tiles the player has removed.'},
['deconstructer_planner_used'] = {name = 'Decon planner used', tooltip = 'How many times the player has used the deconstruction planner.'},
['maps_played'] = {name = 'Maps played', tooltip = 'How many maps the player has played.'},
['afk_time'] = {name = 'Total AFK', tooltip = 'How long the player has been AFK.'},
['distance_moved'] = {name = 'Distance travelled', tooltip = 'How far the player has travelled.\nIncluding standing still in looped belts.'},
['damage_dealt'] = {name = 'Damage dealt', tooltip = 'How much damage the player has dealt.'},
['enemies_killed'] = {name = 'Enemies killed', tooltip = 'How many enemies the player has killed.'},
['friendly_killed'] = {name = 'Friendlies killed', tooltip = 'How many friendlies the player has killed.\n This includes entities such as buildings etc.'},
['rockets_launched'] = {name = 'Rockets launched', tooltip = 'How many rockets the player has launched.'},
['research_complete'] = {name = 'Research completed', tooltip = 'How many researches the player has completed.'},
['force_mined_machines'] = {name = 'Mined friendly entities', tooltip = 'How many friendly entities the player has mined.'},
['trees'] = {name = 'Trees chopped', tooltip = 'How many trees the player has chopped.'},
['rocks'] = {name = 'Rocks mined', tooltip = 'How many rocks the player has mined.'},
['resources'] = {name = 'Ores mined', tooltip = 'How many ores the player has mined.'},
['kicked'] = {name = 'Kicked', tooltip = 'How many times the player has been kicked.'}
}
local statistics = {}
-- Register the statistics table in the global table
Global.register(
{
statistics = statistics
},
function(tbl)
statistics = tbl.statistics
for _, stat in pairs(statistics) do
setmetatable(stat, Public.metatable)
end
end
)
-- Metatable for the statistics table
Public.metatable = {__index = Public}
-- Add a normalization entry to the normalized_names table
function Public.add_normalize(name, normalize)
if _LIFECYCLE == _STAGE.runtime then
error('cannot call during runtime', 2)
end
local mt = setmetatable({name = normalize}, Public.metatable)
normalized_names[name] = mt
return mt
end
-- Set the tooltip for a statistic
function Public:set_tooltip(tooltip)
if _LIFECYCLE == _STAGE.runtime then
error('cannot call during runtime', 2)
end
self.tooltip = tooltip
end
--- Returns the player table or false
---@param player LuaPlayer|number
---@return any
local function get_data(player)
local player_index = player and type(player) == 'number' and player or player and player.valid and player.index or false
if not player_index then
log('Invalid player index at get_data')
return false
end
local data = statistics[player_index]
if not data then
local p = game.get_player(player_index)
local name = p and p.valid and p.name or nil
local player_data = {
name = name,
tick = 0
}
for event, _ in pairs(events) do
player_data[event] = 0
end
local mt = setmetatable(player_data, Public.metatable)
statistics[player_index] = mt
end
return statistics[player_index]
end
local try_download_data_token =
Token.register(
function(data)
local player_name = data.key
local player = game.get_player(player_name)
if not player or not player.valid then
return
end
local stats = data.value
if stats then
local s = setmetatable(stats, Public.metatable)
statistics[player.index] = s
else
get_data(player)
end
end
)
local try_upload_data_token =
Token.register(
function(data)
local player_name = data.key
if not player_name then
return
end
local stats = data.value
local player = game.get_player(player_name)
if not player or not player.valid then
return
end
if stats then
-- we don't want to clutter the database with players less than 10 minutes played.
if player.online_time <= settings.required_only_time_to_save_time then
return
end
set_data(statistics_dataset, player_name, get_data(player))
else
local d = get_data(player)
if player.online_time >= settings.required_only_time_to_save_time then
set_data(statistics_dataset, player_name, d)
end
end
end
)
-- Increase a statistic by a delta value
function Public:increase(name, delta)
if not self[name] then
self[name] = 0
end
self[name] = self[name] + (delta or 1)
self.tick = self.tick + 1
return self
end
-- Save the player's statistics
function Public:save()
local player = game.get_player(self.name)
if not player or not player.valid then
return
end
if player.online_time <= settings.required_only_time_to_save_time then
return
end
if self.tick < 10 then
return
end
set_data(statistics_dataset, player.name, self)
return self
end
-- Clear the player's statistics
function Public:clear(force_clear)
if force_clear then
statistics[self.name] = nil
else
local player = game.get_player(self.name)
if not player or not player.valid then
statistics[self.name] = nil
return
end
local connected = player.connected
if not connected then
statistics[self.name] = nil
end
end
end
-- Try to get the player's data from the dataset
function Public:try_get_data()
try_get_data(statistics_dataset, self.name, try_download_data_token)
return self
end
-- Try to upload the player's data to the dataset
function Public:try_upload_data()
try_get_data(statistics_dataset, self.name, try_upload_data_token)
return self
end
local nth_tick_token =
Token.register(
function(event)
local player_index = event.player_index
local player = game.get_player(player_index)
if not player or not player.valid then
return
end
get_data(player):save()
end
)
--- Uploads each connected players play time to the dataset
local function upload_data()
local players = game.connected_players
local count = 0
for i = 1, #players do
count = count + 10
local player = players[i]
set_timeout_in_ticks(count, nth_tick_token, {player_index = player.index})
end
end
--- Checks if a player exists within the table
---@param player_index string
---@return boolean
function Public.exists(player_index)
return statistics[player_index] ~= nil
end
--- Returns the table of statistics
---@param player LuaPlayer
---@return table|boolean
function Public.get_player(player)
return statistics and player and player.valid and statistics[player.index] or false
end
-- Event handlers
Event.add(
e.on_player_joined_game,
function(event)
get_data(event.player_index):try_get_data()
end
)
Event.add(
e.on_player_left_game,
function(event)
get_data(event.player_index):try_upload_data()
end
)
Event.add(
Public.events.on_player_removed,
function(event)
local player_index = event.player_index
statistics[player_index] = nil
end
)
Event.add(
e.on_player_removed,
function(event)
local player_index = event.player_index
statistics[player_index] = nil
end
)
Event.on_nth_tick(settings.nth_tick, upload_data)
Server.on_data_set_changed(
statistics_dataset,
function(data)
local player = game.get_player(data.key)
if player and player.valid then
local stats = data.value
if stats then
local s = setmetatable(stats, Public.metatable)
statistics[data.key] = s
end
end
end
)
local function on_marked_for_deconstruction_on_player_mined_entity(event)
if not event.player_index then
return
end
local player = game.get_player(event.player_index)
if not player.valid or not player.connected then
return
end
local entity = event.entity
if not entity.valid then
return
end
local data = get_data(event.player_index)
if entity.type == 'resource' then
data:increase('resources')
elseif entity.type == 'tree' then
data:increase('trees')
elseif entity.type == 'simple-entity' then
data:increase('rocks')
elseif entity.force == player.force then
data:increase('force_mined_machines')
end
end
for stat_name, event_name in pairs(events) do
Event.add(
event_name,
function(event)
if not event.player_index then
return
end
local player = game.get_player(event.player_index)
if not player.valid or not player.connected then
return
end
local data = get_data(event.player_index)
data:increase(stat_name)
end
)
end
Event.add(
e.on_research_finished,
function(event)
local research = event.research
if event.by_script or not research or not research.valid then
return
end
local force = research.force
if not force or not force.valid then
return
end
for _, player in pairs(force.connected_players) do
local data = get_data(player)
data:increase('research_complete')
end
end
)
Event.add(
e.on_rocket_launched,
function(event)
local silo = event.rocket_silo
if not silo or not silo.valid then
return
end
local force = silo.force
if not force or not force.valid then
return
end
for _, player in pairs(force.connected_players) do
local data = get_data(player)
data:increase('rockets_launched')
end
end
)
Event.add(
e.on_entity_died,
function(event)
local character = event.cause
if not character or not character.valid or character.type ~= 'character' then
return
end
local player = character.player
if not player or not player.valid or not player.connected then
return
end
local entity = event.entity
if not entity.valid or entity.force.name == 'neutral' then
return
end
local data = get_data(player)
if entity.force == player.force then
data:increase('friendly_killed')
return
end
data:increase('enemies_killed')
end
)
Event.add(
e.on_entity_damaged,
function(event)
local character = event.cause
if not character or not character.valid or character.type ~= 'character' then
return
end
local player = character.player
if not player.valid or not player.connected then
return
end
local entity = event.entity
if not entity.valid or entity.force == player.force or entity.force.name == 'neutral' then
return
end
local final_damage = event.final_damage_amount
local data = get_data(player)
data:increase('damage_dealt', floor(final_damage))
end
)
Event.add(
e.on_player_changed_position,
function(event)
local player = game.get_player(event.player_index)
if not player.valid or not player.connected or player.afk_time > settings.required_only_time_to_save_time then
return
end
local data = get_data(event.player_index)
data:increase('distance_moved')
end
)
Event.on_nth_tick(
3600,
function()
if game.tick == 0 then
return
end
for _, player in pairs(game.connected_players) do
local data = get_data(player)
if player.afk_time > settings.afk_time then
data:increase('afk_time')
end
end
end
)
Event.add(
e.on_player_created,
function(event)
get_data(event.player_index):increase('maps_played')
end
)
Event.add(
e.on_player_kicked,
function(event)
get_data(event.player_index):increase('kicked')
end
)
Event.add(e.on_marked_for_deconstruction, on_marked_for_deconstruction_on_player_mined_entity)
Event.add(e.on_player_mined_entity, on_marked_for_deconstruction_on_player_mined_entity)
Public.upload_data = upload_data
Public.get_data = get_data
Public.normalized_names = normalized_names
return Public