Closed gdeOo closed 3 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
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".
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:
SubHits
seems reasonable enough and is a good complement to AddHits
AddSourceAsHits
/SubSourceAsHits
- I'd need to see a use case for this where it's desirable to modify a HitCount by a memory value. Variables would be a better way to capture a complex value. Then you'd compare the variable to a constant instead of comparing a HitCount to a target HitCount.Multiply
and Divide
would be handled through AddSource
, SubSource
, AddHits
and SubHits
via #417. ShiftLeft
and ShiftRight
are just multiply/divide operations with power of 2 operands. I'd expect users to just use the Multiply
and Divide
functions, as most developers aren't programmers and probably wouldn't understand bit shifting. See also my later note on bit operations.Remainder
and Abs
would be nice to haves. I've already implemented Abs
once using existing semantics. I'd recommend renaming them to ModulusSource
and AbsSubSource
.Binary
operators would have minimal use as we already allow direct access to individual bits. Any logic that could be applied across multiple bits via the Binary operators could be applied to the individual bits and ANDed or ORed together using multiple conditions/alt groups/AndNext
/OrNext
. Most of the proposed operators modify the data, so would only be applicable to generating variables. When processing triggers, you just look at memory.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.
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:
SubHits
: just 2 alts, one for each side. Alt for the right side would be:(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:
Var
to use the value of a variable in other conditions, i.e.,Var
instead ofMem
,Delta
,Value
, ...StoreVar
flag to store the currently accumulated value into the variable with a given index. Left side of the condition would provide a value or a memory reference, whilst the right side would provide the index.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
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
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