Repository navigation
Move the floor select onto the UI framework and warn of floors that limit the party - #250
Open
SirFancyBacon wants to merge 38 commits into
Open
SirFancyBacon wants to merge 38 commits into
SirFancyBacon wants to merge 38 commits into
Conversation
The vanilla item documents were made at start-up from the compiled tables. They are now tracked in port/gameplay/items, as Dark Cloud 2's gameplay documents are, and the build seals them into Kazarov's gameplay seed (assets/data.zip beside dcdata). dcdata extract, or the new publish-gameplay command on data extracted before, writes them into the data under gameplay/. A document is tried from the datapacks down, then the data's own, then the compiled table, which data without the documents (from the first-run window, or extracted before) uses. The items now load once RunGame has indexed the data; indexing it once more beforehand cost 0.6 s per start in a debug build. The Windows and macOS packages carry assets/ beside dcdata; the Flatpak installs the seed there. Tests: the tracked documents hold the tables as compiled, and the data's own document is read under the packs. Pins Kazarov to its game-neutral gameplay seed (gameplay_publish).
The Flatpak removed /bin/data.zip, Kazarov's seed, when Chronicle had no gameplay documents. The seed now installs to /app/bin/assets, where dcdata looks for it, and a fresh configure keeps it there with Kazarov's fix.
A datapack or the data can add a bag item as a document of its own, gameplay/items/custom/<namespace>.<name>.json: its ID, retail's item fields, prices, a retail icon and model, its text by language, and optionally bait. Packs are read before the data, and every pack's items load. A document that cannot be read is reported and left out. Custom items are numbered from 0x800 in ID order, so the same items get the same numbers every run. An item's name is message number + 100 in the system messages, which go up to 2080, so 0x800 keeps custom names clear of retail's. Their descriptions are at number + 500, as retail's are. The game's own lookups answer for them: GetCommonItemInfo and GetItemData, the shop prices, the model paths, and the item rows the dungeon reads. When a message file lacks a custom item's name or description, the lookup gives the item's own text in the current language, or English. The disc's own messages always win. Tests: custom items get numbers, rows, prices and models of their own; are read from every pack and the data; a bad document is left out; and their text answers the game's message lookups.
Retail tells weapons from items by number, and often takes anything from ITEM_WEAPON_START up for a weapon. A custom item, numbered from 0x800, was taken for one: picking it up in a dungeon read past WeaponList, and the shop, the menus, storage and the obtained message treated it as a weapon, or refused it as a bag item. item::KindOf gives a number's kind: an attachment, a bag item (with every custom item), a weapon, or none. The sites that would misclassify a custom item ask it. Below the custom numbers it keeps retail's ranges, one-sided weapon checks included, so retail's items behave as before. Storage keeps retail's ITEM_DUNGEON_END for its own items. --give takes a custom item's ID, since its number depends on the packs loaded. Test: a custom item has the kind of a bag item, and picked up in a dungeon it goes into the bag.
A custom item's document may declare what each copy keeps, as names and
the numbers a new copy starts with ("state"), and the version of those
names ("state_version"). item::NewCopy makes such a copy: a permanent
instance, from a counter the save will keep, and a handle, a number of its
own from 0x1000. Retail's code carries the handle wherever it carries an
item number, so the copy keeps its identity through the bag, storage and
the menus, and copies never stack. Handles stop at 0x7E00, since a
description is looked up at the number + 500, an s16.
Everything the host answers for a custom item resolves a handle to its
item first: the item tables, the name and description, the kind, the
price and the model. A copy whose item is not registered keeps its ID and
state and finds its item again when it returns; item::RestoreCopy takes
back a copy kept in a save either way.
Between areas, where no menu holds an item, the handles of copies in none
of the bag, its quick slots or storage are freed. An item's own number in
the bag stands for a copy with the starting state. --give makes a copy of
an item with state.
Tests: copies get handles and instances of their own and answer as their
item; a freed handle is used again; a copy keeps its state while its item
is gone; a copy kept in a save comes back under a free handle; state that
is not numbers is refused.
save.dat stays retail's image: custom items and copies leave the bag, quick slots and storage before the checksums are made, and go into chronicle.dat, chunks kept under a hash of the save.dat they belong to and written so an interruption leaves a matching pair. An item whose definition is missing loads as an inert orphan that keeps its ID and state until the definition returns.
The save and load screens show a "!" after the number of a save whose chronicle.dat names an item that would load as an orphan.
fish.json, places.json and prizes.json hold retail's fish kinds, the five spots' fish and spawn odds, and the exchange's prizes, read from datapacks like the item documents. Retail's fishing code asks the species and places registries, and each place's fish are drawn by its spawn stage, retail's by default.
A hooked fish holds the hook for half a second to a second, where retail let go after 21 to 50 ticks, and the moment it bites is marked by a splash, rings at the float and a strong rumble, where retail's tug felt much like a nibble.
The session's steps are fishing::Session, which drives the game through an Angler interface and asks the place's stages: the bite (what each fish does about the bait, and when the bait is lost), the fight and the outcome, each retail's, named in places.json with their settings.
The fishing framework keeps a list of baits, retail's thirteen and then every custom item with the Bait trait, which the picker offers and the hook, the fish and the give-back read. A lure comes back to the bag after a catch and is never lost, and a species that doesn't name a bait likes it at its default_affinity. A custom item document names the fields it holds that this version does not read.
Every landed fish and every one that gets away is published with its species, place and bait, and fishing's quest helpers count catches, points, the largest and the species caught into a quest's own numbers through the new quest::ChangeVar.
A pack adds a species with a document of its own, given a kind from 256 and a name by language that the catch message lays over its window. Its catches are recorded in chronicle.dat's FISH chunk rather than save.dat's records, which name retail's kinds, and are kept when its document is gone.
The keep outcome puts a landed fish into the bag as its species' item, each with its size where the item keeps one, and gives points and records only where the place asks for them. A window's message overlay now answers its inserted system messages too, where a custom species' name goes in the catch message.
The brisk bite stage chooses one fish in the water for each cast by its liking for the bait, which swims over in its wait, nibbles for its liking's time and holds the hook for its window; turning or reeling in starts the cast over. DC_FISHING_TRACE prints a session's steps and each fish's actions.
A fight stage now begins a fight for each hooked fish. The tension fight is Dark Cloud 2's at its own pace of 30 ticks a second: Cross reels the line in against its tension, which builds faster than Dark Cloud 2's, and the line snaps at the top. When the fish veers, the stick held its way takes most of what reeling adds and winds faster, for a moment after the arrow too; held out of position it raises tension sharply. The fish is held at the line's end as it shortens, a species' pull sets how hard it fights, and the port draws the tension and line gauges.
The Rod trait carries Dark Cloud 2's rod numbers as Heft, Control, Spring, Balance and Lure, read only by the brisk bite and the tension fight: Heft grows the hooked fish, Control tires it, Spring eases its pull, Lure brings fewer empty rolls and loses less bait, and Balance scales the first three. Retail's Fishing Rod has the stats that change nothing, and a custom item may carry a rod of its own.
The pack in port/datapacks/enhanced_fishing gives every fishing place the brisk bite and the tension fight, each species its pull with retail's rod, and three rods whose Spring brings the Baron Garayan within reach.
Where the place's fight reads rods, Circle at the water opens the port's menu: the rods in the bag to fish with, the water redrawn with the place's odds, and the town's own menu. The rod chosen is used while it is carried and kept with the save.
Ruty, who studies fish in Queens, asks Toan to catch every inland fish, then a Mardan Garayan, then the Baron Garayan from her oldest fish book, and gives him a better rod for each. Quests can now read the game's system messages, so she names each region's towns. Circle at the water still asks retail's "Continue fishing / Quit fishing"; with Enhanced fishing the dialog framework draws it with Change rod between the two, which opens the item board with only the rods in the bag. Quit hands the town's script its answer, so each town's own way out of fishing plays. The bait board lists only bait, and its first row of slots no longer leaves a line of the town showing above it. The Enhanced pack's fish pull 0.5 to 2.0 and its rods spring 18, 33 and 80.
--jump load:<slot> and --jump autosave start in a save's town; --town-menu opens one of its menus and --town-event raises one of its programs, so a headless run can capture any fishing screen.
The bait board, the fish exchange and the fishing records draw with the UI framework's widgets and retail's pictures, the same as before pixel for pixel; the exchange and the records take the mouse. The dialog framework asks the sign's and Circle's questions in the scripts' own windows; with Enhanced fishing, Circle's adds Change rod, which opens the rods' board from the event as Square opens the bait's.
Cross on the tick a fish bites counted as an early press from a nibble, or as reeling in from the wait; it sets the hook now.
The fishing provider took the sign's and Circle's messages wherever the town's folder matched, a house's own scripts included, and Circle's when not fishing; a question left taken kept every script's pad and the menus' mouse. Providers are told whether a question is asked inside a house, fishing takes Circle's only while fishing, a message with no rows is left to the script, and a question is forgotten when its window closes, an event begins or ends, or a town starts. A confirmed row is held while the script's cursor catches up to it. The --town-menu bait list keeps its end marker.
Nothing builds or installs the pack; port/datapacks/README.md says how to copy it beside the data folder and name it in datapacks.json.
The build puts the packs the game ships in assets/datapacks beside the executable, where every package carries them. A pack named in datapacks.json that the player's datapacks folder lacks is taken from there, and a player's own copy wins, so turning Enhanced fishing on is naming it in that file.
A pack that changes a few items no longer has to carry a whole table. It holds a patch beside the table, items.patch.json (kind items_patch) and so on for every item table, listing only the rows and fields it changes. Each pack's patch is applied over the full documents, the lowest priority's first, so packs changing different items combine and a later pack's field wins. A patch is checked whole as a full document is, and one that cannot be used is reported and left out while the others apply. native_config::ReadEach passes every pack's document at a path, where Read stops at the first.
…rs open with their key, off by default gameplay/items/dungeon_keys.json says which items open each dungeon's locked room doors, floor exits and back floor entrance, as compiled from retail's dungeon scripts, and gives each its DungeonKey trait. gameplay/dungeon/rules.json turns two changes on (docs/DUNGEON.md); without it, and in the tracked document, both are off and the game plays as retail: - key_item_filter: an event's item window lists only the items its script takes, where retail listed the whole bag. - auto_open_locked_doors: Cross at a locked room door, told by the map part that placed its event, runs the door's script as Square does and gives its window the dungeon's door key when the bag holds one.
gameplay/dungeon/loot.json (kind dungeon_loot) holds the tables retail compiled into dungeonparts.cpp, by item ID: each dungeon's chest lists for a floor or either half, its back floor's, the clown's two offers, and how often each item is turned down. A pack's loot.patch.json replaces a dungeon's fields or changes its chances, the lowest priority's first, so a pack can put its own items in the boxes. Without packs the draws are retail's, random number for random number. Where items may lie on each map part, and how many floors a dungeon has, stay compiled. --export-gameplay writes the document too.
Each active pack's lang/<language>.json is read over the game's own strings, the lowest priority's first, and the save folder's over both, so a pack brings its quests' and settings' text in every language and can retranslate the game's. The strings are read again once the packs load.
gameplay/quests/<id>.json (kind quest) describes a quest without code: its stages, the rules that move it on, the choices it adds to a villager's menu and the steps each runs (pages, questions, items, money, a shop), its counters on the fishing log, and its English text, translated through lang/ as quest.<id>.<name>. Every active pack's quests are read at start-up after the items, the highest priority's copy of each file, so a pack adds or removes a quest without the game being rebuilt. A document that cannot work, or that names an item no item document has, is reported and left out. Carl's slingshot is now port/gameplay/quests/carls_slingshot.json, and Ruty's rods moves into the Enhanced fishing pack with its translations. Both keep their IDs, so saves keep their progress. The Mayor's supplies stays in C++.
…at limit the party The dungeon's floor select draws on dc::ui as retail drew it, compared pixel for pixel from a save in every state (fading in and out, moving, paging, scrolling with rows clipped at the board's edges, every dungeon), and takes the mouse: a hover puts the cursor on a floor, a click goes down to it, the wheel moves a floor. The framework gains what it needed: pictures clipped as retail's MenuTextureClip clips them, clipped numbers, a full-screen picture and fill, and a screen's own corners and hand. A floor that limits the party shows a red ! after its kill count, and with the cursor on it the window says what the limit is in the words the dungeon says it with on arrival. dungeon/limits.cpp knows the floors the scripts always limit (read out of retail's floor scripts) and the ones a new game rolls into the save, leaving out the fixed-map and boss floors a rolled limit never reaches. The warning changes nothing in play, so it is always on. In English the floors' numbers are drawn in the captions' font with the floor words. The dungeon's name plate is a blank plate (SirFancyBacon's art, CC0) with the name written on it from the game's own messages, in every language. --town-dungeon N opens a dungeon's floor select from a loaded save, for tests.
Circle on the floor select, which retail only refuses, asks "Leave dungeon?" in the battle menu's question window, with retail's own question (its message 300) in every language. Yes fades the select out and the dungeon ends as leaving it does: the party goes back to the dungeon's town, to the same place, recovered, and nothing is paid, as no floor was entered. The mouse answers the question as it answers the battle menu's. The dungeon's map in its town is DungeonTownMap now, which the escape and the floor select share.
The test seeded rand() with 1 to 400, one seed a fish. Windows' rand() gives nearly the same first numbers for neighbouring seeds, so its 400 fish were near enough one fish and never held the hook for both 30 and 60 ticks. One seed, with the numbers running on from fish to fish over 1,000 fish, draws the hold as the game does on every platform.
…indows too The port's rand() is the PS2's on every platform and returns 31 bits, but the framework's spawn and bite rolls divided it by the host's RAND_MAX plus one. Windows' RAND_MAX is 32767, so there a roll that should lie in [0, 1) ran up to 65536: spawns came out wrong and a hooked fish held the hook for millions of ticks. Divide by 2^31, as the rest of the port does. This is what failed FishHook on Windows. The test goes back to a seed for each fish: a30c10f blamed Windows' own rand(), which the port never calls.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Depends on #239: it uses the framework widgets added there (
Picture,PictureNumber,MenuHand,ListCursor). Until #239 merges, the diff againstmasterincludes its commits.What it does
The dungeon floor select is on
dc::ui. It draws as retail's did. I compared a build before the move and the build after on one renderer, starting from a save with--town-dungeon, in 49 states, and every one matched pixel for pixel. The states covered fading in and out, moving, paging, scrolling with rows half-clipped at the board's edges, entering a floor, and every dungeon. Framework additions:Picture::DrawClippedandClippedRows: clip a picture to a band of the screen as retail'sMenuTextureClipdid, quirks included;PictureNumber::DrawClipped;Picture::DrawScreenandScreenFill: a full-screen picture and a full-screen colour;StayFrame::DrawandMenuHand::Drawoverloads for a screen whose corners and hand are in its own picture.It takes the mouse:
It warns of floors that limit the party. A limited floor's row shows a red
!after its kill count. With the cursor on that floor, the window below gives the limit in the dungeon's own arrival message (dunmsd0011, 13 and 14), so every language has the wording. The warning changes nothing in play, so it is always on.port/src/dungeon/limits.cppknows two kinds of limit:Floor numbers and name plates:
port/lang/textures/plates/dungeon_name.png: SirFancyBacon's own art, CC0, at the disc's size. Its licence is in the README beside it, and SirFancyBacon is added toCREDITS.Test hook
--town-dungeon N: opens dungeon N's floor select once a loaded town is walking, so a save reaches the screen with its real floors, Atla and limits.Notes
!disappears for the few frames its row is sliding past the board's edge, instead of being cut off.res_limit_zone_idis plainchar. Reading it throughsigned charkeeps "no limit" (-1) correct on arm64, wherecharis unsigned.Testing
DungeonLimits(3) andDungeonPlates(2).ctest -j4passes on Linux, apart from the five Kazarov GPU tests that fail on lavapipe.