diff --git a/code/__DEFINES/hud.dm b/code/__DEFINES/hud.dm
index 38e5693dcbe5..deee80c7a91d 100644
--- a/code/__DEFINES/hud.dm
+++ b/code/__DEFINES/hud.dm
@@ -23,3 +23,5 @@
#define NOTIFY_ATTACK "attack"
#define NOTIFY_ORBIT "orbit"
#define NOTIFY_JOIN_XENO "join_xeno"
+#define NOTIFY_XENO_TACMAP "xeno_tacmap"
+#define NOTIFY_USCM_TACMAP "uscm_tacmap"
diff --git a/code/__DEFINES/minimap.dm b/code/__DEFINES/minimap.dm
index 71d0ed8e7445..003d723600c4 100644
--- a/code/__DEFINES/minimap.dm
+++ b/code/__DEFINES/minimap.dm
@@ -5,7 +5,17 @@
#define MINIMAP_FLAG_UPP (1<<3)
#define MINIMAP_FLAG_CLF (1<<4)
#define MINIMAP_FLAG_YAUTJA (1<<5)
-#define MINIMAP_FLAG_ALL (1<<6) - 1
+#define MINIMAP_FLAG_XENO_CORRUPTED (1<<6)
+#define MINIMAP_FLAG_XENO_ALPHA (1<<7)
+#define MINIMAP_FLAG_XENO_BRAVO (1<<8)
+#define MINIMAP_FLAG_XENO_CHARLIE (1<<9)
+#define MINIMAP_FLAG_XENO_DELTA (1<<10)
+#define MINIMAP_FLAG_XENO_FERAL (1<<11)
+#define MINIMAP_FLAG_XENO_TAMED (1<<12)
+#define MINIMAP_FLAG_XENO_MUTATED (1<<13)
+#define MINIMAP_FLAG_XENO_FORSAKEN (1<<14)
+#define MINIMAP_FLAG_XENO_RENEGADE (1<<15)
+#define MINIMAP_FLAG_ALL (1<<16) - 1
///Converts the overworld x and y to minimap x and y values
#define MINIMAP_SCALE 2
@@ -77,9 +87,3 @@ GLOBAL_LIST_INIT(all_minimap_flags, bitfield2list(MINIMAP_FLAG_ALL))
#define TACMAP_BASE_OCCLUDED "Occluded"
#define TACMAP_BASE_OPEN "Open"
-
-#define TACMAP_DEFAULT "Default"
-#define TACMAP_XENO "Xeno"
-#define TACMAP_YAUTJA "Yautja"
-#define TACMAP_FACTION "Faction"
-
diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm
index 08fcf5d47915..97243002740d 100644
--- a/code/__HELPERS/icons.dm
+++ b/code/__HELPERS/icons.dm
@@ -681,8 +681,9 @@ world
* * moving - whether or not to use a moving state for the given icon
* * sourceonly - if TRUE, only generate the asset and send back the asset url, instead of tags that display the icon to players
* * extra_clases - string of extra css classes to use when returning the icon string
+ * * keyonly - if TRUE, only returns the asset key to use get_asset_url manually. Overrides sourceonly.
*/
-/proc/icon2html(atom/thing, client/target, icon_state, dir = SOUTH, frame = 1, moving = FALSE, sourceonly = FALSE, extra_classes = null)
+/proc/icon2html(atom/thing, client/target, icon_state, dir = SOUTH, frame = 1, moving = FALSE, sourceonly = FALSE, extra_classes = null, keyonly = FALSE)
if (!thing)
return
@@ -713,6 +714,8 @@ world
SSassets.transport.register_asset(name, thing)
for (var/thing2 in targets)
SSassets.transport.send_assets(thing2, name)
+ if(keyonly)
+ return name
if(sourceonly)
return SSassets.transport.get_asset_url(name)
return ""
@@ -755,6 +758,8 @@ world
SSassets.transport.register_asset(key, rsc_ref, file_hash, icon_path)
for (var/client_target in targets)
SSassets.transport.send_assets(client_target, key)
+ if(keyonly)
+ return key
if(sourceonly)
return SSassets.transport.get_asset_url(key)
return "
"
diff --git a/code/_globalvars/global_lists.dm b/code/_globalvars/global_lists.dm
index bac97d1994a3..6e1b229e562f 100644
--- a/code/_globalvars/global_lists.dm
+++ b/code/_globalvars/global_lists.dm
@@ -6,6 +6,14 @@ GLOBAL_LIST_EMPTY(CMBFaxes)
GLOBAL_LIST_EMPTY(GeneralFaxes) //Inter-machine faxes
GLOBAL_LIST_EMPTY(fax_contents) //List of fax contents to maintain it even if source paper is deleted
+//datum containing a reference to the flattend map png url, the actual png is stored in the user's cache.
+GLOBAL_LIST_EMPTY(uscm_flat_tacmap_data)
+GLOBAL_LIST_EMPTY(xeno_flat_tacmap_data)
+
+//datum containing the svg overlay coords in array format.
+GLOBAL_LIST_EMPTY(uscm_svg_tacmap_data)
+GLOBAL_LIST_EMPTY(xeno_svg_tacmap_data)
+
GLOBAL_LIST_EMPTY(failed_fultons) //A list of fultoned items which weren't collected and fell back down
GLOBAL_LIST_EMPTY(larva_burst_by_hive)
diff --git a/code/_globalvars/misc.dm b/code/_globalvars/misc.dm
index 65f68d014585..0b7a4af0f05f 100644
--- a/code/_globalvars/misc.dm
+++ b/code/_globalvars/misc.dm
@@ -100,6 +100,22 @@ GLOBAL_LIST_INIT(pill_icon_mappings, map_pill_icons())
/// In-round override to default OOC color
GLOBAL_VAR(ooc_color_override)
+// tacmap cooldown for xenos and marines
+GLOBAL_VAR_INIT(uscm_canvas_cooldown, 0)
+GLOBAL_VAR_INIT(xeno_canvas_cooldown, 0)
+
+// getFlatIcon cooldown for xenos and marines
+GLOBAL_VAR_INIT(uscm_flatten_map_icon_cooldown, 0)
+GLOBAL_VAR_INIT(xeno_flatten_map_icon_cooldown, 0)
+
+// latest unannounced flat tacmap for xenos and marines
+GLOBAL_VAR(uscm_unannounced_map)
+GLOBAL_VAR(xeno_unannounced_map)
+
+//global tacmaps for action button access
+GLOBAL_DATUM_INIT(uscm_tacmap_status, /datum/tacmap/drawing/status_tab_view, new)
+GLOBAL_DATUM_INIT(xeno_tacmap_status, /datum/tacmap/drawing/status_tab_view/xeno, new)
+
/// List of roles that can be setup for each gamemode
GLOBAL_LIST_EMPTY(gamemode_roles)
diff --git a/code/controllers/subsystem/minimap.dm b/code/controllers/subsystem/minimap.dm
index 6f5b9303a91f..d28fe916291a 100644
--- a/code/controllers/subsystem/minimap.dm
+++ b/code/controllers/subsystem/minimap.dm
@@ -256,8 +256,6 @@ SUBSYSTEM_DEF(minimaps)
removal_cbs[target] = CALLBACK(src, PROC_REF(removeimage), blip, target)
RegisterSignal(target, COMSIG_PARENT_QDELETING, PROC_REF(remove_marker))
-
-
/**
* removes an image from raw tracked lists, invoked by callback
*/
@@ -322,7 +320,7 @@ SUBSYSTEM_DEF(minimaps)
minimaps_by_z["[z_level]"].images_assoc["[flag]"] -= source
/**
- * Fetches a /atom/movable/screen/minimap instance or creates on if none exists
+ * Fetches a /atom/movable/screen/minimap instance or creates one if none exists
* Note this does not destroy them when the map is unused, might be a potential thing to do?
* Arguments:
* * zlevel: zlevel to fetch map for
@@ -338,6 +336,170 @@ SUBSYSTEM_DEF(minimaps)
hashed_minimaps[hash] = map
return map
+/**
+ * Fetches the datum containing an announced flattend map png reference.
+ *
+ * Arguments:
+ * * faction: FACTION_MARINE or XENO_HIVE_NORMAL
+ */
+/proc/get_tacmap_data_png(faction)
+ var/list/map_list
+
+ if(faction == FACTION_MARINE)
+ map_list = GLOB.uscm_flat_tacmap_data
+ else if(faction == XENO_HIVE_NORMAL)
+ map_list = GLOB.xeno_flat_tacmap_data
+ else
+ return null
+
+ var/map_length = length(map_list)
+
+ if(map_length == 0)
+ return null
+
+ return map_list[map_length]
+
+/**
+ * Fetches the datum containing the latest unannounced flattend map png reference.
+ *
+ * Arguments:
+ * * faction: FACTION_MARINE or XENO_HIVE_NORMAL
+ */
+/proc/get_unannounced_tacmap_data_png(faction)
+ if(faction == FACTION_MARINE)
+ return GLOB.uscm_unannounced_map
+ else if(faction == XENO_HIVE_NORMAL)
+ return GLOB.xeno_unannounced_map
+
+ return null
+
+/**
+ * Fetches the last set of svg coordinates for the tacmap drawing.
+ *
+ * Arguments:
+ * * faction: which faction get the map for: FACTION_MARINE or XENO_HIVE_NORMAL
+ */
+/proc/get_tacmap_data_svg(faction)
+ var/list/map_list
+
+ if(faction == FACTION_MARINE)
+ map_list = GLOB.uscm_svg_tacmap_data
+ else if(faction == XENO_HIVE_NORMAL)
+ map_list = GLOB.xeno_svg_tacmap_data
+ else
+ return null
+
+ var/map_length = length(map_list)
+
+ if(map_length == 0)
+ return null
+
+ return map_list[map_length]
+
+/**
+ * Re-sends relevant flattened tacmaps to a single client.
+ *
+ * Arguments:
+ * * user: The mob that is either an observer, marine, or xeno
+ */
+/proc/resend_current_map_png(mob/user)
+ if(!user.client)
+ return
+
+ var/is_observer = user.faction == FACTION_NEUTRAL && isobserver(user)
+ if(is_observer || user.faction == FACTION_MARINE)
+ // Send marine maps
+ var/datum/flattened_tacmap/latest = get_tacmap_data_png(FACTION_MARINE)
+ if(latest)
+ SSassets.transport.send_assets(user.client, latest.asset_key)
+ var/datum/flattened_tacmap/unannounced = get_unannounced_tacmap_data_png(FACTION_MARINE)
+ if(unannounced && (!latest || latest.asset_key != unannounced.asset_key))
+ SSassets.transport.send_assets(user.client, unannounced.asset_key)
+
+ var/mob/living/carbon/xenomorph/xeno = user
+ if(is_observer || istype(xeno) && xeno.hivenumber == XENO_HIVE_NORMAL)
+ // Send xeno maps
+ var/datum/flattened_tacmap/latest = get_tacmap_data_png(XENO_HIVE_NORMAL)
+ if(latest)
+ SSassets.transport.send_assets(user.client, latest.asset_key)
+ var/datum/flattened_tacmap/unannounced = get_unannounced_tacmap_data_png(XENO_HIVE_NORMAL)
+ if(unannounced && (!latest || latest.asset_key != unannounced.asset_key))
+ SSassets.transport.send_assets(user.client, unannounced.asset_key)
+
+/**
+ * Flattens the current map and then distributes it for the specified faction as an unannounced map.
+ *
+ * Arguments:
+ * * faction: Which faction to distribute the map to: FACTION_MARINE or XENO_HIVE_NORMAL
+ * Return:
+ * * Returns a boolean value, TRUE if the operation was successful, FALSE if it was not (on cooldown generally).
+ */
+/datum/tacmap/drawing/proc/distribute_current_map_png(faction)
+ if(faction == FACTION_MARINE)
+ if(!COOLDOWN_FINISHED(GLOB, uscm_flatten_map_icon_cooldown))
+ return FALSE
+ COOLDOWN_START(GLOB, uscm_flatten_map_icon_cooldown, flatten_map_cooldown_time)
+ else if(faction == XENO_HIVE_NORMAL)
+ if(!COOLDOWN_FINISHED(GLOB, xeno_flatten_map_icon_cooldown))
+ return FALSE
+ COOLDOWN_START(GLOB, xeno_flatten_map_icon_cooldown, flatten_map_cooldown_time)
+ else
+ return FALSE
+
+ var/icon/flat_map = getFlatIcon(map_holder.map, appearance_flags = TRUE)
+ if(!flat_map)
+ to_chat(usr, SPAN_WARNING("A critical error has occurred! Contact a coder.")) // tf2heavy: "Oh, this is bad!"
+ return FALSE
+
+ // Send to only relevant clients
+ var/list/faction_clients = list()
+ for(var/client/client as anything in GLOB.clients)
+ if(!client || !client.mob)
+ continue
+ var/mob/client_mob = client.mob
+ if(client_mob.faction == faction)
+ faction_clients += client
+ else if(client_mob.faction == FACTION_NEUTRAL && isobserver(client_mob))
+ faction_clients += client
+ else if(isxeno(client_mob))
+ var/mob/living/carbon/xenomorph/xeno = client_mob
+ if(xeno.hivenumber == faction)
+ faction_clients += client
+
+ // This may be unnecessary to do this way if the asset url is always the same as the lookup key
+ var/flat_tacmap_key = icon2html(flat_map, faction_clients, keyonly = TRUE)
+ if(!flat_tacmap_key)
+ to_chat(usr, SPAN_WARNING("A critical error has occurred! Contact a coder."))
+ return FALSE
+ var/flat_tacmap_png = SSassets.transport.get_asset_url(flat_tacmap_key)
+ var/datum/flattened_tacmap/new_flat = new(flat_tacmap_png, flat_tacmap_key)
+
+ if(faction == FACTION_MARINE)
+ GLOB.uscm_unannounced_map = new_flat
+ else //if(faction == XENO_HIVE_NORMAL)
+ GLOB.xeno_unannounced_map = new_flat
+
+ return TRUE
+
+/**
+ * Globally stores svg coords for a given faction.
+ *
+ * Arguments:
+ * * faction: which faction to save the data for: FACTION_MARINE or XENO_HIVE_NORMAL
+ * * svg_coords: an array of coordinates corresponding to an svg.
+ * * ckey: the ckey of the user who submitted this
+ */
+/datum/tacmap/drawing/proc/store_current_svg_coords(faction, svg_coords, ckey)
+ var/datum/svg_overlay/svg_store_overlay = new(svg_coords, ckey)
+
+ if(faction == FACTION_MARINE)
+ GLOB.uscm_svg_tacmap_data += svg_store_overlay
+ else if(faction == XENO_HIVE_NORMAL)
+ GLOB.xeno_svg_tacmap_data += svg_store_overlay
+ else
+ qdel(svg_store_overlay)
+ debug_log("SVG coordinates for [faction] are not implemented!")
+
/datum/controller/subsystem/minimaps/proc/fetch_tacmap_datum(zlevel, flags)
var/hash = "[zlevel]-[flags]"
if(hashed_tacmaps[hash])
@@ -442,7 +604,7 @@ SUBSYSTEM_DEF(minimaps)
marker_flags = MINIMAP_FLAG_USCM
/datum/action/minimap/observer
- minimap_flags = MINIMAP_FLAG_XENO|MINIMAP_FLAG_USCM|MINIMAP_FLAG_UPP|MINIMAP_FLAG_CLF|MINIMAP_FLAG_UPP
+ minimap_flags = MINIMAP_FLAG_ALL
marker_flags = NONE
hidden = TRUE
@@ -452,17 +614,61 @@ SUBSYSTEM_DEF(minimaps)
var/targeted_ztrait = ZTRAIT_GROUND
var/atom/owner
+ /// tacmap holder for holding the minimap
var/datum/tacmap_holder/map_holder
+/datum/tacmap/drawing
+ /// A url that will point to the wiki map for the current map as a fall back image
+ var/static/wiki_map_fallback
+
+ /// color selection for the tactical map canvas, defaults to black.
+ var/toolbar_color_selection = "black"
+ var/toolbar_updated_selection = "black"
+
+ var/canvas_cooldown_time = 4 MINUTES
+ var/flatten_map_cooldown_time = 3 MINUTES
+
+ /// boolean value to keep track if the canvas has been updated or not, the value is used in tgui state.
+ var/updated_canvas = FALSE
+ /// current flattend map
+ var/datum/flattened_tacmap/new_current_map
+ /// previous flattened map
+ var/datum/flattened_tacmap/old_map
+ /// current svg
+ var/datum/svg_overlay/current_svg
+
+ var/action_queue_change = 0
+
+ /// The last time the map has been flattened - used as a key to trick react into updating the canvas
+ var/last_update_time = 0
+ /// A temporary lock out time before we can open the new canvas tab to allow the tacmap time to fire
+ var/tacmap_ready_time = 0
+
/datum/tacmap/New(atom/source, minimap_type)
allowed_flags = minimap_type
owner = source
+/datum/tacmap/drawing/status_tab_view/New()
+ var/datum/tacmap/drawing/status_tab_view/uscm_tacmap
+ allowed_flags = MINIMAP_FLAG_USCM
+ owner = uscm_tacmap
+
+/datum/tacmap/drawing/status_tab_view/xeno/New()
+ var/datum/tacmap/drawing/status_tab_view/xeno/xeno_tacmap
+ allowed_flags = MINIMAP_FLAG_XENO
+ owner = xeno_tacmap
+
/datum/tacmap/Destroy()
map_holder = null
owner = null
return ..()
+/datum/tacmap/drawing/Destroy()
+ new_current_map = null
+ old_map = null
+ current_svg = null
+ return ..()
+
/datum/tacmap/tgui_interact(mob/user, datum/tgui/ui)
if(!map_holder)
var/level = SSmapping.levels_by_trait(targeted_ztrait)
@@ -476,11 +682,216 @@ SUBSYSTEM_DEF(minimaps)
ui = new(user, src, "TacticalMap")
ui.open()
+/datum/tacmap/drawing/tgui_interact(mob/user, datum/tgui/ui)
+ var/mob/living/carbon/xenomorph/xeno = user
+ var/is_xeno = istype(xeno)
+ var/faction = is_xeno ? xeno.hivenumber : user.faction
+ if(faction == FACTION_NEUTRAL && isobserver(user))
+ faction = allowed_flags == MINIMAP_FLAG_XENO ? XENO_HIVE_NORMAL : FACTION_MARINE
+
+ new_current_map = get_unannounced_tacmap_data_png(faction)
+ old_map = get_tacmap_data_png(faction)
+ current_svg = get_tacmap_data_svg(faction)
+
+ var/use_live_map = faction == FACTION_MARINE && skillcheck(user, SKILL_LEADERSHIP, SKILL_LEAD_EXPERT) || is_xeno
+
+ if(use_live_map && !map_holder)
+ var/level = SSmapping.levels_by_trait(targeted_ztrait)
+ if(!level[1])
+ return
+ map_holder = SSminimaps.fetch_tacmap_datum(level[1], allowed_flags)
+
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ if(!wiki_map_fallback)
+ var/wiki_url = CONFIG_GET(string/wikiurl)
+ var/obj/item/map/current_map/new_map = new
+ if(wiki_url && new_map.html_link)
+ wiki_map_fallback ="[wiki_url]/[new_map.html_link]"
+ else
+ debug_log("Failed to determine fallback wiki map! Attempted '[wiki_url]/[new_map.html_link]'")
+ qdel(new_map)
+
+ // Ensure we actually have the map image sent
+ resend_current_map_png(user)
+
+ if(use_live_map)
+ tacmap_ready_time = SSminimaps.next_fire + 2 SECONDS
+ addtimer(CALLBACK(src, PROC_REF(on_tacmap_fire), faction), SSminimaps.next_fire - world.time + 1 SECONDS)
+ user.client.register_map_obj(map_holder.map)
+
+ ui = new(user, src, "TacticalMap")
+ ui.open()
+
+/datum/tacmap/drawing/ui_data(mob/user)
+ var/list/data = list()
+
+ data["newCanvasFlatImage"] = new_current_map?.flat_tacmap
+ data["oldCanvasFlatImage"] = old_map?.flat_tacmap
+ data["svgData"] = current_svg?.svg_data
+
+ data["actionQueueChange"] = action_queue_change
+
+ data["toolbarColorSelection"] = toolbar_color_selection
+ data["toolbarUpdatedSelection"] = toolbar_updated_selection
+
+ if(isxeno(user))
+ data["canvasCooldown"] = max(GLOB.xeno_canvas_cooldown - world.time, 0)
+ else
+ data["canvasCooldown"] = max(GLOB.uscm_canvas_cooldown - world.time, 0)
+
+ data["nextCanvasTime"] = canvas_cooldown_time
+ data["updatedCanvas"] = updated_canvas
+
+ data["lastUpdateTime"] = last_update_time
+ data["tacmapReady"] = world.time > tacmap_ready_time
+
+ return data
+
/datum/tacmap/ui_static_data(mob/user)
var/list/data = list()
- data["mapRef"] = map_holder.map_ref
+ data["mapRef"] = map_holder?.map_ref
+ data["canDraw"] = FALSE
+ data["canViewTacmap"] = TRUE
+ data["canViewCanvas"] = FALSE
+ data["isXeno"] = FALSE
+
return data
+/datum/tacmap/drawing/ui_static_data(mob/user)
+ var/list/data = list()
+
+ data["mapRef"] = map_holder?.map_ref
+ data["canDraw"] = FALSE
+ data["mapFallback"] = wiki_map_fallback
+
+ var/mob/living/carbon/xenomorph/xeno = user
+ var/is_xeno = istype(xeno)
+ var/faction = is_xeno ? xeno.hivenumber : user.faction
+
+ data["isXeno"] = is_xeno
+ data["canViewTacmap"] = is_xeno
+ data["canViewCanvas"] = faction == FACTION_MARINE || faction == XENO_HIVE_NORMAL
+
+ if(faction == FACTION_MARINE && skillcheck(user, SKILL_LEADERSHIP, SKILL_LEAD_EXPERT) || faction == XENO_HIVE_NORMAL && isqueen(user))
+ data["canDraw"] = TRUE
+ data["canViewTacmap"] = TRUE
+
+ return data
+
+/datum/tacmap/drawing/status_tab_view/ui_static_data(mob/user)
+ var/list/data = list()
+ data["mapFallback"] = wiki_map_fallback
+ data["canDraw"] = FALSE
+ data["canViewTacmap"] = FALSE
+ data["canViewCanvas"] = TRUE
+ data["isXeno"] = FALSE
+
+ return data
+
+/datum/tacmap/drawing/status_tab_view/xeno/ui_static_data(mob/user)
+ var/list/data = list()
+ data["mapFallback"] = wiki_map_fallback
+ data["canDraw"] = FALSE
+ data["canViewTacmap"] = FALSE
+ data["canViewCanvas"] = TRUE
+ data["isXeno"] = TRUE
+
+ return data
+
+/datum/tacmap/drawing/ui_close(mob/user)
+ . = ..()
+ action_queue_change = 0
+ updated_canvas = FALSE
+ toolbar_color_selection = "black"
+ toolbar_updated_selection = "black"
+
+/datum/tacmap/drawing/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
+
+ var/mob/user = ui.user
+ var/mob/living/carbon/xenomorph/xeno = user
+ var/faction = istype(xeno) ? xeno.hivenumber : user.faction
+ if(faction == FACTION_NEUTRAL && isobserver(user))
+ faction = allowed_flags == MINIMAP_FLAG_XENO ? XENO_HIVE_NORMAL : FACTION_MARINE
+
+ switch (action)
+ if ("menuSelect")
+ if(params["selection"] != "new canvas")
+ if(updated_canvas)
+ updated_canvas = FALSE
+ toolbar_updated_selection = toolbar_color_selection // doing this if it == canvas can cause a latency issue with the stroke.
+ else
+ distribute_current_map_png(faction)
+ last_update_time = world.time
+ // An attempt to get the image to load on first try in the interface, but doesn't seem always reliable
+
+ new_current_map = get_unannounced_tacmap_data_png(faction)
+ old_map = get_tacmap_data_png(faction)
+ current_svg = get_tacmap_data_svg(faction)
+
+ if ("updateCanvas")
+ // forces state change, this will export the svg.
+ toolbar_updated_selection = "export"
+ updated_canvas = TRUE
+ action_queue_change += 1
+
+ if ("clearCanvas")
+ toolbar_updated_selection = "clear"
+ updated_canvas = FALSE
+ action_queue_change += 1
+
+ if ("undoChange")
+ toolbar_updated_selection = "undo"
+ updated_canvas = FALSE
+ action_queue_change += 1
+
+ if ("selectColor")
+ var/newColor = params["color"]
+ if(newColor)
+ toolbar_color_selection = newColor
+ toolbar_updated_selection = newColor
+ action_queue_change += 1
+
+ if ("onDraw")
+ updated_canvas = FALSE
+
+ if ("selectAnnouncement")
+ if(!istype(params["image"], /list)) // potentially very serious?
+ return FALSE
+
+ if(faction == FACTION_MARINE)
+ GLOB.uscm_flat_tacmap_data += new_current_map
+ else if(faction == XENO_HIVE_NORMAL)
+ GLOB.xeno_flat_tacmap_data += new_current_map
+
+ store_current_svg_coords(faction, params["image"], user)
+ current_svg = get_tacmap_data_svg(faction)
+ old_map = get_tacmap_data_png(faction)
+
+ if(faction == FACTION_MARINE)
+ COOLDOWN_START(GLOB, uscm_canvas_cooldown, canvas_cooldown_time)
+ var/mob/living/carbon/human/human_leader = user
+ for(var/datum/squad/current_squad in RoleAuthority.squads)
+ current_squad.send_maptext("Tactical map update in progress...", "Tactical Map:")
+ human_leader.visible_message(SPAN_BOLDNOTICE("Tactical map update in progress..."))
+ playsound_client(human_leader.client, "sound/effects/sos-morse-code.ogg")
+ notify_ghosts(header = "Tactical Map", message = "The USCM tactical map has been updated.", ghost_sound = "sound/effects/sos-morse-code.ogg", notify_volume = 80, action = NOTIFY_USCM_TACMAP, enter_link = "uscm_tacmap=1", enter_text = "View", source = owner)
+
+ else if(faction == XENO_HIVE_NORMAL)
+ var/mutable_appearance/appearance = mutable_appearance(icon('icons/mob/hud/actions_xeno.dmi'), "toggle_queen_zoom")
+ COOLDOWN_START(GLOB, xeno_canvas_cooldown, canvas_cooldown_time)
+ xeno_maptext("The Queen has updated your hive mind map", "You sense something unusual...", faction)
+ notify_ghosts(header = "Tactical Map", message = "The Xenomorph tactical map has been updated.", ghost_sound = "sound/voice/alien_distantroar_3.ogg", notify_volume = 50, action = NOTIFY_XENO_TACMAP, enter_link = "xeno_tacmap=1", enter_text = "View", source = user, alert_overlay = appearance)
+
+ toolbar_updated_selection = toolbar_color_selection
+ message_admins("[key_name(user)] has updated the tactical map for [faction].")
+ updated_canvas = FALSE
+
+ return TRUE
+
/datum/tacmap/ui_status(mob/user)
if(!(isatom(owner)))
return UI_INTERACTIVE
@@ -493,7 +904,7 @@ SUBSYSTEM_DEF(minimaps)
else
return UI_CLOSE
-/datum/tacmap/xeno/ui_status(mob/user)
+/datum/tacmap/drawing/xeno/ui_status(mob/user)
if(!isxeno(user))
return UI_CLOSE
@@ -516,3 +927,71 @@ SUBSYSTEM_DEF(minimaps)
/datum/tacmap_holder/Destroy()
map = null
return ..()
+
+/datum/flattened_tacmap
+ var/flat_tacmap
+ var/asset_key
+ var/time
+
+/datum/flattened_tacmap/New(flat_tacmap, asset_key)
+ src.flat_tacmap = flat_tacmap
+ src.asset_key = asset_key
+ src.time = time_stamp()
+
+/datum/svg_overlay
+ var/svg_data
+ var/ckey
+ var/name
+ var/time
+
+/datum/svg_overlay/New(svg_data, mob/user)
+ src.svg_data = svg_data
+ src.ckey = user?.persistent_ckey
+ src.name = user?.real_name
+ src.time = time_stamp()
+
+/// Callback when timer indicates the tacmap is flattenable now
+/datum/tacmap/drawing/proc/on_tacmap_fire(faction)
+ distribute_current_map_png(faction)
+ last_update_time = world.time
+
+/// Gets the MINIMAP_FLAG for the provided faction or hivenumber if one exists
+/proc/get_minimap_flag_for_faction(faction)
+ switch(faction)
+ if(XENO_HIVE_NORMAL)
+ return MINIMAP_FLAG_XENO
+ if(FACTION_MARINE)
+ return MINIMAP_FLAG_USCM
+ if(FACTION_UPP)
+ return MINIMAP_FLAG_UPP
+ if(FACTION_WY)
+ return MINIMAP_FLAG_USCM
+ if(FACTION_CLF)
+ return MINIMAP_FLAG_CLF
+ if(FACTION_PMC)
+ return MINIMAP_FLAG_PMC
+ if(FACTION_YAUTJA)
+ return MINIMAP_FLAG_YAUTJA
+ if(XENO_HIVE_CORRUPTED)
+ return MINIMAP_FLAG_XENO_CORRUPTED
+ if(XENO_HIVE_ALPHA)
+ return MINIMAP_FLAG_XENO_ALPHA
+ if(XENO_HIVE_BRAVO)
+ return MINIMAP_FLAG_XENO_BRAVO
+ if(XENO_HIVE_CHARLIE)
+ return MINIMAP_FLAG_XENO_CHARLIE
+ if(XENO_HIVE_DELTA)
+ return MINIMAP_FLAG_XENO_DELTA
+ if(XENO_HIVE_FERAL)
+ return MINIMAP_FLAG_XENO_FERAL
+ if(XENO_HIVE_TAMED)
+ return MINIMAP_FLAG_XENO_TAMED
+ if(XENO_HIVE_MUTATED)
+ return MINIMAP_FLAG_XENO_MUTATED
+ if(XENO_HIVE_FORSAKEN)
+ return MINIMAP_FLAG_XENO_FORSAKEN
+ if(XENO_HIVE_YAUTJA)
+ return MINIMAP_FLAG_YAUTJA
+ if(XENO_HIVE_RENEGADE)
+ return MINIMAP_FLAG_XENO_RENEGADE
+ return 0
diff --git a/code/game/gamemodes/cm_initialize.dm b/code/game/gamemodes/cm_initialize.dm
index c15d7f008366..72fc917d81e1 100644
--- a/code/game/gamemodes/cm_initialize.dm
+++ b/code/game/gamemodes/cm_initialize.dm
@@ -423,7 +423,7 @@ Additional game mode variables.
for(var/mob_name in picked_hive.banished_ckeys)
if(picked_hive.banished_ckeys[mob_name] == xeno_candidate.ckey)
to_chat(xeno_candidate, SPAN_WARNING("You are banished from the [picked_hive], you may not rejoin unless the Queen re-admits you or dies."))
- return
+ return FALSE
if(isnewplayer(xeno_candidate))
var/mob/new_player/noob = xeno_candidate
noob.close_spawn_windows()
@@ -443,9 +443,6 @@ Additional game mode variables.
return FALSE
new_xeno = userInput
- if(!xeno_candidate)
- return FALSE
-
if(!(new_xeno in GLOB.living_xeno_list) || new_xeno.stat == DEAD)
to_chat(xeno_candidate, SPAN_WARNING("You cannot join if the xenomorph is dead."))
return FALSE
@@ -479,14 +476,14 @@ Additional game mode variables.
else new_xeno = pick(available_xenos_non_ssd) //Just picks something at random.
if(istype(new_xeno) && xeno_candidate && xeno_candidate.client)
if(isnewplayer(xeno_candidate))
- var/mob/new_player/N = xeno_candidate
- N.close_spawn_windows()
+ var/mob/new_player/noob = xeno_candidate
+ noob.close_spawn_windows()
for(var/mob_name in new_xeno.hive.banished_ckeys)
if(new_xeno.hive.banished_ckeys[mob_name] == xeno_candidate.ckey)
to_chat(xeno_candidate, SPAN_WARNING("You are banished from this hive, You may not rejoin unless the Queen re-admits you or dies."))
- return
+ return FALSE
if(transfer_xeno(xeno_candidate, new_xeno))
- return 1
+ return TRUE
to_chat(xeno_candidate, "JAS01: Something went wrong, tell a coder.")
/datum/game_mode/proc/attempt_to_join_as_facehugger(mob/xeno_candidate)
@@ -614,20 +611,21 @@ Additional game mode variables.
/datum/game_mode/proc/transfer_xeno(xeno_candidate, mob/living/new_xeno)
if(!xeno_candidate || !isxeno(new_xeno) || QDELETED(new_xeno))
return FALSE
+
var/datum/mind/xeno_candidate_mind
if(ismind(xeno_candidate))
xeno_candidate_mind = xeno_candidate
else if(ismob(xeno_candidate))
- var/mob/M = xeno_candidate
- if(M.mind)
- xeno_candidate_mind = M.mind
+ var/mob/xeno_candidate_mob = xeno_candidate
+ if(xeno_candidate_mob.mind)
+ xeno_candidate_mind = xeno_candidate_mob.mind
else
- xeno_candidate_mind = new /datum/mind(M.key, M.ckey)
+ xeno_candidate_mind = new /datum/mind(xeno_candidate_mob.key, xeno_candidate_mob.ckey)
xeno_candidate_mind.active = TRUE
xeno_candidate_mind.current = new_xeno
else if(isclient(xeno_candidate))
- var/client/C = xeno_candidate
- xeno_candidate_mind = new /datum/mind(C.key, C.ckey)
+ var/client/xeno_candidate_client = xeno_candidate
+ xeno_candidate_mind = new /datum/mind(xeno_candidate_client.key, xeno_candidate_client.ckey)
xeno_candidate_mind.active = TRUE
xeno_candidate_mind.current = new_xeno
else
diff --git a/code/game/machinery/computer/communications.dm b/code/game/machinery/computer/communications.dm
index 530b1fe6593d..7a51539b8888 100644
--- a/code/game/machinery/computer/communications.dm
+++ b/code/game/machinery/computer/communications.dm
@@ -43,7 +43,7 @@
var/stat_msg1
var/stat_msg2
- var/datum/tacmap/tacmap
+ var/datum/tacmap/drawing/tacmap
var/minimap_type = MINIMAP_FLAG_USCM
processing = TRUE
diff --git a/code/game/machinery/computer/groundside_operations.dm b/code/game/machinery/computer/groundside_operations.dm
index 1a15606335e9..7b4c2d5df771 100644
--- a/code/game/machinery/computer/groundside_operations.dm
+++ b/code/game/machinery/computer/groundside_operations.dm
@@ -25,7 +25,11 @@
add_pmcs = FALSE
else if(SSticker.current_state < GAME_STATE_PLAYING)
RegisterSignal(SSdcs, COMSIG_GLOB_MODE_PRESETUP, PROC_REF(disable_pmc))
- tacmap = new(src, minimap_type)
+ if(announcement_faction == FACTION_MARINE)
+ tacmap = new /datum/tacmap/drawing(src, minimap_type)
+ else
+ tacmap = new(src, minimap_type) // Non-drawing version
+
return ..()
/obj/structure/machinery/computer/groundside_operations/Destroy()
diff --git a/code/game/objects/items/devices/cictablet.dm b/code/game/objects/items/devices/cictablet.dm
index 2c3171833adf..3f87b2bfbea2 100644
--- a/code/game/objects/items/devices/cictablet.dm
+++ b/code/game/objects/items/devices/cictablet.dm
@@ -24,7 +24,10 @@
COOLDOWN_DECLARE(distress_cooldown)
/obj/item/device/cotablet/Initialize()
- tacmap = new(src, minimap_type)
+ if(announcement_faction == FACTION_MARINE)
+ tacmap = new /datum/tacmap/drawing(src, minimap_type)
+ else
+ tacmap = new(src, minimap_type) // Non-drawing version
if(SSticker.mode && MODE_HAS_FLAG(MODE_FACTION_CLASH))
add_pmcs = FALSE
else if(SSticker.current_state < GAME_STATE_PLAYING)
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index 60d418d969ba..5cd5784cfe2e 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -69,6 +69,7 @@ GLOBAL_LIST_INIT(admin_verbs_default, list(
/client/proc/toggle_ares_ping,
/client/proc/cmd_admin_say, /*staff-only ooc chat*/
/client/proc/cmd_mod_say, /* alternate way of typing asay, no different than cmd_admin_say */
+ /client/proc/cmd_admin_tacmaps_panel,
))
GLOBAL_LIST_INIT(admin_verbs_admin, list(
diff --git a/code/modules/admin/tacmap_panel/tacmap_admin_panel.dm b/code/modules/admin/tacmap_panel/tacmap_admin_panel.dm
new file mode 100644
index 000000000000..dcc8c7d5b664
--- /dev/null
+++ b/code/modules/admin/tacmap_panel/tacmap_admin_panel.dm
@@ -0,0 +1,9 @@
+/client/proc/cmd_admin_tacmaps_panel()
+ set name = "Tacmaps Panel"
+ set category = "Admin.Panels"
+
+ if(!check_rights(R_ADMIN|R_MOD))
+ to_chat(src, "Only administrators may use this command.")
+ return
+
+ GLOB.tacmap_admin_panel.tgui_interact(mob)
diff --git a/code/modules/admin/tacmap_panel/tacmap_admin_panel_tgui.dm b/code/modules/admin/tacmap_panel/tacmap_admin_panel_tgui.dm
new file mode 100644
index 000000000000..e4b6f6846031
--- /dev/null
+++ b/code/modules/admin/tacmap_panel/tacmap_admin_panel_tgui.dm
@@ -0,0 +1,152 @@
+GLOBAL_DATUM_INIT(tacmap_admin_panel, /datum/tacmap_admin_panel, new)
+
+#define LATEST_SELECTION -1
+
+/datum/tacmap_admin_panel
+ var/name = "Tacmap Panel"
+ /// The index picked last for USCM (zero indexed), -1 will try to select latest if it exists
+ var/uscm_selection = LATEST_SELECTION
+ /// The index picked last for Xenos (zero indexed), -1 will try to select latest if it exists
+ var/xeno_selection = LATEST_SELECTION
+ /// A url that will point to the wiki map for the current map as a fall back image
+ var/static/wiki_map_fallback
+ /// The last time the map selection was changed - used as a key to trick react into updating the map
+ var/last_update_time = 0
+
+/datum/tacmap_admin_panel/tgui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ if(!wiki_map_fallback)
+ var/wiki_url = CONFIG_GET(string/wikiurl)
+ var/obj/item/map/current_map/new_map = new
+ if(wiki_url && new_map.html_link)
+ wiki_map_fallback ="[wiki_url]/[new_map.html_link]"
+ else
+ debug_log("Failed to determine fallback wiki map! Attempted '[wiki_url]/[new_map.html_link]'")
+ qdel(new_map)
+
+ // Ensure we actually have the latest map images sent (recache can handle older/different faction maps)
+ resend_current_map_png(user)
+
+ ui = new(user, src, "TacmapAdminPanel", "Tacmap Panel")
+ ui.open()
+
+/datum/tacmap_admin_panel/ui_state(mob/user)
+ return GLOB.admin_state
+
+/datum/tacmap_admin_panel/ui_data(mob/user)
+ var/list/data = list()
+ var/list/uscm_ckeys = list()
+ var/list/xeno_ckeys = list()
+ var/list/uscm_names = list()
+ var/list/xeno_names = list()
+ var/list/uscm_times = list()
+ var/list/xeno_times = list()
+
+ // Assumption: Length of flat_tacmap_data is the same as svg_tacmap_data
+ var/uscm_length = length(GLOB.uscm_svg_tacmap_data)
+ if(uscm_selection < 0 || uscm_selection >= uscm_length)
+ uscm_selection = uscm_length - 1
+ for(var/i = 1, i <= uscm_length, i++)
+ var/datum/svg_overlay/current_svg = GLOB.uscm_svg_tacmap_data[i]
+ uscm_ckeys += current_svg.ckey
+ uscm_names += current_svg.name
+ uscm_times += current_svg.time
+ data["uscm_ckeys"] = uscm_ckeys
+ data["uscm_names"] = uscm_names
+ data["uscm_times"] = uscm_times
+
+ var/xeno_length = length(GLOB.xeno_svg_tacmap_data)
+ if(xeno_selection < 0 || xeno_selection >= xeno_length)
+ xeno_selection = xeno_length - 1
+ for(var/i = 1, i <= xeno_length, i++)
+ var/datum/svg_overlay/current_svg = GLOB.xeno_svg_tacmap_data[i]
+ xeno_ckeys += current_svg.ckey
+ xeno_names += current_svg.name
+ xeno_times += current_svg.time
+ data["xeno_ckeys"] = xeno_ckeys
+ data["xeno_names"] = xeno_names
+ data["xeno_times"] = xeno_times
+
+ if(uscm_selection == LATEST_SELECTION)
+ data["uscm_map"] = null
+ data["uscm_svg"] = null
+ else
+ var/datum/flattened_tacmap/selected_flat = GLOB.uscm_flat_tacmap_data[uscm_selection + 1]
+ var/datum/svg_overlay/selected_svg = GLOB.uscm_svg_tacmap_data[uscm_selection + 1]
+ data["uscm_map"] = selected_flat.flat_tacmap
+ data["uscm_svg"] = selected_svg.svg_data
+
+ if(xeno_selection == LATEST_SELECTION)
+ data["xeno_map"] = null
+ data["xeno_svg"] = null
+ else
+ var/datum/flattened_tacmap/selected_flat = GLOB.xeno_flat_tacmap_data[xeno_selection + 1]
+ var/datum/svg_overlay/selected_svg = GLOB.xeno_svg_tacmap_data[xeno_selection + 1]
+ data["xeno_map"] = selected_flat.flat_tacmap
+ data["xeno_svg"] = selected_svg.svg_data
+
+ data["uscm_selection"] = uscm_selection
+ data["xeno_selection"] = xeno_selection
+ data["map_fallback"] = wiki_map_fallback
+ data["last_update_time"] = last_update_time
+
+ return data
+
+/datum/tacmap_admin_panel/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
+
+ var/mob/user = ui.user
+ var/client/client_user = user.client
+ if(!client_user)
+ return // Is this even possible?
+
+ switch(action)
+ if("recache")
+ var/is_uscm = params["uscm"]
+ var/datum/flattened_tacmap/selected_flat
+ if(is_uscm)
+ if(uscm_selection == LATEST_SELECTION)
+ return TRUE
+ selected_flat = GLOB.uscm_flat_tacmap_data[uscm_selection + 1]
+ else
+ if(xeno_selection == LATEST_SELECTION)
+ return TRUE
+ selected_flat = GLOB.xeno_flat_tacmap_data[xeno_selection + 1]
+ SSassets.transport.send_assets(client_user, selected_flat.asset_key)
+ last_update_time = world.time
+ return TRUE
+
+ if("change_selection")
+ var/is_uscm = params["uscm"]
+ if(is_uscm)
+ uscm_selection = params["index"]
+ else
+ xeno_selection = params["index"]
+ last_update_time = world.time
+ return TRUE
+
+ if("delete")
+ var/is_uscm = params["uscm"]
+ var/datum/svg_overlay/selected_svg
+ if(is_uscm)
+ if(uscm_selection == LATEST_SELECTION)
+ return TRUE
+ selected_svg = GLOB.uscm_svg_tacmap_data[uscm_selection + 1]
+ else
+ if(xeno_selection == LATEST_SELECTION)
+ return TRUE
+ selected_svg = GLOB.xeno_svg_tacmap_data[xeno_selection + 1]
+ selected_svg.svg_data = null
+ last_update_time = world.time
+ message_admins("[key_name_admin(usr)] deleted the tactical map drawing by [selected_svg.ckey].")
+ return TRUE
+
+/datum/tacmap_admin_panel/ui_close(mob/user)
+ . = ..()
+ uscm_selection = LATEST_SELECTION
+ xeno_selection = LATEST_SELECTION
+
+#undef LATEST_SELECTION
diff --git a/code/modules/almayer/machinery.dm b/code/modules/almayer/machinery.dm
index 42aac0e6c0f6..74ce9a81eb88 100644
--- a/code/modules/almayer/machinery.dm
+++ b/code/modules/almayer/machinery.dm
@@ -80,13 +80,19 @@
use_power = USE_POWER_IDLE
density = TRUE
idle_power_usage = 2
- ///flags that we want to be shown when you interact with this table
var/datum/tacmap/map
+ ///flags that we want to be shown when you interact with this table
var/minimap_type = MINIMAP_FLAG_USCM
+ ///The faction that is intended to use this structure (determines type of tacmap used)
+ var/faction = FACTION_MARINE
/obj/structure/machinery/prop/almayer/CICmap/Initialize()
. = ..()
- map = new(src, minimap_type)
+
+ if (faction == FACTION_MARINE)
+ map = new /datum/tacmap/drawing(src, minimap_type)
+ else
+ map = new(src, minimap_type) // Non-drawing version
/obj/structure/machinery/prop/almayer/CICmap/Destroy()
QDEL_NULL(map)
@@ -99,12 +105,15 @@
/obj/structure/machinery/prop/almayer/CICmap/upp
minimap_type = MINIMAP_FLAG_UPP
+ faction = FACTION_UPP
/obj/structure/machinery/prop/almayer/CICmap/clf
minimap_type = MINIMAP_FLAG_CLF
+ faction = FACTION_CLF
/obj/structure/machinery/prop/almayer/CICmap/pmc
minimap_type = MINIMAP_FLAG_PMC
+ faction = FACTION_PMC
//Nonpower using props
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index ec1b48989fc4..e1dbd1f026c1 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -147,7 +147,6 @@ GLOBAL_LIST_INIT(whitelisted_client_procs, list(
return
cmd_admin_pm(receiver_client, null)
return
-
else if(href_list["FaxView"])
var/datum/fax/info = locate(href_list["FaxView"])
@@ -164,6 +163,14 @@ GLOBAL_LIST_INIT(whitelisted_client_procs, list(
else if(href_list["medals_panel"])
GLOB.medals_panel.tgui_interact(mob)
+ else if(href_list["tacmaps_panel"])
+ GLOB.tacmap_admin_panel.tgui_interact(mob)
+
+ else if(href_list["MapView"])
+ if(isxeno(mob))
+ return
+ GLOB.uscm_tacmap_status.tgui_interact(mob)
+
//NOTES OVERHAUL
if(href_list["add_merit_info"])
var/key = href_list["add_merit_info"]
diff --git a/code/modules/cm_aliens/structures/special/pylon_core.dm b/code/modules/cm_aliens/structures/special/pylon_core.dm
index 88964d76c3eb..62a7417c57f8 100644
--- a/code/modules/cm_aliens/structures/special/pylon_core.dm
+++ b/code/modules/cm_aliens/structures/special/pylon_core.dm
@@ -248,7 +248,7 @@
/obj/effect/alien/resin/special/pylon/core/proc/update_minimap_icon()
SSminimaps.remove_marker(src)
- SSminimaps.add_marker(src, z, MINIMAP_FLAG_XENO, "core")
+ SSminimaps.add_marker(src, z, get_minimap_flag_for_faction(linked_hive?.hivenumber), "core")
/obj/effect/alien/resin/special/pylon/core/process()
. = ..()
@@ -318,7 +318,7 @@
to_chat(new_xeno, SPAN_XENOANNOUNCE("You are a xenomorph larva awakened from slumber!"))
playsound(new_xeno, 'sound/effects/xeno_newlarva.ogg', 50, 1)
if(new_xeno.client)
- if(new_xeno.client?.prefs.toggles_flashing & FLASH_POOLSPAWN)
+ if(new_xeno.client.prefs.toggles_flashing & FLASH_POOLSPAWN)
window_flash(new_xeno.client)
linked_hive.stored_larva--
diff --git a/code/modules/cm_aliens/structures/tunnel.dm b/code/modules/cm_aliens/structures/tunnel.dm
index 9686b7253c00..6ac90338045d 100644
--- a/code/modules/cm_aliens/structures/tunnel.dm
+++ b/code/modules/cm_aliens/structures/tunnel.dm
@@ -48,7 +48,7 @@
if(resin_trap)
qdel(resin_trap)
- SSminimaps.add_marker(src, z, MINIMAP_FLAG_XENO, "xenotunnel")
+ SSminimaps.add_marker(src, z, get_minimap_flag_for_faction(hivenumber), "xenotunnel")
/obj/structure/tunnel/Destroy()
if(hive)
diff --git a/code/modules/cm_marines/overwatch.dm b/code/modules/cm_marines/overwatch.dm
index 6f571eeddaf1..8e3150cef1df 100644
--- a/code/modules/cm_marines/overwatch.dm
+++ b/code/modules/cm_marines/overwatch.dm
@@ -39,8 +39,11 @@
/obj/structure/machinery/computer/overwatch/Initialize()
. = ..()
- tacmap = new(src, minimap_type)
+ if (faction == FACTION_MARINE)
+ tacmap = new /datum/tacmap/drawing(src, minimap_type)
+ else
+ tacmap = new(src, minimap_type) // Non-drawing version
/obj/structure/machinery/computer/overwatch/Destroy()
QDEL_NULL(tacmap)
diff --git a/code/modules/cm_preds/yaut_machines.dm b/code/modules/cm_preds/yaut_machines.dm
index a1782ca22b85..f076c6782d9a 100644
--- a/code/modules/cm_preds/yaut_machines.dm
+++ b/code/modules/cm_preds/yaut_machines.dm
@@ -6,6 +6,7 @@
breakable = FALSE
minimap_type = MINIMAP_FLAG_ALL
+ faction = FACTION_YAUTJA
/obj/structure/machinery/autolathe/yautja
name = "yautja autolathe"
diff --git a/code/modules/desert_dam/motion_sensor/sensortower.dm b/code/modules/desert_dam/motion_sensor/sensortower.dm
index 5783d0ce9f20..4ef11c32245d 100644
--- a/code/modules/desert_dam/motion_sensor/sensortower.dm
+++ b/code/modules/desert_dam/motion_sensor/sensortower.dm
@@ -68,7 +68,7 @@
return
SSminimaps.remove_marker(current_xeno)
- current_xeno.add_minimap_marker(MINIMAP_FLAG_USCM|MINIMAP_FLAG_XENO)
+ current_xeno.add_minimap_marker(MINIMAP_FLAG_USCM|get_minimap_flag_for_faction(current_xeno.hivenumber))
minimap_added += WEAKREF(current_xeno)
/obj/structure/machinery/sensortower/proc/checkfailure()
diff --git a/code/modules/escape_menu/admin_buttons.dm b/code/modules/escape_menu/admin_buttons.dm
index e6771d05bf68..661901c1b77a 100644
--- a/code/modules/escape_menu/admin_buttons.dm
+++ b/code/modules/escape_menu/admin_buttons.dm
@@ -46,7 +46,7 @@
new /atom/movable/screen/escape_menu/home_button(
null,
src,
- "Medal Panel",
+ "Medals Panel",
/* offset = */ 5,
CALLBACK(src, PROC_REF(home_medal)),
)
@@ -56,8 +56,18 @@
new /atom/movable/screen/escape_menu/home_button(
null,
src,
- "Teleport Panel",
+ "Tacmaps Panel",
/* offset = */ 6,
+ CALLBACK(src, PROC_REF(home_tacmaps)),
+ )
+ )
+
+ page_holder.give_screen_object(
+ new /atom/movable/screen/escape_menu/home_button(
+ null,
+ src,
+ "Teleport Panel",
+ /* offset = */ 7,
CALLBACK(src, PROC_REF(home_teleport)),
)
)
@@ -67,7 +77,7 @@
null,
src,
"Inview Panel",
- /* offset = */ 7,
+ /* offset = */ 8,
CALLBACK(src, PROC_REF(home_inview)),
)
)
@@ -77,7 +87,7 @@
null,
src,
"Unban Panel",
- /* offset = */ 8,
+ /* offset = */ 9,
CALLBACK(src, PROC_REF(home_unban)),
)
)
@@ -87,7 +97,7 @@
null,
src,
"Shuttle Manipulator",
- /* offset = */ 9,
+ /* offset = */ 10,
CALLBACK(src, PROC_REF(home_shuttle)),
)
)
@@ -117,6 +127,12 @@
GLOB.medals_panel.tgui_interact(client?.mob)
+/datum/escape_menu/proc/home_tacmaps()
+ if(!client?.admin_holder.check_for_rights(R_ADMIN|R_MOD))
+ return
+
+ GLOB.tacmap_admin_panel.tgui_interact(client?.mob)
+
/datum/escape_menu/proc/home_teleport()
if(!client?.admin_holder.check_for_rights(R_MOD))
return
diff --git a/code/modules/maptext_alerts/screen_alerts.dm b/code/modules/maptext_alerts/screen_alerts.dm
index 820c64301bc2..0b923f7dc753 100644
--- a/code/modules/maptext_alerts/screen_alerts.dm
+++ b/code/modules/maptext_alerts/screen_alerts.dm
@@ -246,3 +246,8 @@
ghost_user.do_observe(target)
if(NOTIFY_JOIN_XENO)
ghost_user.join_as_alien()
+ if(NOTIFY_USCM_TACMAP)
+ GLOB.uscm_tacmap_status.tgui_interact(ghost_user)
+ if(NOTIFY_XENO_TACMAP)
+ GLOB.xeno_tacmap_status.tgui_interact(ghost_user)
+
diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm
index b6bc636c2375..cf3f9e8b4702 100644
--- a/code/modules/mob/dead/observer/observer.dm
+++ b/code/modules/mob/dead/observer/observer.dm
@@ -377,6 +377,10 @@
handle_joining_as_freed_mob(locate(href_list["claim_freed"]))
if(href_list["join_xeno"])
join_as_alien()
+ if(href_list[NOTIFY_USCM_TACMAP])
+ GLOB.uscm_tacmap_status.tgui_interact(src)
+ if(href_list[NOTIFY_XENO_TACMAP])
+ GLOB.xeno_tacmap_status.tgui_interact(src)
/mob/dead/observer/proc/set_huds_from_prefs()
if(!client || !client.prefs)
@@ -898,6 +902,23 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
GLOB.hive_datum[hives[faction]].hive_ui.open_hive_status(src)
+/mob/dead/observer/verb/view_uscm_tacmap()
+ set name = "View USCM Tacmap"
+ set category = "Ghost.View"
+
+ GLOB.uscm_tacmap_status.tgui_interact(src)
+
+/mob/dead/observer/verb/view_xeno_tacmap()
+ set name = "View Xeno Tacmap"
+ set category = "Ghost.View"
+
+ var/datum/hive_status/hive = GLOB.hive_datum[XENO_HIVE_NORMAL]
+ if(!hive || !length(hive.totalXenos))
+ to_chat(src, SPAN_ALERT("There seems to be no living normal hive at the moment"))
+ return
+
+ GLOB.xeno_tacmap_status.tgui_interact(src)
+
/mob/dead/verb/join_as_alien()
set category = "Ghost.Join"
set name = "Join as Xeno"
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index 56765f8a6ef1..66892025ff6a 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -115,7 +115,9 @@
. += "Primary Objective: [html_decode(assigned_squad.primary_objective)]"
if(assigned_squad.secondary_objective)
. += "Secondary Objective: [html_decode(assigned_squad.secondary_objective)]"
-
+ if(faction == FACTION_MARINE)
+ . += ""
+ . += "View Tactical Map"
if(mobility_aura)
. += "Active Order: MOVE"
if(protection_aura)
diff --git a/code/modules/mob/living/carbon/xenomorph/Xenomorph.dm b/code/modules/mob/living/carbon/xenomorph/Xenomorph.dm
index 93f1f0da7d20..cf3be6de9086 100644
--- a/code/modules/mob/living/carbon/xenomorph/Xenomorph.dm
+++ b/code/modules/mob/living/carbon/xenomorph/Xenomorph.dm
@@ -508,7 +508,11 @@
if(queen.can_not_harm(src))
return COMPONENT_SCREECH_ACT_CANCEL
-/mob/living/carbon/xenomorph/proc/add_minimap_marker(flags = MINIMAP_FLAG_XENO)
+/// Adds a minimap marker for this xeno using the provided flags.
+/// If flags is 0, it will use get_minimap_flag_for_faction for this xeno
+/mob/living/carbon/xenomorph/proc/add_minimap_marker(flags)
+ if(!flags)
+ flags = get_minimap_flag_for_faction(hivenumber)
if(IS_XENO_LEADER(src))
SSminimaps.add_marker(src, z, hud_flags = flags, given_image = caste.get_minimap_icon(), overlay_iconstates = list(caste.minimap_leadered_overlay))
return
diff --git a/code/modules/mob/living/carbon/xenomorph/abilities/queen/queen_powers.dm b/code/modules/mob/living/carbon/xenomorph/abilities/queen/queen_powers.dm
index b489a6382898..65769eac9cdf 100644
--- a/code/modules/mob/living/carbon/xenomorph/abilities/queen/queen_powers.dm
+++ b/code/modules/mob/living/carbon/xenomorph/abilities/queen/queen_powers.dm
@@ -698,5 +698,5 @@
set name = "View Xeno Tacmap"
set desc = "This opens a tactical map, where you can see where every xenomorph is."
set category = "Alien"
-
hive.tacmap.tgui_interact(src)
+
diff --git a/code/modules/mob/living/carbon/xenomorph/xeno_defines.dm b/code/modules/mob/living/carbon/xenomorph/xeno_defines.dm
index 7e24ac6e5f9a..810c8f58666b 100644
--- a/code/modules/mob/living/carbon/xenomorph/xeno_defines.dm
+++ b/code/modules/mob/living/carbon/xenomorph/xeno_defines.dm
@@ -362,7 +362,7 @@
/// This number divides the total xenos counted for slots to give the max number of lesser drones
var/playable_lesser_drones_max_divisor = 3
- var/datum/tacmap/xeno/tacmap
+ var/datum/tacmap/drawing/xeno/tacmap
var/minimap_type = MINIMAP_FLAG_XENO
/datum/hive_status/New()
@@ -370,6 +370,7 @@
hive_ui = new(src)
mark_ui = new(src)
faction_ui = new(src)
+ minimap_type = get_minimap_flag_for_faction(hivenumber)
tacmap = new(src, minimap_type)
if(!internal_faction)
internal_faction = name
diff --git a/code/modules/mob/new_player/new_player.dm b/code/modules/mob/new_player/new_player.dm
index c53fe47257d2..5938f7f51a2d 100644
--- a/code/modules/mob/new_player/new_player.dm
+++ b/code/modules/mob/new_player/new_player.dm
@@ -150,7 +150,7 @@
observer.set_huds_from_prefs()
qdel(src)
- return 1
+ return TRUE
if("late_join")
@@ -276,11 +276,11 @@
if(player.get_playtime(STATISTIC_HUMAN) == 0 && player.get_playtime(STATISTIC_XENO) == 0)
msg_admin_niche("NEW JOIN: [key_name(character, 1, 1, 0)]. IP: [character.lastKnownIP], CID: [character.computer_id]")
if(character.client)
- var/client/C = character.client
- if(C.player_data && C.player_data.playtime_loaded && length(C.player_data.playtimes) == 0)
+ var/client/client = character.client
+ if(client.player_data && client.player_data.playtime_loaded && length(client.player_data.playtimes) == 0)
msg_admin_niche("NEW PLAYER: [key_name(character, 1, 1, 0)]. IP: [character.lastKnownIP], CID: [character.computer_id]")
- if(C.player_data && C.player_data.playtime_loaded && ((round(C.get_total_human_playtime() DECISECONDS_TO_HOURS, 0.1)) <= 5))
- msg_sea("NEW PLAYER: [key_name(character, 0, 1, 0)] only has [(round(C.get_total_human_playtime() DECISECONDS_TO_HOURS, 0.1))] hours as a human. Current role: [get_actual_job_name(character)] - Current location: [get_area(character)]")
+ if(client.player_data && client.player_data.playtime_loaded && ((round(client.get_total_human_playtime() DECISECONDS_TO_HOURS, 0.1)) <= 5))
+ msg_sea("NEW PLAYER: [key_name(character, 0, 1, 0)] only has [(round(client.get_total_human_playtime() DECISECONDS_TO_HOURS, 0.1))] hours as a human. Current role: [get_actual_job_name(character)] - Current location: [get_area(character)]")
character.client.init_verbs()
qdel(src)
diff --git a/code/modules/vehicles/apc/apc_command.dm b/code/modules/vehicles/apc/apc_command.dm
index f39780ff3426..e0862ae4f2ab 100644
--- a/code/modules/vehicles/apc/apc_command.dm
+++ b/code/modules/vehicles/apc/apc_command.dm
@@ -59,7 +59,7 @@
continue
SSminimaps.remove_marker(current_xeno)
- current_xeno.add_minimap_marker(MINIMAP_FLAG_USCM|MINIMAP_FLAG_XENO)
+ current_xeno.add_minimap_marker(MINIMAP_FLAG_USCM|get_minimap_flag_for_faction(current_xeno.hivenumber))
minimap_added += WEAKREF(current_xeno)
else
if(WEAKREF(current_xeno) in minimap_added)
diff --git a/colonialmarines.dme b/colonialmarines.dme
index 6d079582773c..652878d97895 100644
--- a/colonialmarines.dme
+++ b/colonialmarines.dme
@@ -1375,6 +1375,8 @@
#include "code\modules\admin\tabs\event_tab.dm"
#include "code\modules\admin\tabs\round_tab.dm"
#include "code\modules\admin\tabs\server_tab.dm"
+#include "code\modules\admin\tacmap_panel\tacmap_admin_panel.dm"
+#include "code\modules\admin\tacmap_panel\tacmap_admin_panel_tgui.dm"
#include "code\modules\admin\topic\topic.dm"
#include "code\modules\admin\topic\topic_chems.dm"
#include "code\modules\admin\topic\topic_events.dm"
diff --git a/html/changelogs/AutoChangeLog-pr-4475.yml b/html/changelogs/AutoChangeLog-pr-4475.yml
new file mode 100644
index 000000000000..27d43d1e7aca
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-4475.yml
@@ -0,0 +1,5 @@
+author: "Cthulhu80, Drathek"
+delete-after: True
+changes:
+ - rscadd: "Adds drawing to tactical maps, viewable via stat panel for marines and xeno tacmap for xenos."
+ - bugfix: "Corrupted (and other hives) now have separate tactical maps."
\ No newline at end of file
diff --git a/html/statbrowser.js b/html/statbrowser.js
index 105270ad298e..289536d37da1 100644
--- a/html/statbrowser.js
+++ b/html/statbrowser.js
@@ -374,6 +374,8 @@ function draw_debug() {
document.getElementById("statcontent").appendChild(table3);
}
function draw_status() {
+ var status_tab_map_href_exception =
+ "View Tactical Map";
if (!document.getElementById("Status")) {
createStatusTab("Status");
current_tab = "Status";
@@ -384,6 +386,13 @@ function draw_status() {
document
.getElementById("statcontent")
.appendChild(document.createElement("br"));
+ } else if (
+ // hardcoded because merely using .includes() to test for a href seems unreliable for some reason.
+ status_tab_parts[i] == status_tab_map_href_exception
+ ) {
+ var maplink = document.createElement("a");
+ maplink.innerHTML = status_tab_parts[i];
+ document.getElementById("statcontent").appendChild(maplink);
} else {
var div = document.createElement("div");
div.textContent = status_tab_parts[i];
diff --git a/tgui/packages/tgui/interfaces/CanvasLayer.js b/tgui/packages/tgui/interfaces/CanvasLayer.js
new file mode 100644
index 000000000000..e647ae765b1c
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/CanvasLayer.js
@@ -0,0 +1,311 @@
+import { Box, Icon, Tooltip } from '../components';
+import { Component, createRef } from 'inferno';
+
+// this file should probably not be in interfaces, should move it later.
+export class CanvasLayer extends Component {
+ constructor(props) {
+ super(props);
+ this.canvasRef = createRef();
+
+ // color selection
+ // using this.state prevents unpredictable behavior
+ this.state = {
+ selection: this.props.selection,
+ mapLoad: true,
+ };
+
+ // needs to be of type png of jpg
+ this.img = null;
+ this.imageSrc = this.props.imageSrc;
+
+ // stores the stacked lines
+ this.lineStack = [];
+
+ // stores the individual line drawn
+ this.currentLine = [];
+
+ this.ctx = null;
+ this.isPainting = false;
+ this.lastX = null;
+ this.lastY = null;
+
+ this.complexity = 0;
+ }
+
+ componentDidMount() {
+ this.ctx = this.canvasRef.current.getContext('2d');
+ this.ctx.lineWidth = 4;
+ this.ctx.lineCap = 'round';
+
+ this.img = new Image();
+
+ this.img.src = this.imageSrc;
+
+ this.img.onload = () => {
+ this.setState({ mapLoad: true });
+ };
+
+ this.img.onerror = () => {
+ this.setState({ mapLoad: false });
+ };
+
+ this.drawCanvas();
+ }
+
+ handleMouseDown = (e) => {
+ this.isPainting = true;
+
+ const rect = this.canvasRef.current.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+
+ this.ctx.beginPath();
+ this.ctx.moveTo(this.lastX, this.lastY);
+ this.lastX = x;
+ this.lastY = y;
+ };
+
+ handleMouseMove = (e) => {
+ if (!this.isPainting || !this.state.selection) {
+ return;
+ }
+ if (e.buttons === 0) {
+ // We probably dragged off the window - lets not get stuck drawing
+ this.handleMouseUp(e);
+ return;
+ }
+
+ this.ctx.strokeStyle = this.state.selection;
+
+ const rect = this.canvasRef.current.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+
+ if (this.lastX !== null && this.lastY !== null) {
+ // this controls how often we make new strokes
+ if (Math.abs(this.lastX - x) + Math.abs(this.lastY - y) < 20) {
+ return;
+ }
+
+ this.ctx.moveTo(this.lastX, this.lastY);
+ this.ctx.lineTo(x, y);
+ this.ctx.stroke();
+ this.currentLine.push([
+ this.lastX,
+ this.lastY,
+ x,
+ y,
+ this.ctx.strokeStyle,
+ ]);
+ }
+
+ this.lastX = x;
+ this.lastY = y;
+ };
+
+ handleMouseUp = (e) => {
+ if (
+ this.isPainting &&
+ this.state.selection &&
+ this.lastX !== null &&
+ this.lastY !== null
+ ) {
+ const rect = this.canvasRef.current.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+
+ this.ctx.moveTo(this.lastX, this.lastY);
+ this.ctx.lineTo(x, y);
+ this.ctx.stroke();
+ this.currentLine.push([
+ this.lastX,
+ this.lastY,
+ x,
+ y,
+ this.ctx.strokeStyle,
+ ]);
+ }
+
+ this.isPainting = false;
+ this.lastX = null;
+ this.lastY = null;
+
+ if (this.currentLine.length === 0) {
+ return;
+ }
+
+ this.lineStack.push([...this.currentLine]);
+ this.currentLine = [];
+ this.complexity = this.getComplexity();
+ this.props.onDraw();
+ };
+
+ handleSelectionChange = () => {
+ const { selection } = this.props;
+
+ if (selection === 'clear') {
+ this.ctx.clearRect(
+ 0,
+ 0,
+ this.canvasRef.current.width,
+ this.canvasRef.current.height
+ );
+ this.ctx.drawImage(
+ this.img,
+ 0,
+ 0,
+ this.canvasRef.current.width,
+ this.canvasRef.current.height
+ );
+
+ this.lineStack = [];
+ this.complexity = 0;
+ return;
+ }
+
+ if (selection === 'undo') {
+ if (this.lineStack.length === 0) {
+ return;
+ }
+
+ const line = this.lineStack.pop();
+ if (line.length === 0) {
+ return;
+ }
+
+ const prevColor = line[0][4];
+
+ this.ctx.clearRect(
+ 0,
+ 0,
+ this.canvasRef.current.width,
+ this.canvasRef.current.height
+ );
+ this.ctx.drawImage(
+ this.img,
+ 0,
+ 0,
+ this.canvasRef.current.width,
+ this.canvasRef.current.height
+ );
+ this.ctx.globalCompositeOperation = 'source-over';
+
+ this.lineStack.forEach((currentLine) => {
+ currentLine.forEach(([lastX, lastY, x, y, colorSelection]) => {
+ this.ctx.strokeStyle = colorSelection;
+ this.ctx.beginPath();
+ this.ctx.moveTo(lastX, lastY);
+ this.ctx.lineTo(x, y);
+ this.ctx.stroke();
+ });
+ });
+
+ this.complexity = this.getComplexity();
+ this.setState({ selection: prevColor });
+ this.props.onUndo(prevColor);
+ return;
+ }
+
+ if (selection === 'export') {
+ const svgData = this.convertToSVG();
+ this.props.onImageExport(svgData);
+ return;
+ }
+
+ this.setState({ selection: selection });
+ };
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.actionQueueChange !== this.props.actionQueueChange) {
+ this.handleSelectionChange();
+ }
+ }
+
+ drawCanvas() {
+ this.img.onload = () => {
+ // this onload may or may not be causing problems.
+ this.ctx.drawImage(
+ this.img,
+ 0,
+ 0,
+ this.canvasRef.current?.width,
+ this.canvasRef.current?.height
+ );
+ };
+ }
+
+ convertToSVG() {
+ const lines = this.lineStack.flat();
+ const combinedArray = lines.flatMap(
+ ([lastX, lastY, x, y, colorSelection]) => [
+ lastX,
+ lastY,
+ x,
+ y,
+ colorSelection,
+ ]
+ );
+ return combinedArray;
+ }
+
+ getComplexity() {
+ let count = 0;
+ this.lineStack.forEach((item) => {
+ count += item.length;
+ });
+ return count;
+ }
+
+ displayCanvas() {
+ return (
+