Set-Scoped Variables as Local Read/Write Memory, With Update Assets #1072
redwizard42
started this conversation in
Ideas
Replies: 1 comment
-
I'd be willing to help out with any implementation of this. My primary development language is C#, but I've used C++ and a wide variety of custom languages over the years and think I'm fairly adaptable to learn. I would need to bring myself up to speed on the existing code base, and also am aware that this would have implementation needs in rcheevos and RAWeb. |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
I came up with this idea separately, but I understand it's similar to the 'Option 2' from #780 - I made this its own request because I felt it was different enough in concern. It is for Achievement Set Variables that are somewhat analogous to RAM addresses, but are available for read and write. It calls for a new asset type for assigning variables, to be executed before any Achievement or Leaderboard assets, such that Achievement Set Variables will maintain a Prior, Delta, and Mem value that can be used in Achievement, Leaderboard and Rich Presence logic.
Proposal
After running into some very tough memory usage situations with Metroid Prime, I believe the time is ripe to strongly consider variables. First, let me present two such scenarios. One where it is impossible to do what is needed and one where it is very cumbersome, but squeezes by in the character limit thanks to overflow pointer offsets.
Scenario #1 - Impossible to get boss data within the character limit:
In order to get to a boss in Metroid Prime I need to find out which of 1024 pointers in the object table pointes to the boss. Each table entry is 8-bytes long (1x 4-Byte pointer and 2x 16-Bit indexes to link to previous and next entry). First entry is at +0x04 offset, second is at +0x0c, etc up to the 1024th entry.
To check slot 1 need to do this:
I probably should toss a null pointer check on top of that, however. That'd look like this:
5 conditions... x 1024 Slots = Impossible to not hit the character limit just trying to find the right pointer, let alone use it. This logic would also be checking around 2050 addresses in memory! Yikes.
However there is a nice static-in-RAM address that stores the "UID" of the boss. And from this we can calculate the index in the object array by doing this:
UID & 0x3FF
But that is the index. So we really need to do this to get it to the right offset:
(UID & 0x3FF) * 8 + 4
Doing this calculation is possible in the current toolkit via:
Cumbersome, But I suppose it works... except we have no way of using this as the offset into the object list:
Note: I wouldn't have to do an asset key check necessarily I know this is the right slot, so I could just go to whichever offset has the boss data I need, such as health a collection of UIDs for objects in the room the boss interacts with--something that will be needed for certain planned challenges, and that would then have offsets in the array calculated similarly to the boss and/or scanning the array for them.
Scenario #2 - Checking Event Flags is Crazy Amounts of Conditions:
Events and Item Pickups and other persistent states in the game are stored in a mailbox (one per world) that is a count of how many items there are plus 512 reserved slots that contain the 32-Bit BE asset key ID of the event/pickup/state. These are in arbitrary order, though any new incoming ones get put at the end of the array (the array gets regenerated when save data is refreshed, which happens when loading a game, and also after saving a game (where it just re-loads the world data after saving).
If we want to identify the thing that just happened, we need to check the count of items, and go to an offset of Count*4 from the start of the structure (the first 32-bits is the count, so it's kind of convenient). However since this object is behind a double pointer, we can't actually apply this as if it were an "index pointer".
Instead we have to check what the count is, and each slot individually. In Core, we check world, maybe room, and if the count increased by 1 (or two--some actions trigger two memory relays at the same time. I've yet to see one that does more, but this can be verified as play occurs).
Here's what an alt looks like:
We do that 512 times and the achievement logic comes out to A bit over 58000 characters, just to check for an event/item pickup!
Now we can work around it for item pickups by checking world and room IDs, and if the item was added to the player's "Inventory" (or the capacity increased by 5 for missiles) which has fixed offsets for each item. But for other events, such as defeating Meta-Ridley? 58000 characters. Leaves little room for anything else of interest.
It would be really useful if I could take
And multiply it by 4 and use it as an offset:
Being able to do this would avoid having to scan so many addresses.
Solution Proposed: Variables as Set-Scoped Read/Write Memory with new Asset to process assigning values
How to process the variable scripts?
A. Set Delta Value equal to the last Mem value for each variable.
B. Process all "VariableUpdater" assets
C. Value of each variable at this point in time is the Mem value. Intermediate values during processing don't affect Delta/Prior (It just keeps updating "Mem" if you need to use and assign it multiple times during the update)
D. If Delta != Mem, Set Prior = Delta for applicable variables.
After all this, the existing processing for leaderboards/achievements/etc would be performed, using the tracked values of the variables from this point on.
Anticipated Questions:
Why Global?
For my use case, a global use case would be helpful for me to always have the boss pointer calculated, or to always have the most recent event asset key or second-to-last asset key handy. Things that are frequently calculated and used across many achievements now get calculated once.
Why did I put the variable-to-be-assigned on the right?
Mostly because it seemed to flow better with the existing asset editing. An Add/Sub Source Chain still accumulates on the left-hand side, and so it seemed best to do it this way. I debated suggesting a Cmp column value for it of ->, but worried that those with a programmer background would thing "dereference" and not "assign to".
What happens if no locked achievements remain that need a variable?
Ideally we'd track this and only process a script that sets a variable if anything active is still using it. It may not be necessary for performance however and that should be studied. We could allow the user to specify somehow a condition for when the rest of a script should be processed. I suggest one such thing below.
Can variables be saved between sessions?
That is a consideration. My expectation would be that any initial implementation would not save variables between sessions. It may be desirable to flag variables as persistent between sessions (stored on-site, or volatile between sessions (thrown out).
Suggested Flags and Operands
Suggested Flags For Variable Updating:
Set Var - assigns the accumulated left-hand value to the variable specified on the right.
Set Var If - If this condition [chain] is true, the next flag that updates a variable will execute as normal. Otherwise it shall be skipped.
Process If - Intent would be to allow a set of these at the top of a script and if any of them are true, the processing will continue on to do the rest of the variable-updating in that asset. Otherwise it'll jump ship and move on to the next one. Unsure if this would actually save overhead, but the idea is if you had a bit variable update script where the variables were only needed for a few limited windows, you could help save time by doing this. In theory you could put it mid script and all the things Not necessarily a critical part of this proposal.
Suggested Operands for Variable Updating and Usage
Var
Delta Var
Prior Var
(And an equivalent set for float variables if that's a thing that needs to be distinguished at the operand)
Proposed Usage Examples
Note: I put variables in parentheses using friendly names for convenience. I suspect these may start out as just the index of the set-scoped variable, but perhaps friendly names can be defined and used for additional clarity.
Example #1: Set Var to calculate Boss Pointer Value. Values in parentheses would be index of the variable.
0x45c120 is the UID of the boss in Metroid Prime.
You might do this in a variable updater asset named "Boss Pointer Calculation"
Three steps to calculating this variable. 6 conditions. Very few memory addresses needed to look at Note: Using a variable as an offset would necessary to look at the correct sized data at that offset.
Example usage in achievement to check if Boss just died:
One thing of note is that this replaces a need for an "Add Index" type flag (which would also be ambiguous if the index to add was behind pointers itself!) as requested here: #1038
These three may be variables 0x0000 through 0x0002. They may have notes such as:
0x0000: "Boss Index in Object Table - Calculated from Boss UID"
0x0001: "Offset of Boss Pointer in Object Table - Calculated from Boss Index variable 0x0000"
0x0002: "Boss Pointer - Calculated Using Pointer to object table and Boss Offset variable 0x0001
+0x0268=Boss Health [Float BE]
+0x1038=UID of Flaahgra Mirror 1
+0x103a=UID of Flaahgra Mirror 2
+0x103c=UID of Flaahgra Mirror 3
+0x103e=UID of Flaahgra Mirror 4
"
Summary of Example #1: Instead of scanning over a thousand addresses, which are behind multiple pointers and so wouldn't fit in the character limit using the existing toolkit features, we check somewhere around 10 addresses at most to save the boss pointer, which can then be used to access only a handful of relevant data.
Example #2: A Process If for Boss-Data variables (whitespace added for reading clarity):
In the Variable Updater:
Set Var Value 0x00 Var (BossPointer)
Checks if Player and World Pointers are not null, and if the Boss UID exists (-1 when no boss)
Summary of Example #2: Rather than always process the boss pointer and boss data update logic every frame, it only is done when a boss is active. Otherwise, the boss pointer is set to 0 (null), which achievements can use as an additional condition check for triggering (i.e. Mem (BossPointer) != Value 0x00)
Example #3:A Set Var If - Hypothetical Shmup where the score value is stored as Score/10 and the ones-digit is the number of continues used. (Logic somewhat simplified)
VariableUpdate Logic:
Achievement Logic - "Score 100000 points in Stage Y"
This is a use case that replaces a need for "Add Source As Hits" which is sometimes requested, usually for things like achievements for individual-level score where a player isn't necessarily entering the level with a score of zero.
Summary of Example #3: Rather than try to anticipate every possible score increase and build wickedly long add hits chains to try to add up the points score in a stage where you enter with arbitrary score (something that often won't fit in the character limit and is cumbersome for maintenance), this would allow you to capture an initial value and use it for comparisons long after that frame has passed.
Beta Was this translation helpful? Give feedback.
All reactions