Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Runechat - stop bugging me for gods sake #14141

Merged
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
3 changes: 3 additions & 0 deletions code/__DEFINES/layers.dm
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@
#define MASSIVE_OBJ_LAYER 11
#define POINT_LAYER 12

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

#define LIGHTING_PLANE 15
#define LIGHTING_LAYER 15

Expand Down
5 changes: 3 additions & 2 deletions code/__DEFINES/preferences.dm
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@
#define PREFTOGGLE_2_WINDOWFLASHING 8
#define PREFTOGGLE_2_ANONDCHAT 16
#define PREFTOGGLE_2_AFKWATCH 32
#define PREFTOGGLE_2_RUNECHAT 64

#define TOGGLES_2_TOTAL 63 // If you add or remove a preference toggle above, make sure you update this define with the total value of the toggles combined.
#define TOGGLES_2_TOTAL 127 // If you add or remove a preference toggle above, make sure you update this define with the total value of the toggles combined.

#define TOGGLES_2_DEFAULT (PREFTOGGLE_2_FANCYUI|PREFTOGGLE_2_ITEMATTACK|PREFTOGGLE_2_WINDOWFLASHING)
#define TOGGLES_2_DEFAULT (PREFTOGGLE_2_FANCYUI|PREFTOGGLE_2_ITEMATTACK|PREFTOGGLE_2_WINDOWFLASHING|PREFTOGGLE_2_RUNECHAT)

// Sanity checks
#if TOGGLES_TOTAL > 16777215
Expand Down
1 change: 1 addition & 0 deletions code/__DEFINES/subsystems.dm
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
#define FIRE_PRIORITY_MOBS 100
#define FIRE_PRIORITY_NANOUI 110
#define FIRE_PRIORITY_TICKER 200
#define FIRE_PRIORITY_RUNECHAT 410 // I hate how high the fire priority on this is -aa
#define FIRE_PRIORITY_OVERLAYS 500
#define FIRE_PRIORITY_INPUT 1000 // This must always always be the max highest priority. Player input must never be lost.

Expand Down
3 changes: 3 additions & 0 deletions code/__HELPERS/lists.dm
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,9 @@ proc/dd_sortedObjectList(list/incoming)
// Lazying Episode 3
#define LAZYSET(L, K, V) LAZYINITLIST(L); L[K] = V;

#define LAZYADDASSOC(L, K, V) if(!L) { L = list(); } L[K] += list(V);
#define LAZYREMOVEASSOC(L, K, V) if(L) { if(L[K]) { L[K] -= V; if(!length(L[K])) L -= K; } if(!length(L)) L = null; }

/// Returns whether a numerical index is within a given list's bounds. Faster than isnull(LAZYACCESS(L, I)).
#define ISINDEXSAFE(L, I) (I >= 1 && I <= length(L))

Expand Down
58 changes: 58 additions & 0 deletions code/__HELPERS/text.dm
Original file line number Diff line number Diff line change
Expand Up @@ -615,3 +615,61 @@
text = replacetext(text, "<td>", "\[cell\]")
text = replacetext(text, "<img src = ntlogo.png>", "\[logo\]")
return text

/datum/html/split_holder
var/list/opening
var/inner_text
var/list/closing

/datum/html/split_holder/New()
opening = list()
inner_text = ""
closing = list()

/proc/split_html(raw_text="")
// gently borrowed and re-purposed from code/modules/pda/utilities.dm
// define a datum to hold our result
var/datum/html/split_holder/s = new()

// copy the raw_text to get started
var/text = copytext_char(raw_text, 1)

// search for tag brackets
var/tag_start = findtext_char(text, "<")
var/tag_stop = findtext_char(text, ">")

// until we run out of opening tags
while((tag_start != 0) && (tag_stop != 0))
// if the tag isn't at the beginning of the string
if(tag_start > 1)
// we've found our text, so copy it out
s.inner_text = copytext_char(text, 1, tag_start)
// and chop the text for the next round
text = copytext_char(text, tag_start)
break
// otherwise, we found an opening tag, so add it to the list
var/tag = copytext_char(text, tag_start, tag_stop+1)
s.opening.Add(tag)
// and chop the text for the next round
text = copytext_char(text, tag_stop+1)
// look for the next tag in what's left
tag_start = findtext(text, "<")
tag_stop = findtext(text, ">")

// search for tag brackets
tag_start = findtext(text, "<")
tag_stop = findtext(text, ">")

// until we run out of closing tags
while((tag_start != 0) && (tag_stop != 0))
// we found a closing tag, so add it to the list
var/tag = copytext_char(text, tag_start, tag_stop+1)
s.closing.Add(tag)
// and chop the text for the next round
text = copytext_char(text, tag_stop+1)
// look for the next tag in what's left
tag_start = findtext(text, "<")
tag_stop = findtext(text, ">")

// return the split html object to the caller
return s
233 changes: 233 additions & 0 deletions code/controllers/subsystem/runechat.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
/// 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. Made by Bobbahbrown of /tg/station13
*
* 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.
*
* AA Note:
* One of the primary reasons for this is because each chatmessage has a timer attached to it, which is extra load on the GC
* At 150 population, the GC literally cannot keep up with processing 368,000 runechats and 368,000 extra timers in a 1 hour 30 minute round
* This also makes performance profiling a lot easier.
*
*/
SUBSYSTEM_DEF(runechat)
name = "Runechat"
flags = SS_TICKER | SS_NO_INIT
wait = 1
priority = FIRE_PRIORITY_RUNECHAT
offline_implications = "Runechat messages will no longer clear. Shuttle call recommended."

/// 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)
..("ActMsgs:[bucket_count] SecQueue:[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

// 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

/**
* 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, scheduled_destruction)
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

#undef BUCKET_LEN
#undef BUCKET_POS
#undef BUCKET_LIMIT
Loading