From af447aeab7fc5e584ecc1f3b139139a2e25e3d46 Mon Sep 17 00:00:00 2001 From: ManDude <7569514+ManDude@users.noreply.github.com> Date: Sat, 19 Feb 2022 18:10:10 +0000 Subject: [PATCH] [game] subtitles support (tools + goal + text file). (#1174) * add subtitles support (tools + goal + text file). * add to build system proper * better handling of line speakers * billy test subtitles * adjust timings * better handling of subtitle timing + citadel subs * press square to toggle cutscene subtitles * improve DirectRenderer performance * clang * dont error out if there's no user files * make system supports multiple inputs for subtitles * clang * oh no typo!! * avoid future issues * fix warp gate crash * remove no longer necessary code in DirectRenderer * remove temp prints * delete triplicate code * i found a better way * fix make issues with subtitles * force avx compilation --- CMakeLists.txt | 4 +- common/util/FileUtil.cpp | 2 +- decompiler/config/all-types.gc | 2 +- .../jak1_ntsc_black_label/var_names.jsonc | 2 +- game/assets/appicon.png | Bin 0 -> 4897 bytes .../{appicon.png => assets/appicon_large.png} | Bin game/assets/game_subtitle.txt | 11 + .../assets/subtitle/jak1/game_subtitle_en.txt | 111 ++++++ .../assets/subtitle/jak1/game_subtitle_es.txt | 6 + .../opengl_renderer/DirectRenderer.cpp | 24 +- .../graphics/opengl_renderer/DirectRenderer.h | 1 - game/graphics/pipelines/opengl.cpp | 2 +- goal_src/build/all_files.gc | 1 + goal_src/build/game_dgos.gc | 1 + goal_src/dgos/engine.gd | 1 + goal_src/dgos/game.gd | 1 + goal_src/engine/debug/default-menu.gc | 26 +- goal_src/engine/entity/entity.gc | 5 + goal_src/engine/game/game-info.gc | 2 +- goal_src/engine/load/loader.gc | 5 +- goal_src/engine/pc/pckernel-h.gc | 40 +- goal_src/engine/pc/pckernel.gc | 9 + goal_src/engine/pc/subtitle.gc | 363 ++++++++++++++++++ goal_src/engine/ui/text-h.gc | 2 +- goal_src/engine/ui/text.gc | 5 +- goal_src/game.gp | 19 + goal_src/goal-lib.gc | 9 + goal_src/goos-lib.gs | 2 +- goal_src/kernel/gkernel-h.gc | 24 ++ goal_src/kernel/gstring.gc | 6 - goal_src/levels/common/basebutton.gc | 10 +- goalc/CMakeLists.txt | 1 + goalc/compiler/Compiler.cpp | 10 +- goalc/compiler/Compiler.h | 4 + goalc/compiler/compilation/Atoms.cpp | 1 + .../compiler/compilation/CompilerControl.cpp | 29 +- goalc/data_compiler/DataObjectGenerator.cpp | 3 +- goalc/data_compiler/game_subtitle.cpp | 203 ++++++++++ goalc/data_compiler/game_subtitle.h | 92 +++++ goalc/data_compiler/game_text.cpp | 16 +- goalc/make/MakeSystem.cpp | 67 +++- goalc/make/MakeSystem.h | 3 +- goalc/make/Tool.cpp | 68 ++-- goalc/make/Tool.h | 6 +- goalc/make/Tools.cpp | 95 ++++- goalc/make/Tools.h | 14 +- 46 files changed, 1160 insertions(+), 148 deletions(-) create mode 100644 game/assets/appicon.png rename game/{appicon.png => assets/appicon_large.png} (100%) create mode 100644 game/assets/game_subtitle.txt create mode 100644 game/assets/subtitle/jak1/game_subtitle_en.txt create mode 100644 game/assets/subtitle/jak1/game_subtitle_es.txt create mode 100644 goal_src/engine/pc/subtitle.gc create mode 100644 goalc/data_compiler/game_subtitle.cpp create mode 100644 goalc/data_compiler/game_subtitle.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 37774ea172..58c6ea5701 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,7 +17,7 @@ if(MSVC AND (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")) "-Xclang -fcxx-exceptions \ -Xclang -fexceptions \ -Xclang -std=c++17 \ - -march=native \ + -mavx \ -Wno-c++11-narrowing -W3") # additional c++ flags for release mode for our projects @@ -46,7 +46,7 @@ elseif(UNIX) -Wshadow \ -Wsign-promo \ -fdiagnostics-color=always \ - -march=native") + -mavx") # additional c++ flags for release mode for our projects if(CMAKE_BUILD_TYPE MATCHES "Release") diff --git a/common/util/FileUtil.cpp b/common/util/FileUtil.cpp index ab8b390d73..96c9b93306 100644 --- a/common/util/FileUtil.cpp +++ b/common/util/FileUtil.cpp @@ -292,7 +292,7 @@ void ISONameFromAnimationName(char* dst, const char* src) { // upper case for (i = 0; i < 8; i++) { - if (dst[i] > '`' && dst[i] < '{') { + if (dst[i] >= 'a' && dst[i] <= 'z') { dst[i] -= 0x20; } } diff --git a/decompiler/config/all-types.gc b/decompiler/config/all-types.gc index 06edbfe62c..f76354b41c 100644 --- a/decompiler/config/all-types.gc +++ b/decompiler/config/all-types.gc @@ -5829,7 +5829,7 @@ ;; - Types (deftype game-text (structure) - ((id uint32 :offset-assert 0) + ((id game-text-id :offset-assert 0) (text string :offset-assert 4) ) :pack-me diff --git a/decompiler/config/jak1_ntsc_black_label/var_names.jsonc b/decompiler/config/jak1_ntsc_black_label/var_names.jsonc index f7e4b8e92d..3ddc85c3b7 100644 --- a/decompiler/config/jak1_ntsc_black_label/var_names.jsonc +++ b/decompiler/config/jak1_ntsc_black_label/var_names.jsonc @@ -3052,7 +3052,7 @@ }, "print-game-text": { - "args": ["str", "font-ctxt", "alpha", "offset-thing"] + "args": ["str", "font-ctxt", "opaque", "alpha", "line-height"] }, "display-frame-start": { diff --git a/game/assets/appicon.png b/game/assets/appicon.png new file mode 100644 index 0000000000000000000000000000000000000000..a3a10926d0a909fd5bf8f6540bd15db0805b48f3 GIT binary patch literal 4897 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 m_ogl.vertex_buffer_max_verts) { - lg::warn("Buffer wrapped in {} ({}/{} (+ {}) verts, {} bytes)", m_name, - vertex_offset + m_prim_buffer.vert_count, m_ogl.vertex_buffer_max_verts, - m_prim_buffer.vert_count, m_prim_buffer.vert_count * sizeof(Vertex)); - vertex_offset = 0; - } glBindBuffer(GL_ARRAY_BUFFER, m_ogl.vertex_buffer); - glBufferSubData(GL_ARRAY_BUFFER, vertex_offset * sizeof(Vertex), - m_prim_buffer.vert_count * sizeof(Vertex), m_prim_buffer.vertices.data()); + glBufferData(GL_ARRAY_BUFFER, m_prim_buffer.vert_count * sizeof(Vertex), + m_prim_buffer.vertices.data(), GL_STREAM_DRAW); glActiveTexture(GL_TEXTURE0); int draw_count = 0; @@ -212,11 +205,11 @@ void DirectRenderer::flush_pending(SharedRenderState* render_state, ScopedProfil } if (m_sprite_mode.do_first_draw) { - glDrawArrays(GL_TRIANGLES, vertex_offset, m_prim_buffer.vert_count); + glDrawArrays(GL_TRIANGLES, 0, m_prim_buffer.vert_count); draw_count++; } } else { - glDrawArrays(GL_TRIANGLES, vertex_offset, m_prim_buffer.vert_count); + glDrawArrays(GL_TRIANGLES, 0, m_prim_buffer.vert_count); draw_count++; } @@ -224,7 +217,7 @@ void DirectRenderer::flush_pending(SharedRenderState* render_state, ScopedProfil render_state->shaders[ShaderId::DEBUG_RED].activate(); glDisable(GL_BLEND); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); - glDrawArrays(GL_TRIANGLES, vertex_offset, m_prim_buffer.vert_count); + glDrawArrays(GL_TRIANGLES, 0, m_prim_buffer.vert_count); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); draw_count++; } @@ -235,8 +228,6 @@ void DirectRenderer::flush_pending(SharedRenderState* render_state, ScopedProfil prof.add_draw_call(draw_count); m_stats.triangles += n_tris; m_stats.draw_calls += draw_count; - m_ogl.last_vertex_offset = vertex_offset + m_prim_buffer.vert_count; - m_ogl.last_vertex_offset = (m_ogl.last_vertex_offset + 3) & ~3; m_prim_buffer.vert_count = 0; } @@ -921,7 +912,8 @@ void DirectRenderer::handle_xyzf2_common(u32 x, ASSERT(z < (1 << 24)); (void)f; // TODO: do something with this. if (m_prim_buffer.is_full()) { - // fmt::print("flush due to fill {} {}\n", m_prim_buffer.vert_count, m_prim_buffer.max_verts); + lg::warn("Buffer wrapped in {} ({} verts, {} bytes)", m_name, m_ogl.vertex_buffer_max_verts, + m_prim_buffer.vert_count * sizeof(Vertex)); flush_pending(render_state, prof); } @@ -1074,8 +1066,6 @@ void DirectRenderer::reset_state() { m_prim_building = PrimBuildState(); - m_ogl.last_vertex_offset = 0; - m_stats = {}; } diff --git a/game/graphics/opengl_renderer/DirectRenderer.h b/game/graphics/opengl_renderer/DirectRenderer.h index 18f816b43a..95126963d5 100644 --- a/game/graphics/opengl_renderer/DirectRenderer.h +++ b/game/graphics/opengl_renderer/DirectRenderer.h @@ -239,7 +239,6 @@ class DirectRenderer : public BucketRenderer { GLuint vao; u32 vertex_buffer_bytes = 0; u32 vertex_buffer_max_verts = 0; - u32 last_vertex_offset = 0; } m_ogl; struct { diff --git a/game/graphics/pipelines/opengl.cpp b/game/graphics/pipelines/opengl.cpp index effb122b08..73c3f1cb8f 100644 --- a/game/graphics/pipelines/opengl.cpp +++ b/game/graphics/pipelines/opengl.cpp @@ -157,7 +157,7 @@ static std::shared_ptr gl_make_main_display(int width, return NULL; } - std::string image_path = fmt::format("{}/game/appicon.png", file_util::get_project_path()); + std::string image_path = fmt::format("{}/game/assets/appicon.png", file_util::get_project_path()); GLFWimage images[1]; images[0].pixels = diff --git a/goal_src/build/all_files.gc b/goal_src/build/all_files.gc index 9d71ad2d4f..233524c846 100644 --- a/goal_src/build/all_files.gc +++ b/goal_src/build/all_files.gc @@ -217,6 +217,7 @@ "goal_src/engine/level/level-info.gc" "goal_src/engine/level/level.gc" "goal_src/engine/ui/text.gc" + "goal_src/engine/pc/subtitle.gc" ;; added "goal_src/engine/collide/collide-probe.gc" "goal_src/engine/collide/collide-frag.gc" "goal_src/engine/collide/collide-mesh.gc" diff --git a/goal_src/build/game_dgos.gc b/goal_src/build/game_dgos.gc index 27199a0588..3e4764347b 100644 --- a/goal_src/build/game_dgos.gc +++ b/goal_src/build/game_dgos.gc @@ -418,6 +418,7 @@ ("level-info.o" "level-info") ("level.o" "level") ("text.o" "text") + ("subtitle.o" "subtitle") ;; added ("collide-probe.o" "collide-probe") ("collide-frag.o" "collide-frag") ("collide-mesh.o" "collide-mesh") diff --git a/goal_src/dgos/engine.gd b/goal_src/dgos/engine.gd index 609d30e8dd..9b88c637a5 100644 --- a/goal_src/dgos/engine.gd +++ b/goal_src/dgos/engine.gd @@ -209,6 +209,7 @@ ("level-info.o" "level-info") ("level.o" "level") ("text.o" "text") + ("subtitle.o" "subtitle") ;; added ("collide-probe.o" "collide-probe") ("collide-frag.o" "collide-frag") ("collide-mesh.o" "collide-mesh") diff --git a/goal_src/dgos/game.gd b/goal_src/dgos/game.gd index eeb7ad8ba1..00cf4e009a 100644 --- a/goal_src/dgos/game.gd +++ b/goal_src/dgos/game.gd @@ -205,6 +205,7 @@ ("level-info.o" "level-info") ("level.o" "level") ("text.o" "text") + ("subtitle.o" "subtitle") ;; added ("collide-probe.o" "collide-probe") ("collide-frag.o" "collide-frag") ("collide-mesh.o" "collide-mesh") diff --git a/goal_src/engine/debug/default-menu.gc b/goal_src/engine/debug/default-menu.gc index 2c004fcd5d..503a07ed56 100644 --- a/goal_src/engine/debug/default-menu.gc +++ b/goal_src/engine/debug/default-menu.gc @@ -4271,6 +4271,20 @@ ) ) +(defun dm-subtitle-language ((blang int) (msg debug-menu-msg)) + (let ((lang (the pc-subtitle-lang (/ blang 8)))) + (when (= msg (debug-menu-msg press)) + (set! (-> *pc-settings* subtitle-language) lang)) + (= (-> *pc-settings* subtitle-language) lang) + ) + ) + +(defun dm-subtitle-setting ((setting symbol) (msg debug-menu-msg)) + (when (= msg (debug-menu-msg press)) + (set! (-> *pc-settings* subtitle-speaker?) setting)) + (= (-> *pc-settings* subtitle-speaker?) setting) + ) + (when (-> *debug-menu-context* root-menu) (debug-menu-append-item (-> *debug-menu-context* root-menu) (debug-menu-make-load-menu *debug-menu-context*)) (debug-menu-append-item (-> *debug-menu-context* root-menu) (debug-menu-make-part-menu *debug-menu-context*)) @@ -4336,8 +4350,18 @@ ) (flag "Letterbox" #f ,(dm-lambda-boolean-flag (-> *pc-settings* letterbox?))) ;(flag "Skip movies" #f ,(dm-lambda-boolean-flag (-> *pc-settings* skip-movies?))) - ;(flag "Subtitles" #f ,(dm-lambda-boolean-flag (-> *pc-settings* subtitles?))) + (flag "Subtitles" #f ,(dm-lambda-boolean-flag (-> *pc-settings* subtitles?))) ;(flag "Hinttitles" #f ,(dm-lambda-boolean-flag (-> *pc-settings* hinttitles?))) + (menu "Subtitle speaker" + (flag "Off" #f dm-subtitle-setting) + (flag "On" #t dm-subtitle-setting) + (flag "Auto" auto dm-subtitle-setting) + ) + (menu "Subtitle language" + (flag "english" 0 dm-subtitle-language) + (flag "spanish" 3 dm-subtitle-language) + (flag "uk-english" 6 dm-subtitle-language) + ) (flag "Discord RPC" #t ,(dm-lambda-boolean-flag (-> *pc-settings* discord-rpc?))) (menu "Game fixes" (flag "sagecage crash" #f ,(dm-lambda-boolean-flag (-> *pc-settings* fixes crash-sagecage))) diff --git a/goal_src/engine/entity/entity.gc b/goal_src/engine/entity/entity.gc index 8990e0dbbb..cfcf3d4a06 100644 --- a/goal_src/engine/entity/entity.gc +++ b/goal_src/engine/entity/entity.gc @@ -1923,6 +1923,11 @@ (#when PC_PORT +(when *debug-segment* + ;; temporary string. + (define *debug-temp-string* (new 'debug 'string 4096 (the string #f))) + ) + ;; custom entity functions for pc port (defun-debug entity-inspect-draw ((inspect-info entity-debug-inspect)) "draw text about an entity on screen" diff --git a/goal_src/engine/game/game-info.gc b/goal_src/engine/game/game-info.gc index 046ff88180..f826c8b33b 100644 --- a/goal_src/engine/game/game-info.gc +++ b/goal_src/engine/game/game-info.gc @@ -240,7 +240,7 @@ "demo-start" ) (*debug-segment* - (#if (user? dass) "citadel-generator-end" "village1-hut") + (#if (user? dass) "citadel-launch-end" "village1-hut") ) (else "title-start" diff --git a/goal_src/engine/load/loader.gc b/goal_src/engine/load/loader.gc index b6334047ab..7ed7525541 100644 --- a/goal_src/engine/load/loader.gc +++ b/goal_src/engine/load/loader.gc @@ -414,10 +414,7 @@ ) (('inactive) ;; no usable data here, fill the buffer - ;; TODO macro - (let ((v1-28 (-> obj heap))) - (set! (-> v1-28 current) (-> v1-28 base)) - ) + (kheap-reset (-> obj heap)) (cond ((string= (-> obj load-file) "reserved") ;; we want to reserve this buffer for something (not loading an str file) (cond diff --git a/goal_src/engine/pc/pckernel-h.gc b/goal_src/engine/pc/pckernel-h.gc index 4e76b1dd73..5bccaf42ff 100644 --- a/goal_src/engine/pc/pckernel-h.gc +++ b/goal_src/engine/pc/pckernel-h.gc @@ -76,27 +76,27 @@ ;; subtitle languages. (defenum pc-subtitle-lang :type uint16 - (english) - (french) - (german) - (spanish) - (italian) - (japanese) - (uk-english) + (english 0) + (french 1) + (german 2) + (spanish 3) + (italian 4) + (japanese 5) + (uk-english 6) ;; additional languages. ;; these don't neccessarily have to work but I'm future-proofing a bit here, just in case. ;; languages that use the existing glyphs - (portuguese) - (finnish) - (swedish) - (danish) - (norwegian) + (portuguese 7) + (finnish 8) + (swedish 9) + (danish 10) + (norwegian 11) ;; jak 1 has no glyphs for korean or cyrillic. - (korean) ;; future-proofing here - (russian) ;; same thing + (korean 12) ;; future-proofing here + (russian 13) ;; same thing (custom 500) ;; temp ) @@ -128,6 +128,7 @@ ((crash-sagecage symbol) ;; citadel (crash-dma symbol) ;; general out of memory crash (crash-light-eco symbol) ;; why does this one even happen? + (crash-moles symbol) ;; dont know either (lockout-pelican symbol) ;; kill pelican during chase (lockout-pipegame symbol) ;; last buzzer is pipegame (lockout-gambler symbol) ;; talk without completing @@ -206,10 +207,10 @@ (display-actor-counts symbol) ;; device settings - (device-audio pc-device-info :inline) ;; used audio device - (device-screen pc-device-info :inline) ;; used display device - (device-gpu pc-device-info :inline) ;; used graphics device - (device-pad pc-pad-info 4 :inline) ;; used input devices, like controllers. + ;(device-audio pc-device-info :inline) ;; used audio device + ;(device-screen pc-device-info :inline) ;; used display device + ;(device-gpu pc-device-info :inline) ;; used graphics device + ;(device-pad pc-pad-info 4 :inline) ;; used input devices, like controllers. ;(device-keyboard pc-pad-info :inline) ;; keyboard input information. if nothing else, this must be usable. (stick-deadzone float) ;; analog stick deadzone. 0-1 @@ -248,6 +249,7 @@ (subtitles? symbol) ;; if on, cutscene subtitles will show up (hinttitles? symbol) ;; if on, non-cutscene subtitles will show up (subtitle-language pc-subtitle-lang) ;; language for subtitles + (subtitle-speaker? symbol) ;; #f (force off), #t (force on), auto (on for offscreen) (fixes pc-fixes :inline) ;; extra game fixes @@ -401,6 +403,7 @@ (set! (-> obj skip-movies?) #t) (set! (-> obj subtitles?) #t) (set! (-> obj hinttitles?) #t) + (set! (-> obj subtitle-speaker?) 'auto) (set! (-> obj subtitle-language) (pc-subtitle-lang english)) (none)) @@ -410,6 +413,7 @@ (set! (-> obj fixes crash-sagecage) #t) (set! (-> obj fixes crash-dma) #t) (set! (-> obj fixes crash-light-eco) #t) + (set! (-> obj fixes crash-moles) #t) (set! (-> obj fixes lockout-pelican) #t) (set! (-> obj fixes lockout-pipegame) #t) (set! (-> obj fixes lockout-gambler) #t) diff --git a/goal_src/engine/pc/pckernel.gc b/goal_src/engine/pc/pckernel.gc index 6d683717e0..f097a73a36 100644 --- a/goal_src/engine/pc/pckernel.gc +++ b/goal_src/engine/pc/pckernel.gc @@ -317,6 +317,7 @@ (("aspect4x3") 'aspect4x3) (("aspect16x9") 'aspect16x9) (("borderless") 'borderless) + (("auto") 'auto) (else #t) ) ) @@ -418,6 +419,7 @@ (("lod-force-ocean") (set! (-> obj lod-force-ocean) (file-stream-read-int file))) (("lod-force-actor") (set! (-> obj lod-force-actor) (file-stream-read-int file))) (("subtitle-language") (set! (-> obj subtitle-language) (the-as pc-subtitle-lang (file-stream-read-int file)))) + (("subtitle-speaker") (set! (-> obj subtitle-speaker?) (file-stream-read-symbol file))) (("stick-deadzone") (set! (-> obj stick-deadzone) (file-stream-read-float file))) (("ps2-read-speed?") (set! (-> obj ps2-read-speed?) (file-stream-read-symbol file))) (("ps2-parts?") (set! (-> obj ps2-parts?) (file-stream-read-symbol file))) @@ -425,11 +427,13 @@ (("ps2-se?") (set! (-> obj ps2-se?) (file-stream-read-symbol file))) (("ps2-hints?") (set! (-> obj ps2-hints?) (file-stream-read-symbol file))) (("ps2-lod-dist?") (set! (-> obj ps2-lod-dist?) (file-stream-read-symbol file))) + (("force-actors?") (set! (-> obj force-actors?) (file-stream-read-symbol file))) (("music-fade?") (set! (-> obj music-fade?) (file-stream-read-symbol file))) (("use-vis?") (set! (-> obj use-vis?) (file-stream-read-symbol file))) (("skip-movies?") (set! (-> obj skip-movies?) (file-stream-read-symbol file))) (("subtitles?") (set! (-> obj subtitles?) (file-stream-read-symbol file))) (("hinttitles?") (set! (-> obj hinttitles?) (file-stream-read-symbol file))) + (("discord-rpc?") (set! (-> obj discord-rpc?) (file-stream-read-symbol file))) (("scenes-seen") (dotimes (i 197) (set! (-> obj scenes-seen i) (file-stream-read-int file)) @@ -441,6 +445,7 @@ (("crash-sagecage") (set! (-> obj fixes crash-sagecage) (file-stream-read-symbol file))) (("crash-dma") (set! (-> obj fixes crash-dma) (file-stream-read-symbol file))) (("crash-light-eco") (set! (-> obj fixes crash-light-eco) (file-stream-read-symbol file))) + (("crash-moles") (set! (-> obj fixes crash-moles) (file-stream-read-symbol file))) (("lockout-pelican") (set! (-> obj fixes lockout-pelican) (file-stream-read-symbol file))) (("lockout-pipegame") (set! (-> obj fixes lockout-pipegame) (file-stream-read-symbol file))) (("lockout-gambler") (set! (-> obj fixes lockout-gambler) (file-stream-read-symbol file))) @@ -554,9 +559,12 @@ (format file " (music-fade? ~A)~%" (-> obj music-fade?)) (format file " (use-vis? ~A)~%" (-> obj use-vis?)) (format file " (skip-movies? ~A)~%" (-> obj skip-movies?)) + (format file " (discord-rpc? ~A)~%" (-> obj discord-rpc?)) + (format file " (force-actors? ~A)~%" (-> obj force-actors?)) (format file " (subtitles? ~A)~%" (-> obj subtitles?)) (format file " (hinttitles? ~A)~%" (-> obj hinttitles?)) (format file " (subtitle-language ~D)~%" (-> obj subtitle-language)) + (format file " (subtitle-speaker ~A)~%" (-> obj subtitle-speaker?)) (format file " (scenes-seen") (dotimes (i 197) @@ -571,6 +579,7 @@ (format file " (crash-sagecage ~A)~%" (-> obj fixes crash-sagecage)) (format file " (crash-dma ~A)~%" (-> obj fixes crash-dma)) (format file " (crash-light-eco ~A)~%" (-> obj fixes crash-light-eco)) + (format file " (crash-moles ~A)~%" (-> obj fixes crash-moles)) (format file " (lockout-pelican ~A)~%" (-> obj fixes lockout-pelican)) (format file " (lockout-pipegame ~A)~%" (-> obj fixes lockout-pipegame)) (format file " (lockout-gambler ~A)~%" (-> obj fixes lockout-gambler)) diff --git a/goal_src/engine/pc/subtitle.gc b/goal_src/engine/pc/subtitle.gc new file mode 100644 index 0000000000..3c7c5eb595 --- /dev/null +++ b/goal_src/engine/pc/subtitle.gc @@ -0,0 +1,363 @@ +;;-*-Lisp-*- +(in-package goal) + +#| + + Code for subtitles for the PC port. A PC actor pool is provided, and the subtitle process lives there. + It automatically detects the currently playing cutscene and plays the subtitles for it on channel 0. + Channel 1 and 2 are reserved for ambient sounds, which cannot be auto-detected, so the ambient-control needs to + notify the subtitle process manually (TODO: verify this!) + + Similarly to the generic text file, only one subtitles text file is loaded at once, stored in a specific + heap. + + In Jak 2, the subtitles are stored directly within the .STR files. We don't have that luxury here, unfortunately. + + |# + + +(#when PC_PORT + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; constants +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +(defconstant PC_SUBTITLE_FILE_SIZE (* 64 1024)) +(defconstant PC_SUBTITLE_FILE_NAME "subtit") +(defglobalconstant PC_SUBTITLE_DEBUG #f) + + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; types and enums +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +;;;------------------------ +;; data +;;;------------------------ + +;; enum for the subtitle channels, no more than 2 bytes! +(defenum pc-subtitle-channel + :type int16 + (invalid -1) (movie 0) (ambient 1) (hint 2)) + + +;; information about a single line of subtitles +(deftype subtitle-keyframe (structure) + ( + (frame int32) ;; the frame to play the subtitle line on + (string string) ;; the text for the subtitle line + (speaker string) ;; name of the speaker. leave blank for no speaker. + (offscreen symbol) ;; speaker is offscreen. + ) + :pack-me + ) + +;; an individual entry to a subtitle text making up a scene, comprised of a series of keyframes (frame+line of text) +(deftype subtitle-text (structure) + ( + ;; the channel to play the text on, useful for lookup since it can also be used to tell subtitle types apart + (kind pc-subtitle-channel) + ;; the amount of keyframes + (length int16) + ;; data + (keyframes (inline-array subtitle-keyframe)) + + ;; now we store a way to identify and lookup the subtitles. + + ;; the game-text-id if this is for a hint + (id game-text-id :offset 8) + ;; the name of the spool-anim + (name string :offset 8) + ;; the 8-character name for an ambient or hint + (hash uint64 :offset 8) + + + ) + + :size-assert #x10 ;; compact! + ) + +;; the global subtitle text info bank +(deftype subtitle-text-info (basic) + ((length int32) + (lang pc-subtitle-lang) + (dummy int32) + (data subtitle-text :inline :dynamic) + ) + + (:methods + (get-scene-by-name (_type_ pc-subtitle-channel string) subtitle-text) + ) + ) + + +;;;---------------------------------- +;; process type +;;;---------------------------------- + + +;; the subtitle process! it lives on the PC actor pool and awaits for incoming subtitle messages, or a movie +(deftype subtitle (process) + ( + (font font-context) + ( spool-name string) + (old-spool-name string) + (cur-channel pc-subtitle-channel) + ) + :heap-base #x10 + + (:methods + (subtitle-format (_type_ subtitle-keyframe) string) + ) + ) + + + + +;;;---------------------------------------------- +;; globals +;;;---------------------------------------------- + + +;; the actor pool for PC processes! it has 1K of space for 1 process. +(define *pc-dead-pool* (new 'global 'dead-pool 1 (* 1 1024) '*pc-dead-pool*)) +(define *subtitle-temp-string* (new 'global 'string 16 (the string #f))) +(define *subtitle* (the (pointer subtitle) #f)) + +;; subtitle text data +(define *subtitle-text* (the subtitle-text-info #f)) +(kheap-alloc (define *subtitle-text-heap* (new 'global 'kheap)) PC_SUBTITLE_FILE_SIZE) + + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; loading files +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +(defmethod get-scene-by-name subtitle-text-info ((obj subtitle-text-info) (kind pc-subtitle-channel) (name string)) + "get a subtitle scene info with the corresponding name. #f = none found" + + (dotimes (i (-> obj length)) + ;; name and kind matches, return that! + (when (and (= kind (-> obj data i kind)) (string= (-> obj data i name) name)) + (return (-> obj data i)) + ) + ) + + (the subtitle-text #f)) + +(defun load-subtitle-text-info ((txt-name string) (curr-text symbol) (heap kheap)) + "load a subtitles text file onto a heap. + txt-name = language-agnostic file name + curr-text = a symbol to a subtitle-text-info to link the file to + heap = the text heap to load the file onto" + + (local-vars (v0-2 int) (heap-sym-heap subtitle-text-info) (lang pc-subtitle-lang) (load-status int) (heap-free int)) + (set! heap-sym-heap (the-as subtitle-text-info (-> curr-text value))) + (set! lang (-> *pc-settings* subtitle-language)) + (set! load-status 0) + (set! heap-free (&- (-> heap top) (the-as uint (-> heap base)))) + (when (or (= heap-sym-heap #f) + (!= (-> heap-sym-heap lang) lang) + ) + (kheap-reset heap) + + (while (not (str-load (string-format "~D~S.TXT" lang txt-name) -1 (logand -64 (&+ (-> heap current) 63)) (&- (-> heap top) (-> heap current)))) + (return 0) + ) + + (label retry) + (let ((v1-20 (str-load-status (the-as (pointer int32) (& load-status))))) + (when (= v1-20 'error) + (format 0 "Error loading subtitle~%") + (return 0) + (goto loaded) + ) + (cond + ((>= load-status (+ heap-free -300)) + (format 0 "Game subtitle heap overrun!~%") + (return 0) + ) + ((= v1-20 'busy) + (goto retry) + ) + ) + ) + (label loaded) + (let ((s2-1 (logand -64 (&+ (-> heap current) 63)))) + (flush-cache 0) + (set! (-> curr-text value) (link s2-1 (-> (string-format "~D~S.TXT" lang txt-name) data) load-status heap 0)) + ) + (if (<= (the-as int (-> curr-text value)) 0) + (set! (-> curr-text value) (the-as object #f)) + ) + ) + 0) + + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; subtitle process and drawing! +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +(defmethod subtitle-format subtitle ((obj subtitle) (keyframe subtitle-keyframe)) + "check settings and format subtitle accordingly." + + (cond + ((= 0 (length (-> keyframe speaker))) + (string-format "~S" (-> keyframe string)) + ) + ((or (= #t (-> *pc-settings* subtitle-speaker?)) + (and (= 'auto (-> *pc-settings* subtitle-speaker?)) (-> keyframe offscreen))) + (string-format "~3L~S:~0L ~S" (-> keyframe speaker) (-> keyframe string)) + ) + (else + (string-format "~S" (-> keyframe string)) + ) + ) + + ) + +(defun subtitle? () + "can a subtitle be displayed right now?" + (and (-> *pc-settings* subtitles?) ;; subtitles enabled + (or *debug-segment* (= *master-mode* 'game)) ;; current mode is game, OR we are just debugging + ) + ) + + +(defstate subtitle-process (subtitle) + + :event (behavior ((from process) (argc int) (msg symbol) (block event-message-block)) + (case msg + (('subtitle-start) + 0 + ) + ) + ) + :code (behavior () + (loop + (suspend)) + ) + :trans (behavior () + (load-subtitle-text-info PC_SUBTITLE_FILE_NAME '*subtitle-text* *subtitle-text-heap*) + ;; reset params + (set! (-> self spool-name) #f) + (set! (-> self cur-channel) (pc-subtitle-channel invalid)) + (none)) + :post (behavior () + ;; check what kind of subtitles are running + (when (and (movie?) (-> *art-control* spool-lock) (-> *art-control* active-stream)) + ;; there's a cutscene happening and an active spool with a valid owner. + (set! (-> self spool-name) (-> *art-control* active-stream)) + (set! (-> self cur-channel) (pc-subtitle-channel movie)) + (with-proc ((handle->process (-> *art-control* spool-lock))) + (format *stdcon* "current spool frame is ~D~%" (the int (ja-aframe-num 0))) + ) + ) + + ;; do subtitles + (case (-> self cur-channel) + (((pc-subtitle-channel movie)) + ;; cutscenes, check if user toggled subtitles + (if (and (= 'game *master-mode*) (cpad-pressed? 0 square)) + (not! (-> *pc-settings* subtitles?))) + (when (subtitle?) + ;; subtitles on! get a subtitle info that matches our current cutscene, and find at what point we're in. + (let ((scene (get-scene-by-name *subtitle-text* (-> self cur-channel) (-> self spool-name)))) + (when scene + ;; got a scene! get closest subtitle now. + (let ((pos 0) + (keyframe (the subtitle-keyframe #f))) + ;; get frame num + (with-proc ((handle->process (-> *art-control* spool-lock))) + (set! pos (the int (ja-aframe-num 0))) + ) + ;; find closest keyframe + (dotimes (i (-> scene length)) + (when (>= pos (-> scene keyframes i frame)) + (set! keyframe (-> scene keyframes i))) + ) + (when keyframe + ;; we got the closest keyframe! finally just render the subtitles! + (print-game-text (subtitle-format self keyframe) (-> self font) #f 128 22) + (#when PC_SUBTITLE_DEBUG + (draw-debug-text-box (-> self font)) + ) + ) + ) + ) + ) + + ;; keep this for later + (set! (-> self old-spool-name) (-> self spool-name)) + ) + ) + ) + 0) + + ) + + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; helper functions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + + +(defmethod length subtitle-text-info ((obj subtitle-text-info)) + "Get the length (number of subtitle scenes) in a subtitle-text-info." + (-> obj length) + ) + +(defmethod length subtitle-text ((obj subtitle-text)) + "Get the length (number of subtitle lines) in a subtitle-text." + (-> obj length) + ) + + + +(defun subtitle-stop () + "kill the subtitle process" + + (kill-by-type subtitle *active-pool*) + (set! *subtitle* (the (pointer subtitle) #f))) + +(defun subtitle-start () + "start the subtitle process" + + (when *subtitle* + (subtitle-stop) + ) + + (set! *subtitle* (make-init-process subtitle + (lambda :behavior subtitle () + (set! (-> self font) (new 'process 'font-context *font-default-matrix* + (the int (* 0.12 256)) (+ 112 (* 11 5)) 0.0 + (font-color default) + (font-flags shadow kerning middle large))) + (set-scale! (-> self font) 0.5) + (set-width! (-> self font) (the int (* 0.88 256 2))) + (set-height! (-> self font) (* 11 3)) + (go subtitle-process) + ) + :from *pc-dead-pool* :to *active-pool* + :stack-size (* 1 1024))) + ) + + +;; start the subtitle process +(subtitle-start) + + +) + + + diff --git a/goal_src/engine/ui/text-h.gc b/goal_src/engine/ui/text-h.gc index fe765c03f8..8169ffd902 100644 --- a/goal_src/engine/ui/text-h.gc +++ b/goal_src/engine/ui/text-h.gc @@ -460,7 +460,7 @@ ;; an individual string. (deftype game-text (structure) - ((id uint32 :offset-assert 0) + ((id game-text-id :offset-assert 0) (text string :offset-assert 4) ) :pack-me diff --git a/goal_src/engine/ui/text.gc b/goal_src/engine/ui/text.gc index 1eee9d44cd..67e7f1612c 100644 --- a/goal_src/engine/ui/text.gc +++ b/goal_src/engine/ui/text.gc @@ -113,8 +113,7 @@ (the-as string #f) ) (else - (format (clear *temp-string*) "UNKNOWN ID ~D" arg0) - *temp-string* + (string-format "UNKNOWN ID ~D" arg0) ) ) ) @@ -220,8 +219,6 @@ (s3-1 s2-1 (-> *temp-string* data) load-status heap 0) ) ) - (format 0 "loaded text ~A~%" *common-text*) - (format 0 "~A~%" (lookup-text! *common-text* (game-text-id continue-without-saving) #f)) ) ;; linking error occured? diff --git a/goal_src/game.gp b/goal_src/game.gp index b20306d354..aa0aed7c5a 100644 --- a/goal_src/game.gp +++ b/goal_src/game.gp @@ -324,6 +324,13 @@ "out/iso/6COMMON.TXT") ) +(defstep :in "game/assets/game_subtitle.txt" + :tool 'subtitle + :out '("out/iso/0SUBTIT.TXT" + "out/iso/3SUBTIT.TXT" + "out/iso/6SUBTIT.TXT") + ) + ;;;;;;;;;;;;;;;;;;;;; @@ -333,6 +340,7 @@ (group "engine" "out/iso/0COMMON.TXT" + "out/iso/0SUBTIT.TXT" "out/iso/KERNEL.CGO" "out/iso/GAME.CGO" ) @@ -345,6 +353,7 @@ (group "hub1" "out/iso/0COMMON.TXT" + "out/iso/0SUBTIT.TXT" "out/iso/KERNEL.CGO" "out/iso/GAME.CGO" "out/iso/VI1.DGO" @@ -352,6 +361,7 @@ "out/iso/FIC.DGO" "out/iso/JUN.DGO" "out/iso/BEA.DGO" + "out/iso/MIS.DGO" ) ;;;;;;;;;;;;;;;;;;;;;;;; @@ -1733,6 +1743,7 @@ "level/level-info.gc" "level/level.gc" "ui/text.gc" + "pc/subtitle.gc" ;; added "collide/collide-probe.gc" "collide/collide-frag.gc" "collide/collide-mesh.gc" @@ -1852,6 +1863,7 @@ (group-list "iso" `("out/iso/0COMMON.TXT" + "out/iso/0SUBTIT.TXT" ,@*all-cgos* ,@*all-vis* ,@*all-str*) @@ -1860,3 +1872,10 @@ (group-list "spools" `(,@*all-str*) ) + +(group-list "text" + `( + "out/iso/0COMMON.TXT" + "out/iso/0SUBTIT.TXT" + ) + ) diff --git a/goal_src/goal-lib.gc b/goal_src/goal-lib.gc index 8250881d06..0c5802dc85 100644 --- a/goal_src/goal-lib.gc +++ b/goal_src/goal-lib.gc @@ -97,6 +97,15 @@ ) ) +;; enum for text file encoding versions +(defenum game-text-version + (jak1-v1 10) + (jak1-v2 11) + (jak2 20) + (jak3 30) + (jakx 40) + ) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; CONDITIONAL COMPILATION ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/goal_src/goos-lib.gs b/goal_src/goos-lib.gs index 8f077235a2..8a29a18b0b 100644 --- a/goal_src/goos-lib.gs +++ b/goal_src/goos-lib.gs @@ -404,7 +404,7 @@ (fmt #t "Loading user scripts for user: {}...\n" *user*) ;; i'm not sure what naming scheme to use here. user//user.gs? ;; the GOAL one is loaded in Compiler.cpp - (load-file (fmt #f "goal_src/user/{}/user.gs" *user*)) + (try-load-file (fmt #f "goal_src/user/{}/user.gs" *user*)) ) (defsmacro user? (&rest users) diff --git a/goal_src/kernel/gkernel-h.gc b/goal_src/kernel/gkernel-h.gc index 3c8fc5bdcf..2a4f604430 100644 --- a/goal_src/kernel/gkernel-h.gc +++ b/goal_src/kernel/gkernel-h.gc @@ -598,6 +598,14 @@ ,@body) ) +(defmacro with-proc (bindings &rest body) + `(rlet ((pp :reg r13 :reset-here #t :type process)) + (protect (pp) + (set! pp ,(car bindings)) + ,@body + )) + ) + (defmacro defbehavior (name process-type bindings &rest body) (if (and (> (length body) 1) ;; more than one thing in function @@ -622,6 +630,22 @@ ) ) +(defmacro kheap-alloc (heap size) + "allocate space for a kheap" + `(let ((heap ,heap) (size ,size)) + (set! (-> heap base) (malloc 'global size)) + (set! (-> heap current) (-> heap base)) + (set! (-> heap top-base) (&+ (-> heap base) size)) + (set! (-> heap top) (-> heap top-base)) + ) + ) + +(defmacro kheap-reset (heap) + "reset the kheap, so you can use its memory again" + `(let ((heap ,heap)) + (set! (-> heap current) (-> heap base)) + ) + ) (defmacro with-sp (&rest body) `(rlet ((sp :reg rsp :reset-here #t :type pointer)) diff --git a/goal_src/kernel/gstring.gc b/goal_src/kernel/gstring.gc index 5772ff38aa..77497873f8 100644 --- a/goal_src/kernel/gstring.gc +++ b/goal_src/kernel/gstring.gc @@ -722,9 +722,3 @@ ) ) -(#when PC_PORT -(when *debug-segment* - ;; shared temporary string. - (define *debug-temp-string* (new 'debug 'string 4096 (the string #f))) - ) -) diff --git a/goal_src/levels/common/basebutton.gc b/goal_src/levels/common/basebutton.gc index 198565897b..14437931f3 100644 --- a/goal_src/levels/common/basebutton.gc +++ b/goal_src/levels/common/basebutton.gc @@ -541,8 +541,16 @@ (set! (-> s4-0 param 2) (the-as uint (target-pos 0))) (send-event-function *target* s4-0) ) + ;; NOTE : added case for "training" here. in the original game, the training level does NOT come + ;; with its own code for warp gates and buttons, and uses the villagep-obs imported from village1 + ;; instead. opengoal loads files different enough that warp from training to anywhere except village1 + ;; crashes the game due to running unlinked code. the original game also crashes, but it is not consistent. + ;; the citadel/lavatube case makes it so we wait until it's safe to unload both levels in the heaps, + ;; since the citadel warp gate is located in both levels at once (visually lavatube, technically citadel) + ;; we add "training" to the list here so that the training warp gate waits until it's safe to + ;; dispose the old code from memory. (case (-> self level) - (('citadel 'lavatube) + (('citadel 'lavatube 'training) (while (and *target* (zero? (logand (-> *target* draw status) (draw-status hidden)))) (suspend) ) diff --git a/goalc/CMakeLists.txt b/goalc/CMakeLists.txt index 8ebc84d3b0..3b91999d60 100644 --- a/goalc/CMakeLists.txt +++ b/goalc/CMakeLists.txt @@ -31,6 +31,7 @@ add_library(compiler data_compiler/dir_tpages.cpp data_compiler/DataObjectGenerator.cpp data_compiler/game_count.cpp + data_compiler/game_subtitle.cpp debugger/Debugger.cpp debugger/DebugInfo.cpp listener/Listener.cpp diff --git a/goalc/compiler/Compiler.cpp b/goalc/compiler/Compiler.cpp index 766456f0c6..86040ceb59 100644 --- a/goalc/compiler/Compiler.cpp +++ b/goalc/compiler/Compiler.cpp @@ -20,14 +20,20 @@ Compiler::Compiler(const std::string& user_profile, std::unique_ptr // let the build system run us m_make.add_tool(std::make_shared(this)); + m_make.add_tool(std::make_shared(this)); // load GOAL library Object library_code = m_goos.reader.read_from_file({"goal_src", "goal-lib.gc"}); compile_object_file("goal-lib", library_code, false); if (user_profile != "#f") { - Object user_code = m_goos.reader.read_from_file({"goal_src", "user", user_profile, "user.gc"}); - compile_object_file(user_profile, user_code, false); + try { + Object user_code = + m_goos.reader.read_from_file({"goal_src", "user", user_profile, "user.gc"}); + compile_object_file(user_profile, user_code, false); + } catch (std::exception& e) { + print_compiler_warning("REPL Warning: {}\n", e.what()); + } } // add built-in forms to symbol info diff --git a/goalc/compiler/Compiler.h b/goalc/compiler/Compiler.h index aa01895314..a23f100c50 100644 --- a/goalc/compiler/Compiler.h +++ b/goalc/compiler/Compiler.h @@ -18,6 +18,7 @@ #include "goalc/emitter/Register.h" #include "goalc/listener/Listener.h" #include "goalc/make/MakeSystem.h" +#include "goalc/data_compiler/game_subtitle.h" enum MathMode { MATH_INT, MATH_BINT, MATH_FLOAT, MATH_INVALID }; @@ -53,6 +54,7 @@ class Compiler { listener::Listener& listener() { return m_listener; } void poke_target() { m_listener.send_poke(); } bool connect_to_target(); + GameSubtitleDB& subtitle_db() { return m_subtitle_db; } Replxx::completions_t find_symbols_by_prefix(std::string const& context, int& contextLen, std::vector const& user_data); @@ -83,6 +85,7 @@ class Compiler { SymbolInfoMap m_symbol_info; std::unique_ptr m_repl; MakeSystem m_make; + GameSubtitleDB m_subtitle_db; struct DebugStats { int num_spills = 0; @@ -550,6 +553,7 @@ class Compiler { Val* compile_asm_file(const goos::Object& form, const goos::Object& rest, Env* env); Val* compile_repl_clear_screen(const goos::Object& form, const goos::Object& rest, Env* env); Val* compile_asm_data_file(const goos::Object& form, const goos::Object& rest, Env* env); + Val* compile_asm_text_file(const goos::Object& form, const goos::Object& rest, Env* env); Val* compile_repl_help(const goos::Object& form, const goos::Object& rest, Env* env); Val* compile_listen_to_target(const goos::Object& form, const goos::Object& rest, Env* env); Val* compile_reset_target(const goos::Object& form, const goos::Object& rest, Env* env); diff --git a/goalc/compiler/compilation/Atoms.cpp b/goalc/compiler/compilation/Atoms.cpp index dc85c2af11..1fa7798e3c 100644 --- a/goalc/compiler/compilation/Atoms.cpp +++ b/goalc/compiler/compilation/Atoms.cpp @@ -147,6 +147,7 @@ const std::unordered_map< {":exit", &Compiler::compile_exit}, {"asm-file", &Compiler::compile_asm_file}, {"asm-data-file", &Compiler::compile_asm_data_file}, + {"asm-text-file", &Compiler::compile_asm_text_file}, {"listen-to-target", &Compiler::compile_listen_to_target}, {"reset-target", &Compiler::compile_reset_target}, {":status", &Compiler::compile_poke}, diff --git a/goalc/compiler/compilation/CompilerControl.cpp b/goalc/compiler/compilation/CompilerControl.cpp index d693206935..73de469076 100644 --- a/goalc/compiler/compilation/CompilerControl.cpp +++ b/goalc/compiler/compilation/CompilerControl.cpp @@ -16,6 +16,7 @@ #include "goalc/data_compiler/dir_tpages.h" #include "goalc/data_compiler/game_count.h" #include "goalc/data_compiler/game_text.h" +#include "goalc/data_compiler/game_subtitle.h" /*! * Exit the compiler. Disconnects the listener and tells the target to reset itself. @@ -66,6 +67,7 @@ Val* Compiler::compile_asm_data_file(const goos::Object& form, const goos::Objec va_check(form, args, {goos::ObjectType::SYMBOL, goos::ObjectType::STRING}, {}); auto kind = symbol_string(args.unnamed.at(0)); if (kind == "game-text") { + // TODO version compile_game_text(as_string(args.unnamed.at(1))); } else if (kind == "game-count") { compile_game_count(as_string(args.unnamed.at(1))); @@ -77,6 +79,31 @@ Val* Compiler::compile_asm_data_file(const goos::Object& form, const goos::Objec return get_none(); } +/*! + * Compile a "text data file" + */ +Val* Compiler::compile_asm_text_file(const goos::Object& form, const goos::Object& rest, Env* env) { + (void)env; + auto args = get_va(form, rest); + va_check(form, args, {goos::ObjectType::SYMBOL, goos::ObjectType::INTEGER}, + {{"files", {true, goos::ObjectType::PAIR}}}); + auto kind = symbol_string(args.unnamed.at(0)); + if (kind == "subtitle") { + std::vector files; + for_each_in_list(args.named.at("files"), [this, &files, &form](const goos::Object& o) { + if (o.is_string()) { + files.push_back(o.as_string()->data); + } else { + throw_compiler_error(form, "Invalid object {} in asm-text-file files list.", o.print()); + } + }); + compile_game_subtitle(files, (GameTextVersion)args.unnamed.at(1).as_int(), m_subtitle_db); + } else { + throw_compiler_error(form, "The option {} was not recognized for asm-text-file.", kind); + } + return get_none(); +} + /*! * Compile a file, and optionally color, save, or load. * This should only be used for v3 "code object" files. @@ -620,4 +647,4 @@ Val* Compiler::compile_print_debug_compiler_stats(const goos::Object& form, fmt::print("Size of autocomplete prefix tree: {}\n", m_symbol_info.symbol_count()); return get_none(); -} \ No newline at end of file +} diff --git a/goalc/data_compiler/DataObjectGenerator.cpp b/goalc/data_compiler/DataObjectGenerator.cpp index a05f7a55ef..12fa9767d7 100644 --- a/goalc/data_compiler/DataObjectGenerator.cpp +++ b/goalc/data_compiler/DataObjectGenerator.cpp @@ -56,10 +56,9 @@ void DataObjectGenerator::link_word_to_word(int source, int target, int offset) } void DataObjectGenerator::link_word_to_byte(int source_word, int target_byte) { - PointerLinkRecord rec; + auto& rec = m_ptr_links.emplace_back(); rec.source_word = source_word; rec.target_byte = target_byte; - m_ptr_links.push_back(rec); } int DataObjectGenerator::add_ref_to_string_in_pool(const std::string& str) { diff --git a/goalc/data_compiler/game_subtitle.cpp b/goalc/data_compiler/game_subtitle.cpp new file mode 100644 index 0000000000..c3c0cd5cce --- /dev/null +++ b/goalc/data_compiler/game_subtitle.cpp @@ -0,0 +1,203 @@ +/*! + * @file game_subtitle.cpp + * Builds the XSUBTIT.TXT text files. Each file contains all the strings used as subtitles for + * cutscenes, hints and ambient speech, along with the timings. + * + * This kind of file is completely custom. + */ + +#include +#include +#include +#include "game_subtitle.h" +#include "DataObjectGenerator.h" +#include "common/goos/Reader.h" +#include "common/util/FileUtil.h" +#include "common/goos/ParseHelpers.h" +#include "third-party/fmt/core.h" + +namespace { +int64_t get_int(const goos::Object& obj) { + if (obj.is_int()) { + return obj.integer_obj.value; + } + throw std::runtime_error(obj.print() + " was supposed to be an integer, but isn't"); +} + +const goos::Object& car(const goos::Object& x) { + if (!x.is_pair()) { + throw std::runtime_error("invalid pair"); + } + + return x.as_pair()->car; +} + +const goos::Object& cdr(const goos::Object& x) { + if (!x.is_pair()) { + throw std::runtime_error("invalid pair"); + } + + return x.as_pair()->cdr; +} + +std::string get_string(const goos::Object& x) { + if (x.is_string()) { + return x.as_string()->data; + } + throw std::runtime_error(x.print() + " was supposed to be a string, but isn't"); +} + +std::string uppercase(const std::string& in) { + std::string result; + result.reserve(in.size()); + for (auto c : in) { + if (c >= 'a' && c <= 'z') { + c -= ('a' - 'A'); + } + result.push_back(c); + } + return result; +} + +/*! + * Parse a game subtitle file. + * Information is added to the game subtitles database. + * + * The file should begin with (language-id x y z...) with the given language IDs. + * Each entry should be (name (frame "line-text-0" "line-text-1") ... ) + * This adds the subtitle to each of the specified languages. + */ +void parse(const goos::Object& data, GameTextVersion text_ver, GameSubtitleDB& db) { + auto font = get_font_bank(text_ver); + std::map banks; + bool languages_set = false; + + for_each_in_list(data.as_pair()->cdr, [&](const goos::Object& obj) { + if (obj.is_pair()) { + auto& head = car(obj); + if (head.is_symbol() && head.as_symbol()->name == "language-id") { + if (languages_set) { + throw std::runtime_error("Languages have been set multiple times."); + } + + if (cdr(obj).is_empty_list()) { + throw std::runtime_error("At least one language must be set."); + } + + for_each_in_list(cdr(obj), [&](const goos::Object& obj) { + auto lang = get_int(obj); + if (!db.bank_exists(lang)) { + // database has no lang yet + banks[lang] = db.new_bank(lang); + } else { + banks[lang] = db.bank_by_id(lang); + } + }); + + languages_set = true; + } + + else if (head.is_string()) { + if (!languages_set) { + throw std::runtime_error("At least one language must be set before defining entries."); + } + GameSubtitleSceneInfo scene(head.as_string()->data); + for_each_in_list(cdr(obj), [&](const goos::Object& entry) { + if (entry.is_pair()) { + if (!car(entry).is_int() || !car(cdr(entry)).is_symbol() || + !car(cdr(cdr(entry))).is_string() || !car(cdr(cdr(cdr(entry)))).is_string()) { + throw std::runtime_error( + "Each entry must be of format (number symbol \"string\" \"string\")"); + } + + auto line = font->convert_utf8_to_game(car(cdr(cdr(cdr(entry)))).as_string()->data); + auto speaker = font->convert_utf8_to_game(car(cdr(cdr(entry))).as_string()->data); + auto offscreen = car(cdr(entry)).as_symbol()->name != "#f"; + scene.add_line(car(entry).as_int(), line, speaker, offscreen); + } else { + throw std::runtime_error("Each entry must be a list"); + } + }); + for (auto& [lang, bank] : banks) { + if (!bank->scene_exists(scene.name())) { + bank->add_scene(scene); + } else { + // this should copy the data, so it's safe to delete the new one afterwards. + auto& old_scene = bank->scene_by_name(scene.name()); + old_scene.from_other_scene(scene); + } + } + } else { + throw std::runtime_error("Invalid game subtitles file entry: " + head.print()); + } + } else { + throw std::runtime_error("Invalid game subtitles file"); + } + }); + if (!languages_set) { + throw std::runtime_error("At least one language must be set."); + } +} + +/*! + * Write game subtitle data to a file. Uses the V2 object format which is identical between GOAL and + * OpenGOAL. + */ +void compile(GameSubtitleDB& db) { + for (const auto& [lang, bank] : db.banks()) { + DataObjectGenerator gen; + gen.add_type_tag("subtitle-text-info"); // type + gen.add_word(bank->scenes().size()); // length + gen.add_word(lang); // lang + gen.add_word(0); // dummy + + // fifo queue for scene data arrays + std::queue array_link_sources; + // now add all the scene infos + for (auto& [name, scene] : bank->scenes()) { + gen.add_word(0 | + (scene.lines().size() << 16)); // kind (lower 16 bits), length (upper 16 bits) + + array_link_sources.push(gen.words()); + gen.add_word(0); // keyframes (linked later) + + gen.add_ref_to_string_in_pool(scene.name()); // name + + gen.add_word(0); // pad (string is 4 bytes but we have 8) + } + // now add all the scene *data!* (keyframes) + for (auto& [name, scene] : bank->scenes()) { + // link inline-array with reference from earlier + gen.link_word_to_word(array_link_sources.front(), gen.words()); + array_link_sources.pop(); + + for (auto& subtitle : scene.lines()) { + gen.add_word(subtitle.frame); // frame + gen.add_ref_to_string_in_pool(subtitle.line); // line + gen.add_ref_to_string_in_pool(subtitle.speaker); // speaker + gen.add_symbol_link(subtitle.offscreen ? "#t" : "#f"); // offscreen + } + } + + auto data = gen.generate_v2(); + + file_util::create_dir_if_needed(file_util::get_file_path({"out", "iso"})); + file_util::write_binary_file( + file_util::get_file_path( + {"out", "iso", fmt::format("{}{}.TXT", lang, uppercase("subtit"))}), + data.data(), data.size()); + } +} +} // namespace + +void compile_game_subtitle(const std::vector& filenames, + GameTextVersion text_ver, + GameSubtitleDB& db) { + goos::Reader reader; + for (auto& filename : filenames) { + fmt::print("[Build Game Subtitle] {}\n", filename.c_str()); + auto code = reader.read_from_file({filename}); + parse(code, text_ver, db); + } + compile(db); +} diff --git a/goalc/data_compiler/game_subtitle.h b/goalc/data_compiler/game_subtitle.h new file mode 100644 index 0000000000..59488fda45 --- /dev/null +++ b/goalc/data_compiler/game_subtitle.h @@ -0,0 +1,92 @@ +#pragma once +#include "common/util/FontUtils.h" +#include "common/util/Assert.h" +#include +#include +#include + +class GameSubtitleSceneInfo { + public: + GameSubtitleSceneInfo() {} + GameSubtitleSceneInfo(std::string name) : m_name(name) {} + + struct SubtitleLine { + SubtitleLine(int frame, std::string line, std::string speaker, bool offscreen) + : frame(frame), line(line), speaker(speaker), offscreen(offscreen) {} + + int frame; + std::string line; + std::string speaker; + bool offscreen; + }; + + const std::string& name() const { return m_name; } + const std::vector& lines() const { return m_lines; } + void clear_lines() { m_lines.clear(); } + void from_other_scene(GameSubtitleSceneInfo& scene) { + m_name = scene.name(); + m_lines = scene.lines(); + } + + void add_line(int frame, std::string line, std::string speaker, bool offscreen) { + m_lines.emplace_back(frame, line, speaker, offscreen); + } + + private: + std::string m_name; + std::vector m_lines; +}; + +class GameSubtitleBank { + public: + GameSubtitleBank(int lang_id) : m_lang_id(lang_id) {} + + int lang() const { return m_lang_id; } + const std::map& scenes() const { return m_scenes; } + + bool scene_exists(const std::string& name) const { return m_scenes.find(name) != m_scenes.end(); } + GameSubtitleSceneInfo& scene_by_name(const std::string& name) { return m_scenes.at(name); } + void add_scene(GameSubtitleSceneInfo& scene) { + ASSERT(!scene_exists(scene.name())); + m_scenes[scene.name()] = scene; + } + + private: + int m_lang_id; + + std::map m_scenes; +}; + +/*! + * The subtitles database contains a subtitbles bank for each language. + * Each subtitles bank contains a series of subtitle scene infos. + */ +class GameSubtitleDB { + public: + const std::map& banks() const { return m_banks; } + + bool bank_exists(int id) const { return m_banks.find(id) != m_banks.end(); } + + GameSubtitleBank* new_bank(int id) { + ASSERT(!bank_exists(id)); + m_banks[id] = new GameSubtitleBank(id); + return m_banks.at(id); + } + void add_bank(GameSubtitleBank* bank) { + ASSERT(!bank_exists(bank->lang())); + m_banks[bank->lang()] = bank; + } + GameSubtitleBank* bank_by_id(int id) { + if (!bank_exists(id)) { + return nullptr; + } + return m_banks.at(id); + } + + private: + std::map m_banks; +}; + +void compile_game_subtitle(const std::vector& filenames, + GameTextVersion text_ver, + GameSubtitleDB& db); diff --git a/goalc/data_compiler/game_text.cpp b/goalc/data_compiler/game_text.cpp index 8172c802a9..175916ecf6 100644 --- a/goalc/data_compiler/game_text.cpp +++ b/goalc/data_compiler/game_text.cpp @@ -13,23 +13,10 @@ #include "DataObjectGenerator.h" #include "common/util/FileUtil.h" #include "common/util/FontUtils.h" +#include "common/goos/ParseHelpers.h" #include "third-party/fmt/core.h" namespace { -template -void for_each_in_list(const goos::Object& list, const T& f) { - const goos::Object* iter = &list; - while (iter->is_pair()) { - auto lap = iter->as_pair(); - f(lap->car); - iter = &lap->cdr; - } - - if (!iter->is_empty_list()) { - throw std::runtime_error("Invalid list"); - } -} - int64_t get_int(const goos::Object& obj) { if (obj.is_int()) { return obj.integer_obj.value; @@ -95,6 +82,7 @@ std::vector> parse(const goos::Object& data if (languages_set) { throw std::runtime_error("Languages has been set multiple times."); } + languages_set = true; text.resize(get_int(car(cdr(obj)))); if (!cdr(cdr(obj)).is_empty_list()) { diff --git a/goalc/make/MakeSystem.cpp b/goalc/make/MakeSystem.cpp index c07a21e735..5e04453822 100644 --- a/goalc/make/MakeSystem.cpp +++ b/goalc/make/MakeSystem.cpp @@ -11,7 +11,15 @@ #include "goalc/make/Tools.h" std::string MakeStep::print() const { - std::string result = fmt::format("Tool {} with input \"{}\" and deps:\n ", tool, input); + std::string result = fmt::format("Tool {} with inputs", tool); + int i = 0; + for (auto& in : input) { + if (i++ > 0) { + result += ", "; + } + result += fmt::format("\"{}\"", in); + } + result += " and deps:\n "; for (auto& dep : deps) { result += dep; result += '\n'; @@ -75,8 +83,9 @@ goos::Object MakeSystem::handle_defstep(const goos::Object& form, va_check(form, args, {}, {{"out", {true, {goos::ObjectType::PAIR}}}, {"tool", {true, {goos::ObjectType::SYMBOL}}}, - {"in", {false, {goos::ObjectType::STRING}}}, - {"dep", {false, {}}}}); + {"in", {false, {}}}, + {"dep", {false, {}}}, + {"arg", {false, {}}}}); auto step = std::make_shared(); @@ -91,7 +100,14 @@ goos::Object MakeSystem::handle_defstep(const goos::Object& form, } if (args.has_named("in")) { - step->input = args.get_named("in").as_string()->data; + const auto& in = args.get_named("in"); + if (in.is_pair()) { + step->input.clear(); + goos::for_each_in_list( + in, [&](const goos::Object& o) { step->input.push_back(o.as_string()->data); }); + } else { + step->input = {in.as_string()->data}; + } } if (args.has_named("dep")) { @@ -100,6 +116,12 @@ goos::Object MakeSystem::handle_defstep(const goos::Object& form, }); } + if (args.has_named("arg")) { + step->arg = args.get_named("arg"); + } else { + step->arg = goos::Object::make_empty_list(); + } + for (auto& output : step->outputs) { auto existing = m_output_to_step.find(output); if (existing != m_output_to_step.end()) { @@ -172,8 +194,9 @@ void MakeSystem::get_dependencies(const std::string& master_target, } const auto& rule = rule_it->second; - for (auto& dep : m_tools.at(rule->tool) - ->get_additional_dependencies({rule->input, rule->deps, rule->outputs})) { + for (auto& dep : + m_tools.at(rule->tool) + ->get_additional_dependencies({rule->input, rule->deps, rule->outputs, rule->arg})) { get_dependencies(master_target, dep, result, result_set); } @@ -212,10 +235,11 @@ std::vector MakeSystem::filter_dependencies(const std::vectortool); + const ToolInput task = {rule->input, rule->deps, rule->outputs, rule->arg}; bool added = false; - if (tool->needs_run({rule->input, rule->deps, rule->outputs})) { + if (tool->needs_run(task)) { result.push_back(to_make); stale_deps.insert(to_make); added = true; @@ -235,8 +259,7 @@ std::vector MakeSystem::filter_dependencies(const std::vectorget_additional_dependencies({rule->input, rule->deps, rule->outputs})) { + for (auto& dep : tool->get_additional_dependencies(task)) { if (stale_deps.find(dep) != stale_deps.end()) { result.push_back(to_make); stale_deps.insert(to_make); @@ -253,11 +276,19 @@ std::vector MakeSystem::filter_dependencies(const std::vector 70) { - fmt::print("{}...{}", in.substr(0, 70 - 3), end); +void print_input(const std::vector& in, char end) { + int i = 0; + std::string all_names; + for (auto& name : in) { + if (i++ > 0) { + all_names += ", "; + } + all_names += name; + } + if (all_names.length() > 70) { + fmt::print("{}...{}", all_names.substr(0, 70 - 3), end); } else { - fmt::print("{}{}{}", in, std::string(70 - in.length(), ' '), end); + fmt::print("{}{}{}", all_names, std::string(70 - all_names.length(), ' '), end); } } } // namespace @@ -286,7 +317,8 @@ bool MakeSystem::make(const std::string& target, bool force, bool verbose) { auto& tool = m_tools.at(rule->tool); int percent = (100.0 * (1 + (i++)) / (deps.size())) + 0.5; if (verbose) { - fmt::print("[{:3d}%] [{:8s}] {}\n", percent, tool->name(), rule->input); + fmt::print("[{:3d}%] [{:8s}] {}{}\n", percent, tool->name(), rule->input.at(0), + rule->input.size() > 1 ? ", ..." : ""); } else { fmt::print("[{:3d}%] [{:8s}] ", percent, tool->name()); print_input(rule->input, '\r'); @@ -294,13 +326,14 @@ bool MakeSystem::make(const std::string& target, bool force, bool verbose) { bool success = false; try { - success = tool->run({rule->input, rule->deps, rule->outputs}); + success = tool->run({rule->input, rule->deps, rule->outputs, rule->arg}); } catch (std::exception& e) { fmt::print("\n"); fmt::print("Error: {}\n", e.what()); } if (!success) { - fmt::print("Build failed on {}.\n", rule->input); + fmt::print("Build failed on {}{}.\n", rule->input.at(0), + rule->input.size() > 1 ? ", ..." : ""); throw std::runtime_error("Build failed."); return false; } @@ -329,4 +362,4 @@ bool MakeSystem::make(const std::string& target, bool force, bool verbose) { fmt::print("\nSuccessfully built all {} targets in {:.3f}s\n", deps.size(), make_timer.getSeconds()); return true; -} \ No newline at end of file +} diff --git a/goalc/make/MakeSystem.h b/goalc/make/MakeSystem.h index 2e53fc1c13..4231e90a9e 100644 --- a/goalc/make/MakeSystem.h +++ b/goalc/make/MakeSystem.h @@ -4,8 +4,9 @@ #include "goalc/make/Tool.h" struct MakeStep { - std::string input; + std::vector input; std::vector deps, outputs; + goos::Object arg; std::string tool; std::string print() const; diff --git a/goalc/make/Tool.cpp b/goalc/make/Tool.cpp index e88189b74f..8ee74454da 100644 --- a/goalc/make/Tool.cpp +++ b/goalc/make/Tool.cpp @@ -12,46 +12,48 @@ Tool::Tool(const std::string& name) : m_name(name) {} bool Tool::needs_run(const ToolInput& task) { // for this to return false, all outputs need to be newer than all inputs. - auto in_file = std::filesystem::path(file_util::get_file_path({task.input})); + for (auto& in : task.input) { + auto in_file = std::filesystem::path(file_util::get_file_path({in})); - if (!std::filesystem::exists(in_file)) { - throw std::runtime_error(fmt::format("Input file {} does not exist.", task.input)); - } - - auto newest_input = std::filesystem::last_write_time(in_file); - for (auto& dep : task.deps) { - auto dep_path = std::filesystem::path(file_util::get_file_path({dep})); - if (std::filesystem::exists(dep_path)) { - auto dep_time = std::filesystem::last_write_time(dep_path); - if (dep_time > newest_input) { - newest_input = dep_time; - } - } else { - return true; // don't have a dep. + if (!std::filesystem::exists(in_file)) { + throw std::runtime_error(fmt::format("Input file {} does not exist.", in)); } - } - for (auto& dep : get_additional_dependencies(task)) { - auto dep_path = std::filesystem::path(file_util::get_file_path({dep})); - if (std::filesystem::exists(dep_path)) { - auto dep_time = std::filesystem::last_write_time(dep_path); - if (dep_time > newest_input) { - newest_input = dep_time; + auto newest_input = std::filesystem::last_write_time(in_file); + for (auto& dep : task.deps) { + auto dep_path = std::filesystem::path(file_util::get_file_path({dep})); + if (std::filesystem::exists(dep_path)) { + auto dep_time = std::filesystem::last_write_time(dep_path); + if (dep_time > newest_input) { + newest_input = dep_time; + } + } else { + return true; // don't have a dep. } - } else { - return true; // don't have a dep. } - } - for (auto& out : task.output) { - auto out_path = std::filesystem::path(file_util::get_file_path({out})); - if (std::filesystem::exists(out_path)) { - auto out_time = std::filesystem::last_write_time(out_path); - if (out_time < newest_input) { - return true; + for (auto& dep : get_additional_dependencies(task)) { + auto dep_path = std::filesystem::path(file_util::get_file_path({dep})); + if (std::filesystem::exists(dep_path)) { + auto dep_time = std::filesystem::last_write_time(dep_path); + if (dep_time > newest_input) { + newest_input = dep_time; + } + } else { + return true; // don't have a dep. + } + } + + for (auto& out : task.output) { + auto out_path = std::filesystem::path(file_util::get_file_path({out})); + if (std::filesystem::exists(out_path)) { + auto out_time = std::filesystem::last_write_time(out_path); + if (out_time < newest_input) { + return true; + } + } else { + return true; // don't have a dep. } - } else { - return true; // don't have a dep. } } diff --git a/goalc/make/Tool.h b/goalc/make/Tool.h index 5f58447993..fe477295b6 100644 --- a/goalc/make/Tool.h +++ b/goalc/make/Tool.h @@ -3,11 +3,13 @@ #include #include #include +#include "common/goos/Object.h" struct ToolInput { - std::string& input; // the input file + std::vector& input; // the input file std::vector& deps; // explicit dependencies std::vector& output; // produced output files. + goos::Object arg; // optional argument }; class Tool { @@ -22,4 +24,4 @@ class Tool { private: std::string m_name; -}; \ No newline at end of file +}; diff --git a/goalc/make/Tools.cpp b/goalc/make/Tools.cpp index 69f96fc579..2f90e549a4 100644 --- a/goalc/make/Tools.cpp +++ b/goalc/make/Tools.cpp @@ -15,7 +15,11 @@ CompilerTool::CompilerTool(Compiler* compiler) : Tool("goalc"), m_compiler(compiler) {} bool CompilerTool::needs_run(const ToolInput& task) { - if (!m_compiler->knows_object_file(std::filesystem::path(task.input).stem().u8string())) { + if (task.input.size() != 1) { + throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name())); + } + + if (!m_compiler->knows_object_file(std::filesystem::path(task.input.at(0)).stem().u8string())) { return true; } return Tool::needs_run(task); @@ -25,7 +29,7 @@ bool CompilerTool::run(const ToolInput& task) { // todo check inputs try { m_compiler->run_front_end_on_string( - fmt::format("(asm-file \"{}\" :no-time-prints :color :write)", task.input)); + fmt::format("(asm-file \"{}\" :no-time-prints :color :write)", task.input.at(0))); } catch (std::exception& e) { fmt::print("Compilation failed: {}\n", e.what()); return false; @@ -35,16 +39,16 @@ bool CompilerTool::run(const ToolInput& task) { namespace { DgoDescription parse_desc_file(const std::string& filename, goos::Reader& reader) { - auto dgo_desc = reader.read_from_file({filename}).as_pair()->cdr; + auto& dgo_desc = reader.read_from_file({filename}).as_pair()->cdr; if (goos::list_length(dgo_desc) != 1) { throw std::runtime_error("Invalid DGO description - got too many lists"); } - auto dgo = dgo_desc.as_pair()->car; + auto& dgo = dgo_desc.as_pair()->car; DgoDescription desc; - auto first = dgo.as_pair()->car; + auto& first = dgo.as_pair()->car; desc.dgo_name = first.as_string()->data; - auto dgo_rest = dgo.as_pair()->cdr; + auto& dgo_rest = dgo.as_pair()->cdr; for_each_in_list(dgo_rest, [&](const goos::Object& entry) { goos::Arguments e_arg; @@ -63,19 +67,49 @@ DgoDescription parse_desc_file(const std::string& filename, goos::Reader& reader }); return desc; } + +static const std::unordered_map s_text_ver_enum_map = { + {"jak1-v1", GameTextVersion::JAK1_V1}}; + +std::unordered_map> open_subtitle_project( + const std::string& filename) { + goos::Reader reader; + auto& proj = reader.read_from_file({filename}).as_pair()->cdr.as_pair()->car; + if (!proj.is_pair() || !proj.as_pair()->car.is_symbol() || + proj.as_pair()->car.as_symbol()->name != "subtitle") { + throw std::runtime_error("invalid subtitle project"); + } + + std::unordered_map> inputs; + goos::for_each_in_list(proj.as_pair()->cdr, [&](const goos::Object& o) { + if (!o.is_pair()) { + throw std::runtime_error("invalid entry in subtitle project"); + } + + auto& ver = o.as_pair()->car.as_symbol()->name; + auto& in = o.as_pair()->cdr.as_pair()->car.as_string()->data; + + inputs[s_text_ver_enum_map.at(ver)].push_back(in); + }); + + return inputs; +} } // namespace DgoTool::DgoTool() : Tool("dgo") {} bool DgoTool::run(const ToolInput& task) { - auto desc = parse_desc_file(task.input, m_reader); + if (task.input.size() != 1) { + throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name())); + } + auto desc = parse_desc_file(task.input.at(0), m_reader); build_dgo(desc); return true; } std::vector DgoTool::get_additional_dependencies(const ToolInput& task) { std::vector result; - auto desc = parse_desc_file(task.input, m_reader); + auto desc = parse_desc_file(task.input.at(0), m_reader); for (auto& x : desc.entries) { result.push_back(fmt::format("out/obj/{}", x.file_name)); } @@ -85,15 +119,21 @@ std::vector DgoTool::get_additional_dependencies(const ToolInput& t TpageDirTool::TpageDirTool() : Tool("tpage-dir") {} bool TpageDirTool::run(const ToolInput& task) { - compile_dir_tpages(task.input); + if (task.input.size() != 1) { + throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name())); + } + compile_dir_tpages(task.input.at(0)); return true; } CopyTool::CopyTool() : Tool("copy") {} bool CopyTool::run(const ToolInput& task) { + if (task.input.size() != 1) { + throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name())); + } for (auto& out : task.output) { - std::filesystem::copy(std::filesystem::path(file_util::get_file_path({task.input})), + std::filesystem::copy(std::filesystem::path(file_util::get_file_path({task.input.at(0)})), std::filesystem::path(file_util::get_file_path({out})), std::filesystem::copy_options::overwrite_existing); } @@ -103,14 +143,20 @@ bool CopyTool::run(const ToolInput& task) { GameCntTool::GameCntTool() : Tool("game-cnt") {} bool GameCntTool::run(const ToolInput& task) { - compile_game_count(task.input); + if (task.input.size() != 1) { + throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name())); + } + compile_game_count(task.input.at(0)); return true; } TextTool::TextTool() : Tool("text") {} bool TextTool::run(const ToolInput& task) { - compile_game_text(task.input); + if (task.input.size() != 1) { + throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name())); + } + compile_game_text(task.input.at(0)); return true; } @@ -118,4 +164,27 @@ GroupTool::GroupTool() : Tool("group") {} bool GroupTool::run(const ToolInput&) { return true; -} \ No newline at end of file +} + +SubtitleTool::SubtitleTool(Compiler* compiler) : Tool("subtitle"), m_compiler(compiler) {} + +bool SubtitleTool::needs_run(const ToolInput& task) { + if (task.input.size() != 1) { + throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name())); + } + + std::vector deps; + for (auto& [ver, inputs] : open_subtitle_project(task.input.at(0))) { + for (auto& in : inputs) { + deps.push_back(in); + } + } + return Tool::needs_run({task.input, deps, task.output, task.arg}); +} + +bool SubtitleTool::run(const ToolInput& task) { + for (auto& [ver, in] : open_subtitle_project(task.input.at(0))) { + compile_game_subtitle(in, ver, m_compiler->subtitle_db()); + } + return true; +} diff --git a/goalc/make/Tools.h b/goalc/make/Tools.h index 17baec819d..49622d1320 100644 --- a/goalc/make/Tools.h +++ b/goalc/make/Tools.h @@ -2,13 +2,13 @@ #include "goalc/make/Tool.h" #include "common/goos/Reader.h" +#include "goalc/data_compiler/game_subtitle.h" class Compiler; class CompilerTool : public Tool { public: CompilerTool(Compiler* compiler); - bool run(const ToolInput& task) override; bool needs_run(const ToolInput& task) override; @@ -54,4 +54,14 @@ class GroupTool : public Tool { public: GroupTool(); bool run(const ToolInput& task) override; -}; \ No newline at end of file +}; + +class SubtitleTool : public Tool { + public: + SubtitleTool(Compiler* compiler); + bool run(const ToolInput& task) override; + bool needs_run(const ToolInput& task) override; + + private: + Compiler* m_compiler; +};