Skip to content

Instantly share code, notes, and snippets.

@BenjaminUrquhart
Last active August 7, 2025 16:28
Show Gist options
  • Select an option

  • Save BenjaminUrquhart/0c1039c89061d536c1f227c78c5bf7d4 to your computer and use it in GitHub Desktop.

Select an option

Save BenjaminUrquhart/0c1039c89061d536c1f227c78c5bf7d4 to your computer and use it in GitHub Desktop.
Devlog TV - a ZERO Sievert mod that adds a video feed of the most recent devlog to the bunker
escape = InstanceContext()
funcs = escape.funcs
obj_news = funcs.asset_get_index("obj_news")
obj_example = funcs.asset_get_index("obj_example")
obj_sprite_downloader = funcs.asset_get_index("obj_sprite_downloader")
startDownload = escape.getScript("spr_sprite_downloader_fetch")
downloadFile = func(url, progressCallback, finishCallback) {
if !funcs.os_is_network_connected() {
escape.tryExecute(finishCallback, undefined, true)
return undefined;
}
if !InstanceExists(obj_sprite_downloader) {
InstanceCreate(0, 0, obj_sprite_downloader)
}
let download = undefined
startDownload(url)
with obj_sprite_downloader {
let len = ArrayLength(self.data)
download = self.data[len - 1]
}
if !download.url_worked {
ShowDebugMessage("Failed to start download for " + String(url))
escape.tryExecute(finishCallback, undefined, true)
return undefined
}
else {
ShowDebugMessage("Started download for " + String(url))
}
download.progressCallback = progressCallback
download.finishCallback = finishCallback
ShowDebugMessage(download)
return download
}
masterNews = undefined
if InstanceExists(obj_news) {
masterNews = funcs.instance_find(obj_news, 0)
}
else {
masterNews = InstanceCreate(0, 0, obj_news)
}
getRecentNews = funcs.method(masterNews, masterNews.news_get_recent)
getRemoteJSON = func(url, callback) {
if !funcs.os_is_network_connected() {
return false;
}
ShowDebugMessage("Fetching JSON at " + String(url))
let obj = InstanceCreate(0, 0, obj_example)
with obj {
-- "not an instance" my beloved
funcs.method(self.id, funcs.instance_change)(obj_news, false)
self.request_url = url;
self.http_news_request = funcs.http_get(url)
self.news_get_recent = getRecentNews
self.news_json = undefined
self.callback = callback
self.start_time = funcs.current_time_get()
}
return true;
}
escape.getRemoteJSON = getRemoteJSON
escape.downloadFile = downloadFile
ObjectCreate("downloader")
ObjectSetScript("downloader", "create_event", func(obj) {
obj.persistent = true
})
ObjectSetScript("downloader", "step_normal_event", func(obj) {
with obj_news {
if self.callback != undefined and self.news_json != undefined {
escape.tryExecute(self.callback, self.news_json)
NpcObjectDestroy(self.id)
}
else if self.start_time != undefined and self.start_time + 10000 < funcs.current_time_get() {
ShowDebugMessage("Timed out fetching " + String(self.request_url) + " (failed to connect or invalid JSON received)")
escape.tryExecute(self.callback, undefined)
NpcObjectDestroy(self.id)
}
}
with obj_sprite_downloader {
let index = ArrayLength(self.data)
while index > 0 {
let download = self.data[index - 1]
if download.downloaded {
if download.finishCallback != undefined {
let ret = escape.tryExecute(download.finishCallback, download, false)
if ret.status == 1 and !ret.result and funcs.file_exists(download.full_path) {
funcs.file_delete(download.full_path)
}
else if ret.status == 2 {
ShowDebugMessage("Error in callback for downloaded file " + download.from_url + ":\n" + String(ret.result))
}
download.finishCallback = undefined
}
}
else if download.error {
escape.tryExecute(download.finishCallback, download, true)
download.finishCallback = undefined;
}
else if download.progressCallback != undefined {
escape.tryExecute(download.progressCallback, download)
}
index -= 1
}
}
})
-- objects outside of room bounds aren't suspended when the game is paused
escape.downloader = ObjectSpawn("downloader", -1000, -1000)
-- we do a bit of cheating
ObjectCreate("dud")
ObjectSetScript("dud", "create_event")
StructSet(-5, "__Mods_Context_Current", { "scripts" : -5, "mod_name" : "Placeholder"})
StructSet(-5, "is_vec3", 0)
StructSet(-5, "is_vec4", 0)
funcs = ScriptGet("__katspeak_get_gml_interface")()
ShowDebugMessage("Got interface")
let objects = funcs.variable_global_get("Mods_Objects")
mod = objects.dud.create_event.context
ShowDebugMessage("Mod name is " + mod.mod_name)
StructRemove(objects, "dud")
StructSet(-5, "__Mods_Context_Current", mod)
with obj_mod_generic {
if self.object_id == "downloader" or self.object_id == "devlog_manager" {
NpcObjectDestroy(self.id)
}
}
-- setup helpers
getScript = func(name) {
let index = funcs.asset_get_index(name)
return funcs.method(-5, index)
}
formatException = func(err) {
if funcs.is_struct(err) and StructExists(err, "message") and StructExists(err, "stacktrace") {
let out = String(err.message)
let len = ArrayLength(err.stacktrace)
let index = 0
while index < len {
out += "\n- at " + String(err.stacktrace[index])
index += 1
}
return out
}
return String(err)
}
tryExecuteFunc = getScript("catspeak_compile_string")
tryExecuteFuncTriple = getScript("catspeak_compile_buffer")
let tryExecute = func(f, arg0, arg1, arg2) {
let catspeak = funcs.variable_global_get("Catspeak")
funcs.variable_global_set("Catspeak", {
"compileGML" : func(val) { return val; },
"parseString" : f,
"parse": f
})
let result = undefined
if arg1 != undefined or arg2 != undefined {
result = tryExecuteFuncTriple(arg0, false, arg1, arg2)
}
else {
result = tryExecuteFunc(arg0)
}
if result.state == 1 {
ShowDebugMessage("Call succeeded with return value " + String(result.result))
}
else {
ShowDebugMessage("Call threw exception:\n" + formatException(result.result))
}
funcs.variable_global_set("Catspeak", catspeak)
return result
}
getFileSize = func(path) {
if funcs.file_exists(path) {
let file = funcs.file_bin_open(path, 0)
let size = funcs.file_bin_size(file)
funcs.file_bin_close(file)
return size
}
return -1
}
-- disable main menu animated background (we need the singular video slot gamemaker provides)
obj_main_menu = funcs.asset_get_index("obj_main_menu")
let menu_ui_replace = tryExecute(func() {
let replacement = mod.file_path + "/mm_root_novideo.ui"
let __uiGlobal = getScript("__uiGlobal")
let __uiClassFile = getScript("__uiClassFile")
let ui = __uiGlobal()
let existing = ui.__fileDict["ZS_vanilla/ui/mm_root.ui"]
if existing != undefined and existing.__path == replacement {
ShowDebugMessage("UI override already exists: " + String(existing))
}
else {
ShowDebugMessage(replacement)
ui.__fileDict["ZS_vanilla/ui/mm_root.ui"] = new __uiClassFile(replacement)
if InstanceExists(obj_main_menu) {
NpcObjectDestroy(obj_main_menu)
InstanceCreate(0, 0, obj_main_menu)
with obj_main_menu {
self.stop_video = false
}
}
}
})
disableMenuVideoClose = menu_ui_replace.state == 1
if !disableMenuVideoClose {
-- We gamble if it fails
ShowMessage("An exception occurred when replacing main menu UI element")
}
escape = {
"funcs" : funcs,
"getScript" : getScript,
"tryExecute" : tryExecute,
"mod" : mod
}
StructSet(-5, "__Mods_Variable_Self", escape)
ScriptExecute("downloader_init.script")
-- start setting up for video playback
working_directory = funcs.working_directory_get()
readTextFile = getScript("file_get_text")
writeTextFile = getScript("SnapStringToFile")
progressLogger = func(download) {
ShowDebugMessage(String(download.from_url) + ": " + String(download.download_file_size_current) + "/" + String(download.download_file_size_total))
}
ready = false
rootURL = "http://zsdevlog.benjaminurquhart.com"
manifestLocation = working_directory + "/downloaded/devlog-manifest.json"
devlogLocation = working_directory + "/downloaded/devlog.mp4"
nextVideoHash = undefined
currentVideoHash = undefined
nextVideoID = undefined
currentVideoID = undefined
if funcs.file_exists(manifestLocation) and funcs.file_exists(devlogLocation) {
tryExecute(func() {
let text = readTextFile(manifestLocation)
let json = funcs.json_parse(text)
let info = json.videos[json.latest]
let hash = funcs.sha1_file(devlogLocation)
if hash == info.hash {
currentVideoID = json.latest
currentVideoHash = hash
}
else {
ShowDebugMessage("Hash mismatch (expected " + info.hash + ", got " + hash + ")")
ShowDebugMessage("Video will be redownloaded")
funcs.file_delete(devlogLocation)
}
})
}
manifestRetryTime = 5
loadManifest = func() {
if !escape.getRemoteJSON(rootURL + "/manifest.json", func(json) {
if json == undefined {
ShowDebugMessage("Failed to fetch manifest, retrying in " + String(manifestRetryTime) + " seconds.")
funcs.call_later(manifestRetryTime, 0, loadManifest, false)
manifestRetryTime = Floor(manifestRetryTime * 2)
if manifestRetryTime >= 60 {
manifestRetryTime = 60
}
return
}
let next_hash = json.videos[json.latest].hash
if json.latest != currentVideoID or currentVideoHash != next_hash {
nextVideoID = json.latest
nextVideoHash = next_hash
currentVideoID = undefined
}
writeTextFile(funcs.json_stringify(json), manifestLocation)
}) {
ShowDebugMessage("Cannot connect to the internet, will use cached data (if present).")
return false
}
return true
}
let halt = false;
let submitted = loadManifest()
if !submitted and !funcs.file_exists(manifestLocation) {
if funcs.file_exists(devlogLocation) {
ShowDebugMessage("Missing manifests but we have the video for some reason")
ready = true
}
else {
ShowDebugMessage("Failed to find or retrieve assets.")
ShowMessage("Devlog TV failed to download or locate video data. Please ensure your internet connection is working.")
halt = true
}
}
if !halt {
listen_distance = 300
video_scale = 0.25
video_x = 480
video_y = 825
video_w = 256
video_h = 144
emitter_x = video_x + video_scale * video_w / 2
emitter_y = video_y + video_scale * video_h / 2
spr_loading = SpriteGet(SpriteLoad("DevlogLoadIcon", "spr_load_icon.png"))
font_main_menu = funcs.asset_get_index("font_main_menu")
obj_light_controller = funcs.asset_get_index("obj_light_controller")
obj_cursor = funcs.asset_get_index("obj_cursor")
obj_light = funcs.asset_get_index("obj_light")
r_hub = funcs.asset_get_index("r_hub")
ObjectCreate("devlog_manager")
ObjectSetScript("devlog_manager", "create_event", func(obj) {
obj.video_status = funcs.video_get_status()
obj.persistent = true
obj.playing = false
obj.paused = false
obj.in_hub = false
obj.depth = -4700
obj.delay = 5
})
ObjectSetScript("devlog_manager", "step_normal_event", func(obj) {
let status = funcs.video_get_status()
if status != obj.video_status {
ShowDebugMessage("Video status updated: " + String(obj.video_status) + " -> " + String(status))
obj.video_status = status
}
obj.in_hub = !InstanceExists(obj_main_menu) and funcs.room_get() == r_hub
if ready {
if !obj.playing and obj.in_hub {
if obj.delay < 1 {
if status > 0 {
obj.playing = status == 2
obj.paused = status == 3
}
if obj.paused {
ShowDebugMessage("Resuming playback")
funcs.video_resume()
}
else if !obj.playing {
ShowDebugMessage("Starting playback")
funcs.video_open(devlogLocation)
funcs.video_enable_loop(true)
}
obj.delay = 5
}
else {
obj.delay -= 1
}
}
else if !obj.in_hub and obj.playing {
ShowDebugMessage("Pausing video")
funcs.video_pause()
obj.playing = false
obj.paused = true
}
if obj.in_hub {
with funcs.collision_rectangle(video_x, video_y, video_x + video_scale * video_w, video_y + video_scale * video_h, obj_light, false, false) {
NpcObjectDestroy(self.id)
}
if obj.playing and status {
let player = funcs.instance_find(obj_player, 0)
funcs.video_set_volume(1 - Clamp(PointDistance(emitter_x, emitter_y, player.x, player.y) / listen_distance, 0, 1))
}
}
}
if nextVideoID != undefined and currentVideoID != nextVideoID {
currentVideoID = nextVideoID
if ready or obj.playing or obj.paused {
funcs.video_close()
obj.playing = false
obj.paused = false
ready = false
}
funcs.file_delete(devlogLocation)
escape.downloadFile(rootURL + "/" + nextVideoID + ".mp4", progressLogger, func(download, err) {
if funcs.file_exists(download.full_path) {
let hash = funcs.sha1_file(download.full_path)
if hash != nextVideoHash {
ShowDebugMessage("Hash mismatch (expected " + nextVideoHash + ", got " + hash + ")")
err = true
}
}
if err {
ShowDebugMessage("Video download failed. Retrying in 10 seconds...")
funcs.call_later(10, 0, func() { currentVideoID = undefined }, false)
}
else {
funcs.file_rename(download.full_path, devlogLocation)
ready = true
}
return false
})
}
else if funcs.file_exists(devlogLocation) {
ready = true
}
})
ObjectSetScript("devlog_manager", "step_end_event", func(obj) {
with obj_light_controller {
obj.depth = self.depth - 1
}
with obj_cursor {
if self.depth >= obj.depth {
self.depth = obj.depth - 1
}
}
})
ObjectSetScript("devlog_manager", "draw_normal_event", func(obj) {
if obj.in_hub {
funcs.draw_set_alpha(1)
funcs.draw_set_color(0)
funcs.draw_set_font(font_main_menu)
funcs.draw_rectangle(video_x, video_y, video_x + video_w * video_scale - 1, video_y + video_h * video_scale - 1, false)
funcs.draw_set_color(0xffffff)
if !ready {
DrawSprite(spr_loading, 0, video_x, video_y)
}
}
if obj.playing {
let video = funcs.video_draw()
--ShowDebugMessage(video)
if video[0] == 0 {
let ww = funcs.surface_get_width(video[1])
var hh = funcs.surface_get_height(video[1])
funcs.draw_surface_ext(video[1], video_x, video_y, video_scale * (video_w / ww), video_scale * (video_h / hh), 0, 0xffffff, 1)
}
}
})
ObjectSetScript("devlog_manager", "cleanup_event", func(obj) {
ShowDebugMessage("Clearing video")
funcs.video_close()
})
tv = ObjectSpawn("devlog_manager", -1000, -1000)
-- setup persistence
obj_gamepad = funcs.asset_get_index("obj_gamepad")
if !InstanceExists(obj_gamepad) {
InstanceCreate(0, 0, obj_gamepad)
}
with obj_gamepad {
let system = {
"value" : funcs.array_create(31, 0),
"update" : func() {
if disableMenuVideoClose {
with obj_main_menu {
if self.stop_video {
self.stop_video = false
}
}
}
-- TODO: handle race condition of the room changing before we get a chance to reactivate
if !InstanceExists(tv) {
ShowDebugMessage("Reactivating devlog object")
funcs.instance_activate_object(tv)
}
if !InstanceExists(escape.downloader) {
ShowDebugMessage("Reactivating downloader object")
funcs.instance_activate_object(escape.downloader)
}
let player = GetInstanceOfObject("obj_player")
if player != -1 {
escape.downloader.x = player.x
escape.downloader.y = player.y
tv.x = player.x
tv.y = player.y
}
}
}
ArrayPush(self.array_of_input_systems, system)
}
}
funcs.call_later(1, 0, func() {
let data = funcs.variable_global_get("Mods_Data")
mod.failed_to_compile = false
mod.compile_error = {}
data[mod.mod_name] = mod
}, true)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment