phylll / mychs-macro-magic

A simple, sane, and friendly little scripting language for your Roll20 macros.
MIT License
0 stars 0 forks source link

Develop reusable code libraries for MMM #32

Closed phylll closed 2 years ago

phylll commented 2 years ago

There is too much complicated code being stored in various copies across scripts, which leads to annoying and avoidable bugs. Roll20's chat syntax makes it easy to parcel out MMM code between different global macros or character sheet abilities:

!mmm script
!mmm   chat: Once upon a time there was an innocent MMM script that knew no evil.
%{BigNastyCodeLibrary|bigBadWolfCodeSnippet}
!mmm   chat: Operating happily ever after.
!mmm end script

Which may or may not, depending on the contents of the ability bigBadWolfCodeSnippet in the character sheet BigNastyCodeLibrary, produce output like this:

Once upon a time there was an innocent MMM script that knew no evil.
AND THEN CAME THE BIG BAD WOLF AND ATE THE LITTLE SCRIPT.
Operating happily ever after.

The narrative continuity problem points toward the key drawback: there is no protected namespace or anything similar, no guarantee that either the code in the very pedestrian "code library" receives exactly the variables it expects, nor can the main script be sure that its variables will be left untouched or touched only in expected ways.

Still, some basic validation of key variables is easy to implement for both library snippets and main code elements after a library snippet was called. Some sane coding practices and supporting conventions for easier debugging may help make this a feasible option and leverage its benefits for reusable standard code.

phylll commented 2 years ago

Started some work in https://github.com/phylll/mychs-macro-magic/tree/dev/mgd%2Freusable-code-libraries

phylll commented 2 years ago

Here are the precautionary coding standards I've come up with in implementing three such library snippets so far in this branch:

  1. "Library snippets" are prefixed with lib in Roll20 storage (abilities or global macros) and internal names (see below), and ideally stored in a character sheet of their own, separate from individual scripts (I'm using MacroSheetLibrary).

  2. Any script using library snippets sets a variable called CURRENT to identify itself in a way that helps to understand which part of the code is being executed in a debugging situation: debug chat: ${CURRENT} foo bar

  3. Any library snippet begins and ends with temporarily shadowing CURRENT with its own identifier, while storing the previous value in PARENT, so that both variables together tell us which library snippet is being executed as part of which parent script at any moment.

This makes for this skeleton code for a new library snippet:

!rem // begin MMM fragment
!mmm   set PARENT = CURRENT
!mmm   set CURRENT = "libFlushExchange 1.0.0 (2021-12-24)"
!mmm
[...]
!mmm
!mmm   set CURRENT = PARENT
!mmm   set PARENT = undef
!rem // end MMM fragment
  1. Variables used both outside and inside a library snippet are prefixed with _ and all data exchange between library code and script code should be handling using such prefixed variables, to avoid confusion with "local" script variables and "local" library snippet variables. Only exception is access to globally available data such as the MMM Midgard data exchange sheet, for which there is a global library snippet that initializes and validates access to the sheet and provides a global set of variables prefixed by m3mgd.

When the defense script calls the data table output snippet stored in libDefenseDataTable, it looks like this now -- a bit cumbersome but very clear, and allowing for necessary conversations where the script and the library snippet treat variables in slightly different ways, as with the two instances of 0/1 vs. false/true below:

!rem   // Output data table using data table execution code from library
!mmm   set CURRENT = scriptVersion
!mmm   set _GMSilentMode = (cGMSilentMode == 1)
!mmm   set _ownID = cOwnID
!mmm   set _weaponLabel = cWeaponLabel
!mmm   set _armorPiercing = (armorPiercing == 1)
!mmm   set _defenseSuccess = defenseSuccess
!mmm   set _defenseResult = defenseResult
!mmm   set _prvEndurance = endurance
!mmm   set _maxEndurance = m3mgdExchange.(cEnduranceAttr).max
!mmm   set _newEndurance = newEndurance
!mmm   set _prvHealth = health
!mmm   set _maxHealth = maxHealth
!mmm   set _newHealth = newHealth
!mmm   set _timeToDie = timeToDie
%{MacroSheetLibrary|libDefenseDataTable}
  1. Variables used only inside a library snippet, i.e. variables that we would like to be treated as local to that snippet, should be prefixed by two underscores: __myLocalCounter. Seemed logical and all-but-impossible to use by accident in a script only to find it overwritten by some library snippet, even if it makes writing library snippets a bit of a pain since it is pretty easy to mix up single-underscore-prefixed variables with double-underscore-prefixed-variables. Best to use quite different names for these two groups.

  2. While self-documenting code isn't quite possible, this convention for comments at the beginning of every library snippet helps me keep track of variables logically considered "imported" to and "exported" from snippets:

!rem // libFlushExchange: delete everything from the exchange data structure
!rem // 
!rem // Expects ("imports") variables:
!rem //   CURRENT
!rem //   
!rem // Sets ("exports") variables:
!rem //   _m3mgdExchangeAttributesFlushed   Number of attributes set to "" in both their current and max values
!rem // 
!rem // begin MMM fragment
!mmm   set PARENT = CURRENT
!mmm   set CURRENT = "libFlushExchange 1.0.0 (2021-12-24)"
[...]
michael-buschbeck commented 2 years ago

This topic makes for some intriguing holiday reading :)

I've felt inspired to think about MMM support for custom functions. Shouldn't be too difficult to implement. Syntax is more interesting to ponder.

Define function – scoped to the script that contains the definition, like variables:

!mmm function foo(bar, baz)
!mmm     chat: Foo, my ${bar} is ${baz}'d!
!mmm     return sender.(bar & "_" & baz)
!mmm end function

Call function – like any other:

!mmm do foo("ammo", "use")
!mmm set remaining = foo("life", "challenge")

That's the baseline. What else?

michael-buschbeck commented 2 years ago

Notes from michael-buschbeck#160's current state of implementation:

I'm planning to make it so that functions defined outside a script block are stored per-player in the API sandbox session, and all scripts run by that player can access them. A library macro would then simply be a bunch of !mmm function blocks, and it'd need to be run only once per session (or after an update).

phylll commented 2 years ago

https://github.com/michael-buschbeck/mychs-macro-magic/issues/160 makes this issue redundant, closing.