Last active
August 7, 2025 16:28
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- 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