Persistent Map Vote Tallies (#86788)

## About The Pull Request

Changes map votes to be based on a persistent tally count.
Tallies for maps are cached between rounds and are added to by map
votes.
When a map is chosen, and it wasn't the only valid one, the tallies for
said chosen map will be reset.

Refactors map vote handling and moves it from SSmapping to SSmap_vote.
Rock the Vote has been removed as a result of this refactor.

## Why It's Good For The Game

Makes it more likely that all maps will be played over the course of a
server instead of always being truly random.
Removes some clutter off of SSmapping.

🆑
refactor: Map Votes are now carried over between rounds. When a map vote
is actually a contest, the winning map will have its votes reset.
/🆑
This commit is contained in:
Zephyr
2024-09-29 20:09:08 -04:00
committed by GitHub
parent ee5911b6d1
commit 0ef5c3d049
33 changed files with 291 additions and 303 deletions
+1 -1
View File
@@ -221,7 +221,7 @@ Always compile, always use that verb, and always make sure that it works for wha
#define CLUSTER_CHECK_ALL 30 //!Don't let anything cluster, like, at all
/// Checks the job changes in the map config for the passed change key.
#define CHECK_MAP_JOB_CHANGE(job, change) SSmapping.config.job_changes?[job]?[change]
#define CHECK_MAP_JOB_CHANGE(job, change) SSmapping.current_map.job_changes?[job]?[change]
///Identifiers for away mission spawnpoints
#define AWAYSTART_BEACH "AWAYSTART_BEACH"
+1 -1
View File
@@ -1,7 +1,7 @@
///name of the file that has all the memory strings
#define MEMORY_FILE "memories.json"
///name of the file that has all the saved engravings
#define ENGRAVING_SAVE_FILE "data/engravings/[SSmapping.config.map_name]_engravings.json"
#define ENGRAVING_SAVE_FILE "data/engravings/[SSmapping.current_map.map_name]_engravings.json"
///name of the file that has all the prisoner tattoos
#define PRISONER_TATTOO_SAVE_FILE "data/engravings/prisoner_tattoos.json"
///Current version of the engraving persistence json
@@ -738,3 +738,27 @@
/datum/config_entry/number/upload_limit_admin
default = 5242880
min_val = 0
/// The minimum number of tallies a map vote entry can have.
/datum/config_entry/number/map_vote_minimum_tallies
default = 1
min_val = 0
max_val = 50
/// The flat amount all maps get by default
/datum/config_entry/number/map_vote_flat_bonus
default = 5
min_val = 0
max_val = INFINITY
/// The maximum number of tallies a map vote entry can have.
/datum/config_entry/number/map_vote_maximum_tallies
default = 200
min_val = 0
max_val = INFINITY
/// The number of tallies that are carried over between rounds.
/datum/config_entry/number/map_vote_tally_carryover_percentage
default = 100
min_val = 0
max_val = 100
+1 -1
View File
@@ -357,7 +357,7 @@ Versioning
"z_coord" = L.z,
"last_words" = L.last_words,
"suicide" = did_they_suicide,
"map" = SSmapping.config.map_name,
"map" = SSmapping.current_map.map_name,
"internet_address" = world.internet_address || "0",
"port" = "[world.port]",
"round_id" = GLOB.round_id,
+3 -3
View File
@@ -26,14 +26,14 @@ SUBSYSTEM_DEF(library)
/datum/controller/subsystem/library/proc/load_shelves()
var/list/datum/callback/load_callbacks = list()
for(var/obj/structure/bookcase/case_to_load as anything in shelves_to_load)
if(!case_to_load)
stack_trace("A null bookcase somehow ended up in SSlibrary's shelves_to_load list. Did something harddel?")
continue
load_callbacks += CALLBACK(case_to_load, TYPE_PROC_REF(/obj/structure/bookcase, load_shelf))
shelves_to_load = null
//Load all of the shelves asyncronously at the same time, blocking until the last one is finished.
callback_select(load_callbacks, savereturns = FALSE)
@@ -59,6 +59,6 @@ SUBSYSTEM_DEF(library)
/datum/controller/subsystem/library/proc/prepare_library_areas()
library_areas = typesof(/area/station/service/library) - /area/station/service/library/abandoned
var/list/additional_areas = SSmapping.config.library_areas
var/list/additional_areas = SSmapping.current_map.library_areas
if(additional_areas)
library_areas += additional_areas
+160
View File
@@ -0,0 +1,160 @@
#define MAP_VOTE_CACHE_LOCATION "data/map_vote_cache.json"
SUBSYSTEM_DEF(map_vote)
name = "Map Vote"
flags = SS_NO_FIRE
/// Has an admin specifically set a map.
var/admin_override = FALSE
/// Have we already done a vote.
var/already_voted = FALSE
/// The map that has been chosen for next round.
var/datum/map_config/next_map_config
/// Stores the current map vote cache, so that players can look at the current tally.
var/list/map_vote_cache
/// Stores the previous map vote cache, used when a map vote is reverted.
var/list/previous_cache
/// Stores a formatted html string of the tally counts
var/tally_printout = span_red("Loading...")
/datum/controller/subsystem/map_vote/Initialize()
if(rustg_file_exists(MAP_VOTE_CACHE_LOCATION))
map_vote_cache = json_decode(file2text(MAP_VOTE_CACHE_LOCATION))
var/carryover = CONFIG_GET(number/map_vote_tally_carryover_percentage)
for(var/map_id in map_vote_cache)
map_vote_cache[map_id] = round(map_vote_cache[map_id] * (carryover / 100))
sanitize_cache()
else
map_vote_cache = list()
update_tally_printout()
return SS_INIT_SUCCESS
/datum/controller/subsystem/map_vote/proc/write_cache()
rustg_file_write(json_encode(map_vote_cache), MAP_VOTE_CACHE_LOCATION)
/datum/controller/subsystem/map_vote/proc/sanitize_cache()
var/max = CONFIG_GET(number/map_vote_maximum_tallies)
for(var/map_id in map_vote_cache)
if(!(map_id in config.maplist))
map_vote_cache -= map_id
var/count = map_vote_cache[map_id]
if(count > max)
map_vote_cache[map_id] = max
/datum/controller/subsystem/map_vote/proc/send_map_vote_notice(...)
var/static/last_message_at
if(last_message_at == world.time)
message_admins("Call to send_map_vote_notice twice in one game tick. Yell at someone to condense messages.")
last_message_at = world.time
var/list/messages = args.Copy()
to_chat(world, span_purple(examine_block("Map Vote\n<hr>\n[messages.Join("\n")]")))
/datum/controller/subsystem/map_vote/proc/finalize_map_vote(datum/vote/map_vote/map_vote)
if(already_voted)
message_admins("Attempted to finalize a map vote after a map vote has already been finalized.")
return
already_voted = TRUE
var/flat = CONFIG_GET(number/map_vote_flat_bonus)
previous_cache = map_vote_cache.Copy()
for(var/map_id in map_vote.choices)
var/datum/map_config/map = config.maplist[map_id]
map_vote_cache[map_id] += (map_vote.choices[map_id] * map.voteweight) + flat
sanitize_cache()
write_cache()
update_tally_printout()
if(admin_override)
send_map_vote_notice("Admin Override is in effect. Map will not be changed.", "Tallies are recorded and saved.")
return
var/list/valid_maps = filter_cache_to_valid_maps()
if(!length(valid_maps))
send_map_vote_notice("No valid maps.")
return
var/winner = pick_weight(filter_cache_to_valid_maps())
set_next_map(config.maplist[winner])
send_map_vote_notice("Map Selected - [span_bold(next_map_config.map_name)]")
// do not reset tallies if only one map is even possible
if(length(valid_maps) > 1)
map_vote_cache[winner] = CONFIG_GET(number/map_vote_minimum_tallies)
write_cache()
update_tally_printout()
/// Returns a list of all map options that are invalid for the current population.
/datum/controller/subsystem/map_vote/proc/get_valid_map_vote_choices()
var/list/valid_maps = list()
// Fill in our default choices with all of the maps in our map config, if they are votable and not blocked.
var/list/maps = shuffle(global.config.maplist)
for(var/map in maps)
var/datum/map_config/possible_config = config.maplist[map]
if(!possible_config.votable || (possible_config.map_name in SSpersistence.blocked_maps))
continue
valid_maps += possible_config.map_name
var/filter_threshold = 0
if(SSticker.HasRoundStarted())
filter_threshold = get_active_player_count(alive_check = FALSE, afk_check = TRUE, human_check = FALSE)
else
filter_threshold = length(GLOB.clients)
for(var/map in valid_maps)
var/datum/map_config/possible_config = config.maplist[map]
if(possible_config.config_min_users > 0 && filter_threshold < possible_config.config_min_users)
valid_maps -= map
else if(possible_config.config_max_users > 0 && filter_threshold > possible_config.config_max_users)
valid_maps -= map
return valid_maps
/datum/controller/subsystem/map_vote/proc/filter_cache_to_valid_maps()
var/connected_players = length(GLOB.player_list)
var/list/valid_maps = list()
for(var/map_id in map_vote_cache)
var/datum/map_config/map = config.maplist[map_id]
if(!map.votable)
continue
if(map.config_min_users > 0 && (connected_players < map.config_min_users))
continue
if(map.config_max_users > 0 && (connected_players > map.config_max_users))
continue
valid_maps[map_id] = map_vote_cache[map_id]
return valid_maps
/datum/controller/subsystem/map_vote/proc/set_next_map(datum/map_config/change_to)
if(!change_to.MakeNextMap())
message_admins("Failed to set new map with next_map.json for [change_to.map_name]!")
return FALSE
next_map_config = change_to
return TRUE
/datum/controller/subsystem/map_vote/proc/revert_next_map()
if(!next_map_config)
return
if(previous_cache)
map_vote_cache = previous_cache
previous_cache = null
already_voted = FALSE
admin_override = FALSE
send_map_vote_notice("Next map reverted. Voting re-enabled.")
#undef MAP_VOTE_CACHE_LOCATION
/datum/controller/subsystem/map_vote/proc/update_tally_printout()
var/list/data = list()
for(var/map_id in map_vote_cache)
var/datum/map_config/map = config.maplist[map_id]
data += "[map.map_name] - [map_vote_cache[map_id]]"
tally_printout = examine_block("Current Tallies\n<hr>\n[data.Join("\n")]")
+26 -119
View File
@@ -6,15 +6,8 @@ SUBSYSTEM_DEF(mapping)
var/list/nuke_tiles = list()
var/list/nuke_threats = list()
var/datum/map_config/config
var/datum/map_config/next_map_config
/// Has the map for the next round been voted for already?
var/map_voted = FALSE
/// Has the map for the next round been deliberately chosen by an admin?
var/map_force_chosen = FALSE
/// Has the map vote been rocked?
var/map_vote_rocked = FALSE
/// The current map config the server loaded at round start.
var/datum/map_config/current_map
var/list/map_templates = list()
@@ -95,20 +88,20 @@ SUBSYSTEM_DEF(mapping)
/datum/controller/subsystem/mapping/PreInit()
..()
#ifdef FORCE_MAP
config = load_map_config(FORCE_MAP, FORCE_MAP_DIRECTORY)
current_map = load_map_config(FORCE_MAP, FORCE_MAP_DIRECTORY)
#else
config = load_map_config(error_if_missing = FALSE)
current_map = load_map_config(error_if_missing = FALSE)
#endif
/datum/controller/subsystem/mapping/Initialize()
if(initialized)
return SS_INIT_SUCCESS
if(config.defaulted)
var/old_config = config
config = global.config.defaultmap
if(!config || config.defaulted)
to_chat(world, span_boldannounce("Unable to load next or default map config, defaulting to MetaStation."))
config = old_config
if(current_map.defaulted)
var/datum/map_config/old_config = current_map
current_map = config.defaultmap
if(!current_map || current_map.defaulted)
to_chat(world, span_boldannounce("Unable to load next or default map config, defaulting to [old_config.map_name]."))
current_map = old_config
plane_offset_to_true = list()
true_to_offset_planes = list()
plane_to_offset = list()
@@ -132,11 +125,11 @@ SUBSYSTEM_DEF(mapping)
#ifndef LOWMEMORYMODE
// Create space ruin levels
while (space_levels_so_far < config.space_ruin_levels)
while (space_levels_so_far < current_map.space_ruin_levels)
add_new_zlevel("Ruin Area [space_levels_so_far+1]", ZTRAITS_SPACE)
++space_levels_so_far
// Create empty space levels
while (space_levels_so_far < config.space_empty_levels + config.space_ruin_levels)
while (space_levels_so_far < current_map.space_empty_levels + current_map.space_ruin_levels)
empty_space = add_new_zlevel("Empty Area [space_levels_so_far+1]", list(ZTRAIT_LINKAGE = CROSSLINKED))
++space_levels_so_far
@@ -144,7 +137,7 @@ SUBSYSTEM_DEF(mapping)
if(CONFIG_GET(flag/roundstart_away))
createRandomZlevel(prob(CONFIG_GET(number/config_gateway_chance)))
else if (SSmapping.config.load_all_away_missions) // we're likely in a local testing environment, so punch it.
else if (SSmapping.current_map.load_all_away_missions) // we're likely in a local testing environment, so punch it.
load_all_away_missions()
loading_ruins = TRUE
@@ -363,9 +356,7 @@ Used by the AI doomsday and the self-destruct nuke.
holodeck_templates = SSmapping.holodeck_templates
areas_in_z = SSmapping.areas_in_z
config = SSmapping.config
next_map_config = SSmapping.next_map_config
current_map = SSmapping.current_map
clearing_reserved_turfs = SSmapping.clearing_reserved_turfs
z_list = SSmapping.z_list
@@ -431,22 +422,22 @@ Used by the AI doomsday and the self-destruct nuke.
// load the station
station_start = world.maxz + 1
INIT_ANNOUNCE("Loading [config.map_name]...")
LoadGroup(FailedZs, "Station", config.map_path, config.map_file, config.traits, ZTRAITS_STATION)
INIT_ANNOUNCE("Loading [current_map.map_name]...")
LoadGroup(FailedZs, "Station", current_map.map_path, current_map.map_file, current_map.traits, ZTRAITS_STATION)
if(SSdbcore.Connect())
var/datum/db_query/query_round_map_name = SSdbcore.NewQuery({"
UPDATE [format_table_name("round")] SET map_name = :map_name WHERE id = :round_id
"}, list("map_name" = config.map_name, "round_id" = GLOB.round_id))
"}, list("map_name" = current_map.map_name, "round_id" = GLOB.round_id))
query_round_map_name.Execute()
qdel(query_round_map_name)
#ifndef LOWMEMORYMODE
if(config.minetype == "lavaland")
if(current_map.minetype == "lavaland")
LoadGroup(FailedZs, "Lavaland", "map_files/Mining", "Lavaland.dmm", default_traits = ZTRAITS_LAVALAND)
else if (!isnull(config.minetype) && config.minetype != "none")
INIT_ANNOUNCE("WARNING: An unknown minetype '[config.minetype]' was set! This is being ignored! Update the maploader code!")
else if (!isnull(current_map.minetype) && current_map.minetype != "none")
INIT_ANNOUNCE("WARNING: An unknown minetype '[current_map.minetype]' was set! This is being ignored! Update the maploader code!")
#endif
if(LAZYLEN(FailedZs)) //but seriously, unless the server's filesystem is messed up this will never happen
@@ -459,10 +450,8 @@ Used by the AI doomsday and the self-destruct nuke.
#undef INIT_ANNOUNCE
// Custom maps are removed after station loading so the map files does not persist for no reason.
if(config.map_path == CUSTOM_MAP_PATH)
fdel("_maps/custom/[config.map_file]")
// And as the file is now removed set the next map to default.
next_map_config = load_default_map_config()
if(current_map.map_path == CUSTOM_MAP_PATH)
fdel("_maps/custom/[current_map.map_file]")
/**
* Global list of AREA TYPES that are associated with the station.
@@ -494,88 +483,6 @@ GLOBAL_LIST_EMPTY(the_station_areas)
for(var/area/A as anything in GLOB.areas)
A.RunTerrainPopulation()
/datum/controller/subsystem/mapping/proc/maprotate()
if(map_voted || SSmapping.next_map_config) //If voted or set by other means.
return
var/players = GLOB.clients.len
var/list/mapvotes = list()
//count votes
var/pmv = CONFIG_GET(flag/preference_map_voting)
if(pmv)
for (var/client/c in GLOB.clients)
var/vote = c.prefs.read_preference(/datum/preference/choiced/preferred_map)
if (!vote)
if (global.config.defaultmap)
mapvotes[global.config.defaultmap.map_name] += 1
continue
mapvotes[vote] += 1
else
for(var/M in global.config.maplist)
mapvotes[M] = 1
//filter votes
for (var/map in mapvotes)
if (!map)
mapvotes.Remove(map)
continue
if (!(map in global.config.maplist))
mapvotes.Remove(map)
continue
if(map in SSpersistence.blocked_maps)
mapvotes.Remove(map)
continue
var/datum/map_config/VM = global.config.maplist[map]
if (!VM)
mapvotes.Remove(map)
continue
if (VM.voteweight <= 0)
mapvotes.Remove(map)
continue
if (VM.config_min_users > 0 && players < VM.config_min_users)
mapvotes.Remove(map)
continue
if (VM.config_max_users > 0 && players > VM.config_max_users)
mapvotes.Remove(map)
continue
if(pmv)
mapvotes[map] = mapvotes[map]*VM.voteweight
var/pickedmap = pick_weight(mapvotes)
if (!pickedmap)
return
var/datum/map_config/VM = global.config.maplist[pickedmap]
message_admins("Randomly rotating map to [VM.map_name]")
. = changemap(VM)
if (. && VM.map_name != config.map_name)
to_chat(world, span_boldannounce("Map rotation has chosen [VM.map_name] for next round!"))
/datum/controller/subsystem/mapping/proc/mapvote()
if(map_voted || SSmapping.next_map_config) //If voted or set by other means.
return
if(SSvote.current_vote) //Theres already a vote running, default to rotation.
maprotate()
return
SSvote.initiate_vote(/datum/vote/map_vote, "automatic map rotation", forced = TRUE)
/datum/controller/subsystem/mapping/proc/changemap(datum/map_config/change_to)
if(!change_to.MakeNextMap())
next_map_config = load_default_map_config()
message_admins("Failed to set new map with next_map.json for [change_to.map_name]! Using default as backup!")
return
var/filter_threshold = get_active_player_count(alive_check = FALSE, afk_check = TRUE, human_check = FALSE)
if (change_to.config_min_users > 0 && filter_threshold != 0 && filter_threshold < change_to.config_min_users)
message_admins("[change_to.map_name] was chosen for the next map, despite there being less current players than its set minimum population range!")
log_game("[change_to.map_name] was chosen for the next map, despite there being less current players than its set minimum population range!")
if (change_to.config_max_users > 0 && filter_threshold > change_to.config_max_users)
message_admins("[change_to.map_name] was chosen for the next map, despite there being more current players than its set maximum population range!")
log_game("[change_to.map_name] was chosen for the next map, despite there being more current players than its set maximum population range!")
next_map_config = change_to
return TRUE
/datum/controller/subsystem/mapping/proc/preloadTemplates(path = "_maps/templates/") //see master controller setup
var/list/filelist = flist(path)
for(var/map in filelist)
@@ -590,10 +497,10 @@ GLOBAL_LIST_EMPTY(the_station_areas)
/datum/controller/subsystem/mapping/proc/preloadRuinTemplates()
// Still supporting bans by filename
var/list/banned = generateMapList("spaceruinblacklist.txt")
if(config.minetype == "lavaland")
if(current_map.minetype == "lavaland")
banned += generateMapList("lavaruinblacklist.txt")
else if(config.blacklist_file)
banned += generateMapList(config.blacklist_file)
else if(current_map.blacklist_file)
banned += generateMapList(current_map.blacklist_file)
for(var/item in sort_list(subtypesof(/datum/map_template/ruin), GLOBAL_PROC_REF(cmp_ruincost_priority)))
var/datum/map_template/ruin/ruin_type = item
@@ -953,7 +860,7 @@ ADMIN_VERB(load_away_mission, R_FUN, "Load Away Mission", "Load a specific away
/// Returns true if the map we're playing on is on a planet
/datum/controller/subsystem/mapping/proc/is_planetary()
return config.planetary
return current_map.planetary
/// For debug purposes, will add every single away mission present in a given directory.
/// You can optionally pass in a string directory to load from instead of the default.
@@ -111,7 +111,7 @@ SUBSYSTEM_DEF(persistence)
for(var/map in config.maplist)
var/datum/map_config/VM = config.maplist[map]
var/run = 0
if(VM.map_name == SSmapping.config.map_name)
if(VM.map_name == SSmapping.current_map.map_name)
run++
for(var/name in SSpersistence.saved_maps)
if(VM.map_name == name)
@@ -128,7 +128,7 @@ SUBSYSTEM_DEF(persistence)
saved_maps += mapstosave
for(var/i = mapstosave; i > 1; i--)
saved_maps[i] = saved_maps[i-1]
saved_maps[1] = SSmapping.config.map_name
saved_maps[1] = SSmapping.current_map.map_name
var/json_file = file(FILE_RECENT_MAPS)
var/list/file_data = list()
file_data["data"] = saved_maps
@@ -14,7 +14,7 @@
saved_engravings = json["entries"]
if(!saved_engravings.len)
log_world("Failed to load engraved messages on map [SSmapping.config.map_name]")
log_world("Failed to load engraved messages on map [SSmapping.current_map.map_name]")
return
var/list/viable_turfs = get_area_turfs(/area/station/maintenance, subtypes = TRUE) + get_area_turfs(/area/station/security/prison, subtypes = TRUE)
@@ -42,7 +42,7 @@
successfully_loaded_engravings++
turfs_to_pick_from -= engraved_wall
log_world("Loaded [successfully_loaded_engravings] engraved messages on map [SSmapping.config.map_name]")
log_world("Loaded [successfully_loaded_engravings] engraved messages on map [SSmapping.current_map.map_name]")
///Saves all new engravings in the world.
/datum/controller/subsystem/persistence/proc/save_wall_engravings()
+2 -2
View File
@@ -22,9 +22,9 @@ SUBSYSTEM_DEF(statpanels)
/datum/controller/subsystem/statpanels/fire(resumed = FALSE)
if (!resumed)
num_fires++
var/datum/map_config/cached = SSmapping.next_map_config
var/datum/map_config/cached = SSmap_vote.next_map_config
global_data = list(
"Map: [SSmapping.config?.map_name || "Loading..."]",
"Map: [SSmapping.current_map?.map_name || "Loading..."]",
cached ? "Next Map: [cached.map_name]" : null,
"Round ID: [GLOB.round_id ? GLOB.round_id : "NULL"]",
"Server Time: [time2text(world.timeofday, "YYYY-MM-DD hh:mm:ss")]",
+4 -12
View File
@@ -93,12 +93,12 @@ SUBSYSTEM_DEF(ticker)
switch(L.len)
if(3) //rare+MAP+sound.ogg or MAP+rare.sound.ogg -- Rare Map-specific sounds
if(use_rare_music)
if(L[1] == "rare" && L[2] == SSmapping.config.map_name)
if(L[1] == "rare" && L[2] == SSmapping.current_map.map_name)
music += S
else if(L[2] == "rare" && L[1] == SSmapping.config.map_name)
else if(L[2] == "rare" && L[1] == SSmapping.current_map.map_name)
music += S
if(2) //rare+sound.ogg or MAP+sound.ogg -- Rare sounds or Map-specific sounds
if((use_rare_music && L[1] == "rare") || (L[1] == SSmapping.config.map_name))
if((use_rare_music && L[1] == "rare") || (L[1] == SSmapping.current_map.map_name))
music += S
if(1) //sound.ogg -- common sound
if(L[1] == "exclude")
@@ -157,7 +157,7 @@ SUBSYSTEM_DEF(ticker)
for(var/client/C in GLOB.clients)
window_flash(C, ignorepref = TRUE) //let them know lobby has opened up.
to_chat(world, span_notice("<b>Welcome to [station_name()]!</b>"))
send2chat(new /datum/tgs_message_content("New round starting on [SSmapping.config.map_name]!"), CONFIG_GET(string/channel_announce_new_game))
send2chat(new /datum/tgs_message_content("New round starting on [SSmapping.current_map.map_name]!"), CONFIG_GET(string/channel_announce_new_game))
current_state = GAME_STATE_PREGAME
SEND_SIGNAL(src, COMSIG_TICKER_ENTER_PREGAME)
@@ -211,7 +211,6 @@ SUBSYSTEM_DEF(ticker)
toggle_ooc(TRUE) // Turn it on
toggle_dooc(TRUE)
declare_completion(force_ending)
check_maprotate()
Master.SetRunLevel(RUNLEVEL_POSTGAME)
/// Checks if the round should be ending, called every ticker tick
@@ -514,13 +513,6 @@ SUBSYSTEM_DEF(ticker)
queued_players -= next_in_line
queue_delay = 0
/datum/controller/subsystem/ticker/proc/check_maprotate()
if(!CONFIG_GET(flag/maprotation))
return
if(world.time - SSticker.round_start_time < 10 MINUTES) //Not forcing map rotation for very short rounds.
return
INVOKE_ASYNC(SSmapping, TYPE_PROC_REF(/datum/controller/subsystem/mapping/, maprotate))
/datum/controller/subsystem/ticker/proc/HasRoundStarted()
return current_state >= GAME_STATE_PLAYING
+1 -1
View File
@@ -42,7 +42,7 @@ SUBSYSTEM_DEF(time_track)
)
/datum/controller/subsystem/time_track/Initialize()
GLOB.perf_log = "[GLOB.log_directory]/perf-[GLOB.round_id ? GLOB.round_id : "NULL"]-[SSmapping.config?.map_name].csv"
GLOB.perf_log = "[GLOB.log_directory]/perf-[GLOB.round_id ? GLOB.round_id : "NULL"]-[SSmapping.current_map.map_name].csv"
world.Profile(PROFILE_RESTART, type = "sendmaps")
//Need to do the sendmaps stuff in its own file, since it works different then everything else
var/list/sendmaps_headers = list()
+1 -1
View File
@@ -25,7 +25,7 @@ SUBSYSTEM_DEF(title)
for(var/S in provisional_title_screens)
var/list/L = splittext(S,"+")
if((L.len == 1 && (L[1] != "exclude" && L[1] != "blank.png")) || (L.len > 1 && ((use_rare_screens && LOWER_TEXT(L[1]) == "rare") || (LOWER_TEXT(L[1]) == LOWER_TEXT(SSmapping.config.map_name)))))
if((L.len == 1 && (L[1] != "exclude" && L[1] != "blank.png")) || (L.len > 1 && ((use_rare_screens && LOWER_TEXT(L[1]) == "rare") || (LOWER_TEXT(L[1]) == LOWER_TEXT(SSmapping.current_map.map_name)))))
title_screens += S
if(length(title_screens))
+12 -8
View File
@@ -101,24 +101,28 @@ SUBSYSTEM_DEF(vote)
// stringify the winners to prevent potential unimplemented serialization errors.
// Perhaps this can be removed in the future and we assert that vote choices must implement serialization.
var/final_winner_string = final_winner && "[final_winner]"
var/final_winner_string = (final_winner && "[final_winner]") || "NO WINNER"
var/list/winners_string = list()
for(var/winner in winners)
winners_string += "[winner]"
if(length(winners))
for(var/winner in winners)
winners_string += "[winner]"
else
winners_string = list("NO WINNER")
var/list/vote_log_data = list(
"type" = "[current_vote.type]",
"choices" = vote_choice_data,
"total" = total_votes,
"winners" = winners_string,
"final_winner" = final_winner_string,
)
var/log_string = replacetext(to_display, "\n", "\\n") // 'keep' the newlines, but dont actually print them as newlines
log_vote(log_string, vote_log_data)
to_chat(world, span_infoplain(vote_font("\n[to_display]")))
log_vote("vote finalized", vote_log_data)
if(to_display)
to_chat(world, span_infoplain(vote_font("\n[to_display]")))
// Finally, doing any effects on vote completion
if (final_winner) // if no one voted, or the vote cannot be won, final_winner will be null
current_vote.finalize_vote(final_winner)
current_vote.finalize_vote(final_winner)
/**
* One selection per person, and the selection with the most votes wins.
+1 -1
View File
@@ -27,7 +27,7 @@
job = SSjob.get_job_type(job)
if(isnull(job_changes))
job_changes = SSmapping.config.job_changes
job_changes = SSmapping.current_map.job_changes
if(!length(job_changes))
refresh_trim_access()
+8 -54
View File
@@ -2,29 +2,18 @@
name = "Map"
default_message = "Vote for next round's map!"
count_method = VOTE_COUNT_METHOD_SINGLE
winner_method = VOTE_WINNER_METHOD_WEIGHTED_RANDOM
winner_method = VOTE_WINNER_METHOD_NONE
display_statistics = FALSE
/datum/vote/map_vote/New()
. = ..()
default_choices = list()
// Fill in our default choices with all of the maps in our map config, if they are votable and not blocked.
var/list/maps = shuffle(global.config.maplist)
for(var/map in maps)
var/datum/map_config/possible_config = config.maplist[map]
if(!possible_config.votable || (possible_config.map_name in SSpersistence.blocked_maps))
continue
default_choices += possible_config.map_name
default_choices = SSmap_vote.get_valid_map_vote_choices()
/datum/vote/map_vote/create_vote()
. = ..()
if(!.)
return FALSE
choices -= get_choices_invalid_for_population()
if(length(choices) == 1) // Only one choice, no need to vote. Let's just auto-rotate it to the only remaining map because it would just happen anyways.
var/datum/map_config/change_me_out = global.config.maplist[choices[1]]
finalize_vote(choices[1])// voted by not voting, very sad.
@@ -48,35 +37,16 @@
. = ..()
if(. != VOTE_AVAILABLE)
return .
if(forced)
return VOTE_AVAILABLE
var/num_choices = length(default_choices - get_choices_invalid_for_population())
var/num_choices = length(default_choices)
if(num_choices <= 1)
return "There [num_choices == 1 ? "is only one map" : "are no maps"] to choose from."
if(SSmapping.map_vote_rocked)
return VOTE_AVAILABLE
if(SSmapping.map_voted)
if(SSmap_vote.next_map_config)
return "The next map has already been selected."
return VOTE_AVAILABLE
/// Returns a list of all map options that are invalid for the current population.
/datum/vote/map_vote/proc/get_choices_invalid_for_population()
var/filter_threshold = 0
if(SSticker.HasRoundStarted())
filter_threshold = get_active_player_count(alive_check = FALSE, afk_check = TRUE, human_check = FALSE)
else
filter_threshold = GLOB.clients.len
var/list/invalid_choices = list()
for(var/map in default_choices)
var/datum/map_config/possible_config = config.maplist[map]
if(possible_config.config_min_users > 0 && filter_threshold < possible_config.config_min_users)
invalid_choices += map
else if(possible_config.config_max_users > 0 && filter_threshold > possible_config.config_max_users)
invalid_choices += map
return invalid_choices
/datum/vote/map_vote/get_result_text(list/all_winners, real_winner, list/non_voters)
return null
/datum/vote/map_vote/get_vote_result(list/non_voters)
// Even if we have default no vote off,
@@ -97,20 +67,4 @@
return ..()
/datum/vote/map_vote/finalize_vote(winning_option)
var/datum/map_config/winning_map = global.config.maplist[winning_option]
if(!istype(winning_map))
CRASH("[type] wasn't passed a valid winning map choice. (Got: [winning_option || "null"] - [winning_map || "null"])")
SSmapping.changemap(winning_map)
SSmapping.map_voted = TRUE
if(SSmapping.map_vote_rocked)
SSmapping.map_vote_rocked = FALSE
/proc/revert_map_vote()
var/datum/map_config/override_map = SSmapping.config
if(isnull(override_map))
return
SSmapping.changemap(override_map)
log_game("The next map has been reset to [override_map.map_name].")
send_to_playing_players(span_boldannounce("The next map is: [override_map.map_name]."))
SSmap_vote.finalize_map_vote(src)
+2 -2
View File
@@ -57,10 +57,10 @@
return
// If there was a previous map vote, we revert the change.
if(!isnull(SSmapping.next_map_config))
if(!isnull(SSmap_vote.next_map_config))
log_game("The next map has been reset due to successful restart vote.")
send_to_playing_players(span_boldannounce("The next map has been reset due to successful restart vote."))
revert_map_vote()
SSmap_vote.revert_next_map()
SSticker.force_ending = FORCE_END_ROUND
log_game("End round forced by successful restart vote.")
-62
View File
@@ -1,62 +0,0 @@
#define CHOICE_TO_ROCK "Yes, re-do the map vote."
#define CHOICE_NOT_TO_ROCK "No, keep the currently selected map."
/// If a map vote is called before the emergency shuttle leaves the station, the players can call another vote to re-run the vote on the shuttle leaving.
/datum/vote/rock_the_vote
name = "Rock the Vote"
override_question = "Rock the Vote?"
contains_vote_in_name = TRUE //lol
default_choices = list(
CHOICE_TO_ROCK,
CHOICE_NOT_TO_ROCK,
)
default_message = "Override the current map vote."
/// The number of times we have rocked the vote thus far.
var/rocking_votes = 0
/datum/vote/rock_the_vote/toggle_votable()
CONFIG_SET(flag/allow_rock_the_vote, !CONFIG_GET(flag/allow_rock_the_vote))
/datum/vote/rock_the_vote/is_config_enabled()
return CONFIG_GET(flag/allow_rock_the_vote)
/datum/vote/rock_the_vote/can_be_initiated(forced)
. = ..()
if(. != VOTE_AVAILABLE)
return .
if(SSticker.current_state == GAME_STATE_FINISHED)
return "The game is finished, no map votes can be initiated."
if(rocking_votes >= CONFIG_GET(number/max_rocking_votes))
return "The maximum number of times to rock the vote has been reached."
if(SSmapping.map_vote_rocked)
return "The vote has already been rocked! Initiate a map vote!"
if(!SSmapping.map_voted)
return "Rocking the vote is disabled because no map has been voted on yet!"
if(SSmapping.map_force_chosen)
return "Rocking the vote is disabled because an admin has forcibly set the map!"
if(EMERGENCY_ESCAPED_OR_ENDGAMED && SSmapping.map_voted)
return "The emergency shuttle has already left the station and the next map has already been chosen!"
return VOTE_AVAILABLE
/datum/vote/rock_the_vote/finalize_vote(winning_option)
rocking_votes++
if(winning_option == CHOICE_NOT_TO_ROCK)
return
if(winning_option == CHOICE_TO_ROCK)
to_chat(world, span_boldannounce("The vote has been rocked! Players are now able to re-run the map vote once more."))
message_admins("The players have successfully rocked the vote.")
SSmapping.map_vote_rocked = TRUE
return
CRASH("[type] wasn't passed a valid winning choice. (Got: [winning_option || "null"])")
#undef CHOICE_TO_ROCK
#undef CHOICE_NOT_TO_ROCK
+1 -1
View File
@@ -212,7 +212,7 @@
.["admins"] = presentmins.len + afkmins.len //equivalent to the info gotten from adminwho
.["gamestate"] = SSticker.current_state
.["map_name"] = SSmapping.config?.map_name || "Loading..."
.["map_name"] = SSmapping.current_map.map_name || "Loading..."
if(key_valid)
.["active_players"] = get_active_player_count()
@@ -672,7 +672,7 @@
/// Returns TRUE if the user can buy shuttles.
/// If they cannot, returns FALSE or a string detailing why.
/obj/machinery/computer/communications/proc/can_buy_shuttles(mob/user)
if (!SSmapping.config.allow_custom_shuttles)
if (!SSmapping.current_map.allow_custom_shuttles)
return FALSE
if (HAS_SILICON_ACCESS(user))
return FALSE
+4 -4
View File
@@ -399,10 +399,10 @@ GLOBAL_VAR(tracy_log)
new_status += " | Shuttle: <b>[SSshuttle.emergency.getModeStr()] [SSshuttle.emergency.getTimerStr()]</b>"
else if(SSticker.current_state == GAME_STATE_FINISHED)
new_status += "<br><b>RESTARTING</b>"
if(SSmapping.config)
new_status += "<br>Map: <b>[SSmapping.config.map_path == CUSTOM_MAP_PATH ? "Uncharted Territory" : SSmapping.config.map_name]</b>"
if(SSmapping.next_map_config)
new_status += "[SSmapping.config ? " | " : "<br>"]Next: <b>[SSmapping.next_map_config.map_path == CUSTOM_MAP_PATH ? "Uncharted Territory" : SSmapping.next_map_config.map_name]</b>"
if(SSmapping.current_map)
new_status += "<br>Map: <b>[SSmapping.current_map.map_path == CUSTOM_MAP_PATH ? "Uncharted Territory" : SSmapping.current_map.map_name]</b>"
if(SSmap_vote.next_map_config)
new_status += "[SSmapping.current_map ? " | " : "<br>"]Next: <b>[SSmap_vote.next_map_config.map_path == CUSTOM_MAP_PATH ? "Uncharted Territory" : SSmap_vote.next_map_config.map_name]</b>"
status = new_status
+4 -13
View File
@@ -1,12 +1,3 @@
ADMIN_VERB(force_random_rotate, R_SERVER, "Trigger 'Random' Map Rotation", "Force a map vote.", ADMIN_CATEGORY_SERVER)
var/rotate = tgui_alert(user,"Force a random map rotation to trigger?", "Rotate map?", list("Yes", "Cancel"))
if (rotate != "Yes")
return
message_admins("[key_name_admin(user)] is forcing a random map rotation.")
log_admin("[key_name(user)] is forcing a random map rotation.")
SSmapping.maprotate()
ADMIN_VERB(admin_change_map, R_SERVER, "Change Map", "Set the next map.", ADMIN_CATEGORY_SERVER)
var/list/maprotatechoices = list()
for (var/map in config.maplist)
@@ -119,14 +110,14 @@ ADMIN_VERB(admin_change_map, R_SERVER, "Change Map", "Set the next map.", ADMIN_
fdel(PATH_TO_NEXT_MAP_JSON)
text2file(json_encode(json_value), PATH_TO_NEXT_MAP_JSON)
if(SSmapping.changemap(virtual_map))
if(SSmap_vote.set_next_map(virtual_map))
message_admins("[key_name_admin(user)] has changed the map to [virtual_map.map_name]")
SSmapping.map_force_chosen = TRUE
SSmap_vote.admin_override = TRUE
fdel("data/custom_map_json/[config_file]")
else
var/datum/map_config/virtual_map = maprotatechoices[chosenmap]
message_admins("[key_name_admin(user)] is changing the map to [virtual_map.map_name]")
log_admin("[key_name(user)] is changing the map to [virtual_map.map_name]")
if (SSmapping.changemap(virtual_map))
if (SSmap_vote.set_next_map(virtual_map))
message_admins("[key_name_admin(user)] has changed the map to [virtual_map.map_name]")
SSmapping.map_force_chosen = TRUE
SSmap_vote.admin_override = TRUE
@@ -38,7 +38,7 @@
var/static/list/doom_options = list()
if (!length(doom_options))
doom_options = list(DOOM_SINGULARITY, DOOM_TESLA)
if (!SSmapping.config.planetary)
if (!SSmapping.is_planetary())
doom_options += DOOM_METEORS
switch(pick(doom_options))
+6
View File
@@ -456,3 +456,9 @@ ADMIN_VERB(reset_ooc_color, R_FUN, "Reset Player OOC Color", "Returns player OOC
ASSERT(prefs, "User attempted to export preferences while preferences were null!") // what the fuck
prefs.savefile.export_json_to_client(usr, ckey)
/client/verb/map_vote_tally_count()
set name = "Show Map Vote Tallies"
set desc = "View the current map vote tally counts."
set category = "Server"
to_chat(mob, SSmap_vote.tally_printout)
+1 -1
View File
@@ -4,7 +4,7 @@
/datum/tgs_chat_command/tgscheck/Run(datum/tgs_chat_user/sender, params)
var/server = CONFIG_GET(string/server)
return new /datum/tgs_message_content("[GLOB.round_id ? "Round #[GLOB.round_id]: " : ""][GLOB.clients.len] players on [SSmapping.config.map_name]; Round [SSticker.HasRoundStarted() ? (SSticker.IsRoundInProgress() ? "Active" : "Finishing") : "Starting"] -- [server ? server : "[world.internet_address]:[world.port]"]")
return new /datum/tgs_message_content("[GLOB.round_id ? "Round #[GLOB.round_id]: " : ""][GLOB.clients.len] players on [SSmapping.current_map.map_name]; Round [SSticker.HasRoundStarted() ? (SSticker.IsRoundInProgress() ? "Active" : "Finishing") : "Starting"] -- [server ? server : "[world.internet_address]:[world.port]"]")
/datum/tgs_chat_command/gameversion
name = "gameversion"
+1 -1
View File
@@ -35,7 +35,7 @@ GLOBAL_DATUM(escape_menu_details, /atom/movable/screen/escape_menu/details)
<span style='text-align: right; line-height: 0.7'>
Round ID: [GLOB.round_id || "Unset"]<br />
Round Time: [ROUND_TIME()]<br />
Map: [SSmapping.config?.map_name || "Loading..."]<br />
Map: [SSmapping.current_map.map_name || "Loading..."]<br />
Time Dilation: [round(SStime_track.time_dilation_current,1)]%<br />
</span>
"}
+2 -2
View File
@@ -278,6 +278,6 @@
///Generates a persistence id unique to the current map. Every bar should feel a little bit different after all.
/obj/structure/sign/picture_frame/portrait/bar/Initialize(mapload)
if(SSmapping.config.map_path != CUSTOM_MAP_PATH) //skip adminloaded custom maps.
persistence_id = "frame_bar_[SSmapping.config.map_name]"
if(SSmapping.current_map.map_path != CUSTOM_MAP_PATH) //skip adminloaded custom maps.
persistence_id = "frame_bar_[SSmapping.current_map.map_name]"
return ..()
@@ -27,7 +27,7 @@
// changed to allow Z cropping and that's a mess
var/z_offset = SSmapping.station_start
var/list/bounds
for (var/path in SSmapping.config.GetFullMapPaths())
for (var/path in SSmapping.current_map.GetFullMapPaths())
var/datum/parsed_map/parsed = load_map(
file(path),
1,
+1 -1
View File
@@ -549,7 +549,7 @@
color_override = "orange",
)
INVOKE_ASYNC(SSticker, TYPE_PROC_REF(/datum/controller/subsystem/ticker, poll_hearts))
SSmapping.mapvote() //If no map vote has been run yet, start one.
INVOKE_ASYNC(SSvote, TYPE_PROC_REF(/datum/controller/subsystem/vote, initiate_vote), /datum/vote/map_vote, initiator_name = "Map Rotation")
if(!is_reserved_level(z))
CRASH("Emergency shuttle did not move to transit z-level!")
+1 -1
View File
@@ -264,7 +264,7 @@
/obj/docking_port/stationary/proc/load_roundstart()
if(json_key)
var/sid = SSmapping.config.shuttles[json_key]
var/sid = SSmapping.current_map.shuttles[json_key]
roundstart_template = SSmapping.shuttle_templates[sid]
if(!roundstart_template)
CRASH("json_key:[json_key] value \[[sid]\] resulted in a null shuttle template for [src]")
+3 -3
View File
@@ -158,7 +158,7 @@ GLOBAL_VAR_INIT(focused_tests, focused_tests())
/// Logs a test message. Will use GitHub action syntax found at https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
/datum/unit_test/proc/log_for_test(text, priority, file, line)
var/map_name = SSmapping.config.map_name
var/map_name = SSmapping.current_map.map_name
// Need to escape the text to properly support newlines.
var/annotation_text = replacetext(text, "%", "%25")
@@ -177,14 +177,14 @@ GLOBAL_VAR_INIT(focused_tests, focused_tests())
GLOB.current_test = test
var/duration = REALTIMEOFDAY
var/skip_test = (test_path in SSmapping.config.skipped_tests)
var/skip_test = (test_path in SSmapping.current_map.skipped_tests)
var/test_output_desc = "[test_path]"
var/message = ""
log_world("::group::[test_path]")
if(skip_test)
log_world("[TEST_OUTPUT_YELLOW("SKIPPED")] Skipped run on map [SSmapping.config.map_name].")
log_world("[TEST_OUTPUT_YELLOW("SKIPPED")] Skipped run on map [SSmapping.current_map.map_name].")
else
+12
View File
@@ -0,0 +1,12 @@
## A flat bonus to give to all maps after a map vote is concluded.
MAP_VOTE_FLAT_BONUS 5
## The minimum number of tallies a map can have for purposes of map rotation.
MAP_VOTE_MINIMUM_TALLIES 1
## The maximum number of tallies a map can have for purposes of keeping things sane.
MAP_VOTE_MAXIMUM_TALLIES 200
## The percentage of tallies that are carried over between rounds.
MAP_VOTE_TALLY_CARRYOVER_PERCENTAGE 100
+1 -1
View File
@@ -659,6 +659,7 @@
#include "code\controllers\subsystem\lighting.dm"
#include "code\controllers\subsystem\lua.dm"
#include "code\controllers\subsystem\machines.dm"
#include "code\controllers\subsystem\map_vote.dm"
#include "code\controllers\subsystem\mapping.dm"
#include "code\controllers\subsystem\market.dm"
#include "code\controllers\subsystem\materials.dm"
@@ -1924,7 +1925,6 @@
#include "code\datums\votes\custom_vote.dm"
#include "code\datums\votes\map_vote.dm"
#include "code\datums\votes\restart_vote.dm"
#include "code\datums\votes\rock_the_vote.dm"
#include "code\datums\weather\weather.dm"
#include "code\datums\weather\weather_types\ash_storm.dm"
#include "code\datums\weather\weather_types\floor_is_lava.dm"