1
0
mirror of https://github.com/ComfyFactory/ComfyFactorio.git synced 2025-01-14 02:34:09 +02:00
ComfyFactorio/maps/pirates/highscore.lua
danielmartin0 7b17e3860e Stylua pt2
2024-10-22 21:47:11 +02:00

781 lines
24 KiB
Lua

-- This file is part of thesixthroc's Pirate Ship softmod, licensed under GPLv3 and stored at https://github.com/ComfyFactory/ComfyFactorio and https://github.com/danielmartin0/ComfyFactorio-Pirates.
-- == This code is mostly a fork of the file from Mountain Fortress
local Event = require('utils.event')
local Global = require('utils.global')
local Server = require('utils.server')
local Gui = require('utils.gui')
local Math = require('maps.pirates.math')
local Token = require('utils.token')
require('utils.core')
local _inspect = require('utils.inspect').inspect
local SpamProtection = require('utils.spam_protection')
-- local Memory = require 'maps.pirates.memory'
local Utils = require('maps.pirates.utils_local')
local CoreData = require('maps.pirates.coredata')
local Common = require('maps.pirates.common')
local module_name = Gui.uid_name()
-- local module_name = 'Highscore'
local score_dataset = 'highscores'
local score_key = 'pirate_ship_scores'
local score_key_debug = 'pirate_ship_scores_debug'
local score_key_modded = 'pirate_ship_scores_modded'
local Public = {}
local insert = table.insert
local this = {
score_table = { player = {} },
sort_by = {},
}
Global.register(this, function(t)
this = t
end)
local function sort_list(method, column_name, score_list)
local comparators = {
['ascending'] = function(a, b)
if column_name == 'completion_time' then
return ((a[column_name] < b[column_name]) and not (a[column_name] == 0 and b[column_name] ~= 0))
or (a[column_name] ~= 0 and b[column_name] == 0) --put all 0s at the end
elseif column_name == 'version' then
return Common.version_greater_than(b[column_name], a[column_name])
elseif type(a[column_name]) == 'string' then
return a[column_name] > b[column_name]
elseif a[column_name] then
return a[column_name] < b[column_name]
end
end,
--nosort
['descending'] = function(a, b)
if column_name == 'completion_time' then
return ((b[column_name] < a[column_name]) and not (a[column_name] == 0 and b[column_name] ~= 0))
or (a[column_name] ~= 0 and b[column_name] == 0) --put all 0s at the end
elseif column_name == 'version' then
return Common.version_greater_than(a[column_name], b[column_name])
elseif type(a[column_name]) == 'string' then
return a[column_name] < b[column_name]
elseif a[column_name] then
return a[column_name] > b[column_name]
end
end,
}
Utils.stable_sort(score_list, comparators[method])
-- table.sort(score_list, comparators[method])
return score_list
end
local function get_tables_of_scores_by_type(scores)
local completion_times = {}
local leagues_travelled = {}
local completion_times_mediump_latestv = {}
local leagues_travelled_mediump_latestv = {}
local completion_times_hard = {}
local leagues_travelled_hard = {}
local completion_times_nightmare = {}
local leagues_travelled_nightmare = {}
local completion_times_latestv = {}
local leagues_travelled_latestv = {}
local versions = {}
for _, score in pairs(scores) do
if score.version then
versions[#versions + 1] = score.version
end
if score.completion_time and score.completion_time > 0 then
completion_times[#completion_times + 1] = score.completion_time
end
if score.leagues_travelled and score.leagues_travelled > 0 then
leagues_travelled[#leagues_travelled + 1] = score.leagues_travelled
end
if score.difficulty and score.difficulty > 1 then
if score.completion_time and score.completion_time > 0 then
completion_times_hard[#completion_times_hard + 1] = score.completion_time
end
if score.leagues_travelled and score.leagues_travelled > 0 then
leagues_travelled_hard[#leagues_travelled_hard + 1] = score.leagues_travelled
end
end
if score.difficulty and score.difficulty > 2 then
if score.completion_time and score.completion_time > 0 then
completion_times_nightmare[#completion_times_nightmare + 1] = score.completion_time
end
if score.leagues_travelled and score.leagues_travelled > 0 then
leagues_travelled_nightmare[#leagues_travelled_nightmare + 1] = score.leagues_travelled
end
end
end
local latest_version = 0
for _, v in pairs(versions) do
if Common.version_greater_than(v, latest_version) then
latest_version = v
end
end
for _, score in pairs(scores) do
if score.version and type(score.version) == type(latest_version) and score.version == latest_version then
if score.completion_time and score.completion_time > 0 then
completion_times_latestv[#completion_times_latestv + 1] = score.completion_time
end
if score.leagues_travelled and score.leagues_travelled > 0 then
leagues_travelled_latestv[#leagues_travelled_latestv + 1] = score.leagues_travelled
end
if score.difficulty and score.difficulty >= 1 then
if score.completion_time and score.completion_time > 0 then
completion_times_mediump_latestv[#completion_times_mediump_latestv + 1] = score.completion_time
end
if score.leagues_travelled and score.leagues_travelled > 0 then
leagues_travelled_mediump_latestv[#leagues_travelled_mediump_latestv + 1] = score.leagues_travelled
end
end
end
end
table.sort(completion_times)
table.sort(leagues_travelled)
table.sort(completion_times_mediump_latestv)
table.sort(leagues_travelled_mediump_latestv)
table.sort(completion_times_hard)
table.sort(leagues_travelled_hard)
table.sort(completion_times_nightmare)
table.sort(leagues_travelled_nightmare)
table.sort(completion_times_latestv)
table.sort(leagues_travelled_latestv)
return {
latest_version = latest_version,
completion_times = completion_times,
leagues_travelled = leagues_travelled,
completion_times_mediump_latestv = completion_times_mediump_latestv,
leagues_travelled_mediump_latestv = leagues_travelled_mediump_latestv,
completion_times_hard = completion_times_hard,
leagues_travelled_hard = leagues_travelled_hard,
completion_times_nightmare = completion_times_nightmare,
leagues_travelled_nightmare = leagues_travelled_nightmare,
completion_times_latestv = completion_times_latestv,
leagues_travelled_latestv = leagues_travelled_latestv,
}
end
local function get_score_cuttofs(tables_of_scores_by_type)
local completion_times_cutoff = #tables_of_scores_by_type.completion_times > 8
and tables_of_scores_by_type.completion_times[8]
or 9999999
local completion_times_mediump_latestv_cutoff = #tables_of_scores_by_type.completion_times_mediump_latestv > 4
and tables_of_scores_by_type.completion_times_mediump_latestv[4]
or 9999999
local completion_times_hard_cutoff = #tables_of_scores_by_type.completion_times_hard > 4
and tables_of_scores_by_type.completion_times_hard[4]
or 9999999
local completion_times_nightmare_cutoff = #tables_of_scores_by_type.completion_times_hard > 2
and tables_of_scores_by_type.completion_times_hard[2]
or 9999999
local completion_times_latestv_cutoff = #tables_of_scores_by_type.completion_times_latestv > 8
and tables_of_scores_by_type.completion_times_latestv[8]
or 9999999
local leagues_travelled_cutoff = #tables_of_scores_by_type.leagues_travelled > 8
and tables_of_scores_by_type.leagues_travelled[-8]
or 0
local leagues_travelled_mediump_latestv_cutoff = #tables_of_scores_by_type.leagues_travelled_mediump_latestv > 4
and tables_of_scores_by_type.leagues_travelled_mediump_latestv[-4]
or 0
local leagues_travelled_hard_cutoff = #tables_of_scores_by_type.leagues_travelled_hard > 4
and tables_of_scores_by_type.leagues_travelled_hard[-4]
or 0
local leagues_travelled_nightmare_cutoff = #tables_of_scores_by_type.leagues_travelled_hard > 2
and tables_of_scores_by_type.leagues_travelled_hard[-2]
or 0
local leagues_travelled_latestv_cutoff = #tables_of_scores_by_type.leagues_travelled_latestv > 86
and tables_of_scores_by_type.leagues_travelled_latestv[-8]
or 0
return {
completion_times_cutoff = completion_times_cutoff,
completion_times_mediump_latestv_cutoff = completion_times_mediump_latestv_cutoff,
completion_times_hard_cutoff = completion_times_hard_cutoff,
completion_times_nightmare_cutoff = completion_times_nightmare_cutoff,
completion_times_latestv_cutoff = completion_times_latestv_cutoff,
leagues_travelled_cutoff = leagues_travelled_cutoff,
leagues_travelled_mediump_latestv_cutoff = leagues_travelled_mediump_latestv_cutoff,
leagues_travelled_hard_cutoff = leagues_travelled_hard_cutoff,
leagues_travelled_nightmare_cutoff = leagues_travelled_nightmare_cutoff,
leagues_travelled_latestv_cutoff = leagues_travelled_latestv_cutoff,
}
end
local function saved_scores_trim(scores)
-- the goal here is to trim away highscores so we don't have too many.
local tables_of_scores_by_type = get_tables_of_scores_by_type(scores)
local cutoffs = get_score_cuttofs(tables_of_scores_by_type)
-- log(_inspect{completion_times_cutoff,completion_times_mediump_latestv_cutoff,completion_times_hard_cutoff,completion_times_latestv_cutoff,leagues_travelled_cutoff,leagues_travelled_mediump_latestv_cutoff,leagues_travelled_hard_cutoff,leagues_travelled_latestv_cutoff})
local delete = {}
for secs_id, score in pairs(scores) do
local include = false
if
cutoffs.completion_times_cutoff
and score.completion_time
and score.completion_time < cutoffs.completion_times_cutoff
then
include = true
elseif
cutoffs.completion_times_mediump_latestv_cutoff
and score.completion_time
and score.completion_time < cutoffs.completion_times_mediump_latestv_cutoff
and type(score.version) == type(cutoffs.latest_version)
and score.version == cutoffs.latest_version
and score.difficulty >= 1
then
include = true
elseif
cutoffs.completion_times_hard_cutoff
and score.completion_time
and score.completion_time < cutoffs.completion_times_hard_cutoff
and score.difficulty > 1
then
include = true
elseif
cutoffs.completion_times_nightmare_cutoff
and score.completion_time
and score.completion_time < cutoffs.completion_times_nightmare_cutoff
and score.difficulty > 2
then
include = true
elseif
cutoffs.completion_times_latestv_cutoff
and score.completion_time
and score.completion_time < cutoffs.completion_times_latestv_cutoff
and type(score.version) == type(cutoffs.latest_version)
and score.version == cutoffs.latest_version
then
include = true
elseif
cutoffs.leagues_travelled_cutoff
and score.leagues_travelled
and score.leagues_travelled > cutoffs.leagues_travelled_cutoff
then
include = true
elseif
cutoffs.leagues_travelled_mediump_latestv_cutoff
and score.leagues_travelled
and score.leagues_travelled > cutoffs.leagues_travelled_mediump_latestv_cutoff
and type(score.version) == type(cutoffs.latest_version)
and score.version == cutoffs.latest_version
and score.difficulty >= 1
then
include = true
elseif
cutoffs.leagues_travelled_hard_cutoff
and score.leagues_travelled
and score.leagues_travelled > cutoffs.leagues_travelled_hard_cutoff
and score.difficulty > 1
then
include = true
elseif
cutoffs.leagues_travelled_nightmare_cutoff
and score.leagues_travelled
and score.leagues_travelled > cutoffs.leagues_travelled_nightmare_cutoff
and score.difficulty > 2
then
include = true
elseif
cutoffs.leagues_travelled_latestv_cutoff
and score.leagues_travelled
and score.leagues_travelled > cutoffs.leagues_travelled_latestv_cutoff
and type(score.version) == type(cutoffs.latest_version)
and score.version == cutoffs.latest_version
then
include = true
end
if not include then
delete[#delete + 1] = secs_id
end
end
-- log(_inspect(delete))
for _, secs_id in pairs(delete) do
scores[secs_id] = nil
end
return scores
end
local function local_highscores_write_stats(
crew_secs_id,
name,
captain_name,
completion_time,
leagues_travelled,
version,
difficulty,
max_players
)
if not this.score_table['player'] then
this.score_table['player'] = {}
end
if not this.score_table['player'].runs then
this.score_table['player'].runs = {}
end
local t = this.score_table['player']
if t then
-- if name then
-- t.name = name
-- end
-- if version then
-- t.version = version
-- end
-- if completion_time then
-- t.completion_time = completion_time
-- end
-- if leagues_travelled then
-- t.leagues_travelled = leagues_travelled
-- end
-- if difficulty then
-- t.difficulty = difficulty
-- end
-- if max_players then
-- t.max_players = max_players
-- end
if crew_secs_id then
t.runs[crew_secs_id] = {
name = name,
captain_name = captain_name,
version = version,
completion_time = completion_time,
leagues_travelled = leagues_travelled,
difficulty = difficulty,
max_players = max_players,
}
-- log(_inspect(t))
saved_scores_trim(t.runs)
end
end
this.score_table['player'] = t
-- log(_inspect(t))
end
local load_in_scores = Token.register(function(data)
local value = data.value
if not this.score_table['player'] then
this.score_table['player'] = {}
end
this.score_table['player'] = value
end)
function Public.load_in_scores()
local secs = Server.get_current_time()
-- if secs then game.print('secs2: ' .. secs) else game.print('secs: false') end
if not secs then
return
else
-- FULL CLEAN task (erases everything...):
-- server_set_data(score_dataset, score_key, {})
if is_game_modded() then
Server.try_get_data(score_dataset, score_key_modded, load_in_scores)
elseif _DEBUG then
Server.try_get_data(score_dataset, score_key_debug, load_in_scores)
else
Server.try_get_data(score_dataset, score_key, load_in_scores)
end
end
end
function Public.dump_highscores()
log(_inspect(this.score_table['player']))
end
function Public.overwrite_scores_specific()
-- the correct format is to put _everything_ from a dump into the third argument:
-- Server.set_data(score_dataset, score_key, )
-- return true
return true
end
function Public.write_score(
crew_secs_id,
name,
captain_name,
completion_time,
leagues_travelled,
version,
difficulty,
max_players
)
local secs = Server.get_current_time()
-- if secs then game.print('secs1: ' .. secs) else game.print('secs: false') end
if not secs then
return
else
local_highscores_write_stats(
crew_secs_id,
name,
captain_name,
completion_time,
leagues_travelled,
version,
difficulty,
max_players
)
if is_game_modded() then
Server.set_data(score_dataset, score_key_modded, this.score_table['player'])
elseif _DEBUG then
Server.set_data(score_dataset, score_key_debug, this.score_table['player'])
else
Server.set_data(score_dataset, score_key, this.score_table['player'])
end
end
end
local function on_init()
local secs = Server.get_current_time()
if not secs then
local_highscores_write_stats() --just to init tables presumably
return
end
end
local sorting_symbol = { ascending = '', descending = '' }
local function get_saved_scores_for_displaying()
local score_data = this.score_table['player']
local score_list = {}
if score_data and score_data.runs then
for _, score in pairs(score_data.runs or {}) do
insert(score_list, {
name = score and score.name,
captain_name = score and score.captain_name,
completion_time = score and score.completion_time or 99999,
leagues_travelled = score and score.leagues_travelled or 0,
version = score and score.version or 0,
difficulty = score and score.difficulty or 0,
max_players = score and score.max_players or 0,
})
end
else
score_list[#score_list + 1] = {
name = 'Nothing here yet',
captain_name = '',
completion_time = 0,
leagues_travelled = 0,
version = 0,
difficulty = 0,
max_players = 0,
}
end
return score_list
end
local function score_gui(data)
local player = data.player
local frame = data.frame
frame.clear()
local columnwidth = 96
-- local flow = frame.add {type = 'flow'}
-- local sFlow = flow.style
-- sFlow.horizontally_stretchable = true
-- sFlow.horizontal_align = 'center'
-- sFlow.vertical_align = 'center'
-- local stats = flow.add {type = 'label', caption = 'Highest score so far:'}
-- local s_stats = stats.style
-- s_stats.font = 'heading-1'
-- s_stats.font_color = {r = 0.98, g = 0.66, b = 0.22}
-- s_stats.horizontal_align = 'center'
-- s_stats.vertical_align = 'center'
-- -- Global stats : rockets, biters kills
-- add_global_stats(frame)
-- -- Separator
-- local line = frame.add {type = 'line'}
-- line.style.top_margin = 8
-- line.style.bottom_margin = 8
-- Score per player
local t = frame.add({ type = 'table', column_count = 7 })
-- Score headers
local headers = {
{ name = '_name', caption = { 'pirates.highscore_heading_crew' } },
{
column = 'captain_name',
name = '_captain_name',
caption = { 'pirates.highscore_heading_captain' },
tooltip = { 'pirates.highscore_heading_captain_tooltip' },
},
{
column = 'completion_time',
name = '_completion_time',
caption = { 'pirates.highscore_heading_completion' },
},
{
column = 'leagues_travelled',
name = '_leagues_travelled',
caption = { 'pirates.highscore_heading_leagues' },
},
{
column = 'version',
name = '_version',
caption = { 'pirates.highscore_heading_version' },
},
{
column = 'difficulty',
name = '_difficulty',
caption = { 'pirates.highscore_heading_difficulty' },
},
{
column = 'max_players',
name = '_max_players',
caption = { 'pirates.highscore_heading_peak_players' },
},
}
local sorting_pref = this.sort_by[player.index] or {}
for _, header in ipairs(headers) do
local cap = header.caption
-- log(header.caption)
-- Add sorting symbol if any
if header.column and sorting_pref[1] and sorting_pref[1].column == header.column then
local symbol = sorting_symbol[sorting_pref[1].method]
cap = { '', symbol, cap }
end
-- Header
local label = t.add({
type = 'label',
caption = cap,
name = header.name,
})
if header.tooltip then
label.tooltip = header.tooltip
end
label.style.font = 'default-listbox'
label.style.font_color = { r = 0.98, g = 0.66, b = 0.22 } -- yellow
label.style.minimal_width = columnwidth
label.style.horizontal_align = 'right'
end
-- Score list
local score_list = get_saved_scores_for_displaying()
-- log(_inspect(score_list))
for i = #sorting_pref, 1, -1 do
local sort = sorting_pref[i]
if sort then
-- log(_inspect(score_list))
score_list = sort_list(sort.method, sort.column, score_list)
end
end
-- New pane for scores (while keeping headers at same position)
local scroll_pane = frame.add({
type = 'scroll-pane',
name = 'score_scroll_pane',
direction = 'vertical',
horizontal_scroll_policy = 'never',
vertical_scroll_policy = 'auto',
})
scroll_pane.style.maximal_height = 400
t = scroll_pane.add({ type = 'table', column_count = 7 })
-- Score entries
for _, entry in pairs(score_list) do
local p = { color = { r = Math.random(1, 255), g = Math.random(1, 255), b = Math.random(1, 255) } }
-- local p
-- if not (entry and entry.name) then
-- p = {color = {r = random(1, 255), g = random(1, 255), b = random(1, 255)}}
-- else
-- p = game.players[entry.name]
-- if not p then
-- p = {color = {r = random(1, 255), g = random(1, 255), b = random(1, 255)}}
-- end
-- end
local special_color = {
r = p.color.r * 0.6 + 0.4,
g = p.color.g * 0.6 + 0.4,
b = p.color.b * 0.6 + 0.4,
a = 1,
}
-- displayforms:
local n = entry.completion_time > 0 and Utils.time_mediumform(entry.completion_time or 0) or 'N/A'
local l = entry.leagues_travelled > 0 and entry.leagues_travelled or '?'
local v = entry.version and entry.version or '?'
local d = entry.difficulty > 0
and CoreData.difficulty_options[CoreData.get_difficulty_option_from_value(entry.difficulty)].text
or '?'
local c = entry.max_players > 0 and entry.max_players or '?'
local line = {
{ caption = entry.name, color = special_color },
{ caption = entry.captain_name or '?' },
{ caption = tostring(n) },
{ caption = tostring(l) },
{ caption = tostring(v) },
{ caption = d },
{ caption = tostring(c) },
}
local default_color = { r = 0.9, g = 0.9, b = 0.9 }
for _, column in ipairs(line) do
local label = t.add({
type = 'label',
caption = column.caption,
color = column.color or default_color,
})
label.style.font = 'default'
label.style.minimal_width = columnwidth
label.style.maximal_width = columnwidth
label.style.horizontal_align = 'right'
end -- foreach column
end -- foreach entry
end
local score_gui_token = Token.register(score_gui)
local function on_gui_click(event)
if not event.element then
return
end
if not event.element.valid then
return
end
local player = game.get_player(event.element.player_index)
local frame = Gui.get_player_active_frame(player)
if not frame then
return
end
if frame.name ~= 'Highscore' then
return
end
local is_spamming = SpamProtection.is_spamming(player, nil, 'HighScore Gui Click')
if is_spamming then
return
end
local name = event.element.name
-- Handles click on a score header
local element_to_column = {
['_captain_name'] = 'captain_name',
['_version'] = 'version',
['_completion_time'] = 'completion_time',
['_leagues_travelled'] = 'leagues_travelled',
['_difficulty'] = 'difficulty',
['_max_players'] = 'max_players',
}
if element_to_column[name] then
--@TODO: Extend
local sorting_pref = this.sort_by[player.index]
local found_index = nil
local new_method = 'descending'
for i, sort in ipairs(sorting_pref) do
if sort.column == element_to_column[name] then
found_index = i
if sort.method == 'descending' and i == 1 then
new_method = 'ascending'
end
end
end
if found_index then
--remove this and shuffle everything before it up by 1:
for j = found_index, 2, -1 do
sorting_pref[j] = Utils.deepcopy(sorting_pref[j - 1]) --deepcopy just as I'm slightly unsure about refernces here
end
else
--prepend:
for j = #sorting_pref + 1, 2, -1 do
sorting_pref[j] = Utils.deepcopy(sorting_pref[j - 1]) --deepcopy just as I'm slightly unsure about references here
end
end
sorting_pref[1] = { column = element_to_column[name], method = new_method }
score_gui({ player = player, frame = frame })
return
end
end
local function on_player_joined_game(event)
local player = game.players[event.player_index]
if player.index and this.sort_by and not this.sort_by[player.index] then
this.sort_by[player.index] = {
{ method = 'ascending', column = 'completion_time' },
{ method = 'descending', column = 'leagues_travelled' },
{ method = 'descending', column = 'version' },
{ method = 'descending', column = 'difficulty' },
{ method = 'ascending', column = 'captain_name' },
}
end
end
local function on_player_left_game(event)
local player = game.players[event.player_index]
if this.sort_by[player.index] then
this.sort_by[player.index] = nil
end
end
Server.on_data_set_changed(score_dataset, function(data)
local key
if is_game_modded() then
key = score_key_modded
elseif _DEBUG then
key = score_key_debug
else
key = score_key
end
if data.key == key then
if data.value then
this.score_table['player'] = data.value
end
end
end)
Gui.add_tab_to_gui({
name = module_name,
caption = 'Highscore',
id = score_gui_token,
admin = false,
only_server_sided = true,
})
Gui.on_click(module_name, function(event)
local player = event.player
Gui.reload_active_tab(player)
end)
Event.on_init(on_init)
Event.add(defines.events.on_player_left_game, on_player_left_game)
Event.add(defines.events.on_player_joined_game, on_player_joined_game)
Event.add(defines.events.on_gui_click, on_gui_click)
Event.add(Server.events.on_server_started, Public.load_in_scores)
return Public