diff --git a/code/__defines/MC.dm b/code/__defines/MC.dm index 6acc667f2d..fb2e30f587 100644 --- a/code/__defines/MC.dm +++ b/code/__defines/MC.dm @@ -1,4 +1,5 @@ #define MC_TICK_CHECK ( ( TICK_USAGE > Master.current_ticklimit || src.state != SS_RUNNING ) ? pause() : 0 ) +#define GAME_STATE 2 ** (Master.current_runlevel - 1) #define MC_SPLIT_TICK_INIT(phase_count) var/original_tick_limit = Master.current_ticklimit; var/split_tick_phases = ##phase_count #define MC_SPLIT_TICK \ @@ -55,6 +56,30 @@ /// This flag overrides SS_KEEP_TIMING #define SS_POST_FIRE_TIMING 64 +// -- SStimer stuff -- +//Don't run if there is an identical unique timer active +#define TIMER_UNIQUE 0x1 + +//For unique timers: Replace the old timer rather then not start this one +#define TIMER_OVERRIDE 0x2 + +//Timing should be based on how timing progresses on clients, not the sever. +// tracking this is more expensive, +// should only be used in conjuction with things that have to progress client side, such as animate() or sound() +#define TIMER_CLIENT_TIME 0x4 + +//Timer can be stopped using deltimer() +#define TIMER_STOPPABLE 0x8 + +//To be used with TIMER_UNIQUE +//prevents distinguishing identical timers with the wait variable +#define TIMER_NO_HASH_WAIT 0x10 + +//number of byond ticks that are allowed to pass before the timer subsystem thinks it hung on something +#define TIMER_NO_INVOKE_WARNING 600 + +#define TIMER_ID_NULL -1 + //! SUBSYSTEM STATES #define SS_IDLE 0 /// aint doing shit. #define SS_QUEUED 1 /// queued to run diff --git a/code/controllers/subsystems/timer.dm b/code/controllers/subsystems/timer.dm index b743e8ff9e..1b9b17623a 100644 --- a/code/controllers/subsystems/timer.dm +++ b/code/controllers/subsystems/timer.dm @@ -1,21 +1,19 @@ -#define BUCKET_LEN (world.fps*1*60) //how many ticks should we keep in the bucket. (1 minutes worth) -#define BUCKET_POS(timer) ((round((timer.timeToRun - SStimer.head_offset) / world.tick_lag) % BUCKET_LEN)||BUCKET_LEN) -#define TIMER_MAX (world.time + TICKS2DS(min(BUCKET_LEN-(SStimer.practical_offset-DS2TICKS(world.time - SStimer.head_offset))-1, BUCKET_LEN-1))) +#define BUCKET_LEN (round(10*(60/world.tick_lag), 1)) //how many ticks should we keep in the bucket. (1 minutes worth) +#define BUCKET_POS(timer) (round((timer.timeToRun - SStimer.head_offset) / world.tick_lag) + 1) #define TIMER_ID_MAX (2**24) //max float with integer precision SUBSYSTEM_DEF(timer) name = "Timer" wait = 1 //SS_TICKER subsystem, so wait is in ticks - priority = FIRE_PRIORITY_TIMERS //VOREStation Emergency Edit - init_order = INIT_ORDER_TIMER + init_order = 1 flags = SS_TICKER|SS_NO_INIT - var/list/datum/timedevent/second_queue = list() //awe, yes, you've had first queue, but what about second queue? + var/list/datum/timedevent/processing = list() var/list/hashes = list() var/head_offset = 0 //world.time of the first entry in the the bucket. - var/practical_offset = 1 //index of the first non-empty item in the bucket. + var/practical_offset = 0 //index of the first non-empty item in the bucket. var/bucket_resolution = 0 //world.tick_lag the bucket was designed for var/bucket_count = 0 //how many timers are in the buckets @@ -29,31 +27,31 @@ SUBSYSTEM_DEF(timer) var/static/last_invoke_warning = 0 var/static/bucket_auto_reset = TRUE -/datum/controller/subsystem/timer/PreInit() - bucket_list.len = BUCKET_LEN - head_offset = world.time - bucket_resolution = world.tick_lag + var/static/times_flushed = 0 + var/static/times_crashed = 0 /datum/controller/subsystem/timer/stat_entry(msg) - ..("B:[bucket_count] P:[length(second_queue)] H:[length(hashes)] C:[length(clienttime_timers)] S:[length(timer_id_dict)]") + ..("B:[bucket_count] P:[length(processing)] H:[length(hashes)] C:[length(clienttime_timers)][times_crashed ? " F:[times_crashed]" : ""]") /datum/controller/subsystem/timer/fire(resumed = FALSE) var/lit = last_invoke_tick - var/last_check = world.time - TICKS2DS(BUCKET_LEN*1.5) + var/last_check = world.time - TIMER_NO_INVOKE_WARNING var/list/bucket_list = src.bucket_list + var/static/list/spent = list() if(!bucket_count) last_invoke_tick = world.time - if(lit && lit < last_check && head_offset < last_check && last_invoke_warning < last_check) + if(lit && lit < last_check && last_invoke_warning < last_check) last_invoke_warning = world.time - var/msg = "No regular timers processed in the last [BUCKET_LEN*1.5] ticks[bucket_auto_reset ? ", resetting buckets" : ""]!" + var/msg = "No regular timers processed in the last [TIMER_NO_INVOKE_WARNING] ticks[bucket_auto_reset ? ", resetting buckets" : ""]!" + times_crashed++ message_admins(msg) WARNING(msg) if(bucket_auto_reset) bucket_resolution = 0 - log_world("Timer bucket reset. world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset]") + log_world("Timer bucket reset. world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset], times_flushed: [times_flushed], length(spent): [length(spent)]") for (var/i in 1 to length(bucket_list)) var/datum/timedevent/bucket_head = bucket_list[i] if (!bucket_head) @@ -68,174 +66,85 @@ SUBSYSTEM_DEF(timer) bucket_node = bucket_node.next anti_loop_check-- while(bucket_node && bucket_node != bucket_head && anti_loop_check) - log_world("Active timers in the second_queue queue:") - for(var/I in second_queue) + log_world("Active timers in the processing queue:") + for(var/I in processing) log_world(get_timer_debug_string(I)) - var/cut_start_index = 1 - var/next_clienttime_timer_index = 0 - var/len = length(clienttime_timers) - - for (next_clienttime_timer_index in 1 to len) - if (MC_TICK_CHECK) - next_clienttime_timer_index-- - break - var/datum/timedevent/ctime_timer = clienttime_timers[next_clienttime_timer_index] - if (ctime_timer.timeToRun > REALTIMEOFDAY) - next_clienttime_timer_index-- - break - - var/datum/callback/callBack = ctime_timer.callBack - if (!callBack) - clienttime_timers.Cut(next_clienttime_timer_index,next_clienttime_timer_index+1) - CRASH("Invalid timer: [get_timer_debug_string(ctime_timer)] world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset], REALTIMEOFDAY: [REALTIMEOFDAY]") - - ctime_timer.spent = REALTIMEOFDAY - callBack.InvokeAsync() - - if(ctime_timer.flags & TIMER_LOOP) - ctime_timer.spent = 0 - clienttime_timers.Insert(ctime_timer, 1) - cut_start_index++ - else + while(length(clienttime_timers)) + var/datum/timedevent/ctime_timer = clienttime_timers[clienttime_timers.len] + if (ctime_timer.timeToRun <= REALTIMEOFDAY) + --clienttime_timers.len + var/datum/callback/callBack = ctime_timer.callBack + ctime_timer.spent = TRUE + callBack.InvokeAsync() qdel(ctime_timer) + else + break //None of the rest are ready to run + if (MC_TICK_CHECK) + return - - if (next_clienttime_timer_index) - clienttime_timers.Cut(cut_start_index,next_clienttime_timer_index+1) - - if (MC_TICK_CHECK) - return - - var/static/list/spent = list() var/static/datum/timedevent/timer - if (practical_offset > BUCKET_LEN) - head_offset += TICKS2DS(BUCKET_LEN) - practical_offset = 1 - resumed = FALSE + var/static/datum/timedevent/head - if ((length(bucket_list) != BUCKET_LEN) || (world.tick_lag != bucket_resolution)) - reset_buckets() + if (practical_offset > BUCKET_LEN || (!resumed && length(bucket_list) != BUCKET_LEN || world.tick_lag != bucket_resolution)) + shift_buckets() bucket_list = src.bucket_list resumed = FALSE - if (!resumed) timer = null + head = null - while (practical_offset <= BUCKET_LEN && head_offset + ((practical_offset-1)*world.tick_lag) <= world.time) - var/datum/timedevent/head = bucket_list[practical_offset] + while (practical_offset <= BUCKET_LEN && head_offset + (practical_offset*world.tick_lag) <= world.time && !MC_TICK_CHECK) if (!timer || !head || timer == head) head = bucket_list[practical_offset] + if (!head) + practical_offset++ + if (MC_TICK_CHECK) + break + continue timer = head - while (timer) + do var/datum/callback/callBack = timer.callBack if (!callBack) + qdel(timer) bucket_resolution = null //force bucket recreation - CRASH("Invalid timer: [get_timer_debug_string(timer)] world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset]") + CRASH("Invalid timer: [timer] timer.timeToRun=[timer.timeToRun]||QDELETED(timer)=[QDELETED(timer)]||world.time=[world.time]||head_offset=[head_offset]||practical_offset=[practical_offset]||timer.spent=[timer.spent]") if (!timer.spent) spent += timer - timer.spent = world.time + timer.spent = TRUE callBack.InvokeAsync() last_invoke_tick = world.time + timer = timer.next + if (MC_TICK_CHECK) return - - timer = timer.next - if (timer == head) - break - - - bucket_list[practical_offset++] = null - - //we freed up a bucket, lets see if anything in second_queue needs to be shifted to that bucket. - var/i = 0 - var/L = length(second_queue) - for (i in 1 to L) - timer = second_queue[i] - if (timer.timeToRun >= TIMER_MAX) - i-- - break - - if (timer.timeToRun < head_offset) - bucket_resolution = null //force bucket recreation - CRASH("[i] Invalid timer state: Timer in long run queue with a time to run less then head_offset. [get_timer_debug_string(timer)] world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset]") - - if (timer.callBack && !timer.spent) - timer.callBack.InvokeAsync() - spent += timer - bucket_count++ - else if(!QDELETED(timer)) - qdel(timer) - continue - - if (timer.timeToRun < head_offset + TICKS2DS(practical_offset-1)) - bucket_resolution = null //force bucket recreation - CRASH("[i] Invalid timer state: Timer in long run queue that would require a backtrack to transfer to short run queue. [get_timer_debug_string(timer)] world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset]") - if (timer.callBack && !timer.spent) - timer.callBack.InvokeAsync() - spent += timer - bucket_count++ - else if(!QDELETED(timer)) - qdel(timer) - continue - - bucket_count++ - var/bucket_pos = max(1, BUCKET_POS(timer)) - - var/datum/timedevent/bucket_head = bucket_list[bucket_pos] - if (!bucket_head) - bucket_list[bucket_pos] = timer - timer.next = null - timer.prev = null - continue - - if (!bucket_head.prev) - bucket_head.prev = bucket_head - timer.next = bucket_head - timer.prev = bucket_head.prev - timer.next.prev = timer - timer.prev.next = timer - if (i) - second_queue.Cut(1, i+1) - + while (timer && timer != head) timer = null + bucket_list[practical_offset++] = null + if (MC_TICK_CHECK) + return + + times_flushed++ bucket_count -= length(spent) - for (var/i in spent) - var/datum/timedevent/qtimer = i - if(QDELETED(qtimer)) - bucket_count++ - continue - if(!(qtimer.flags & TIMER_LOOP)) - qdel(qtimer) - else - bucket_count++ - qtimer.spent = 0 - qtimer.bucketEject() - if(qtimer.flags & TIMER_CLIENT_TIME) - qtimer.timeToRun = REALTIMEOFDAY + qtimer.wait - else - qtimer.timeToRun = world.time + qtimer.wait - qtimer.bucketJoin() + for (var/spent_timer in spent) + qdel(spent_timer) spent.len = 0 -//formated this way to be runtime resistant /datum/controller/subsystem/timer/proc/get_timer_debug_string(datum/timedevent/TE) . = "Timer: [TE]" . += "Prev: [TE.prev ? TE.prev : "NULL"], Next: [TE.next ? TE.next : "NULL"]" if(TE.spent) - . += ", SPENT([TE.spent])" + . += ", SPENT" if(QDELETED(TE)) . += ", QDELETED" - if(!TE.callBack) - . += ", NO CALLBACK" -/datum/controller/subsystem/timer/proc/reset_buckets() +/datum/controller/subsystem/timer/proc/shift_buckets() var/list/bucket_list = src.bucket_list var/list/alltimers = list() //collect the timers currently in the bucket @@ -256,37 +165,33 @@ SUBSYSTEM_DEF(timer) head_offset = world.time bucket_resolution = world.tick_lag - alltimers += second_queue + alltimers += processing if (!length(alltimers)) return - sortTim(alltimers, .proc/cmp_timer) + sortTim(alltimers, /proc/cmp_timer) var/datum/timedevent/head = alltimers[1] if (head.timeToRun < head_offset) head_offset = head.timeToRun - var/new_bucket_count - var/i = 1 - for (i in 1 to length(alltimers)) - var/datum/timedevent/timer = alltimers[1] + var/list/timers_to_remove = list() + + for (var/thing in alltimers) + var/datum/timedevent/timer = thing if (!timer) + timers_to_remove += timer continue var/bucket_pos = BUCKET_POS(timer) - if (timer.timeToRun >= TIMER_MAX) - i-- + if (bucket_pos > BUCKET_LEN) break - + timers_to_remove += timer //remove it from the big list once we are done if (!timer.callBack || timer.spent) - WARNING("Invalid timer: [get_timer_debug_string(timer)] world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset]") - if (timer.callBack) - qdel(timer) continue - - new_bucket_count++ + bucket_count++ var/datum/timedevent/bucket_head = bucket_list[bucket_pos] if (!bucket_head) bucket_list[bucket_pos] = timer @@ -300,14 +205,12 @@ SUBSYSTEM_DEF(timer) timer.prev = bucket_head.prev timer.next.prev = timer timer.prev.next = timer - if (i) - alltimers.Cut(1, i+1) - second_queue = alltimers - bucket_count = new_bucket_count + + processing = (alltimers - timers_to_remove) /datum/controller/subsystem/timer/Recover() - second_queue |= SStimer.second_queue + processing |= SStimer.processing hashes |= SStimer.hashes timer_id_dict |= SStimer.timer_id_dict bucket_list |= SStimer.bucket_list @@ -316,130 +219,73 @@ SUBSYSTEM_DEF(timer) var/id var/datum/callback/callBack var/timeToRun - var/wait var/hash var/list/flags - var/spent = 0 //time we ran the timer. + var/spent = FALSE //set to true right before running. var/name //for easy debugging. + //cicular doublely linked list var/datum/timedevent/next var/datum/timedevent/prev -/datum/timedevent/New(datum/callback/callBack, wait, flags, hash) var/static/nextid = 1 + +/datum/timedevent/New(datum/callback/callBack, timeToRun, flags, hash) id = TIMER_ID_NULL src.callBack = callBack - src.wait = wait + src.timeToRun = timeToRun src.flags = flags src.hash = hash - if (flags & TIMER_CLIENT_TIME) - timeToRun = REALTIMEOFDAY + wait - else - timeToRun = world.time + wait - if (flags & TIMER_UNIQUE) SStimer.hashes[hash] = src - if (flags & TIMER_STOPPABLE) - id = num2text(nextid, 100) - if (nextid >= SHORT_REAL_LIMIT) - nextid += min(1, 2**round(nextid/SHORT_REAL_LIMIT)) - else - nextid++ - SStimer.timer_id_dict[id] = src + do + if (nextid >= TIMER_ID_MAX) + nextid = 1 + id = nextid++ + while(SStimer.timer_id_dict["timerid" + num2text(id, 8)]) + SStimer.timer_id_dict["timerid" + num2text(id, 8)] = src - name = "Timer: [id] (\ref[src]), TTR: [timeToRun], Flags: [jointext(bitfield2list(flags, list("TIMER_UNIQUE", "TIMER_OVERRIDE", "TIMER_CLIENT_TIME", "TIMER_STOPPABLE", "TIMER_NO_HASH_WAIT", "TIMER_LOOP")), ", ")], callBack: \ref[callBack], callBack.object: [callBack.object]\ref[callBack.object]([getcallingtype()]), callBack.delegate:[callBack.delegate]([callBack.arguments ? callBack.arguments.Join(", ") : ""])" + name = "Timer: " + num2text(id, 8) + ", TTR: [timeToRun], Flags: [jointext(bitfield2list(flags, list("TIMER_UNIQUE", "TIMER_OVERRIDE", "TIMER_CLIENT_TIME", "TIMER_STOPPABLE", "TIMER_NO_HASH_WAIT")), ", ")], callBack: \ref[callBack], callBack.object: [callBack.object]\ref[callBack.object]([getcallingtype()]), callBack.delegate:[callBack.delegate]([callBack.arguments ? callBack.arguments.Join(", ") : ""])" - if ((timeToRun < world.time || timeToRun < SStimer.head_offset) && !(flags & TIMER_CLIENT_TIME)) - CRASH("Invalid timer state: Timer created that would require a backtrack to run (addtimer would never let this happen): [SStimer.get_timer_debug_string(src)]") - - if (callBack.object != GLOBAL_PROC && !QDESTROYING(callBack.object)) + if (callBack.object != GLOBAL_PROC) LAZYADD(callBack.object.active_timers, src) - bucketJoin() - -/datum/timedevent/Destroy() - ..() - if (flags & TIMER_UNIQUE && hash) - SStimer.hashes -= hash - - if (callBack && callBack.object && callBack.object != GLOBAL_PROC && callBack.object.active_timers) - callBack.object.active_timers -= src - UNSETEMPTY(callBack.object.active_timers) - - callBack = null - - if (flags & TIMER_STOPPABLE) - SStimer.timer_id_dict -= id - if (flags & TIMER_CLIENT_TIME) - if (!spent) - spent = world.time - SStimer.clienttime_timers -= src - return QDEL_HINT_IWILLGC - - if (!spent) - spent = world.time - bucketEject() - else - if (prev && prev.next == src) - prev.next = next - if (next && next.prev == src) - next.prev = prev - next = null - prev = null - return QDEL_HINT_IWILLGC - -/datum/timedevent/proc/bucketEject() - var/bucketpos = BUCKET_POS(src) - var/list/bucket_list = SStimer.bucket_list - var/list/second_queue = SStimer.second_queue - var/datum/timedevent/buckethead - if(bucketpos > 0) - buckethead = bucket_list[bucketpos] - if(buckethead == src) - bucket_list[bucketpos] = next - SStimer.bucket_count-- - else if(timeToRun < TIMER_MAX || next || prev) - SStimer.bucket_count-- - else - var/l = length(second_queue) - second_queue -= src - if(l == length(second_queue)) - SStimer.bucket_count-- - if(prev != next) - prev.next = next - next.prev = prev - else - prev?.next = null - next?.prev = null - prev = next = null - -/datum/timedevent/proc/bucketJoin() - var/list/L - - if (flags & TIMER_CLIENT_TIME) - L = SStimer.clienttime_timers - else if (timeToRun >= TIMER_MAX) - L = SStimer.second_queue - - if(L) - BINARY_INSERT(src, L, datum/timedevent, timeToRun) + //sorted insert + var/list/ctts = SStimer.clienttime_timers + var/cttl = length(ctts) + if(cttl) + var/datum/timedevent/Last = ctts[cttl] + if(Last.timeToRun >= timeToRun) + ctts += src + else if(cttl > 1) + for(var/I in cttl to 1) + var/datum/timedevent/E = ctts[I] + if(E.timeToRun <= timeToRun) + ctts.Insert(src, I) + break + else + ctts += src return //get the list of buckets var/list/bucket_list = SStimer.bucket_list - //calculate our place in the bucket list var/bucket_pos = BUCKET_POS(src) - + //we are too far aways from needing to run to be in the bucket list, shift_buckets() will handle us. + if (bucket_pos > length(bucket_list)) + SStimer.processing += src + return //get the bucket for our tick var/datum/timedevent/bucket_head = bucket_list[bucket_pos] SStimer.bucket_count++ //empty bucket, we will just add ourselves if (!bucket_head) bucket_list[bucket_pos] = src + if (bucket_pos < SStimer.practical_offset) + SStimer.practical_offset = bucket_pos return //other wise, lets do a simplified linked list add. if (!bucket_head.prev) @@ -449,27 +295,68 @@ SUBSYSTEM_DEF(timer) next.prev = src prev.next = src +/datum/timedevent/Destroy() + ..() + if (flags & TIMER_UNIQUE) + SStimer.hashes -= hash + + + if (callBack && callBack.object && callBack.object != GLOBAL_PROC && callBack.object.active_timers) + callBack.object.active_timers -= src + UNSETEMPTY(callBack.object.active_timers) + + callBack = null + + if (flags & TIMER_STOPPABLE) + SStimer.timer_id_dict -= "timerid[id]" + + if (flags & TIMER_CLIENT_TIME) + SStimer.clienttime_timers -= src + return QDEL_HINT_IWILLGC + + if (!spent) + if (prev == next && next) + next.prev = null + prev.next = null + else + if (prev) + prev.next = next + if (next) + next.prev = prev + + var/bucketpos = BUCKET_POS(src) + var/datum/timedevent/buckethead + var/list/bucket_list = SStimer.bucket_list + + if (bucketpos > 0 && bucketpos <= length(bucket_list)) + buckethead = bucket_list[bucketpos] + SStimer.bucket_count-- + else + SStimer.processing -= src + + if (buckethead == src) + bucket_list[bucketpos] = next + else + if (prev && prev.next == src) + prev.next = next + if (next && next.prev == src) + next.prev = prev + next = null + prev = null + return QDEL_HINT_IWILLGC + /datum/timedevent/proc/getcallingtype() . = "ERROR" if (callBack.object == GLOBAL_PROC) - . = "GLOBAL_PROC" + . = "GLOBAL PROC" else . = "[callBack.object.type]" -/proc/addtimer(datum/callback/callback, wait = 0, flags = 0) +/proc/addtimer(datum/callback/callback, wait, flags) if (!callback) - CRASH("addtimer called without a callback") + return - if (wait < 0) - crash_with("addtimer called with a negative wait. Converting to [world.tick_lag]") - - if (callback.object != GLOBAL_PROC && QDELETED(callback.object) && !QDESTROYING(callback.object)) - crash_with("addtimer called with a callback assigned to a qdeleted object. In the future such timers will not be supported and may refuse to run or run with a 0 wait") - - wait = max(CEILING(wait, world.tick_lag), world.tick_lag) - - if(wait >= INFINITY) - CRASH("Attempted to create timer with INFINITY delay") + wait = max(wait, 0) var/hash @@ -485,32 +372,36 @@ SUBSYSTEM_DEF(timer) var/datum/timedevent/hash_timer = SStimer.hashes[hash] if(hash_timer) if (hash_timer.spent) //it's pending deletion, pretend it doesn't exist. - hash_timer.hash = null //but keep it from accidentally deleting us + hash_timer.hash = null + SStimer.hashes -= hash else + if (flags & TIMER_OVERRIDE) - hash_timer.hash = null //no need having it delete it's hash if we are going to replace it qdel(hash_timer) else if (hash_timer.flags & TIMER_STOPPABLE) . = hash_timer.id return - else if(flags & TIMER_OVERRIDE) - crash_with("TIMER_OVERRIDE used without TIMER_UNIQUE") - var/datum/timedevent/timer = new(callback, wait, flags, hash) - return timer.id + + var/timeToRun = world.time + wait + if (flags & TIMER_CLIENT_TIME) + timeToRun = REALTIMEOFDAY + wait + + var/datum/timedevent/timer = new(callback, timeToRun, flags, hash) + if (flags & TIMER_STOPPABLE) + return timer.id /proc/deltimer(id) if (!id) return FALSE if (id == TIMER_ID_NULL) - CRASH("Tried to delete a null timerid. Use TIMER_STOPPABLE flag") + CRASH("Tried to delete a null timerid. Use the TIMER_STOPPABLE flag.") if (!istext(id)) if (istype(id, /datum/timedevent)) qdel(id) return TRUE - //id is string - var/datum/timedevent/timer = SStimer.timer_id_dict[id] + var/datum/timedevent/timer = SStimer.timer_id_dict["timerid[id]"] if (timer && !timer.spent) qdel(timer) return TRUE @@ -519,5 +410,4 @@ SUBSYSTEM_DEF(timer) #undef BUCKET_LEN #undef BUCKET_POS -#undef TIMER_MAX #undef TIMER_ID_MAX diff --git a/code/modules/client/preference_setup/general/03_body.dm b/code/modules/client/preference_setup/general/03_body.dm index cda61be1b3..6f35aff05f 100644 --- a/code/modules/client/preference_setup/general/03_body.dm +++ b/code/modules/client/preference_setup/general/03_body.dm @@ -383,6 +383,16 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O if(!pref.species_preview || !(pref.species_preview in all_species)) return TOPIC_NOACTION + var/datum/species/setting_species + + if(all_species[href_list["set_species"]]) + setting_species = all_species[href_list["set_species"]] + else + return TOPIC_NOACTION + + if(((!(setting_species.spawn_flags & SPECIES_CAN_JOIN)) || (!is_alien_whitelisted(preference_mob(),setting_species))) && !check_rights(R_ADMIN, 0)) + return TOPIC_NOACTION + var/prev_species = pref.species pref.species = href_list["set_species"] if(prev_species != pref.species)