diff --git a/mods/randomizer/generator/data/flow_patches.yaml b/mods/randomizer/generator/data/flow_patches.yaml index d8cb5d83df..edc856d85f 100644 --- a/mods/randomizer/generator/data/flow_patches.yaml +++ b/mods/randomizer/generator/data/flow_patches.yaml @@ -1,7 +1,5 @@ # Patches for NPC text flows -# NOTE: All data is expressed in little endian - 0: # zel_00.bmg # Change the following indices of Midna's flows to jump to our custom choices # instead. These are all the flow jumps that normally determine which "Talk to Midna" @@ -18,9 +16,9 @@ - 0x1BC - 0x1C9 type: event - event: 42 + event: no-op parameters: 0x00000000 - next node index: Custom Midna Call Begin + next: Custom Midna Call Begin 1: # zel_01.bmg - Ordon Village # Patch the flow in Bo's house that normally prevents us from leaving without @@ -28,33 +26,29 @@ # no matter what - index: 0x8F type: branch - num results: 2 - query: 21 + query: item owned parameters: 0x0045 - next node index: 0x4B results: - 0x90 - 0x90 2: # zel_02.bmg - # Patch Barnes branch to see if you can buy the bomb bag to use - # query 54 (custom event check) instead of query 22 (how many bomb bags the player has) + # Patch Barnes to use the randomizer purchase flag instead of counting bomb bags. - index: 0x47C type: branch - num results: 2 - query: 53 + query: randomizer event flag parameters: 0x004D - next node index: 0x340 + results: + - 0x47D + - 0x485 # Change the flow of buying the red potion item in Kak Malo Mart to not check for an empty bottle first # if the item is randomized. Change both branch paths to lead to the same next node - only if: Shop_Items == On index: 0x43F type: branch - num results: 2 - query: 24 + query: empty bottles parameters: 0x00000001 - next node index: 0x2EA results: - 0x440 - 0x440 @@ -64,59 +58,58 @@ - only if: Shop_Items == On index: 0x445 type: event - event: 3 + event: remove rupees parameters: 0x0000001E - next node index: Kakariko Malo Mart Red Potion Sold Out + next: Kakariko Malo Mart Red Potion Sold Out # Change the flow of buying the wooden shield in Kak Malo Mart to set the sold out flag if shop # items are randomized - only if: Shop_Items == On index: 0x417 type: event - event: 3 + event: remove rupees parameters: 0x00000032 - next node index: Kakariko Malo Mart Wooden Shield Sold Out + next: Kakariko Malo Mart Wooden Shield Sold Out #3: # zel_03.bmg 4: # zel_04.bmg - Castle Town # Patch Charlo to check for 100 rupees instead of 30 - index: 0x34A type: branch - num results: 2 - query: 6 + query: rupees parameters: 0x0064 - next node index: 0x222 + results: + - 0x34E + - 0x342 # Patch Charlo to take 100 rupees from Link instead of 30 - index: 0x34E type: event - event: 41 + event: add donation parameters: 0x00000064 - next node index: 0x226 + next: 0x357 # Patch Charlo to add 100 rupees to his counter instead of 30 - index: 0x357 type: event - event: 3 + event: remove rupees parameters: 0x00000064 - next node index: 0x22B + next: 0x350 # Patch Malo Mart door person to not check for time of day. # Change from branch node that checks time of day to event node # that does nothing. - index: 0x1A5 type: event - event: 42 + event: no-op parameters: 0x00000000 - next node index: 0x10C + next: 0x1A4 # Patch Jovani flow so that we can turn in poe souls before completing MDH - index: 0x832 type: branch - num results: 2 - query: 1 + query: event flag parameters: 0x01C8 - next node index: 0x763 results: - 0x834 # Skip over 0x833 and go directly to 0x834 - 0x84E @@ -126,54 +119,53 @@ # talking to him for the first time with 60 souls - index: 0x844 type: message - inf index: 0x322 - next flow index: 0x851 + message: 0x322 + next: 0x851 # Patch Jovani to check if we've received the 20 reward yet at # the end of the 60 reward sequence. - index: 0x81F type: event - event: 14 + event: set switch parameters: 0x00000063 - next node index: Jovani Check 20 Reward Obtained + next: Jovani Check 20 Reward Obtained # Patch the flow of showing the Doctor the Invoice to call our custom # event which calls offWarashibeItem instead of setWarashibeItem - index: 0x2D7 type: event - event: 47 + event: randomizer remove trade item parameters: 0x00000081 - next node index: 0x1D3 + next: 0x2D8 5: # zel_05.bmg # Patch Yeta flow to always give the map check even if the player has obtained the Ordon Cheese - index: 0x8 type: event - event: 42 + event: no-op parameters: 0x00000000 - next node index: 0x8 + next: 0x6 # Patch Yeta flow to always give the map check even if the player has obtained the Ordon Cheese - index: 0x5 type: event - event: 42 + event: no-op parameters: 0x00000000 - next node index: 0x3 + next: 0xC - # Patch Gor Liggs to not set the flag for the third key shard - # Change event index 17 to event index 42 which does nothing + # Patch Gor Liggs to not set the flag for the third key shard. - index: 0x258 type: event - event: 42 + event: no-op parameters: 0x00000000 - next node index: 0x1AC + next: 0x248 # Patch Cave of Ordeals Great Fairy to skip over the check # for an empty bottle the first time - index: 0x850 type: message - inf index: 0x25B - next flow index: 0x202 + message: 0x25B + next: 0x202 #6: # zel_06.bmg #7: # zel_07.bmg @@ -181,19 +173,18 @@ # Skip over the node which deletes auru's memo from the inventory after showing it to Fyer - index: 0x53 type: message - inf index: 0xA7 - next flow index: 0x54 # Go directly to 0x54 instead of 0x5D + message: 0xA7 + next: 0x54 # Go directly to 0x54 instead of 0x5D # Custom Flow Nodes -9: +custom: # Start of custom midna call tree. Begin by checking if we can change time # of day to see if we show that choice - name: Custom Midna Call Begin + group: 0 type: branch - num results: 2 - query: 54 + query: randomizer change time parameters: 0x0000 - next node index: 0xFFFF results: - Custom Midna Call 3 Choice Select Setup # Can change time of day - Custom Midna Call 2 Choice Select Setup # Can not change time of day @@ -203,27 +194,29 @@ ################################################# - name: Custom Midna Call 2 Choice Select Setup + group: 0 type: event - event: 13 + event: select vertical parameters: 0x00000003 - next node index: Custom Midna Call 2 Choice Intro + next: Custom Midna Call 2 Choice Intro - name: Custom Midna Call 2 Choice Intro + group: 0 type: message - inf index: Custom Midna Call Need Something Text - next flow index: Custom Midna Call 2 Choice + message: Custom Midna Call Need Something Text + next: Custom Midna Call 2 Choice - name: Custom Midna Call 2 Choice + group: 0 type: message - inf index: Custom Midna Call 2 Choice Text - next flow index: Custom Midna Call 2 Choice Branch + message: Custom Midna Call 2 Choice Text + next: Custom Midna Call 2 Choice Branch - name: Custom Midna Call 2 Choice Branch + group: 0 type: branch - num results: 3 - query: 35 + query: select 2 cancel parameters: 0x0000 - next node index: 0xFFFF results: - Custom Midna Call Hints # Hints - Custom Midna Call Return to Spawn Branch # Return to Spawn @@ -234,27 +227,29 @@ ################################################# - name: Custom Midna Call 3 Choice Select Setup + group: 0 type: event - event: 13 + event: select vertical parameters: 0x00000004 - next node index: Custom Midna Call 3 Choice Intro + next: Custom Midna Call 3 Choice Intro - name: Custom Midna Call 3 Choice Intro + group: 0 type: message - inf index: Custom Midna Call Need Something Text - next flow index: Custom Midna Call 3 Choice + message: Custom Midna Call Need Something Text + next: Custom Midna Call 3 Choice - name: Custom Midna Call 3 Choice + group: 0 type: message - inf index: Custom Midna Call 3 Choice Text - next flow index: Custom Midna Call 3 Choice Branch + message: Custom Midna Call 3 Choice Text + next: Custom Midna Call 3 Choice Branch - name: Custom Midna Call 3 Choice Branch + group: 0 type: branch - num results: 4 - query: 36 + query: select 3 cancel parameters: 0x0000 - next node index: 0xFFFF results: - Custom Midna Call Hints # Hints - Custom Midna Call Change Time # Change time of day @@ -266,22 +261,23 @@ ################################################# - name: Custom Midna Call Hints + group: 0 type: message - inf index: Custom Midna Call Hints Text - next flow index: 0xFFFF + message: Custom Midna Call Hints Text + next: 0xFFFF - name: Custom Midna Call Change Time + group: 0 type: event - event: 44 + event: randomizer change time parameters: 0x00000000 - next node index: 0x116 + next: 0xFFFF - name: Custom Midna Call Return to Spawn Branch + group: 0 type: branch - num results: 3 - query: 55 + query: randomizer return to spawn parameters: 0x0000 - next node index: 0xFFFF results: - Return to Spawn Dungeon Choice Select Setup - Return to Spawn Dungeon No Choice Select Setup @@ -292,27 +288,29 @@ ################################################# - name: Return to Spawn Dungeon Choice Select Setup + group: 0 type: event - event: 13 + event: select vertical parameters: 0x00000004 - next node index: Return to Spawn Dungeon Choice Intro + next: Return to Spawn Dungeon Choice Intro - name: Return to Spawn Dungeon Choice Intro + group: 0 type: message - inf index: Return to Spawn Dungeon Intro Text - next flow index: Return to Spawn Dungeon Choice + message: Return to Spawn Dungeon Intro Text + next: Return to Spawn Dungeon Choice - name: Return to Spawn Dungeon Choice + group: 0 type: message - inf index: Return to Spawn Dungeon Choice Text - next flow index: Return to Spawn Dungeon Choice Branch + message: Return to Spawn Dungeon Choice Text + next: Return to Spawn Dungeon Choice Branch - name: Return to Spawn Dungeon Choice Branch + group: 0 type: branch - num results: 4 - query: 36 + query: select 3 cancel parameters: 0x0000 - next node index: 0xFFFF results: - Return to Dungeon Spawn # Dungeon entrance - 0xFFFF # Nevermind @@ -324,27 +322,29 @@ ################################################# - name: Return to Spawn Dungeon No Choice Select Setup + group: 0 type: event - event: 13 + event: select vertical parameters: 0x00000003 - next node index: Return to Spawn Dungeon No Choice Intro + next: Return to Spawn Dungeon No Choice Intro - name: Return to Spawn Dungeon No Choice Intro + group: 0 type: message - inf index: Return to Spawn Dungeon Intro Text - next flow index: Return to Spawn No Dungeon Choice + message: Return to Spawn Dungeon Intro Text + next: Return to Spawn Dungeon No Choice - name: Return to Spawn Dungeon No Choice + group: 0 type: message - inf index: Return to Spawn Dungeon No Choice Text - next flow index: Return to Spawn Dungeon No Choice Branch + message: Return to Spawn Dungeon No Choice Text + next: Return to Spawn Dungeon No Choice Branch - name: Return to Spawn Dungeon No Choice Branch + group: 0 type: branch - num results: 3 - query: 35 + query: select 2 cancel parameters: 0x0000 - next node index: 0xFFFF results: - 0xFFFF # Nevermind - Return to Base Spawn # Spawn @@ -354,40 +354,43 @@ # RETURN TO SPAWN EVENTS # ################################################# - # Use custom event index 45 to setup returning to spawn. A non-zero - # parameter value indicates to try and use a place override if one - # exists for the current stage + # A non-zero parameter uses the current stage's place override when one exists. - name: Return to Dungeon Spawn + group: 0 type: event - event: 45 + event: randomizer return to spawn parameters: 0x00000001 - next node index: 0x116 + next: 0xFFFF - # Use custom event index 45 to setup returning to spawn. A parameter - # value of zero indicates to always return to the starting spawn + # A zero parameter always returns to the starting spawn. - name: Return to Base Spawn + group: 0 type: event - event: 45 + event: randomizer return to spawn parameters: 0x00000000 - next node index: 0x116 + next: 0xFFFF ################################################# # KAKARIKO MALO MART # ################################################# - # Run event 24 which sets the shop sold out flag + # Set the shop sold-out flag. - name: Kakariko Malo Mart Red Potion Sold Out + only if: Shop_Items == On + group: 2 type: event - event: 24 + event: shop sold out parameters: 0x00000000 - next node index: 0x2F2 + next: 0x449 - # Run event 24 which sets the shop sold out flag + # Set the shop sold-out flag. - name: Kakariko Malo Mart Wooden Shield Sold Out + only if: Shop_Items == On + group: 2 type: event - event: 24 + event: shop sold out parameters: 0x00000000 - next node index: 0x2D0 + next: 0x41C ################################################# # JOVANI # @@ -396,225 +399,191 @@ # Check to see if we've obtained the 20 Poe Soul Reward. # If we haven't then branch to giving the reward - name: Jovani Check 20 Reward Obtained + group: 4 type: branch - num results: 2 - query: 1 + query: event flag parameters: 0x0272 - next node index: 0xFFFF results: - 0xFFFF # Yes, we've obtained the reward, do nothing - Jovani Give 20 Reward - name: Jovani Give 20 Reward + group: 4 type: event - event: 8 + event: start event parameters: 0x00010075 - next node index: 0x77E + next: 0x851 ################################################# # HINT SIGNS # ################################################# - name: Ordon Hint Sign - index: 21100 type: message - inf index: Ordon Hint Sign Text - next flow index: 0xFFFF + message: Ordon Hint Sign Text + next: 0xFFFF - name: South Faron Woods Hint Sign - index: 21101 type: message - inf index: South Faron Woods Hint Sign Text - next flow index: 0xFFFF + message: South Faron Woods Hint Sign Text + next: 0xFFFF - name: Sacred Grove Hint Sign - index: 21102 type: message - inf index: Sacred Grove Hint Sign Text - next flow index: 0xFFFF + message: Sacred Grove Hint Sign Text + next: 0xFFFF - name: Faron Field Hint Sign - index: 21103 type: message - inf index: Faron Field Hint Sign Text - next flow index: 0xFFFF + message: Faron Field Hint Sign Text + next: 0xFFFF - name: Kakariko Gorge Hint Sign - index: 21104 type: message - inf index: Kakariko Gorge Hint Sign Text - next flow index: 0xFFFF + message: Kakariko Gorge Hint Sign Text + next: 0xFFFF - name: Kakariko Village Hint Sign - index: 21105 type: message - inf index: Kakariko Village Hint Sign Text - next flow index: 0xFFFF + message: Kakariko Village Hint Sign Text + next: 0xFFFF - name: Kakariko Graveyard Hint Sign - index: 21106 type: message - inf index: Kakariko Graveyard Hint Sign Text - next flow index: 0xFFFF + message: Kakariko Graveyard Hint Sign Text + next: 0xFFFF - name: Eldin Field Hint Sign - index: 21107 type: message - inf index: Eldin Field Hint Sign Text - next flow index: 0xFFFF + message: Eldin Field Hint Sign Text + next: 0xFFFF - name: North Eldin Field Hint Sign - index: 21108 type: message - inf index: North Eldin Field Hint Sign Text - next flow index: 0xFFFF + message: North Eldin Field Hint Sign Text + next: 0xFFFF - name: Hidden Village Hint Sign - index: 21109 type: message - inf index: Hidden Village Hint Sign Text - next flow index: 0xFFFF + message: Hidden Village Hint Sign Text + next: 0xFFFF - name: Lanayru Field Hint Sign - index: 21110 type: message - inf index: Lanayru Field Hint Sign Text - next flow index: 0xFFFF + message: Lanayru Field Hint Sign Text + next: 0xFFFF - name: Beside Castle Town Hint Sign - index: 21111 type: message - inf index: Beside Castle Town Hint Sign Text - next flow index: 0xFFFF + message: Beside Castle Town Hint Sign Text + next: 0xFFFF - name: Castle Town Center Hint Sign - index: 21112 type: message - inf index: Castle Town Center Hint Sign Text - next flow index: 0xFFFF + message: Castle Town Center Hint Sign Text + next: 0xFFFF - name: Outside South Castle Town Hint Sign - index: 21113 type: message - inf index: Outside South Castle Town Hint Sign Text - next flow index: 0xFFFF + message: Outside South Castle Town Hint Sign Text + next: 0xFFFF - name: Lake Hylia Bridge Hint Sign - index: 21114 type: message - inf index: Lake Hylia Bridge Hint Sign Text - next flow index: 0xFFFF + message: Lake Hylia Bridge Hint Sign Text + next: 0xFFFF - name: Lake Hylia Hint Sign - index: 21115 type: message - inf index: Lake Hylia Hint Sign Text - next flow index: 0xFFFF + message: Lake Hylia Hint Sign Text + next: 0xFFFF - name: Lanayru Spring Hint Sign - index: 21116 type: message - inf index: Lanayru Spring Hint Sign Text - next flow index: 0xFFFF + message: Lanayru Spring Hint Sign Text + next: 0xFFFF - name: Lake Lantern Cave Hint Sign - index: 21117 type: message - inf index: Lake Lantern Cave Hint Sign Text - next flow index: 0xFFFF + message: Lake Lantern Cave Hint Sign Text + next: 0xFFFF - name: Fishing Hole Hint Sign - index: 21118 type: message - inf index: Fishing Hole Hint Sign Text - next flow index: 0xFFFF + message: Fishing Hole Hint Sign Text + next: 0xFFFF - name: Zoras Domain Hint Sign - index: 21119 type: message - inf index: Zoras Domain Hint Sign Text - next flow index: 0xFFFF + message: Zoras Domain Hint Sign Text + next: 0xFFFF - name: Snowpeak Hint Sign - index: 21120 type: message - inf index: Snowpeak Hint Sign Text - next flow index: 0xFFFF + message: Snowpeak Hint Sign Text + next: 0xFFFF - name: Gerudo Desert Hint Sign - index: 21121 type: message - inf index: Gerudo Desert Hint Sign Text - next flow index: 0xFFFF + message: Gerudo Desert Hint Sign Text + next: 0xFFFF - name: Bulblin Camp Hint Sign - index: 21122 type: message - inf index: Bulblin Camp Hint Sign Text - next flow index: 0xFFFF + message: Bulblin Camp Hint Sign Text + next: 0xFFFF - name: Forest Temple Hint Sign - index: 21123 type: message - inf index: Forest Temple Hint Sign Text - next flow index: 0xFFFF + message: Forest Temple Hint Sign Text + next: 0xFFFF - name: Goron Mines Hint Sign - index: 21124 type: message - inf index: Goron Mines Hint Sign Text - next flow index: 0xFFFF + message: Goron Mines Hint Sign Text + next: 0xFFFF - name: Lakebed Temple Hint Sign - index: 21125 type: message - inf index: Lakebed Temple Hint Sign Text - next flow index: 0xFFFF + message: Lakebed Temple Hint Sign Text + next: 0xFFFF - name: Arbiters Grounds Hint Sign - index: 21126 type: message - inf index: Arbiters Grounds Hint Sign Text - next flow index: 0xFFFF + message: Arbiters Grounds Hint Sign Text + next: 0xFFFF - name: Snowpeak Ruins Hint Sign - index: 21127 type: message - inf index: Snowpeak Ruins Hint Sign Text - next flow index: 0xFFFF + message: Snowpeak Ruins Hint Sign Text + next: 0xFFFF - name: Temple of Time First Hint Sign - index: 21128 type: message - inf index: Temple of Time First Hint Sign Text - next flow index: 0xFFFF + message: Temple of Time First Hint Sign Text + next: 0xFFFF - name: Temple of Time Second Hint Sign - index: 21129 type: message - inf index: Temple of Time Second Hint Sign Text - next flow index: 0xFFFF + message: Temple of Time Second Hint Sign Text + next: 0xFFFF - name: City in the Sky Hint Sign - index: 21130 type: message - inf index: City in the Sky Hint Sign Text - next flow index: 0xFFFF + message: City in the Sky Hint Sign Text + next: 0xFFFF - name: Palace of Twilight Hint Sign - index: 21131 type: message - inf index: Palace of Twilight Hint Sign Text - next flow index: 0xFFFF + message: Palace of Twilight Hint Sign Text + next: 0xFFFF - name: Hyrule Castle Hint Sign - index: 21132 type: message - inf index: Hyrule Castle Hint Sign Text - next flow index: 0xFFFF + message: Hyrule Castle Hint Sign Text + next: 0xFFFF - name: Cave of Ordeals Hint Sign - index: 21133 type: message - inf index: Cave of Ordeals Hint Sign Text - next flow index: 0xFFFF + message: Cave of Ordeals Hint Sign Text + next: 0xFFFF diff --git a/mods/randomizer/generator/data/object_patches.yaml b/mods/randomizer/generator/data/object_patches.yaml index 32258b63ec..74da6574a9 100644 --- a/mods/randomizer/generator/data/object_patches.yaml +++ b/mods/randomizer/generator/data/object_patches.yaml @@ -12,8 +12,9 @@ D_MN01: x: -4765.38037 y: -1.21 z: 272.192261 + flow: Lakebed Temple Hint Sign angle: - x: 21125 # Flow node id + x: 0 y: 0x46B9 z: 0x0000 set id: 0xFFFF @@ -32,8 +33,9 @@ D_MN04: x: 11394.1855 y: 2878.65 z: -17913.05 + flow: Goron Mines Hint Sign angle: - x: 21124 # Flow node id + x: 0 y: 0xD556 z: 0x0000 set id: 0xFFFF @@ -52,8 +54,9 @@ D_MN05: x: -1972.11682 y: 3150 z: 7610.9751 + flow: Forest Temple Hint Sign angle: - x: 21123 # Flow node id + x: 0 y: 0x6000 z: 0x0000 set id: 0xFFFF @@ -72,8 +75,9 @@ D_MN06: x: -618.9335 y: 725.0 z: 3112.2 + flow: Temple of Time First Hint Sign angle: - x: 21128 # Flow node id + x: 0 y: 0x8000 z: 0x0000 set id: 0xFFFF @@ -90,8 +94,9 @@ D_MN06: x: -3885.2157 y: 4450.0 z: -6353.38135 + flow: Temple of Time Second Hint Sign angle: - x: 21129 # Flow node id + x: 0 y: 0x2000 z: 0x0000 set id: 0xFFFF @@ -155,8 +160,9 @@ D_MN07: x: 3376.54 y: 0.0 z: -12709.0352 + flow: City in the Sky Hint Sign angle: - x: 21130 # Flow node id + x: 0 y: 0xC846 z: 0x0000 set id: 0xFFFF @@ -237,8 +243,9 @@ D_MN08: x: 679.0 y: -200.0 z: 9311.0 + flow: Palace of Twilight Hint Sign angle: - x: 21131 # Flow node id + x: 0 y: 0x0000 z: 0x0000 set id: 0xFFFF @@ -257,8 +264,9 @@ D_MN09: x: 0.0 y: 25.0 z: 11625.0 + flow: Hyrule Castle Hint Sign angle: - x: 21132 # Flow node id + x: 0 y: 0x0000 z: 0x0000 set id: 0xFFFF @@ -277,8 +285,9 @@ D_MN10: x: -349.4044 y: 450.0 z: -2876.90771 + flow: Arbiters Grounds Hint Sign angle: - x: 21126 # Flow node id + x: 0 y: 0x0000 z: 0x0000 set id: 0xFFFF @@ -297,8 +306,9 @@ D_MN11: x: -530.0 y: 0.0 z: -669.69 + flow: Snowpeak Ruins Hint Sign angle: - x: 21127 # Flow node id + x: 0 y: 0x0000 z: 0x0000 set id: 0xFFFF @@ -317,8 +327,9 @@ D_SB01: x: -1191.42 y: 1100.0 z: -260.65 + flow: Cave of Ordeals Hint Sign angle: - x: 21133 # Flow node id + x: 0 y: 0xE483 z: 0x0000 set id: 0xFFFF @@ -337,8 +348,9 @@ D_SB03: x: -2897.38672 y: -1636.68994 z: -17674.8691 + flow: Lake Lantern Cave Hint Sign angle: - x: 21117 # Flow node id + x: 0 y: 0x3E21 z: 0x0000 set id: 0xFFFF @@ -533,8 +545,9 @@ F_SP103: x: 687.89 y: 800.0 z: -1424.16 + flow: Ordon Hint Sign angle: - x: 21100 # Flow node id + x: 0 y: 0xA019 z: 0x0000 set id: 0xFFFF @@ -586,8 +599,9 @@ F_SP108: x: -12423.8467 y: 273.277985 z: -11518.958 + flow: South Faron Woods Hint Sign angle: - x: 21101 # Flow node id + x: 0 y: 0x9228 z: 0x0000 set id: 0xFFFF @@ -721,8 +735,9 @@ F_SP109: x: -3347.52734 y: 2999.16138 z: -2865.99341 + flow: Kakariko Village Hint Sign angle: - x: 21105 # Flow node id + x: 0 y: 0x0000 z: 0x0000 set id: 0xFFFF @@ -786,8 +801,9 @@ F_SP111: x: 21765.9863 y: 500.0 z: -62.247 + flow: Kakariko Graveyard Hint Sign angle: - x: 21106 # Flow node id + x: 0 y: 0xC000 z: 0x0000 set id: 0xFFFF @@ -1217,8 +1233,9 @@ F_SP113: x: -2964.84839 y: -2500.0 z: 17146.2676 + flow: Zoras Domain Hint Sign angle: - x: 21119 # Flow node id + x: 0 y: 0xAF09 z: 0x0000 set id: 0xFFFF @@ -1240,8 +1257,9 @@ F_SP114: x: 15931.9238 y: -14389.4912 z: -17388.1992 + flow: Snowpeak Hint Sign angle: - x: 21120 # Flow node id + x: 0 y: 0x2C5A z: 0x0000 set id: 0xFFFF @@ -1854,8 +1872,9 @@ F_SP115: x: -109203.461 y: -7220.0 z: 33083.7344 + flow: Lake Hylia Hint Sign angle: - x: 21115 # Flow node id + x: 0 y: 0x64B5 z: 0x0000 set id: 0xFFFF @@ -2030,8 +2049,9 @@ F_SP115: x: -309.997833 y: -1614.82178 z: 157.970795 + flow: Lanayru Spring Hint Sign angle: - x: 21116 # Flow node id + x: 0 y: 0x8000 z: 0x0000 set id: 0xFFFF @@ -2071,8 +2091,9 @@ F_SP116: x: 0.0 y: -200.0 z: 835.0 + flow: Castle Town Center Hint Sign angle: - x: 21112 # Flow node id + x: 0 y: 0x0000 z: 0x0000 set id: 0xFFFF @@ -2229,8 +2250,9 @@ F_SP117: x: -1543.0 y: 1725.0 z: 7964.85498 + flow: Sacred Grove Hint Sign angle: - x: 21102 # Flow node id + x: 0 y: 0x4000 z: 0x0000 set id: 0xFFFF @@ -2377,8 +2399,9 @@ F_SP118: x: -568.556152 y: 260.0 z: -3969.31 + flow: Bulblin Camp Hint Sign angle: - x: 21122 # Flow node id + x: 0 y: 0x0000 z: 0x0000 set id: 0xFFFF @@ -2531,8 +2554,9 @@ F_SP121: x: -12433.2744 y: -1075.2218 z: 20885.7129 + flow: Eldin Field Hint Sign angle: - x: 21107 # Flow node id + x: 0 y: 0x8000 z: 0x0000 set id: 0xFFFF @@ -2769,8 +2793,9 @@ F_SP121: x: -10116.0215 y: -4923.46191 z: 43064.4375 + flow: Kakariko Gorge Hint Sign angle: - x: 21104 # Flow node id + x: 0 y: 0x0789 z: 0x0000 set id: 0xFFFF @@ -2862,8 +2887,9 @@ F_SP121: x: -46039.4922 y: -9250.0 z: 81859.2891 + flow: Faron Field Hint Sign angle: - x: 21103 # Flow node id + x: 0 y: 0x8000 z: 0x0000 set id: 0xFFFF @@ -2930,8 +2956,9 @@ F_SP121: x: 18468.8418 y: 1580.0 z: -63560.9531 + flow: North Eldin Field Hint Sign angle: - x: 21108 # Flow node id + x: 0 y: 0xCDC6 z: 0x0000 set id: 0xFFFF @@ -3177,8 +3204,9 @@ F_SP121: x: -64943.7422 y: -1359.66711 z: -31897.8828 + flow: Lanayru Field Hint Sign angle: - x: 21110 # Flow node id + x: 0 y: 0xE223 z: 0x0000 set id: 0xFFFF @@ -3403,8 +3431,9 @@ F_SP121: x: -94678.8672 y: -3900.0 z: 18410.543 + flow: Lake Hylia Bridge Hint Sign angle: - x: 21114 # Flow node id + x: 0 y: 0x0000 z: 0x0000 set id: 0xFFFF @@ -3727,8 +3756,9 @@ F_SP122: x: -80699.5 y: -765.58374 z: 1903.61462 + flow: Beside Castle Town Hint Sign angle: - x: 21111 # Flow node id + x: 0 y: 0x25B7 z: 0x0000 set id: 0xFFFF @@ -4012,8 +4042,9 @@ F_SP122: x: -51500.0 y: -5500.0 z: 27368.3086 + flow: Outside South Castle Town Hint Sign angle: - x: 21113 # Flow node id + x: 0 y: 0x0000 z: 0x0000 set id: 0xFFFF @@ -4096,8 +4127,9 @@ F_SP124: x: 20356.23 y: 556.7 z: 38694.8047 + flow: Gerudo Desert Hint Sign angle: - x: 21121 # Flow node id + x: 0 y: 0x0099 z: 0x0000 set id: 0xFFFF @@ -4692,8 +4724,9 @@ F_SP127: x: -2924.74585 y: 35.0 z: 8386.28906 + flow: Fishing Hole Hint Sign angle: - x: 21118 # Flow node id + x: 0 y: 0x8000 z: 0x0000 set id: 0xFFFF @@ -4716,8 +4749,9 @@ F_SP128: x: 5161.03 y: 0.0 z: -5264.33 + flow: Hidden Village Hint Sign angle: - x: 21109 # Flow node id + x: 0 y: 0x0000 z: 0x0000 set id: 0xFFFF diff --git a/mods/randomizer/generator/data/text/text_overrides.yaml b/mods/randomizer/generator/data/text/text_overrides.yaml index 4cb6ae019f..53b7980abb 100644 --- a/mods/randomizer/generator/data/text/text_overrides.yaml +++ b/mods/randomizer/generator/data/text/text_overrides.yaml @@ -401,25 +401,57 @@ # Completely Custom Text IDs - Name: Custom Midna Call Need Something Text - Attributes: 0x07D30000150D0100FF000D001F1F0400 + Style: + Speaker: 0x15 + Box Kind: 0x0D + Draw Type: 1 + Speaker Mood: 0x0D + Talk Animation: 0x1F + Face Animation: 0x1F - Name: Custom Midna Call 3 Choice Text - Attributes: 0x07F8000015000000FF00000000000400 + Style: + Speaker: 0x15 + Talk Animation: 0 + Face Animation: 0 - Name: Custom Midna Call 2 Choice Text - Attributes: 0x07F8000015000000FF00000000000400 + Style: + Speaker: 0x15 + Talk Animation: 0 + Face Animation: 0 - Name: Custom Midna Call Hints Text - Attributes: 0x07D30000150D0100FF000D001F1F0400 + Style: + Speaker: 0x15 + Box Kind: 0x0D + Draw Type: 1 + Speaker Mood: 0x0D + Talk Animation: 0x1F + Face Animation: 0x1F - Name: Return to Spawn Dungeon Intro Text - Attributes: 0x07D30000150D0100FF000D001F1F0400 + Style: + Speaker: 0x15 + Box Kind: 0x0D + Draw Type: 1 + Speaker Mood: 0x0D + Talk Animation: 0x1F + Face Animation: 0x1F - Name: Return to Spawn Dungeon Choice Text - Attributes: 0x07F60000150D0000FF00000000000400 + Style: + Speaker: 0x15 + Box Kind: 0x0D + Talk Animation: 0 + Face Animation: 0 - Name: Return to Spawn Dungeon No Choice Text - Attributes: 0x07F60000150D0000FF00000000000400 + Style: + Speaker: 0x15 + Box Kind: 0x0D + Talk Animation: 0 + Face Animation: 0 - Name: Ordon Hint Sign Text @@ -487,4 +519,4 @@ - Name: Hyrule Castle Hint Sign Text -- Name: Cave of Ordeals Hint Sign Text \ No newline at end of file +- Name: Cave of Ordeals Hint Sign Text diff --git a/mods/randomizer/src/custom_flow_ids.hpp b/mods/randomizer/src/custom_flow_ids.hpp deleted file mode 100644 index faa355d899..0000000000 --- a/mods/randomizer/src/custom_flow_ids.hpp +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -#include "dolphin/types.h" - -inline constexpr u16 BASE_CUSTOM_MSG_AND_FLOW_ID = 21000; -inline constexpr u16 CUSTOM_BMG_GROUP = 9; diff --git a/mods/randomizer/src/messages.cpp b/mods/randomizer/src/messages.cpp index febf217b0c..61c13e01ca 100644 --- a/mods/randomizer/src/messages.cpp +++ b/mods/randomizer/src/messages.cpp @@ -1,6 +1,5 @@ #include "messages.hpp" -#include "custom_flow_ids.hpp" #include "randomizer_context.hpp" #include "stages.h" #include "tools.h" @@ -12,21 +11,17 @@ #include "d/d_save.h" #include "d/d_stage.h" -#include #include #include #include -#include #include #include #include -#include #include #include #include -#include #include #include @@ -34,17 +29,11 @@ namespace randomizer::messages { namespace { constexpr uint16_t kMessageGroupCount = 9; -constexpr uint16_t kLegacyQueryEventFlag = 53; -constexpr uint16_t kLegacyQueryChangeTime = 54; -constexpr uint16_t kLegacyQueryReturnToSpawn = 55; -constexpr uint8_t kLegacyEventNoOp = 43; -constexpr uint8_t kLegacyEventChangeTime = 44; -constexpr uint8_t kLegacyEventReturnToSpawn = 45; -constexpr uint8_t kLegacyEventSetTrackerFlag = 46; -constexpr uint8_t kLegacyEventRemoveTradeItem = 47; +constexpr uint32_t kPoeSoulGetMessage = 325; +constexpr uint32_t kSkyCharacterGetMessage = 335; -using LegacyIdMap = std::unordered_map; -using MessageIdMaps = std::array; +using MessageIdMaps = std::array, kMessageGroupCount>; +using NodeIdMaps = std::array, kMessageGroupCount>; mods::flow::Query s_eventFlagQuery; mods::flow::Query s_changeTimeQuery; @@ -61,6 +50,15 @@ uint32_t read_parameter(const uint8_t parameters[4]) { static_cast(parameters[2]) << 8 | parameters[3]; } +std::array parameter_bytes(uint32_t parameter) { + return { + static_cast(parameter >> 24), + static_cast(parameter >> 16), + static_cast(parameter >> 8), + static_cast(parameter), + }; +} + uint16_t query_event_flag(ModContext*, const FlowQueryContext* query, void*) { if (query == nullptr || query->parameter >= std::size(dSv_event_flag_c::saveBitLabels)) { return 0; @@ -120,65 +118,46 @@ std::vector encoded_text(const std::string& text) { return result; } -MessageEntryData default_message_entry() { - MessageEntryData entry{}; - entry.bytes[8] = 0x24; - entry.bytes[12] = 0xff; - entry.bytes[16] = 0x02; - entry.bytes[17] = 0x03; - entry.bytes[18] = 0x04; - return entry; -} - -MessageEntryData message_entry(const RandomizerContext& context, uint32_t legacyKey) { - MessageEntryData entry = default_message_entry(); - const auto found = context.mAttributeOverrides.find(legacyKey); - if (found != context.mAttributeOverrides.end()) { - std::memcpy(entry.bytes, found->second.data(), sizeof(entry.bytes)); - std::fill_n(entry.bytes, 6, uint8_t{}); - } - return entry; -} - -std::vector text_keys(const RandomizerContext& context, uint16_t group) { - std::unordered_set unique; - for (const auto& [language, overrides] : context.mTextOverrides) { - for (const auto& [key, text] : overrides) { - if (key >> 16 == group) { - unique.insert(key); - } - } - } - std::vector result{unique.begin(), unique.end()}; - std::ranges::sort(result); - return result; +mods::flow::MessageStyle message_style(const RandomizerContext::MessageStyleData& source) { + return mods::flow::MessageStyle{} + .event_label_id(source.eventLabelId) + .speaker(source.speaker) + .box_kind(static_cast(source.boxKind)) + .draw_type(static_cast(source.drawType)) + .box_position(static_cast(source.boxPosition)) + .line_alignment(source.lineAlignment) + .speaker_mood(source.speakerMood) + .camera_attr(source.cameraAttr) + .talk_anim(source.talkAnim) + .face_anim(source.faceAnim) + .trailing_data(source.trailingData); } ModResult register_custom_messages(const RandomizerContext& context, MessageIdMaps& messageIds) { - const auto keys = text_keys(context, CUSTOM_BMG_GROUP); - for (uint16_t group = 0; group < kMessageGroupCount; ++group) { - for (const uint32_t key : keys) { - std::vector variants; - for (const auto& [language, overrides] : context.mTextOverrides) { - const auto found = overrides.find(key); - if (found == overrides.end() || language < MESSAGE_LANGUAGE_ENGLISH || - language > MESSAGE_LANGUAGE_ITALIAN) - { - continue; - } - variants.emplace_back(static_cast(language), - message_entry(context, key), encoded_text(found->second)); - } - if (variants.empty()) { - return MOD_INVALID_ARGUMENT; - } - auto message = mods::flow::register_message(group, variants); - if (!message) { - return message.result(); - } - messageIds[group].emplace(static_cast(key), message.id()); - s_messages.push_back(std::move(message)); + for (const auto& definition : context.mCustomMessages) { + if (definition.group >= kMessageGroupCount || definition.name.empty() || + messageIds[definition.group].contains(definition.name)) + { + return MOD_INVALID_ARGUMENT; } + const auto style = message_style(definition.style); + std::vector variants; + for (const auto& [language, text] : definition.text) { + if (language < MESSAGE_LANGUAGE_ENGLISH || language > MESSAGE_LANGUAGE_ITALIAN) { + continue; + } + variants.emplace_back(static_cast(language), + style.data(), encoded_text(text)); + } + if (variants.empty()) { + return MOD_INVALID_ARGUMENT; + } + auto message = mods::flow::register_message(definition.group, variants); + if (!message) { + return message.result(); + } + messageIds[definition.group].emplace(definition.name, message.id()); + s_messages.push_back(std::move(message)); } return MOD_OK; } @@ -200,14 +179,12 @@ bool formatted_override( } uint32_t value = 0; - // For item counts, execItemGet hasn't run yet, so add one to the count + // Message resolution runs before the acquisition updates these counters. switch (key) { - case 325: // Group 0, id 325 - // Poe Soul get item text + case kPoeSoulGetMessage: value = dComIfGs_getPohSpiritNum() + 1; break; - case 335: // Group 0, id 335 - // Sky book characters get item text + case kSkyCharacterGetMessage: value = getAncientDocumentNum() + 1; break; default: @@ -222,338 +199,318 @@ bool formatted_override( } ModResult register_native_overrides(const RandomizerContext& context) { - for (uint16_t group = 0; group < kMessageGroupCount; ++group) { - for (const uint32_t key : text_keys(context, group)) { - for (const auto& [language, overrides] : context.mTextOverrides) { - const auto found = overrides.find(key); - if (found == overrides.end() || language < MESSAGE_LANGUAGE_ENGLISH || - language > MESSAGE_LANGUAGE_ITALIAN) - { - continue; - } - mods::flow::MessageOverride message; - if (key == 325 || key == 335) { - message = mods::flow::override_message_fn(group, static_cast(key), - static_cast(language), formatted_override); - } else { - const auto text = encoded_text(found->second); - message = mods::flow::override_message(group, static_cast(key), - static_cast(language), std::span{text}); - } - if (!message) { - return message.result(); - } - s_overrides.push_back(std::move(message)); + for (const auto& [language, overrides] : context.mTextOverrides) { + if (language < MESSAGE_LANGUAGE_ENGLISH || language > MESSAGE_LANGUAGE_ITALIAN) { + continue; + } + for (const auto& [key, value] : overrides) { + const uint16_t group = key >> 16; + const uint16_t messageId = static_cast(key); + mods::flow::MessageOverride message; + if (key == kPoeSoulGetMessage || key == kSkyCharacterGetMessage) { + message = mods::flow::override_message_fn(group, messageId, + static_cast(language), formatted_override); + } else { + const auto text = encoded_text(value); + message = mods::flow::override_message( + group, messageId, static_cast(language), std::span{text}); } + if (!message) { + return message.result(); + } + s_overrides.push_back(std::move(message)); } } return MOD_OK; } -std::vector custom_node_ids(const RandomizerContext& context) { - std::vector result; - for (const auto& [key, node] : context.mFlowPatches) { - if (key >> 16 == CUSTOM_BMG_GROUP) { - result.push_back(static_cast(key)); - } +bool resolve_query(const std::string& name, FlowQueryId& out) { + static const std::unordered_map builtins{ + {"event flag", FLOW_QUERY_EVENT_FLAG}, + {"rupees", FLOW_QUERY_RUPEES}, + {"item owned", FLOW_QUERY_ITEM_OWNED}, + {"empty bottles", FLOW_QUERY_EMPTY_BOTTLES}, + {"select 2 cancel", FLOW_QUERY_SELECT_2_CANCEL}, + {"select 3 cancel", FLOW_QUERY_SELECT_3_CANCEL}, + }; + if (const auto found = builtins.find(name); found != builtins.end()) { + out = found->second; + return true; } - std::ranges::sort(result); - return result; + if (name == "randomizer event flag") { + out = s_eventFlagQuery.id(); + } else if (name == "randomizer change time") { + out = s_changeTimeQuery.id(); + } else if (name == "randomizer return to spawn") { + out = s_returnToSpawnQuery.id(); + } else { + return false; + } + return true; } -ModResult allocate_shared_node_ids(const std::array& handles, - size_t count, std::array, kMessageGroupCount>& allocated, - LegacyIdMap& nodeIds, const std::vector& legacyIds) { - if (count == 0) { - return MOD_OK; +bool resolve_event(const std::string& name, FlowEventId& out) { + static const std::unordered_map builtins{ + {"remove rupees", FLOW_EVENT_REMOVE_RUPEES}, + {"start event", FLOW_EVENT_START_EVENT}, + {"select vertical", FLOW_EVENT_SELECT_VERTICAL}, + {"set switch", FLOW_EVENT_SET_SWITCH}, + {"shop sold out", FLOW_EVENT_SHOP_SOLD_OUT}, + {"add donation", FLOW_EVENT_ADD_DONATION}, + {"no-op", FLOW_EVENT_UNUSED_42}, + }; + if (const auto found = builtins.find(name); found != builtins.end()) { + out = found->second; + return true; } - - std::array, kMessageGroupCount> allocatedSets; - std::vector common; - while (common.size() < count) { - for (size_t group = 0; group < handles.size(); ++group) { - uint16_t id = 0; - const ModResult result = svc_flow->allocate_node(mod_ctx, handles[group], &id); - if (result != MOD_OK) { - return result; - } - allocated[group].push_back(id); - allocatedSets[group].insert(id); - } - - common.clear(); - for (const uint16_t candidate : allocatedSets.front()) { - const bool present = std::ranges::all_of( - allocatedSets, [candidate](const auto& ids) { return ids.contains(candidate); }); - if (present) { - common.push_back(candidate); - } - } + if (name == "randomizer change time") { + out = s_changeTimeEvent.id(); + } else if (name == "randomizer return to spawn") { + out = s_returnToSpawnEvent.id(); + } else if (name == "randomizer remove trade item") { + out = s_removeTradeItemEvent.id(); + } else { + return false; } - - std::ranges::sort(common); - for (size_t i = 0; i < legacyIds.size(); ++i) { - nodeIds.emplace(legacyIds[i], common[i]); - } - return MOD_OK; + return true; } -ModResult remap_target(uint16_t legacy, const LegacyIdMap& nodeIds, uint16_t& out) { - if (legacy == mods::flow::kEnd || legacy < BASE_CUSTOM_MSG_AND_FLOW_ID) { - out = legacy; - return MOD_OK; +bool resolve_reference(const RandomizerContext::FlowReference& reference, uint16_t group, + const NodeIdMaps& nodeIds, uint16_t& out) { + if (reference.nativeId.has_value()) { + out = reference.nativeId.value(); + return true; } - const auto found = nodeIds.find(legacy); - if (found == nodeIds.end()) { - return MOD_INVALID_ARGUMENT; + const auto found = nodeIds[group].find(reference.name); + if (found == nodeIds[group].end()) { + return false; } out = found->second; - return MOD_OK; + return true; } -ModResult remap_query(FlowNodeData& node) { - const uint16_t legacy = mods::read_bits(node.bytes + 2); - FlowQueryId query = legacy; - switch (legacy) { - case kLegacyQueryEventFlag: - query = s_eventFlagQuery.id(); - break; - case kLegacyQueryChangeTime: - query = s_changeTimeQuery.id(); - break; - case kLegacyQueryReturnToSpawn: - query = s_returnToSpawnQuery.id(); - break; - default: - if (legacy >= FLOW_QUERY_BUILTIN_COUNT) { +bool resolve_message(const RandomizerContext::FlowReference& reference, uint16_t group, + const MessageIdMaps& messageIds, uint16_t& out) { + if (reference.nativeId.has_value()) { + out = reference.nativeId.value(); + return true; + } + const auto found = messageIds[group].find(reference.name); + if (found == messageIds[group].end()) { + return false; + } + out = found->second; + return true; +} + +ModResult add_custom_node(const RandomizerContext::FlowNode& flow, + const MessageIdMaps& messageIds, mods::flow::GraphBuilder& builder, + mods::flow::NodeRef& out) { + if (flow.type == RandomizerContext::FlowNodeType::MESSAGE) { + uint16_t message = 0; + if (!resolve_message(flow.message, flow.group, messageIds, message)) { return MOD_INVALID_ARGUMENT; } - break; - } - mods::write_bits(node.bytes + 2, query); - return MOD_OK; -} - -ModResult remap_event(FlowNodeData& node) { - switch (node.bytes[1]) { - case kLegacyEventNoOp: - node.bytes[1] = FLOW_EVENT_UNUSED_42; - break; - case kLegacyEventChangeTime: - node.bytes[1] = s_changeTimeEvent.id(); - break; - case kLegacyEventReturnToSpawn: - node.bytes[1] = s_returnToSpawnEvent.id(); - break; - case kLegacyEventSetTrackerFlag: - // Older seed files may contain the retired pre-grant tracker event. - node.bytes[1] = FLOW_EVENT_UNUSED_42; - break; - case kLegacyEventRemoveTradeItem: - node.bytes[1] = s_removeTradeItemEvent.id(); - break; - default: - if (node.bytes[1] >= FLOW_EVENT_BUILTIN_COUNT) { - return MOD_INVALID_ARGUMENT; - } - break; - } - return MOD_OK; -} - -FlowNodeData raw_node(uint64_t value) { - FlowNodeData node{}; - std::memcpy(node.bytes, &value, sizeof(node.bytes)); - return node; -} - -ModResult remap_node(const RandomizerContext& context, uint32_t legacyKey, uint16_t group, - FlowGraphHandle handle, const LegacyIdMap& nodeIds, const MessageIdMaps& messageIds, - FlowNodeData& node) { - switch (node.bytes[0]) { - case 1: { - const uint16_t legacyMessage = mods::read_bits(node.bytes + 2); - if (legacyMessage >= BASE_CUSTOM_MSG_AND_FLOW_ID) { - const auto found = messageIds[group].find(legacyMessage); - if (found == messageIds[group].end()) { - return MOD_INVALID_ARGUMENT; - } - mods::write_bits(node.bytes + 2, found->second); - } - uint16_t target = 0; - const ModResult result = - remap_target(mods::read_bits(node.bytes + 4), nodeIds, target); - if (result != MOD_OK) { - return result; - } - mods::write_bits(node.bytes + 4, target); + out = builder.add_message(message); return MOD_OK; } - case 2: { - ModResult result = remap_query(node); - if (result != MOD_OK) { - return result; - } - const auto overrides = context.mFlowPatchesBranchOverrides.find(legacyKey); - if (overrides == context.mFlowPatchesBranchOverrides.end()) { - return MOD_OK; - } - if (overrides->second.empty() || overrides->second.size() != node.bytes[1] || - overrides->second.size() > std::numeric_limits::max()) - { + if (flow.type == RandomizerContext::FlowNodeType::BRANCH) { + FlowQueryId query = 0; + if (flow.parameters > 0xffff || !resolve_query(flow.operation, query)) { return MOD_INVALID_ARGUMENT; } + out = builder.add_branch(query, static_cast(flow.parameters)); + return MOD_OK; + } + FlowEventId event = 0; + if (!resolve_event(flow.operation, event)) { + return MOD_INVALID_ARGUMENT; + } + out = builder.add_event(event, parameter_bytes(flow.parameters)); + return MOD_OK; +} + +ModResult wire_custom_node(const RandomizerContext::FlowNode& flow, uint16_t group, + const NodeIdMaps& nodeIds, mods::flow::NodeRef node) { + if (flow.type == RandomizerContext::FlowNodeType::BRANCH) { std::vector targets; - targets.reserve(overrides->second.size()); - for (const uint16_t legacyTarget : overrides->second) { + targets.reserve(flow.results.size()); + for (const auto& result : flow.results) { uint16_t target = 0; - result = remap_target(legacyTarget, nodeIds, target); - if (result != MOD_OK) { - return result; + if (!resolve_reference(result, group, nodeIds, target)) { + return MOD_INVALID_ARGUMENT; } targets.push_back(target); } - uint16_t firstEdge = 0; - result = svc_flow->add_edges( - mod_ctx, handle, targets.data(), static_cast(targets.size()), &firstEdge); - if (result == MOD_OK) { - mods::write_bits(node.bytes + 6, firstEdge); - } - return result; + node.results(targets); + return MOD_OK; } - case 3: { - ModResult result = remap_event(node); - if (result != MOD_OK || node.bytes[1] == FLOW_EVENT_JUMP_FLOW) { - return result; - } - const uint16_t legacyEdge = mods::read_bits(node.bytes + 2); - if (legacyEdge < BASE_CUSTOM_MSG_AND_FLOW_ID) { - return MOD_OK; - } - uint16_t target = 0; - result = remap_target(legacyEdge, nodeIds, target); - if (result != MOD_OK) { - return result; - } - uint16_t edge = 0; - result = svc_flow->add_edges(mod_ctx, handle, &target, 1, &edge); - if (result == MOD_OK) { - mods::write_bits(node.bytes + 2, edge); - } - return result; - } - default: + uint16_t target = 0; + if (!resolve_reference(flow.next, group, nodeIds, target)) { return MOD_INVALID_ARGUMENT; } + node.next(target); + return MOD_OK; } -void remap_actor_record(std::vector& bytes, const LegacyIdMap& nodeIds) { - if (bytes.size() < sizeof(stage_actor_data_class)) { - return; +ModResult apply_flow_patch(const RandomizerContext::FlowNode& flow, uint16_t group, + const MessageIdMaps& messageIds, const NodeIdMaps& nodeIds, + mods::flow::GraphBuilder& builder) { + if (!flow.patchIndex.has_value()) { + return MOD_INVALID_ARGUMENT; } - stage_actor_data_class actor{}; - std::memcpy(&actor, bytes.data(), sizeof(actor)); - if (std::memcmp(actor.name, "Obj_kn2", 7) != 0) { - return; + if (flow.type == RandomizerContext::FlowNodeType::MESSAGE) { + uint16_t message = 0; + uint16_t target = 0; + if (!resolve_message(flow.message, group, messageIds, message) || + !resolve_reference(flow.next, group, nodeIds, target)) + { + return MOD_INVALID_ARGUMENT; + } + builder.patch_node(flow.patchIndex.value(), mods::flow::message(0, message, target)); + return MOD_OK; } - const uint16_t legacy = static_cast(actor.base.angle.x); - const auto found = nodeIds.find(legacy); - if (found == nodeIds.end()) { - return; + if (flow.type == RandomizerContext::FlowNodeType::BRANCH) { + FlowQueryId query = 0; + if (flow.parameters > 0xffff || !resolve_query(flow.operation, query)) { + return MOD_INVALID_ARGUMENT; + } + std::vector targets; + targets.reserve(flow.results.size()); + for (const auto& result : flow.results) { + uint16_t target = 0; + if (!resolve_reference(result, group, nodeIds, target)) { + return MOD_INVALID_ARGUMENT; + } + targets.push_back(target); + } + builder.patch_branch(flow.patchIndex.value(), query, + static_cast(flow.parameters), targets); + return MOD_OK; } - actor.base.angle.x = static_cast(found->second); - std::memcpy(bytes.data(), &actor, sizeof(actor)); + FlowEventId event = 0; + uint16_t target = 0; + if (!resolve_event(flow.operation, event) || + !resolve_reference(flow.next, group, nodeIds, target)) + { + return MOD_INVALID_ARGUMENT; + } + builder.patch_event( + flow.patchIndex.value(), event, parameter_bytes(flow.parameters), target); + return MOD_OK; } -void remap_actor_flows(RandomizerContext& context, const LegacyIdMap& nodeIds) { - for (auto& [stage, patches] : context.mObjectPatches) { - for (auto& [crc, bytes] : patches) { - remap_actor_record(bytes, nodeIds); +ModResult resolve_actor_flow(RandomizerContext::ActorData& actor, uint16_t group, + const NodeIdMaps& nodeIds) { + if (actor.flow.empty()) { + return MOD_OK; + } + const auto found = nodeIds[group].find(actor.flow); + if (found == nodeIds[group].end() || actor.bytes.size() < sizeof(stage_actor_data_class)) { + return MOD_INVALID_ARGUMENT; + } + stage_actor_data_class data{}; + std::memcpy(&data, actor.bytes.data(), sizeof(data)); + data.base.angle.x = static_cast(found->second); + std::memcpy(actor.bytes.data(), &data, sizeof(data)); + return MOD_OK; +} + +ModResult resolve_actor_flows(RandomizerContext& context, const NodeIdMaps& nodeIds) { + auto group_for_key = [](uint32_t key, uint16_t& group) { + const uint32_t stage = key >> 16; + if (stage >= std::size(allStageMessageGroups)) { + return false; + } + group = allStageMessageGroups[stage]; + return group < kMessageGroupCount; + }; + for (auto& [key, patches] : context.mObjectPatches) { + uint16_t group = 0; + if (!group_for_key(key, group)) { + return MOD_INVALID_ARGUMENT; + } + for (auto& [crc, actor] : patches) { + const ModResult result = resolve_actor_flow(actor, group, nodeIds); + if (result != MOD_OK) { + return result; + } } } - for (auto& [stage, additions] : context.mObjectAdditions) { - for (auto& bytes : additions) { - remap_actor_record(bytes, nodeIds); + for (auto& [key, additions] : context.mObjectAdditions) { + uint16_t group = 0; + if (!group_for_key(key, group)) { + return MOD_INVALID_ARGUMENT; + } + for (auto& actor : additions) { + const ModResult result = resolve_actor_flow(actor, group, nodeIds); + if (result != MOD_OK) { + return result; + } } } + return MOD_OK; } ModResult register_graphs(RandomizerContext& context, const MessageIdMaps& messageIds) { - const auto legacyIds = custom_node_ids(context); - std::array handles{}; - std::array guards; - for (uint16_t group = 0; group < kMessageGroupCount; ++group) { - ModResult result = svc_flow->begin_graph(mod_ctx, group, &handles[group]); - if (result != MOD_OK) { - return result; + NodeIdMaps nodeIds; + for (const auto& flow : context.mFlowNodes) { + if (flow.group >= kMessageGroupCount) { + return MOD_INVALID_ARGUMENT; } - guards[group] = mods::flow::Graph{handles[group], MOD_OK}; } - - std::array, kMessageGroupCount> allocated; - LegacyIdMap nodeIds; - ModResult result = - allocate_shared_node_ids(handles, legacyIds.size(), allocated, nodeIds, legacyIds); - if (result != MOD_OK) { - return result; - } - - std::unordered_map dynamicToLegacy; - for (const auto& [legacy, dynamic] : nodeIds) { - dynamicToLegacy.emplace(dynamic, legacy); - } - - const FlowNodeData unused = mods::flow::event(FLOW_EVENT_UNUSED_42, 0, {}); for (uint16_t group = 0; group < kMessageGroupCount; ++group) { - for (const uint16_t dynamicId : allocated[group]) { - FlowNodeData node = unused; - const auto legacy = dynamicToLegacy.find(dynamicId); - if (legacy != dynamicToLegacy.end()) { - const uint32_t key = static_cast(CUSTOM_BMG_GROUP) << 16 | legacy->second; - const auto raw = context.mFlowPatches.find(key); - if (raw == context.mFlowPatches.end()) { - return MOD_INVALID_ARGUMENT; - } - node = raw_node(raw->second); - result = remap_node(context, key, group, handles[group], nodeIds, messageIds, node); - if (result != MOD_OK) { - return result; - } - } - result = svc_flow->fill_node(mod_ctx, handles[group], dynamicId, &node); - if (result != MOD_OK) { - return result; - } + bool hasNodes = false; + for (const auto& flow : context.mFlowNodes) { + hasNodes |= flow.group == group; + } + if (!hasNodes) { + continue; } - for (const auto& [key, value] : context.mFlowPatches) { - if (key >> 16 != group) { + mods::flow::GraphBuilder builder{group}; + std::unordered_map refs; + for (const auto& flow : context.mFlowNodes) { + if (flow.group != group || flow.patchIndex.has_value()) { continue; } - FlowNodeData node = raw_node(value); - result = remap_node(context, key, group, handles[group], nodeIds, messageIds, node); + if (flow.name.empty() || refs.contains(flow.name)) { + return MOD_INVALID_ARGUMENT; + } + mods::flow::NodeRef ref; + const ModResult result = add_custom_node(flow, messageIds, builder, ref); if (result != MOD_OK) { return result; } - result = - svc_flow->patch_node(mod_ctx, handles[group], static_cast(key), &node); + refs.emplace(flow.name, ref); + nodeIds[group].emplace(flow.name, ref.id()); + } + for (const auto& flow : context.mFlowNodes) { + if (flow.group != group || flow.patchIndex.has_value()) { + continue; + } + const ModResult result = wire_custom_node(flow, group, nodeIds, refs.at(flow.name)); if (result != MOD_OK) { return result; } } - } - - for (const FlowGraphHandle handle : handles) { - result = svc_flow->commit_graph(mod_ctx, handle); - if (result != MOD_OK) { - return result; + for (const auto& flow : context.mFlowNodes) { + if (flow.group != group || !flow.patchIndex.has_value()) { + continue; + } + const ModResult result = + apply_flow_patch(flow, group, messageIds, nodeIds, builder); + if (result != MOD_OK) { + return result; + } + } + auto graph = builder.commit(); + if (!graph) { + return graph.result(); } - } - for (auto& graph : guards) { s_graphs.push_back(std::move(graph)); } - remap_actor_flows(context, nodeIds); - return MOD_OK; + return resolve_actor_flows(context, nodeIds); } } // namespace diff --git a/mods/randomizer/src/randomizer_context.cpp b/mods/randomizer/src/randomizer_context.cpp index e49705fff4..ede15583f8 100644 --- a/mods/randomizer/src/randomizer_context.cpp +++ b/mods/randomizer/src/randomizer_context.cpp @@ -16,18 +16,98 @@ #include "../generator/logic/entrance_shuffle.hpp" #include +#include +#include #include -#include "custom_flow_ids.hpp" #include "d/actor/d_a_alink.h" #include "d/d_com_inf_game.h" #include "d/d_meter2.h" #include "d/d_meter2_draw.h" #include "d/d_meter2_info.h" #include "d/d_msg_class.h" -#include "d/d_msg_flow.h" #include "m_Do/m_Do_audio.h" +namespace { + +const char* flow_node_type_name(RandomizerContext::FlowNodeType type) { + switch (type) { + case RandomizerContext::FlowNodeType::MESSAGE: + return "message"; + case RandomizerContext::FlowNodeType::BRANCH: + return "branch"; + case RandomizerContext::FlowNodeType::EVENT: + return "event"; + } + return ""; +} + +RandomizerContext::FlowNodeType parse_flow_node_type(const YAML::Node& node) { + const auto type = node.as(); + if (type == "message") { + return RandomizerContext::FlowNodeType::MESSAGE; + } + if (type == "branch") { + return RandomizerContext::FlowNodeType::BRANCH; + } + if (type == "event") { + return RandomizerContext::FlowNodeType::EVENT; + } + throw std::runtime_error("Unknown flow node type: " + type); +} + +void write_flow_reference( + YAML::Node node, const RandomizerContext::FlowReference& reference) { + if (reference.nativeId.has_value()) { + node = reference.nativeId.value(); + } else { + node = reference.name; + } +} + +RandomizerContext::FlowReference parse_flow_reference(const YAML::Node& node) { + RandomizerContext::FlowReference reference{}; + try { + reference.nativeId = node.as(); + } catch (const YAML::BadConversion&) { + reference.name = node.as(); + } + return reference; +} + +void write_message_style( + YAML::Node node, const RandomizerContext::MessageStyleData& style) { + node["eventLabelId"] = style.eventLabelId; + node["speaker"] = style.speaker; + node["boxKind"] = style.boxKind; + node["drawType"] = style.drawType; + node["boxPosition"] = style.boxPosition; + node["lineAlignment"] = style.lineAlignment; + node["speakerMood"] = style.speakerMood; + node["cameraAttr"] = style.cameraAttr; + node["talkAnim"] = style.talkAnim; + node["faceAnim"] = style.faceAnim; + node["trailingData"] = style.trailingData; +} + +RandomizerContext::MessageStyleData parse_message_style(const YAML::Node& node) { + RandomizerContext::MessageStyleData style{}; + style.eventLabelId = node["eventLabelId"].as(); + style.speaker = node["speaker"].as(); + style.boxKind = node["boxKind"].as(); + style.drawType = node["drawType"].as(); + style.boxPosition = node["boxPosition"].as(); + style.lineAlignment = node["lineAlignment"].as(); + style.speakerMood = node["speakerMood"].as(); + style.cameraAttr = node["cameraAttr"].as(); + style.talkAnim = node["talkAnim"].as(); + style.faceAnim = node["faceAnim"].as(); + style.trailingData = node["trailingData"].as(); + return style; +} + +} // namespace + std::optional RandomizerContext::WriteToFile() { std::ofstream seedData(this->GetSeedDataPath()); @@ -36,6 +116,7 @@ std::optional RandomizerContext::WriteToFile() { } YAML::Node out{}; + out["formatVersion"] = FORMAT_VERSION; for (const auto& [setting, option] : this->mSettings) { out["mSettings"][setting] = option; @@ -88,23 +169,64 @@ std::optional RandomizerContext::WriteToFile() { for (const auto& [stageRoomLayer, actorPatches] : this->mObjectPatches) { for (const auto& [actorCRC, actorPatch] : actorPatches) { - out["mObjectPatches"][stageRoomLayer][actorCRC] = ContainerToHexString(actorPatch); + auto node = out["mObjectPatches"][stageRoomLayer][actorCRC]; + node["data"] = ContainerToHexString(actorPatch.bytes); + if (!actorPatch.flow.empty()) { + node["flow"] = actorPatch.flow; + } } } for (const auto& [stageRoomLayer, newActors] : this->mObjectAdditions) { for (const auto& actor : newActors) { - out["mObjectAdditions"][stageRoomLayer].push_back(ContainerToHexString(actor)); + YAML::Node node{}; + node["data"] = ContainerToHexString(actor.bytes); + if (!actor.flow.empty()) { + node["flow"] = actor.flow; + } + out["mObjectAdditions"][stageRoomLayer].push_back(node); } } - - out["mFlowPatches"] = this->mFlowPatches; - - for (const auto& [key, branchOverrides]: this->mFlowPatchesBranchOverrides) { - for (auto override : branchOverrides) { - out["mFlowPatchesBranchOverrides"][key].push_back(override); + for (const auto& flow : mFlowNodes) { + YAML::Node node{}; + node["type"] = flow_node_type_name(flow.type); + node["group"] = flow.group; + if (flow.patchIndex.has_value()) { + node["patchIndex"] = flow.patchIndex.value(); + } else { + node["name"] = flow.name; } + node["parameters"] = flow.parameters; + if (!flow.operation.empty()) { + node["operation"] = flow.operation; + } + if (flow.type == FlowNodeType::MESSAGE) { + write_flow_reference(node["message"], flow.message); + } + if (flow.type != FlowNodeType::BRANCH) { + write_flow_reference(node["next"], flow.next); + } + for (const auto& result : flow.results) { + YAML::Node resultNode{}; + write_flow_reference(resultNode, result); + node["results"].push_back(resultNode); + } + out["mFlowNodes"].push_back(node); + } + + for (const auto& message : mCustomMessages) { + YAML::Node node{}; + node["group"] = message.group; + node["name"] = message.name; + write_message_style(node["style"], message.style); + for (const auto& [language, text] : message.text) { + const auto languageName = randomizer::languageToString( + static_cast(language)); + node["text"][languageName] = YAML::Binary( + reinterpret_cast(text.data()), text.size()); + } + out["mCustomMessages"].push_back(node); } // Dump text overrides as binary to avoid losing intentional null characters @@ -125,10 +247,6 @@ std::optional RandomizerContext::WriteToFile() { textData << YAML::EndMap; textData << YAML::EndMap; - for (const auto& [key, override] : mAttributeOverrides) { - out["mAttributeOverrides"][key] = ContainerToHexString(override); - } - for (const auto& [key, override] : mEntranceOverrides) { out["mEntranceOverrides"][std::bit_cast(key)] = std::bit_cast(override); } @@ -154,6 +272,11 @@ std::optional RandomizerContext::LoadFromHash(const std::string& ha } auto in = LoadYAML(this->GetSeedDataPath()); + if (!in["formatVersion"] || in["formatVersion"].as() != FORMAT_VERSION) { + mods::log::error("Seed {} uses an obsolete data format and must be regenerated", hash); + mHash.clear(); + return "Seed data format is obsolete; regenerate this seed"; + } // Necessary settings for (const auto& settingNode : in["mSettings"] ) { @@ -257,7 +380,11 @@ std::optional RandomizerContext::LoadFromHash(const std::string& ha u32 stageRoomLayer = stageRoomLayerNode.first.as(); for (const auto& actorPatchNode : stageRoomLayerNode.second) { u32 actorCRC = actorPatchNode.first.as(); - this->mObjectPatches[stageRoomLayer][actorCRC] = HexToBytes(actorPatchNode.second.as()); + auto& actor = this->mObjectPatches[stageRoomLayer][actorCRC]; + actor.bytes = HexToBytes(actorPatchNode.second["data"].as()); + if (actorPatchNode.second["flow"]) { + actor.flow = actorPatchNode.second["flow"].as(); + } } } @@ -265,24 +392,52 @@ std::optional RandomizerContext::LoadFromHash(const std::string& ha for (const auto& stageNode: in["mObjectAdditions"]) { u32 stageRoomLayer = stageNode.first.as(); for (const auto& objectData : stageNode.second) { - this->mObjectAdditions[stageRoomLayer].emplace_back(HexToBytes(objectData.as())); + ActorData actor{}; + actor.bytes = HexToBytes(objectData["data"].as()); + if (objectData["flow"]) { + actor.flow = objectData["flow"].as(); + } + this->mObjectAdditions[stageRoomLayer].push_back(std::move(actor)); } } - // Flow Patches - for (const auto& flowNode: in["mFlowPatches"]) { - auto key = flowNode.first.as(); - auto value = flowNode.second.as(); - this->mFlowPatches[key] = value; + for (const auto& flowNode : in["mFlowNodes"]) { + FlowNode flow{}; + flow.type = parse_flow_node_type(flowNode["type"]); + flow.group = flowNode["group"].as(); + if (flowNode["patchIndex"]) { + flow.patchIndex = flowNode["patchIndex"].as(); + } else { + flow.name = flowNode["name"].as(); + } + flow.parameters = flowNode["parameters"].as(); + if (flowNode["operation"]) { + flow.operation = flowNode["operation"].as(); + } + if (flowNode["message"]) { + flow.message = parse_flow_reference(flowNode["message"]); + } + if (flowNode["next"]) { + flow.next = parse_flow_reference(flowNode["next"]); + } + for (const auto& result : flowNode["results"]) { + flow.results.push_back(parse_flow_reference(result)); + } + mFlowNodes.push_back(std::move(flow)); } - // Flow Patch Branch Overrides - for (const auto& flowNode : in["mFlowPatchesBranchOverrides"]) { - auto key = flowNode.first.as(); - for (const auto& branchNode : flowNode.second) { - auto override = branchNode.as(); - this->mFlowPatchesBranchOverrides[key].push_back(override); + for (const auto& messageNode : in["mCustomMessages"]) { + CustomMessage message{}; + message.group = messageNode["group"].as(); + message.name = messageNode["name"].as(); + message.style = parse_message_style(messageNode["style"]); + for (const auto& textNode : messageNode["text"]) { + const auto language = randomizer::stringToLanguage(textNode.first.as()); + const auto binary = textNode.second.as(); + message.text[language] = + std::string(reinterpret_cast(binary.data()), binary.size()); } + mCustomMessages.push_back(std::move(message)); } // Text Overrides @@ -297,15 +452,6 @@ std::optional RandomizerContext::LoadFromHash(const std::string& ha } } - // Attribute Overrides - for (const auto& attributeNode : in["mAttributeOverrides"]) { - auto key = attributeNode.first.as(); - std::vector overrideVec = HexToBytes(attributeNode.second.as()); - std::array override{}; - std::copy(overrideVec.begin(), overrideVec.end(), override.begin()); - this->mAttributeOverrides[key] = override; - } - // Entrance Overrides for (const auto& entranceNode : in["mEntranceOverrides"]) { const auto key = std::bit_cast(entranceNode.first.as());; @@ -997,68 +1143,9 @@ void parseObjPatchData(stage_tgsc_data_class& object, const YAML::Node& patchNod } } -static std::array CreateAttributeData(const YAML::Node& node, const std::string& name) { - auto attributesStr = node.as(); - auto attributesVec = HexToBytes(attributesStr); - if (attributesVec.size() != 16) { - throw std::runtime_error(fmt::format("Attributes for Text Override {} " - "are the wrong length. (Expected: 16, Actual: {}", name, attributesVec.size())); - } - - std::array attributes{}; - for (size_t i = 0; i < attributesVec.size(); ++i) { - attributes[i + 4] = attributesVec[i]; - } - return attributes; -} - RandomizerContext WriteSeedData(randomizer::logic::world::World* world) { RandomizerContext randoData{}; - // Give custom flows and messages new indices as we read them in/create them - std::unordered_map customMessageIDs{}; - std::unordered_map customFlowIDs{}; - std::unordered_set usedMessageIDs{}; - std::unordered_set usedFlowIDs{}; - u16 curCustomMessageID = BASE_CUSTOM_MSG_AND_FLOW_ID; - u16 curCustomFlowID = BASE_CUSTOM_MSG_AND_FLOW_ID; - - // Helper functions for assigning new custom flow IDs/message IDs - auto handleCustomID = [](const std::string& name, auto& customIds, auto& usedIds, u16& curCustomID) { - u16 resultIndex{}; - // Check to see if we're setting a custom index - auto resultInt = randomizer::utility::str::toInt(name); - // If we have a regular index, then use that directly - if (resultInt.has_value()) { - resultIndex = resultInt.value(); - } else { - // If we don't, assume we're setting the index as custom - if (customIds.contains(name)) { - resultIndex = customIds[name]; - } else { - while (usedIds.contains(curCustomID)) { - ++curCustomID; - } - auto newIndex = curCustomID++; - resultIndex = newIndex; - customIds[name] = newIndex; - } - } - - usedIds.insert(resultIndex); - return resultIndex; - }; - - - auto handleCustomFlowID = [&](const std::string& name) { - return handleCustomID(name, customFlowIDs, usedFlowIDs, curCustomFlowID); - }; - - - auto handleCustomMessageID = [&](const std::string& name) { - return handleCustomID(name, customMessageIDs, usedMessageIDs, curCustomMessageID); - }; - // Settings we need to check ingame for (const auto& [setting, info] : *randomizer::seedgen::settings::GetAllSettingsInfo()) { if (info->NeedInGame()) { @@ -1268,6 +1355,7 @@ RandomizerContext WriteSeedData(randomizer::logic::world::World* world) { randoData.mStartHour = 24; // Actor Patches + std::unordered_map objectFlowGroups{}; auto actorPatches = LOAD_EMBED_YAML(RANDO_DATA_PATH "object_patches.yaml"); for (const auto& stageNode : actorPatches) { const auto& stageName = stageNode.first.as(); @@ -1295,20 +1383,36 @@ RandomizerContext WriteSeedData(randomizer::logic::world::World* world) { u32 objectCRC32 = getStageObjCRC32(reinterpret_cast(&object), objDataSize); // Depending on the action, store data on this actor - std::vector actorData(0); + RandomizerContext::ActorData actorData{}; + if (objectNode["flow"]) { + actorData.flow = objectNode["flow"].as(); + const int stageId = getStageID(stageName.c_str()); + if (stageId < 0 || + static_cast(stageId) >= std::size(allStageMessageGroups)) + { + throw std::runtime_error("Unknown stage for flow-bearing actor: " + stageName); + } + const u8 group = allStageMessageGroups[stageId]; + const auto [found, inserted] = objectFlowGroups.emplace(actorData.flow, group); + if (!inserted && found->second != group) { + throw std::runtime_error( + "Actor flow is referenced from multiple message groups: " + actorData.flow); + } + object.base.angle.x = 0; + } // If we're patching this object, Then override the object with whatever parts are being patched // and add that patch data to our actorData if (action == "patch") { parseObjPatchData(object, objectNode["patch"]); - actorData.resize(objDataSize); - std::memcpy(actorData.data(), &object, objDataSize); + actorData.bytes.resize(objDataSize); + std::memcpy(actorData.bytes.data(), &object, objDataSize); } else if (action == "add") { // If we're adding the object, add it's regular data to the actorData - actorData.resize(objDataSize); - std::memcpy(actorData.data(), &object, objDataSize); + actorData.bytes.resize(objDataSize); + std::memcpy(actorData.bytes.data(), &object, objDataSize); } else if (action == "delete") { // If we're deleting this actor, give it a specific size to indicate we're deleting it - actorData.resize(RandomizerContext::OBJ_DELETE_SIZE); + actorData.bytes.resize(RandomizerContext::OBJ_DELETE_SIZE); } else { // Unknown action. Don't continue throw std::runtime_error("object patch action \"" + action + "\" not recognized"); @@ -1333,228 +1437,224 @@ RandomizerContext WriteSeedData(randomizer::logic::world::World* world) { } } - // Flow Patches + auto source_reference = [](const YAML::Node& node) { + RandomizerContext::FlowReference reference{}; + const auto value = node.as(); + if (const auto numeric = randomizer::utility::str::toInt(value); numeric.has_value()) { + if (numeric.value() < 0 || numeric.value() > 0xffff) { + throw std::runtime_error("Flow reference is outside the 16-bit range: " + value); + } + reference.nativeId = static_cast(numeric.value()); + } else { + reference.name = value; + } + return reference; + }; + + std::unordered_map> customMessageGroups{}; + std::unordered_map splitMessageStyles{}; auto flowPatches = LOAD_EMBED_YAML(RANDO_DATA_PATH "flow_patches.yaml"); for (const auto& groupNode : flowPatches) { - u8 groupNo = groupNode.first.as(); + const auto groupName = groupNode.first.as(); + const bool customSection = groupName == "custom"; + const u8 defaultGroup = customSection ? 0 : groupNode.first.as(); for (const auto& flowNode : groupNode.second) { - // Check to see if this patch is contingent on a specific setting - if (flowNode["only if"]) { - const auto& condition = flowNode["only if"].as(); - // If the required condition isn't set, then skip this one - if (!world->EvaluateSettingCondition(condition)) { - continue; - } + if (flowNode["only if"] && + !world->EvaluateSettingCondition(flowNode["only if"].as())) + { + continue; } - std::string name{}; - std::list indices{}; - if (flowNode["index"]) { - // If we're specifying a sequence of indices - if (flowNode["index"].IsSequence()) { - for (const auto& indexNode : flowNode["index"]) { - auto index = indexNode.as(); - indices.push_back(index); - usedFlowIDs.insert(index); - } - name = std::to_string(indices.front()); - } - // If we have just a single index - else if (flowNode["index"].IsScalar()) { - auto index = flowNode["index"].as(); - indices.push_back(index); - name = std::to_string(index); - usedFlowIDs.insert(index); - } - // If we're specifying an index as well as a name, add the index to the custom - // ids - if (flowNode["name"]) { - name = flowNode["name"].as(); - customFlowIDs[name] = indices.front(); + RandomizerContext::FlowNode flow{}; + flow.type = parse_flow_node_type(flowNode["type"]); + flow.parameters = flowNode["parameters"].as(); + + std::vector patchIndices{}; + if (customSection) { + flow.name = flowNode["name"].as(); + if (flowNode["group"]) { + flow.group = flowNode["group"].as(); + } else if (const auto found = objectFlowGroups.find(flow.name); + found != objectFlowGroups.end()) + { + flow.group = found->second; + } else { + throw std::runtime_error("Custom flow has no message group: " + flow.name); } } else { - name = flowNode["name"].as(); - indices.push_back(handleCustomFlowID(flowNode["name"].as())); - } - - const auto& type = flowNode["type"].as(); - u64 value{}; - if (type == "branch") { - auto branch = reinterpret_cast(&value); - branch->type = 2; - branch->result_count = flowNode["num results"].as(); - branch->query_idx = flowNode["query"].as(); - branch->param = flowNode["parameters"].as(); - branch->next_node_idx = flowNode["next node index"].as(); - // If we're using custom result indices - if (flowNode["results"]) { - auto& results = flowNode["results"]; - if (results.size() != branch->result_count) { - throw std::runtime_error(fmt::format("Flow results size for {} " - "do not match num results. (expected: {}. size: {})", name, branch->result_count, results.size())); - } - for (const auto& resultNode : results) { - auto resultIndex = handleCustomFlowID(resultNode.as()); - for (auto index : indices) { - u32 key = (groupNo << 16) | index; - randoData.mFlowPatchesBranchOverrides[key].push_back(resultIndex); - } + flow.group = defaultGroup; + if (flowNode["index"].IsSequence()) { + for (const auto& index : flowNode["index"]) { + patchIndices.push_back(index.as()); } + } else { + patchIndices.push_back(flowNode["index"].as()); } } - else if (type == "event") { - auto event = reinterpret_cast(&value); - event->type = 3; - event->event_idx = flowNode["event"].as(); - event->next_node_idx = handleCustomFlowID(flowNode["next node index"].as()); - u32 params = flowNode["parameters"].as(); - event->params[0] = (params >> 24) & 0xFF; - event->params[1] = (params >> 16) & 0xFF; - event->params[2] = (params >> 8) & 0xFF; - event->params[3] = params & 0xFF; - } else if (type == "message") { - auto message = reinterpret_cast(&value); - message->type = 1; - message->msg_index = handleCustomMessageID(flowNode["inf index"].as()); - message->next_node_idx = handleCustomFlowID(flowNode["next flow index"].as()); - // If a custom message is too long, split it up among additional flow/message nodes - if (message->msg_index >= BASE_CUSTOM_MSG_AND_FLOW_ID) { - auto textName = flowNode["inf index"].as(); - if (world->GetTextDatabase().contains(textName)) { - auto& text = world->GetTextObject(textName); - if (text.IsTooLong()) { - // Get the attributes for the text at this ID. Pretty inefficient since we - // have to loop through every element unfortunately - std::optional> customAttributes{}; - auto textOverrides = LOAD_EMBED_YAML(RANDO_DATA_PATH "text/text_overrides.yaml"); - for (const auto& overrideNode : textOverrides) { - const auto& overrideName = overrideNode["Name"].as(); - if (overrideName == textName && overrideNode["Attributes"]) { - customAttributes = CreateAttributeData(overrideNode["Attributes"], textName); - break; - } + if (flow.type == RandomizerContext::FlowNodeType::BRANCH) { + flow.operation = flowNode["query"].as(); + for (const auto& result : flowNode["results"]) { + flow.results.push_back(source_reference(result)); + } + if (flow.parameters > 0xffff || flow.results.empty() || + flow.results.size() > 0xff) + { + throw std::runtime_error("Flow branch has invalid parameters or results"); + } + } else if (flow.type == RandomizerContext::FlowNodeType::EVENT) { + flow.operation = flowNode["event"].as(); + flow.next = source_reference(flowNode["next"]); + } else { + flow.message = source_reference(flowNode["message"]); + flow.next = source_reference(flowNode["next"]); + if (!flow.message.nativeId.has_value()) { + customMessageGroups[flow.message.name].insert(flow.group); + if (world->GetTextDatabase().contains(flow.message.name)) { + auto& text = world->GetTextObject(flow.message.name); + // The game still owns textbox pagination, so preserve the existing + // per-message limit while representing overflow as ordinary flow nodes. + const auto extraText = text.SplitToFitTextLimits(); + if (!extraText.empty()) { + const auto originalNext = flow.next; + std::vector extraNames{}; + for (size_t i = 0; i < extraText.size(); ++i) { + auto extraName = flow.message.name + std::to_string(i + 1); + world->AddNewText(extraName) = extraText[i]; + splitMessageStyles[extraName] = flow.message.name; + customMessageGroups[extraName].insert(flow.group); + extraNames.push_back(std::move(extraName)); } - - - // Add each split text as a new custom message entry. The original entry - // still exists but has been sliced down to fit properly. - auto extraText = text.SplitToFitTextLimits(); - std::vector newMsgFlows{}; - for (size_t i = 0; i < extraText.size(); i++) { - // Add this custom text to the world - auto extraTextName = textName + std::to_string(i + 1); - world->AddNewText(extraTextName) = extraText[i]; - - // Create new Flow and Message Ids for the split text object - auto newCustomFlowIndex = handleCustomFlowID(extraTextName); - auto newCustomMessageIndex = handleCustomMessageID(extraTextName); - - // Create the new flow node. We're storing its own flow index with - // itself for now, but we'll shift it back to the previous node later - mesg_flow_node newMsgFlow{}; - newMsgFlow.type = 1; - newMsgFlow.msg_index = newCustomMessageIndex; - newMsgFlow.next_node_idx = newCustomFlowIndex; - - newMsgFlows.push_back(newMsgFlow); - - //Add the custom text to the rando data - u32 key = (CUSTOM_BMG_GROUP << 16) | newCustomMessageIndex; - for (auto language : randomizer::supportedLanguages) { - std::string newText = extraText[i].mText[language]; - randomizer::applyMessageCodes(newText); - randoData.mTextOverrides[language][key] = newText; - } - - // Add custom attribute data as well if it exists - if (customAttributes.has_value()) { - auto attributes = customAttributes.value(); - - // Set the message id in the attribute data - attributes[4] = newCustomMessageIndex >> 8; - attributes[5] = newCustomMessageIndex & 0xFF; - - randoData.mAttributeOverrides[key] = attributes; - } - } - - // Shift all the next_node_idx fields back a node and set the original - // next node idx as the next node idx for the final of the new flows - auto finalNodeIdx = message->next_node_idx; - message->next_node_idx = newMsgFlows[0].next_node_idx; - for (size_t i = 0; i < newMsgFlows.size(); i++) { - auto& curFlow = newMsgFlows[i]; - auto curFlowIdx = curFlow.next_node_idx; - if (i == newMsgFlows.size() - 1) { - curFlow.next_node_idx = finalNodeIdx; - } else { - curFlow.next_node_idx = newMsgFlows[i + 1].next_node_idx; - } - - // Also Add the new custom flows to our rando data - u32 key = (CUSTOM_BMG_GROUP << 16) | curFlowIdx; - randoData.mFlowPatches[key] = std::bit_cast(curFlow); + flow.next = {.name = extraNames.front()}; + for (size_t i = 0; i < extraNames.size(); ++i) { + RandomizerContext::FlowNode extraFlow{ + .type = RandomizerContext::FlowNodeType::MESSAGE, + .group = flow.group, + .name = extraNames[i], + .message = {.name = extraNames[i]}, + .next = i + 1 < extraNames.size() ? + RandomizerContext::FlowReference{ + .name = extraNames[i + 1]} : + originalNext, + }; + randoData.mFlowNodes.push_back(std::move(extraFlow)); } } } } } - for (auto index : indices) { - u32 key = (groupNo << 16) | index; - randoData.mFlowPatches[key] = value; + + if (customSection) { + randoData.mFlowNodes.push_back(std::move(flow)); + } else { + for (const u16 patchIndex : patchIndices) { + auto patch = flow; + patch.patchIndex = patchIndex; + randoData.mFlowNodes.push_back(std::move(patch)); + } } } } - // Text Overrides + std::array, 9> flowNames{}; + for (const auto& flow : randoData.mFlowNodes) { + if (flow.group >= flowNames.size()) { + throw std::runtime_error("Flow node has an invalid message group"); + } + if (!flow.patchIndex.has_value() && + (flow.name.empty() || !flowNames[flow.group].insert(flow.name).second)) + { + throw std::runtime_error("Custom flow name is empty or duplicated: " + flow.name); + } + } + auto validate_flow_reference = [&](u8 group, const RandomizerContext::FlowReference& reference) { + if (!reference.nativeId.has_value() && + (reference.name.empty() || !flowNames[group].contains(reference.name))) + { + throw std::runtime_error(fmt::format( + "Unresolved flow reference in group {}: {}", group, reference.name)); + } + }; + for (const auto& [name, group] : objectFlowGroups) { + validate_flow_reference(group, {.name = name}); + } + for (const auto& flow : randoData.mFlowNodes) { + if (flow.type == RandomizerContext::FlowNodeType::BRANCH) { + for (const auto& result : flow.results) { + validate_flow_reference(flow.group, result); + } + } else { + validate_flow_reference(flow.group, flow.next); + } + } + + auto parse_style = [](const YAML::Node& node) { + RandomizerContext::MessageStyleData style{}; + if (!node) { + return style; + } + auto set = [&](const char* key, auto& field) { + if (node[key]) { + field = node[key].as>(); + } + }; + set("Event Label", style.eventLabelId); + set("Speaker", style.speaker); + set("Box Kind", style.boxKind); + set("Draw Type", style.drawType); + set("Box Position", style.boxPosition); + set("Line Alignment", style.lineAlignment); + set("Speaker Mood", style.speakerMood); + set("Camera", style.cameraAttr); + set("Talk Animation", style.talkAnim); + set("Face Animation", style.faceAnim); + set("Trailing Data", style.trailingData); + return style; + }; + auto textOverrides = LOAD_EMBED_YAML(RANDO_DATA_PATH "text/text_overrides.yaml"); + std::unordered_map textDefinitions{}; for (const auto& overrideNode : textOverrides) { - // Check to see if this override is contingent on a specific setting - if (overrideNode["Only If"]) { - const auto& condition = overrideNode["Only If"].as(); - // If the required condition isn't set, then skip this one - if (!world->EvaluateSettingCondition(condition)) { - continue; - } + const auto name = overrideNode["Name"].as(); + textDefinitions.emplace(name, overrideNode); + if (!overrideNode["Message Id"] || + (overrideNode["Only If"] && + !world->EvaluateSettingCondition(overrideNode["Only If"].as()))) + { + continue; } - const auto& name = overrideNode["Name"].as(); - u8 group; - u16 messageId; - if (overrideNode["Group"]) { - group = overrideNode["Group"].as(); - } else { - // If no group specified, assume custom bmg group - group = CUSTOM_BMG_GROUP; - } - if (overrideNode["Message Id"]) { - messageId = overrideNode["Message Id"].as(); - } else { - // If no message id specified, assume a custom one - messageId = handleCustomMessageID(overrideNode["Name"].as()); - } - u32 key = (group << 16) | messageId; - for (auto language : randomizer::supportedLanguages) { - std::string text; - if (world->GetTextDatabase().contains(name)) { - text = world->GetText(name, randomizer::Text::STANDARD, language); - } else { - text = randomizer::getTextStr(name, randomizer::Text::STANDARD, language); - } - + const u8 group = overrideNode["Group"].as(); + const u16 messageId = overrideNode["Message Id"].as(); + const u32 key = static_cast(group) << 16 | messageId; + for (const auto language : randomizer::supportedLanguages) { + std::string text = world->GetTextDatabase().contains(name) ? + world->GetText(name, randomizer::Text::STANDARD, language) : + randomizer::getTextStr(name, randomizer::Text::STANDARD, language); randomizer::applyMessageCodes(text); - randoData.mTextOverrides[language][key] = text; + randoData.mTextOverrides[language][key] = std::move(text); } + } - // If we have custom attributes - if (overrideNode["Attributes"]) { - auto attributes = CreateAttributeData(overrideNode["Attributes"], name); - - // Set the message id in the attribute data - attributes[4] = messageId >> 8; - attributes[5] = messageId & 0xFF; - - randoData.mAttributeOverrides[key] = attributes; + for (const auto& [name, groups] : customMessageGroups) { + const auto styleName = splitMessageStyles.contains(name) ? splitMessageStyles[name] : name; + const auto definition = textDefinitions.find(styleName); + if (definition == textDefinitions.end()) { + throw std::runtime_error("Custom flow message has no text definition: " + styleName); + } + for (const u8 group : groups) { + RandomizerContext::CustomMessage message{ + .group = group, + .name = name, + .style = parse_style(definition->second["Style"]), + }; + for (const auto language : randomizer::supportedLanguages) { + std::string text = world->GetTextDatabase().contains(name) ? + world->GetText(name, randomizer::Text::STANDARD, language) : + randomizer::getTextStr(name, randomizer::Text::STANDARD, language); + randomizer::applyMessageCodes(text); + message.text[language] = std::move(text); + } + randoData.mCustomMessages.push_back(std::move(message)); } } diff --git a/mods/randomizer/src/randomizer_context.hpp b/mods/randomizer/src/randomizer_context.hpp index 1e6e80ee89..91cee30c2f 100644 --- a/mods/randomizer/src/randomizer_context.hpp +++ b/mods/randomizer/src/randomizer_context.hpp @@ -19,6 +19,7 @@ */ class RandomizerContext { public: + static constexpr u32 FORMAT_VERSION = 2; static constexpr size_t ACTR_CRC_SIZE = 32; static constexpr size_t TGSC_CRC_SIZE = 35; // 3 extra bytes for scale x, y, z static constexpr size_t OBJ_DELETE_SIZE = 1; @@ -56,17 +57,62 @@ public: u8 mStartHour{0}; u8 mMapBits{}; - std::unordered_map>> mObjectPatches{}; - std::unordered_map>> mObjectAdditions{}; - // std::unordered_map> mTgscDeletions{}; - std::unordered_map mFlowPatches{}; - std::unordered_map> mFlowPatchesBranchOverrides{}; - std::unordered_map> mAttributeOverrides{}; + struct ActorData { + std::vector bytes{}; + std::string flow{}; + }; + + std::unordered_map> mObjectPatches{}; + std::unordered_map> mObjectAdditions{}; + // std::unordered_map> mTgscDeletions{}; + + struct FlowReference { + std::optional nativeId{}; + std::string name{}; + }; + + enum class FlowNodeType : u8 { + MESSAGE, + BRANCH, + EVENT, + }; + + struct FlowNode { + FlowNodeType type{}; + u8 group{}; + std::optional patchIndex{}; + std::string name{}; + std::string operation{}; + FlowReference message{}; + FlowReference next{}; + std::vector results{}; + u32 parameters{}; + }; + + struct MessageStyleData { + u16 eventLabelId{}; + u8 speaker{0x24}; + u8 boxKind{}; + u8 drawType{}; + u8 boxPosition{}; + u8 lineAlignment{}; + u8 speakerMood{}; + u8 cameraAttr{}; + u8 talkAnim{0x02}; + u8 faceAnim{0x03}; + u16 trailingData{0x0400}; + }; + + struct CustomMessage { + u8 group{}; + std::string name{}; + MessageStyleData style{}; + std::unordered_map text{}; + }; + + std::vector mFlowNodes{}; + std::vector mCustomMessages{}; - // struct TextOverride { - // std::array mAttributes{}; - // std::string mText{}; - // }; // Map of language -> map of key -> string std::unordered_map> mTextOverrides{}; diff --git a/mods/randomizer/src/session.cpp b/mods/randomizer/src/session.cpp index edd6c47ff4..6db90ecaa6 100644 --- a/mods/randomizer/src/session.cpp +++ b/mods/randomizer/src/session.cpp @@ -292,15 +292,15 @@ void registerStageEdits() { const u8 room = (key >> 8) & 0xFF; const s8 layer = static_cast(key & 0xFF); - for (const auto& [crc, bytes] : patches) { + for (const auto& [crc, actor] : patches) { StageActorHandle handle{}; ModResult res; - if (bytes.size() == RandomizerContext::OBJ_DELETE_SIZE) { + if (actor.bytes.size() == RandomizerContext::OBJ_DELETE_SIZE) { res = svc_mng.stage->delete_actor(mod_ctx, stage, room, layer, crc, &handle); } else { - res = svc_mng.stage->patch_actor( - mod_ctx, stage, room, layer, crc, bytes.data(), bytes.size(), &handle); + res = svc_mng.stage->patch_actor(mod_ctx, stage, room, layer, crc, + actor.bytes.data(), actor.bytes.size(), &handle); } if (res == MOD_OK) { @@ -317,10 +317,11 @@ void registerStageEdits() { const u8 room = (key >> 8) & 0xFF; const s8 layer = static_cast(key & 0xFF); - for (const auto& bytes : additions) { + for (const auto& actor : additions) { StageActorHandle handle{}; ModResult rt; - rt = svc_mng.stage->add_actor(mod_ctx, stage, room, layer, bytes.data(), bytes.size(), &handle); + rt = svc_mng.stage->add_actor(mod_ctx, stage, room, layer, actor.bytes.data(), + actor.bytes.size(), &handle); if (rt == MOD_OK) { s_stage_edits.push_back(handle); } diff --git a/mods/randomizer/src/stages.cpp b/mods/randomizer/src/stages.cpp index 904cf5e70f..773a2d591e 100644 --- a/mods/randomizer/src/stages.cpp +++ b/mods/randomizer/src/stages.cpp @@ -79,4 +79,12 @@ const char allStages[78][8] = { "R_SP209", // 75 "R_SP300", // 76 "R_SP301" // 77 -}; \ No newline at end of file +}; + +// Message groups are the STAG metadata for the corresponding allStages entry. +const unsigned char allStageMessageGroups[78] = { + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 1, 8, 1, 1, 6, 2, 3, 2, 8, 7, 6, 8, 4, 6, 6, 8, 8, 8, 6, + 6, 8, 7, 6, 6, 1, 4, 6, 2, 3, 4, 7, 6, 4, 4, 2, 5, 4, +}; diff --git a/mods/randomizer/src/stages.h b/mods/randomizer/src/stages.h index 607c4a7959..afee4bf189 100644 --- a/mods/randomizer/src/stages.h +++ b/mods/randomizer/src/stages.h @@ -83,3 +83,4 @@ enum StageIDs }; extern const char allStages[78][8]; +extern const unsigned char allStageMessageGroups[78]; diff --git a/sdk/include/mods/svc/flow.hpp b/sdk/include/mods/svc/flow.hpp index 2b1cb1b594..e0d6e5c2f1 100644 --- a/sdk/include/mods/svc/flow.hpp +++ b/sdk/include/mods/svc/flow.hpp @@ -100,6 +100,7 @@ public: NodeRef next(uint16_t target) const; /* Set a branch node's result targets */ NodeRef results(std::initializer_list targets) const; + NodeRef results(std::span targets) const; uint16_t id() const { return mId; } operator uint16_t() const { return mId; } @@ -153,6 +154,35 @@ public: } return *this; } + GraphBuilder& patch_branch(uint16_t nodeIndex, FlowQueryId query, uint16_t parameter, + std::span targets) { + if (mResult != MOD_OK || targets.empty() || targets.size() > 0xff) { + if (mResult == MOD_OK) { + mResult = MOD_INVALID_ARGUMENT; + } + return *this; + } + uint16_t first = 0; + mResult = svc_flow->add_edges( + mod_ctx, mHandle, targets.data(), static_cast(targets.size()), &first); + if (mResult == MOD_OK) { + patch_node(nodeIndex, + branch(static_cast(targets.size()), query, parameter, first)); + } + return *this; + } + GraphBuilder& patch_event(uint16_t nodeIndex, FlowEventId eventId, + std::array params, uint16_t target) { + if (mResult != MOD_OK) { + return *this; + } + uint16_t edge = 0; + mResult = svc_flow->add_edges(mod_ctx, mHandle, &target, 1, &edge); + if (mResult == MOD_OK) { + patch_node(nodeIndex, event(eventId, edge, params)); + } + return *this; + } Graph commit() { for (const auto& node : mNodes) { @@ -232,7 +262,7 @@ private: } } - void set_results(uint16_t id, std::initializer_list targets) { + void set_results(uint16_t id, std::span targets) { if (mResult != MOD_OK) { return; } @@ -246,7 +276,7 @@ private: node->wired = true; uint16_t first = 0; mResult = svc_flow->add_edges( - mod_ctx, mHandle, targets.begin(), static_cast(targets.size()), &first); + mod_ctx, mHandle, targets.data(), static_cast(targets.size()), &first); if (mResult == MOD_OK) { node->data.bytes[1] = static_cast(targets.size()); write_bits(node->data.bytes + 6, first); @@ -266,6 +296,10 @@ inline NodeRef NodeRef::next(uint16_t target) const { } inline NodeRef NodeRef::results(std::initializer_list targets) const { + return results(std::span{targets.begin(), targets.size()}); +} + +inline NodeRef NodeRef::results(std::span targets) const { if (mBuilder != nullptr) { mBuilder->set_results(mId, targets); } @@ -368,6 +402,10 @@ public: [[nodiscard]] constexpr MessageStyle face_anim(uint8_t value) const { return set_u8(17, value); } + /* INF1 bytes 18-19, normally 0x0400. */ + [[nodiscard]] constexpr MessageStyle trailing_data(uint16_t value) const { + return set_u16(18, value); + } [[nodiscard]] constexpr const MessageEntryData& data() const { return mData; } private: @@ -432,6 +470,9 @@ public: MessageBuilder& camera_attr(uint8_t value) { return style(&MessageStyle::camera_attr, value); } MessageBuilder& talk_anim(uint8_t value) { return style(&MessageStyle::talk_anim, value); } MessageBuilder& face_anim(uint8_t value) { return style(&MessageStyle::face_anim, value); } + MessageBuilder& trailing_data(uint16_t value) { + return style(&MessageStyle::trailing_data, value); + } /* Content builder functions */ MessageBuilder& text(std::string_view value) {