mirror of
https://github.com/VOREStation/VOREStation.git
synced 2026-08-23 12:08:28 +01:00
TGUI Say (#16160)
* TGUI Say * Add icon_ref_map.json to make tgui-dev-server stop screaming * Update tgui.bundle.js * bundle recompile --------- Co-authored-by: Heroman3003 <31296024+Heroman3003@users.noreply.github.com> Co-authored-by: Heroman <alesha3000@list.ru>
This commit is contained in:
co-authored by
Heroman3003
Heroman
parent
bd1d79aebf
commit
7a7920f29e
Vendored
+16
@@ -46,6 +46,22 @@
|
||||
],
|
||||
"group": "build",
|
||||
"label": "tgui: prettybuild"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "bin/tgui --dev",
|
||||
"windows": {
|
||||
"command": ".\\bin\\tgui.bat --dev"
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$tsc",
|
||||
"$eslint-stylish"
|
||||
],
|
||||
"options": {
|
||||
"cwd": ".\\tgui\\",
|
||||
},
|
||||
"group": "build",
|
||||
"label": "tgui: dev server"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
#undef ASSERT
|
||||
|
||||
/// Override BYOND's native ASSERT to optionally specify a message
|
||||
#define ASSERT(condition, message...) \
|
||||
if (!(condition)) { \
|
||||
CRASH(assertion_message(__FILE__, __LINE__, #condition, ##message)) \
|
||||
}
|
||||
|
||||
/proc/assertion_message(file, line, condition, message)
|
||||
if (!isnull(message))
|
||||
message = " - [message]"
|
||||
|
||||
return "[file]:[line]:Assertion failed: [condition][message]"
|
||||
@@ -0,0 +1,9 @@
|
||||
// Used to direct channels to speak into.
|
||||
#define SAY_CHANNEL "Say"
|
||||
#define RADIO_CHANNEL "Radio"
|
||||
#define ME_CHANNEL "Me"
|
||||
#define OOC_CHANNEL "OOC"
|
||||
#define ADMIN_CHANNEL "Admin"
|
||||
#define LOOC_CHANNEL "LOOC"
|
||||
#define WHIS_CHANNEL "Whis"
|
||||
#define SUBTLE_CHANNEL "Subtle"
|
||||
@@ -0,0 +1,122 @@
|
||||
#define SIGNAL_ADDTRAIT(trait_ref) "addtrait [trait_ref]"
|
||||
#define SIGNAL_REMOVETRAIT(trait_ref) "removetrait [trait_ref]"
|
||||
|
||||
// trait accessor defines
|
||||
#define ADD_TRAIT(target, trait, source) \
|
||||
do { \
|
||||
var/list/_L; \
|
||||
if (!target._status_traits) { \
|
||||
target._status_traits = list(); \
|
||||
_L = target._status_traits; \
|
||||
_L[trait] = list(source); \
|
||||
SEND_SIGNAL(target, SIGNAL_ADDTRAIT(trait), trait); \
|
||||
} else { \
|
||||
_L = target._status_traits; \
|
||||
if (_L[trait]) { \
|
||||
_L[trait] |= list(source); \
|
||||
} else { \
|
||||
_L[trait] = list(source); \
|
||||
SEND_SIGNAL(target, SIGNAL_ADDTRAIT(trait), trait); \
|
||||
} \
|
||||
} \
|
||||
} while (0)
|
||||
#define REMOVE_TRAIT(target, trait, sources) \
|
||||
do { \
|
||||
var/list/_L = target._status_traits; \
|
||||
var/list/_S; \
|
||||
if (sources && !islist(sources)) { \
|
||||
_S = list(sources); \
|
||||
} else { \
|
||||
_S = sources\
|
||||
}; \
|
||||
if (_L?[trait]) { \
|
||||
for (var/_T in _L[trait]) { \
|
||||
if ((!_S && (_T != ROUNDSTART_TRAIT)) || (_T in _S)) { \
|
||||
_L[trait] -= _T \
|
||||
} \
|
||||
};\
|
||||
if (!length(_L[trait])) { \
|
||||
_L -= trait; \
|
||||
SEND_SIGNAL(target, SIGNAL_REMOVETRAIT(trait), trait); \
|
||||
}; \
|
||||
if (!length(_L)) { \
|
||||
target._status_traits = null \
|
||||
}; \
|
||||
} \
|
||||
} while (0)
|
||||
#define REMOVE_TRAIT_NOT_FROM(target, trait, sources) \
|
||||
do { \
|
||||
var/list/_traits_list = target._status_traits; \
|
||||
var/list/_sources_list; \
|
||||
if (sources && !islist(sources)) { \
|
||||
_sources_list = list(sources); \
|
||||
} else { \
|
||||
_sources_list = sources\
|
||||
}; \
|
||||
if (_traits_list?[trait]) { \
|
||||
for (var/_trait_source in _traits_list[trait]) { \
|
||||
if (!(_trait_source in _sources_list)) { \
|
||||
_traits_list[trait] -= _trait_source \
|
||||
} \
|
||||
};\
|
||||
if (!length(_traits_list[trait])) { \
|
||||
_traits_list -= trait; \
|
||||
SEND_SIGNAL(target, SIGNAL_REMOVETRAIT(trait), trait); \
|
||||
}; \
|
||||
if (!length(_traits_list)) { \
|
||||
target._status_traits = null \
|
||||
}; \
|
||||
} \
|
||||
} while (0)
|
||||
#define REMOVE_TRAITS_NOT_IN(target, sources) \
|
||||
do { \
|
||||
var/list/_L = target._status_traits; \
|
||||
var/list/_S = sources; \
|
||||
if (_L) { \
|
||||
for (var/_T in _L) { \
|
||||
_L[_T] &= _S;\
|
||||
if (!length(_L[_T])) { \
|
||||
_L -= _T; \
|
||||
SEND_SIGNAL(target, SIGNAL_REMOVETRAIT(_T), _T); \
|
||||
}; \
|
||||
};\
|
||||
if (!length(_L)) { \
|
||||
target._status_traits = null\
|
||||
};\
|
||||
}\
|
||||
} while (0)
|
||||
|
||||
#define REMOVE_TRAITS_IN(target, sources) \
|
||||
do { \
|
||||
var/list/_L = target._status_traits; \
|
||||
var/list/_S = sources; \
|
||||
if (sources && !islist(sources)) { \
|
||||
_S = list(sources); \
|
||||
} else { \
|
||||
_S = sources\
|
||||
}; \
|
||||
if (_L) { \
|
||||
for (var/_T in _L) { \
|
||||
_L[_T] -= _S;\
|
||||
if (!length(_L[_T])) { \
|
||||
_L -= _T; \
|
||||
SEND_SIGNAL(target, SIGNAL_REMOVETRAIT(_T)); \
|
||||
}; \
|
||||
};\
|
||||
if (!length(_L)) { \
|
||||
target._status_traits = null\
|
||||
};\
|
||||
}\
|
||||
} while (0)
|
||||
|
||||
#define HAS_TRAIT(target, trait) (target._status_traits?[trait] ? TRUE : FALSE)
|
||||
#define HAS_TRAIT_FROM(target, trait, source) (HAS_TRAIT(target, trait) && (source in target._status_traits[trait]))
|
||||
#define HAS_TRAIT_FROM_ONLY(target, trait, source) (HAS_TRAIT(target, trait) && (source in target._status_traits[trait]) && (length(target._status_traits[trait]) == 1))
|
||||
#define HAS_TRAIT_NOT_FROM(target, trait, source) (HAS_TRAIT(target, trait) && (length(target._status_traits[trait] - source) > 0))
|
||||
/// Returns a list of trait sources for this trait. Only useful for wacko cases and internal futzing
|
||||
/// You should not be using this
|
||||
#define GET_TRAIT_SOURCES(target, trait) (target._status_traits?[trait] || list())
|
||||
/// Returns the amount of sources for a trait. useful if you don't want to have a "thing counter" stuck around all the time
|
||||
#define COUNT_TRAIT_SOURCES(target, trait) length(GET_TRAIT_SOURCES(target, trait))
|
||||
/// A simple helper for checking traits in a mob's mind
|
||||
#define HAS_MIND_TRAIT(target, trait) (HAS_TRAIT(target, trait) || (target.mind ? HAS_TRAIT(target.mind, trait) : FALSE))
|
||||
@@ -0,0 +1,11 @@
|
||||
// This file contains all of the "static" define strings that tie to a trait.
|
||||
// WARNING: The sections here actually matter in this file as it's tested by CI. Please do not toy with the sections."
|
||||
|
||||
// BEGIN TRAIT DEFINES
|
||||
|
||||
/*
|
||||
Remember to update _globalvars/traits.dm if you're adding/removing/renaming traits.
|
||||
*/
|
||||
|
||||
/// Trait given to a mob that is currently thinking (giving off the "thinking" icon), used in an IC context
|
||||
#define TRAIT_THINKING_IN_CHARACTER "currently_thinking_IC"
|
||||
@@ -0,0 +1,7 @@
|
||||
// This file contains all of the trait sources, or all of the things that grant traits.
|
||||
// Several things such as `type` or `REF(src)` may be used in the ADD_TRAIT() macro as the "source", but this file contains all of the defines for immutable static strings.
|
||||
|
||||
/// cannot be removed without admin intervention
|
||||
#define ROUNDSTART_TRAIT "roundstart"
|
||||
/// This trait comes from when a mob is currently typing.
|
||||
#define CURRENTLY_TYPING_TRAIT "currently_typing"
|
||||
@@ -0,0 +1,12 @@
|
||||
// This file should contain every single global trait in the game in a type-based list, as well as any additional trait-related information that's useful to have on a global basis.
|
||||
// This file is used in linting, so make sure to add everything alphabetically and what-not.
|
||||
// Do consider adding your trait entry to the similar list in `admin_tooling.dm` if you want it to be accessible to admins (which is probably the case for 75% of traits).
|
||||
|
||||
// Please do note that there is absolutely no bearing on what traits are added to what subtype of `/datum`, this is just an easily referenceable list sorted by type.
|
||||
// The only thing that truly matters about traits is the code that is built to handle the traits, and where that code is located. Nothing else.
|
||||
|
||||
GLOBAL_LIST_INIT(traits_by_type, list(
|
||||
/mob = list(
|
||||
"TRAIT_THINKING_IN_CHARACTER" = TRAIT_THINKING_IN_CHARACTER,
|
||||
)
|
||||
))
|
||||
@@ -0,0 +1,45 @@
|
||||
#define TRAIT_CALLBACK_ADD(target, trait, source) CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(___TraitAdd), ##target, ##trait, ##source)
|
||||
#define TRAIT_CALLBACK_REMOVE(target, trait, source) CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(___TraitRemove), ##target, ##trait, ##source)
|
||||
|
||||
///DO NOT USE ___TraitAdd OR ___TraitRemove as a replacement for ADD_TRAIT / REMOVE_TRAIT defines. To be used explicitly for callback.
|
||||
/proc/___TraitAdd(target, trait, source)
|
||||
if(!target || !trait || !source)
|
||||
return
|
||||
|
||||
if(islist(target))
|
||||
for(var/datum/listed_target in target)
|
||||
ADD_TRAIT(listed_target, trait, source)
|
||||
return
|
||||
|
||||
ASSERT(isdatum(target), "Invalid target used in TRAIT_CALLBACK_ADD! Expected a datum reference, got [target] instead.")
|
||||
|
||||
var/datum/datum_target = target
|
||||
ADD_TRAIT(datum_target, trait, source)
|
||||
|
||||
///DO NOT USE ___TraitAdd OR ___TraitRemove as a replacement for ADD_TRAIT / REMOVE_TRAIT defines. To be used explicitly for callback.
|
||||
/proc/___TraitRemove(target, trait, source)
|
||||
if(!target || !trait || !source)
|
||||
return
|
||||
|
||||
if(islist(target))
|
||||
for(var/datum/listed_target in target)
|
||||
REMOVE_TRAIT(listed_target, trait, source)
|
||||
return
|
||||
|
||||
ASSERT(isdatum(target), "Invalid target used in TRAIT_CALLBACK_REMOVE! Expected a datum reference, got [target] instead.")
|
||||
|
||||
var/datum/datum_target = target
|
||||
REMOVE_TRAIT(datum_target, trait, source)
|
||||
|
||||
|
||||
/// Proc that handles adding multiple traits to a target via a list. Must have a common source and target.
|
||||
/datum/proc/add_traits(list/list_of_traits, source)
|
||||
ASSERT(islist(list_of_traits), "Invalid arguments passed to add_traits! Invoked on [src] with [list_of_traits], source being [source].")
|
||||
for(var/trait in list_of_traits)
|
||||
ADD_TRAIT(src, trait, source)
|
||||
|
||||
/// Proc that handles removing multiple traits from a target via a list. Must have a common source and target.
|
||||
/datum/proc/remove_traits(list/list_of_traits, source)
|
||||
ASSERT(islist(list_of_traits), "Invalid arguments passed to remove_traits! Invoked on [src] with [list_of_traits], source being [source].")
|
||||
for(var/trait in list_of_traits)
|
||||
REMOVE_TRAIT(src, trait, source)
|
||||
@@ -23,6 +23,8 @@
|
||||
|
||||
/// Active timers with this datum as the target
|
||||
var/list/active_timers
|
||||
/// Status traits attached to this datum. associative list of the form: list(trait name (string) = list(source1, source2, source3,...))
|
||||
var/list/_status_traits
|
||||
|
||||
/**
|
||||
* Components attached to this datum
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/// Maps icon names to ref values
|
||||
/datum/asset/json/icon_ref_map
|
||||
name = "icon_ref_map"
|
||||
early = TRUE
|
||||
|
||||
/datum/asset/json/icon_ref_map/generate()
|
||||
var/list/data = list() //"icons/obj/drinks.dmi" => "[0xc000020]"
|
||||
|
||||
//var/start = "0xc000000"
|
||||
var/value = 0
|
||||
|
||||
while(TRUE)
|
||||
value += 1
|
||||
var/ref = "\[0xc[num2text(value,6,16)]\]"
|
||||
var/mystery_meat = locate(ref)
|
||||
|
||||
if(isicon(mystery_meat))
|
||||
if(!isfile(mystery_meat)) // Ignore the runtime icons for now
|
||||
continue
|
||||
var/path = get_icon_dmi_path(mystery_meat) //Try to get the icon path
|
||||
if(path)
|
||||
data[path] = ref
|
||||
else if(mystery_meat)
|
||||
continue; //Some other non-icon resource, ogg/json/whatever
|
||||
else //Out of resources end this, could also try to end this earlier as soon as runtime generated icons appear but eh
|
||||
break;
|
||||
|
||||
return data
|
||||
@@ -1,12 +1,12 @@
|
||||
/datum/asset/simple/tgui
|
||||
// keep_local_name = TRUE
|
||||
keep_local_name = TRUE
|
||||
assets = list(
|
||||
"tgui.bundle.js" = file("tgui/public/tgui.bundle.js"),
|
||||
"tgui.bundle.css" = file("tgui/public/tgui.bundle.css"),
|
||||
)
|
||||
|
||||
/datum/asset/simple/tgui_panel
|
||||
// keep_local_name = TRUE
|
||||
keep_local_name = TRUE
|
||||
assets = list(
|
||||
"tgui-panel.bundle.js" = file("tgui/public/tgui-panel.bundle.js"),
|
||||
"tgui-panel.bundle.css" = file("tgui/public/tgui-panel.bundle.css"),
|
||||
|
||||
@@ -127,6 +127,9 @@
|
||||
asset_cache_preload_data(href_list["asset_cache_preload_data"])
|
||||
return
|
||||
|
||||
if(href_list["commandbar_typing"])
|
||||
handle_commandbar_typing(href_list)
|
||||
|
||||
switch(href_list["_src_"])
|
||||
if("holder") hsrc = holder
|
||||
if("mentorholder") hsrc = (check_rights(R_ADMIN, 0) ? holder : mentorholder)
|
||||
@@ -179,6 +182,8 @@
|
||||
GLOB.directory[ckey] = src
|
||||
|
||||
// Instantiate tgui panel
|
||||
tgui_say = new(src, "tgui_say")
|
||||
initialize_commandbar_spy()
|
||||
tgui_panel = new(src, "browseroutput")
|
||||
|
||||
GLOB.ahelp_tickets.ClientLogin(src)
|
||||
@@ -211,6 +216,7 @@
|
||||
prefs.selecting_slots = FALSE
|
||||
|
||||
// Initialize tgui panel
|
||||
tgui_say.initialize()
|
||||
tgui_panel.initialize()
|
||||
|
||||
connection_time = world.time
|
||||
|
||||
@@ -226,7 +226,13 @@ var/list/_client_preferences_by_type
|
||||
|
||||
/datum/client_preference/show_typing_indicator/toggled(var/mob/preference_mob, var/enabled)
|
||||
if(!enabled)
|
||||
preference_mob.set_typing_indicator(FALSE)
|
||||
preference_mob.client?.stop_thinking()
|
||||
|
||||
/datum/client_preference/show_typing_indicator_subtle
|
||||
description ="Typing indicator (subtle)"
|
||||
key = "SHOW_TYPING_SUBTLE"
|
||||
enabled_description = "Show"
|
||||
disabled_description = "Hide"
|
||||
|
||||
/datum/client_preference/show_ooc
|
||||
description ="OOC chat"
|
||||
@@ -401,6 +407,19 @@ var/list/_client_preferences_by_type
|
||||
enabled_description = "Automatic"
|
||||
disabled_description = "Manual Only"
|
||||
|
||||
/datum/client_preference/tgui_say
|
||||
description = "TGUI Say: Use TGUI For Say Input"
|
||||
key = "TGUI_SAY"
|
||||
enabled_by_default = TRUE
|
||||
enabled_description = "Yes"
|
||||
disabled_description = "No"
|
||||
|
||||
/datum/client_preference/tgui_say_light
|
||||
description = "TGUI Say: Use Light Mode"
|
||||
key = "TGUI_SAY_LIGHT_MODE"
|
||||
enabled_by_default = FALSE
|
||||
enabled_description = "Yes"
|
||||
disabled_description = "No"
|
||||
|
||||
/********************
|
||||
* Staff Preferences *
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#define IC_VERBS list("say", "me", "whisper", "subtle")
|
||||
|
||||
/client/var/commandbar_thinking = FALSE
|
||||
/client/var/commandbar_typing = FALSE
|
||||
|
||||
/client/proc/initialize_commandbar_spy()
|
||||
src << output('html/typing_indicator.html', "commandbar_spy")
|
||||
|
||||
/client/proc/handle_commandbar_typing(href_list)
|
||||
if(!is_preference_enabled(/datum/client_preference/show_typing_indicator))
|
||||
return
|
||||
|
||||
if(length(href_list["verb"]) < 1 || !(lowertext(href_list["verb"]) in IC_VERBS) || text2num(href_list["argument_length"]) < 1)
|
||||
if(commandbar_typing)
|
||||
commandbar_typing = FALSE
|
||||
stop_typing()
|
||||
|
||||
if(commandbar_thinking)
|
||||
commandbar_thinking = FALSE
|
||||
stop_thinking()
|
||||
return
|
||||
|
||||
if(!commandbar_thinking)
|
||||
commandbar_thinking = TRUE
|
||||
start_thinking(href_list["verb"])
|
||||
|
||||
if(!commandbar_typing)
|
||||
commandbar_typing = TRUE
|
||||
start_typing(href_list["verb"])
|
||||
|
||||
|
||||
/** Sets the mob as "thinking" - with indicator and the TRAIT_THINKING_IN_CHARACTER trait */
|
||||
/client/proc/start_thinking(channel)
|
||||
if(!is_preference_enabled(/datum/client_preference/show_typing_indicator))
|
||||
return FALSE
|
||||
if(channel == "Whis" || channel == "Subtle" || channel == "whisper" || channel == "subtle")
|
||||
if(!is_preference_enabled(/datum/client_preference/show_typing_indicator_subtle))
|
||||
return FALSE
|
||||
ADD_TRAIT(mob, TRAIT_THINKING_IN_CHARACTER, CURRENTLY_TYPING_TRAIT)
|
||||
mob.create_thinking_indicator()
|
||||
|
||||
/** Removes typing/thinking indicators and flags the mob as not thinking */
|
||||
/client/proc/stop_thinking(channel)
|
||||
mob?.remove_all_indicators()
|
||||
|
||||
/**
|
||||
* Handles the user typing. After a brief period of inactivity,
|
||||
* signals the client mob to revert to the "thinking" icon.
|
||||
*/
|
||||
/client/proc/start_typing(channel)
|
||||
var/mob/client_mob = mob
|
||||
client_mob.remove_thinking_indicator()
|
||||
if(!is_preference_enabled(/datum/client_preference/show_typing_indicator) || !HAS_TRAIT(client_mob, TRAIT_THINKING_IN_CHARACTER))
|
||||
return FALSE
|
||||
if(channel == "Whis" || channel == "Subtle" || channel == "whisper" || channel == "subtle")
|
||||
if(!is_preference_enabled(/datum/client_preference/show_typing_indicator_subtle))
|
||||
return FALSE
|
||||
client_mob.create_typing_indicator()
|
||||
addtimer(CALLBACK(src, PROC_REF(stop_typing), channel), 5 SECONDS, TIMER_UNIQUE | TIMER_OVERRIDE | TIMER_STOPPABLE)
|
||||
|
||||
/**
|
||||
* Callback to remove the typing indicator after a brief period of inactivity.
|
||||
* If the user was typing IC, the thinking indicator is shown.
|
||||
*/
|
||||
/client/proc/stop_typing(channel)
|
||||
if(isnull(mob))
|
||||
return FALSE
|
||||
var/mob/client_mob = mob
|
||||
client_mob.remove_typing_indicator()
|
||||
if(!is_preference_enabled(/datum/client_preference/show_typing_indicator) || !HAS_TRAIT(client_mob, TRAIT_THINKING_IN_CHARACTER))
|
||||
return FALSE
|
||||
if(channel == "Whis" || channel == "Subtle" || channel == "whisper" || channel == "subtle")
|
||||
if(!is_preference_enabled(/datum/client_preference/show_typing_indicator_subtle))
|
||||
return FALSE
|
||||
client_mob.create_thinking_indicator()
|
||||
|
||||
#undef IC_VERBS
|
||||
@@ -51,33 +51,6 @@
|
||||
|
||||
return loc
|
||||
|
||||
/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
|
||||
|
||||
var/cur_bubble_appearance = custom_speech_bubble
|
||||
if(!cur_bubble_appearance || cur_bubble_appearance == "default")
|
||||
cur_bubble_appearance = speech_bubble_appearance()
|
||||
if(!typing_indicator || cur_typing_indicator != cur_bubble_appearance)
|
||||
init_typing_indicator("[cur_bubble_appearance]_typing")
|
||||
|
||||
if(state && !typing)
|
||||
add_overlay(typing_indicator, TRUE)
|
||||
typing = TRUE
|
||||
typing_indicator_active = typing_indicator
|
||||
else if(typing)
|
||||
cut_overlay(typing_indicator_active, TRUE)
|
||||
typing = FALSE
|
||||
if(typing_indicator_active != typing_indicator)
|
||||
qdel(typing_indicator_active)
|
||||
typing_indicator_active = null
|
||||
|
||||
return state
|
||||
|
||||
// Vorestation edit start
|
||||
|
||||
/mob/living/carbon/brain/verb/backup_ping()
|
||||
|
||||
@@ -219,10 +219,6 @@
|
||||
|
||||
var/get_rig_stats = 0 //Moved from computer.dm
|
||||
|
||||
var/typing
|
||||
var/obj/effect/decal/typing_indicator
|
||||
var/obj/effect/decal/typing_indicator_active
|
||||
var/cur_typing_indicator
|
||||
var/custom_speech_bubble = "default"
|
||||
|
||||
var/low_priority = FALSE //Skip processing life() if there's just no players on this Z-level
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
/mob/verb/whisper(message as text)
|
||||
set name = "Whisper"
|
||||
set category = "IC"
|
||||
set hidden = 1
|
||||
//VOREStation Addition Start
|
||||
if(forced_psay)
|
||||
psay(message)
|
||||
@@ -15,18 +16,21 @@
|
||||
/mob/verb/say_verb(message as text)
|
||||
set name = "Say"
|
||||
set category = "IC"
|
||||
set hidden = 1
|
||||
//VOREStation Addition Start
|
||||
if(forced_psay)
|
||||
psay(message)
|
||||
return
|
||||
//VOREStation Addition End
|
||||
|
||||
set_typing_indicator(FALSE)
|
||||
client?.stop_thinking()
|
||||
usr.say(message)
|
||||
|
||||
/mob/verb/me_verb(message as message)
|
||||
set name = "Me"
|
||||
set category = "IC"
|
||||
set desc = "Emote to nearby people (and your pred/prey)"
|
||||
set hidden = 1
|
||||
|
||||
if(say_disabled) //This is here to try to identify lag problems
|
||||
to_chat(usr, span_red("Speech is currently admin-disabled."))
|
||||
@@ -45,7 +49,7 @@
|
||||
message = sanitize_or_reflect(message,src) //VOREStation Edit - Reflect too-long messages (within reason)
|
||||
//VOREStation Edit End
|
||||
|
||||
set_typing_indicator(FALSE)
|
||||
client?.stop_thinking()
|
||||
if(use_me)
|
||||
custom_emote(usr.emote_type, message)
|
||||
else
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
set name = "Subtle"
|
||||
set category = "IC"
|
||||
set desc = "Emote to nearby people (and your pred/prey)"
|
||||
set hidden = 1
|
||||
|
||||
if(say_disabled) //This is here to try to identify lag problems
|
||||
to_chat(usr, "Speech is currently admin-disabled.")
|
||||
@@ -18,7 +19,7 @@
|
||||
if(!message)
|
||||
return
|
||||
|
||||
set_typing_indicator(FALSE)
|
||||
client?.stop_thinking()
|
||||
if(use_me)
|
||||
usr.emote_vr("me",4,message)
|
||||
else
|
||||
@@ -40,7 +41,7 @@
|
||||
if(!message)
|
||||
return
|
||||
|
||||
set_typing_indicator(FALSE)
|
||||
client?.stop_thinking()
|
||||
if(use_me)
|
||||
usr.emote_vr("me",4,message,TRUE)
|
||||
else
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/proc/generate_speech_bubble(var/bubble_loc, var/speech_state, var/set_layer = FLOAT_LAYER)
|
||||
/proc/generate_speech_bubble(bubble_loc, speech_state, set_layer = FLOAT_LAYER)
|
||||
var/image/I = image('icons/mob/talk_vr.dmi', bubble_loc, speech_state, set_layer) //VOREStation Edit - talk_vr.dmi instead of talk.dmi for right-side icons
|
||||
I.appearance_flags |= (RESET_COLOR|PIXEL_SCALE) //VOREStation Edit
|
||||
/* //VOREStation Removal Start
|
||||
@@ -11,77 +11,69 @@
|
||||
*/ //VOREStation Removal Start
|
||||
return I
|
||||
|
||||
/mob/proc/init_typing_indicator(var/set_state = "typing")
|
||||
if(typing_indicator)
|
||||
qdel(typing_indicator)
|
||||
typing_indicator = null
|
||||
typing_indicator = new
|
||||
typing_indicator.appearance = generate_speech_bubble(null, set_state)
|
||||
typing_indicator.appearance_flags |= (RESET_COLOR|PIXEL_SCALE) //VOREStation Edit
|
||||
/mob/verb/say_wrapper()
|
||||
set name = "Say verb"
|
||||
set category = "IC"
|
||||
|
||||
/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)
|
||||
if(is_preference_enabled(/datum/client_preference/tgui_say))
|
||||
winset(src, null, "command=[client.tgui_say_create_open_command(SAY_CHANNEL)]")
|
||||
return
|
||||
|
||||
var/cur_bubble_appearance = custom_speech_bubble
|
||||
if(!cur_bubble_appearance || cur_bubble_appearance == "default")
|
||||
cur_bubble_appearance = speech_bubble_appearance()
|
||||
if(!typing_indicator || cur_typing_indicator != cur_bubble_appearance)
|
||||
init_typing_indicator("[cur_bubble_appearance]_typing")
|
||||
|
||||
if(state && !typing)
|
||||
add_overlay(typing_indicator, TRUE)
|
||||
typing = TRUE
|
||||
typing_indicator_active = typing_indicator
|
||||
else if(typing)
|
||||
cut_overlay(typing_indicator_active, TRUE)
|
||||
typing = FALSE
|
||||
if(typing_indicator_active != typing_indicator)
|
||||
qdel(typing_indicator_active)
|
||||
typing_indicator_active = null
|
||||
|
||||
return state
|
||||
|
||||
/mob/verb/say_wrapper()
|
||||
set name = ".Say"
|
||||
set hidden = 1
|
||||
|
||||
set_typing_indicator(TRUE)
|
||||
client?.start_thinking()
|
||||
client?.start_typing()
|
||||
var/message = tgui_input_text(usr, "Type your message:", "Say")
|
||||
set_typing_indicator(FALSE)
|
||||
client?.stop_thinking()
|
||||
|
||||
if(message)
|
||||
say_verb(message)
|
||||
|
||||
/mob/verb/me_wrapper()
|
||||
set name = ".Me"
|
||||
set hidden = 1
|
||||
set name = "Me verb"
|
||||
set category = "IC"
|
||||
|
||||
set_typing_indicator(TRUE)
|
||||
if(is_preference_enabled(/datum/client_preference/tgui_say))
|
||||
winset(src, null, "command=[client.tgui_say_create_open_command(ME_CHANNEL)]")
|
||||
return
|
||||
|
||||
client?.start_thinking()
|
||||
client?.start_typing()
|
||||
var/message = tgui_input_text(usr, "Type your message:", "Emote", multiline = TRUE)
|
||||
set_typing_indicator(FALSE)
|
||||
client?.stop_thinking()
|
||||
|
||||
if(message)
|
||||
me_verb(message)
|
||||
|
||||
// No typing indicators here, but this is the file where the wrappers are, so...
|
||||
/mob/verb/whisper_wrapper()
|
||||
set name = ".Whisper"
|
||||
set hidden = 1
|
||||
set name = "Whisper verb"
|
||||
set category = "IC"
|
||||
|
||||
if(is_preference_enabled(/datum/client_preference/tgui_say))
|
||||
winset(src, null, "command=[client.tgui_say_create_open_command(WHIS_CHANNEL)]")
|
||||
return
|
||||
|
||||
if(is_preference_enabled(/datum/client_preference/show_typing_indicator_subtle))
|
||||
client?.start_thinking()
|
||||
client?.start_typing()
|
||||
var/message = tgui_input_text(usr, "Type your message:", "Whisper")
|
||||
client?.stop_thinking()
|
||||
|
||||
if(message)
|
||||
whisper(message)
|
||||
|
||||
/mob/verb/subtle_wrapper()
|
||||
set name = ".Subtle"
|
||||
set hidden = 1
|
||||
set name = "Subtle verb"
|
||||
set category = "IC"
|
||||
set desc = "Emote to nearby people (and your pred/prey)"
|
||||
|
||||
if(is_preference_enabled(/datum/client_preference/tgui_say))
|
||||
winset(src, null, "command=[client.tgui_say_create_open_command(SUBTLE_CHANNEL)]")
|
||||
return
|
||||
|
||||
if(is_preference_enabled(/datum/client_preference/show_typing_indicator_subtle))
|
||||
client?.start_thinking()
|
||||
client?.start_typing()
|
||||
var/message = tgui_input_text(usr, "Type your message:", "Subtle", multiline = TRUE)
|
||||
client?.stop_thinking()
|
||||
|
||||
if(message)
|
||||
me_verb_subtle(message)
|
||||
|
||||
@@ -126,6 +126,8 @@
|
||||
/datum/asset/simple/namespaced/fontawesome))
|
||||
flush_queue |= window.send_asset(get_asset_datum(
|
||||
/datum/asset/simple/namespaced/tgfont))
|
||||
flush_queue |= window.send_asset(get_asset_datum(
|
||||
/datum/asset/json/icon_ref_map))
|
||||
for(var/datum/asset/asset in src_object.ui_assets(user))
|
||||
flush_queue |= window.send_asset(asset)
|
||||
if (flush_queue)
|
||||
|
||||
@@ -119,11 +119,11 @@
|
||||
html = replacetextEx(html, "<!-- tgui:inline-html -->", inline_html)
|
||||
// Inject inline JS
|
||||
if (inline_js)
|
||||
inline_js = "<script>\n'use strict';\n[inline_js]\n</script>"
|
||||
inline_js = "<script>\n'use strict';\n[isfile(inline_js) ? file2text(inline_js) : inline_js]\n</script>"
|
||||
html = replacetextEx(html, "<!-- tgui:inline-js -->", inline_js)
|
||||
// Inject inline CSS
|
||||
if (inline_css)
|
||||
inline_css = "<style>\n[inline_css]\n</style>"
|
||||
inline_css = "<style>\n[isfile(inline_css) ? file2text(inline_css) : inline_css]\n</style>"
|
||||
html = replacetextEx(html, "<!-- tgui:inline-css -->", inline_css)
|
||||
// Open the window
|
||||
client << browse(html, "window=[id];[options]")
|
||||
@@ -146,6 +146,9 @@
|
||||
inline_html = initial_inline_html,
|
||||
inline_js = initial_inline_js,
|
||||
inline_css = initial_inline_css)
|
||||
// Resend assets
|
||||
for(var/datum/asset/asset in sent_assets)
|
||||
send_asset(asset)
|
||||
|
||||
/**
|
||||
* public
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
/** Assigned say modal of the client */
|
||||
/client/var/datum/tgui_say/tgui_say
|
||||
|
||||
/**
|
||||
* Creates a JSON encoded message to open TGUI say modals properly.
|
||||
*
|
||||
* Arguments:
|
||||
* channel - The channel to open the modal in.
|
||||
* Returns:
|
||||
* string - A JSON encoded message to open the modal.
|
||||
*/
|
||||
/client/proc/tgui_say_create_open_command(channel)
|
||||
var/message = TGUI_CREATE_MESSAGE("open", list(
|
||||
channel = channel,
|
||||
))
|
||||
return "\".output tgui_say.browser:update [message]\""
|
||||
|
||||
/**
|
||||
* The tgui say modal. This initializes an input window which hides until
|
||||
* the user presses one of the speech hotkeys. Once something is entered, it will
|
||||
* delegate the speech to the proper channel.
|
||||
*/
|
||||
/datum/tgui_say
|
||||
/// The user who opened the window
|
||||
var/client/client
|
||||
/// Injury phrases to blurt out
|
||||
var/list/hurt_phrases = list("GACK!", "GLORF!", "OOF!", "AUGH!", "OW!", "URGH!", "HRNK!")
|
||||
/// Max message length
|
||||
var/max_length = MAX_MESSAGE_LEN
|
||||
/// The modal window
|
||||
var/datum/tgui_window/window
|
||||
/// Boolean for whether the tgui_say was opened by the user.
|
||||
var/window_open
|
||||
|
||||
/** Creates the new input window to exist in the background. */
|
||||
/datum/tgui_say/New(client/client, id)
|
||||
src.client = client
|
||||
window = new(client, id)
|
||||
winset(client, "tgui_say", "size=1,1;is-visible=0;")
|
||||
window.subscribe(src, PROC_REF(on_message))
|
||||
window.is_browser = TRUE
|
||||
|
||||
/**
|
||||
* After a brief period, injects the scripts into
|
||||
* the window to listen for open commands.
|
||||
*/
|
||||
/datum/tgui_say/proc/initialize()
|
||||
set waitfor = FALSE
|
||||
// Sleep to defer initialization to after client constructor
|
||||
sleep(3 SECONDS)
|
||||
window.initialize(
|
||||
strict_mode = TRUE,
|
||||
fancy = TRUE,
|
||||
inline_css = file("tgui/public/tgui-say.bundle.css"),
|
||||
inline_js = file("tgui/public/tgui-say.bundle.js"),
|
||||
);
|
||||
|
||||
/**
|
||||
* Ensures nothing funny is going on window load.
|
||||
* Minimizes the window, sets max length, closes all
|
||||
* typing and thinking indicators. This is triggered
|
||||
* as soon as the window sends the "ready" message.
|
||||
*/
|
||||
/datum/tgui_say/proc/load()
|
||||
window_open = FALSE
|
||||
|
||||
winset(client, "tgui_say", "pos=410,400;size=360,30;is-visible=0;")
|
||||
|
||||
window.send_message("props", list(
|
||||
lightMode = client.is_preference_enabled(/datum/client_preference/tgui_say_light),
|
||||
maxLength = max_length,
|
||||
))
|
||||
|
||||
stop_thinking()
|
||||
return TRUE
|
||||
|
||||
/**
|
||||
* Sets the window as "opened" server side, though it is already
|
||||
* visible to the user. We do this to set local vars &
|
||||
* start typing (if enabled and in an IC channel). Logs the event.
|
||||
*
|
||||
* Arguments:
|
||||
* payload - A list containing the channel the window was opened in.
|
||||
*/
|
||||
/datum/tgui_say/proc/open(payload)
|
||||
if(!payload?["channel"])
|
||||
CRASH("No channel provided to an open TGUI-Say")
|
||||
window_open = TRUE
|
||||
if(payload["channel"] != OOC_CHANNEL && payload["channel"] != ADMIN_CHANNEL)
|
||||
start_thinking()
|
||||
return TRUE
|
||||
|
||||
/**
|
||||
* Closes the window serverside. Closes any open chat bubbles
|
||||
* regardless of preference. Logs the event.
|
||||
*/
|
||||
/datum/tgui_say/proc/close()
|
||||
window_open = FALSE
|
||||
stop_thinking()
|
||||
|
||||
/**
|
||||
* The equivalent of ui_act, this waits on messages from the window
|
||||
* and delegates actions.
|
||||
*/
|
||||
/datum/tgui_say/proc/on_message(type, payload)
|
||||
if(type == "ready")
|
||||
load()
|
||||
return TRUE
|
||||
if(type == "open")
|
||||
open(payload)
|
||||
return TRUE
|
||||
if(type == "close")
|
||||
close()
|
||||
return TRUE
|
||||
if(type == "thinking")
|
||||
if(payload["visible"] == TRUE)
|
||||
start_thinking(payload["channel"])
|
||||
return TRUE
|
||||
if(payload["visible"] == FALSE)
|
||||
stop_thinking(payload["channel"])
|
||||
return TRUE
|
||||
return FALSE
|
||||
if(type == "typing")
|
||||
start_typing(payload["channel"])
|
||||
return TRUE
|
||||
if(type == "entry" || type == "force")
|
||||
handle_entry(type, payload)
|
||||
return TRUE
|
||||
return FALSE
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Alters text when players are injured.
|
||||
* Adds text, trims left and right side
|
||||
*
|
||||
* Arguments:
|
||||
* payload - a string list containing entry & channel
|
||||
* Returns:
|
||||
* string - the altered entry
|
||||
*/
|
||||
/datum/tgui_say/proc/alter_entry(payload)
|
||||
var/entry = payload["entry"]
|
||||
/// No OOC leaks
|
||||
if(!entry || payload["channel"] == OOC_CHANNEL || payload["channel"] == ME_CHANNEL || payload["channel"] == LOOC_CHANNEL)
|
||||
return pick(hurt_phrases)
|
||||
/// Random trimming for larger sentences
|
||||
if(length(entry) > 50)
|
||||
entry = trim(entry, rand(40, 50))
|
||||
else
|
||||
/// Otherwise limit trim to just last letter
|
||||
if(length(entry) > 1)
|
||||
entry = trim(entry, length(entry))
|
||||
return entry + "-" + pick(hurt_phrases)
|
||||
|
||||
/**
|
||||
* Delegates the speech to the proper channel.
|
||||
*
|
||||
* Arguments:
|
||||
* entry - the text to broadcast
|
||||
* channel - the channel to broadcast in
|
||||
* Returns:
|
||||
* boolean - on success or failure
|
||||
*/
|
||||
/datum/tgui_say/proc/delegate_speech(entry, channel)
|
||||
switch(channel)
|
||||
if(SAY_CHANNEL)
|
||||
client.mob.say_verb(entry)
|
||||
return TRUE
|
||||
if(RADIO_CHANNEL)
|
||||
client.mob.say_verb(";" + entry)
|
||||
return TRUE
|
||||
if(ME_CHANNEL)
|
||||
client.mob.me_verb(entry)
|
||||
return TRUE
|
||||
if(WHIS_CHANNEL)
|
||||
client.mob.whisper(entry)
|
||||
return TRUE
|
||||
if(SUBTLE_CHANNEL)
|
||||
client.mob.me_verb_subtle(entry)
|
||||
return TRUE
|
||||
if(OOC_CHANNEL)
|
||||
client.ooc(entry)
|
||||
return TRUE
|
||||
if(LOOC_CHANNEL)
|
||||
client.looc(entry)
|
||||
return TRUE
|
||||
if(ADMIN_CHANNEL)
|
||||
if(check_rights(R_ADMIN, show_msg = FALSE))
|
||||
client.cmd_admin_say(entry)
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
/**
|
||||
* Force say handler.
|
||||
* Sends a message to the say modal to send its current value.
|
||||
*/
|
||||
// /datum/tgui_say/proc/force_say()
|
||||
// window.send_message("force")
|
||||
// stop_typing()
|
||||
|
||||
/**
|
||||
* Makes the player force say what's in their current input box.
|
||||
*/
|
||||
// /mob/living/carbon/human/proc/force_say()
|
||||
// if(stat != CONSCIOUS || !client?.tgui_say?.window_open)
|
||||
// return FALSE
|
||||
// client.tgui_say.force_say()
|
||||
// if(client.typing_indicators)
|
||||
// log_speech_indicators("[key_name(client)] FORCED to stop typing, indicators enabled.")
|
||||
// else
|
||||
// log_speech_indicators("[key_name(client)] FORCED to stop typing, indicators DISABLED.")
|
||||
// SEND_SIGNAL(src, COMSIG_HUMAN_FORCESAY)
|
||||
|
||||
/**
|
||||
* Handles text entry and forced speech.
|
||||
*
|
||||
* Arguments:
|
||||
* type - a string "entry" or "force" based on how this function is called
|
||||
* payload - a string list containing entry & channel
|
||||
* Returns:
|
||||
* boolean - success or failure
|
||||
*/
|
||||
/datum/tgui_say/proc/handle_entry(type, payload)
|
||||
if(!payload?["channel"] || !payload["entry"])
|
||||
CRASH("[usr] entered in a null payload to the chat window.")
|
||||
if(length(payload["entry"]) > max_length)
|
||||
CRASH("[usr] has entered more characters than allowed into a TGUI-Say")
|
||||
if(type == "entry")
|
||||
delegate_speech(payload["entry"], payload["channel"])
|
||||
return TRUE
|
||||
if(type == "force")
|
||||
var/target_channel = payload["channel"]
|
||||
if(target_channel == ME_CHANNEL || target_channel == OOC_CHANNEL || target_channel == LOOC_CHANNEL)
|
||||
target_channel = SAY_CHANNEL // No ooc leaks - this is fine because alter_entry will override this completely
|
||||
delegate_speech(alter_entry(payload), target_channel)
|
||||
return TRUE
|
||||
return FALSE
|
||||
@@ -0,0 +1,75 @@
|
||||
/mob
|
||||
///the icon currently used for the typing indicator's bubble
|
||||
var/mutable_appearance/active_typing_indicator
|
||||
///the icon currently used for the thinking indicator's bubble
|
||||
var/mutable_appearance/active_thinking_indicator
|
||||
|
||||
/** Creates a thinking indicator over the mob. Note: Prefs are checked in /client/proc/start_thinking() */
|
||||
/mob/proc/create_thinking_indicator()
|
||||
if(active_thinking_indicator || active_typing_indicator || stat != CONSCIOUS || !HAS_TRAIT(src, TRAIT_THINKING_IN_CHARACTER))
|
||||
return FALSE
|
||||
var/cur_bubble_appearance = custom_speech_bubble
|
||||
if(!cur_bubble_appearance || cur_bubble_appearance == "default")
|
||||
cur_bubble_appearance = speech_bubble_appearance()
|
||||
active_thinking_indicator = mutable_appearance('icons/mob/talk_vr.dmi', "[cur_bubble_appearance]_thinking", FLOAT_LAYER)
|
||||
active_thinking_indicator.appearance_flags |= (RESET_COLOR|PIXEL_SCALE)
|
||||
add_overlay(active_thinking_indicator)
|
||||
|
||||
/** Removes the thinking indicator over the mob. */
|
||||
/mob/proc/remove_thinking_indicator()
|
||||
if(!active_thinking_indicator)
|
||||
return FALSE
|
||||
cut_overlay(active_thinking_indicator)
|
||||
active_thinking_indicator = null
|
||||
|
||||
/** Creates a typing indicator over the mob. Note: Prefs are checked in /client/proc/start_typing() */
|
||||
/mob/proc/create_typing_indicator()
|
||||
if(active_typing_indicator || active_thinking_indicator || stat != CONSCIOUS || !HAS_TRAIT(src, TRAIT_THINKING_IN_CHARACTER))
|
||||
return FALSE
|
||||
var/cur_bubble_appearance = custom_speech_bubble
|
||||
if(!cur_bubble_appearance || cur_bubble_appearance == "default")
|
||||
cur_bubble_appearance = speech_bubble_appearance()
|
||||
active_typing_indicator = mutable_appearance('icons/mob/talk_vr.dmi', "[cur_bubble_appearance]_typing", ABOVE_MOB_LAYER)
|
||||
active_typing_indicator.appearance_flags |= (RESET_COLOR|PIXEL_SCALE)
|
||||
add_overlay(active_typing_indicator)
|
||||
|
||||
/** Removes the typing indicator over the mob. */
|
||||
/mob/proc/remove_typing_indicator()
|
||||
if(!active_typing_indicator)
|
||||
return FALSE
|
||||
cut_overlay(active_typing_indicator)
|
||||
active_typing_indicator = null
|
||||
|
||||
/** Removes any indicators and marks the mob as not speaking IC. */
|
||||
/mob/proc/remove_all_indicators()
|
||||
REMOVE_TRAIT(src, TRAIT_THINKING_IN_CHARACTER, CURRENTLY_TYPING_TRAIT)
|
||||
remove_thinking_indicator()
|
||||
remove_typing_indicator()
|
||||
|
||||
/mob/set_stat(new_stat)
|
||||
. = ..()
|
||||
if(.)
|
||||
remove_all_indicators()
|
||||
|
||||
/mob/Logout()
|
||||
remove_all_indicators()
|
||||
return ..()
|
||||
|
||||
/** Sets the mob as "thinking" - with indicator and the TRAIT_THINKING_IN_CHARACTER trait */
|
||||
/datum/tgui_say/proc/start_thinking(channel)
|
||||
if(!window_open)
|
||||
return FALSE
|
||||
return client.start_thinking(channel)
|
||||
|
||||
/** Removes typing/thinking indicators and flags the mob as not thinking */
|
||||
/datum/tgui_say/proc/stop_thinking(channel)
|
||||
return client.stop_thinking(channel)
|
||||
|
||||
/**
|
||||
* Handles the user typing. After a brief period of inactivity,
|
||||
* signals the client mob to revert to the "thinking" icon.
|
||||
*/
|
||||
/datum/tgui_say/proc/start_typing(channel)
|
||||
if(!window_open)
|
||||
return FALSE
|
||||
return client.start_typing(channel)
|
||||
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
lastseentypedtext = "";
|
||||
function getoutput() {
|
||||
setTimeout(getoutput, 1000);
|
||||
window.location = "byond://winget?callback=checkoutput&id=:Input&property=text";
|
||||
}
|
||||
function checkoutput(props) {
|
||||
if (typeof props !== 'object')
|
||||
return;
|
||||
|
||||
if (typeof props.text !== 'string' && !(props.text instanceof String))
|
||||
return;
|
||||
|
||||
var text = props.text;
|
||||
|
||||
if (text == lastseentypedtext)
|
||||
return;
|
||||
|
||||
lastseentypedtext = text;
|
||||
|
||||
var words = text.split(" ");
|
||||
var verb = words[0];
|
||||
var argument = "";
|
||||
var argument_length = -1;
|
||||
|
||||
if (words.length >= 2) {
|
||||
words.splice(0, 1)
|
||||
argument = words.join(" ");
|
||||
argument_length = argument.length;
|
||||
}
|
||||
|
||||
if (argument_length > 0 && argument[0] == "\"")
|
||||
argument_length -= 1;
|
||||
|
||||
window.location = "byond://?commandbar_typing=1&verb="+encodeURIComponent(verb)+"&argument_length="+argument_length;
|
||||
}
|
||||
setTimeout(getoutput, 2000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 15 KiB |
+46
-18
@@ -124,10 +124,10 @@ macro "borghotkeymode"
|
||||
command = "a-intent left"
|
||||
elem
|
||||
name = "5"
|
||||
command = ".me"
|
||||
command = "Me-verb"
|
||||
elem
|
||||
name = "6"
|
||||
command = ".Subtle"
|
||||
command = "Subtle-verb"
|
||||
elem
|
||||
name = "A"
|
||||
command = "KeyDown A"
|
||||
@@ -178,7 +178,7 @@ macro "borghotkeymode"
|
||||
command = "KeyUp S"
|
||||
elem
|
||||
name = "T"
|
||||
command = ".say"
|
||||
command = "Say-verb"
|
||||
elem "w_key"
|
||||
name = "W"
|
||||
command = "KeyDown W"
|
||||
@@ -193,10 +193,10 @@ macro "borghotkeymode"
|
||||
command = ".northeast"
|
||||
elem
|
||||
name = "Y"
|
||||
command = ".Whisper"
|
||||
command = "Whisper-verb"
|
||||
elem
|
||||
name = "CTRL+Y"
|
||||
command = ".Whisper"
|
||||
command = "Whisper-verb"
|
||||
elem
|
||||
name = "Z"
|
||||
command = "Activate-Held-Object"
|
||||
@@ -244,10 +244,10 @@ macro "borghotkeymode"
|
||||
command = ".screenshot"
|
||||
elem
|
||||
name = "F3"
|
||||
command = ".say"
|
||||
command = "Say-verb"
|
||||
elem
|
||||
name = "F4"
|
||||
command = ".me"
|
||||
command = "Me-verb"
|
||||
elem
|
||||
name = "F5"
|
||||
command = "asay"
|
||||
@@ -474,10 +474,10 @@ macro "macro"
|
||||
command = ".screenshot"
|
||||
elem
|
||||
name = "F3"
|
||||
command = ".say"
|
||||
command = "Say-verb"
|
||||
elem
|
||||
name = "F4"
|
||||
command = ".me"
|
||||
command = "Me-verb"
|
||||
elem
|
||||
name = "F5"
|
||||
command = "asay"
|
||||
@@ -626,10 +626,10 @@ macro "hotkeymode"
|
||||
command = "a-intent harm"
|
||||
elem
|
||||
name = "5"
|
||||
command = ".me"
|
||||
command = "Me-verb"
|
||||
elem
|
||||
name = "6"
|
||||
command = ".Subtle"
|
||||
command = "Subtle-verb"
|
||||
elem
|
||||
name = "A"
|
||||
command = "KeyDown A"
|
||||
@@ -692,7 +692,7 @@ macro "hotkeymode"
|
||||
command = "KeyUp S"
|
||||
elem
|
||||
name = "T"
|
||||
command = ".say"
|
||||
command = "Say-verb"
|
||||
elem "w_key"
|
||||
name = "W"
|
||||
command = "KeyDown W"
|
||||
@@ -707,10 +707,10 @@ macro "hotkeymode"
|
||||
command = ".northeast"
|
||||
elem
|
||||
name = "Y"
|
||||
command = ".Whisper"
|
||||
command = "Whisper-verb"
|
||||
elem
|
||||
name = "CTRL+Y"
|
||||
command = ".Whisper"
|
||||
command = "Whisper-verb"
|
||||
elem
|
||||
name = "Z"
|
||||
command = "Activate-Held-Object"
|
||||
@@ -767,10 +767,10 @@ macro "hotkeymode"
|
||||
command = ".screenshot"
|
||||
elem
|
||||
name = "F3"
|
||||
command = ".say"
|
||||
command = "Say-verb"
|
||||
elem
|
||||
name = "F4"
|
||||
command = ".me"
|
||||
command = "Me-verb"
|
||||
elem
|
||||
name = "F5"
|
||||
command = "asay"
|
||||
@@ -991,10 +991,10 @@ macro "borgmacro"
|
||||
command = ".screenshot"
|
||||
elem
|
||||
name = "F3"
|
||||
command = ".say"
|
||||
command = "Say-verb"
|
||||
elem
|
||||
name = "F4"
|
||||
command = ".me"
|
||||
command = "Me-verb"
|
||||
elem
|
||||
name = "F5"
|
||||
command = "asay"
|
||||
@@ -1230,6 +1230,15 @@ window "mainwindow"
|
||||
anchor2 = -1,-1
|
||||
is-visible = false
|
||||
saved-params = ""
|
||||
elem "commandbar_spy"
|
||||
type = BROWSER
|
||||
is-default = false
|
||||
pos = 0,0
|
||||
size = 200x200
|
||||
anchor1 = -1,-1
|
||||
anchor2 = -1,-1
|
||||
is-visible = false
|
||||
saved-params = ""
|
||||
|
||||
window "mapwindow"
|
||||
elem "mapwindow"
|
||||
@@ -1562,3 +1571,22 @@ window "text_editor"
|
||||
border = line
|
||||
saved-params = ""
|
||||
multi-line = true
|
||||
|
||||
window "tgui_say"
|
||||
elem "tgui_say"
|
||||
type = MAIN
|
||||
pos = 410,400
|
||||
size = 360x30
|
||||
anchor1 = 50,50
|
||||
anchor2 = 50,50
|
||||
is-visible = false
|
||||
saved-params = ""
|
||||
statusbar = false
|
||||
can-minimize = false
|
||||
elem "browser"
|
||||
type = BROWSER
|
||||
pos = 0,0
|
||||
size = 360x30
|
||||
anchor1 = 0,0
|
||||
anchor2 = 0,0
|
||||
saved-params = ""
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { ChannelIterator } from './ChannelIterator';
|
||||
|
||||
describe('ChannelIterator', () => {
|
||||
let channelIterator: ChannelIterator;
|
||||
|
||||
beforeEach(() => {
|
||||
channelIterator = new ChannelIterator();
|
||||
});
|
||||
|
||||
it('should cycle through channels properly', () => {
|
||||
expect(channelIterator.current()).toBe('Say');
|
||||
expect(channelIterator.next()).toBe('Radio');
|
||||
expect(channelIterator.next()).toBe('Me');
|
||||
expect(channelIterator.next()).toBe('Whis');
|
||||
expect(channelIterator.next()).toBe('Subtle');
|
||||
expect(channelIterator.next()).toBe('LOOC');
|
||||
expect(channelIterator.next()).toBe('OOC');
|
||||
expect(channelIterator.next()).toBe('Say'); // Admin is blacklisted so it should be skipped
|
||||
});
|
||||
|
||||
it('should cycle through channels backwards properly', () => {
|
||||
expect(channelIterator.current()).toBe('Say');
|
||||
expect(channelIterator.prev()).toBe('OOC'); // Admin is blacklisted so it should be skipped
|
||||
expect(channelIterator.prev()).toBe('LOOC');
|
||||
expect(channelIterator.prev()).toBe('Subtle');
|
||||
expect(channelIterator.prev()).toBe('Whis');
|
||||
expect(channelIterator.prev()).toBe('Me');
|
||||
expect(channelIterator.prev()).toBe('Radio');
|
||||
expect(channelIterator.prev()).toBe('Say');
|
||||
});
|
||||
|
||||
it('should set a channel properly', () => {
|
||||
channelIterator.set('OOC');
|
||||
expect(channelIterator.current()).toBe('OOC');
|
||||
});
|
||||
|
||||
it('should return true when current channel is "Say"', () => {
|
||||
channelIterator.set('Say');
|
||||
expect(channelIterator.isSay()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when current channel is not "Say"', () => {
|
||||
channelIterator.set('Radio');
|
||||
expect(channelIterator.isSay()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when current channel is visible', () => {
|
||||
channelIterator.set('Say');
|
||||
expect(channelIterator.isVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when current channel is not visible', () => {
|
||||
channelIterator.set('OOC');
|
||||
expect(channelIterator.isVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not leak a message from a blacklisted channel', () => {
|
||||
channelIterator.set('Admin');
|
||||
expect(channelIterator.next()).toBe('Admin');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
export type Channel =
|
||||
| 'Say'
|
||||
| 'Radio'
|
||||
| 'Me'
|
||||
| 'Whis'
|
||||
| 'Subtle'
|
||||
| 'LOOC'
|
||||
| 'OOC'
|
||||
| 'Admin';
|
||||
|
||||
/**
|
||||
* ### ChannelIterator
|
||||
* Cycles a predefined list of channels,
|
||||
* skipping over blacklisted ones,
|
||||
* and providing methods to manage and query the current channel.
|
||||
*/
|
||||
export class ChannelIterator {
|
||||
private index: number = 0;
|
||||
private readonly channels: Channel[] = [
|
||||
'Say',
|
||||
'Radio',
|
||||
'Me',
|
||||
'Whis',
|
||||
'Subtle',
|
||||
'LOOC',
|
||||
'OOC',
|
||||
'Admin',
|
||||
];
|
||||
private readonly blacklist: Channel[] = ['Admin'];
|
||||
private readonly quiet: Channel[] = ['OOC', 'LOOC', 'Admin'];
|
||||
private readonly multiline: Channel[] = ['Me', 'Subtle'];
|
||||
|
||||
public next(): Channel {
|
||||
if (this.blacklist.includes(this.channels[this.index])) {
|
||||
return this.channels[this.index];
|
||||
}
|
||||
|
||||
for (let index = 1; index <= this.channels.length; index++) {
|
||||
let nextIndex = (this.index + index) % this.channels.length;
|
||||
if (!this.blacklist.includes(this.channels[nextIndex])) {
|
||||
this.index = nextIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this.channels[this.index];
|
||||
}
|
||||
|
||||
public prev(): Channel {
|
||||
if (this.blacklist.includes(this.channels[this.index])) {
|
||||
return this.channels[this.index];
|
||||
}
|
||||
|
||||
for (let index = 1; index <= this.channels.length; index++) {
|
||||
let nextIndex =
|
||||
(this.index - index + this.channels.length) % this.channels.length;
|
||||
if (!this.blacklist.includes(this.channels[nextIndex])) {
|
||||
this.index = nextIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this.channels[this.index];
|
||||
}
|
||||
|
||||
public set(channel: Channel): void {
|
||||
this.index = this.channels.indexOf(channel) || 0;
|
||||
}
|
||||
|
||||
public current(): Channel {
|
||||
return this.channels[this.index];
|
||||
}
|
||||
|
||||
public isSay(): boolean {
|
||||
return this.channels[this.index] === 'Say';
|
||||
}
|
||||
|
||||
public isMultiline(): boolean {
|
||||
return this.multiline.includes(this.channels[this.index]);
|
||||
}
|
||||
|
||||
public isVisible(): boolean {
|
||||
return !this.quiet.includes(this.channels[this.index]);
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.index = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ChatHistory } from './ChatHistory';
|
||||
|
||||
describe('ChatHistory', () => {
|
||||
let chatHistory: ChatHistory;
|
||||
|
||||
beforeEach(() => {
|
||||
chatHistory = new ChatHistory();
|
||||
});
|
||||
|
||||
it('should add a message to the history', () => {
|
||||
chatHistory.add('Hello');
|
||||
expect(chatHistory.getOlderMessage()).toEqual('Hello');
|
||||
});
|
||||
|
||||
it('should retrieve older and newer messages', () => {
|
||||
chatHistory.add('Hello');
|
||||
chatHistory.add('World');
|
||||
expect(chatHistory.getOlderMessage()).toEqual('World');
|
||||
expect(chatHistory.getOlderMessage()).toEqual('Hello');
|
||||
expect(chatHistory.getNewerMessage()).toEqual('World');
|
||||
expect(chatHistory.getNewerMessage()).toBeNull();
|
||||
expect(chatHistory.getOlderMessage()).toEqual('World');
|
||||
});
|
||||
|
||||
it('should limit the history to 5 messages', () => {
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
chatHistory.add(`Message ${i}`);
|
||||
}
|
||||
|
||||
expect(chatHistory.getOlderMessage()).toEqual('Message 6');
|
||||
for (let i = 5; i >= 2; i--) {
|
||||
expect(chatHistory.getOlderMessage()).toEqual(`Message ${i}`);
|
||||
}
|
||||
expect(chatHistory.getOlderMessage()).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle temp message correctly', () => {
|
||||
chatHistory.saveTemp('Temp message');
|
||||
expect(chatHistory.getTemp()).toEqual('Temp message');
|
||||
expect(chatHistory.getTemp()).toBeNull();
|
||||
});
|
||||
|
||||
it('should reset correctly', () => {
|
||||
chatHistory.add('Hello');
|
||||
chatHistory.getOlderMessage();
|
||||
chatHistory.reset();
|
||||
expect(chatHistory.isAtLatest()).toBe(true);
|
||||
expect(chatHistory.getOlderMessage()).toEqual('Hello');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* ### ChatHistory
|
||||
* A class to manage a chat history,
|
||||
* maintaining a maximum of five messages and supporting navigation,
|
||||
* temporary message storage, and query operations.
|
||||
*/
|
||||
export class ChatHistory {
|
||||
private messages: string[] = [];
|
||||
private index: number = -1; // Initialize index at -1
|
||||
private temp: string | null = null;
|
||||
|
||||
public add(message: string): void {
|
||||
this.messages.unshift(message);
|
||||
this.index = -1; // Reset index
|
||||
if (this.messages.length > 5) {
|
||||
this.messages.pop();
|
||||
}
|
||||
}
|
||||
|
||||
public getIndex(): number {
|
||||
return this.index + 1;
|
||||
}
|
||||
|
||||
public getOlderMessage(): string | null {
|
||||
if (this.messages.length === 0 || this.index >= this.messages.length - 1) {
|
||||
return null;
|
||||
}
|
||||
this.index++;
|
||||
return this.messages[this.index];
|
||||
}
|
||||
|
||||
public getNewerMessage(): string | null {
|
||||
if (this.index <= 0) {
|
||||
this.index = -1;
|
||||
return null;
|
||||
}
|
||||
this.index--;
|
||||
return this.messages[this.index];
|
||||
}
|
||||
|
||||
public isAtLatest(): boolean {
|
||||
return this.index === -1;
|
||||
}
|
||||
|
||||
public saveTemp(message: string): void {
|
||||
this.temp = message;
|
||||
}
|
||||
|
||||
public getTemp(): string | null {
|
||||
const temp = this.temp;
|
||||
this.temp = null;
|
||||
return temp;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.index = -1;
|
||||
this.temp = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
import { KEY } from 'common/keys';
|
||||
import { BooleanLike } from 'common/react';
|
||||
import { Component, createRef, RefObject } from 'react';
|
||||
import { dragStartHandler } from 'tgui/drag';
|
||||
import {
|
||||
removeAllSkiplines,
|
||||
sanitizeMultiline,
|
||||
} from 'tgui/interfaces/TextInputModal';
|
||||
|
||||
import { Channel, ChannelIterator } from './ChannelIterator';
|
||||
import { ChatHistory } from './ChatHistory';
|
||||
import { LINE_LENGTHS, RADIO_PREFIXES, WINDOW_SIZES } from './constants';
|
||||
import { windowClose, windowOpen, windowSet } from './helpers';
|
||||
import { byondMessages } from './timers';
|
||||
|
||||
type ByondOpen = {
|
||||
channel: Channel;
|
||||
};
|
||||
|
||||
type ByondProps = {
|
||||
maxLength: number;
|
||||
lightMode: BooleanLike;
|
||||
};
|
||||
|
||||
type State = {
|
||||
buttonContent: string | number;
|
||||
size: WINDOW_SIZES;
|
||||
};
|
||||
|
||||
const CHANNEL_REGEX = /^:\w\s/;
|
||||
|
||||
export class TguiSay extends Component<{}, State> {
|
||||
private channelIterator: ChannelIterator;
|
||||
private chatHistory: ChatHistory;
|
||||
private currentPrefix: keyof typeof RADIO_PREFIXES | null;
|
||||
private innerRef: RefObject<HTMLTextAreaElement>;
|
||||
private lightMode: boolean;
|
||||
private maxLength: number;
|
||||
private messages: typeof byondMessages;
|
||||
state: State;
|
||||
|
||||
constructor(props: never) {
|
||||
super(props);
|
||||
|
||||
this.channelIterator = new ChannelIterator();
|
||||
this.chatHistory = new ChatHistory();
|
||||
this.currentPrefix = null;
|
||||
this.innerRef = createRef();
|
||||
this.lightMode = false;
|
||||
this.maxLength = 1024;
|
||||
this.messages = byondMessages;
|
||||
this.state = {
|
||||
buttonContent: '',
|
||||
size: WINDOW_SIZES.small,
|
||||
};
|
||||
|
||||
this.handleArrowKeys = this.handleArrowKeys.bind(this);
|
||||
this.handleBackspaceDelete = this.handleBackspaceDelete.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
this.handleEnter = this.handleEnter.bind(this);
|
||||
this.handleForceSay = this.handleForceSay.bind(this);
|
||||
this.handleIncrementChannel = this.handleIncrementChannel.bind(this);
|
||||
this.handleInput = this.handleInput.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this.handleOpen = this.handleOpen.bind(this);
|
||||
this.handleProps = this.handleProps.bind(this);
|
||||
this.reset = this.reset.bind(this);
|
||||
this.setSize = this.setSize.bind(this);
|
||||
this.setValue = this.setValue.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
Byond.subscribeTo('props', this.handleProps);
|
||||
Byond.subscribeTo('force', this.handleForceSay);
|
||||
Byond.subscribeTo('open', this.handleOpen);
|
||||
}
|
||||
|
||||
handleArrowKeys(direction: KEY.Up | KEY.Down) {
|
||||
const currentValue = this.innerRef.current?.value;
|
||||
|
||||
if (direction === KEY.Up) {
|
||||
if (this.chatHistory.isAtLatest() && currentValue) {
|
||||
// Save current message to temp history if at the most recent message
|
||||
this.chatHistory.saveTemp(currentValue);
|
||||
}
|
||||
// Try to get the previous message, fall back to the current value if none
|
||||
const prevMessage = this.chatHistory.getOlderMessage();
|
||||
|
||||
if (prevMessage) {
|
||||
this.setState({ buttonContent: this.chatHistory.getIndex() });
|
||||
this.setSize(prevMessage.length);
|
||||
this.setValue(prevMessage);
|
||||
}
|
||||
} else {
|
||||
const nextMessage =
|
||||
this.chatHistory.getNewerMessage() || this.chatHistory.getTemp() || '';
|
||||
|
||||
const buttonContent = this.chatHistory.isAtLatest()
|
||||
? this.channelIterator.current()
|
||||
: this.chatHistory.getIndex();
|
||||
|
||||
this.setState({ buttonContent });
|
||||
this.setSize(nextMessage.length);
|
||||
this.setValue(nextMessage);
|
||||
}
|
||||
}
|
||||
|
||||
handleBackspaceDelete() {
|
||||
const typed = this.innerRef.current?.value;
|
||||
|
||||
// User is on a chat history message
|
||||
if (!this.chatHistory.isAtLatest()) {
|
||||
this.chatHistory.reset();
|
||||
this.setState({
|
||||
buttonContent: this.currentPrefix ?? this.channelIterator.current(),
|
||||
});
|
||||
// Empty input, resets the channel
|
||||
} else if (
|
||||
!!this.currentPrefix &&
|
||||
this.channelIterator.isSay() &&
|
||||
typed?.length === 0
|
||||
) {
|
||||
this.currentPrefix = null;
|
||||
this.setState({ buttonContent: this.channelIterator.current() });
|
||||
}
|
||||
|
||||
this.setSize(typed?.length);
|
||||
}
|
||||
|
||||
handleClose() {
|
||||
const current = this.innerRef.current;
|
||||
|
||||
if (current) {
|
||||
current.blur();
|
||||
}
|
||||
|
||||
this.reset();
|
||||
this.chatHistory.reset();
|
||||
this.channelIterator.reset();
|
||||
this.currentPrefix = null;
|
||||
windowClose();
|
||||
}
|
||||
|
||||
handleEnter() {
|
||||
const prefix = this.currentPrefix ?? '';
|
||||
const value = this.innerRef.current?.value;
|
||||
|
||||
if (value?.length && value.length < this.maxLength) {
|
||||
this.chatHistory.add(value);
|
||||
|
||||
// Everything can be multiline, but only emotes get passed that way to the game
|
||||
const sanitizedValue = this.channelIterator.isMultiline()
|
||||
? sanitizeMultiline(value)
|
||||
: removeAllSkiplines(value);
|
||||
|
||||
Byond.sendMessage('entry', {
|
||||
channel: this.channelIterator.current(),
|
||||
entry: this.channelIterator.isSay()
|
||||
? prefix + sanitizedValue
|
||||
: sanitizedValue,
|
||||
});
|
||||
}
|
||||
|
||||
this.handleClose();
|
||||
}
|
||||
|
||||
handleForceSay() {
|
||||
const currentValue = this.innerRef.current?.value;
|
||||
// Only force say if we're on a visible channel and have typed something
|
||||
if (!currentValue || !this.channelIterator.isVisible()) return;
|
||||
|
||||
const prefix = this.currentPrefix ?? '';
|
||||
const grunt = this.channelIterator.isSay()
|
||||
? prefix + currentValue
|
||||
: currentValue;
|
||||
|
||||
this.messages.forceSayMsg(grunt);
|
||||
this.reset();
|
||||
}
|
||||
|
||||
handleIncrementChannel() {
|
||||
this.currentPrefix = null;
|
||||
|
||||
this.channelIterator.next();
|
||||
|
||||
// If we've looped onto a quiet channel, tell byond to hide thinking indicators
|
||||
if (!this.channelIterator.isVisible()) {
|
||||
this.messages.channelIncrementMsg(false, this.channelIterator.current());
|
||||
} else {
|
||||
this.messages.channelIncrementMsg(true, this.channelIterator.current());
|
||||
}
|
||||
|
||||
this.setState({ buttonContent: this.channelIterator.current() });
|
||||
}
|
||||
|
||||
handleDecrementChannel() {
|
||||
this.currentPrefix = null;
|
||||
|
||||
this.channelIterator.prev();
|
||||
|
||||
// If we've looped onto a quiet channel, tell byond to hide thinking indicators
|
||||
if (!this.channelIterator.isVisible()) {
|
||||
this.messages.channelIncrementMsg(false, this.channelIterator.current());
|
||||
} else {
|
||||
this.messages.channelIncrementMsg(true, this.channelIterator.current());
|
||||
}
|
||||
|
||||
this.setState({ buttonContent: this.channelIterator.current() });
|
||||
}
|
||||
|
||||
handleInput() {
|
||||
const typed = this.innerRef.current?.value;
|
||||
|
||||
// If we're typing, send the message
|
||||
if (this.channelIterator.isVisible()) {
|
||||
this.messages.typingMsg(this.channelIterator.current());
|
||||
}
|
||||
|
||||
this.setSize(typed?.length);
|
||||
|
||||
// Is there a value? Is it long enough to be a prefix?
|
||||
if (!typed || typed.length < 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CHANNEL_REGEX.test(typed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Is it a valid prefix?
|
||||
const prefix = typed
|
||||
.slice(0, 3)
|
||||
?.toLowerCase() as keyof typeof RADIO_PREFIXES;
|
||||
if (!RADIO_PREFIXES[prefix] || prefix === this.currentPrefix) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.channelIterator.set('Say');
|
||||
this.currentPrefix = prefix;
|
||||
this.setState({ buttonContent: RADIO_PREFIXES[prefix] });
|
||||
this.setValue(typed.slice(3));
|
||||
}
|
||||
|
||||
handleKeyDown(event: React.KeyboardEvent<HTMLTextAreaElement>) {
|
||||
const currentValue = this.innerRef.current?.value;
|
||||
switch (event.key) {
|
||||
case KEY.Up:
|
||||
case KEY.Down:
|
||||
// Allow moving between lines if there are newlines
|
||||
if (currentValue?.includes('\n')) {
|
||||
break;
|
||||
}
|
||||
event.preventDefault();
|
||||
this.handleArrowKeys(event.key);
|
||||
break;
|
||||
|
||||
case KEY.Delete:
|
||||
case KEY.Backspace:
|
||||
this.handleBackspaceDelete();
|
||||
break;
|
||||
|
||||
case KEY.Enter:
|
||||
// Allow inputting newlines
|
||||
if (event.shiftKey) {
|
||||
break;
|
||||
}
|
||||
event.preventDefault();
|
||||
this.handleEnter();
|
||||
break;
|
||||
|
||||
case KEY.Tab:
|
||||
event.preventDefault();
|
||||
if (event.shiftKey) {
|
||||
this.handleDecrementChannel();
|
||||
} else {
|
||||
this.handleIncrementChannel();
|
||||
}
|
||||
break;
|
||||
|
||||
case KEY.Escape:
|
||||
this.handleClose();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleOpen = (data: ByondOpen) => {
|
||||
setTimeout(() => {
|
||||
this.innerRef.current?.focus();
|
||||
}, 0);
|
||||
|
||||
const { channel } = data;
|
||||
// Catches the case where the modal is already open
|
||||
if (this.channelIterator.isSay()) {
|
||||
this.channelIterator.set(channel);
|
||||
}
|
||||
this.setState({ buttonContent: this.channelIterator.current() });
|
||||
|
||||
windowOpen(this.channelIterator.current());
|
||||
};
|
||||
|
||||
handleProps = (data: ByondProps) => {
|
||||
const { maxLength, lightMode } = data;
|
||||
this.maxLength = maxLength;
|
||||
this.lightMode = !!lightMode;
|
||||
};
|
||||
|
||||
reset() {
|
||||
this.setValue('');
|
||||
this.setSize();
|
||||
this.setState({
|
||||
buttonContent: this.channelIterator.current(),
|
||||
});
|
||||
}
|
||||
|
||||
setSize(length = 0) {
|
||||
let newSize: WINDOW_SIZES;
|
||||
|
||||
const currentValue = this.innerRef.current?.value;
|
||||
|
||||
if (currentValue?.includes('\n')) {
|
||||
newSize = WINDOW_SIZES.large;
|
||||
} else if (length > LINE_LENGTHS.medium) {
|
||||
newSize = WINDOW_SIZES.large;
|
||||
} else if (length <= LINE_LENGTHS.medium && length > LINE_LENGTHS.small) {
|
||||
newSize = WINDOW_SIZES.medium;
|
||||
} else {
|
||||
newSize = WINDOW_SIZES.small;
|
||||
}
|
||||
|
||||
if (this.state.size !== newSize) {
|
||||
this.setState({ size: newSize });
|
||||
windowSet(newSize);
|
||||
}
|
||||
}
|
||||
|
||||
setValue(value: string) {
|
||||
const textArea = this.innerRef.current;
|
||||
if (textArea) {
|
||||
textArea.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme =
|
||||
(this.lightMode && 'lightMode') ||
|
||||
(this.currentPrefix && RADIO_PREFIXES[this.currentPrefix]) ||
|
||||
this.channelIterator.current();
|
||||
|
||||
return (
|
||||
<div className={`window window-${theme} window-${this.state.size}`}>
|
||||
<Dragzone position="top" theme={theme} />
|
||||
<div className="center">
|
||||
<Dragzone position="left" theme={theme} />
|
||||
<div className="input">
|
||||
<button
|
||||
className={`button button-${theme}`}
|
||||
onClick={this.handleIncrementChannel}
|
||||
type="button"
|
||||
>
|
||||
{this.state.buttonContent}
|
||||
</button>
|
||||
<textarea
|
||||
className={`textarea textarea-${theme}`}
|
||||
maxLength={this.maxLength}
|
||||
onInput={this.handleInput}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
ref={this.innerRef}
|
||||
/>
|
||||
</div>
|
||||
<Dragzone position="right" theme={theme} />
|
||||
</div>
|
||||
<Dragzone position="bottom" theme={theme} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Dragzone = ({ theme, position }: { theme: string; position: string }) => {
|
||||
// Horizontal or vertical?
|
||||
const location =
|
||||
position === 'left' || position === 'right' ? 'vertical' : 'horizontal';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`dragzone-${location} dragzone-${position} dragzone-${theme}`}
|
||||
onMouseDown={dragStartHandler}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
/** Window sizes in pixels */
|
||||
export enum WINDOW_SIZES {
|
||||
small = 30,
|
||||
medium = 50,
|
||||
large = 70,
|
||||
width = 360,
|
||||
}
|
||||
|
||||
/** Line lengths for autoexpand */
|
||||
export enum LINE_LENGTHS {
|
||||
small = 22,
|
||||
medium = 45,
|
||||
}
|
||||
|
||||
/**
|
||||
* Radio prefixes.
|
||||
* Displays the name in the left button, tags a css class.
|
||||
*/
|
||||
export const RADIO_PREFIXES = {
|
||||
':b ': '010',
|
||||
':c ': 'Cmd',
|
||||
':e ': 'Eng',
|
||||
':h ': 'Dept',
|
||||
':m ': 'Med',
|
||||
':n ': 'Sci',
|
||||
':p ': 'AI',
|
||||
':s ': 'Sec',
|
||||
':t ': 'Merc',
|
||||
':u ': 'Sup',
|
||||
':v ': 'Srv',
|
||||
':y ': 'ITV',
|
||||
} as const;
|
||||
Binary file not shown.
@@ -0,0 +1,46 @@
|
||||
import { Channel } from './ChannelIterator';
|
||||
import { WINDOW_SIZES } from './constants';
|
||||
|
||||
/**
|
||||
* Once byond signals this via keystroke, it
|
||||
* ensures window size, visibility, and focus.
|
||||
*/
|
||||
export const windowOpen = (channel: Channel) => {
|
||||
setWindowVisibility(true);
|
||||
Byond.sendMessage('open', { channel });
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets the state of the window and hides it from user view.
|
||||
* Sending "close" logs it server side.
|
||||
*/
|
||||
export const windowClose = () => {
|
||||
setWindowVisibility(false);
|
||||
Byond.winset('map', {
|
||||
focus: true,
|
||||
});
|
||||
Byond.sendMessage('close');
|
||||
};
|
||||
|
||||
/**
|
||||
* Modifies the window size.
|
||||
*/
|
||||
export const windowSet = (size = WINDOW_SIZES.small) => {
|
||||
let sizeStr = `${WINDOW_SIZES.width}x${size}`;
|
||||
|
||||
Byond.winset('tgui_say.browser', {
|
||||
size: sizeStr,
|
||||
});
|
||||
|
||||
Byond.winset('tgui_say', {
|
||||
size: sizeStr,
|
||||
});
|
||||
};
|
||||
|
||||
/** Helper function to set window size and visibility */
|
||||
const setWindowVisibility = (visible: boolean) => {
|
||||
Byond.winset('tgui_say', {
|
||||
'is-visible': visible,
|
||||
size: `${WINDOW_SIZES.width}x${WINDOW_SIZES.small}`,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import './styles/main.scss';
|
||||
|
||||
import { createRoot, Root } from 'react-dom/client';
|
||||
|
||||
import { TguiSay } from './TguiSay';
|
||||
|
||||
let reactRoot: Root | null = null;
|
||||
|
||||
document.onreadystatechange = function () {
|
||||
if (document.readyState !== 'complete') return;
|
||||
|
||||
if (!reactRoot) {
|
||||
const root = document.getElementById('react-root');
|
||||
reactRoot = createRoot(root!);
|
||||
}
|
||||
|
||||
reactRoot.render(<TguiSay />);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "tgui-say",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@types/react": "^18.2.74",
|
||||
"@types/react-dom": "^18.2.24",
|
||||
"common": "workspace:*",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tgui": "workspace:*",
|
||||
"tgui-polyfill": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
@use 'sass:color';
|
||||
@use './colors.scss';
|
||||
|
||||
.button {
|
||||
align-items: center;
|
||||
background-color: colors.$button;
|
||||
border-radius: 0.3rem;
|
||||
border: thin solid;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
font-family: inherit;
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
width: 1rem;
|
||||
&:hover {
|
||||
background-color: lighten(colors.$button, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
.button-lightMode {
|
||||
background-color: colors.$lightBorder;
|
||||
border: none;
|
||||
color: black;
|
||||
&:hover {
|
||||
background-color: colors.$lightHover;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
@use 'sass:map';
|
||||
|
||||
$background: #131313;
|
||||
$button: #1f1f1f;
|
||||
$lightMode: #ffffff;
|
||||
$lightBorder: #bbbbbb;
|
||||
$lightHover: #eaeaea;
|
||||
|
||||
$_channel_map: (
|
||||
// Radio
|
||||
'010': #1e90ff,
|
||||
'Cmd': #57b8f0,
|
||||
'Eng': #fcdf03,
|
||||
'Dept': #1ecc43,
|
||||
'Med': #57f09e,
|
||||
'Sci': #c68cfa,
|
||||
'AI': #d65d95,
|
||||
'Sec': #dd3535,
|
||||
'Merc': #8f4a4b,
|
||||
'Sup': #b88646,
|
||||
'Srv': #6ca729,
|
||||
'ITV': #8a8a8a,
|
||||
// Modes
|
||||
'LOOC': #3a9696,
|
||||
'Me': #5975da,
|
||||
'OOC': #cca300,
|
||||
'Radio': #1ecc43,
|
||||
'Say': #a4bad6,
|
||||
'Whis': #7c7fd9,
|
||||
'Subtle': #6366bd
|
||||
);
|
||||
|
||||
$channel_keys: map.keys($_channel_map) !default;
|
||||
|
||||
$channel-map: ();
|
||||
|
||||
@each $channel in $channel_keys {
|
||||
$channel-map: map-merge(
|
||||
$channel-map,
|
||||
(
|
||||
$channel: map.get($_channel_map, $channel),
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
.center {
|
||||
display: flex;
|
||||
flex: 1 1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input {
|
||||
display: flex;
|
||||
flex: 1 1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
font-family: 'Consolas', monospace;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
@use 'sass:color';
|
||||
@use './colors.scss';
|
||||
|
||||
$dragSize: 0.6rem;
|
||||
$borderSize: 0.2rem;
|
||||
|
||||
.dragzone-horizontal {
|
||||
border-left: $borderSize solid;
|
||||
border-right: $borderSize solid;
|
||||
color: transparent;
|
||||
width: 100%;
|
||||
height: $dragSize;
|
||||
}
|
||||
.dragzone-left {
|
||||
border-left: $borderSize solid;
|
||||
}
|
||||
|
||||
.dragzone-right {
|
||||
border-right: $borderSize solid;
|
||||
}
|
||||
|
||||
.dragzone-vertical {
|
||||
color: transparent;
|
||||
height: 100%;
|
||||
width: $dragSize;
|
||||
}
|
||||
|
||||
.dragzone-top {
|
||||
border-top: $borderSize solid;
|
||||
}
|
||||
|
||||
.dragzone-bottom {
|
||||
border-bottom: $borderSize solid;
|
||||
}
|
||||
|
||||
/** Lightmode static theme */
|
||||
.dragzone-lightMode {
|
||||
border-color: colors.$lightBorder;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
@use 'sass:meta';
|
||||
@use 'sass:color';
|
||||
@use './colors.scss';
|
||||
|
||||
// Core styles
|
||||
@include meta.load-css('~tgui/styles/reset.scss');
|
||||
// Atomic styles
|
||||
@include meta.load-css('~tgui/styles/atomic/text.scss');
|
||||
// External styles
|
||||
@include meta.load-css('~tgui/styles/components/TextArea.scss');
|
||||
// Local styles
|
||||
@include meta.load-css('./button.scss');
|
||||
@include meta.load-css('./content.scss');
|
||||
@include meta.load-css('./dragzone.scss');
|
||||
@include meta.load-css('./textarea.scss');
|
||||
@include meta.load-css('./window.scss');
|
||||
|
||||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@each $channel, $color in colors.$channel-map {
|
||||
$darkened: darken($color, 20%);
|
||||
|
||||
.button-#{$channel} {
|
||||
border-color: darken($color, 10%);
|
||||
color: $color;
|
||||
&:hover {
|
||||
border-color: lighten($color, 10%);
|
||||
color: lighten($color, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
.dragzone-#{$channel} {
|
||||
border-color: $darkened;
|
||||
}
|
||||
|
||||
.textarea-#{$channel} {
|
||||
color: $color;
|
||||
}
|
||||
|
||||
.window-#{$channel} {
|
||||
&:after {
|
||||
animation: gradient 10s linear infinite;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
darken($color, 35%),
|
||||
$color,
|
||||
lighten($color, 10%),
|
||||
$color,
|
||||
darken($color, 35%)
|
||||
);
|
||||
background-position: 0% 0%;
|
||||
background-size: 500% auto;
|
||||
bottom: 0px;
|
||||
content: '';
|
||||
height: 2px;
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
z-index: 999;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
.textarea {
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
display: flex;
|
||||
flex-grow: 5;
|
||||
font-family: inherit;
|
||||
font-size: 1.1rem;
|
||||
overflow: hidden;
|
||||
margin: 0 0 0 0.4rem;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
@use 'sass:color';
|
||||
@use './colors.scss';
|
||||
|
||||
.window {
|
||||
background-color: colors.$background;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 360px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.window-lightMode {
|
||||
background-color: colors.$lightMode;
|
||||
}
|
||||
|
||||
/** Window sizes */
|
||||
.window-30 {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.window-50 {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.window-70 {
|
||||
height: 70px;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { debounce, throttle } from 'common/timer';
|
||||
|
||||
const SECONDS = 1000;
|
||||
|
||||
/** Timers: Prevents overloading the server, throttles messages */
|
||||
export const byondMessages = {
|
||||
// Debounce: Prevents spamming the server
|
||||
channelIncrementMsg: debounce(
|
||||
(visible: boolean, channel: string) =>
|
||||
Byond.sendMessage('thinking', { visible, channel }),
|
||||
0.4 * SECONDS,
|
||||
),
|
||||
forceSayMsg: debounce(
|
||||
(entry: string) => Byond.sendMessage('force', { entry, channel: 'Say' }),
|
||||
1 * SECONDS,
|
||||
true,
|
||||
),
|
||||
// Throttle: Prevents spamming the server
|
||||
typingMsg: throttle(
|
||||
(channel: string) => Byond.sendMessage('typing', { channel }),
|
||||
4 * SECONDS,
|
||||
),
|
||||
} as const;
|
||||
@@ -22,7 +22,7 @@ export const sanitizeMultiline = (toSanitize: string) => {
|
||||
};
|
||||
|
||||
export const removeAllSkiplines = (toSanitize: string) => {
|
||||
return toSanitize.replace(/[\r\n]+/, '');
|
||||
return toSanitize.replace(/[\r\n]+/, ' ');
|
||||
};
|
||||
|
||||
export const TextInputModal = (props) => {
|
||||
|
||||
+142
-142
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+91
-91
File diff suppressed because one or more lines are too long
@@ -33,6 +33,7 @@ module.exports = (env = {}, argv) => {
|
||||
entry: {
|
||||
tgui: ['./packages/tgui-polyfill', './packages/tgui'],
|
||||
'tgui-panel': ['./packages/tgui-polyfill', './packages/tgui-panel'],
|
||||
'tgui-say': ['./packages/tgui-polyfill', './packages/tgui-say'],
|
||||
},
|
||||
output: {
|
||||
path: argv.useTmpFolder
|
||||
|
||||
@@ -1453,6 +1453,25 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react-dom@npm:^18.2.24":
|
||||
version: 18.3.0
|
||||
resolution: "@types/react-dom@npm:18.3.0"
|
||||
dependencies:
|
||||
"@types/react": "npm:*"
|
||||
checksum: 10c0/6c90d2ed72c5a0e440d2c75d99287e4b5df3e7b011838cdc03ae5cd518ab52164d86990e73246b9d812eaf02ec351d74e3b4f5bd325bf341e13bf980392fd53b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react@npm:*":
|
||||
version: 18.3.3
|
||||
resolution: "@types/react@npm:18.3.3"
|
||||
dependencies:
|
||||
"@types/prop-types": "npm:*"
|
||||
csstype: "npm:^3.0.2"
|
||||
checksum: 10c0/fe455f805c5da13b89964c3d68060cebd43e73ec15001a68b34634604a78140e6fc202f3f61679b9d809dde6d7a7c2cb3ed51e0fd1462557911db09879b55114
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react@npm:^18.2.74":
|
||||
version: 18.3.2
|
||||
resolution: "@types/react@npm:18.3.2"
|
||||
@@ -8276,6 +8295,20 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"tgui-say@workspace:packages/tgui-say":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "tgui-say@workspace:packages/tgui-say"
|
||||
dependencies:
|
||||
"@types/react": "npm:^18.2.74"
|
||||
"@types/react-dom": "npm:^18.2.24"
|
||||
common: "workspace:*"
|
||||
react: "npm:^18.2.0"
|
||||
react-dom: "npm:^18.2.0"
|
||||
tgui: "workspace:*"
|
||||
tgui-polyfill: "workspace:*"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"tgui-workspace@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "tgui-workspace@workspace:."
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
#include "code\__defines\ammunition.dm"
|
||||
#include "code\__defines\appearance.dm"
|
||||
#include "code\__defines\assemblies.dm"
|
||||
#include "code\__defines\assert.dm"
|
||||
#include "code\__defines\atmos.dm"
|
||||
#include "code\__defines\atoms_movable.dm"
|
||||
#include "code\__defines\belly_modes_vr.dm"
|
||||
@@ -122,6 +123,7 @@
|
||||
#include "code\__defines\span_vr.dm"
|
||||
#include "code\__defines\species_languages.dm"
|
||||
#include "code\__defines\species_languages_vr.dm"
|
||||
#include "code\__defines\speech_channels.dm"
|
||||
#include "code\__defines\spells.dm"
|
||||
#include "code\__defines\sprite_sheets.dm"
|
||||
#include "code\__defines\sqlite_defines.dm"
|
||||
@@ -150,6 +152,9 @@
|
||||
#include "code\__defines\dcs\flags.dm"
|
||||
#include "code\__defines\dcs\helpers.dm"
|
||||
#include "code\__defines\dcs\signals.dm"
|
||||
#include "code\__defines\traits\_traits.dm"
|
||||
#include "code\__defines\traits\declarations.dm"
|
||||
#include "code\__defines\traits\sources.dm"
|
||||
#include "code\_global_vars\_regexes.dm"
|
||||
#include "code\_global_vars\bitfields.dm"
|
||||
#include "code\_global_vars\misc.dm"
|
||||
@@ -160,6 +165,7 @@
|
||||
#include "code\_global_vars\lists\mapping.dm"
|
||||
#include "code\_global_vars\lists\misc.dm"
|
||||
#include "code\_global_vars\lists\species.dm"
|
||||
#include "code\_global_vars\traits\_traits.dm"
|
||||
#include "code\_helpers\_global_objects.dm"
|
||||
#include "code\_helpers\_global_objects_vr.dm"
|
||||
#include "code\_helpers\_lists.dm"
|
||||
@@ -185,6 +191,7 @@
|
||||
#include "code\_helpers\string_lists.dm"
|
||||
#include "code\_helpers\text.dm"
|
||||
#include "code\_helpers\time.dm"
|
||||
#include "code\_helpers\traits.dm"
|
||||
#include "code\_helpers\turfs.dm"
|
||||
#include "code\_helpers\type2type.dm"
|
||||
#include "code\_helpers\unsorted.dm"
|
||||
@@ -1887,6 +1894,7 @@
|
||||
#include "code\modules\asset_cache\asset_list_items.dm"
|
||||
#include "code\modules\asset_cache\assets\chat.dm"
|
||||
#include "code\modules\asset_cache\assets\fontawesome.dm"
|
||||
#include "code\modules\asset_cache\assets\icon_ref_map.dm"
|
||||
#include "code\modules\asset_cache\assets\jquery.dm"
|
||||
#include "code\modules\asset_cache\assets\tgfont.dm"
|
||||
#include "code\modules\asset_cache\assets\tgui.dm"
|
||||
@@ -2034,6 +2042,7 @@
|
||||
#include "code\modules\client\verbs\ooc.dm"
|
||||
#include "code\modules\client\verbs\ping.dm"
|
||||
#include "code\modules\client\verbs\suicide.dm"
|
||||
#include "code\modules\client\verbs\typing.dm"
|
||||
#include "code\modules\client\verbs\who.dm"
|
||||
#include "code\modules\clothing\chameleon.dm"
|
||||
#include "code\modules\clothing\clothing.dm"
|
||||
@@ -4115,6 +4124,9 @@
|
||||
#include "code\modules\tgui_input\list.dm"
|
||||
#include "code\modules\tgui_input\number.dm"
|
||||
#include "code\modules\tgui_input\text.dm"
|
||||
#include "code\modules\tgui_input\say_modal\modal.dm"
|
||||
#include "code\modules\tgui_input\say_modal\speech.dm"
|
||||
#include "code\modules\tgui_input\say_modal\typing.dm"
|
||||
#include "code\modules\tgui_panel\audio.dm"
|
||||
#include "code\modules\tgui_panel\external.dm"
|
||||
#include "code\modules\tgui_panel\telemetry.dm"
|
||||
|
||||
Reference in New Issue
Block a user