1
0
mirror of https://github.com/ComfyFactory/ComfyFactorio.git synced 2025-01-04 00:15:45 +02:00
ComfyFactorio/maps/pirates/highscore.lua
Piratux f72a577b7d New private runs
Changes:
- Players now can create private runs protected by a password. This run becomes public if the crew is empty or inactive for 24 horus (Limit is currently 1 private run at a time).
2022-10-10 20:21:14 +03:00

639 lines
25 KiB
Lua

-- This file is part of thesixthroc's Pirate Ship softmod, licensed under GPLv3 and stored at 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