mirror of
https://github.com/PolarisSS13/Polaris.git
synced 2025-12-17 05:31:53 +00:00
Merge pull request #8722 from Spookerton/spkrtn/sys/sstyping
Adds SSTyping
This commit is contained in:
@@ -1,3 +1,12 @@
|
|||||||
// Consider these images/atoms as part of the UI/HUD
|
// Consider these images/atoms as part of the UI/HUD
|
||||||
#define APPEARANCE_UI_IGNORE_ALPHA RESET_COLOR|RESET_TRANSFORM|NO_CLIENT_COLOR|RESET_ALPHA
|
#define APPEARANCE_UI_IGNORE_ALPHA RESET_COLOR|RESET_TRANSFORM|NO_CLIENT_COLOR|RESET_ALPHA
|
||||||
#define APPEARANCE_UI RESET_COLOR|RESET_TRANSFORM|NO_CLIENT_COLOR
|
#define APPEARANCE_UI RESET_COLOR|RESET_TRANSFORM|NO_CLIENT_COLOR
|
||||||
|
|
||||||
|
/// The atom is not clickable
|
||||||
|
#define XMOUSE_OPACITY_NEVER 0
|
||||||
|
|
||||||
|
/// The atom is clickable normally
|
||||||
|
#define XMOUSE_OPACITY_DEFAULT 1
|
||||||
|
|
||||||
|
/// The atom steals clicks from other clickables
|
||||||
|
#define XMOUSE_OPACITY_ALWAYS 2
|
||||||
|
|||||||
234
code/controllers/subsystems/typing.dm
Normal file
234
code/controllers/subsystems/typing.dm
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
SUBSYSTEM_DEF(typing)
|
||||||
|
name = "Typing"
|
||||||
|
flags = SS_BACKGROUND | SS_NO_INIT
|
||||||
|
runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME
|
||||||
|
wait = 0.5 SECONDS
|
||||||
|
|
||||||
|
/// The skin control to poll for TYPING_STATE_INPUT status.
|
||||||
|
var/const/INPUT_HANDLE = "mainwindow.input"
|
||||||
|
|
||||||
|
var/const/INFLIGHT_TIMEOUT = 5 SECONDS
|
||||||
|
|
||||||
|
/// The status entry index of the related client's typing indicator visibility preference.
|
||||||
|
var/const/INDEX_PREFERENCE = 1
|
||||||
|
|
||||||
|
/// The status entry index of the inflight state.
|
||||||
|
var/const/INDEX_INFLIGHT = 2
|
||||||
|
|
||||||
|
/// The status entry index of the timeout threshold.
|
||||||
|
var/const/INDEX_TIMEOUT = 3
|
||||||
|
|
||||||
|
/// The status entry index of the input bar typing state.
|
||||||
|
var/const/INDEX_INPUT_STATE = 4
|
||||||
|
|
||||||
|
/// The status entry index of the verb input typing state.
|
||||||
|
var/const/INDEX_VERB_STATE = 5
|
||||||
|
|
||||||
|
/// The highest index in a status entry.
|
||||||
|
var/const/MAX_INDEX = 5
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A list of (ckey = list(
|
||||||
|
preference = 0|1,
|
||||||
|
inflight = 0|1,
|
||||||
|
timeout = num,
|
||||||
|
istyping_input = 0|1,
|
||||||
|
istyping_hotkey = 0|1
|
||||||
|
), ...)
|
||||||
|
See .proc/GetEntry for details.
|
||||||
|
*/
|
||||||
|
var/static/list/status = list()
|
||||||
|
|
||||||
|
/// Matches input bar verbs that should set TYPING_STATE_INPUT.
|
||||||
|
var/static/regex/match_verbs = regex("^(Me|Say) +\"?\\w+")
|
||||||
|
|
||||||
|
/// A list of clients waiting to be polled for input state.
|
||||||
|
var/static/list/client/queue = list()
|
||||||
|
|
||||||
|
|
||||||
|
/datum/controller/subsystem/typing/Recover()
|
||||||
|
status = list()
|
||||||
|
queue = list()
|
||||||
|
|
||||||
|
|
||||||
|
/datum/controller/subsystem/typing/fire(resumed, no_mc_tick)
|
||||||
|
if (!resumed)
|
||||||
|
queue = list()
|
||||||
|
for (var/client/client as anything in GLOB.clients)
|
||||||
|
queue += client
|
||||||
|
if (!length(queue))
|
||||||
|
return
|
||||||
|
var/cut_until = 1
|
||||||
|
var/list/entry
|
||||||
|
for (var/client/client as anything in queue)
|
||||||
|
++cut_until
|
||||||
|
if (QDELETED(client))
|
||||||
|
continue
|
||||||
|
entry = GetEntry(client)
|
||||||
|
if (!entry[INDEX_PREFERENCE])
|
||||||
|
continue
|
||||||
|
if (!entry[INDEX_INFLIGHT])
|
||||||
|
UpdateInputState(client, entry)
|
||||||
|
else if (world.time < entry[INDEX_TIMEOUT])
|
||||||
|
entry[INDEX_INFLIGHT] = FALSE
|
||||||
|
if (no_mc_tick)
|
||||||
|
CHECK_TICK
|
||||||
|
else if (MC_TICK_CHECK)
|
||||||
|
queue.Cut(1, cut_until)
|
||||||
|
return
|
||||||
|
queue.Cut()
|
||||||
|
|
||||||
|
|
||||||
|
/// Return, generating if necessary, a ckey-indexed list holding typing status.
|
||||||
|
/datum/controller/subsystem/typing/proc/GetEntry(client/client)
|
||||||
|
PRIVATE_PROC(TRUE)
|
||||||
|
var/ckey
|
||||||
|
if (istext(client))
|
||||||
|
ckey = client
|
||||||
|
else if (istype(client))
|
||||||
|
ckey = client.ckey
|
||||||
|
else
|
||||||
|
return
|
||||||
|
var/list/entry = status[ckey]
|
||||||
|
if (!entry)
|
||||||
|
entry = new (MAX_INDEX)
|
||||||
|
entry[INDEX_PREFERENCE] = client.is_preference_enabled(/datum/client_preference/show_typing_indicator)
|
||||||
|
entry[INDEX_INFLIGHT] = FALSE
|
||||||
|
entry[INDEX_TIMEOUT] = world.time
|
||||||
|
entry[INDEX_INPUT_STATE] = FALSE
|
||||||
|
entry[INDEX_VERB_STATE] = FALSE
|
||||||
|
status[ckey] = entry
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
/// Updates client's preference bool for whether typing indicators should be shown.
|
||||||
|
/datum/controller/subsystem/typing/proc/UpdatePreference(client/client, preference)
|
||||||
|
var/list/entry = GetEntry(client)
|
||||||
|
entry[INDEX_PREFERENCE] = preference
|
||||||
|
UpdateIndicator(client, entry)
|
||||||
|
|
||||||
|
|
||||||
|
/// Updates client|ckey's verb typing state to new_state.
|
||||||
|
/datum/controller/subsystem/typing/proc/UpdateVerbState(client/client, state)
|
||||||
|
var/list/entry = GetEntry(client)
|
||||||
|
entry[INDEX_VERB_STATE] = state
|
||||||
|
UpdateIndicator(client, entry)
|
||||||
|
|
||||||
|
|
||||||
|
/// Request client's input bar state using winget and updating entry accordingly.
|
||||||
|
/datum/controller/subsystem/typing/proc/UpdateInputState(client/client, list/entry)
|
||||||
|
PRIVATE_PROC(TRUE)
|
||||||
|
set waitfor = FALSE
|
||||||
|
var/timeout = world.time + INFLIGHT_TIMEOUT
|
||||||
|
entry[INDEX_INFLIGHT] = TRUE
|
||||||
|
entry[INDEX_TIMEOUT] = timeout
|
||||||
|
var/content = winget(client, INPUT_HANDLE, "text")
|
||||||
|
if (timeout != entry[INDEX_TIMEOUT]) // We're stale. Touch nothing.
|
||||||
|
return
|
||||||
|
entry[INDEX_INFLIGHT] = FALSE
|
||||||
|
if (QDELETED(client) || !isliving(client.mob))
|
||||||
|
return
|
||||||
|
entry[INDEX_INPUT_STATE] = match_verbs.Find(content) != 0
|
||||||
|
UpdateIndicator(client, entry)
|
||||||
|
|
||||||
|
|
||||||
|
/// Attempt to update the mob's typing state and indicator according to new state.
|
||||||
|
/datum/controller/subsystem/typing/proc/UpdateIndicator(client/client, list/entry)
|
||||||
|
PRIVATE_PROC(TRUE)
|
||||||
|
var/mob/target = client.mob
|
||||||
|
var/display = target.stat == CONSCIOUS && entry[INDEX_PREFERENCE] && (entry[INDEX_INPUT_STATE] || entry[INDEX_VERB_STATE])
|
||||||
|
if (display == target.is_typing)
|
||||||
|
return
|
||||||
|
if (display)
|
||||||
|
if (!target.typing_indicator)
|
||||||
|
target.typing_indicator = new (null, target)
|
||||||
|
target.typing_indicator.icon_state = "[target.speech_bubble_appearance()]_typing"
|
||||||
|
target.typing_indicator.pixel_y = target.icon_expected_height - 32
|
||||||
|
target.vis_contents += target.typing_indicator
|
||||||
|
target.is_typing = TRUE
|
||||||
|
if (target.shadow)
|
||||||
|
target.vis_contents += target.typing_indicator
|
||||||
|
else
|
||||||
|
if (target.typing_indicator)
|
||||||
|
target.vis_contents -= target.typing_indicator
|
||||||
|
target.is_typing = FALSE
|
||||||
|
if (target.shadow)
|
||||||
|
target.vis_contents -= target.typing_indicator
|
||||||
|
|
||||||
|
|
||||||
|
/atom/movable/typing_indicator
|
||||||
|
icon = 'icons/mob/talk.dmi'
|
||||||
|
icon_state = "typing"
|
||||||
|
plane = PLANE_LIGHTING_ABOVE
|
||||||
|
mouse_opacity = XMOUSE_OPACITY_NEVER
|
||||||
|
simulated = FALSE
|
||||||
|
anchored = TRUE
|
||||||
|
|
||||||
|
var/mob/owner
|
||||||
|
|
||||||
|
|
||||||
|
/atom/movable/typing_indicator/Destroy()
|
||||||
|
if (owner)
|
||||||
|
owner.vis_contents -= src
|
||||||
|
owner.typing_indicator = null
|
||||||
|
owner = null
|
||||||
|
return ..()
|
||||||
|
|
||||||
|
|
||||||
|
/atom/movable/typing_indicator/Initialize(mapload, mob/_owner)
|
||||||
|
. = ..()
|
||||||
|
if (!istype(_owner))
|
||||||
|
return INITIALIZE_HINT_QDEL
|
||||||
|
owner = _owner
|
||||||
|
|
||||||
|
|
||||||
|
/// If this mob is or was piloted by a player with typing indicators enabled, an instance of one.
|
||||||
|
/mob/var/atom/movable/typing_indicator/typing_indicator
|
||||||
|
|
||||||
|
|
||||||
|
/// Whether this mob is currently typing, if piloted by a player.
|
||||||
|
/mob/var/is_typing
|
||||||
|
|
||||||
|
|
||||||
|
/mob/Destroy()
|
||||||
|
QDEL_NULL(typing_indicator)
|
||||||
|
return ..()
|
||||||
|
|
||||||
|
|
||||||
|
/mob/Logout()
|
||||||
|
if (typing_indicator)
|
||||||
|
vis_contents -= typing_indicator
|
||||||
|
is_typing = FALSE
|
||||||
|
..()
|
||||||
|
|
||||||
|
|
||||||
|
/mob/verb/say_wrapper()
|
||||||
|
set name = ".Say"
|
||||||
|
set hidden = TRUE
|
||||||
|
SStyping.UpdateVerbState(client, TRUE)
|
||||||
|
var/message = input("","say (text)") as null | text
|
||||||
|
SStyping.UpdateVerbState(client, FALSE)
|
||||||
|
if (message)
|
||||||
|
say_verb(message)
|
||||||
|
|
||||||
|
|
||||||
|
/mob/verb/me_wrapper()
|
||||||
|
set name = ".Me"
|
||||||
|
set hidden = TRUE
|
||||||
|
SStyping.UpdateVerbState(client, TRUE)
|
||||||
|
var/message = input("","me (text)") as null | text
|
||||||
|
SStyping.UpdateVerbState(client, FALSE)
|
||||||
|
if (message)
|
||||||
|
me_verb(message)
|
||||||
|
|
||||||
|
|
||||||
|
// Retained for uses by non-player assets
|
||||||
|
/proc/generate_speech_bubble(atom/movable/loc, icon_state, layer = FLOAT_LAYER)
|
||||||
|
var/image/image = image('icons/mob/talk.dmi', loc, icon_state, layer)
|
||||||
|
image.appearance_flags |= KEEP_APART | RESET_COLOR | PIXEL_SCALE
|
||||||
|
if (ismovable(loc))
|
||||||
|
var/x_scale = loc.get_icon_scale_x()
|
||||||
|
if (abs(x_scale) < 2) // reset transform on bubbles, except for the Very Large
|
||||||
|
image.pixel_z = (loc.icon_expected_height * (x_scale - 1))
|
||||||
|
image.appearance_flags |= RESET_TRANSFORM
|
||||||
|
return image
|
||||||
@@ -178,8 +178,7 @@ var/global/list/_client_preferences_by_type
|
|||||||
disabled_description = "Hide"
|
disabled_description = "Hide"
|
||||||
|
|
||||||
/datum/client_preference/show_typing_indicator/toggled(var/mob/preference_mob, var/enabled)
|
/datum/client_preference/show_typing_indicator/toggled(var/mob/preference_mob, var/enabled)
|
||||||
if(!enabled)
|
SStyping.UpdatePreference(preference_mob.client, enabled)
|
||||||
preference_mob.set_typing_indicator(FALSE)
|
|
||||||
|
|
||||||
/datum/client_preference/show_ooc
|
/datum/client_preference/show_ooc
|
||||||
description ="OOC chat"
|
description ="OOC chat"
|
||||||
|
|||||||
@@ -43,23 +43,3 @@
|
|||||||
|
|
||||||
/mob/living/carbon/brain/isSynthetic()
|
/mob/living/carbon/brain/isSynthetic()
|
||||||
return istype(loc, /obj/item/mmi)
|
return istype(loc, /obj/item/mmi)
|
||||||
|
|
||||||
/mob/living/carbon/brain/set_typing_indicator(var/state)
|
|
||||||
if(isturf(loc))
|
|
||||||
return ..()
|
|
||||||
|
|
||||||
if(!is_preference_enabled(/datum/client_preference/show_typing_indicator))
|
|
||||||
loc.cut_overlay(typing_indicator, TRUE)
|
|
||||||
return
|
|
||||||
|
|
||||||
if(!typing_indicator)
|
|
||||||
init_typing_indicator("[speech_bubble_appearance()]_typing")
|
|
||||||
|
|
||||||
if(state && !typing)
|
|
||||||
loc.add_overlay(typing_indicator, TRUE)
|
|
||||||
typing = TRUE
|
|
||||||
else if(typing)
|
|
||||||
loc.cut_overlay(typing_indicator, TRUE)
|
|
||||||
typing = FALSE
|
|
||||||
|
|
||||||
return state
|
|
||||||
|
|||||||
@@ -212,9 +212,6 @@
|
|||||||
|
|
||||||
var/get_rig_stats = 0 //Moved from computer.dm
|
var/get_rig_stats = 0 //Moved from computer.dm
|
||||||
|
|
||||||
var/typing
|
|
||||||
var/obj/effect/decal/typing_indicator
|
|
||||||
|
|
||||||
var/low_priority = FALSE //Skip processing life() if there's just no players on this Z-level
|
var/low_priority = FALSE //Skip processing life() if there's just no players on this Z-level
|
||||||
|
|
||||||
var/default_pixel_x = 0 //For offsetting mobs
|
var/default_pixel_x = 0 //For offsetting mobs
|
||||||
@@ -225,4 +222,4 @@
|
|||||||
|
|
||||||
var/registered_z
|
var/registered_z
|
||||||
|
|
||||||
var/in_enclosed_vehicle = 0 //For mechs and fighters ambiance. Can be used in other cases.
|
var/in_enclosed_vehicle = 0 //For mechs and fighters ambiance. Can be used in other cases.
|
||||||
|
|||||||
@@ -1,35 +1,32 @@
|
|||||||
/mob/proc/say(var/message, var/datum/language/speaking = null, var/whispering = 0)
|
/mob/proc/say(var/message, var/datum/language/speaking = null, var/whispering = 0)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
/mob/verb/whisper(message as text)
|
/mob/verb/whisper(message as text)
|
||||||
set name = "Whisper"
|
set name = "Whisper"
|
||||||
set category = "IC"
|
set category = "IC"
|
||||||
|
usr.say(message, whispering = TRUE)
|
||||||
|
|
||||||
usr.say(message,whispering=1)
|
|
||||||
|
|
||||||
/mob/verb/say_verb(message as text)
|
/mob/verb/say_verb(message as text)
|
||||||
set name = "Say"
|
set name = "Say"
|
||||||
set category = "IC"
|
set category = "IC"
|
||||||
|
|
||||||
set_typing_indicator(FALSE)
|
|
||||||
usr.say(message)
|
usr.say(message)
|
||||||
|
|
||||||
|
|
||||||
/mob/verb/me_verb(message as message)
|
/mob/verb/me_verb(message as message)
|
||||||
set name = "Me"
|
set name = "Me"
|
||||||
set category = "IC"
|
set category = "IC"
|
||||||
|
|
||||||
if(say_disabled) //This is here to try to identify lag problems
|
if(say_disabled) //This is here to try to identify lag problems
|
||||||
to_chat(usr, "<font color='red'>Speech is currently admin-disabled.</font>")
|
to_chat(usr, SPAN_WARNING("Speech is currently admin-disabled."))
|
||||||
return
|
return
|
||||||
|
|
||||||
message = sanitize(message)
|
message = sanitize(message)
|
||||||
|
|
||||||
set_typing_indicator(FALSE)
|
|
||||||
if(use_me)
|
if(use_me)
|
||||||
custom_emote(usr.emote_type, message)
|
custom_emote(usr.emote_type, message)
|
||||||
else
|
else
|
||||||
usr.emote(message)
|
usr.emote(message)
|
||||||
|
|
||||||
|
|
||||||
/mob/proc/say_dead(var/message)
|
/mob/proc/say_dead(var/message)
|
||||||
if(say_disabled) //This is here to try to identify lag problems
|
if(say_disabled) //This is here to try to identify lag problems
|
||||||
to_chat(usr, "<span class='danger'>Speech is currently admin-disabled.</span>")
|
to_chat(usr, "<span class='danger'>Speech is currently admin-disabled.</span>")
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
/proc/generate_speech_bubble(var/bubble_loc, var/speech_state, var/set_layer = FLOAT_LAYER)
|
|
||||||
var/image/I = image('icons/mob/talk.dmi', bubble_loc, speech_state, set_layer)
|
|
||||||
I.appearance_flags |= (KEEP_APART|RESET_COLOR|PIXEL_SCALE)
|
|
||||||
if(istype(bubble_loc, /atom/movable))
|
|
||||||
var/atom/movable/AM = bubble_loc
|
|
||||||
var/x_scale = AM.get_icon_scale_x()
|
|
||||||
if(abs(x_scale) < 2) // reset transform on bubbles, except for the Very Large
|
|
||||||
I.pixel_z = (AM.icon_expected_height * (x_scale-1))
|
|
||||||
I.appearance_flags |= RESET_TRANSFORM
|
|
||||||
return I
|
|
||||||
|
|
||||||
/mob/proc/init_typing_indicator(var/set_state = "typing")
|
|
||||||
typing_indicator = new
|
|
||||||
typing_indicator.appearance = generate_speech_bubble(null, set_state)
|
|
||||||
typing_indicator.appearance_flags |= (KEEP_APART|RESET_COLOR|RESET_TRANSFORM|PIXEL_SCALE)
|
|
||||||
|
|
||||||
/mob/proc/set_typing_indicator(var/state) //Leaving this here for mobs.
|
|
||||||
|
|
||||||
if(!is_preference_enabled(/datum/client_preference/show_typing_indicator))
|
|
||||||
if(typing_indicator)
|
|
||||||
cut_overlay(typing_indicator, TRUE)
|
|
||||||
return
|
|
||||||
|
|
||||||
if(!typing_indicator)
|
|
||||||
init_typing_indicator("[speech_bubble_appearance()]_typing")
|
|
||||||
|
|
||||||
if(state && !typing)
|
|
||||||
add_overlay(typing_indicator, TRUE)
|
|
||||||
typing = TRUE
|
|
||||||
else if(typing)
|
|
||||||
cut_overlay(typing_indicator, TRUE)
|
|
||||||
typing = FALSE
|
|
||||||
|
|
||||||
if(shadow) //Multi-Z above-me shadows
|
|
||||||
shadow.set_typing_indicator(state)
|
|
||||||
|
|
||||||
return state
|
|
||||||
|
|
||||||
/mob/verb/say_wrapper()
|
|
||||||
set name = ".Say"
|
|
||||||
set hidden = 1
|
|
||||||
|
|
||||||
set_typing_indicator(TRUE)
|
|
||||||
var/message = input("","say (text)") as text
|
|
||||||
set_typing_indicator(FALSE)
|
|
||||||
|
|
||||||
if(message)
|
|
||||||
say_verb(message)
|
|
||||||
|
|
||||||
/mob/verb/me_wrapper()
|
|
||||||
set name = ".Me"
|
|
||||||
set hidden = 1
|
|
||||||
|
|
||||||
set_typing_indicator(TRUE)
|
|
||||||
var/message = input("","me (text)") as text
|
|
||||||
set_typing_indicator(FALSE)
|
|
||||||
|
|
||||||
if(message)
|
|
||||||
me_verb(message)
|
|
||||||
@@ -113,15 +113,3 @@ INITIALIZE_IMMEDIATE(/mob/zshadow)
|
|||||||
. = ..()
|
. = ..()
|
||||||
if(shadow)
|
if(shadow)
|
||||||
shadow.set_dir(new_dir)
|
shadow.set_dir(new_dir)
|
||||||
|
|
||||||
/mob/zshadow/set_typing_indicator(var/state)
|
|
||||||
if(!typing_indicator)
|
|
||||||
init_typing_indicator("typing")
|
|
||||||
if(state && !typing)
|
|
||||||
add_overlay(typing_indicator)
|
|
||||||
typing = 1
|
|
||||||
else if(!state && typing)
|
|
||||||
cut_overlay(typing_indicator)
|
|
||||||
typing = 0
|
|
||||||
if(shadow)
|
|
||||||
shadow.set_typing_indicator(state)
|
|
||||||
|
|||||||
@@ -248,6 +248,7 @@
|
|||||||
#include "code\controllers\subsystems\ticker.dm"
|
#include "code\controllers\subsystems\ticker.dm"
|
||||||
#include "code\controllers\subsystems\time_track.dm"
|
#include "code\controllers\subsystems\time_track.dm"
|
||||||
#include "code\controllers\subsystems\timer.dm"
|
#include "code\controllers\subsystems\timer.dm"
|
||||||
|
#include "code\controllers\subsystems\typing.dm"
|
||||||
#include "code\controllers\subsystems\vote.dm"
|
#include "code\controllers\subsystems\vote.dm"
|
||||||
#include "code\controllers\subsystems\webhooks.dm"
|
#include "code\controllers\subsystems\webhooks.dm"
|
||||||
#include "code\controllers\subsystems\xenoarch.dm"
|
#include "code\controllers\subsystems\xenoarch.dm"
|
||||||
@@ -2205,7 +2206,6 @@
|
|||||||
#include "code\modules\mob\say.dm"
|
#include "code\modules\mob\say.dm"
|
||||||
#include "code\modules\mob\skillset.dm"
|
#include "code\modules\mob\skillset.dm"
|
||||||
#include "code\modules\mob\transform_procs.dm"
|
#include "code\modules\mob\transform_procs.dm"
|
||||||
#include "code\modules\mob\typing_indicator.dm"
|
|
||||||
#include "code\modules\mob\update_icons.dm"
|
#include "code\modules\mob\update_icons.dm"
|
||||||
#include "code\modules\mob\_modifiers\aura.dm"
|
#include "code\modules\mob\_modifiers\aura.dm"
|
||||||
#include "code\modules\mob\_modifiers\changeling.dm"
|
#include "code\modules\mob\_modifiers\changeling.dm"
|
||||||
|
|||||||
Reference in New Issue
Block a user