[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
This commit is contained in:
ManDude 2022-02-19 18:10:10 +00:00 committed by GitHub
parent f91d1f4056
commit af447aeab7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 1160 additions and 148 deletions

View file

@ -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")

View file

@ -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;
}
}

View file

@ -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

View file

@ -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": {

BIN
game/assets/appicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -0,0 +1,11 @@
;; "project file" for subtitles make tool.
;; it's very simple... a list of (version file)
;; eventually should also include output filename
;; you can find the game-text-version parsing in .cpp and an enum in goal-lib.gc
(subtitle
(jak1-v1 "game/assets/subtitle/jak1/game_subtitle_en.txt")
(jak1-v1 "game/assets/subtitle/jak1/game_subtitle_es.txt")
)

View file

@ -0,0 +1,111 @@
(language-id 0 6)
;; -----------------
;; oracle
("oracle-right-eye-1" (0 #f "ORACLE" "FOR YOUR GIFT, ANOTHER POWER CELL IS YOURS."))
("oracle-left-eye-1" (0 #f "ORACLE" "YOU HAVE PROVEN YOURSELF WORTHY. HERE IS A POWER CELL."))
("oracle-right-eye-2" (0 #f "ORACLE" "HERE IS ANOTHER POWER CELL FOR YOUR QUEST."))
("oracle-left-eye-2" (0 #f "ORACLE" "FOR YOUR SACRIFICE, I OFFER YOU A POWER CELL."))
("oracle-right-eye-3" (0 #f "ORACLE" "YOU HAVE OBTAINED ANOTHER POWER CELL."))
("oracle-left-eye-3" (0 #f "ORACLE" "FOR YOUR EFFORT, A POWER CELL IS THE REWARD."))
;; -----------------
;; swamp
("billy-introduction"
(6 #f "BILLY" "HOWDY, FRIENDS! ENJOYIN' MY BEAUTIFUL SWAMP? I OWN THESE HERE PARTS. EVERYTHING THAT DOESN'T SINK INTO THE MUD, THAT IS! (LAUGHS)")
(349 #f "" "")
(369 #f "DAXTER" "JUDGING BY THE SMELL, I'D WAGER YOUR BATHTUB SANK IN THE MUD LONG AGO.")
(497 #f "" "")
(519 #f "BILLY" "WHAT'S A BATHTUB? ANYWAY I GOT BIGGER PROBLEMS NOW...")
(677 #f "" "")
(691 #f "BILLY" "SEEMS SOME NASTY LURKER VARMINTS ARE GROUSIN' ABOUTS SNATCHIN' EVERYTHING THEY CAN GET THEIR GRUBBY LITTLE PAWS ON.")
(910 #f "BILLY" "AND SCARED AWAY MY PET HIPHOG, FARTHY.")
(1000 #f "BILLY" "HE'S BEEN MISSIN' FOR NIGH ON TO A COON'S AGE.")
(1136 #t "BILLY" "I'VE BEEN PUTTIN' OUT HIS FAVORITE SNACK, BUT THOSE ORNERY SWAMP RATS KEEP STEALIN' EM!")
(1318 #t "BILLY" "IF YOU COULD KEEP THOSE PESKY CRITTERS AWAY LONG ENOUGH, I JUST KNOW FARTHY WOULD SMELL THEM VITTLES AND COME BACK!")
(1509 #f "" "")
(1527 #f "BILLY" "WILL YA HELP ME OUT?")
)
("billy-reject"
(7 #f "BILLY" "WELL, IF YOU CHANGE YOUR MIND, YOU KNOW WHERE TO FIND ME! (LAUGHS)")
)
("billy-accept"
(5 #f "BILLY" "GOOD! THOSE RATS WILL BE BACK ANYTIME.")
(109 #f "BILLY" "SHOOT ALL THEM RATS, AND KEEP 'EM FROM EATING AT LEAST ONE OF THEM SNACKS.")
)
("billy-resolution"
(1 #f "BILLY" "WELL FRY MY HIDE! YOU SURE KNOW HOW TO SHOOT! THANKS A HEAP FOR THE HELP.")
)
("billy-reminder-1"
(36 #f "BILLY" "AHH, Y'ALL BACK TO HELP STOP THEM RATS?")
)
;; -----------------
;; citadel
("green-sagecage-introduction"
(130 #f "SAGE" "IT'S ABOUT TIME YOU TWO DECIDED TO SHOW UP!")
(207 #f "DAXTER" "NICE TO SEE YOU TOO! DO THEY HAVE YOU MOPPING THE FLOORS NOW?")
(343 #f "SAGE" "THERE'S NO TIME FOR JOKES, DAXTER. GOL AND MAIA KIDNAPPED US TO SAP OUR ENERGIES TO POWER THEIR ABOMINABLE MACHINE.")
(574 #t "SAGE" "IT APPEARS THEY HAVE COMBINED THE FUNCTIONAL REMAINS OF A PRECURSOR ROBOT WITH SCAVENGED ARTIFACTS FROM ACROSS THE LAND.")
(800 #t "SAGE" "THEN THEY ADDED A FEW DIABOLICAL ADDITIONS OF THEIR OWN, CREATING THE ONE THING CAPABLE OF OPENING THE DARK ECO SILOS.")
(1060 #f "SAGE" "IF YOU CAN FREE THE FOUR OF US, WE CAN USE OUR COMBINED POWERS")
(1180 #f "SAGE" "TO BREAK THE FORCE SHIELD SURROUNDING THE ROBOT, BEFORE THEY USE IT TO DESTROY THE WORLD.")
)
("redsage-resolution"
(60 #f "RED SAGE" "(CHUCKLES) YOU'VE FINALLY COME TO RESCUE ME.")
(190 #f "RED SAGE" "DO YOU KNOW HOW LONG I'VE BEEN IN HERE? WHAT TOOK YOU SO LONG, YOU KNOW? (CHUCKLES) WHAT ARE YOUR NAMES?")
(429 #f "DAXTER" "I'M DAXTER! HE'S JAK, HE'S WITH ME.")
(540 #t "RED SAGE" "GOOD JOB, DAXTER. YOU'RE A REAL HERO.")
(670 #f "" "")
(700 #f "RED SAGE" "YOU'VE GOT TO STOP GOL FROM LAUNCHING THE ROBOT. I'LL USE MY ECO POWER TO HELP OPEN THE SHIELD DOOR.")
)
("bluesage-resolution"
(60 #f "BLUE SAGE" "GOOD WORK, FELLOWS! OLD SAMOS WAS RIGHT ABOUT YOU!")
(165 #f "BLUE SAGE" "GREAT PILES OF PRECURSOR METAL! THAT INSIDIOUS MECHANICAL CREATION MUST NOT BE ALLOWED TO WREAK ITS TERRIBLE HAVOC!")
(420 #f "BLUE SAGE" "I WILL TRY TO ACTUATE THE SHIELD DOOR BY ELLICITING A CONDUIT OF ENERGY BETWEEN MYSELF AND THE VAST PORTAL BELOW!")
(666 #f "DAXTER" "UH... YEAH. YOU DO THAT. WE'LL UH... JUST GO FIND MORE HELP.")
(873 #f "" "")
(891 #t "DAXTER" "WEIRDO!")
(980 #f "" "")
)
("yellowsage-resolution"
(50 #f "YELLOW SAGE" "WHO WOULDA THOUGHT I'D LIVE TO SEE THE DAY WHEN I NEEDED TO BE RESCUED BY A BOY AND HIS MUSKRAT!")
(271 #f "YELLOW SAGE" "(SIGHS) I'M GONNA GIVE GOL AND MAIA A LITTLE PAYBACK FOR THIS EMBARRASSMENT!")
(480 #f "YELLOW SAGE" "THEN WE'LL SEE ABOUT COOKING UP SOME MUSKRAT STEW...")
(606 #f "" "")
)
("green-sagecage-resolution"
(30 #f "SAGE" "GOOD WORK, BOYS! YOU'RE REAL HEROES NOW. I'LL COMBINE MY GREEN ECO POWER WITH THE OTHER THREE SAGES")
(284 #f "SAGE" "AND TOGETHER WE'LL OPEN THE SHIELD DOOR SURROUNDING THE PRECURSOR ROBOT.")
(405 #f "DAXTER" "YEAH YEAH THAT SOUNDS LIKE A GOOD START. AND THEN AFTER YOU GUYS OPEN THAT SHIELD, WHAT ARE YOU GONNA DO ABOUT THE ROBOT?")
(576 #f "SAGE" "NOTHING, DAXTER. WE HAVE TO KEEP THE SHIELD OPEN, IT'S UP TO YOU TWO TO FIGURE OUT HOW TO DESTROY THE ROBOT.")
(823 #f "DAXTER" "OH, GREAT. I GET TO HELP THE GUY THAT TURNED ME INTO A FURBALL DESTROY THE ONLY PERSON WHO CAN TURN ME BACK!")
(1111 #f "" "")
(1156 #f "SAGE" "FIRST, SAVE THE WORLD! THEN WE'LL TRY TO CONVINCE GOL TO HELP DAXTER.")
)
("green-sagecage-outro-preboss"
(50 #f "GOL" "YOU'RE TOO LATE, SAMOS. ONCE I POSSESS LIMITLESS DARK ECO, I WILL HAVE THE KEY TO CREATION ITSELF!")
(335 #f "SAGE" "THIS IS MADNESS! RELEASING THAT MUCH DARK ECO WILL DESTROY EVERYTHING WE KNOW!")
(485 #f "SAGE" "JUST LOOK WHAT IT'S DONE TO YOU!")
(581 #f "MAIA" "IT HAS GIVEN US A BEAUTY BEYOND ANYTHING YOU COULD UNDERSTAND.")
(696 #f "DAXTER" "BEAUTY? HAVE YOU TWO LOOKED IN A MIRROR LATELY?")
(828 #f "MAIA" "JUST WAIT UNTIL WE OPEN THE SILOS, LITTLE ONE. YOU THINK SHORT AND FUZZY IS BAD?")
(1017 #f "GOL" "AND TO THINK, YOU TWO TRAVELLED ALL THIS WAY FOR MY HELP.")
(1180 #f "GOL" "FOOLS! ENJOY YOUR FRONT ROW SEATS TO THE RECREATION OF THE WORLD!")
(1382 #f "" "")
(1605 #f "SAGE" "JAK! TAKE THE ELEVATOR UP AND STOP THAT ROBOT!")
)

View file

@ -0,0 +1,6 @@
(language-id 3)
;; -----------------
;; oracle
("oracle-left-eye-1" (0 #f "ORÁCULO" "HABÉIS DEMOSTRADO SER HONESTOS. É AQUÍ UNA BATERÍA."))

View file

@ -189,16 +189,9 @@ void DirectRenderer::flush_pending(SharedRenderState* render_state, ScopedProfil
// render!
// update buffers:
u32 vertex_offset = m_ogl.last_vertex_offset;
if (vertex_offset + m_prim_buffer.vert_count >= 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 = {};
}

View file

@ -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 {

View file

@ -157,7 +157,7 @@ static std::shared_ptr<GfxDisplay> 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 =

View file

@ -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"

View file

@ -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")

View file

@ -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")

View file

@ -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")

View file

@ -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)))

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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)

View file

@ -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))

View file

@ -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)
)

View file

@ -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

View file

@ -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?

View file

@ -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"
)
)

View file

@ -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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -404,7 +404,7 @@
(fmt #t "Loading user scripts for user: {}...\n" *user*)
;; i'm not sure what naming scheme to use here. user/<name>/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)

View file

@ -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))

View file

@ -722,9 +722,3 @@
)
)
(#when PC_PORT
(when *debug-segment*
;; shared temporary string.
(define *debug-temp-string* (new 'debug 'string 4096 (the string #f)))
)
)

View file

@ -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)
)

View file

@ -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

View file

@ -20,14 +20,20 @@ Compiler::Compiler(const std::string& user_profile, std::unique_ptr<ReplWrapper>
// let the build system run us
m_make.add_tool(std::make_shared<CompilerTool>(this));
m_make.add_tool(std::make_shared<SubtitleTool>(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"});
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

View file

@ -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<std::string> const& user_data);
@ -83,6 +85,7 @@ class Compiler {
SymbolInfoMap m_symbol_info;
std::unique_ptr<ReplWrapper> 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);

View file

@ -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},

View file

@ -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<std::string> 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.

View file

@ -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) {

View file

@ -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 <algorithm>
#include <new>
#include <queue>
#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<int, GameSubtitleBank*> 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<int> 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<std::string>& 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);
}

View file

@ -0,0 +1,92 @@
#pragma once
#include "common/util/FontUtils.h"
#include "common/util/Assert.h"
#include <string>
#include <map>
#include <unordered_set>
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<SubtitleLine>& 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<SubtitleLine> m_lines;
};
class GameSubtitleBank {
public:
GameSubtitleBank(int lang_id) : m_lang_id(lang_id) {}
int lang() const { return m_lang_id; }
const std::map<std::string, GameSubtitleSceneInfo>& 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<std::string, GameSubtitleSceneInfo> 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<int, GameSubtitleBank*>& 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<int, GameSubtitleBank*> m_banks;
};
void compile_game_subtitle(const std::vector<std::string>& filenames,
GameTextVersion text_ver,
GameSubtitleDB& db);

View file

@ -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 <typename T>
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<std::unordered_map<int, std::string>> 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()) {

View file

@ -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<MakeStep>();
@ -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<std::string> MakeSystem::filter_dependencies(const std::vector<std::
for (auto& to_make : all_deps) {
auto& rule = m_output_to_step.at(to_make);
auto& tool = m_tools.at(rule->tool);
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<std::string> MakeSystem::filter_dependencies(const std::vector<std::
if (!added) {
// check transitive dependencies
for (auto& dep :
tool->get_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<std::string> MakeSystem::filter_dependencies(const std::vector<std::
}
namespace {
void print_input(const std::string& in, char end) {
if (in.length() > 70) {
fmt::print("{}...{}", in.substr(0, 70 - 3), end);
void print_input(const std::vector<std::string>& 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;
}

View file

@ -4,8 +4,9 @@
#include "goalc/make/Tool.h"
struct MakeStep {
std::string input;
std::vector<std::string> input;
std::vector<std::string> deps, outputs;
goos::Object arg;
std::string tool;
std::string print() const;

View file

@ -12,10 +12,11 @@ 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));
throw std::runtime_error(fmt::format("Input file {} does not exist.", in));
}
auto newest_input = std::filesystem::last_write_time(in_file);
@ -54,6 +55,7 @@ bool Tool::needs_run(const ToolInput& task) {
return true; // don't have a dep.
}
}
}
return false;
}

View file

@ -3,11 +3,13 @@
#include <optional>
#include <string>
#include <vector>
#include "common/goos/Object.h"
struct ToolInput {
std::string& input; // the input file
std::vector<std::string>& input; // the input file
std::vector<std::string>& deps; // explicit dependencies
std::vector<std::string>& output; // produced output files.
goos::Object arg; // optional argument
};
class Tool {

View file

@ -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<std::string, GameTextVersion> s_text_ver_enum_map = {
{"jak1-v1", GameTextVersion::JAK1_V1}};
std::unordered_map<GameTextVersion, std::vector<std::string>> 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<GameTextVersion, std::vector<std::string>> 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<std::string> DgoTool::get_additional_dependencies(const ToolInput& task) {
std::vector<std::string> 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<std::string> 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;
}
@ -119,3 +165,26 @@ GroupTool::GroupTool() : Tool("group") {}
bool GroupTool::run(const ToolInput&) {
return true;
}
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<std::string> 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;
}

View file

@ -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;
@ -55,3 +55,13 @@ class GroupTool : public Tool {
GroupTool();
bool run(const ToolInput& task) override;
};
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;
};