diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm
index 0885a4ed180..81db855b15d 100644
--- a/code/__HELPERS/game.dm
+++ b/code/__HELPERS/game.dm
@@ -9,6 +9,12 @@
src:Topic(href, href_list)
return null
+/proc/get_area_master(O)
+ var/area/A = get_area(O)
+ if(A && A.master)
+ A = A.master
+ return A
+
/proc/get_area(O)
var/atom/location = O
var/i
@@ -176,10 +182,10 @@
for(var/mob/M in range)
hear += M
-
+
var/list/objects = list()
- for(var/obj/O in range) //Get a list of objects in hearing range. We'll check to see if any clients have their "eye" set to the object
+ for(var/obj/O in range) //Get a list of objects in hearing range. We'll check to see if any clients have their "eye" set to the object
objects += O
for(var/client/C in clients)
@@ -196,7 +202,7 @@
if(C.eye in hear_and_objects)
hear += C.mob
-
+
else if(C.mob.loc in hear_and_objects)
hear += C.mob
else if(C.mob.loc.loc in hear_and_objects)
@@ -376,3 +382,68 @@ proc/get_candidates(be_special_flag=0)
sleep(duration)
for(var/client/C in show_to)
C.images -= I
+
+/proc/get_active_player_count()
+ // Get active players who are playing in the round
+ var/active_players = 0
+ for(var/i = 1; i <= player_list.len; i++)
+ var/mob/M = player_list[i]
+ if(M && M.client)
+ if(istype(M, /mob/new_player)) // exclude people in the lobby
+ continue
+ else if(isobserver(M)) // Ghosts are fine if they were playing once (didn't start as observers)
+ var/mob/dead/observer/O = M
+ if(O.started_as_observer) // Exclude people who started as observers
+ continue
+ active_players++
+ return active_players
+
+/datum/projectile_data
+ var/src_x
+ var/src_y
+ var/time
+ var/distance
+ var/power_x
+ var/power_y
+ var/dest_x
+ var/dest_y
+
+/datum/projectile_data/New(var/src_x, var/src_y, var/time, var/distance, \
+ var/power_x, var/power_y, var/dest_x, var/dest_y)
+ src.src_x = src_x
+ src.src_y = src_y
+ src.time = time
+ src.distance = distance
+ src.power_x = power_x
+ src.power_y = power_y
+ src.dest_x = dest_x
+ src.dest_y = dest_y
+
+/proc/projectile_trajectory(var/src_x, var/src_y, var/rotation, var/angle, var/power)
+
+ // returns the destination (Vx,y) that a projectile shot at [src_x], [src_y], with an angle of [angle],
+ // rotated at [rotation] and with the power of [power]
+ // Thanks to VistaPOWA for this function
+
+ var/power_x = power * cos(angle)
+ var/power_y = power * sin(angle)
+ var/time = 2* power_y / 10 //10 = g
+
+ var/distance = time * power_x
+
+ var/dest_x = src_x + distance*sin(rotation);
+ var/dest_y = src_y + distance*cos(rotation);
+
+ return new /datum/projectile_data(src_x, src_y, time, distance, power_x, power_y, dest_x, dest_y)
+
+
+/proc/mobs_in_area(var/area/the_area, var/client_needed=0, var/moblist=mob_list)
+ var/list/mobs_found[0]
+ var/area/our_area = get_area_master(the_area)
+ for(var/mob/M in moblist)
+ if(client_needed && !M.client)
+ continue
+ if(our_area != get_area_master(M))
+ continue
+ mobs_found += M
+ return mobs_found
diff --git a/code/_hooks/hooks.dm b/code/_hooks/hooks.dm
index 30f5d892f39..51aea802165 100644
--- a/code/_hooks/hooks.dm
+++ b/code/_hooks/hooks.dm
@@ -32,14 +32,14 @@ var/global/list/hooks = list()
for (var/hook_path in typesof(/hook))
var/hook/hook = new hook_path
hooks[hook.name] = hook
- //world.log << "Found hook: " + hook.name
+ world.log << "Found hook: " + hook.name
for (var/hook_path in typesof(/hook_handler))
var/hook_handler/hook_handler = new hook_path
for (var/name in hooks)
if (hascall(hook_handler, "On" + name))
var/hook/hook = hooks[name]
hook.handlers += hook_handler
- //world.log << "Found hook handler for: " + name
+ world.log << "Found hook handler for: " + name
for (var/hook/hook in hooks)
hook.Setup()
diff --git a/code/game/machinery/alarm.dm b/code/game/machinery/alarm.dm
index 1e42420be6d..271e2ff4a02 100644
--- a/code/game/machinery/alarm.dm
+++ b/code/game/machinery/alarm.dm
@@ -1043,6 +1043,13 @@ FIRE ALARM
var/last_process = 0
var/wiresexposed = 0
var/buildstage = 2 // 2 = complete, 1 = no wires, 0 = circuit gone
+ var/area/master_area
+
+/obj/machinery/firealarm/New()
+ var/area/A = get_area_master(src)
+ if (!( istype(A, /area) ))
+ return
+ master_area=A
/obj/machinery/firealarm/update_icon()
@@ -1368,6 +1375,14 @@ Code shamelessly copied from apc_frame
idle_power_usage = 2
active_power_usage = 6
+ var/area/master_area
+
+/obj/machinery/partyalarm/New()
+ var/area/A = get_area_master(src)
+ if (!( istype(A, /area) ))
+ return
+ master_area=A
+
/obj/machinery/partyalarm/attack_paw(mob/user as mob)
return attack_hand(user)
diff --git a/code/game/machinery/atmoalter/area_atmos_computer.dm b/code/game/machinery/atmoalter/area_atmos_computer.dm
index fff75b24e3d..29834f1c943 100644
--- a/code/game/machinery/atmoalter/area_atmos_computer.dm
+++ b/code/game/machinery/atmoalter/area_atmos_computer.dm
@@ -164,9 +164,7 @@
var/turf/T = get_turf(src)
if(!T.loc) return
- var/area/A = T.loc
- if (A.master)
- A = A.master
+ var/area/A = get_area_master(T)
for(var/obj/machinery/portable_atmospherics/scrubber/huge/scrubber in world )
var/turf/T2 = get_turf(scrubber)
if(T2 && T2.loc)
diff --git a/code/game/machinery/doors/firedoor.dm b/code/game/machinery/doors/firedoor.dm
index 8e432e86289..9391bbb9d46 100644
--- a/code/game/machinery/doors/firedoor.dm
+++ b/code/game/machinery/doors/firedoor.dm
@@ -136,7 +136,7 @@
user << "\red \The [src] is welded solid!"
return
- var/area/A = get_area(src)
+ var/area/A = get_area_master(src)
ASSERT(istype(A))
if(A.master)
A = A.master
@@ -273,4 +273,4 @@
/obj/machinery/door/firedoor/multi_tile/triple
icon = 'icons/obj/doors/DoorHazard3x1.dmi'
- width = 3
\ No newline at end of file
+ width = 3
diff --git a/code/game/objects/items/blueprints.dm b/code/game/objects/items/blueprints.dm
index 92ed4e92447..9992c18340c 100644
--- a/code/game/objects/items/blueprints.dm
+++ b/code/game/objects/items/blueprints.dm
@@ -75,8 +75,7 @@ move an amendment to the drawing.
/obj/item/blueprints/proc/get_area()
var/turf/T = get_turf_loc(usr)
- var/area/A = T.loc
- A = A.master
+ var/area/A = get_area_master(T)
return A
/obj/item/blueprints/proc/get_area_type(var/area/A = get_area())
diff --git a/code/modules/client/preferences_toggles.dm b/code/modules/client/preferences_toggles.dm
index fc52d59c6ee..91babc9f7c3 100644
--- a/code/modules/client/preferences_toggles.dm
+++ b/code/modules/client/preferences_toggles.dm
@@ -183,11 +183,21 @@
I.color = UI_style_color_new
I.alpha = UI_style_alpha_new
-
-
if(alert("Like it? Save changes?",,"Yes", "No") == "Yes")
prefs.UI_style = UI_style_new
prefs.UI_style_alpha = UI_style_alpha_new
prefs.UI_style_color = UI_style_color_new
prefs.save_preferences()
usr << "UI was saved"
+
+/client/verb/toggle_media()
+ set name = "Hear/Silence Streaming"
+ set category = "Preferences"
+ set desc = "Toggle hearing streaming media (radios, jukeboxes, etc)"
+
+ prefs.toggles ^= SOUND_STREAMING
+ prefs.save_preferences()
+ usr << "You will [(prefs.toggles & SOUND_STREAMING) ? "now" : "no longer"] hear streamed media."
+ // Restart.
+ media.stop_music()
+ media.update_music()
diff --git a/code/modules/media/jukebox.dm b/code/modules/media/jukebox.dm
index 22c07e9b6f0..67d6eebfeba 100644
--- a/code/modules/media/jukebox.dm
+++ b/code/modules/media/jukebox.dm
@@ -9,6 +9,8 @@
#define JUKEMODE_PLAY_ONCE 3 // Play, then stop.
#define JUKEMODE_COUNT 3
+#define JUKEBOX_RELOAD_COOLDOWN 600 // 60s
+
// Represents a record returned.
/datum/song_info
var/title = ""
@@ -43,30 +45,40 @@ var/global/loopModeNames=list(
anchored = 1
luminosity = 4 // Why was this 16
+ playing=0
+
var/loop_mode = JUKEMODE_SHUFFLE
// /datum/song_info
var/list/playlist
- var/current_song=0
+ var/current_song = 0
+ var/autoplay = 0
+ var/last_reload = 0
/obj/machinery/media/jukebox/attack_ai()
return
/obj/machinery/media/jukebox/attack_paw()
return
-
+/obj/machinery/media/jukebox/proc/check_reload()
+ return world.time > last_reload + JUKEBOX_RELOAD_COOLDOWN
/obj/machinery/media/jukebox/attack_hand(var/mob/user)
var/t = "Jukebox Interface
"
t += "Power: [playing?"On":"Off"]
"
t += "Play Mode: [loopModeNames[loop_mode]]
"
- if(current_song)
- var/datum/song_info/song=playlist[current_song]
- t += "Current song: [song.artist] - [song.title]
"
- t += "| Artist - Title | Album |
"
- var/i
- for(i = 1,i <= playlist.len,i++)
- var/datum/song_info/song=playlist[i]
- t += "| #[i] | [song.artist] - [song.title] | [song.album] |
"
- t += "
"
+ if(playlist == null)
+ t += "\[DOWNLOADING PLAYLIST, PLEASE WAIT\]"
+ else
+ if(current_song)
+ var/datum/song_info/song=playlist[current_song]
+ t += "Current song: [song.artist] - [song.title]
"
+ if(check_reload())
+ t += "\[Reload Playlist\]"
+ t += "| Artist - Title | Album |
"
+ var/i
+ for(i = 1,i <= playlist.len,i++)
+ var/datum/song_info/song=playlist[i]
+ t += "| #[i] | [song.artist] - [song.title] | [song.album] |
"
+ t += "
"
user.set_machine(src)
var/datum/browser/popup = new (user,"jukebox",name,420,700)
popup.set_content(t)
@@ -78,6 +90,16 @@ var/global/loopModeNames=list(
..()
if (href_list["power"])
playing=!playing
+ update_music()
+
+ if (href_list["reload"])
+ if(!check_reload())
+ usr << "\red You must wait 60 seconds between reloads."
+ return
+ last_reload=world.time
+ playlist=null
+ current_song=0
+ update_music()
if (href_list["song"])
current_song=Clamp(text2num(href_list["song"]),1,playlist.len)
@@ -91,6 +113,7 @@ var/global/loopModeNames=list(
/obj/machinery/media/jukebox/process()
if(!playlist)
var/url="[config.media_base_url]/index.php"
+ testing("[src] - Updating playlist from [url]...")
var/response = world.Export(url)
playlist=list()
if(response)
@@ -101,7 +124,9 @@ var/global/loopModeNames=list(
var/songdata = reader.read_value()
for(var/list/record in songdata)
playlist += new /datum/song_info(record)
+ visible_message("\icon[src] \The [src] beeps, and the menu on its front fills with [playlist.len] items.","You hear a beep.")
else
+ //testing("Failed to update playlist: Response null.")
stat &= BROKEN
update_icon()
return
@@ -119,13 +144,18 @@ var/global/loopModeNames=list(
if(JUKEMODE_PLAY_ONCE)
playing=0
return
- if(!playing) return
+ if(!playing)
+ return
update_music()
/obj/machinery/media/jukebox/update_music()
- var/datum/song_info/song = playlist[current_song]
- media_url = song.url
- media_start_time = world.time
- visible_message("\icon[src] \The [src] begins to play \"[song.title]\", by [song.artist].","You hear music.")
- visible_message("\icon[src] \The [src] warbles: [song.length]ds, [song.url]")
+ if(current_song)
+ var/datum/song_info/song = playlist[current_song]
+ media_url = song.url
+ media_start_time = world.time
+ visible_message("\icon[src] \The [src] begins to play \"[song.title]\", by [song.artist].","You hear music.")
+ //visible_message("\icon[src] \The [src] warbles: [song.length/10]s @ [song.url]")
+ else
+ media_url=""
+ media_start_time = 0
..()
\ No newline at end of file
diff --git a/code/modules/media/machinery.dm b/code/modules/media/machinery.dm
index 6778bfd988b..902c91b0330 100644
--- a/code/modules/media/machinery.dm
+++ b/code/modules/media/machinery.dm
@@ -4,12 +4,63 @@
var/media_url=""
var/media_start_time=0
+ var/area/master_area
+
// Notify everyone in the area of new music.
// YOU MUST SET MEDIA_URL AND MEDIA_START_TIME YOURSELF!
/obj/machinery/media/proc/update_music()
- var/area/A = get_area(src)
- if(A.master)
- A=A.master
- for(var/mob/M in A)
+ update_media_source()
+
+ // Bail if we lost connection to master.
+ if(!master_area)
+ return
+
+ // Send update to clients.
+ for(var/mob/M in mobs_in_area(master_area))
if(M && M.client)
- M.update_music()
\ No newline at end of file
+ M.update_music()
+
+/obj/machinery/media/proc/update_media_source()
+ var/area/A = get_area_master(src)
+
+ // Check if there's a media source already.
+ if(A.media_source && A.media_source!=src)
+ master_area=null
+ return
+
+ // Update Media Source.
+ if(!A.media_source)
+ A.media_source=src
+
+ master_area=A
+
+/obj/machinery/media/proc/disconnect_media_source()
+ var/area/A = get_area_master(src)
+
+ // Sanity
+ if(!A)
+ master_area=null
+ return
+
+ // Check if there's a media source already.
+ if(A && A.media_source && A.media_source!=src)
+ master_area=null
+ return
+
+ // Update Media Source.
+ A.media_source=null
+
+ // Clients
+ for(var/mob/M in mobs_in_area(A))
+ if(M && M.client)
+ M.update_music()
+
+ master_area=null
+
+/obj/machinery/media/New()
+ ..()
+ update_media_source()
+
+/obj/machinery/media/Destroy()
+ disconnect_media_source()
+ ..()
\ No newline at end of file
diff --git a/code/modules/media/mediamanager.dm b/code/modules/media/mediamanager.dm
index f895bcce5e4..ecb2771ef43 100644
--- a/code/modules/media/mediamanager.dm
+++ b/code/modules/media/mediamanager.dm
@@ -25,20 +25,24 @@ function SetMusic(url, time, volume) {
/hook_handler/soundmanager
// Set up player on login
proc/OnLogin(var/list/args)
+ //testing("Received OnLogin.")
var/client/C = args["client"]
- C.media = new /datum/media_manager(C)
+ C.media = new /datum/media_manager(args["mob"])
C.media.open()
+ C.media.update_music()
// Update when moving between areas.
proc/OnMobAreaChange(var/list/args)
+ //testing("Received OnMobAreaChange.")
var/mob/M = args["mob"]
M.update_music()
/mob/proc/update_music()
- if (client)
- if(client.media)
- client.media.update_music()
+ if (client && client.media)
+ client.media.update_music()
+ //else
+ // testing("[src] - client: [client?"Y":"N"]; client.media: [client && client.media ? "Y":"N"]")
/area
// One media source per area.
@@ -50,18 +54,31 @@ function SetMusic(url, time, volume) {
var/volume = 100
var/client/owner
+ var/mob/mob
+
+ var/const/window = "rpane.hosttracker"
+ //var/const/window = "mediaplayer" // For debugging.
New(var/mob/holder)
- src.owner=holder
+ src.mob=holder
+ owner=src.mob.client
// Actually pop open the player in the background.
proc/open()
- owner << browse(PLAYER_HTML, "window=rpane.hosttracker")
+ owner << browse(PLAYER_HTML, "window=[window]")
send_update()
// Tell the player to play something via JS.
proc/send_update()
- owner << output(list2params(list(url, (world.timeofday - start_time) / 10, volume)), "rpane.hosttracker:SetMusic")
+ if(!(owner.prefs.toggles & SOUND_STREAMING))
+ return // Nope.
+ //testing("Sending update to WMP...")
+ owner << output(list2params(list(url, (world.time - start_time) / 10, volume)), "[window]:SetMusic")
+
+ proc/stop_music()
+ url=""
+ start_time=world.time
+ send_update()
// Scan for media sources and use them.
proc/update_music()
@@ -69,16 +86,25 @@ function SetMusic(url, time, volume) {
var/targetStartTime = 0
var/targetVolume = 100
- if (owner)
- var/area/A = get_area()
- var/obj/machinery/media/M = A.media_source
- if(M.playing)
- targetURL = M.media_url
- targetStartTime = M.media_start_time
+ if (!owner)
+ //testing("owner is null")
+ return
+
+ var/area/A = get_area_master(mob)
+ if(!A)
+ //testing("[owner] in [mob.loc]. Aborting.")
+ stop_music()
+ return
+ var/obj/machinery/media/M = A.media_source
+ if(M && M.playing)
+ targetURL = M.media_url
+ targetStartTime = M.media_start_time
+ //owner << "Found audio source: [M.media_url] @ [(world.time - start_time) / 10]s."
+ //else
+ // testing("M is not playing or null.")
if (url != targetURL || abs(targetStartTime - start_time) > 1 || targetVolume != volume)
url = targetURL
start_time = targetStartTime
volume = targetVolume
-
send_update()
\ No newline at end of file
diff --git a/code/modules/power/lighting.dm b/code/modules/power/lighting.dm
index 7ff2be8c2a2..1e7737bc406 100644
--- a/code/modules/power/lighting.dm
+++ b/code/modules/power/lighting.dm
@@ -611,8 +611,7 @@
// called when area power state changes
/obj/machinery/light/power_change()
spawn(10)
- var/area/A = src.loc.loc
- A = A.master
+ var/area/A = get_area_master(src)
seton(A.lightswitch && A.power_light)
// called when on fire
diff --git a/code/setup.dm b/code/setup.dm
index 44384aba264..efed8a48d8c 100644
--- a/code/setup.dm
+++ b/code/setup.dm
@@ -679,8 +679,9 @@ var/list/TAGGERLOCATIONS = list("Disposals",
#define SOUND_AMBIENCE 4
#define SOUND_LOBBY 8
#define SOUND_VOICES 16
+#define SOUND_STREAMING 32
-#define SOUND_DEFAULT (SOUND_ADMINHELP|SOUND_MIDI|SOUND_AMBIENCE|SOUND_LOBBY)
+#define SOUND_DEFAULT (SOUND_ADMINHELP|SOUND_MIDI|SOUND_AMBIENCE|SOUND_LOBBY|SOUND_STREAMING)
#define TOGGLES_DEFAULT (CHAT_OOC|CHAT_DEAD|CHAT_GHOSTEARS|CHAT_GHOSTSIGHT|CHAT_PRAYER|CHAT_RADIO|CHAT_ATTACKLOGS|CHAT_LOOC)
#define BE_TRAITOR 1
diff --git a/code/world.dm b/code/world.dm
index bbc6a7cbddf..818c6583e10 100644
--- a/code/world.dm
+++ b/code/world.dm
@@ -40,6 +40,7 @@
appearance_loadbanfile()
jobban_updatelegacybans()
LoadBans()
+ SetupHooks() // /vg/
if(config && config.server_name != null && config.server_suffix && world.port > 0)
// dumb and hardcoded but I don't care~
diff --git a/interface/skin.dmf b/interface/skin.dmf
index 9cefbdf1d78..99e6ece48d4 100644
--- a/interface/skin.dmf
+++ b/interface/skin.dmf
@@ -1541,6 +1541,32 @@ window "rpane"
splitter = 50
show-splitter = true
lock = none
+ elem "hosttracker"
+ type = BROWSER
+ pos = 392,25
+ size = 1x1
+ anchor1 = none
+ anchor2 = none
+ font-family = ""
+ font-size = 0
+ font-style = ""
+ text-color = #000000
+ background-color = none
+ is-visible = true
+ is-disabled = false
+ is-transparent = false
+ is-default = false
+ border = none
+ drop-zone = false
+ right-click = false
+ saved-params = ""
+ on-size = ""
+ show-history = false
+ show-url = false
+ auto-format = true
+ use-title = false
+ on-show = ""
+ on-hide = ""
elem "rulesb"
type = BUTTON
pos = 278,0