RetroAchievements / RAIntegration

The DLL responsible to integrate emulators with RetroAchievements.org
https://retroachievements.org
MIT License
97 stars 23 forks source link

Achievement Variables / Memory #478

Closed gdeOo closed 3 years ago

gdeOo commented 5 years ago

This is a toolkit feature request / suggestion / discussion.

The aim is to allow achievements to have their own memory and versatile ways of manipulating it. This would allow for triggers based on past events that the game itself may not "remember".

This is a long post, so here's a sneak preview:


Option 1: Hit targets as variables

Achievement developers can already work with their own memory using Hit Targets, which can be thought of as variables in a programming sense.

These variables are, however, very limited in how they can be manipulated: they can only be incremented by 1 or reset to 0. They can also be incremented by more than 1 using AddHits, though with additional caveats.

SubHits

The simplest extension would be the creation of a SubHits flag. Just by itself this flag would enable much more complex use of hit targets as variables.

Motivating example that I ran into myself:

(Add|Sub)SourceAsHits

Add the currently accumulated value (from a simple memory access, from a sequence of AddSources, etc...) to the Hit count.

This would enable additions to and subtractions from the variables (hit targets) without having to rely on a fixed-length chain of (Add|Sub)Hits.


Option 2: Variables as a new concept

Even though we can "abuse" hit targets for our variables, I think it might be preferable to introduce them as a new concept.

I think the following approach could work with the current achievement serialization & editor:

StoreVar Value(1234) Var(0) // variable 0 = 1234

AddSource Mem(0xabcd) StoreVar Value(1234) Var(0) // variable 0 = (value at 0xabcd) + 1234

AndNext Mem(0xabcd) != Value(0) // if value at 0xabcd is not zero... StoreVar Mem(0xabcd) Var(0) // store it in variable 0

  * `AddToVar` & `SubFromVar` with obvious semantics similar to `StoreVar`.

#### Bonus: variables as the main avenue for multi session

The set of variables used by an achievement could be the perfect thing to store in order to support multi session achievements.

Since variables are named (through their index), they have a consistent meaning across revisions of an achievement, which is not the case for hit targets. Developers can leverage this fact to ensure changes to a multi session achievement are backwards compatible, for example.

----

### Bonus: More value manipulation operators

If variables become a thing, the only thing keeping the toolkit from nearly becoming a full blown programming language is the lack of ways to manipulate values. I've seen some other issues that request specific value manipulation instructions: #417, #184, maybe more.

To keep the achievement editor as is, operations on values would probably have to be implemented as conditions with operation-specific Flags operating on the currently accumulated value. For binary operations, the right side would provide the second operand. The resulting value would then become the currently accumulated value, available for the next condition to use.

Multiply Mem(0xabcd) Val(2) // inlined 1st and 2nd operands

AddSource Value(1) Multiply Mem(0xabcd) Val(2) // accumulated + inlined

ToBool Mem(0xabcd) // unary operator


There might be a problem here in that I'm not sure if the existing conditions can have empty left sides. If they can't, we could still use the value manipulator operations like an AddSource though. For example:

ToBool Mem(0xabcd) ResetIf = 1 // might not be ok

ToBool Mem(0xabcd) ResetIf Value(0) = 1 // awkward, but ok because ToBool acts like an AddSource



Some useful operator off the top of my head: `Multiply`, `Divide`, `Remainder`, `Abs`, `ToBool`, `Shift(Left|Right)`, `BinaryAnd`, `BinaryOr`, `BinaryXor`, `BinaryFlip`.

----

I'm sure I haven't thought of everything. If there's interest, let's iterate on this.

Also, I only provided one motivating example, but I can provide more if necessary.
televandalist commented 5 years ago

Multi-session is, without a doubt, my most wanted feature that isn't screen rotation. Not only for achievements involving long-term goals, but I feel like "a password used must be a password earned" could be made possible with it.

<3 <3 <3

Keltron3030 commented 5 years ago

I used to express the need for variables early on, but with the coming of andnext the need has lessened somewhat in my opinion. There was previously talk of incorporating Lua scripting but some felt that would be overkill for our needs.

There is of course always the consideration of ensuring compatibility with Retroarch and any solution of this kind must ensure that compatibility is maintained given a large swath of our users rely on that software to play on their various devices, myself included.

In any case I am always in favor of a solution that expands what's possible, like adding multi-session for example. My only desire would be for the more advanced features to be implemented with an intuitive interphase since the biggest benefit to the current tools is they are relatively accessible to people of all skill levels and makes for a low access barrier where "anyone can make achievements".

Jamiras commented 5 years ago

I've imagined variables in a slightly different way. Each variable would behave like a leaderboard value. It could be a constant (to handle regional differences), mathematically computed or be a resettable HitCount counter. Variables would be updated once each frame (before evaluating the achievements/leaderboards which might reference them). Each variable could be referenced by multiple achievements to avoid duplication.

And as you've suggested, variables could be marked to be stored between sessions.

My thoughts on #417 were that Multiply would not be a flag. Values that can be multiplied would have no left side, and the "blank" right side would be replaced with the multiplication logic. You might do something like this to check if a value multiplied by 5 is greater than 100 (though it would be simpler to just compare the value to 20):

AddSource Mem(0xabcd) * 5.0
          0 > 100

The multiplier would be a floating point value to simulate division and for consistency with leaderboard values.

Regarding the additional operators:

gdeOo commented 5 years ago

I was mostly interested in variables as a way of memorizing prior data/events that the game does not itself memorize. If I understood correctly, the way you've imagined them, @Jamiras, has more to do with reusing expressions, though I'm kinda suspicious that I did not fully grasp your idea.

The HitCount counter you mentioned could be used as a variable in the sense I suggested, but only if (Add|Sub)SourceAsHits were implemented. Otherwise we're still limited to the unitary increments + reset to 0 logic. A variable that is not bound to this limitation would be preferable IMO.


Regarding the scenario for using a memory value as hits, here's an example I actually ran into:

Using actual variables, I was imagining this solution:

ResetIf   0x<in_puzzle>              = Value(0)   // only process when in the puzzle (also reset variables to 0)
AndNext   Mem(0x<turn_just_started>) = Value(1)   // if turn just started:
AddSource Mem(0x<current_num_moves>)
StoreVar  Var(0)                       Var(0)     //   total_moves = total_moves + current_num_moves
ResetIf   Var(0)                     > 6          // reset if total_moves > 6
          Mem(0x<puzzle_solved>)     = Value(1)   // trigger if puzzle is solved

The HitCount based solution would be something like this:

ResetIf         0x<in_puzzle>              = Value(0)
AndNext         Mem(0x<turn_just_started>) = Value(1)
AddSourceAsHits Mem(0x<current_num_moves>)
SubHits         Value(1)                   = Value(1)
ResetIf         Value(1)                   = Value(1)  (Hit Target = 7)
                Mem(0x<puzzle_solved>)     = Value(1)

Due to the small numbers involved, this scenario can be solved with the current toolkit, but I think it still illustrates my intent well.

How would this solution be with the way you imagined variables?


Regarding the value manipulation operators, they were a bit of an after-thought, and I'm fine with everything you said @Jamiras. The only advantage to having them all would be clearer intent when reading an achievement's code, but the number of flags could become unnecessarily overwhelming.


I had not considered sharing variables across achievements, though it makes perfect sense (for the password scenario that @Televandalist mentioned, for example). But I also see many variables being useful in a single achievement, so maybe we could have local & global variables?

Global variables would have to be explicitly reset, instead of reseting along with the cheevo. There would also be an issue with inter-achievement sincronization if StoreVar were allowed on them. Maybe @Jamiras' idea of calculating variables every frame before evaluating achievements would work better in this case.


Regarding the interface, @Keltron3030, I'm sure there would be more intuitive ways of implementing this. My suggestion was meant to work inside the limits of the current achievement editor, because I get the impression that changing it is not a good idea. It would also have the least impact on developers that wouldn't want to use these features.

@Jamiras' idea of having variables separate from achievements would probably need a separate interface though, so maybe there's an opening to do things outside the constraints of the editor.