Files
Bubberstation/code/modules/basketball/controller.dm
SkyratBot d9cbbd4aaa [MIRROR] [NO GBP] Fix minigames UI not removing inactive clients [MDB IGNORE] (#20392)
* [NO GBP] Fix minigames UI not removing inactive clients (#74391)

## About The Pull Request
Fixes #74260

The UI for the basketball menu was not properly updating when clients
would disconnect. This would allow someone to signup twice leading to
silly situations as detailed in the issue report. This issue also
affected the mafia minigame due to both UIs having similar code.

The fix is simple, just check the signups when the tgui menu has
interactions and also check to make sure the signups use a boolean
instead of a client for the value part of the key/value list. Why? After
a client would disconnect, the list would change and remove the client
object since it no longer exists. The solution is to keep the ckey as a
key, but use a boolean for the value.

## Why It's Good For The Game
Less bugs, more stability.

## Changelog
🆑
fix: Fix basketball and mafia minigame UI not removing inactive clients
/🆑

---------

Co-authored-by: Fikou <23585223+Fikou@ users.noreply.github.com>

* [NO GBP] Fix minigames UI not removing inactive clients

---------

Co-authored-by: Tim <timothymtorres@gmail.com>
Co-authored-by: Fikou <23585223+Fikou@ users.noreply.github.com>
2023-04-07 16:30:12 -07:00

417 lines
17 KiB
Plaintext

///how many people can play basketball without issues (running out of spawns, procs not expecting more than this amount of people, etc)
#define BASKETBALL_MIN_PLAYER_COUNT 2
#define BASKETBALL_MAX_PLAYER_COUNT 7
#define BASKETBALL_TEAM_HOME "home"
#define BASKETBALL_TEAM_AWAY "away"
/// list of ghosts who want to play basketball, every time someone enters the list it checks to see if enough are in
GLOBAL_LIST_EMPTY(basketball_signup)
/// list of ghosts who want to play basketball that have since disconnected. They are kept in the lobby, but not counted for starting a game.
GLOBAL_LIST_EMPTY(basketball_bad_signup)
/// the current global basketball game running.
GLOBAL_VAR(basketball_game)
/**
* The basketball controller handles the basketball minigame in progress.
* It is first created when the first ghost signs up to play.
*/
/datum/basketball_controller
/// Template picked when the game starts. used for the name and desc reading
var/datum/map_template/basketball/current_map
/// Map generation tool that deletes the current map after the game finishes
var/datum/map_generator/massdelete/map_deleter
/// Total amount of time basketball is played for
var/game_duration = 3 MINUTES
/// List of all players ckeys involved in the minigame
var/list/minigame_players = list()
/// Spawn points for home team players
var/list/home_team_landmarks = list()
/// List of home team players ckeys
var/list/home_team_players = list()
/// The basketball hoop used by home team
var/obj/structure/hoop/minigame/home_hoop
/// Spawn points for away team players
var/list/away_team_landmarks = list()
/// List of away team players ckeys
var/list/away_team_players = list()
/// The basketball hoop used by away team
var/obj/structure/hoop/minigame/away_hoop
/// Spawn point for referee (there should only be one spot on minigame map)
var/list/referee_landmark = list()
/datum/basketball_controller/New()
. = ..()
GLOB.basketball_game = src
map_deleter = new
/datum/basketball_controller/Destroy(force, ...)
. = ..()
GLOB.basketball_game = null
end_game()
qdel(map_deleter)
/**
* Triggers at beginning of the game when there is a confirmed list of valid, ready players.
* Creates a 100% ready game that has NOT started (no players in bodies)
* Followed by start game
*
* Does the following:
* * Picks map, and loads it
* * Grabs landmarks if it is the first time it's loading
* * Puts players in each team randomly
* Arguments:
* * ready_players: list of filtered, sane players (so not playing or disconnected) for the game to put into roles
*/
/datum/basketball_controller/proc/prepare_game(ready_players)
var/list/possible_maps = subtypesof(/datum/map_template/basketball)
var/turf/spawn_area = get_turf(locate(/obj/effect/landmark/basketball/game_area) in GLOB.landmarks_list)
current_map = pick(possible_maps)
current_map = new current_map
if(!spawn_area)
CRASH("No spawn area detected for Basketball Minigame!")
var/list/bounds = current_map.load(spawn_area)
if(!bounds)
CRASH("Loading basketball map failed!")
map_deleter.defineRegion(spawn_area, locate(spawn_area.x + 23, spawn_area.y + 25, spawn_area.z), replace = TRUE) //so we're ready to mass delete when round ends
var/turf/home_hoop_turf = get_turf(locate(/obj/effect/landmark/basketball/team_spawn/home_hoop) in GLOB.landmarks_list)
if(!home_hoop_turf)
CRASH("No home landmark for basketball hoop detected!")
home_hoop = (locate(/obj/structure/hoop/minigame) in home_hoop_turf)
if(!home_hoop)
CRASH("No minigame basketball hoop detected for home team!")
if(!home_team_landmarks.len)
for(var/obj/effect/landmark/basketball/team_spawn/home/possible_spawn in GLOB.landmarks_list)
home_team_landmarks += possible_spawn
var/turf/away_hoop_turf = get_turf(locate(/obj/effect/landmark/basketball/team_spawn/away_hoop) in GLOB.landmarks_list)
if(!away_hoop_turf)
CRASH("No away landmark for basketball hoop detected!")
away_hoop = (locate(/obj/structure/hoop/minigame) in away_hoop_turf)
if(!away_hoop)
CRASH("No minigame basketball hoop detected for away team!")
if(!away_team_landmarks.len)
for(var/obj/effect/landmark/basketball/team_spawn/away/possible_spawn in GLOB.landmarks_list)
away_team_landmarks += possible_spawn
for(var/obj/effect/landmark/basketball/team_spawn/referee/possible_spawn in GLOB.landmarks_list)
referee_landmark += possible_spawn
start_game(ready_players)
/**
* The game by this point is now all set up, and so we can put people in their bodies.
*/
/datum/basketball_controller/proc/start_game(ready_players)
message_admins("The players have spoken! Voting has enabled the basketball minigame!")
notify_ghosts(
"Basketball minigame is about to start!",
source = home_hoop,
header = "Basketball Minigame",
ghost_sound = 'sound/effects/ghost2.ogg',
notify_volume = 75,
action = NOTIFY_ORBIT,
)
create_bodies(ready_players)
addtimer(CALLBACK(src, PROC_REF(victory)), game_duration)
for(var/i in 1 to 10)
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(playsound), home_hoop, 'sound/items/timer.ogg', 75, FALSE), game_duration - (i SECONDS))
addtimer(CALLBACK(home_hoop, TYPE_PROC_REF(/atom/movable/, say), "[i] seconds left"), game_duration - (i SECONDS))
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(playsound), away_hoop, 'sound/items/timer.ogg', 75, FALSE), game_duration - (i SECONDS))
addtimer(CALLBACK(away_hoop, TYPE_PROC_REF(/atom/movable/, say), "[i] seconds left"), game_duration - (i SECONDS))
/**
* Called when the game is setting up, AFTER map is loaded but BEFORE the game start. Creates and places each body and gives the correct player key
*/
/datum/basketball_controller/proc/create_bodies(ready_players)
var/list/possible_away_teams = subtypesof(/datum/map_template/basketball) - current_map.type
var/datum/map_template/basketball/away_map = pick(possible_away_teams)
away_map = new away_map
var/list/home_spawnpoints = home_team_landmarks.Copy()
var/list/away_spawnpoints = away_team_landmarks.Copy()
var/list/referee_spawnpoint = referee_landmark.Copy()
var/obj/effect/landmark/basketball/team_spawn/spawn_landmark
var/team_uniform
var/team_name
// rename the hoops to their appropriate teams names
home_hoop.name = current_map.team_name
away_hoop.name = away_map.team_name
var/player_count = 0
// if total players is odd number then the odd man out is a referee
var/minigame_has_referee = length(ready_players) % 2
for(var/player_key in ready_players)
player_count++
minigame_players |= player_key
var/is_player_referee = (player_count == length(ready_players) && minigame_has_referee)
if(is_player_referee)
spawn_landmark = pick_n_take(referee_spawnpoint)
team_uniform = /datum/outfit/basketball/referee
else if(player_count % 2) // odd is home team
spawn_landmark = pick_n_take(home_spawnpoints)
home_team_players |= player_key
away_hoop.team_ckeys |= player_key // to restrict scoring on opponents hoop rapidly
team_uniform = current_map.home_team_uniform
team_name = current_map.team_name
else // even is away team
spawn_landmark = pick_n_take(away_spawnpoints)
away_team_players |= player_key
home_hoop.team_ckeys |= player_key // to restrict scoring on opponents hoop rapidly
team_uniform = away_map.home_team_uniform
team_name = away_map.team_name
var/mob/living/carbon/human/baller = new(get_turf(spawn_landmark))
if(baller.dna.species.outfit_important_for_life)
baller.set_species(/datum/species/human)
ADD_TRAIT(baller, TRAIT_NOFIRE, BASKETBALL_MINIGAME_TRAIT)
ADD_TRAIT(baller, TRAIT_NOBREATH, BASKETBALL_MINIGAME_TRAIT)
ADD_TRAIT(baller, TRAIT_CANNOT_CRYSTALIZE, BASKETBALL_MINIGAME_TRAIT)
// this is basketball, not a boxing match
ADD_TRAIT(baller, TRAIT_PACIFISM, BASKETBALL_MINIGAME_TRAIT)
baller.equipOutfit(team_uniform)
var/client/player_client = GLOB.directory[player_key]
if(player_client)
player_client.prefs.safe_transfer_prefs_to(baller, is_antag = TRUE)
baller.key = player_key
SEND_SOUND(baller, sound('sound/misc/whistle.ogg', volume=30))
if(is_player_referee)
to_chat(baller, span_notice("You are a referee. Make sure the teams play fair and use your whistle to call fouls appropriately."))
else
to_chat(baller, span_notice("You are a basketball player for the [team_name]. Score as much as you can before time runs out."))
to_chat(baller, span_info("LMB to pass the ball while on help intent (zero stamina cost/) - accuracy penalty when scoring)"))
to_chat(baller, span_info("RMB to shoot the ball ([STAMINA_COST_SHOOTING] stamina cost) - this goes over players heads"))
to_chat(baller, span_info("Click directly on hoop while adjacent to dunk ([STAMINA_COST_DUNKING] stamina cost)"))
to_chat(baller, span_info("Spinning decreases other players disarm chance against you but reduces shooting accuracy ([STAMINA_COST_SPINNING] stamina cost)"))
/**
* Called after the game is finished. Sends end game notifications to teams and dusts the losers.
*/
/datum/basketball_controller/proc/victory()
var/is_game_draw
var/list/winner_team_ckeys = list()
var/list/loser_team_ckeys = list()
var/winner_team_name
if(home_hoop.total_score == away_hoop.total_score)
is_game_draw = TRUE
winner_team_ckeys |= home_team_players
winner_team_ckeys |= away_team_players
else if(home_hoop.total_score > away_hoop.total_score)
winner_team_ckeys = away_team_players
winner_team_name = away_hoop.name
loser_team_ckeys = home_team_players
else if(home_hoop.total_score < away_hoop.total_score)
winner_team_ckeys = home_team_players
winner_team_name = home_hoop.name
loser_team_ckeys = away_team_players
if(is_game_draw)
for(var/ckey in winner_team_ckeys)
var/mob/living/competitor = get_mob_by_ckey(ckey)
var/area/mob_area = get_area(competitor)
if(istype(competitor) && istype(mob_area, /area/centcom/basketball))
to_chat(competitor, span_hypnophrase("The game resulted in a draw!"))
else
for(var/ckey in winner_team_ckeys)
var/mob/living/competitor = get_mob_by_ckey(ckey)
var/area/mob_area = get_area(competitor)
if(istype(competitor) && istype(mob_area, /area/centcom/basketball))
to_chat(competitor, span_hypnophrase("[winner_team_name] team wins!"))
for(var/ckey in loser_team_ckeys)
var/mob/living/competitor = get_mob_by_ckey(ckey)
var/area/mob_area = get_area(competitor)
if(istype(competitor) && istype(mob_area, /area/centcom/basketball))
to_chat(competitor, span_hypnophrase("[winner_team_name] team wins!"))
competitor.dust()
addtimer(CALLBACK(src, PROC_REF(end_game)), 20 SECONDS) // give winners time for a victory lap
/**
* Cleans up the game, resetting variables back to the beginning and removing the map with the generator.
*/
/datum/basketball_controller/proc/end_game()
for(var/ckey in minigame_players)
var/mob/living/competitor = get_mob_by_ckey(ckey)
var/area/mob_area = get_area(competitor)
if(istype(competitor) && istype(mob_area, /area/centcom/basketball))
QDEL_NULL(competitor)
map_deleter.generate() //remove the map, it will be loaded at the start of the next one
QDEL_NULL(current_map)
//map gen does not deal with landmarks
QDEL_LIST(home_team_landmarks)
QDEL_LIST(away_team_landmarks)
QDEL_LIST(referee_landmark)
/**
* Called when enough players have signed up to fill a setup. DOESN'T NECESSARILY MEAN THE GAME WILL START.
*
* Checks for a custom setup, if so gets the required players from that and if not it sets the player requirement to BASKETBALL_MAX_PLAYER_COUNT and generates one IF basic setup starts a game.
* Checks if everyone signed up is an observer, and is still connected. If people aren't, they're removed from the list.
* If there aren't enough players post sanity, it aborts. otherwise, it selects enough people for the game and starts preparing the game for real.
*/
/datum/basketball_controller/proc/basic_setup()
//final list for all the players who will be in this game
var/list/filtered_keys = list()
//cuts invalid players from signups (disconnected/not a ghost)
var/list/possible_keys = list()
for(var/key in GLOB.basketball_signup)
if(GLOB.directory[key])
var/client/C = GLOB.directory[key]
if(isobserver(C.mob))
possible_keys += key
continue
GLOB.basketball_signup -= key //not valid to play when we checked so remove them from signups
//if there were not enough players, don't start. we already trimmed the list to now hold only valid signups
if(length(possible_keys) < BASKETBALL_MIN_PLAYER_COUNT)
return
var/req_players = length(possible_keys) >= BASKETBALL_MAX_PLAYER_COUNT ? BASKETBALL_MAX_PLAYER_COUNT : length(possible_keys)
//if there were too many players, still start but only make filtered keys as big as it needs to be (cut excess)
//also removes people who do get into final player list from the signup so they have to sign up again when game ends
for(var/i in 1 to req_players)
var/chosen_key = pick_n_take(possible_keys)
filtered_keys += chosen_key
GLOB.basketball_signup -= chosen_key
//small message about not getting into this game for clarity on why they didn't get in
for(var/unpicked in possible_keys)
var/client/unpicked_client = GLOB.directory[unpicked]
to_chat(unpicked_client, span_danger("Sorry, the starting basketball game has too many players and you were not picked."))
to_chat(unpicked_client, span_warning("You're still signed up, getting messages from the current round, and have another chance to join when the one starting now finishes."))
prepare_game(filtered_keys)
/**
* Filters inactive player into a different list until they reconnect, and removes players who are no longer ghosts.
*/
/datum/basketball_controller/proc/check_signups()
for(var/bad_key in GLOB.basketball_bad_signup)
var/client/signup_client = GLOB.directory[bad_key]
if(signup_client) //they have reconnected if we can search their key and get a client
GLOB.basketball_bad_signup -= bad_key
GLOB.basketball_signup[bad_key] = TRUE
for(var/key in GLOB.basketball_signup)
var/client/signup_client = GLOB.directory[key]
if(!signup_client) //vice versa but in a variable we use later
GLOB.basketball_signup -= key
GLOB.basketball_bad_signup[key] = TRUE
continue
if(!isobserver(signup_client.mob))
//they are back to playing the game, remove them from the signups
GLOB.basketball_signup -= key
/**
* Called when someone signs up, and sees if there are enough people in the signup list to begin.
*
* Only checks if everyone is actually valid to start (still connected and an observer) if there are enough players (basic_setup)
*/
/datum/basketball_controller/proc/try_autostart()
if(!(GLOB.ghost_role_flags & GHOSTROLE_MINIGAME))
return
if(GLOB.basketball_signup.len >= BASKETBALL_MIN_PLAYER_COUNT) //enough people to try and make something (or debug mode)
basic_setup()
/datum/basketball_controller/ui_state(mob/user)
return GLOB.always_state
/datum/basketball_controller/ui_interact(mob/user, datum/tgui/ui)
check_signups()
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "BasketballPanel")
ui.set_autoupdate(FALSE)
ui.open()
/datum/basketball_controller/ui_data(mob/user)
. = ..()
.["total_votes"] = GLOB.basketball_signup.len
.["players_min"] = BASKETBALL_MIN_PLAYER_COUNT
.["players_max"] = BASKETBALL_MAX_PLAYER_COUNT
var/list/lobby_data = list()
for(var/key in GLOB.basketball_signup + GLOB.basketball_bad_signup)
var/list/lobby_member = list()
lobby_member["ckey"] = key
lobby_member["status"] = (key in GLOB.basketball_bad_signup) ? "Disconnected" : "Ready"
lobby_data += list(lobby_member)
.["lobbydata"] = lobby_data
/datum/basketball_controller/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
var/mob/dead/observer/user = ui.user
if(!istype(user)) // only ghosts
return
var/client/ghost_client = user.client
if(!SSticker.HasRoundStarted())
to_chat(ghost_client, span_warning("Wait for the round to start."))
return
switch(action)
if("basketball_signup")
if(GLOB.basketball_signup[ghost_client.ckey] || GLOB.basketball_bad_signup[ghost_client.ckey])
GLOB.basketball_signup -= ghost_client.ckey
GLOB.basketball_bad_signup -= ghost_client.ckey
to_chat(ghost_client, span_notice("You unregister from basketball."))
else
GLOB.basketball_signup[ghost_client.ckey] = TRUE
to_chat(ghost_client, span_notice("You sign up for basketball."))
check_signups()
return TRUE
if("basketball_start")
if(!GLOB.basketball_signup[ghost_client.ckey])
to_chat(ghost_client, span_notice("You must sign up to start the game."))
return
if(current_map)
to_chat(ghost_client, span_notice("Wait for current basketball game to finish."))
return
try_autostart()
return TRUE
/**
* Creates the global datum for playing basketball games, destroys the last if that's required and returns the new.
*/
/proc/create_basketball_game()
if(GLOB.basketball_game)
QDEL_NULL(GLOB.basketball_game)
var/datum/basketball_controller/basketball_minigame = new()
return basketball_minigame
#undef BASKETBALL_MIN_PLAYER_COUNT
#undef BASKETBALL_MAX_PLAYER_COUNT
#undef BASKETBALL_TEAM_HOME
#undef BASKETBALL_TEAM_AWAY