Closed st-pasha closed 7 months ago
Thanks for the proposal, you may have seen the recent Yarn Spinner 3 expectations announcement that there is a section on enums. Based on the excellent breakdown in this proposal we agree that expanding the available types in Yarn Spinner is desirable.
While enums aren't as advanced as what you are proposing we feel they capture a lot of the same expressiveness and flexibility. Thanks so much for the proposal.
Introduction
Similar to how users can extend Yarn with custom commands and functions, this feature would allow users to define variables of custom (user-defined) types.
Rationale
Currently, Yarn language supports 3 types only:
bool
,numeric
, andstring
. Which may be fine for some purposes, but too limited for others. There are situations where users may want to have more advanced types:List
,Set
,Map
,GameObject
,Item
,Quest
,Location
,Time
, etc. Instead of supporting all this variety natively, we can allow the developer to define their own variable types, as many as they need.I believe this feature would have very large benefit for the developers, while having a relatively small impact on the Yarn language itself.
Proposed solution (Detailed design)
In Yarn, we introduce a new variable type:
object(T)
, whereT
is a string representing the backend type of the variable. A typeT
must be explicitly registered with theYarnProject
for a variable of typeobject(T)
being usable; though it is up to the implementation whether to enforce this restriction at compile- or run-time.There are no literals corresponding to
object(T)
. Instead, values of this type can be returned from user-defined functions, or passed to user-defined functions. The compiler should distinguish between variables of typeobject(T1)
andobject(T2)
-- these should be considered as distinct types for the purpose of type checking.When the developer defines the type
T
, they have an opportunity to provide a list of user-defined functions associated with this type. These functions will be called "methods", and they are namespaced toT
, in the sense that different typesT1
andT2
are allowed to have methods with same names.The Yarn language introduces a special
.
syntax for invoking methods:is equivalent to calling function
foo($x, $a, $b)
, wherefoo
is looked up in the namespace of$x
's type.Some of the methods defined for type
T
can have special names:operator+
,operator*
,operator==
,operator<
, etc. These will be used for defining operators between values of typesobject(T)
:$x + $y
,$x == $y
, etc. A binary operation will be considered valid if the corresponding "operator" method is defined for typeT
. In addition:!=
will be defined automatically if==
is defined;>
,<=
, and>=
are defined automatically from<
;toString
,toBool
,toNumber
will be used to convert the value of typeT
into a string, a bool, or a numeric respectively;new
can be used to construct "default" values of typeT
in the contexts where this may be needed.(Optional) it may be useful to extend the method-invocation syntax to the current types
numeric
,bool
, andstring
as well; allowing for example to write$value.round()
instead ofround($value)
for a numeric$value
. This is intended to serve as an additional syntactic sugar without replacing the existing syntax.It would also be useful to be able to pass these variables into user-defined commands. Currently this wouldn't be possible since the arguments of a user-defined command are parsed as a dialogue line, and any variables should be embedded as an interpolated expression, which causes them to be stringified. However, stringifying an object is lossy: it is not possible to get the original variable value back.
In order to solve this problem, we could add the following rule to how the user-defined commands are parsed: after the argument string is evaluated normally and then broken into chunks at whitespace, we check whether any of the fragments looks like a variable (i.e.
$
followed by an ID). If yes, then that fragment is replaced with the value of the variable. For example:Under the current rules this would invoke user-defined command
accept("$quest")
, under the new rules it would invokeaccept(variableStorage.getValue("$quest"))
-- allowing to have a user-defined command with an argument of typeQuest
.Example
Backwards Compatibility
The proposed feature is backward-compatible, since the only new syntax that it introduces (
.
) is currently not used within the context of an expression.Alternatives considered
An alternative would be to keep the values in an external storage, and then provide numeric or string references to those values into the Yarn runtime.
In the example above, the function
equipped_items()
could store the actual value into the mapextraValues
, and then return the key to that value into Yarn (say,1237
). The method syntax would not work, but you can call functionitems_broken(1237)
which would know that it has received a key and the actual value has to be looked up in theextraValues
map.The disadvantages of this approach are as follows:
Acknowledgments