diff --git a/citadel.dme b/citadel.dme
index 97305cff74d..79af566be9b 100644
--- a/citadel.dme
+++ b/citadel.dme
@@ -79,6 +79,7 @@
#include "code\__DEFINES\shields.dm"
#include "code\__DEFINES\shuttle.dm"
#include "code\__DEFINES\shuttles.dm"
+#include "code\__DEFINES\skin.dm"
#include "code\__DEFINES\sonar.dm"
#include "code\__DEFINES\spaceman_dmm.dm"
#include "code\__DEFINES\spans.dm"
@@ -4210,6 +4211,8 @@
#include "interface\interface.dm"
#include "interface\stylesheet.dm"
#include "interface\skin.dmf"
+#include "interface\menus\_menus.dm"
+#include "interface\menus\main.dm"
#include "maps\Generic\death_triggers.dm"
#include "maps\Generic\map_data.dm"
#include "maps\Generic\misc.dm"
diff --git a/code/__DEFINES/_planes+layers.dm b/code/__DEFINES/_planes+layers.dm
index 469a38acb53..8c1c48fb6a9 100644
--- a/code/__DEFINES/_planes+layers.dm
+++ b/code/__DEFINES/_planes+layers.dm
@@ -50,10 +50,10 @@ What is the naming convention for planes or layers?
/// Reserved for use in space/parallax
#define PARALLAX_PLANE -90
/// everything layering below
-#define PARALLAX_VIS_LAYER_BELOW -10000
+#define PARALLAX_VIS_LAYER_BELOW -100
#define PARALLAX_LAYER_CENTER 0
/// ditto
-#define PARALLAX_VIS_LAYER_ABOVE 10000
+#define PARALLAX_VIS_LAYER_ABOVE 100
///For the Looking Glass holodecks
#define PLANE_LOOKINGGLASS -77
///For the Looking Glass holodecks
diff --git a/code/__DEFINES/skin.dm b/code/__DEFINES/skin.dm
new file mode 100644
index 00000000000..ae37f4970e0
--- /dev/null
+++ b/code/__DEFINES/skin.dm
@@ -0,0 +1,54 @@
+//? WINCLONE IDs; NO SKIN CONTROL MAY HAVE THESE IDs, OR WINCLONE WILL NOT CREATE "NAKED" CONTROLS OF THIS TYPE!
+#define SKIN_ID_ABSTRACT_MENU "menu"
+#define SKIN_ID_ABSTRACT_WINDOW "window"
+#define SKIN_ID_ABSTRACT_PANE "pane"
+#define SKIN_ID_ABSTRACT_MACRO "macro"
+
+//? SKIN ELEMENT IDS; MAKE SURE TO UPDATE THESE WHEN CHANGING SKIN.DMF.
+//! Window IDs
+/// main game window
+#define SKIN_WINDOW_ID_MAIN "mainwindow"
+
+//! Map IDs
+/// main game viewport
+#define SKIN_MAP_ID_VIEWPORT "mapwindow.map"
+
+//! Splitter IDs
+/// main window split
+#define SKIN_SPLITTER_ID_MAIN "mainwindow.split"
+
+//? Dynamic skin IDs used by code-generated elements
+//? button groups MUST have unique ids to all skin elements!
+// TOP LEVEL MENU //
+#define SKIN_ID_MAIN_MENU "menu_main"
+ // what it attaches to
+ #define SKIN_ID_MAIN_MENU_ATTACH_TO "mainwindow"
+ // categories
+ #define SKIN_ID_MENU_CATEGORY_FILE "menu_file"
+ #define SKIN_ID_MENU_CATEGORY_ZOOM "menu_zoom"
+ #define SKIN_ID_MENU_CATEGORY_SCALING "menu_scaling"
+ #define SKIN_ID_MENU_CATEGORY_HELP "menu_help"
+ // groups
+ #define SKIN_BUTTON_GROUP_MAP_ZOOM "group_map_zoom"
+ #define SKIN_BUTTON_GROUP_MAP_SCALING "group_map_scaling"
+ #define SKIN_BUTTON_GROUP_MAP_WIDESCREEN "group_map_widescreen"
+ // buttons - file
+ #define SKIN_ID_MENU_BUTTON_SCREENSHOT "menubutton_screenshot"
+ #define SKIN_ID_MENU_BUTTON_SCREENSHOT_QUICK "menubutton_screenshot_quick"
+ #define SKIN_ID_MENU_BUTTON_RECONNECT "menubutton_reconnect"
+ #define SKIN_ID_MENU_BUTTON_PING "menubutton_ping"
+ #define SKIN_ID_MENU_BUTTON_QUIT "menubutton_quit"
+ // buttons - size
+ #define SKIN_ID_MENU_BUTTON_STRETCH_TO_FIT "menubutton_stretchtofit"
+ #define SKIN_ID_MENU_BUTTON_STRETCH_NO_LETTERBOX "menubutton_stretchtofill"
+ #define SKIN_ID_MENU_BUTTON_FOR_RESOLUTION(resolution) "menubutton_[resolution]x[resolution]"
+ #define SKIN_ID_MENU_BUTTON_WIDESCREEN_ENABLED "menubutton_widescreen_on"
+ #define SKIN_ID_MENU_BUTTON_WIDESCREEN_DISABLED "menubutton_widescreen_off"
+ #define SKIN_ID_MENU_BUTTON_AUTO_FIT_VIEWPORT "menubutton_auto_fit_viewport"
+ #define SKIN_ID_MENU_BUTTON_FIT_VIEWPORT "menubutton_fit_viewport"
+ // buttons - scaling
+ #define SKIN_ID_MENU_BUTTON_NEAREST_NEIGHBOR "menubutton_scale_distort"
+ #define SKIN_ID_MENU_BUTTON_POINT_SAMPLE "menubutton_scale_normal"
+ #define SKIN_ID_MENU_BUTTON_BILINEAR "menubutton_scale_blur"
+ // buttons - help
+ #define SKIN_ID_MENU_BUTTON_ADMINHELP "menubutton_adminhelp"
diff --git a/code/__HELPERS/view.dm b/code/__HELPERS/view.dm
index 0ea599b9800..a31533592d7 100644
--- a/code/__HELPERS/view.dm
+++ b/code/__HELPERS/view.dm
@@ -1,167 +1,3 @@
-/**
- * This is intended to be a full wrapper. DO NOT directly modify its values.
- * Container for client viewsize.
- */
-/datum/view_data
- /// Width offset to apply to the default view string if we're not supressed for some reason.
- var/width = 0
- /// Height offset to apply to the default view string, see above.
- var/height = 0
- /**
- * This client's current "default" view, in the format "WidthxHeight".
- * We add/remove from this when we want to change their window size.
- */
- var/default = ""
- /**
- * This client's current zoom level, if it's not being supressed.
- * If it's 0, we autoscale to the size of the window. Otherwise it's treated as the ratio between
- * the pixels on the map and output pixels. Only looks proper nice in increments of whole numbers (iirc).
- * Stored here so other parts of the code have a non blocking way of getting a user's functional zoom.
- */
- var/zoom = 0
- /**
- * If the view is currently being supressed by some other "monitor".
- * For when you want to own the client's eye without fucking with their viewport.
- * Doesn't make sense for a binocoler to effect your view in a camera console.
- */
- var/is_suppressed = FALSE
- /// The client that owns this view packet.
- var/client/chief = null
-
-/datum/view_data/New(client/owner, view_string)
- default = view_string
- chief = owner
- apply()
-
-/datum/view_data/proc/setDefault(string)
- default = string
- apply()
-
-/datum/view_data/proc/safeApplyFormat()
- if(isZooming())
- assertFormat()
- return
- resetFormat()
-
-/// T-Pose
-/datum/view_data/proc/assertFormat()
-// if(!is_preference_enabled(/datum/client_preference/scaling_viewport))
-// return
- // winset(chief, "mapwindow.map", "zoom=0")
- // We're using icon dropdown instead
-
-/// Cuck
-/datum/view_data/proc/resetFormat()
-// if(!is_preference_enabled(/datum/client_preference/scaling_viewport))
-// return
- // winset(chief, "mapwindow.map", "zoom=[chief.prefs.pixel_size]")
- // We're using icon dropdown instead
-
-/datum/view_data/proc/setZoomMode()
-// if(!is_preference_enabled(/datum/client_preference/scaling_viewport))
-// return
- // winset(chief, "mapwindow.map", "zoom-mode=[chief.prefs.scaling_method]")
- // We're using icon dropdown instead
-
-/datum/view_data/proc/isZooming()
- return (width || height)
-
-/datum/view_data/proc/resetToDefault()
- width = 0
- height = 0
- apply()
-
-/datum/view_data/proc/add(toAdd)
- width += toAdd
- height += toAdd
- apply()
-
-/datum/view_data/proc/addTo(toAdd)
- var/list/shitcode = getviewsize(toAdd)
- width += shitcode[1]
- height += shitcode[2]
- apply(toAdd)
-
-/datum/view_data/proc/setTo(toAdd)
- /**
- * Backward compatability to account.
- * For a change in how sizes get calculated. we used to include world.view in
- * This, but it was jank, so I had to move it.
- */
- var/list/shitcode = getviewsize(toAdd)
- width = shitcode[1]
- height = shitcode[2]
- apply(toAdd)
-
-/datum/view_data/proc/setBoth(wid, hei)
- width = wid
- height = hei
- apply()
-
-/datum/view_data/proc/setWidth(wid)
- width = wid
- apply()
-
-/datum/view_data/proc/setHeight(hei)
- width = hei
- apply()
-
-/datum/view_data/proc/addToWidth(toAdd)
- width += toAdd
- apply()
-
-/datum/view_data/proc/addToHeight(screen, toAdd)
- height += toAdd
- apply()
-
-/datum/view_data/proc/apply(forced)
- if(!chief.is_preference_enabled(/datum/client_preference/scaling_viewport) && !forced)
- return
- chief.change_view(getView(), forced)
- safeApplyFormat()
- if(chief.prefs.auto_fit_viewport)
- chief.fit_viewport()
-
-/datum/view_data/proc/supress()
- is_suppressed = TRUE
- apply()
-
-/datum/view_data/proc/unsupress()
- is_suppressed = FALSE
- apply()
-
-/datum/view_data/proc/getView()
- var/list/temp = getviewsize(default)
- if(is_suppressed)
- return "[temp[1]]x[temp[2]]"
- return "[width + temp[1]]x[height + temp[2]]"
-
-/datum/view_data/proc/zoomIn()
- resetToDefault()
- animate(chief, pixel_x = 0, pixel_y = 0, 0, FALSE, LINEAR_EASING, ANIMATION_END_NOW)
-
-/datum/view_data/proc/zoomOut(radius = 0, offset = 0, direction = FALSE)
- if(direction)
- var/_x = 0
- var/_y = 0
- switch(direction)
- if(NORTH)
- _y = offset
- if(EAST)
- _x = offset
- if(SOUTH)
- _y = -offset
- if(WEST)
- _x = -offset
- animate(chief, pixel_x = world.icon_size*_x, pixel_y = world.icon_size*_y, 0, FALSE, LINEAR_EASING, ANIMATION_END_NOW)
- // Ready for this one?
- setTo(radius)
-/*
-/proc/getScreenSize(widescreen)
- if(widescreen)
- return CONFIG_GET(string/default_view)
- return CONFIG_GET(string/default_view_square)
-*/
/proc/getviewsize(view)
if(isnum(view))
var/totalviewrange = 1 + 2 * view
@@ -169,9 +5,3 @@
else
var/list/viewrangelist = splittext(view,"x")
return list(text2num(viewrangelist[1]), text2num(viewrangelist[2]))
-
-/proc/in_view_range(mob/user, atom/A)
- var/list/view_range = getviewsize(user.client.view)
- var/turf/source = get_turf(user)
- var/turf/target = get_turf(A)
- return ISINRANGE(target.x, source.x - view_range[1], source.x + view_range[1]) && ISINRANGE(target.y, source.y - view_range[1], source.y + view_range[1])
diff --git a/code/__global_init.dm b/code/__global_init.dm
index 08cc2aa8b61..cafa0772844 100644
--- a/code/__global_init.dm
+++ b/code/__global_init.dm
@@ -24,3 +24,12 @@ var/world_log_redirected = FALSE
/datum/world_log_shunter/New()
world.ensure_logging_active()
+
+//! Debugging
+var/datum/world_debug_enabler/world_debug_enabler = new
+
+/datum/world_debug_enabler/New()
+ var/debug_server = world.GetConfig("env", "AUXTOOLS_DEBUG_DLL")
+ if (debug_server)
+ call(debug_server, "auxtools_init")()
+ enable_debugging()
diff --git a/code/_onclick/cyborg.dm b/code/_onclick/cyborg.dm
index eb8e7ee2c2f..d66d5c6d469 100644
--- a/code/_onclick/cyborg.dm
+++ b/code/_onclick/cyborg.dm
@@ -6,6 +6,7 @@
adjacency code.
*/
+// todo: unify with normal click procs
/mob/living/silicon/robot/ClickOn(var/atom/A, var/params)
if(world.time <= next_click)
return
@@ -15,23 +16,23 @@
build_click(src, client.buildmode, params, A)
return
- var/list/modifiers = params2list(params)
- if(modifiers["shift"] && modifiers["ctrl"])
+ var/list/unpacked_params = params2list(params)
+ if(unpacked_params["shift"] && unpacked_params["ctrl"])
CtrlShiftClickOn(A)
return
- if(modifiers["shift"] && modifiers["middle"])
+ if(unpacked_params["shift"] && unpacked_params["middle"])
ShiftMiddleClickOn(A)
return
- if(modifiers["middle"])
+ if(unpacked_params["middle"])
MiddleClickOn(A)
return
- if(modifiers["shift"])
+ if(unpacked_params["shift"])
ShiftClickOn(A)
return
- if(modifiers["alt"]) // alt and alt-gr (rightalt)
+ if(unpacked_params["alt"]) // alt and alt-gr (rightalt)
AltClickOn(A)
return
- if(modifiers["ctrl"])
+ if(unpacked_params["ctrl"])
CtrlClickOn(A)
return
@@ -58,49 +59,47 @@
return
*/
- var/obj/item/W = get_active_held_item()
+ //? Grab click semantics
+ var/obj/item/I = get_active_held_item()
+ //? Core cyborg code
// Cyborgs have no range-checking unless there is item use
- if(!W)
+ if(!I)
if(bolt && !bolt.malfunction && A.loc != module)
return
A.add_hiddenprint(src)
A.attack_robot(src)
return
- // buckled cannot prevent machine interlinking but stops arm movement
- if( buckled )
+ //? Handle special cases
+ if(I == A)
+ // attack_self
+ I.attack_self(src)
+ // todo: refactor
+ trigger_aiming(TARGET_CAN_CLICK)
return
- if(W == A)
+ //? check if we can click from our current location
+ var/ranged_generics_allowed = loc?.AllowClick(src, A, I)
- W.attack_self(src)
- return
-
- // cyborgs are prohibited from using storage items so we can I think safely remove (A.loc in contents)
- if(A == loc || (A in loc) || (A in contents))
- // No adjacency checks
-
- var/resolved = A.attackby(W, src, params)
- if(!resolved && A && W)
- W.afterattack(A,src,1,params)
- return
-
- if(!isturf(loc))
- return
-
- // cyborgs are prohibited from using storage items so we can I think safely remove (A.loc && isturf(A.loc.loc))
- if(isturf(A) || isturf(A.loc))
- if(A.Adjacent(src)) // see adjacent.dm
-
- var/resolved = A.attackby(W, src, params)
- if(!resolved && A && W)
- W.afterattack(A, src, 1, params)
- return
+ if(Reachability(A, null, I?.reach, I))
+ //? attempt melee attack chain
+ if(I)
+ I.melee_attack_chain(A, src, CLICKCHAIN_HAS_PROXIMITY, unpacked_params)
else
- W.afterattack(A, src, 0, params)
- return
- return
+ melee_attack_chain(A, CLICKCHAIN_HAS_PROXIMITY, unpacked_params)
+ // todo: refactor aiming
+ trigger_aiming(TARGET_CAN_CLICK)
+ return
+ else if(ranged_generics_allowed)
+ //? attempt ranged attack chain
+ if(I)
+ I.ranged_attack_chain(A, src, NONE, params)
+ else
+ ranged_attack_chain(A, NONE, unpacked_params)
+ // todo: refactor aiming
+ trigger_aiming(TARGET_CAN_CLICK)
+ return
//Middle click cycles through selected modules.
/mob/living/silicon/robot/MiddleClickOn(var/atom/A)
diff --git a/code/_onclick/hud/_screen_object.dm b/code/_onclick/hud/_screen_object.dm
index 2f8c9178b61..78c80cd123e 100644
--- a/code/_onclick/hud/_screen_object.dm
+++ b/code/_onclick/hud/_screen_object.dm
@@ -9,7 +9,7 @@
/atom/movable/screen
name = ""
icon = 'icons/mob/screen1.dmi'
- appearance_flags = TILE_BOUND|PIXEL_SCALE|NO_CLIENT_COLOR
+ appearance_flags = PIXEL_SCALE | NO_CLIENT_COLOR
layer = LAYER_HUD_BASE
plane = PLANE_PLAYER_HUD
var/obj/master = null //A reference to the object in the slot. Grabs or items, generally.
diff --git a/code/_onclick/hud/ability_screen_objects.dm b/code/_onclick/hud/ability_screen_objects.dm
index e6e4a2db14c..aef68a6f5a3 100644
--- a/code/_onclick/hud/ability_screen_objects.dm
+++ b/code/_onclick/hud/ability_screen_objects.dm
@@ -176,8 +176,6 @@
/mob/Login()
. = ..()
- if(client?.is_preference_enabled(/datum/client_preference/scaling_viewport))
- INVOKE_ASYNC(client, /client.verb/SetWindowIconSize, client.prefs.icon_size)
if(ability_master)
ability_master.toggle_open(1)
diff --git a/code/_rendering/clickcatcher/clickcatcher.dm b/code/_rendering/clickcatcher/clickcatcher.dm
index 6ea14a666e0..92f25672ec5 100644
--- a/code/_rendering/clickcatcher/clickcatcher.dm
+++ b/code/_rendering/clickcatcher/clickcatcher.dm
@@ -31,7 +31,9 @@
*/
/atom/movable/screen/click_catcher/proc/UpdateFill(view_size_x, view_size_y)
- screen_loc = "1,1 to [view_size_x],[view_size_y]"
+ var/matrix/transforming = matrix()
+ transforming.Scale(view_size_x, view_size_y)
+ transform = transforming
/atom/movable/screen/click_catcher/Click(location, control, params)
var/list/modifiers = params2list(params)
@@ -63,12 +65,8 @@
/**
* Makes a clickcatcher if necessary, and ensures it's fit to our size.
*/
-/client/proc/update_clickcatcher(list/view_override)
+/client/proc/update_clickcatcher()
if(!click_catcher)
click_catcher = new
screen |= click_catcher
- if(view_override)
- click_catcher.UpdateFill(view_override[1], view_override[2])
- else
- var/list/view_list = getviewsize(view)
- click_catcher.UpdateFill(view_list[1], view_list[2])
+ click_catcher.UpdateFill(current_viewport_width, current_viewport_height)
diff --git a/code/_rendering/mob.dm b/code/_rendering/mob.dm
index 4035b71cce2..413af28f7d1 100644
--- a/code/_rendering/mob.dm
+++ b/code/_rendering/mob.dm
@@ -14,6 +14,16 @@
reload_fullscreen()
client.update_clickcatcher()
+ INVOKE_ASYNC(client, /client/proc/init_viewport_blocking)
+
+/**
+ * reloads rendering after screen viewport size change
+ */
+/mob/proc/refit_rendering()
+ client?.parallax_holder?.Reset(force = TRUE)
+ reload_fullscreen()
+ client?.update_clickcatcher()
+
/**
* destroys screen rendering. call on mob del
*/
diff --git a/code/_rendering/parallax/parallax_holder.dm b/code/_rendering/parallax/parallax_holder.dm
index c2863b2f37b..20c436c35ce 100644
--- a/code/_rendering/parallax/parallax_holder.dm
+++ b/code/_rendering/parallax/parallax_holder.dm
@@ -175,6 +175,8 @@
continue
L.SetView(C.view, TRUE)
. |= L
+ if(vis_holder)
+ . |= vis_holder
C.screen |= .
if(!secondary_map)
var/atom/movable/screen/plane_master/parallax_white/PM = locate() in C.screen
@@ -191,6 +193,7 @@
if(QDELETED(C))
return
C.screen -= layers
+ C.screen -= vis_holder
if(!secondary_map)
var/atom/movable/screen/plane_master/parallax_white/PM = locate() in C.screen
if(PM)
@@ -311,4 +314,5 @@
parallax_holder = new(src)
/atom/movable/screen/parallax_vis
- screen_loc = "CENTER,CENTER"
+ screen_loc = "LEFT,BOTTOM"
+ icon = null
diff --git a/code/_rendering/parallax/parallax_object.dm b/code/_rendering/parallax/parallax_object.dm
index e0c909fba6c..f807d1b3798 100644
--- a/code/_rendering/parallax/parallax_object.dm
+++ b/code/_rendering/parallax/parallax_object.dm
@@ -5,7 +5,7 @@
plane = PARALLAX_PLANE
screen_loc = "CENTER-7,CENTER-7"
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
- appearance_flags = PIXEL_SCALE | KEEP_TOGETHER
+ appearance_flags = PIXEL_SCALE | KEEP_TOGETHER | TILE_BOUND
// notice - all parallax layers are 15x15 tiles. They roll over every 240 pixels.
/// pixel x/y shift per real x/y
diff --git a/code/controllers/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game_options.dm
index cfd8435bd3f..4152e18afd0 100644
--- a/code/controllers/configuration/entries/game_options.dm
+++ b/code/controllers/configuration/entries/game_options.dm
@@ -1,3 +1,30 @@
+/datum/config_entry/string/max_viewport_size
+ default = "19x15"
+
+/datum/config_entry/string/max_viewport_size/ValidateAndSet(str_val)
+ if(!istext(str_val))
+ return FALSE
+ var/list/split = splittext(str_val, "x")
+ if(length(split) != 2)
+ return FALSE
+ var/width = text2num(split[1])
+ var/height = text2num(split[2])
+ if(height < 1 || width < 1 || height > 67 || width > 67)
+ return FALSE
+ . = ..()
+ if(!.)
+ return
+ config.update_player_viewsize()
+
+/*
+/datum/config_entry/string/game_viewport_size
+ default = "19x15"
+
+/datum/config_entry/string/max_viewport_size/ValidateAndSet(str_val)
+ . = ..()
+*/
+//! BYONd world.view is immutable, this config is left here as a todo. If it's still immutable by, say, 516, remove it.
+
/datum/config_entry/number/starlight
default = 2
@@ -51,13 +78,3 @@
/datum/config_entry/flag/emojis
default = TRUE
-
-/datum/config_entry/number/max_client_view_x
- default = 21
- min_val = 15
- max_val = 50 // Byond (the) limits
-
-/datum/config_entry/number/max_client_view_y
- default = 15
- min_val = 15
- max_val = 50 // Byond (the) limits
diff --git a/code/datums/mocking/client.dm b/code/datums/mocking/client.dm
index fd99e34520c..cb7d37ff967 100644
--- a/code/datums/mocking/client.dm
+++ b/code/datums/mocking/client.dm
@@ -11,3 +11,6 @@
/// The mob the client controls
var/mob/mob
+
+ var/current_viewport_width = 15
+ var/current_viewport_height = 15
diff --git a/code/datums/perspective.dm b/code/datums/perspective.dm
index a6587b74b74..de3728ac07c 100644
--- a/code/datums/perspective.dm
+++ b/code/datums/perspective.dm
@@ -3,6 +3,25 @@
*
* allows managed control of client viewport/eye changes
*
+ * ! - - - ATTENTION - - -
+ * ! These datums will modify mob variables.
+ * ! This can result in AI mobs having inconsistent sight/whatevers
+ * ! from what they'd usually have after a player enters, and
+ * ! leaves them.
+ * ! Mob reset_perspective() should handle most of these cases, but be wary.
+ *
+ * ! - - - ATTENTION - - -
+ * ! View sizes should only be for clients.
+ * ! Do not read off this for stuff like AI view/target tracking.
+ *
+ * ? - - - ATTENTION - - -
+ * ? We track MAXIMUM view sizes.
+ * ? Client can resize up/down as needed.
+ * ? If you want to force a client to a specific size,
+ * ? regardless of their widescreen setting,
+ * ? use client.set_temporary_view.
+ * ? Augmented view will also ignore client prefs on widescreen, *kinda.*
+ *
* as of right now, perspectives will **trample** the following on every set:
* client.eye
* client.lazy_eye (unimplemented)
@@ -42,11 +61,9 @@
/// sight var
var/sight = SEE_SELF
/// active clients - this is not the same as mobs because a client can be looking somewhere that isn't their mob
- var/list/client/clients = list()
+ var/list/client/clients
/// mobs that are using this - required for clean gcs
var/list/mob/mobs = list()
- /// view size
- var/view_size
/// when a client logs out of a mob, and it's using us, the mob should reset to its self_perspective
var/reset_on_logout = TRUE
/// see in dark
@@ -54,6 +71,22 @@
/// see_invisible
var/see_invisible = SEE_INVISIBLE_LIVING
+ //! view size
+ /// default view; if null, world.view
+ var/default_view_size
+ /// view size increase x
+ var/augment_view_width = 0
+ /// view size increase y
+ var/augment_view_height = 0
+ /// suppression sources
+ var/list/view_suppression
+ /// cached view x
+ var/cached_view_width
+ /// cached view y
+ var/cached_view_height
+ /// view cache needs recompute
+ var/view_dirty = TRUE
+
/datum/perspective/Destroy()
KickAll()
ClearMobs()
@@ -73,7 +106,7 @@
return
if(C.using_perspective)
CRASH("client already had perspective")
- clients += C
+ LAZYADD(clients, C)
C.using_perspective = src
SEND_SIGNAL(src, COMSIG_PERSPECTIVE_CLIENT_REGISTER, C)
Apply(C)
@@ -85,7 +118,7 @@
SHOULD_CALL_PARENT(TRUE)
if(!(C in clients))
return
- clients -= C
+ LAZYREMOVE(clients, C)
Remove(C)
// if we're not doing this as part of a switch have them immediately switch to the mob
// oh and make sure they unregister
@@ -100,7 +133,7 @@
* gets all clients viewing us
*/
/datum/perspective/proc/GetClients()
- return clients.Copy()
+ return isnull(clients)? list() : clients.Copy()
/**
* kicks all clients off us
@@ -118,6 +151,7 @@
/**
* registers as a mob's current perspective
+ * sets mob vars as necessary
*/
/datum/perspective/proc/AddMob(mob/M)
SHOULD_CALL_PARENT(TRUE)
@@ -127,13 +161,20 @@
return
mobs += M
M.using_perspective = src
+ M.sight = sight
+ M.see_in_dark = see_in_dark
+ M.see_invisible = see_invisible
SEND_SIGNAL(src, COMSIG_PERSPECTIVE_MOB_ADD, M)
/**
* unregisters as a mob's current perspective
+ * resets mob vars to initial() values
*/
/datum/perspective/proc/RemoveMob(mob/M, switching = FALSE)
SHOULD_CALL_PARENT(TRUE)
+ M.sight = initial(M.sight)
+ M.see_in_dark = initial(M.see_in_dark)
+ M.see_invisible = initial(M.see_invisible)
mobs -= M
SEND_SIGNAL(src, COMSIG_PERSPECTIVE_MOB_REMOVE, M, switching)
if(M.using_perspective == src)
@@ -184,11 +225,7 @@
if(changed != C.eye)
C.parallax_holder?.Reset(force = TRUE)
C.perspective = GetEyeMode(C)
- C.mob.sight = sight
- C.mob.see_in_dark = see_in_dark
- C.mob.see_invisible = see_invisible
- if(view_size)
- C.change_view(view_size, TRUE)
+ update_view_size(C)
/**
* update all viewers
@@ -294,13 +331,94 @@
for(var/client/C as anything in clients)
C.mob.see_invisible = see_invisible
-/datum/perspective/proc/SetViewSize(new_size)
- var/change = view_size == new_size
- view_size = new_size
- if(change)
- for(var/client/C as anything in clients)
- C.change_view(new_size, TRUE)
+//! view size
+/**
+ * Sets our default size.
+ *
+ * DO NOT use this in place of augmented.
+ * User widescreen preferences WILL override this!
+ *
+ * Ideally this shouldn't be used at all.
+ */
+/datum/perspective/proc/set_default_view(size)
+ if(size == default_view_size)
+ return
+ // assert it y'know, works
+ if(isnum(size))
+ default_view_size = size
+ else if(isnull(size))
+ default_view_size = size
+ else if(istext(size))
+ var/list/split = splittext(size, "x")
+ ASSERT(split.len == 2)
+ // if the above runtimes, let it so we know who fucked up
+ default_view_size = size
+ else
+ CRASH("What?")
+ view_dirty = TRUE
+ update_view_size()
+/**
+ * Sets augmented size ontop of default size.
+ *
+ * Currently only supports positive numbers. Numbers apply equally
+ * to both directions of a dimension.
+ */
+/datum/perspective/proc/set_augmented_view(width, height)
+ if(!isnum(height) || !isnum(width))
+ CRASH("invalid")
+ // no negative augment values for now
+ augment_view_height = max(0, height)
+ augment_view_width = max(0, width)
+ view_dirty = TRUE
+ update_view_size()
+
+/datum/perspective/proc/update_view_size(client/C)
+ if(view_dirty)
+ recompute_view_size()
+ if(C)
+ C.request_viewport_update()
+ else
+ for(var/client/_C as anything in clients)
+ update_view_size(_C)
+
+/datum/perspective/proc/recompute_view_size()
+ view_dirty = FALSE
+ if(isnum(default_view_size))
+ cached_view_height = default_view_size + augment_view_width
+ cached_view_width = default_view_size + augment_view_height
+ return
+ else if(isnull(default_view_size))
+ cached_view_width = GLOB.max_client_view_x + augment_view_width
+ cached_view_height = GLOB.max_client_view_y + augment_view_height
+ return
+ var/list/parsed = splittext(default_view_size, "x")
+ if(length(parsed) != 2)
+ cached_view_width = GLOB.max_client_view_x + augment_view_width
+ cached_view_height = GLOB.max_client_view_y + augment_view_height
+ return
+ cached_view_width = parsed[1] + augment_view_width
+ cached_view_height = parsed[2] + augment_view_height
+
+/datum/perspective/proc/suppress_view(source)
+ var/was = LAZYLEN(view_suppression)
+ LAZYOR(view_suppression, source)
+ if(!was && LAZYLEN(view_suppression))
+ view_dirty = TRUE
+ update_view_size()
+
+/datum/perspective/proc/unsuppress_view(source)
+ var/was = LAZYLEN(view_suppression)
+ LAZYREMOVE(view_suppression, source)
+ if(was && !LAZYLEN(view_suppression))
+ view_dirty = TRUE
+ update_view_size()
+
+/datum/perspective/proc/ensure_view_cached()
+ if(view_dirty)
+ recompute_view_size()
+
+//! remotes
/datum/perspective/proc/considered_remote(mob/M)
return eye == M
diff --git a/code/game/atoms/atoms_movable.dm b/code/game/atoms/atoms_movable.dm
index eea32440766..da36fc7b20c 100644
--- a/code/game/atoms/atoms_movable.dm
+++ b/code/game/atoms/atoms_movable.dm
@@ -1,6 +1,7 @@
/atom/movable
layer = OBJ_LAYER
- appearance_flags = TILE_BOUND|PIXEL_SCALE|KEEP_TOGETHER
+ // todo: evaluate if we need TILE_BOUND
+ SET_APPEARANCE_FLAGS(TILE_BOUND | PIXEL_SCALE)
// todo: kill this (only used for elcetropacks)
var/moved_recently = FALSE
diff --git a/code/game/mecha/combat/fighter.dm b/code/game/mecha/combat/fighter.dm
index 3abcdefd5b1..749d09ddee5 100644
--- a/code/game/mecha/combat/fighter.dm
+++ b/code/game/mecha/combat/fighter.dm
@@ -225,7 +225,12 @@
//causes damage when running into objects
/obj/mecha/combat/fighter/Bump(atom/obstacle)
. = ..()
+ // this isn't defined becuase this is shitcode anyways and i'm just patching it
+ // todo: why the fuck are you guys doing snowflake collision code??
+ if(TIMER_COOLDOWN_CHECK(src, "fighter_collision"))
+ return
if(istype(obstacle, /obj) || istype(obstacle, /turf))
+ TIMER_COOLDOWN_START(src, "fighter_collision", 5 SECONDS)
occupant_message("Collision Alert!")
take_damage(20, "brute")
playsound(src, 'sound/effects/grillehit.ogg', 50, 1)
diff --git a/code/game/mecha/mecha.dm b/code/game/mecha/mecha.dm
index 183d9528199..3c151c9ff0c 100644
--- a/code/game/mecha/mecha.dm
+++ b/code/game/mecha/mecha.dm
@@ -837,7 +837,7 @@
/obj/mecha/proc/mechstep(direction)
var/current_dir = dir //For strafing
var/result = get_step(src,direction)
- if(result && Move(result))
+ if(result && Move(result, direction))
if(stomp_sound)
playsound(src,stomp_sound,40,1)
handle_equipment_movement()
diff --git a/code/game/mecha/mecha_actions.dm b/code/game/mecha/mecha_actions.dm
index 891159fb3af..46f019bcb5e 100644
--- a/code/game/mecha/mecha_actions.dm
+++ b/code/game/mecha/mecha_actions.dm
@@ -363,10 +363,10 @@
src.log_message("Toggled zoom mode.")
src.occupant_message("Zoom mode [zoom?"en":"dis"]abled.")
if(zoom)
- myclient.change_view(12, TRUE)
+ myclient.set_temporary_view(GLOB.max_client_view_x + 5, GLOB.max_client_view_y + 5)
src.occupant << sound('sound/mecha/imag_enh.ogg',volume=50)
else
- myclient.change_view("[myclient.last_view_x_dim*2]x[myclient.last_view_y_dim*2]", TRUE) // Reset to default
+ myclient.reset_temporary_view()
return
diff --git a/code/game/objects/_items.dm b/code/game/objects/_items.dm
index 96f1bc1b407..7efc229789a 100644
--- a/code/game/objects/_items.dm
+++ b/code/game/objects/_items.dm
@@ -629,11 +629,11 @@ modules/mob/living/carbon/human/life.dm if you die, you will be zoomed out.
else if(!zoom && user.item_by_slot(wornslot) != src && wornslot != FALSE)
to_chat(user, "You need to wear the [devicename] to look through it properly")
cannotzoom = 1
-
//We checked above if they are a human and returned already if they weren't.
var/mob/living/carbon/human/H = user
if(!zoom && !cannotzoom)
- INVOKE_ASYNC(H.client, /client.proc/change_view, viewsize, TRUE) // Reset to default
+ H.ensure_self_perspective()
+ H.self_perspective.set_augmented_view(viewsize - 2, viewsize - 2)
zoom = 1
var/tilesize = 32
@@ -652,14 +652,12 @@ modules/mob/living/carbon/human/life.dm if you die, you will be zoomed out.
if (WEST)
H.client.pixel_x = -viewoffset
H.client.pixel_y = 0
-
H.visible_message("[user] peers through the [zoomdevicename ? "[zoomdevicename] of the [src.name]" : "[src.name]"].")
H.looking_elsewhere = TRUE
H.handle_vision()
-
else
-
-
+ H.ensure_self_perspective()
+ H.self_perspective.set_augmented_view(0, 0)
zoom = 0
H.client.pixel_x = 0
@@ -667,8 +665,6 @@ modules/mob/living/carbon/human/life.dm if you die, you will be zoomed out.
H.looking_elsewhere = FALSE
H.handle_vision()
- INVOKE_ASYNC(H.client, H.client.is_preference_enabled(/datum/client_preference/scaling_viewport) ? /client.verb/OnResize : /client.proc/change_view, world.view) // Reset to default
-
if(!cannotzoom)
user.visible_message("[zoomdevicename ? "[user] looks up from the [src.name]" : "[user] lowers the [src.name]"].")
diff --git a/code/game/objects/effects/map_effects/portal.dm b/code/game/objects/effects/map_effects/portal.dm
index 0611f6def2a..536863d3247 100644
--- a/code/game/objects/effects/map_effects/portal.dm
+++ b/code/game/objects/effects/map_effects/portal.dm
@@ -50,7 +50,7 @@ when portals are shortly lived, or when portals are made to be obvious with spec
opacity = TRUE
plane = TURF_PLANE
layer = ABOVE_TURF_LAYER
- appearance_flags = PIXEL_SCALE|KEEP_TOGETHER // Removed TILE_BOUND so things not visible on the other side stay hidden from the viewer.
+ SET_APPEARANCE_FLAGS(PIXEL_SCALE)
var/obj/effect/map_effect/portal/counterpart = null // The portal line or master that this is connected to, on the 'other side'.
diff --git a/code/game/world.dm b/code/game/world.dm
index bdc185f122e..a22d9fe1582 100644
--- a/code/game/world.dm
+++ b/code/game/world.dm
@@ -18,13 +18,6 @@ GLOBAL_LIST(topic_status_cache)
//This happens after the Master subsystem new(s) (it's a global datum)
//So subsystems globals exist, but are not initialised
/world/New()
- //! init auxtools
- var/debug_server = world.GetConfig("env", "AUXTOOLS_DEBUG_DLL")
- if (debug_server)
- call(debug_server, "auxtools_init")()
- enable_debugging()
- // AUXTOOLS_CHECK(AUXTOOLS_YAML)
-
log_world("World loaded at [TIME_STAMP("hh:mm:ss", FALSE)]!")
var/tempfile = "data/logs/config_error.[GUID()].log" //temporary file used to record errors with loading config, moved to log directory once logging is set
@@ -40,6 +33,7 @@ GLOBAL_LIST(topic_status_cache)
InitTgs()
config.Load(params[OVERRIDE_CONFIG_DIRECTORY_PARAMETER])
+ config.update_world_viewsize() //! Since world.view is immutable, we load it here.
//SetupLogs depends on the RoundID, so lets check
//DB schema and set RoundID if we can
diff --git a/code/modules/admin/verbs/SDQL2/SDQL_2_wrappers.dm b/code/modules/admin/verbs/SDQL2/SDQL_2_wrappers.dm
index e67da7bef99..c60de4eabe0 100644
--- a/code/modules/admin/verbs/SDQL2/SDQL_2_wrappers.dm
+++ b/code/modules/admin/verbs/SDQL2/SDQL_2_wrappers.dm
@@ -54,6 +54,12 @@
/proc/_ispath(path, type)
return ispath(path, type)
+/proc/_json_encode(list/L)
+ return json_encode(L)
+
+/proc/_json_decode(json)
+ return json_decode(json)
+
/proc/_length(E)
return length(E)
@@ -153,9 +159,17 @@
/proc/_sin(X)
return sin(X)
+/proc/_list2params(L)
+ return list2params(L)
+
/proc/_list_construct(...)
. = args.Copy()
+/proc/_list_construct_assoc(...)
+ . = list()
+ for(var/i in 1 to (args.len - 1) step 2)
+ .[args[i]] = args[i+1]
+
/proc/_list_add(list/L, ...)
if (args.len < 2)
return
diff --git a/code/modules/admin/verbs/randomverbs.dm b/code/modules/admin/verbs/randomverbs.dm
index 26c9f8a9d17..2dbfdc5d133 100644
--- a/code/modules/admin/verbs/randomverbs.dm
+++ b/code/modules/admin/verbs/randomverbs.dm
@@ -831,11 +831,11 @@ Traitors and the like can also be revived with the previous role mostly intact.
set name = "Change View Range"
set desc = "switches between 1x and custom views"
- var/view = src.view
- if(view == world.view)
- view = input("Select view range:", "FUCK YE", 7) in list(1,2,3,4,5,6,7,8,9,10,11,12,13,14,128)
+ if(!using_temporary_viewsize)
+ var/number = input("Select view range:", "FUCK YE", 7) in list(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,20,22,24,26,28,30,32,34)
+ set_temporary_view(number * 2 + 1, number * 2 + 1)
else
- SetWindowIconSize(prefs.icon_size)
+ reset_temporary_view()
log_admin("[key_name(usr)] changed their view range to [view].")
//message_admins("[key_name_admin(usr)] changed their view range to [view].", 1) //why? removed by order of XSI
diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm
index 765a4532320..304d73c8023 100644
--- a/code/modules/client/client_defines.dm
+++ b/code/modules/client/client_defines.dm
@@ -53,6 +53,35 @@
/// the perspective we're currently using
var/datum/perspective/using_perspective
+ //! Viewport
+ /// what we *think* their current viewport size is in pixels
+ var/assumed_viewport_spx
+ /// what we *think* their current viewport size is in pixels
+ var/assumed_viewport_spy
+ /// what we *think* their current viewport zoom is
+ var/assumed_viewport_zoom
+ /// what we *think* their current viewport letterboxing setting is
+ var/assumed_viewport_box
+ /// current view x - for fast access
+ var/current_viewport_width
+ /// current view y - for fast access
+ var/current_viewport_height
+ /// if things are manipulating the viewport we don't want other things to touch it
+ var/viewport_rwlock = TRUE //! default block so we can release it during init_viewport
+ /// viewport update queued?
+ var/viewport_queued = FALSE
+ /// forced temporary view
+ var/temporary_viewsize_width
+ /// forced temporary view
+ var/temporary_viewsize_height
+ /// temporary view active?
+ var/using_temporary_viewsize = FALSE
+
+ //! menu button statuses
+ var/list/menu_buttons_checked = list()
+ //! menu group statuses
+ var/list/menu_group_status = list()
+
////////////////
//ADMIN THINGS//
////////////////
@@ -149,6 +178,3 @@
/// If this client has been fully initialized or not
var/fully_created = FALSE
-
- /// datum wrapper for client view
- var/datum/view_data/view_size
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index 9c58fe62cdf..f9e7330cf00 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -151,32 +151,38 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
///////////
/client/New(TopicData)
+ //! pre-connect-ish
+ // set appadmin for profiling or it might not work (?) (this is old code we just assume it's here for a reason)
world.SetConfig("APP/admin", ckey, "role=admin")
- //var/tdata = TopicData //save this for later use
- TopicData = null //Prevent calls to client.Topic from connect
-
- if(connection != "seeker" && connection != "web")//Invalid connection type.
+ // block client.Topic() calls from connect
+ TopicData = null
+ // kick out invalid connections
+ if(connection != "seeker" && connection != "web")
return null
-
+ // kick out guests
if(!config_legacy.guests_allowed && IsGuestKey(key))
alert(src,"This server doesn't allow guest accounts to play. Please go to http://www.byond.com/ and register for a key.","Guest","OK")
del(src)
return
-
+ // pre-connect greeting
to_chat(src, "If the title screen is black, resources are still downloading. Please be patient until the title screen appears.")
-
+ // register in globals
GLOB.clients += src
GLOB.directory[ckey] = src
+ //! Resolve storage datums
// resolve persistent data
persistent = resolve_client_data(ckey)
- // todo: move resolve database data up here
+ // todo: move resolve database data up here but above preferences
+ // todo: move preferences up here but above persistent
+ //! Setup user interface
+ // todo: move top level menu here, for now it has to be under prefs.
// Instantiate tgui panel
tgui_panel = new(src, "browseroutput")
+ //! Setup admin tooling
GLOB.ahelp_tickets.ClientLogin(src)
- SSserver_maint.UpdateHubStatus()
var/connecting_admin = FALSE //because de-admined admins connecting should be treated like admins.
//Admin Authorisation
holder = admin_datums[ckey]
@@ -221,6 +227,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
if(!debug_tools_allowed)
world.SetConfig("APP/admin", ckey, null)
//END CITADEL EDIT
+ // todo: refactor and hoist
//preferences datum - also holds some persistent data for the client (because we may as well keep these datums to a minimum)
prefs = GLOB.preferences_datums[ckey]
if(!prefs)
@@ -228,10 +235,15 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
GLOB.preferences_datums[ckey] = prefs
prefs.client = src
+ // todo: refactor
prefs.last_ip = address //these are gonna be used for banning
prefs.last_id = computer_id //these are gonna be used for banning
//fps = prefs.clientfps //(prefs.clientfps < 0) ? RECOMMENDED_FPS : prefs.clientfps
+ // todo: hoist
+ // build top level menu
+ GLOB.main_window_menu.setup(src)
+
var/full_version = "[byond_version].[byond_build ? byond_build : "xxx"]"
log_access("Login: [key_name(src)] from [address ? address : "localhost"]-[computer_id] || BYOND v[full_version]")
/*
@@ -382,6 +394,8 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
send_resources()
+ //? Startup rendering
+ pre_init_viewport()
mob.reload_rendering()
if(prefs.lastchangelog != GLOB.changelog_hash) //bolds the changelog button on the interface so we know there are updates.
@@ -401,8 +415,13 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
if(isnum(account_age) && account_age <= 2)
log_and_message_admins("PARANOIA: [key_name(src)] has a very new BYOND account ([account_age] days).")
+ //? We are done
+ // set initialized
initialized = TRUE
+ // show any migration errors
prefs.auto_flush_errors()
+ // update our hub label
+ SSserver_maint.UpdateHubStatus()
//////////////
//DISCONNECT//
@@ -744,40 +763,38 @@ GLOBAL_VAR_INIT(log_clicks, FALSE)
. = ..()
/client/proc/change_view(new_size, forced, translocate)
- if(!is_preference_enabled(/datum/client_preference/scaling_viewport))
- if (isnull(new_size))
- CRASH("change_view called without argument.")
+ set waitfor = FALSE // to async temporary view
+ // todo: refactor this, client view changes should be ephemeral.
+ var/list/L = getviewsize(new_size)
+ set_temporary_view(L[1], L[2])
- if(view == new_size)
- // unnecessary
- return
+/**
+ * directly sets our view
+ * you should probably be using perspective datums most of the time instead
+ * WARNING: this is verbatim; aka, view = 7 is 15 width 15 height, NOT 7x7!
+ *
+ * furthermore, this proc is BLOCKING.
+ */
+/client/proc/set_temporary_view(width, height)
+ if(!width || !height || width < 0 || height < 0)
+ reset_temporary_view()
+ return
+ using_temporary_viewsize = FALSE
+ temporary_viewsize_width = width
+ temporary_viewsize_height = height
+ request_viewport_update()
- /*
- if(prefs && !prefs.widescreenpref && new_size == CONFIG_GET(string/default_view))
- new_size = CONFIG_GET(string/default_view_square)
- */
-
- view = new_size
- mob.reload_rendering()
- if(!translocate)
- reset_perspective()
- else if(!. || forced)
- if(!translocate)
- reset_perspective()
- mob?.reload_fullscreen()
- update_clickcatcher()
- if(forced)
- view = new_size
- else
- INVOKE_ASYNC(src, /client.verb/OnResize)
- /*
- mob.reload_fullscreen()
- if (isliving(mob))
- var/mob/living/M = mob
- M.update_damage_hud()
- if (prefs.auto_fit_viewport)
- addtimer(CALLBACK(src,.verb/fit_viewport,10)) //Delayed to avoid wingets from Login calls.
- */
+/**
+ * resets our temporary view
+ * you should probably be using perspective datums most of the time instead
+ *
+ * furthermore, this proc is BLOCKING
+ */
+/client/proc/reset_temporary_view()
+ using_temporary_viewsize = FALSE
+ temporary_viewsize_height = null
+ temporary_viewsize_width = null
+ request_viewport_update()
/**
* switch perspective - null will cause us to shunt our eye to nullspace!
diff --git a/code/modules/client/verbs/ooc.dm b/code/modules/client/verbs/ooc.dm
index c09fd9e5a2d..f9449480d4e 100644
--- a/code/modules/client/verbs/ooc.dm
+++ b/code/modules/client/verbs/ooc.dm
@@ -235,49 +235,3 @@
if(eyeobj)
return eyeobj
return src
-
-/client/verb/fit_viewport()
- set name = "Fit Viewport"
- set category = "OOC"
- set desc = "Fit the width of the map window to match the viewport"
-
- // Fetch the client's aspect ratio
- var/view_size = getviewsize(view)
- var/aspect_ratio = view_size[1] / view_size[2]
-
- // Calculate desired pixel width using window size and aspect ratio
- var/sizes = params2list(winget(src, "mainwindow.split;mapwindow", "size"))
- var/map_size = splittext(sizes["mapwindow.size"], "x")
- var/height = text2num(map_size[2])
- var/desired_width = round(height * aspect_ratio)
- if (text2num(map_size[1]) == desired_width)
- return // You're already good fam.
-
- var/split_size = splittext(sizes["mainwindow.split.size"], "x")
- var/split_width = text2num(split_size[1])
-
- // Calculate and apply our best estimate
- // +4 pixels are for the eidth of the splitter's handle
- var/pct = 100 * (desired_width + 4) / split_width
- winset(src, "mainwindow.split", "splitter=[pct]")
-
- // Apply an ever-lowering offset until we finish or fail
- var/delta
- for(var/safety in 1 to 10)
- var/after_size = winget(src, "mapwindow", "size")
- map_size = splittext(after_size, "x")
- var/got_width = text2num(map_size[1])
-
- if (got_width == desired_width)
- return // Success!
-
- // Calculate a probable delta value based on the diff
- else if (isnull(delta))
- delta = 100 * (desired_width - got_width) / split_width
-
- // If we overshot, halve the delta and reverse direction
- else if ((delta > 0 && got_width > desired_width) || (delta < 0 && got_width < desired_width))
- delta = -delta/2
-
- pct += delta
- winset(src, "mainwindow.split", "splitter=[pct]")
diff --git a/code/modules/client/viewport.dm b/code/modules/client/viewport.dm
index cfcb3d87e78..f2e49f035ab 100644
--- a/code/modules/client/viewport.dm
+++ b/code/modules/client/viewport.dm
@@ -1,78 +1,401 @@
-GLOBAL_VAR_INIT(lock_client_view_x, null)
-GLOBAL_VAR_INIT(lock_client_view_y, null)
-GLOBAL_LIST_INIT(valid_icon_sizes, list(32, 48, 64, 72, 96, 128))
+// these two variables control max dynamic resize for viewport
+GLOBAL_VAR_INIT(max_client_view_x, 19)
+GLOBAL_VAR_INIT(max_client_view_y, 15)
+// these two variables **reflect** (config updates, not this!) effective default viewsize for view(), range(), etc.
+GLOBAL_VAR_INIT(game_view_x, 19)
+GLOBAL_VAR_INIT(game_view_y, 15)
+// minimum size viewports can be
+GLOBAL_VAR_INIT(min_client_view_x, 15)
+GLOBAL_VAR_INIT(min_client_view_y, 15)
+// these two variables, if set, lock all clients to a certain viewsize no matter what
+GLOBAL_VAR(lock_client_view_x)
+GLOBAL_VAR(lock_client_view_y)
-/datum/preferences
- var/icon_size = 64
-
-/client
- var/last_view_x_dim = 7
- var/last_view_y_dim = 7
-
-
-/client/verb/SetCozyViewInwards(val as num|text)
- set hidden = 1
-
- if(prefs && val != prefs.icon_size)
- prefs.icon_size += val
- SScharacters.queue_preferences_save(prefs)
- winset(src, "mapwindow.map", "icon-size=[prefs.icon_size]")
- OnResize()
-
-/client/verb/SetWindowIconSize(val as num|text)
- set hidden = 1
- winset(src, "mapwindow.map", "icon-size=[val]")
- if(prefs && val != prefs.icon_size)
- prefs.icon_size = val
- SScharacters.queue_preferences_save(prefs)
- OnResize()
-
-/client/verb/OnResize()
- set hidden = 1
- if(!is_preference_enabled(/datum/client_preference/scaling_viewport))
- return
- var/divisor = text2num(winget(src, "mapwindow.map", "icon-size")) || world.icon_size
- if(!isnull(GLOB.lock_client_view_x) && !isnull(GLOB.lock_client_view_y))
- last_view_x_dim = GLOB.lock_client_view_x
- last_view_y_dim = GLOB.lock_client_view_y
+/**
+ * forces world viewsize; use for config VAS
+ */
+/datum/controller/configuration/proc/update_world_viewsize()
+ //! BYONd world.view is immutable, for now we're stuck read-onlying it.
+ if(isnum(world.view))
+ GLOB.game_view_x = GLOB.game_view_y = world.view
else
- var/winsize_string = winget(src, "mapwindow.map", "size")
- last_view_x_dim = GLOB.lock_client_view_x || clamp(ROUND_UP(text2num(winsize_string) / divisor), 15, (CONFIG_GET(number/max_client_view_x)) || (CONFIG_GET(number/max_client_view_x)))
- last_view_y_dim = GLOB.lock_client_view_y || clamp(ROUND_UP(text2num(copytext(winsize_string,findtext(winsize_string,"x")+1,0)) / divisor), 15, (CONFIG_GET(number/max_client_view_y)) || 41)
+ // is text
+ var/list/viewsize = splittext(world.view, "x")
+ GLOB.game_view_x = viewsize[1]
+ GLOB.game_view_y = viewsize[2]
- if(last_view_x_dim % 2 == 0)
- last_view_x_dim++
- if(last_view_y_dim % 2 == 0)
- last_view_y_dim++
+/**
+ * forces screensize update; use for config VAS
+ */
+/datum/controller/configuration/proc/update_player_viewsize()
+ var/viewsize_raw = CONFIG_GET(string/max_viewport_size)
+ var/list/viewsize = splittext(viewsize_raw, "x")
+ GLOB.max_client_view_x = text2num(viewsize[1])
+ GLOB.max_client_view_y = text2num(viewsize[2])
- for(var/check_icon_size in GLOB.valid_icon_sizes)
- winset(src, "menu.icon[check_icon_size]", "is-checked=false")
- winset(src, "menu.icon[divisor]", "is-checked=true")
+//! Core viewport procs
+//! These don't obey the read-write lock, and should only be called from the synchronized procs!
- view = "[last_view_x_dim]x[last_view_y_dim]"
+/**
+ * populates important vars that things may read early before we sleep
+ */
+/client/proc/pre_init_viewport()
+ PRIVATE_PROC(TRUE)
+ current_viewport_width = GLOB.max_client_view_x
+ current_viewport_height = GLOB.max_client_view_y
- // Reset eye/perspective
- reset_perspective()
-/* var/last_perspective = perspective
+/**
+ * called on client init to do this without blocking client/New
+ */
+/client/proc/init_viewport_blocking()
+ PRIVATE_PROC(TRUE)
+ fetch_viewport()
+ refit_viewsize()
+ // release the initial lock
+ viewport_rwlock = FALSE
+
+/**
+ * called to manually update viewport vars since the skin macro is only triggered on resize
+ *
+ * return TRUE if changed
+ */
+/client/proc/fetch_viewport()
+ PRIVATE_PROC(TRUE)
+ . = FALSE
+ // get vars only; they have to manually refit
+ var/list/got = params2list(winget(src, SKIN_MAP_ID_VIEWPORT, "size;zoom;letterbox"))
+ var/got_zoom = text2num(got["zoom"]) || 0
+ var/got_box = (got["letterbox"] == "true")
+ var/list/split = splittext(got["size"], "x")
+ var/got_spx
+ var/got_spy
+ if(length(split) == 2)
+ got_spx = text2num(split[1]) || (WORLD_ICON_SIZE * GLOB.game_view_x)
+ got_spy = text2num(split[2]) || (WORLD_ICON_SIZE * GLOB.game_view_y)
+ else
+ stack_trace("fetch_viewport failed to get spx/spy")
+ got_spx = (WORLD_ICON_SIZE * GLOB.game_view_x)
+ got_spy = (WORLD_ICON_SIZE * GLOB.game_view_y)
+ if(got_zoom != assumed_viewport_zoom || got_spx != assumed_viewport_spx || got_spy != assumed_viewport_spy || got_box != assumed_viewport_box)
+ . = TRUE
+ assumed_viewport_zoom = got_zoom
+ assumed_viewport_spx = got_spx
+ assumed_viewport_spy = got_spy
+ assumed_viewport_box = got_box
+
+/**
+ * called to refit our view size as necessary
+ *
+ * this is automatically called every time something modifies us
+ */
+/client/proc/refit_viewsize(no_fit)
+ PRIVATE_PROC(TRUE)
+ if(!isnull(GLOB.lock_client_view_x) && !isnull(GLOB.lock_client_view_y))
+ view = "[GLOB.lock_client_view_x]x[GLOB.lock_client_view_y]"
+ on_refit_viewsize(GLOB.lock_client_view_x, GLOB.lock_client_view_y, no_fit)
+ return
+ if(using_temporary_viewsize)
+ view = "[temporary_viewsize_width]x[temporary_viewsize_height]"
+ on_refit_viewsize(temporary_viewsize_width, temporary_viewsize_height, no_fit)
+ return
+ var/widescreen = is_widescreen_enabled()
+ if(!widescreen)
+ // if not widescreen, we should just force to 15 x 15 and their augmented view
+ var/width = 15 + (using_perspective.augment_view_width * 2)
+ var/height = 15 + (using_perspective.augment_view_height * 2)
+ view = "[width]x[height]"
+ on_refit_viewsize(width, height, no_fit)
+ return
+ var/stretch_to_fit = assumed_viewport_zoom == 0
+ using_perspective.ensure_view_cached()
+ var/max_width = using_perspective.cached_view_width
+ var/max_height = using_perspective.cached_view_height
+ if(stretch_to_fit)
+ // option 1: they're stretching to fit
+ if(assumed_viewport_box)
+ // fit everything
+ view = "[max_width]x[max_height]"
+ on_refit_viewsize(max_width, max_height)
+ return
+ // option 2: they're stretching to fit the longest side
+ else
+ // todo: handle horizontally-wider view sizes
+ // for now we only care about horizontal fit
+ var/stretch_pixel_amount = assumed_viewport_spy / max_height
+ var/available_width = assumed_viewport_spx / stretch_pixel_amount
+ available_width = CEILING(available_width, 1)
+ available_width = clamp(available_width, GLOB.min_client_view_x, max_width)
+ view = "[available_width]x[max_height]"
+ on_refit_viewsize(available_width, max_height, no_fit)
+ return
+ // option 3: scale as necessary
+ var/pixels_per_tile = assumed_viewport_zoom * WORLD_ICON_SIZE
+ var/div_x = assumed_viewport_spx / pixels_per_tile
+ var/div_y = assumed_viewport_spy / pixels_per_tile
+ div_x = CEILING(div_x, 1)
+ div_y = CEILING(div_y, 1)
+ var/desired_width = clamp(div_x, GLOB.min_client_view_x, max_width)
+ var/desired_height = clamp(div_y, GLOB.min_client_view_y, max_height)
+ view = "[desired_width]x[desired_height]"
+ on_refit_viewsize(desired_width, desired_height, no_fit)
+
+/client/proc/on_refit_viewsize(new_width, new_height, no_fit)
+ var/changed = (current_viewport_height != new_height) || (current_viewport_width != new_width)
+ if(changed)
+ current_viewport_width = new_width
+ current_viewport_height = new_height
+ if(!no_fit && is_auto_fit_viewport_enabled())
+ fit_viewport()
+ post_refit_viewsize(changed)
+
+/**
+ * updates everything when our viewport changes
+ */
+/client/proc/post_refit_viewsize(changed)
+ if(!changed)
+ return
+ // force perspective swap for ??? reasons (???)
+ var/old_perspective = perspective
perspective = MOB_PERSPECTIVE
- if(perspective != last_perspective)
- perspective = last_perspective
- var/last_eye = eye
- eye = mob
- if(eye != last_eye)
- eye = last_eye
- if(mob)
- mob.reload_fullscreen() */
- update_clickcatcher()
+ if(old_perspective != perspective)
+ perspective = old_perspective
+ // i don't understand the above
+ mob?.refit_rendering()
-/client/verb/force_onresize_view_update()
- set name = "Force Client View Update"
+/**
+ * called to fit our viewport to what we **should** have to get our effective view.
+ */
+/client/proc/fit_viewport()
+ // by now we already fetched viewport
+ // figure out how big they want it to be based on their settings
+ // note: we only care about horizontal because the splitter.. is, well, horizontal.
+ var/desired_pixel_width
+ // todo: pref
+ // maximum % they're willing to have their map
+ var/maximum_splitter_percent = 80
+ // splitter intrinsic width
+ var/assumed_splitter_width = 4
+ // effective size
+ var/widescreen = is_widescreen_enabled()
+ var/effective_view_width
+ var/effective_view_height
+ // todo: optimize the shit out of this proc and the view system
+ if(!isnull(GLOB.lock_client_view_x) && !isnull(GLOB.lock_client_view_y))
+ effective_view_width = GLOB.lock_client_view_x
+ effective_view_height = GLOB.lock_client_view_y
+ else if(widescreen)
+ effective_view_width = clamp(temporary_viewsize_width || using_perspective.cached_view_width, GLOB.min_client_view_x, 67)
+ effective_view_height = clamp(temporary_viewsize_height || using_perspective.cached_view_height, GLOB.min_client_view_y, 67)
+ else
+ effective_view_width = clamp(temporary_viewsize_width || (15 + (using_perspective.augment_view_width * 2)), GLOB.min_client_view_x, 67)
+ effective_view_height = clamp(temporary_viewsize_height || (15 + (using_perspective.augment_view_height * 2)), GLOB.min_client_view_y, 67)
+ if(assumed_viewport_zoom == 0)
+ // they're stretching to fit; this makes it annoying
+ var/aspect_ratio = effective_view_width / effective_view_height
+ // we have to fit everything
+ // viewport_spy should never change since that's not what the player can drag,
+ // unless they resize the outer window itself.
+ desired_pixel_width = assumed_viewport_spy * aspect_ratio
+ else
+ // they're not using stretch to fit; this makes it very easy
+ desired_pixel_width = WORLD_ICON_SIZE * assumed_viewport_zoom * effective_view_width
+ // grab their screen size (or what counts as it)
+ var/list/fetching = winget(src, SKIN_SPLITTER_ID_MAIN, "size")
+ var/list/parsed = splittext(fetching, "x")
+ var/screen_width = text2num(parsed[1])
+ // grab what percent we should go to
+ var/current_percent = min(100 * ((desired_pixel_width + assumed_splitter_width) / screen_width), maximum_splitter_percent)
+ // initial set
+ winset(src, SKIN_SPLITTER_ID_MAIN, "splitter=[current_percent]")
+ // funny method of correcting for misalignments via binary-search-like behavior
+ var/delta
+ fetching = winget(src, SKIN_MAP_ID_VIEWPORT, "size")
+ parsed = splittext(fetching, "x")
+ var/current_width = text2num(parsed[1])
+ var/last_width = current_width
+ if(current_width == desired_pixel_width)
+ // done
+ return
+ // nope, start adjusting
+ delta = 100 * ((desired_pixel_width - current_width) / screen_width)
+ for(var/safety in 1 to 10)
+ current_percent = min(current_percent + delta, maximum_splitter_percent)
+ winset(src, SKIN_SPLITTER_ID_MAIN, "splitter=[current_percent]")
+ fetching = winget(src, SKIN_MAP_ID_VIEWPORT, "size")
+ parsed = splittext(fetching, "x")
+ current_width = text2num(parsed[1])
+ // done?
+ if(current_width == desired_pixel_width)
+ return
+ // if we're literally down to a pixel how about like Don't
+ if(last_width == current_width)
+ return
+ last_width = current_width
+ // keep adjusting
+ // overshot?
+ if ((delta > 0 && current_width > desired_pixel_width) || (delta < 0 && current_width < desired_pixel_width))
+ // reverse in half
+ delta = -delta / 2
+
+//! Synchronized procs; calling these from outside is fine.
+/**
+ * call this when things change to queue an update
+ *
+ * should only be called **in code**, not by the skin!
+ *
+ * warning: this proc doesn't refit instantly, it only queues.
+ * client decides what time is best to refit.
+ */
+/client/proc/request_viewport_update()
+ set waitfor = FALSE
+ if(!viewport_rwlock)
+ _request_viewport_update()
+ if(!viewport_queued)
+ viewport_queued = TRUE
+ addtimer(CALLBACK(src, .proc/_request_viewport_update), 0)
+
+/**
+ * call this when things change to queue an update
+ *
+ * should only be called **in code**, not by the skin!
+ *
+ * blocks until finished
+ */
+/client/proc/request_viewport_update_blocking()
+ if(!viewport_rwlock)
+ _request_viewport_update()
+ return
+ if(!viewport_queued)
+ viewport_queued = TRUE
+ addtimer(CALLBACK(src, .proc/_request_viewport_update), 0)
+ UNTIL(!viewport_queued)
+
+// todo : locks are probably bad when working with request fit
+/client/proc/_request_viewport_update(no_fit, no_fetch)
+ PRIVATE_PROC(TRUE)
+ // if rwlocked, wait; people should be queuing and not calling this directly
+ UNTIL(!viewport_rwlock)
+ // clear queued
+ viewport_queued = FALSE
+ // lock
+ viewport_rwlock = TRUE
+ if(!no_fetch)
+ fetch_viewport()
+ // refit size based on viewport
+ refit_viewsize(no_fit)
+ // release lock
+ viewport_rwlock = FALSE
+
+/**
+ * call this to request a viewport fit
+ */
+/client/proc/request_viewport_fit(no_recalc)
+ set waitfor = FALSE
+ _request_viewport_fit(no_recalc)
+
+/**
+ * call this to request a viewport fit
+ * forcefully fits
+ * blocks until finished
+ * don't call this from non-buttons because it blocks and
+ * absolutely can stack and ruin everyone's day.
+ */
+/client/proc/viewport_fit_blocking(no_recalc)
+ UNTIL(!viewport_rwlock)
+ return _request_viewport_fit(no_recalc)
+
+// todo : locks are probably bad when working with request update
+/client/proc/_request_viewport_fit(no_recalc)
+ PRIVATE_PROC(TRUE)
+ // if viewport is locked, ignore it
+ if(viewport_rwlock)
+ return
+ // acquire lock
+ viewport_rwlock = TRUE
+ // just in case because this is called by a lot of things that are too shitcoded to update (like my own code)
+ fetch_viewport()
+ // fit viewport
+ fit_viewport()
+ // fetch values; we do not trust on-size to trigger
+ fetch_viewport()
+ // then refit the viewsize if needed
+ if(!no_recalc)
+ refit_viewsize()
+ // release lock
+ viewport_rwlock = FALSE
+
+/**
+ * called directly by the skin when resizing
+ *
+ * @params
+ * - w - width of viewport in pixels
+ * - h - height of viewport in pixels
+ * - z - zoom of viewport
+ * - b - are we letterboxing?
+ */
+/client/verb/on_viewport(w as text, h as text, z as num, b as num)
+ set name = ".on_viewport"
+ set hidden = TRUE
+ if(viewport_rwlock) // something is fucking around, don't edit for them
+ return
+ // get vars
+ assumed_viewport_spx = text2num(w)
+ assumed_viewport_spy = text2num(h)
+ assumed_viewport_zoom = z
+ assumed_viewport_box = b
+ // refit
+ _request_viewport_update(TRUE, TRUE)
+
+/**
+ * automatically fit their viewport to show everything optimally
+ */
+/client/verb/user_fit_viewport()
+ set name = "Fit Viewport"
+ set category = "OOC"
+ set desc = "Fit the width of the map window to match the viewport"
+ request_viewport_fit()
+
+/client/verb/force_map_zoom(n as num)
+ set name = ".viewport_zoom"
+ set hidden = TRUE
set src = usr
set category = "Debug"
- OnResize()
+ if(!isnum(n) || n < 0)
+ to_chat(usr, SPAN_WARNING("invalid number"))
+ return
+ if(viewport_rwlock)
+ to_chat(usr, SPAN_WARNING("Viewport is rwlocked; try again later."))
+ return
+ winset(src, SKIN_MAP_ID_VIEWPORT, "zoom=[n]")
+ assumed_viewport_zoom = n
+ request_viewport_update(TRUE)
+
+/client/verb/force_map_box(n as num)
+ set name = ".viewport_box"
+ set hidden = TRUE
+ set src = usr
+ set category = "Debug"
+ if(viewport_rwlock)
+ to_chat(usr, SPAN_WARNING("Viewport is rwlocked; try again later."))
+ return
+ n = !!n // force bool
+ winset(src, SKIN_MAP_ID_VIEWPORT, "letterbox=[n? "true" : "false"]")
+ assumed_viewport_box = n
+ request_viewport_update(TRUE)
+
+/client/verb/force_onresize_view_update()
+ set name = ".viewport_update"
+ set hidden = TRUE
+ set src = usr
+ set category = "Debug"
+ if(viewport_rwlock)
+ to_chat(usr, SPAN_WARNING("Viewport is rwlocked; try again later."))
+ return
+ request_viewport_update()
/client/verb/show_winset_debug_values()
- set name = "Show Client View Debug Values"
+ set name = ".viewport_debug"
+ set hidden = TRUE
set src = usr
set category = "Debug"
@@ -83,3 +406,5 @@ GLOBAL_LIST_INIT(valid_icon_sizes, list(32, 48, 64, 72, 96, 128))
to_chat(usr, "Icon size: [divisor]")
to_chat(usr, "xDim: [round(text2num(winsize_string) / divisor)]")
to_chat(usr, "yDim: [round(text2num(copytext(winsize_string,findtext(winsize_string,"x")+1,0)) / divisor)]")
+ to_chat(usr, "server thinks viewport is:[assumed_viewport_spx]/[assumed_viewport_spy]/[assumed_viewport_zoom]/[assumed_viewport_box]")
+ to_chat(usr, "server thinks view is:[current_viewport_width]x[current_viewport_height]")
diff --git a/code/modules/client/winset_wrappers.dm b/code/modules/client/winset_wrappers.dm
index 96050471ff5..4c467629174 100644
--- a/code/modules/client/winset_wrappers.dm
+++ b/code/modules/client/winset_wrappers.dm
@@ -12,3 +12,6 @@
/client/proc/_winshow(window, show)
winshow(src, window, show)
+
+/client/proc/_command(str)
+ winset(src, null, "command=[str]")
diff --git a/code/modules/mapping/map_helpers/network_builder/power_cable.dm b/code/modules/mapping/map_helpers/network_builder/power_cable.dm
index 55b5c0f7188..d5c61e30007 100644
--- a/code/modules/mapping/map_helpers/network_builder/power_cable.dm
+++ b/code/modules/mapping/map_helpers/network_builder/power_cable.dm
@@ -40,7 +40,7 @@
/obj/map_helper/network_builder/power_cable/build()
if(!network_directions)
return
- var/knot = KNOT_FORCED || (KNOT_AUTO && detect_knot())
+ var/knot = (src.knot == KNOT_FORCED) || ((src.knot == KNOT_AUTO) && detect_knot())
if(knot)
for(var/i in GLOB.cardinal)
if(!(network_directions & i))
diff --git a/code/modules/mob/living/carbon/human/human_defines.dm b/code/modules/mob/living/carbon/human/human_defines.dm
index a42907e7eed..21b9d64b89c 100644
--- a/code/modules/mob/living/carbon/human/human_defines.dm
+++ b/code/modules/mob/living/carbon/human/human_defines.dm
@@ -4,6 +4,7 @@
voice_name = "unknown"
icon = 'icons/effects/effects.dmi' //We have an ultra-complex update icons that overlays everything, don't load some stupid random male human
icon_state = "nothing"
+ SET_APPEARANCE_FLAGS(TILE_BOUND | PIXEL_SCALE | KEEP_TOGETHER)
/// To check if we've need to roll for damage on movement while an item is imbedded in us.
var/embedded_flag
diff --git a/code/modules/mob/new_player/login.dm b/code/modules/mob/new_player/login.dm
index fd95d20c3b5..192471fa842 100644
--- a/code/modules/mob/new_player/login.dm
+++ b/code/modules/mob/new_player/login.dm
@@ -19,9 +19,6 @@ GLOBAL_DATUM_INIT(lobby_image, /obj/effect/lobby_image, new)
icon_state = known_icon_states[1]
. = ..()
-/mob/new_player
- var/client/my_client // Need to keep track of this ourselves, since by the time Logout() is called the client has already been nulled
-
/mob/new_player/Login()
update_Login_details() //handles setting lastKnownIP and computer_id for use by the ban systems as well as checking for multikeying
@@ -37,7 +34,6 @@ GLOBAL_DATUM_INIT(lobby_image, /obj/effect/lobby_image, new)
mind.current = src
loc = null
- my_client = client
GLOB.player_list |= src
new_player_panel()
@@ -45,5 +41,4 @@ GLOBAL_DATUM_INIT(lobby_image, /obj/effect/lobby_image, new)
if(client)
handle_privacy_poll()
client.playtitlemusic()
- client.is_preference_enabled(/datum/client_preference/scaling_viewport) ? client.OnResize() : client.change_view(world.view)
return ..()
diff --git a/code/modules/multi-tile/multi-tile.dm b/code/modules/multi-tile/multi-tile.dm
index 3300a9e18ea..e19320086a0 100644
--- a/code/modules/multi-tile/multi-tile.dm
+++ b/code/modules/multi-tile/multi-tile.dm
@@ -21,11 +21,11 @@
// check enter
if(!T2.Enter(src, T))
return FALSE
-
+
// uh oh, second loop, check exits
for(var/turf/T as anything in locs)
if(!(T in moving_to))
if(!T.Exit(src, get_step(T, stepdir)))
return FALSE
-
+
return TRUE
diff --git a/code/modules/multiz/turf.dm b/code/modules/multiz/turf.dm
index 15da416aabc..2baf2c7a352 100644
--- a/code/modules/multiz/turf.dm
+++ b/code/modules/multiz/turf.dm
@@ -227,6 +227,7 @@
bottom_turf.plane = src.plane
bottom_turf.color = below.color
bottom_turf.copy_overlays(below)
+ // todo: zmimic
bottom_turf.appearance_flags = KEEP_TOGETHER
underlays = list(bottom_turf)
diff --git a/code/modules/preferences/_preferences.dm b/code/modules/preferences/_preferences.dm
index 407f2d43852..ca4e3175bb0 100644
--- a/code/modules/preferences/_preferences.dm
+++ b/code/modules/preferences/_preferences.dm
@@ -40,6 +40,8 @@ GLOBAL_LIST_EMPTY(preferences_datums)
var/list/character
/// global data
var/list/options
+ /// data for byond skin - checkboxes and whatnot; this is ENTIRELY synchronized by the skin system.
+ var/list/skin
//! ## Game Preferences
var/tgui_fancy = TRUE
@@ -252,6 +254,7 @@ GLOBAL_LIST_EMPTY(preferences_datums)
load_path(client.ckey)
if(load_preferences())
if(load_character())
+ load_skin()
sanitize_everything()
player_setup.sanitize_setup()
client.update_movement_keys()
diff --git a/code/modules/preferences/data.dm b/code/modules/preferences/data.dm
index f23ce4a764c..e8222a277bd 100644
--- a/code/modules/preferences/data.dm
+++ b/code/modules/preferences/data.dm
@@ -268,3 +268,11 @@
for(var/key in skipped)
errors?.Add("Skipping key [key] - no datum found.")
return TRUE
+
+//! Skin stuff
+/datum/preferences/proc/set_skin_data(key, value)
+ skin[key] = value
+ save_skin()
+
+/datum/preferences/proc/get_skin_data(key)
+ return skin[key]
diff --git a/code/modules/preferences/preference_setup/global/setting_datums.dm b/code/modules/preferences/preference_setup/global/setting_datums.dm
index 24beca3aa08..54d35a75597 100644
--- a/code/modules/preferences/preference_setup/global/setting_datums.dm
+++ b/code/modules/preferences/preference_setup/global/setting_datums.dm
@@ -293,13 +293,6 @@ var/list/_client_preferences_by_type
disabled_description = "Hide"
enabled_by_default = TRUE
-/datum/client_preference/scaling_viewport
- description = "Scaling Viewport"
- key = "scaling_viewport"
- enabled_description = "Show"
- disabled_description = "Hide"
- enabled_by_default = TRUE
-
/********************
* Staff Preferences *
********************/
diff --git a/code/modules/preferences/preferences_savefile.dm b/code/modules/preferences/preferences_savefile.dm
index ae69e8930b2..37f037d9be6 100644
--- a/code/modules/preferences/preferences_savefile.dm
+++ b/code/modules/preferences/preferences_savefile.dm
@@ -8,7 +8,7 @@
if(!path)
return FALSE
// check DOS guard
- if(world.time < loadprefcooldown) //This is done before checking if the file exists to ensure that the server can't hang on read attempts
+ if(initialized && world.time < loadprefcooldown) //This is done before checking if the file exists to ensure that the server can't hang on read attempts
if(istype(client))
to_chat(client, "You're attempting to load your preferences a little too fast. Wait half a second, then try again.")
return FALSE
@@ -48,7 +48,7 @@
if(!path)
return FALSE
// check DOS guard
- if(world.time < saveprefcooldown)
+ if(initialized && world.time < saveprefcooldown)
if(istype(client))
to_chat(client, "You're attempting to save your preferences a little too fast. Wait half a second, then try again.")
return FALSE
@@ -69,12 +69,42 @@
auto_flush_errors()
return TRUE
+/datum/preferences/proc/save_skin()
+ if(!path)
+ return FALSE
+ // heyooo no interruptions please
+ if(initialized)
+ UNTIL(world.time >= saveprefcooldown)
+ var/savefile/S = new /savefile(path)
+ if(!S)
+ return FALSE
+ S.cd = "/"
+ skin = sanitize_islist(skin)
+ WRITE_FILE(S["skin"], skin)
+ return TRUE
+
+/datum/preferences/proc/load_skin()
+ if(!path)
+ return FALSE
+ // heyooo no interruptions please
+ if(initialized)
+ UNTIL(world.time >= loadprefcooldown)
+ if(!fexists(path))
+ return FALSE
+ var/savefile/S = new /savefile(path)
+ if(!S)
+ return FALSE
+ S.cd = "/"
+ READ_FILE(S["skin"], skin)
+ skin = sanitize_islist(skin)
+ return TRUE
+
/datum/preferences/proc/load_character(slot)
// todo: storage handler datums...
if(!path)
return FALSE
// check DOS guard
- if(world.time < loadcharcooldown)
+ if(initialized && world.time < loadcharcooldown)
if(istype(client))
to_chat(client, "You're attempting to load your character a little too fast. Wait half a second, then try again.")
return FALSE
@@ -134,7 +164,7 @@
if(!path)
return FALSE
// check DOS guard
- if(world.time < savecharcooldown)
+ if(initialized && world.time < savecharcooldown)
if(istype(client))
to_chat(client, "You're attempting to save your character a little too fast. Wait half a second, then try again.")
return FALSE
diff --git a/code/modules/preferences/preferences_toggle_procs.dm b/code/modules/preferences/preferences_toggle_procs.dm
index 145e5487889..47e552c6ca6 100644
--- a/code/modules/preferences/preferences_toggle_procs.dm
+++ b/code/modules/preferences/preferences_toggle_procs.dm
@@ -306,19 +306,6 @@
feedback_add_details("admin_verb","TOHChat")
-/client/verb/toggle_scaling_viewport()
- set name = "Toggle Scaling Viewport"
- set category = "Preferences"
- set desc = "Enable/Disable Viewport Scaling."
-
- var/pref_path = /datum/client_preference/scaling_viewport
- toggle_preference(pref_path)
- SScharacters.queue_preferences_save(prefs)
-
- to_chat(src, "You will now [(is_preference_enabled(/datum/client_preference/scaling_viewport)) ? "see" : "not see"] more...")
- is_preference_enabled(/datum/client_preference/scaling_viewport) ? OnResize() : change_view(world.view)
- feedback_add_details("admin_verb","TOHChat")
-
//Toggles for Staff
//Developers
diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm
index 24fb462a674..124ece8a9f3 100644
--- a/code/modules/projectiles/projectile.dm
+++ b/code/modules/projectiles/projectile.dm
@@ -430,9 +430,8 @@
var/y = text2num(screen_loc_Y[1]) * 32 + text2num(screen_loc_Y[2]) - 32
//Calculate the "resolution" of screen based on client's view and world's icon size. This will work if the user can view more tiles than average.
- var/list/screenview = user.client? getviewsize(user.client.view) : world.view
- var/screenviewX = screenview[1] * world.icon_size
- var/screenviewY = screenview[2] * world.icon_size
+ var/screenviewX = user.client.current_viewport_width * world.icon_size
+ var/screenviewY = user.client.current_viewport_height * world.icon_size
var/ox = round(screenviewX/2) - user.client.pixel_x //"origin" x
var/oy = round(screenviewY/2) - user.client.pixel_y //"origin" y
diff --git a/code/modules/tgui/states.dm b/code/modules/tgui/states.dm
index 601f6af4d93..edbc733d326 100644
--- a/code/modules/tgui/states.dm
+++ b/code/modules/tgui/states.dm
@@ -29,8 +29,8 @@
// Regular ghosts can always at least view if in range.
if(user.client)
- var/clientviewlist = getviewsize(user.client.view)
- if(get_dist(src_object, user) < max(clientviewlist[1], clientviewlist[2]))
+ // todo: in view range for zooming
+ if(get_dist(src_object, user) < max(CEILING(user.client.current_viewport_width / 2, 1), CEILING(user.client.current_viewport_height / 2, 1)))
. = max(., UI_UPDATE)
// Check if the state allows interaction
diff --git a/code/modules/tgui/states/default.dm b/code/modules/tgui/states/default.dm
index f25f85b785f..59167784ebc 100644
--- a/code/modules/tgui/states/default.dm
+++ b/code/modules/tgui/states/default.dm
@@ -31,8 +31,8 @@ GLOBAL_DATUM_INIT(default_state, /datum/ui_state/default, new)
return
// Robots can interact with anything they can see.
- var/list/clientviewlist = getviewsize(client.view)
- if(get_dist(src, src_object) <= min(clientviewlist[1],clientviewlist[2]))
+ // todo: in view range for zooming
+ if(get_dist(src, src_object) <= min(CEILING(client.current_viewport_width / 2, 1), CEILING(client.current_viewport_height / 2, 1)))
return UI_INTERACTIVE
return UI_DISABLED // Otherwise they can keep the UI open.
diff --git a/code/modules/tgui/status_composers.dm b/code/modules/tgui/status_composers.dm
index 73f3b4ac37d..a061b6c6208 100644
--- a/code/modules/tgui/status_composers.dm
+++ b/code/modules/tgui/status_composers.dm
@@ -35,8 +35,7 @@
// Regular ghosts can always at least view if in range.
var/datum/client_interface/client = GET_CLIENT(user)
if(client)
- var/clientviewlist = getviewsize(client.view)
- if(get_dist(source, user) < max(clientviewlist[1], clientviewlist[2]))
+ if(get_dist(source, user) < max(client.current_viewport_width, client.current_viewport_height))
return UI_UPDATE
return UI_CLOSE
@@ -74,8 +73,8 @@
/mob/living/silicon/robot/get_ui_access(atom/source)
// Robots can interact with anything they can see.
- var/list/clientviewlist = getviewsize(client.view)
- if(get_dist(src, source) <= min(clientviewlist[1],clientviewlist[2]))
+ // todo: in view range for zooming
+ if(get_dist(src, source) <= min(CEILING(client.current_viewport_width / 2, 1), CEILING(client.current_viewport_height / 2, 1)))
return UI_INTERACTIVE
return UI_DISABLED // Otherwise they can keep the UI open.
diff --git a/code/modules/tooltip/tooltip.dm b/code/modules/tooltip/tooltip.dm
index 79a5c8b0556..0e07254c3ae 100644
--- a/code/modules/tooltip/tooltip.dm
+++ b/code/modules/tooltip/tooltip.dm
@@ -75,8 +75,7 @@ Notes:
params = {"{ "cursor": "[params]", "screenLoc": "[thing.screen_loc]" }"}
//Send stuff to the tooltip
- var/view_size = getviewsize(owner.view)
- owner << output(list2params(list(params, view_size[1] , view_size[2], "[title][content]", theme, special)), "[control]:tooltip.update")
+ owner << output(list2params(list(params, owner.current_viewport_width, owner.current_viewport_height, "[title][content]", theme, special)), "[control]:tooltip.update")
//If a hide() was hit while we were showing, run hide() again to avoid stuck tooltips
showing = 0
diff --git a/config/entries/game_options.txt b/config/entries/game_options.txt
index 55729334e71..53df494473d 100644
--- a/config/entries/game_options.txt
+++ b/config/entries/game_options.txt
@@ -1,3 +1,10 @@
+## Max distance for clients to view with widescren mode enabled. Maximum: 35 in either dimension.
+MAX_VIEWPORT_SIZE 19x15
+## Effective distance used for world.view, affects stuff like AI target aquisition. Maximum: 35 in either dimension.
+## GAME_VIEWPORT_SIZE 15x19
+## AT THE CURRENT TIME, BYOND WORLD.VIEW IS READ-ONLY
+## THIS CONFIGURATION IS LEFT HERE AS A TODO.
+
## Set to 0 to disable holidays (you monster)
ALLOW_HOLIDAYS
@@ -10,7 +17,3 @@ ENGINE_SUBMAP EngineSubmap_RUST 40
ENGINE_SUBMAP EngineSubmap_Tesla 20
## Singulo
ENGINE_SUBMAP EngineSubmap_Singulo 20
-
-## Max distance for clients to view with widescren mode enabled.
-MAX_CLIENT_VIEW_X 21
-MAX_CLIENT_VIEW_Y 15
diff --git a/interface/menus/_menus.dm b/interface/menus/_menus.dm
new file mode 100644
index 00000000000..0d766f8312b
--- /dev/null
+++ b/interface/menus/_menus.dm
@@ -0,0 +1,334 @@
+//! skin menus
+/**
+ * dynamic in-code menu system with blackjack and hookers
+ */
+/datum/skin_menu
+ /// id - used for both skin and savefiles
+ var/id
+ /// id of window to bind to; we will OVERWRITE ANY EXISTING MENU.
+ var/bind_to
+ /// category datum types - transformed into datums at runtime
+ var/list/datum/skin_menu_category/categories
+ /// during initial build, we will construct groups list, group name associated to button id for default
+ var/list/button_groups
+
+/datum/skin_menu/New()
+ init_categories()
+
+/datum/skin_menu/Destroy()
+ QDEL_LIST(categories)
+ button_groups = null
+ return ..()
+
+/datum/skin_menu/proc/init_categories()
+ button_groups = list()
+ for(var/i in 1 to categories.len)
+ if(ispath(categories[i]))
+ var/path = categories[i]
+ var/datum/skin_menu_category/C = new path(src)
+ categories[i] = C
+ // category will init in /New(), so...
+ for(var/datum/skin_menu_entry/E as anything in C.entries)
+ // again let it runtime if it's bad, we WANT it to runtime so integration tests fail
+ if(E.group)
+ button_groups |= E.group
+ if(E.is_default)
+ button_groups[E.group] = E.id
+ else
+ continue // wtf? let it runtime later
+ // verify
+ for(var/group in button_groups)
+ if(!button_groups[group])
+ stack_trace("no default for group [group]")
+
+/datum/skin_menu/proc/creation_list(client/C)
+ . = list()
+ // create categories after
+ for(var/datum/skin_menu_category/cat as anything in categories)
+ . |= cat.creation_list(C)
+
+/datum/skin_menu/proc/setup(client/C, load = TRUE)
+ create_and_bind(C)
+ if(load)
+ load_settings(C)
+
+/datum/skin_menu/proc/all_entries()
+ . = list()
+ for(var/datum/skin_menu_category/C as anything in categories)
+ for(var/datum/skin_menu_entry/E as anything in C.entries)
+ if(istype(C, /datum/skin_menu_entry/spacer))
+ continue
+ . += E
+
+/datum/skin_menu/proc/load_settings(client/C)
+ // let's like, not do that before they load
+ UNTIL(C.prefs.initialized)
+ // basically we need to load saved settings, and default if not
+ for(var/datum/skin_menu_entry/E as anything in all_entries())
+ if(!E.checkbox || !E.is_saved)
+ continue // don't care
+ // don't load groups, they only load enable, not disable
+ if(E.group)
+ // we, do, however, want to load *unchecking* it.
+ winset(C, E.id, "is-checked=false")
+ continue
+ // load anything else
+ var/enabled = C.prefs.skin[E.id]
+ if(isnull(enabled))
+ // default
+ enabled = E.is_default
+ // save it while we're at it
+ E.save(C, enabled)
+ E.load(C, enabled)
+ // we do need to load groups if possible
+ for(var/group in button_groups)
+ var/choice = C.prefs.skin[group]
+ var/datum/skin_menu_entry/E
+ if(isnull(choice) || !GLOB.skin_menu_entries[choice])
+ // default if null or if choice doesn't exist anymore
+ E = GLOB.skin_menu_entries[button_groups[group]]
+ // save it while we're at it
+ E.save(C, TRUE)
+ else
+ E = GLOB.skin_menu_entries[choice]
+ E.load(C, TRUE)
+
+/datum/skin_menu/proc/create_and_bind(client/C)
+ // menu creation is special
+ // we must winclone; skin must not have a control named "menu"
+ winclone(C, SKIN_ID_ABSTRACT_MENU, id)
+ var/list/creation = creation_list(C)
+ for(var/id in creation)
+ winset(C, id, creation[id])
+ winset(C, bind_to, "menu=[id]")
+
+//! skin menu categories
+/datum/skin_menu_category
+ /// id - used for both skin and savefiles
+ var/id
+ /// name
+ var/name
+ /// entry datum types - transformed into datums at runtime; insert a null to create a divider
+ var/list/datum/skin_menu_entry/entries
+ /// menu we belong to
+ var/datum/skin_menu/menu
+
+/datum/skin_menu_category/New(datum/skin_menu/menu)
+ src.menu = menu
+ init_entries()
+
+/datum/skin_menu_category/Destroy()
+ QDEL_LIST(entries)
+ return ..()
+
+/datum/skin_menu_category/proc/init_entries()
+ for(var/i in 1 to entries.len)
+ var/path = entries[i]
+ var/datum/skin_menu_entry/entry
+ if(isnull(path))
+ entry = new /datum/skin_menu_entry/spacer(src)
+ else if(ispath(path))
+ entry = new path(src)
+ else
+ continue // wtf? let it runtime later
+ entries[i] = entry
+
+/datum/skin_menu_category/proc/creation_list(client/C)
+ . = list()
+ // create us first
+ .[id] = list2params(list(
+ "parent" = menu.id,
+ "name" = name,
+ ))
+ // create our children
+ for(var/datum/skin_menu_entry/E as anything in entries)
+ .[E.id] = E.cached_constructor
+
+//! skin menu entries
+/// lookup for skin menu entries
+GLOBAL_LIST_EMPTY(skin_menu_entries)
+/datum/skin_menu_entry
+ /// id - used for both skin and savefiles
+ var/id
+ /// name
+ var/name
+ /**
+ * command to execute; \n to execute multiple.
+ * this datum will also inject our own command for synchronization automatically.
+ * generate_command() overrides this.
+ */
+ var/command
+ /// do we have a button group? checkbox is required for this; we will NOT load disabled for groups!
+ var/group
+ /// default enabled for checkable? only one in a group can have this!! infact, only one in a group **MUST** have tihs!!
+ var/is_default = FALSE
+ /// checkable?
+ var/checkbox = FALSE
+ /// command to execute, if any, when loading prefs if we're checked
+ var/load_command_enabled
+ /// command to execute, if any, when loading prefs if we're unchecked
+ var/load_command_disabled
+ /// use proc for load command
+ var/load_command_proc = FALSE
+ /// default load_command_enabled to command
+ var/load_command_default = FALSE
+ /// is saved? only applicable for checkboxes
+ var/is_saved = TRUE
+ /// cached creation parameters
+ var/cached_constructor
+ /// category we belong to
+ var/datum/skin_menu_category/category
+
+/datum/skin_menu_entry/New(datum/skin_menu_category/category)
+ src.category = category
+ cache_constructor()
+ if(load_command_default)
+ load_command_enabled = generate_command(TRUE)
+ register()
+
+/datum/skin_menu_entry/Destroy()
+ unregister()
+ return ..()
+
+/datum/skin_menu_entry/proc/register()
+ if(GLOB.skin_menu_entries[id])
+ stack_trace("warning: collision on [id] between [src] and [GLOB.skin_menu_entries[id]]; overwriting...")
+ GLOB.skin_menu_entries[id] = src
+
+/datum/skin_menu_entry/proc/unregister()
+ GLOB.skin_menu_entries -= id
+
+/datum/skin_menu_entry/proc/load(client/C, enabled)
+ if(!checkbox || !is_saved)
+ return // why do we care?
+ var/command
+ if(load_command_proc)
+ if(enabled)
+ command = load_command_enabled(C)
+ else
+ command = load_command_enabled(C)
+ else
+ if(enabled)
+ command = load_command_enabled
+ else
+ command = load_command_disabled
+ if(command)
+ C.menu_run_command(command)
+ if(enabled)
+ C.menu_buttons_checked[id] = TRUE
+ if(group)
+ C.menu_group_status[group] = id
+ else
+ C.menu_buttons_checked -= id
+ // we assume group is taken care of by the other item overwriting it.
+ winset(C, id, "is-checked=[enabled? "true" : "false"]")
+
+/datum/skin_menu_entry/proc/save(client/C, enabled)
+ if(!checkbox || !is_saved)
+ return // we don't care
+ if(group)
+ C.prefs.set_skin_data(group, id)
+ else
+ C.prefs.set_skin_data(id, enabled)
+
+/**
+ * called when someone presses us; using usr here is fine as this should only be called from verb anyways!
+ */
+/datum/skin_menu_entry/proc/pressed(client/C, new_checked)
+ save(C, new_checked)
+ if(!checkbox)
+ return // don't care
+ if(group && new_checked)
+ C.menu_group_status[group] = id
+ else
+ if(new_checked)
+ C.menu_buttons_checked[id] = TRUE
+ else
+ C.menu_buttons_checked -= id
+
+/datum/skin_menu_entry/proc/load_command_enabled(client/C)
+ CRASH("why did the proc get called without an override?")
+
+/datum/skin_menu_entry/proc/load_command_disabled(client/C)
+ CRASH("why did the proc get called without an override?")
+
+/**
+ * generates our normal command
+ *
+ * ! WARNING !
+ * While loading, you CANNOT use chained commands with \n.
+ */
+/datum/skin_menu_entry/proc/generate_command(loading)
+ return command
+
+/**
+ * should omit id, that'll be set separately
+ */
+/datum/skin_menu_entry/proc/creation_parameters()
+ var/list/params = list()
+ params["parent"] = category.id
+ params["command"] = ".menutrigger \"[id]\" \[\[is-checked\]\]\n[generate_command()]"
+ params["name"] = name
+ if(group)
+ params["group"] = group
+ params["is-checked"] = is_default? "true" : "false"
+ params["can-check"] = checkbox? "true" : "false"
+ return list2params(params)
+
+/datum/skin_menu_entry/proc/cache_constructor()
+ cached_constructor = creation_parameters()
+
+/**
+ * the empty entry used as a spacer
+ */
+/datum/skin_menu_entry/spacer
+ id = null
+ name = null
+ /// next id
+ var/static/next_id = 0
+
+/datum/skin_menu_entry/spacer/New()
+ ..()
+ id = "menubutton_spacer_[++next_id]"
+
+/datum/skin_menu_entry/spacer/register()
+ return
+
+/datum/skin_menu_entry/spacer/unregister()
+ return
+
+/datum/skin_menu_entry/spacer/creation_parameters()
+ return list2params(list(
+ parent = category.id,
+ name = ""
+ ))
+
+//! client stuff needed to make this work
+/client/verb/menu_button_triggered(id as text, checked as num)
+ set name = ".menutrigger"
+ set hidden = TRUE
+
+ var/datum/skin_menu_entry/E = GLOB.skin_menu_entries[id]
+ if(!E)
+ stack_trace("no entry found for [id], triggered by [src]")
+ to_chat(usr, SPAN_WARNING("error: no menu entry found; this is a bug. id: [id]"))
+ return
+ E.pressed(src, checked)
+
+/**
+ * gets which button of a group we have selected
+ */
+/client/proc/menu_group_query(group)
+ return menu_group_status[group]
+
+/**
+ * gets if a button is checked
+ */
+/client/proc/menu_button_checked(id)
+ return menu_buttons_checked[id]
+
+/**
+ * executes a command
+ */
+/client/proc/menu_run_command(str)
+ winset(src, null, list2params(list("command" = str)))
diff --git a/interface/menus/main.dm b/interface/menus/main.dm
new file mode 100644
index 00000000000..171a91ff18c
--- /dev/null
+++ b/interface/menus/main.dm
@@ -0,0 +1,229 @@
+GLOBAL_DATUM_INIT(main_window_menu, /datum/skin_menu/main, new)
+
+/datum/skin_menu/main
+ id = SKIN_ID_MAIN_MENU
+ bind_to = SKIN_WINDOW_ID_MAIN
+ categories = list(
+ /datum/skin_menu_category/main_file,
+ /datum/skin_menu_category/main_zoom,
+ /datum/skin_menu_category/main_scaling,
+ /datum/skin_menu_category/main_help,
+ )
+
+/datum/skin_menu_category/main_file
+ id = SKIN_ID_MENU_CATEGORY_FILE
+ name = "File"
+ entries = list(
+ /datum/skin_menu_entry/quick_screenshot,
+ /datum/skin_menu_entry/screenshot,
+ /datum/skin_menu_entry/ping,
+ /datum/skin_menu_entry/reconnect,
+ /datum/skin_menu_entry/quit,
+ )
+
+/datum/skin_menu_category/main_zoom
+ id = SKIN_ID_MENU_CATEGORY_ZOOM
+ name = "Zoom"
+ entries = list(
+ /datum/skin_menu_entry/widescreen/automatic,
+ /datum/skin_menu_entry/widescreen/legacy,
+ null,
+ /datum/skin_menu_entry/auto_fit,
+ /datum/skin_menu_entry/fit_viewport,
+ null,
+ /datum/skin_menu_entry/zoom/stretch_to_fit,
+ /datum/skin_menu_entry/zoom/stretch_to_fill,
+ )
+
+/datum/skin_menu_category/main_zoom/init_entries()
+ . = ..()
+ var/static/list/auto_sizes = list(
+ 32,
+ 48,
+ 64,
+ 72,
+ 96,
+ 128,
+ )
+ for(var/size in auto_sizes)
+ entries += new /datum/skin_menu_entry/zoom/specific(src, size)
+
+/datum/skin_menu_category/main_scaling
+ id = SKIN_ID_MENU_CATEGORY_SCALING
+ name = "Scaling"
+ entries = list(
+ /datum/skin_menu_entry/scaling/sharp,
+ /datum/skin_menu_entry/scaling/normal,
+ /datum/skin_menu_entry/scaling/blur,
+ )
+
+/datum/skin_menu_category/main_help
+ id = SKIN_ID_MENU_CATEGORY_HELP
+ name = "Help"
+ entries = list(
+ /datum/skin_menu_entry/adminhelp,
+ )
+
+//! file
+/datum/skin_menu_entry/quick_screenshot
+ id = SKIN_ID_MENU_BUTTON_SCREENSHOT_QUICK
+ name = "Quick Screenshot"
+ command = ".screenshot auto"
+
+/datum/skin_menu_entry/screenshot
+ id = SKIN_ID_MENU_BUTTON_SCREENSHOT
+ name = "Screenshot..."
+ command = ".screenshot"
+
+/datum/skin_menu_entry/quit
+ id = SKIN_ID_MENU_BUTTON_QUIT
+ name = "Quit"
+ command = ".quit"
+
+/datum/skin_menu_entry/reconnect
+ id = SKIN_ID_MENU_BUTTON_RECONNECT
+ name = "Reconnect"
+ command = ".reconnect"
+
+/datum/skin_menu_entry/ping
+ id = SKIN_ID_MENU_BUTTON_PING
+ name = "Ping (legacy chat)"
+ command = ".ping"
+
+//! zoom
+/datum/skin_menu_entry/zoom
+ group = SKIN_BUTTON_GROUP_MAP_ZOOM
+ load_command_default = TRUE
+ checkbox = TRUE
+
+/datum/skin_menu_entry/zoom/pressed(client/C, new_checked)
+ . = ..()
+ if(C.is_auto_fit_viewport_enabled())
+ C.request_viewport_fit()
+ C.request_viewport_update()
+
+/datum/skin_menu_entry/zoom/load(client/C, enabled)
+ . = ..()
+ if(C.is_auto_fit_viewport_enabled())
+ C.request_viewport_fit()
+ C.request_viewport_update()
+
+/datum/skin_menu_entry/zoom/stretch_to_fit
+ name = "Stretch to Fit"
+ id = SKIN_ID_MENU_BUTTON_STRETCH_TO_FIT
+ is_default = TRUE
+
+/datum/skin_menu_entry/zoom/stretch_to_fit/generate_command(loading)
+ if(loading)
+ return ".winset \"[SKIN_MAP_ID_VIEWPORT].zoom=0;[SKIN_MAP_ID_VIEWPORT].letterbox=true\""
+ return ".winset \"[SKIN_MAP_ID_VIEWPORT].zoom=0;[SKIN_MAP_ID_VIEWPORT].letterbox=true\"\n.update_viewport"
+
+/datum/skin_menu_entry/zoom/stretch_to_fill
+ name = "Stretch to Fill"
+ id = SKIN_ID_MENU_BUTTON_STRETCH_NO_LETTERBOX
+
+/datum/skin_menu_entry/zoom/stretch_to_fill/generate_command(loading)
+ if(loading)
+ return ".winset \"[SKIN_MAP_ID_VIEWPORT].zoom=0;[SKIN_MAP_ID_VIEWPORT].letterbox=false\""
+ return ".winset \"[SKIN_MAP_ID_VIEWPORT].zoom=0;[SKIN_MAP_ID_VIEWPORT].letterbox=false\"\n.update_viewport"
+
+//? the normal sizes are created by main_zoom category
+/datum/skin_menu_entry/zoom/specific
+ /// our pixel size
+ var/pixel_size
+
+/datum/skin_menu_entry/zoom/specific/New(datum/skin_menu_category/category, pixel_size)
+ src.pixel_size = pixel_size
+ src.id = SKIN_ID_MENU_BUTTON_FOR_RESOLUTION(pixel_size)
+ src.name = "[pixel_size]x[pixel_size]"
+ ..()
+
+/datum/skin_menu_entry/zoom/specific/generate_command(loading)
+ var/zoom = pixel_size / WORLD_ICON_SIZE
+ if(loading)
+ return ".winset \"[SKIN_MAP_ID_VIEWPORT].zoom=[zoom]\""
+ return ".winset \"[SKIN_MAP_ID_VIEWPORT].zoom=[zoom]\"\n.update_viewport"
+
+//! widescreen
+/datum/skin_menu_entry/widescreen
+ group = SKIN_BUTTON_GROUP_MAP_WIDESCREEN
+ checkbox = TRUE
+
+/datum/skin_menu_entry/widescreen/pressed(client/C, new_checked)
+ . = ..()
+ if(C.is_auto_fit_viewport_enabled())
+ C.request_viewport_fit()
+ C.request_viewport_update()
+
+/datum/skin_menu_entry/widescreen/load(client/C, enabled)
+ . = ..()
+ if(C.is_auto_fit_viewport_enabled())
+ C.request_viewport_fit()
+ C.request_viewport_update()
+
+/datum/skin_menu_entry/widescreen/automatic
+ id = SKIN_ID_MENU_BUTTON_WIDESCREEN_ENABLED
+ name = "Automatic Widescreen"
+ is_default = TRUE
+
+/datum/skin_menu_entry/widescreen/legacy
+ id = SKIN_ID_MENU_BUTTON_WIDESCREEN_DISABLED
+ name = "Lock to 15x15 (Legacy)"
+
+/client/proc/is_widescreen_enabled()
+ return menu_group_query(SKIN_BUTTON_GROUP_MAP_WIDESCREEN) == SKIN_ID_MENU_BUTTON_WIDESCREEN_ENABLED
+
+//! misc zoom
+/datum/skin_menu_entry/auto_fit
+ id = SKIN_ID_MENU_BUTTON_AUTO_FIT_VIEWPORT
+ name = "Automatically Fit Viewport (EXPERIMENTAL)"
+ checkbox = TRUE
+ is_default = FALSE
+
+/datum/skin_menu_entry/auto_fit/load(client/C, enabled)
+ . = ..()
+ if(enabled)
+ C.request_viewport_fit()
+
+/client/proc/is_auto_fit_viewport_enabled()
+ return menu_button_checked(SKIN_ID_MENU_BUTTON_AUTO_FIT_VIEWPORT)
+
+/datum/skin_menu_entry/fit_viewport
+ id = SKIN_ID_MENU_BUTTON_FIT_VIEWPORT
+ name = "Fit Viewport"
+
+/datum/skin_menu_entry/fit_viewport/pressed(client/C, new_checked)
+ . = ..()
+ C.request_viewport_fit()
+
+//! scaling
+/datum/skin_menu_entry/scaling
+ group = SKIN_BUTTON_GROUP_MAP_SCALING
+ checkbox = TRUE
+ load_command_default = TRUE
+ var/zoom_mode
+
+/datum/skin_menu_entry/scaling/generate_command(loading)
+ return ".winset \"[SKIN_MAP_ID_VIEWPORT].zoom-mode=[zoom_mode]\""
+
+/datum/skin_menu_entry/scaling/sharp
+ id = SKIN_ID_MENU_BUTTON_NEAREST_NEIGHBOR
+ name = "Nearest Neighbor (Sharp)"
+ zoom_mode = "distort"
+ is_default = TRUE
+
+/datum/skin_menu_entry/scaling/normal
+ id = SKIN_ID_MENU_BUTTON_POINT_SAMPLE
+ name = "Point Sampling (Normal)"
+ zoom_mode = "normal"
+
+/datum/skin_menu_entry/scaling/blur
+ id = SKIN_ID_MENU_BUTTON_BILINEAR
+ name = "Bilinear (Blurry)"
+ zoom_mode = "blur"
+
+//! help
+/datum/skin_menu_entry/adminhelp
+ id = SKIN_ID_MENU_BUTTON_ADMINHELP
+ name = "Admin Help (F1)"
+ command = "adminhelp"
diff --git a/interface/skin.dmf b/interface/skin.dmf
index 9395c1e4081..3b8304d6e93 100644
--- a/interface/skin.dmf
+++ b/interface/skin.dmf
@@ -1,284 +1,59 @@
macro "default"
-menu "menu"
- elem
- name = "&File"
- command = ""
- saved-params = "is-checked"
- elem
- name = "&Quick screenshot\tF2"
- command = ".screenshot auto"
- category = "&File"
- saved-params = "is-checked"
- elem
- name = "&Save screenshot as...\tShift+F2"
- command = ".screenshot"
- category = "&File"
- saved-params = "is-checked"
- elem
- name = ""
- command = ""
- category = "&File"
- saved-params = "is-checked"
- elem
- name = "&Reconnect"
- command = ".reconnect"
- category = "&File"
- saved-params = "is-checked"
- elem
- name = "&Check ping"
- command = ".ping"
- category = "&File"
- saved-params = "is-checked"
- elem
- name = "&Quit"
- command = ".quit"
- category = "&File"
- saved-params = "is-checked"
- elem
- name = "&Size"
- command = ""
- saved-params = "is-checked"
- elem "stretch"
- name = "&Round up and lock to size"
- command = ".winset \"mapwindow.map.icon-size=0\""
- category = "&Size"
- is-checked = true
- can-check = true
- group = "size"
- saved-params = "is-checked"
- elem "cozyin"
- name = "Get Cozy Inwards"
- command = "SetCozyViewInwards 8"
- category = "&Size"
- can-check = false
- group = "size"
- saved-params = "is-checked"
- elem "icon128"
- name = "&128x128 (4K)"
- command = "SetWindowIconSize 128"
- category = "&Size"
- can-check = true
- group = "size"
- saved-params = "is-checked"
- elem "icon96"
- name = "&96x96"
- command = "SetWindowIconSize 96"
- category = "&Size"
- can-check = true
- group = "size"
- saved-params = "is-checked"
- elem "icon72"
- name = "&72x72 (Cozy 1080p)"
- command = "SetWindowIconSize 72"
- category = "&Size"
- is-checked = true
- can-check = true
- group = "size"
- saved-params = "is-checked"
- elem "icon64"
- name = "&64x64 (1080p)"
- command = "SetWindowIconSize 64"
- category = "&Size"
- is-checked = true
- can-check = true
- group = "size"
- saved-params = "is-checked"
- elem "icon48"
- name = "&48x48"
- command = "SetWindowIconSize 48"
- category = "&Size"
- can-check = true
- group = "size"
- saved-params = "is-checked"
- elem "icon32"
- name = "&32x32"
- command = "SetWindowIconSize 32"
- category = "&Size"
- can-check = true
- group = "size"
- saved-params = "is-checked"
- elem
- name = "&Scaling"
- command = ""
- saved-params = "is-checked"
- elem "ZM"
- name = "&Smooth Scaling"
- command = ".winset \"zoommode.is-checked=true?map.zoom-mode=normal:map.zoom-mode=distort\""
- category = "&Scaling"
- is-checked = true
- can-check = true
- group = "scale"
- saved-params = "is-checked"
- elem "NN"
- name = "&Nearest Neighbor"
- command = ".winset \"mapwindow.map.zoom-mode=distort\""
- category = "&Scaling"
- can-check = true
- group = "scale"
- saved-params = "is-checked"
- elem "PS"
- name = "&Point Sampling"
- command = ".winset \"mapwindow.map.zoom-mode=normal\""
- category = "&Scaling"
- can-check = true
- group = "scale"
- saved-params = "is-checked"
- elem "BL"
- name = "&Bilinear"
- command = ".winset \"mapwindow.map.zoom-mode=blur\""
- category = "&Scaling"
- can-check = true
- group = "scale"
- saved-params = "is-checked"
- elem
- name = "&Help"
- command = ""
- saved-params = "is-checked"
- elem
- name = "&Admin help\tF1"
- command = "adminhelp"
- category = "&Help"
- saved-params = "is-checked"
- elem
- name = "&Hotkeys"
- command = "hotkeys-help"
- category = "&Help"
- saved-params = "is-checked"
-
-
-window "Telecomms IDE"
- elem "Telecomms IDE"
- type = MAIN
- pos = 281,0
- size = 569x582
- anchor1 = none
- anchor2 = none
- background-color = #ffffff
- is-visible = false
- saved-params = "pos;size;is-minimized;is-maximized"
- title = "TCS IDE"
- statusbar = false
- on-close = "exittcs"
- elem "button5"
- type = BUTTON
- pos = 209,464
- size = 70x20
- anchor1 = 37,80
- anchor2 = 49,83
- saved-params = "is-checked"
- text = "Clear Memory"
- command = "tcsclearmem"
- elem "button4"
- type = BUTTON
- pos = 157,464
- size = 52x20
- anchor1 = 28,80
- anchor2 = 37,83
- saved-params = "is-checked"
- text = "Revert"
- command = "tcsrevert"
- elem "button3"
- type = BUTTON
- pos = 105,464
- size = 52x20
- anchor1 = 18,80
- anchor2 = 28,83
- saved-params = "is-checked"
- text = "Execute"
- command = "tcsrun"
- elem "tcserror"
- type = OUTPUT
- pos = 0,488
- size = 566x94
- anchor1 = 0,84
- anchor2 = 99,100
- font-family = "sans-serif"
- font-size = 9
- saved-params = "max-lines"
- elem "button2"
- type = BUTTON
- pos = 53,464
- size = 52x20
- anchor1 = 9,80
- anchor2 = 18,83
- saved-params = "is-checked"
- text = "Compile"
- command = "tcscompile"
- elem "button1"
- type = BUTTON
- pos = 0,464
- size = 53x20
- anchor1 = 0,80
- anchor2 = 9,83
- saved-params = "is-checked"
- text = "Apply"
- command = "tcssave"
- elem "tcscode"
- type = INPUT
- pos = 0,0
- size = 569x464
- anchor1 = 0,0
- anchor2 = 100,80
- font-family = "Courier"
- font-size = 10
- saved-params = ""
- command = "cancel"
- multi-line = true
- no-command = true
-
window "mainwindow"
elem "mainwindow"
type = MAIN
pos = 0,0
- size = 640x440
- anchor1 = none
- anchor2 = none
+ size = 640x480
+ anchor1 = -1,-1
+ anchor2 = -1,-1
+ background-color = none
is-default = true
saved-params = "pos;size;is-minimized;is-maximized"
icon = 'icons\\CS13.png'
macro = "default"
- menu = "menu"
- on-size = "OnResize"
+ outer-size = 656x538
+ inner-size = 640x499
elem "split"
type = CHILD
- pos = 3,0
- size = 640x440
+ pos = 0,0
+ size = 640x480
anchor1 = 0,0
anchor2 = 100,100
+ background-color = none
saved-params = "splitter"
left = "mapwindow"
right = "infowindow"
is-vert = true
- on-size = "OnResize"
elem "asset_cache_browser"
type = BROWSER
pos = 0,0
size = 200x200
- anchor1 = none
- anchor2 = none
+ anchor1 = -1,-1
+ anchor2 = -1,-1
+ background-color = none
is-visible = false
saved-params = ""
elem "tooltip"
type = BROWSER
pos = 0,0
size = 999x999
- anchor1 = none
- anchor2 = none
+ anchor1 = -1,-1
+ anchor2 = -1,-1
+ background-color = none
is-visible = false
saved-params = ""
window "mapwindow"
elem "mapwindow"
type = MAIN
- pos = 0,0
+ pos = 281,0
size = 640x480
- anchor1 = none
- anchor2 = none
+ anchor1 = -1,-1
+ anchor2 = -1,-1
saved-params = "pos;size;is-minimized;is-maximized"
is-pane = true
- on-size = "OnResize"
elem "map"
type = MAP
pos = 0,0
@@ -287,9 +62,9 @@ window "mapwindow"
anchor2 = 100,100
font-family = "Arial"
font-size = 7
- text-color = none
is-default = true
- saved-params = "zoom;letterbox;zoom-mode"
+ saved-params = ""
+ on-size = ".on_viewport [[width]] [[height]] [[zoom]] [[letterbox]]"
style = ".center { text-align: center; } .maptext { font-family: 'Small Fonts'; font-size: 7px; -dm-text-outline: 1px black; color: white; line-height: 1.1; } .command_headset { font-weight: bold;\tfont-size: 8px; } .small { font-size: 6px; } .big { font-size: 8px; } .reallybig { font-size: 8px; } .extremelybig { font-size: 8px; } .greentext { color: #00FF00; font-size: 7px; } .redtext { color: #FF0000; font-size: 7px; } .clown { color: #FF69Bf; font-size: 7px; font-weight: bold; } .his_grace { color: #15D512; } .hypnophrase { color: #0d0d0d; font-weight: bold; } .yell { font-weight: bold; } .italics { font-size: 6px; }"
window "preferences_window"
@@ -297,8 +72,8 @@ window "preferences_window"
type = MAIN
pos = 281,0
size = 1000x800
- anchor1 = none
- anchor2 = none
+ anchor1 = -1,-1
+ anchor2 = -1,-1
is-visible = false
saved-params = "pos;size;is-minimized;is-maximized"
statusbar = false
@@ -323,20 +98,16 @@ window "infowindow"
type = MAIN
pos = 418,0
size = 640x480
- anchor1 = none
- anchor2 = none
- background-color = none
+ anchor1 = -1,-1
+ anchor2 = -1,-1
saved-params = "pos;size;is-minimized;is-maximized"
is-pane = true
- outer-size = 662x561
- inner-size = 640x505
elem "mediapanel"
type = BROWSER
pos = 88,216
size = 200x200
- anchor1 = none
- anchor2 = none
- background-color = none
+ anchor1 = -1,-1
+ anchor2 = -1,-1
is-visible = false
saved-params = ""
elem "info"
@@ -345,7 +116,6 @@ window "infowindow"
size = 640x445
anchor1 = 0,0
anchor2 = 100,100
- background-color = none
saved-params = "splitter"
left = "statwindow"
right = "outputwindow"
@@ -356,7 +126,6 @@ window "infowindow"
size = 104x20
anchor1 = 3,0
anchor2 = 19,0
- background-color = none
saved-params = "is-checked"
text = "Changelog"
command = "changelog"
@@ -366,7 +135,6 @@ window "infowindow"
size = 100x20
anchor1 = 19,0
anchor2 = 34,0
- background-color = none
saved-params = "is-checked"
text = "Rules"
command = "rules"
@@ -376,7 +144,6 @@ window "infowindow"
size = 100x20
anchor1 = 34,0
anchor2 = 50,0
- background-color = none
saved-params = "is-checked"
text = "Wiki"
command = "wiki"
@@ -386,7 +153,6 @@ window "infowindow"
size = 100x20
anchor1 = 50,0
anchor2 = 66,0
- background-color = none
saved-params = "is-checked"
text = "Forum"
command = "forum"
@@ -396,7 +162,6 @@ window "infowindow"
size = 100x20
anchor1 = 66,0
anchor2 = 81,0
- background-color = none
saved-params = "is-checked"
text = "Github"
command = "github"
@@ -406,7 +171,6 @@ window "infowindow"
size = 100x20
anchor1 = 81,0
anchor2 = 97,0
- background-color = none
saved-params = "is-checked"
text = "Report Issue"
command = "report-issue"
@@ -416,8 +180,8 @@ window "outputwindow"
type = MAIN
pos = 0,0
size = 640x480
- anchor1 = none
- anchor2 = none
+ anchor1 = -1,-1
+ anchor2 = -1,-1
saved-params = "pos;size;is-minimized;is-maximized"
is-pane = true
elem "browseroutput"
@@ -436,7 +200,6 @@ window "outputwindow"
size = 40x20
anchor1 = 100,100
anchor2 = -1,-1
- background-color = none
saved-params = "is-checked"
text = "Chat"
command = ".winset \"saybutton.is-checked=true?input.command=\"!say \\\"\" macrobutton.is-checked=false:input.command=\""
@@ -457,7 +220,6 @@ window "outputwindow"
size = 80x20
anchor1 = 100,100
anchor2 = -1,-1
- background-color = none
saved-params = ""
text = "Hotkey Toggle"
command = ".winset \"mainwindow.macro!=macro ? mainwindow.macro=macro hotkey_toggle.is-checked=false input.focus=true : mainwindow.macro=hotkeymode hotkey_toggle.is-checked=true mapwindow.map.focus=true\""
@@ -474,8 +236,8 @@ window "outputwindow"
type = BROWSER
pos = 0,0
size = 200x200
- anchor1 = none
- anchor2 = none
+ anchor1 = -1,-1
+ anchor2 = -1,-1
is-visible = false
saved-params = ""
@@ -484,8 +246,8 @@ window "statwindow"
type = MAIN
pos = 372,0
size = 640x480
- anchor1 = none
- anchor2 = none
+ anchor1 = -1,-1
+ anchor2 = -1,-1
saved-params = "pos;size;is-minimized;is-maximized"
is-pane = true
elem "statbrowser"
diff --git a/maps/submaps/_healpers.dm b/maps/submaps/_healpers.dm
index 8ce99a51dae..baf6ce160c4 100644
--- a/maps/submaps/_healpers.dm
+++ b/maps/submaps/_healpers.dm
@@ -40,7 +40,7 @@
anchored = 1
plane = TURF_PLANE
layer = ABOVE_TURF_LAYER
- appearance_flags = PIXEL_SCALE|KEEP_TOGETHER
+ SET_APPEARANCE_FLAGS(TILE_BOUND)
var/_stair_tag //Make this match another one and they'll connect!
diff --git a/tgui/packages/tgui-panel/themes.js b/tgui/packages/tgui-panel/themes.js
index 0bee64b7cd4..a972e051db5 100644
--- a/tgui/packages/tgui-panel/themes.js
+++ b/tgui/packages/tgui-panel/themes.js
@@ -132,10 +132,10 @@ export const setClientTheme = name => {
// Say, OOC, me Buttons etc.
'saybutton.background-color': COLOR_DARK_BG,
'saybutton.text-color': COLOR_DARK_TEXT,
- 'oocbutton.background-color': COLOR_DARK_BG,
- 'oocbutton.text-color': COLOR_DARK_TEXT,
- 'mebutton.background-color': COLOR_DARK_BG,
- 'mebutton.text-color': COLOR_DARK_TEXT,
+ // 'oocbutton.background-color': COLOR_DARK_BG,
+ // 'oocbutton.text-color': COLOR_DARK_TEXT,
+ // 'mebutton.background-color': COLOR_DARK_BG,
+ // 'mebutton.text-color': COLOR_DARK_TEXT,
'asset_cache_browser.background-color': COLOR_DARK_BG,
'asset_cache_browser.text-color': COLOR_DARK_TEXT,
'tooltip.background-color': COLOR_DARK_BG,
@@ -182,10 +182,10 @@ export const setClientTheme = name => {
// Say, OOC, me Buttons etc.
'saybutton.background-color': COLOR_DARK_BG,
'saybutton.text-color': COLOR_DARK_TEXT,
- 'oocbutton.background-color': COLOR_DARK_BG,
- 'oocbutton.text-color': COLOR_DARK_TEXT,
- 'mebutton.background-color': COLOR_DARK_BG,
- 'mebutton.text-color': COLOR_DARK_TEXT,
+ // 'oocbutton.background-color': COLOR_DARK_BG,
+ // 'oocbutton.text-color': COLOR_DARK_TEXT,
+ // 'mebutton.background-color': COLOR_DARK_BG,
+ // 'mebutton.text-color': COLOR_DARK_TEXT,
'asset_cache_browser.background-color': COLOR_DARK_BG,
'asset_cache_browser.text-color': COLOR_DARK_TEXT,
'tooltip.background-color': COLOR_DARK_BG,