Skip to content
This repository was archived by the owner on May 22, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions code/__DEFINES/MC.dm
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@
}\
/datum/controller/subsystem/##X

#define TIMER_SUBSYSTEM_DEF(X) GLOBAL_REAL(SS##X, /datum/controller/subsystem/timer/##X);\
/datum/controller/subsystem/timer/##X/New(){\
NEW_SS_GLOBAL(SS##X);\
PreInit();\
}\
/datum/controller/subsystem/timer/##X/fire() {..() /*just so it shows up on the profiler*/} \
/datum/controller/subsystem/timer/##X

#define PROCESSING_SUBSYSTEM_DEF(X) GLOBAL_REAL(SS##X, /datum/controller/subsystem/processing/##X);\
/datum/controller/subsystem/processing/##X/New(){\
NEW_SS_GLOBAL(SS##X);\
Expand Down
5 changes: 5 additions & 0 deletions code/__DEFINES/_tick.dm
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
/// runs stoplag if tick_usage is above the limit
#define CHECK_TICK ( TICK_CHECK ? stoplag() : 0 )

/// Checks if a sleeping proc is running before or after the master controller
#define RUNNING_BEFORE_MASTER ( Master.last_run != null && Master.last_run != world.time )
/// Returns true if a verb ought to yield to the MC (IE: queue up to be processed by a subsystem)
#define VERB_SHOULD_YIELD ( TICK_CHECK || RUNNING_BEFORE_MASTER )

/// Returns true if tick usage is above 95, for high priority usage
#define TICK_CHECK_HIGH_PRIORITY ( TICK_USAGE > 95 )
/// runs stoplag if tick_usage is above 95, for high priority usage
Expand Down
3 changes: 3 additions & 0 deletions code/__DEFINES/chat.dm
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,8 @@
#define MESSAGE_TYPE_MENTORPM "mentorpm"
#define MESSAGE_TYPE_DONATOR "donator"

/// Max length of chat message in characters
#define CHAT_MESSAGE_MAX_LENGTH 110

/// Adds a generic box around whatever message you're sending in chat. Really makes things stand out.
#define examine_block(str) ("<div class='examine_block'>" + str + "</div>")
7 changes: 7 additions & 0 deletions code/__DEFINES/fonts.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Font metrics bitfield
/// Include leading A width and trailing C width in GetWidth() or in DrawText()
#define INCLUDE_AC (1<<0)

DEFINE_BITFIELD(font_flags, list(
"INCLUDE_AC" = INCLUDE_AC,
))
3 changes: 0 additions & 3 deletions code/__DEFINES/layers.dm
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,6 @@
#define EMISSIVE_BLOCKER_LAYER 12
#define EMISSIVE_BLOCKER_RENDER_TARGET "*EMISSIVE_BLOCKER_PLANE"

#define CHAT_LAYER 12.0001 // Do not insert layers between these two values
#define CHAT_LAYER_MAX 12.9999

#define EMISSIVE_PLANE 13
#define EMISSIVE_LAYER 13
#define EMISSIVE_RENDER_TARGET "*EMISSIVE_PLANE"
Expand Down
21 changes: 19 additions & 2 deletions code/__DEFINES/text.dm
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
/// Prepares a text to be used for maptext. Use this so it doesn't look hideous.
#define MAPTEXT(text) {"<span class='maptext'>[##text]</span>"}

/// Macro from Lummox used to get height from a MeasureText proc
#define WXH_TO_HEIGHT(x) text2num(copytext(x, findtextEx(x, "x") + 1))
/// Prepares a text to be used for maptext, using a variable size font.
/// Variable size font. More flexible but doesn't scale pixel perfect to BYOND icon resolutions. (May be blurry.) Can use any size in pt or px.
#define MAPTEXT_VCR_OSD_MONO(text) {"<span style='font-family: \"VCR OSD Mono\"'>[##text]</span>"}

/// Prepares a text to be used for maptext using a pixel font. Cleaner but less size choices.
/// Standard size (ie: normal runechat) Use only sizing pt, multiples of 6: 6pt 12pt 18pt 24pt etc. - Not for use with px sizing
#define MAPTEXT_GRAND9K(text) {"<span style='font-family: \"Grand9K Pixel\"'>[##text]</span>"}

/// Prepares a text to be used for maptext using a pixel font. Cleaner but less size choices.
/// Small size. (ie: whisper runechat) Use only size pt, multiples of 12: 12pt 24pt 48pt etc. - Not for use with px sizing
#define MAPTEXT_TINY_UNICODE(text) {"<span style='font-family: \"TinyUnicode\"'>[##text]</span>"}

/// Macro from Lummox used to get height from a MeasureText proc.
/// resolves the MeasureText() return value once, then resolves the height, then sets return_var to that.
#define WXH_TO_HEIGHT(measurement, return_var) \
do { \
var/_measurement = measurement; \
return_var = text2num(copytext(_measurement, findtextEx(_measurement, "x") + 1)); \
} while(FALSE);

/*
* Uses MAPTEXT to format antag points into a more appealing format
Expand Down
2 changes: 2 additions & 0 deletions code/__DEFINES/traits.dm
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@
#define TRAIT_CRITICAL_CONDITION "critical-condition"
/// Is frozen in place
#define TRAIT_FROZEN "frozen"
/// Is runechat for this atom/movable currently disabled, regardless of prefs or anything?
#define TRAIT_RUNECHAT_HIDDEN "runechat_hidden"
/// trait associated to a stat value or range of
#define STAT_TRAIT "stat"
#define TRAIT_INCAPACITATED "incapacitated"
Expand Down
1 change: 1 addition & 0 deletions code/_globalvars/traits.dm
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
GLOBAL_LIST_INIT(traits_by_type, list(
/mob = list(/atom/movable = list(
"TRAIT_MOVE_PHASING" = TRAIT_MOVE_PHASING,
"TRAIT_RUNECHAT_HIDDEN" = TRAIT_RUNECHAT_HIDDEN,
))))
/// value -> trait name, generated on use from trait_by_type global
GLOBAL_LIST(trait_name_map)
Expand Down
249 changes: 10 additions & 239 deletions code/controllers/subsystem/runechat.dm
Original file line number Diff line number Diff line change
@@ -1,243 +1,14 @@
/// Controls how many buckets should be kept, each representing a tick. (30 seconds worth)
#define BUCKET_LEN (world.fps * 1 * 30)
/// Helper for getting the correct bucket for a given chatmessage
#define BUCKET_POS(scheduled_destruction) (((round((scheduled_destruction - SSrunechat.head_offset) / world.tick_lag) + 1) % BUCKET_LEN) || BUCKET_LEN)
/// Gets the maximum time at which messages will be handled in buckets, used for deferring to secondary queue
#define BUCKET_LIMIT (world.time + TICKS2DS(min(BUCKET_LEN - (SSrunechat.practical_offset - DS2TICKS(world.time - SSrunechat.head_offset)) - 1, BUCKET_LEN - 1)))

/**
* # Runechat Subsystem
*
* Maintains a timer-like system to handle destruction of runechat messages. Much of this code is modeled
* after or adapted from the timer subsystem.
*
* Note that this has the same structure for storing and queueing messages as the timer subsystem does
* for handling timers: the bucket_list is a list of chatmessage datums, each of which are the head
* of a circularly linked list. Any given index in bucket_list could be null, representing an empty bucket.
*/
SUBSYSTEM_DEF(runechat)
TIMER_SUBSYSTEM_DEF(runechat)
name = "Runechat"
flags = SS_TICKER | SS_NO_INIT
wait = 1
priority = FIRE_PRIORITY_RUNECHAT

/// world.time of the first entry in the bucket list, effectively the 'start time' of the current buckets
var/head_offset = 0
/// Index of the first non-empty bucket
var/practical_offset = 1
/// world.tick_lag the bucket was designed for
var/bucket_resolution = 0
/// How many messages are in the buckets
var/bucket_count = 0
/// List of buckets, each bucket holds every message that has to be killed that byond tick
var/list/bucket_list = list()
/// Queue used for storing messages that are scheduled for deletion too far in the future for the buckets
var/list/datum/chatmessage/second_queue = list()

/datum/controller/subsystem/runechat/PreInit()
bucket_list.len = BUCKET_LEN
head_offset = world.time
bucket_resolution = world.tick_lag

/datum/controller/subsystem/runechat/stat_entry(msg)
msg += "ActMsgs:[bucket_count] SecQueue:[length(second_queue)]"
return msg

/datum/controller/subsystem/runechat/get_metrics()
. = ..()
.["buckets"] = bucket_count
.["second_queue"] = length(second_queue)

/datum/controller/subsystem/runechat/fire(resumed = FALSE)
// Store local references to datum vars as it is faster to access them this way
var/list/bucket_list = src.bucket_list

if (MC_TICK_CHECK)
return

// Check for when we need to loop the buckets, this occurs when
// the head_offset is approaching BUCKET_LEN ticks in the past
if (practical_offset > BUCKET_LEN)
head_offset += TICKS2DS(BUCKET_LEN)
practical_offset = 1
resumed = FALSE

// Check for when we have to reset buckets, typically from auto-reset
if ((length(bucket_list) != BUCKET_LEN) || (world.tick_lag != bucket_resolution))
reset_buckets()
bucket_list = src.bucket_list
resumed = FALSE

// Store a reference to the 'working' chatmessage so that we can resume if the MC
// has us stop mid-way through processing
var/static/datum/chatmessage/cm
if (!resumed)
cm = null

// Iterate through each bucket starting from the practical offset
while (practical_offset <= BUCKET_LEN && head_offset + ((practical_offset - 1) * world.tick_lag) <= world.time)
var/datum/chatmessage/bucket_head = bucket_list[practical_offset]
if (!cm || !bucket_head || cm == bucket_head)
bucket_head = bucket_list[practical_offset]
cm = bucket_head

while (cm)
// If the chatmessage hasn't yet had its life ended then do that now
var/datum/chatmessage/next = cm.next
if (!cm.eol_complete)
cm.end_of_life()
else if (!QDELETED(cm)) // otherwise if we haven't deleted it yet, do so (this is after EOL completion)
qdel(cm)

if (MC_TICK_CHECK)
return

// Break once we've processed the entire bucket
cm = next
if (cm == bucket_head)
break

// Empty the bucket, check if anything in the secondary queue should be shifted to this bucket
bucket_list[practical_offset++] = null
var/i = 0
for (i in 1 to length(second_queue))
cm = second_queue[i]
if (cm.scheduled_destruction >= BUCKET_LIMIT)
i--
break

// Transfer the message into the bucket, performing necessary circular doubly-linked list operations
bucket_count++
var/bucket_pos = max(1, BUCKET_POS(cm.scheduled_destruction))
var/datum/timedevent/head = bucket_list[bucket_pos]
if (!head)
bucket_list[bucket_pos] = cm
cm.next = null
cm.prev = null
continue

if (!head.prev)
head.prev = head
cm.next = head
cm.prev = head.prev
cm.next.prev = cm
cm.prev.next = cm
if (i)
second_queue.Cut(1, i + 1)
cm = null

/datum/controller/subsystem/runechat/Recover()
bucket_list |= SSrunechat.bucket_list
second_queue |= SSrunechat.second_queue

/datum/controller/subsystem/runechat/proc/reset_buckets()
bucket_list.len = BUCKET_LEN
head_offset = world.time
bucket_resolution = world.tick_lag

/**
* Enters the runechat subsystem with this chatmessage, inserting it into the end-of-life queue
*
* This will also account for a chatmessage already being registered, and in which case
* the position will be updated to remove it from the previous location if necessary
*
* Arguments:
* * new_sched_destruction Optional, when provided is used to update an existing message with the new specified time
*/
/datum/chatmessage/proc/enter_subsystem(new_sched_destruction = 0)
// Get local references from subsystem as they are faster to access than the datum references
var/list/bucket_list = SSrunechat.bucket_list
var/list/second_queue = SSrunechat.second_queue

// When necessary, de-list the chatmessage from its previous position
if (new_sched_destruction)
if (scheduled_destruction >= BUCKET_LIMIT)
second_queue -= src
else
SSrunechat.bucket_count--
var/bucket_pos = BUCKET_POS(scheduled_destruction)
if (bucket_pos > 0)
var/datum/chatmessage/bucket_head = bucket_list[bucket_pos]
if (bucket_head == src)
bucket_list[bucket_pos] = next
if (prev != next)
prev.next = next
next.prev = prev
else
prev?.next = null
next?.prev = null
prev = next = null
scheduled_destruction = new_sched_destruction

// Ensure the scheduled destruction time is properly bound to avoid missing a scheduled event
scheduled_destruction = max(CEILING(scheduled_destruction, world.tick_lag), world.time + world.tick_lag)

// Handle insertion into the secondary queue if the required time is outside our tracked amounts
if (scheduled_destruction >= BUCKET_LIMIT)
BINARY_INSERT(src, SSrunechat.second_queue, /datum/chatmessage, src, scheduled_destruction, COMPARE_KEY)
return

// Get bucket position and a local reference to the datum var, it's faster to access this way
var/bucket_pos = BUCKET_POS(scheduled_destruction)

// Get the bucket head for that bucket, increment the bucket count
var/datum/chatmessage/bucket_head = bucket_list[bucket_pos]
SSrunechat.bucket_count++

// If there is no existing head of this bucket, we can set this message to be that head
if (!bucket_head)
bucket_list[bucket_pos] = src
return

// Otherwise it's a simple insertion into the circularly doubly-linked list
if (!bucket_head.prev)
bucket_head.prev = bucket_head
next = bucket_head
prev = bucket_head.prev
next.prev = src
prev.next = src


/**
* Removes this chatmessage datum from the runechat subsystem
*/
/datum/chatmessage/proc/leave_subsystem()
// Attempt to find the bucket that contains this chat message
var/bucket_pos = BUCKET_POS(scheduled_destruction)

// Get local references to the subsystem's vars, faster than accessing on the datum
var/list/bucket_list = SSrunechat.bucket_list
var/list/second_queue = SSrunechat.second_queue

// Attempt to get the head of the bucket
var/datum/chatmessage/bucket_head
if (bucket_pos > 0)
bucket_head = bucket_list[bucket_pos]

// Decrement the number of messages in buckets if the message is
// the head of the bucket, or has a SD less than BUCKET_LIMIT implying it fits
// into an existing bucket, or is otherwise not present in the secondary queue
if(bucket_head == src)
bucket_list[bucket_pos] = next
SSrunechat.bucket_count--
else if(scheduled_destruction < BUCKET_LIMIT)
SSrunechat.bucket_count--
else
var/l = length(second_queue)
second_queue -= src
if(l == length(second_queue))
SSrunechat.bucket_count--

// Remove the message from the bucket, ensuring to maintain
// the integrity of the bucket's list if relevant
if(prev != next)
prev.next = next
next.prev = prev
else
prev?.next = null
next?.prev = null
prev = next = null
var/list/datum/callback/message_queue = list()

#undef BUCKET_LEN
#undef BUCKET_POS
#undef BUCKET_LIMIT
/datum/controller/subsystem/timer/runechat/fire(resumed)
. = ..() //poggers
while(message_queue.len)
var/datum/callback/queued_message = message_queue[message_queue.len]
queued_message.Invoke()
message_queue.len--
if(MC_TICK_CHECK)
return
Loading