Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: GVar Dictionaries (aka variable "options") #773

Merged
merged 7 commits into from
Sep 18, 2023
Merged

Conversation

benloh
Copy link
Contributor

@benloh benloh commented Sep 17, 2023

See #755 for discussion.

GVar Dictionaries

GVar Dictionaries adds the ability to define multiple static key-value pairings for character (agent) properties and to conduct test operations on the dictionary values.

  • Dictionary items are defined via an addOption method for GVars.
  • A specific GVar property's value can be assigned to one of the predefined "option" values using the setToOption method.
  • GVar property values can be tested against dictionary keys using GVar methods. e.g. ifProp character.colour equalToOption RED checks if character.colour value is equal to the dictionary value #f00 (not the key RED).
  • Assignment of Dictionary items to a another character property or feature property needs to be handled via stack operations.

Example Scripts:

// options
addProp colour string 'black'
prop character.colour addOption 'RED' '#f00'
prop character.colour addOption 'GREEN' '#0f0'
prop character.colour addOption 'BLUE' '#00f'
prop character.colour setToOption 'RED'

// test assignment
propPush colour
dbgStack
// -> should log "STACK(enum0): ['#f00']"

// test equalToOption condition
ifProp character.colour equalToOption 'RED' [[
  dbgOut 'is RED!'
]]
// -> should log ''is RED'

// test notEqualToOption condition
ifProp character.colour notEqualToOption 'GREEN' [[
  dbgOut 'is NOT GREEN!'
]]
// -> should log 'is NOT GREEN'

// assign dictionary option to a feature property
prop character.colour setToOption `BLUE`
// -> should set character.colour value to `#00f`
propPush character.colour
featPropPop Costume.colorCSS
// -> should set the Costume feature's `colorCSS` property to `#00f`

NOTE GVar dictionaries only work in the context of the current character. You cannot reference global agent properties or properties from other characters.

Methods

General Options Methods

addOption GVar method

Add a new option to the designated GVar. Note options values match the GVar type, e.g. a string GVar's option will be a string, a number GVar's option would be string.

Syntax

prop <propName> addOption <optionLabel> <optionValue>

where

  • <propName> is the name of a property object reference. e.g. could be character.propname or agent.propname. or simply propname
  • <optionLabel> is the name of the option. Use this to refer to the options in the option methods. e.g. defining a 'RED' option label allows you to use a constant label to set a constant value, e.g. ...setToOption 'RED'
  • <optionValue> is the value that the <optionLabel> is mapped to, e.g. if the RED option label is set to the option value #f00, then setting a prop value to the option RED would set the prop to #f00.

Example

prop character.colour addOption 'RED' '#f00'

setToOption GVar method

Assign a prop value to the selected option value.

Syntax

prop <propName> setToOption <optionLabel>

where

  • <propName> is the name of a property object reference. e.g. could be character.propname or agent.propname. or simply propname
  • <optionLabel> is the name of the option. Use this to refer to the options in the option methods. e.g. defining a 'RED' option label allows you to use a constant label to set a constant value, e.g. ...setToOption 'RED'.

Example

prop character.colour setToOption 'RED'

equalToOption GVar method

Evaluates whether a prop value matches the selected option value. This is generally used with ifProp

Syntax

ifProp <propName> equalToOption <optionLabel> [[ ... ]]

where

  • <propName> is the name of a property object reference. e.g. could be character.propname or agent.propname. or simply propname
  • <optionLabel> is the name of the option. Use this to refer to the option value in the option methods. e.g. testing a property value against a 'RED' option label allows you to see if your property value matches 'RED''s value ('#f00').

Example

ifProp character.colour equalToOption 'RED' [[ ... ]]

notEqualToOption GVar method

Evaluates whether a prop value does not match the selected option value. This is generally used with ifProp

Syntax

ifProp <propName> notEqualToOption <optionLabel> [[ ... ]]

where

  • <propName> is the name of a property object reference. e.g. could be character.propname or agent.propname. or simply propname
  • <optionLabel> is the name of the option. Use this to refer to the option value in the option methods. e.g. testing a property value against a 'RED' option label allows you to see if your property value does not match 'RED''s value ('#f00').

Example

ifProp character.colour notEqualToOption 'RED' [[ ... ]]

Number Methods

greaterThanOption number-only GVar method

Evaluates whether a prop number value is greater than the selected option value. This is generally used with ifProp

Syntax

ifProp <propName> greaterThanOption <optionLabel> [[ ... ]]

where

  • <propName> is the name of a property object reference. e.g. could be character.propname or agent.propname. or simply propname
  • <optionLabel> is the name of the option. Use this to refer to the option value in the option methods. e.g. testing a property value against a 'LOW' option label allows you to see if your property value is greater than 'LOW''s value ('10').

Example

ifProp character.colour greaterThanOption 'LOW' [[ ... ]]

greaterThanOrEqualToOption number-only GVar method

Evaluates whether a prop number value is greater than or equal to the selected option value. This is generally used with ifProp

Syntax

ifProp <propName> greaterThanOrEqualToOption <optionLabel> [[ ... ]]

where

  • <propName> is the name of a property object reference. e.g. could be character.propname or agent.propname. or simply propname
  • <optionLabel> is the name of the option. Use this to refer to the option value in the option methods. e.g. testing a property value against a 'LOW' option label allows you to see if your property value is greater than or equal to the 'LOW''s value ('10').

Example

ifProp character.colour greaterThanOrEqualToOption 'LOW' [[ ... ]]

lessThanOption number-only GVar method

Evaluates whether a prop number value is less than the selected option value. This is generally used with ifProp

Syntax

ifProp <propName> lessThanOption <optionLabel> [[ ... ]]

where

  • <propName> is the name of a property object reference. e.g. could be character.propname or agent.propname. or simply propname
  • <optionLabel> is the name of the option. Use this to refer to the option value in the option methods. e.g. testing a property value against a 'LOW' option label allows you to see if your property value is less than 'LOW''s value ('10').

Example

ifProp character.colour lessThanOption 'LOW' [[ ... ]]

lessThanOrEqualToOption number-only GVar method

Evaluates whether a prop number value is less than or equal to the selected option value. This is generally used with ifProp

Syntax

ifProp <propName> lessThanOrEqualToOption <optionLabel> [[ ... ]]

where

  • <propName> is the name of a property object reference. e.g. could be character.propname or agent.propname. or simply propname
  • <optionLabel> is the name of the option. Use this to refer to the option value in the option methods. e.g. testing a property value against a 'LOW' option label allows you to see if your property value is less than or equal to the 'LOW''s value ('10').

Example

ifProp character.colour lessThanOrEqualToOption 'LOW' [[ ... ]]

Use Case

There are three basic needs:

  1. Define multiple Dictionary Items -- a list of items each consisting of a key that is assigned to a value.
  2. Be able to set a character property to a dictionary item by simply selecting the key from a list (rather than having to type it).
  3. Be able to test whether a character property matches a dictionary item key (rather than matching it to the dictionary value).

Four example use cases:

  1. Simple CONSTANT key with no value-- Assign an entityType property to a character designating it a PRODUCER, CONSUMER, or DECOMPOSER, and use a property operation test to trigger behaviors based on the selected entityType. No value is used.
  2. Assignment to a "numeric" value -- Assign a energyLevel property where LOW is 0, MEDIUM is 5, and HIGH is 10, and use a property operation test to trigger behaviors based on numeric thresholds, e.g. do something if energyLevel is higher than LOW.
  3. Assignment to a "string" value -- Assign a entityState property to a character designating it HUNGRY, CONTENT, FULL, or DEAD, displaying the selected value during exectution, e.g. HUNGRY would be displayed as the value Hungry.
  4. Assignment to a "string" value and use a Feature property to trigger special operations -- Assign a colour to a character property where RED = "#f00", GREEN = "#0f0", and BLUE = "#00f"and use the value to set a costume color.

Implementation History

  1. Original Request
  2. Trying Constants
  3. Hacking in Options + UI

1. Original Request

The original request was to provide a way to define constant-like declarations to use as part of GEMSCRIPT. For example:

  • Organism type: producer, consumer, decomposer
  • Energy level low (1), medium (5), high (10)
  • Fish state: hungry, content, full, dead

2. Trying Constants

After conferring with Sri, we felt that a cleaner implementation of "options" would be to emulate the notion of constants. The requested features were less "enums" and more like "dictionaries". We suggested the use of the keywords addConstants and constantPush as a way to implement dictionaries. The implementation was fairly complex but essentially introduced a new set of agent properties called constants to go along with the existing properties and methods properties. constants essentially replicated the functionality of properties as a parallel subsystem.

While this implementation does work, the problem was that GEMSCRIPT can only handle assignments and tests to a simple javascript string, number, or boolean. We could not support assignments or tests to objects that are properties of agents or feature properties. e.g. you cannot use prop character.colour setTo RED because the setTo method only supports the assignment of pure strings, not RED which would be a constant object. e.g. you cannot use ifProp character.colour equal RED for the same reason -- the equal method can only work with a simple string, not RED which is a constant object. The only way to assign values was to use stack operations. While this implementation "works", it is confusing for students and because of the complexity does not lend itself to "Let's try this" comment instructions.

See discussion of constants implementation

3. Hacking in Options + UI

The third approach was to implement a hybrid solution. This involves a two part solution.

First, it adds the ability to add and use "options" to existing GVars like class-sm-string, class-sm-number, and class-sm-boolean. By adding options directly to GVars, we can introduce GVar-specific options methods for use within the scope of the GVar. This allows us to do assignments and run tests on options. This also allows the simulation engine to compile and run code that references agent property-specific options during assignments and tests without fundamentally altering how the simulation engine works.

Second, although we can introduce "options" methods to GVars that support the simulation engine, using the Script Wizard to construct GEMSCRIPT for "options" manipulation is not as straightforward. The Script Wizard uses a complex system that parses script text into tokens that are then in turn used to construct the wizard UI. The system is designed to work with standard keywords and syntax. But "options" do not quite adhere to the way keywords are handled, as a result, it has no way of constructing a list of user-defined "options" for the user to select -- we can generate lists of properties that have been added via code (e.g. user-defined character properties), but not the derivative methods (where the "options" are defined).

The workaround is to do two things:

  1. Add an extra compile loop that is used purely for pulling out the list of "options" that have been defined for the blueprint, and...
  2. Inject a custom UI into the standard Script Wizard UI to render and allow selection of an "option" from the list of possible "options" (emulating the approach taken with the Comment Styles selection interface).

This way the code data remains pure, can run on the standard simulation engine, and wizard hacks are confined to the purely visual rendering side.

LIMITATIONS with this approach

  • Cross-agent options do not work. e.g. you can't define an option for the global agent and use it with the Fish agent. Similarly, a Fish agent's option is completely separate from a Algae agent's options.

  • We also cannot completely get around the need for some stack operations when working with GVar Dictionaries. E.g. if we're working with a character prop and you need to reference a featProp, you can only do cross-type assignments using a stack operation. e.g.

// 1. set a character prop
prop character.colour setToOption 'RED'
// 2. this is illegal because 'RED' is defined in the context of the character prop, the featProp
~featProp Costume.colorHex setToOption 'RED'~
// 3. instead, use a stack operation to do a cross-type assignment
propPush character.colour
featPropPop Costume.colorCSS
// -> assigns the value of character.colour option 'RED' (value = "#f00") to the Costume.colorCSS property

Technical Aside

[In which Ben tries to document different approaches to the problem and serve as reminders of how the system works...feel free to skip.]

Bundle the option symbols as part of normal compile

One possible approach to streamline the "options" implementation: If we could bundle the "options" definitions as part of the tokens used to render the wizard, then we do not need a extra separate compile cycle.

  • How does addProps figure out the list of newly defined props?
    addProps defines new prop names that are listed when editing a prop script line. So in wizard slot editor, the interpreted bundles do contain a list of the prop names as keys to the prop object (e.g. props: { colour, scale, ...}).
  • How are the bundles constructed? Bundles are constructed during the compile process. script-compiler calls:
    • script-compiler.CompileBlueprint, which breaks the blueprint into single lines in
    • script-compiler.CompileStatement, which breaks down into
    • script-compiler.DecodeStatement, which in turn is broken down into tokens in
    • script-compiler.DecodeToken, which returns the single token.
    • When adding a new prop, we deconstruct the statement into four tokens (e.g. addProp colour string 'black':
      a. keyword: addProp
      b. propName: colour
      c. GVar type: string
      d. GVar arg: black
    • During compile for addProp, the fourth token for "b. propName" is added to an array of newly defined property names.
    • Later, when processing a statement that references the prop keyword, we look up the list of previously defined propNames to provide a list of options. This also requires processing 4 tokens (e.g. prop character.colour setToOption 'RED'):
      a. keyword: prop
      b. propName: character.colour
      c. GVar method: setToOption
      d. GVar method argument: RED
    • WHERE ARE THESE propNames stored and retrieved from? In order to show the list of available predefined propNames when defining the second propName slot, class-symbol-interpreter has to look up the available propNames.
      • Bundler adds symbols by retrieving all the symbols for each keyword (e.g. each keyword has a symbolize function that generates all the available symbols). This is fed to...
      • ...script-bundler.AddSymbols constructs the features, and props (and constants if we keep that) lists from the symbol data generated by the bundler. Everything else is derived from that as symbolData.
      • ...which eventually leads to...
      • ...EditSymbol_Block.f_renderchoices generates the list of options from symbolData
      • ...which is constructed from symbol-utilities.DecodeSymbolViewData generates lists based on props (and constants if we keep that).
    • If we wanted to add propOptions to the list, it would need to happen here in script-bundler.AddSymbols. We would probably either:
      • a) figure out how to shove it into props, or
      • b) figure out how to shove it in as constants (if we decide to implement constants)
      • c) add a new symbol set propOptions or something like that?
    • This means we either add the symbolization in the prop keyword (if we're adding symbols to the addOption keyword), which is awkward, because we would then be diving down to the next level to process prop methods in order to symbolize values...
    • ...or we introduce a new addConstant or addPropOption keyword that allows us to symbolize the keywords separately. BUT, then we would lose the ability to use prop methods to reference stored dictionary values (since they no longer a part of the prop object)?

SO...we either have a nicely constructed menu of symbols that cannot be compiled, or we have a compilable script but a wizard UI that is not able to generate the list of symbols.

  • This is where things get even more complicated. In order to provide a list of options (RED, GREEN, BLUE) for the 4th token "d. GVar method argument", we need to look up the options that have been defined for the second token "2. propName".
    THIS IS WHERE using a constant is potentially easier? e.g. rather than having to look up the propName, and then look up the option stuffed into the propName, we can use looking up addConstant keyword the same way that addProp works. But again, we have nice UI, but the code can't compile because we can't reference the option's value.

CONCLUSION: Although it's less than ideal, the "simple" solution is to use the hybrid approach:
a. define options as part of the GVar
b. during wizard construction, scrub the script text to pull out "options" that have been defined in the code
c. inject a menu selection UI into the normal wizard to allow selection of the "option"
d. during code compile and execution, the "option" references the value of the "option" (e.g. #f00) rather than the "label" (e.g. RED).


To Test

  1. git checkout dev-bl/enum
  2. npm run gem
  3. Edit a blueprint, adding the following lines to any blueprint:
# BLUEPRINT enum
# TAG isCharControllable true
# TAG isPozyxControllable false
# TAG isPTrackControllable false

# PROGRAM INIT
addFeature Costume
featProp character.Costume.costumeName setTo 'AQ_algae.png'

// STRING OPTIONS
addProp colour string 'black'
prop character.colour addOption 'RED' '#f00'
prop character.colour addOption 'GREEN' '#0f0'
prop character.colour addOption 'BLUE' '#00f'
prop character.colour setToOption 'RED'

// test assignment
propPush colour
dbgStack
// -> should log '#f00'

// test equalToOption condition
ifProp character.colour equalToOption 'RED' [[
  dbgOut '1 is RED!'
]]
// -> should log ''is RED'

// test equalToOption condition
ifProp character.colour notEqualToOption 'GREEN' [[
  dbgOut '2 is NOT GREEN!'
]]
// -> should log 'is NOT GREEN'



// NUMBER OPTIONS
addProp hungerLevel number 0
prop character.hungerLevel addOption 'LOW' 0
prop character.hungerLevel addOption 'MED' 50
prop character.hungerLevel addOption 'HIGH' 100
prop character.hungerLevel setToOption 'MED'
ifProp character.hungerLevel equalToOption 'MED' [[
  dbgOut '0 Fish hunger is not MED!'
]]
ifProp character.hungerLevel notEqualToOption 'HIGH' [[
  dbgOut '1 Fish hunger is not HIGH!'
]]
ifProp character.hungerLevel greaterThanOption 'LOW' [[
  dbgOut '2 Fish hunger is greater than LOW'
]]
ifProp character.hungerLevel greaterThanOrEqualToOption 'MED' [[
  dbgOut '3 Fish hunger is greater or equal to MED'
]]
ifProp character.hungerLevel lessThanOption 'HIGH' [[
  dbgOut '4 Fish hunger is greater than HIGH'
]]
ifProp character.hungerLevel lessThanOrEqualToOption 'MED' [[
  dbgOut '5 Fish hunger is greater than MED'
]]



// BOOLEAN OPTIONS
addProp isDead boolean false
prop character.isDead addOption 'DEAD' true
prop character.isDead addOption 'ALIVE' false
ifProp character.isDead equalToOption 'ALIVE' [[
  dbgOut '1 Fish isDead is ALIVE!'
]]
ifProp character.isDead notEqualToOption 'DEAD' [[
  dbgOut '2 Fish isDead is not DEAD!'
]]
  1. The web console on "Main" should show the following output:
    screenshot_1320

This should demonstrate:

  • adding a new option
  • setting a property to an option
  • testing an option's assignment

@jdanish I'm still writing this out, but it's good enough to try out. number and boolean haven't been implemented yet.

TO DO

  • Add number and boolean tests
  • Add method definitions
  • Stash or push constant-abandoned
  • Add "To Test" instructions

@benloh benloh requested review from jdanish and dsriseah September 17, 2023 01:13
@jdanish
Copy link
Contributor

jdanish commented Sep 17, 2023

What's in there looks good!!

Question: is there a reason we won't be able to check other characters or global? Long-term they'd be nice to have. I might even prioritize them over other things when we re-evaluate our list.

@jdanish
Copy link
Contributor

jdanish commented Sep 17, 2023

Also a suggestion: when you update the to test section it might be good long-term if the comments indicating the output are valid Gem-script since the current ones crash the system. Easy enough to delete so don't bother if it's a hassle.

@benloh
Copy link
Contributor Author

benloh commented Sep 17, 2023

Question: is there a reason we won't be able to check other characters or global? Long-term they'd be nice to have. I might even prioritize them over other things when we re-evaluate our list.

Ironically, our wizard hack will allow us to construct the UI to support other characters and global characters, but the sim engine/compile side will not.

It occurs to me however, that we could introduce a new keyword similar to when that explicitly sets one character prop to another. So perhaps something like this (with a better keyword name).

// simple approach -- set one prop to another prop not using options
# BLUEPRINT Global
addProp 'GOOD' string 'GOOD'
addProp 'EVIL' string 'EVIL'

# BLUEPRINT Shark
addProp alignment string
propSetTo character.alignment global.GOOD

So propSetTo would use a two part column selector where you first select the agent, followed by the props that have been defined. In this case, we wouldn't have a popup menu, but just the standard prop selector. We would not need to use options at all.

If you want to set props just for the character, you would just use standard props:

# BLUEPRINT Shark
addProp 'GOOD' string 'GOOD'
addProp 'EVIL' string 'EVIL'
addProp alignment string
propSetTo character.alignment character.GOOD

And for tests, we would use:

# BLUEPRINT Shark
ifPropEqualTo character.alignment global.GOOD [[ ... ]]

(but this means we have to introduce a ton of keywords for all the math operations).

We probably would not want to use options at all as the constructs would get REALLY hairy.

// options would require...
# BLUEPRINT Global
addProp ALIGNMENTTYPE string
prop ALIGNMENTTYPE addOption 'GOOD'
prop ALIGNMENTTYPE addOption 'EVIL'

# BLUEPRINT Shark
propSetToOption character.alignment global.ALIGNMENTTYPE.getOption GOOD
// -> REALLY confusing

It's definitely awkward, and I'm not sure this would actually work. I'd have to confer with Sri to see if this is a terrible hack that breaks all kinds of rules. This will probably eat up another week (it'll be another half week to clean up and add support for numbers and strings already). Did you want to revisit the priorities before we do this?

Also a suggestion: when you update the to test section it might be good long-term if the comments indicating the output are valid Gem-script since the current ones crash the system. Easy enough to delete so don't bother if it's a hassle.

Yeah, good suggestion. I think I'm actually using a working test script, but may have taken a shortcut.

@jdanish
Copy link
Contributor

jdanish commented Sep 17, 2023

OH, never mind. I think we are good. I wanted to do the following, in Beaver and both appear to work.


when Beaver touches Twig [[
  ifProp global.season equalToOption 'FALL' [[
    dbgOut 'IT IS FALL'
  ]]

  ifProp Twig.type equalToOption 'CORRECT_TYPE' [[
      dbgOut 'IT IS CORRECT'
  
  ]]
]]

The one other case I can imagine is something like color it might help to do something like the following, possibly even via a feature:


// in global ... 
addProp colour string 'black'
prop character.colour addOption 'RED' '#f00'
prop character.colour addOption 'GREEN' '#0f0'
prop character.colour addOption 'BLUE' '#00f'
prop character.colour setToOption 'RED'

// in init for something else (fish?)

addProp coulour
prop character.colour copyOptions global.colour

// or maybe something like
copyProp global.coulour

The idea being that it'd sync them up. HOWEVER, I think we have what we need for now, and moving forward on the GUI is ideal next-step? Thanks!

@benloh
Copy link
Contributor Author

benloh commented Sep 17, 2023

prop character.colour copyOptions global.colour
Remember, we can't access other agents as the target of assignments. So we can't reference global.colour as the argument for the copyOptions method. This is why we have to introduce a keyword (propSetTo) that can access two separate agents as arguments to a keyword.

copyProp might be doable if we use the dual-agent reference method for when -- we have to add references to both agents. Then you might be able to do something like this:

# BLUEPRINT Global
addProp colour string
prop colour addOption 'RED' '#f00'
prop colour addOption 'GREEN' '#0f0'

# BLUEPRINT FIsh
addProp colour string
copyProp Fish.colour global.colour
prop Fish.colour setToOption 'RED'

But accessing options for featProps would still be a problem.

For example, assuming we add a Costume featProp called colorCSS if we did not use globals, we could potentially do this:

# BLUEPRINT Fish
featProp Costume.colorCSS addOption 'RED' '#f00'
featProp Costume.colorCSS addOption 'GREEN' '#0f0'
featProp Costume.colorCSS setToOption 'RED'

But we might have to introduce a copyFeatProp command that only works on predefined featProps across agents, e.g.

# BLUEPRINT Global
addProp colour string
prop colour addOption 'RED' '#f00'
prop colour addOption 'GREEN' '#0f0'

# BLUEPRINT Fish
copyFeatProp Costume.colorCSS global.colour
featProp Costume.colorCSS setToOption 'RED'

Otherwise, as before we'd have to revert to using stack operations:

# BLUEPRINT Global
addProp 'RED' string '#f00'
addProp 'GREEN' string '#0f0'

// Set
# BLUEPRINT Fish
propPush global.RED
featPropPop Costume.colorCSS

@jdanish
Copy link
Contributor

jdanish commented Sep 17, 2023 via email

@benloh
Copy link
Contributor Author

benloh commented Sep 17, 2023

If you have specific ideas for how you'd like to set colors we can see which of these approaches would be cleanest. But basically any color work that involves Features will at the very least need some new Feature methods/props to handle colors within agents, and at worse use stack operations across agents.

@jdanish
Copy link
Contributor

jdanish commented Sep 17, 2023

Short term, we can use the stack operations and the feature Costume.colorCSS you had listed above (but not implemented) if that is doable? Then we can see what we use, etc, and go from there? Long-term I'd rather we find a way to override color with a color picker (maybe in the next grant)?

@benloh
Copy link
Contributor Author

benloh commented Sep 18, 2023

@jdanish I believe I have numbers and booleans working now. Please see the updated test scripts.

@benloh benloh changed the title WIP: Feature: GVar Dictionaries (aka variable "options") Feature: GVar Dictionaries (aka variable "options") Sep 18, 2023
@jdanish
Copy link
Contributor

jdanish commented Sep 18, 2023

Looks good!

@benloh benloh merged commit c80eeb2 into dev-next Sep 18, 2023
@benloh benloh deleted the dev-bl/enum branch September 18, 2023 22:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants