Page MenuHomeFeedback Tracker

[Feature Request] OOP support for SQF using hashmaps (with destructor support)
Closed, ResolvedPublic

Description

We can have OOP support in SQF with simply 1 new command (createObjectFromClass), 1 command overload (new call), and slight change in internal hashmap data structure:

_class = [
  ["constructor", 
    {
      _thisObj call ["SomeMethod", _this] 
    }
  ],
  ["SomeMethod", 
    {
    _someObj = createVehicle ["bla", [0,0,0]];
    _thisObj set ["mVehicle", _someObj];
    }
  ],
  ["Method", {...}],
  ["mVehicle", objNull],
  ["destructor", {deleteVehicle (_thisObj get "mVehicle");}
];

_obj = createObjectFromClass [_class, [_constArg1, _constArg2]];
_obj call ["Method", _args];

Under the hood, _obj is just a hashmap (typeName _obj == "HASHMAP"). createObjectFromClass simply does this:

  1. createHashmapFromArray _class
  2. calls the "constructor" (see below for call behavior)
  3. sets a flag on the hashmap in the game data: map.treatAsObject = true

We also need an overload of call: _hashmap call ["method", args]. the only difference between this and normal call is that it also defines a magic variable _thisObj when it calls the method

And finally, when the game wants to destroy the hashmap, if map.treatAsObject is true, it calls the destructor (if it exists)

This might look a bit clunky at first but it'll definitely open up so many new possibilities and makes many things more convenient.

Details

Severity
Feature
Resolution
Open
Reproducibility
N/A
Operating System
Windows 10 x64
Category
Scripting

Event Timeline

dedmen added a subscriber: dedmen.EditedMar 1 2023, 12:55 PM

I see how you set variables and such.
Problem, how to set a CODE type variable, if CODE type is used to identify a method

I like this ALOT. Thank you.
Years ago I thought about a script wrapper thing, that would wrap a variable and add a destructor function that is called when the value goes away.

Problem with destruct is, that may fire at.. inconvenient points. For example cleanup post mission end, all variables are gone, all entities are gone, and in the middle of deleting all variables you try to call your destructor.. That might be problematic :/

We may also want a way to block set on methods? to prevent overwriting them after creation?

dedmen set Ref Ticket to AIII-55574.Mar 1 2023, 12:55 PM
Leopard20 added a comment.EditedMar 1 2023, 6:39 PM

Problem, how to set a CODE type variable, if CODE type is used to identify a method

Just set it normally, and let the user take care of it. You can prevent modification if you want (explained below), but for my personal use case it won't be necessary.

Problem with destruct is, that may fire at.. inconvenient points. For example cleanup post mission end, all variables are gone, all entities are gone, and in the middle of deleting all variables you try to call your destructor.. That might be problematic :/

Well I think if the missionNamespace is destroyed before the objects it will be fine no? But either way, you can just add a warning or note something about this on the wiki.

We may also want a way to block set on methods? to prevent overwriting them after creation?

I'm personally fine with it either way. I think letting them be modifiable is the easiest way, unless all hashmap modifier commands call the set command, in which case making them non-modifiable makes more sense. So in that case you can add something like this to set and deleteAt methods I guess:

if (treatAsObject) {
  auto found = find(key);
  if (found && found->value.type() == Type::Code)
   return; // don't modify method
}
killerswin2 added a subscriber: killerswin2.EditedMar 3 2023, 5:30 AM

+1, We actually have plans to do this in Antistasi with our new task management framework. createObjectFromClass is a good start but we might want to construct the object with a copy of the object, or steal the resources of the another object to create a new object. I.E. maybe two more commands?

  • createObjectFromCopy
  • createObjectWithMove

Still I don't really see this as a good idea, yes python uses hashmaps for "objects", but is sqf really up to the syntax of python? Hell will we ever have member access operators that don't involve an sqf command ( get, getOrDefault). I support this wholly but would not time be better spent on maybe integrating an open source embedded scripting language that promotes the ideas of oop rather than increasing the sqf rabit hole? Seriously why not integrate AngelScript or gravity or wren, or anyone of these languages. The engine already supports two languages, sqs and sqf, why not pull in another language that has better support for oop ideas then trying to add "objects" into sqf.

I do want want to end this by saying I fully support and want this, but at the same time I want a real or a close enough language. I apologies if this comes off as abrasive or down right rude. I had no intention of being so.

Leopard20 added a comment.EditedMar 3 2023, 9:35 PM
is a good start but we might want to construct the object with a copy of the object,

you can just copy the hashmap:

_copy = +_oldObject.

or

_copy = _oldObject;

depending on whether you want a deep or shallow copy.

createObjectWithMove

SQF doesn't need move (and move doesn't make sense in it either, because it's entirely pointer based).

why not pull in another language

Because they won't implement a whole new language at this stage of development (A3 is EOL). What I suggested was an extremely simple solution to having OOP, which is more likely to be implemented.

Ah, that's right is all polymorphic.

Well if this is gonna be implemented, could we get hashmap serialization. It would make creating the objects easier.

dedmen added a comment.EditedMar 22 2023, 12:33 PM

Preventing modification should be somewhat easy.
compileFinal, if you put a final/read-only value, it cannot be overwritten.
Then you can also choose if you want it or not.
We should have alt syntax for compileFinal that takes code on right.. We should've had that a long time ago.

I support this wholly but would not time be better spent on maybe integrating an open source embedded scripting language that promotes the ideas of oop rather than increasing the sqf rabit hole?

Lol what, you think my time is better spent wasting weeks at adding another scripting language, rather than a day or two at doing this... lol. Funny guy

The engine already supports two languages, sqs and sqf

lol, "supports" is a very far stretch. Funny guy

Copy brings up a question. How to handle the constructor on copy.
Have extra constructor for copying that just gets the old one as arg?

_class = [ 
  ["Method", {"testo"}]
]; 

_hm = createHashMapFromArray _class;
_hm call ["Method"];

-> "testo"

Got this working, very simple

dedmen added a comment.EditedMar 23 2023, 3:45 PM

As of now

sqf
ClassDef = [  
  ["#flags", ["sealed"]],
  ["#create", {diag_log "Hello!";}],
  ["#clone", {diag_log "We were copied!";}],
  ["#delete", {diag_log "Goodbye";}],
  ["#str", { "Very Special Object" }],
  ["Method", {diag_log "Method";}]
];

EVERY method inside a HashMapObject receives the Object itself in _self variable, IF the method is called via HashMapObject's facilities. Thus not if you do call (_myObject get "Method"), you need to use the specific call syntax.
Names for "reserved" entries (#create, #clone, #delete, ...) are case-sensitive.
Constructor receives its constructor arguments in _this;
Clone is called after someone copied the object, receives the original copied Object in _this, and has the new object in _self.
Destructor

  • The Destructor is called when the last reference to the object goes out of scope.
  • has no _this
  • is always called in unscheduled (you cannot suspend)
  • after destructor returns the object will be deleted,
  • you should not store _self into a variable that survives outside the destructor's scope (If you do, the destructor will be called multiple times).
  • is NOT called on mission end when missionNamespace variables are cleaned up.

ToString is always called in unscheduled (you cannot suspend). If its not a method the object will remember that and never re-try calling it (for performance reasons) even if you use set to set a different function.

Flags:
Case-insensitive array of strings

  • NoCopy - Forbids copying, +_object will throw error
  • Unscheduled - All Methods (Constructor, Clone, Method's) will be executed in unscheduled
  • Sealed - Cannot Add or Remove any keys, but can edit key's values

Values that are of type CODE, can be passed using compileFinal, which will prevent them from being overwritten.

New command compileFinal hashMap returns a read-only copy, no keys can be added/removed or edited. Also all values (Arrays and HashMaps) will be read-only too.
New command createHashMapObject [ClassDef, ConstructorArguments] that creates a new object from a class definition and calls its constructor.

Notes on Object behaviour
insert/merge on a HashMapObject will insert/merge until a violation occurs (A key is added to a Sealed Object, Tried to overwrite a compileFinal'ed method) then will throw error and abort mid-merge/insert. So you may get half completed insertions.
getOrDefault/getOrDefaultCall on a sealed Object will throw an error if they attempt to insert a new value.
deleteAt on a sealed object will throw an error.
set on a sealed object will throw an error if it tries to add a new key. Replacing existing key will work fine (except when trying to overwrite a compileFinal'ed method)
deleteAt on a compileFinal'ed method will throw an error.

Notes on read-only
insert/merge`/set/deleteAt commands are completely blocked on read-only instances and will throw an error.
getOrDefault/getOrDefaultCall on a read-only instance will throw an error if they attempt to insert a new value.

Creating a object instance
_myObject = createHashMapObject [ClassDef, constructorArguments];

Calling a method:
_myObject call ["Method", arguments];

Example usage for a temporary vehicle:

sqf
TemporaryVehicleClassDef = [  
  ["#flags", ["sealed", "nocopy"]],
  ["#create", compileFinal {
    params ["_vehicleClass", "_pos"];
    _self set ["_vehicle", createVehicle [_vehicleClass, _pos]];
  }],
  ["#clone", compileFinal {
    _sourceVehicle = _this get "mVehicle";
    _self set ["_vehicle", createVehicle [typeOf _sourceVehicle, getPos _sourceVehicle]]; /*Our own vehicle for our copy*/
  }],
  ["#delete", compileFinal {deleteVehicle (_this get "mVehicle");}],
  ["#str", compileFinal { format["TempVehicle %1", typeOf (_this get "mVehicle")] }]
];


call {
  _tmpCar = createHashMapObject [TemporaryVehicleClassDef, ["Car_F", [0,0,0]]];
  _tmpHouse = createHashMapObject [TemporaryVehicleClassDef, ["House_F", getPos player]];
  /*do things*/

  // Vehicles are automatically deleted at end of scope
}

I'll be following this, looks very interesting. I'm currently using HashMaps for my triangulation plugin, where I've made a system which generates a UID and stores it in a HashMap along with its custom type. For example;

_uidMap = [
    [123456, "Point"],
    [654321, "Triangle"]
];

_pointMap = [
    [123456, [/* Point Data */]]
];

Thus I can easily and quickly grab my "custom objects" using their UID as well as check what type a object(UID) is supposed to be.

Would it be possible to implement such a feature so we could use typeName directly to retrieve a custom type name for our objects?

dedmen added a comment.EditedMar 27 2023, 2:36 PM

Would it be possible to implement such a feature so we could use typeName directly to retrieve a custom type name for our objects?

Doesn't seem useful for me. If you want a typename entry, you can add a typename entry to your objects and use it with get. You can also use toString for that

Doesn't seem useful for me. If you want a typename entry, you can add a typename entry to your objects and use it with get. You can also use toString for that

That's a fair assessment, it would more so be to keep code easy to read and in line with existing scripting commands. Admittedly, it would be but a rather small convenience and I've no idea what amount of resources would go into implementing it.

the command takes game value and returns associated with it type name very much like dedmen suggested you create your own implementation.

Xeno added a subscriber: Xeno.Mar 29 2023, 9:44 PM
crashdome added a comment.EditedApr 15 2023, 9:33 PM

I really like this. I had already implemented using hashmaps in an OO type fashion for a project but, the main purpose was for inheritance. I am looking at changing up my project and seeing the pros/cons of using this. I like a lot of the features such as constructor/destructor and the sealed option. However, looking at my project and what I would have to change or add I run into a big con. Class definitions. I cant see a clean way to provide a simple base class and inherit from it without duplicating a bunch of class def arrays everywhere. That, or handling inserting new properties or methods to the def array in code which can break very easily. I'd type up an example but, it would become a much longer post. I hope you understand what I mean by an explosion of definitions could occur in a project. A proposed simple solution might be to allow using a hashmap as an alt definition

createhashmapobject (_baseClass merge [_newClass, true]) //true means can overwrite existing but only if valid as already implemented (e.g. not compileFinal'd, sealed, etc..)

Although child classes would be less bulky (only overriding whats needed), an even cleaner way to do it is to have class definitions in a config file and use the #str property as the type name. Reading from config can be done in a self-implemented function but, to have it natively would be awesome.

Regarding typename, I do like we can choose to make it strict or loose ourselves but, there should be a standard way to do it in the community and I like proposing using the #str as a defacto typename property when it is required.

Edit: I know you can have a base array definition and simply merge or insert an array to get a new class definition but, you lose any benefits of the flags by doing so. As it is simply a similar but completely new class i.e. no way to control inheritance and making it very fragile

I actually played around a bit more and came up with this example. Note, compileFinal on an array is not working for me (expecting String,Code,Hashmap). See comments in code of what I am having issue with. If I want to make sub classes, I would either have to create each array by hand or through the following unless someone has a better implementation:

//Create an abstract base type
//CompileFinal on an array currently is not working for me (v2.13.150442)
//So this definition can be manipulated
TAG_defShape = [
	["#str", "Shape"],
	["Area",{"Error"}]
];

//We have to convert base class and overrides to a hashmap in order to merge properly
//without expensive array lookups and fragile array manipulation code
//and then convert back to an array in the most expensive way possible
TAG_defCircle = call {
	private _finalDefArray = (createhashmapfromarray TAG_defShape) merge (
		createhashmapfromarray [
		[
			["#str", "Circle"],
			["#create", {
				params [["_radius",0,[0]]];
				_self set ["Radius",_radius];
			}],
			//Overrides Area method
			["Area", {private _r = _self get "r"; _r * _r * pi}]
		],true];
	);
	_finalDefArray toarray false; //Expensive
};

//Also it is duplicating code unless you implement some side function to hande it
TAG_defSphere = call {
	private _finalDefArray = (createhashmapfromarray TAG_defCircle) merge (
		createhashmapfromarray [
		[
			["#str", "Sphere"],
			//Note: Inherits "Radius" Property through merge and overrides Area method again
			["Area", {private _r = _self get "r"; 4 * _r * _r * pi}]
		],true];
	);
	_finalDefArray toarray false; //Expensive
};

_myCircle = compilefinal createhashmapobject [Tag_defCircle,[3]]; 
_mySphere = compilefinal createhashmapobject [Tag_defSphere,[3]]; 

//----------------ALTERNATIVE??----------------------------
//If we could pass hashmaps it would save some steps
//e.g. createhashmapobject hashmap  - as an alt syntax
//Hashmap is safe through compilefinal
TAG_defShape = compilefinal createhashmapfromarray [
	["#str", "Shape"],
	["Area",{"Error"}]
];

//We can still inherit using a deepcopy and then merging while keeping base class and new class safe 
TAG_defCircle = compilefinal (+TAG_defShape merge (
	createhashmapfromarray [
	[
		["#str", "Circle"],
		["#create", {
			params [["_radius",0,[0]]];
			_self set ["Radius",_radius];
		}],
		["Area", {private _r = _self get "r"; _r * _r * pi}]
	],true];
	//No need to convert to [[key1,value1],[key2,value2]...] just keep it a hashmap
));

TAG_defSphere = compilefinal (+TAG_defCircle merge (
	createhashmapfromarray [
	[
		["#str", "Sphere"],
		["Area", {private _r = _self get "r"; 4 * _r * _r * pi}]
	],true];
));

_myCircle = compilefinal createhashmapobject [Tag_defCircle,[3]];   
_mySphere = compilefinal createhashmapobject [Tag_defSphere,[3]];

Re: rev. 150658 - you guys are awesome!

crashdome added a comment.EditedMay 30 2023, 2:06 AM

On the subject of interfaces, I have been doing this exactly (prior to the new changes);

TAG_ifc_myInterface1 = [ ["MethodA","CODE"] , ["PropertyA","STRING"] ];
TAG_ifc_myInterface2 = [ ["MethodB","CODE"] , ["PropertyB","NUMBER"] ];

TAG_typ_base = 
[
  ["#str",{"tSomething"}],
  ["interfaces" , [TAG_ifc_myInterface1,TAG_ifc_myInterface2]],
  ["MethodA",{ "abstract MethodA"}],
  ["MethodB",{"abstract MethodB"}],
  ["PropertyA","Hello"],
  ["PropertyB",0]
];

and then I call this function which checks both that key exists but also return type with the option to use "ANY"/"ANYTHING" as a bypass for multiple types -and/or- accept nil values during the check (in the case its allowed or exxpected)

fnc_checkInterfaces.sqf

if !(params [["_hashmap",nil,[createhashmap]],["_interfaces",nil,[[]]],"_allowNils"]) exitwith {false;};
_allowNils = [_allowNils] param [0,true,[true]];

scopename "function";
for "_a" from 0 to (count _interfaces -1) do {
	private _interface = _interfaces#_a;
	for "_i" from 0 to (count _interface -1) do {
		private _check = _interface#_i;
		//Check key exists
		private _key = _check#0;
		if !(_key in keys _hashmap) then {false breakout "function";};
		//Check value type
		if !(_check#1 in ["ANYTHING","ANY"]) then {
			private _type = typename (_hashmap get _key);
			if (isNil {_type} && !_allowNils) then {false breakout "function"};
			if !(_type == _check#1) then {false breakout "function"};
		};
	};
};
true;

Just food for thought. Maybe make #interfaces a thing? Although what I am doing right now I am happy with as is.

Using this, I can implement multiple interfaces and check during init of definition and also in some other function or method which performs a call to make sure that not only keys exist but, also that their values are appropriate. And this way I leave my abstract base implementations separate from my interfaces.

For example - maybe this is in a block where I call MethodA and I want to ensure everything is good

if ([myObject,[Tag_ifc_myInterface1],false] call fnc_checkInterface) then { call an interface method/property...}; //This does not allow nil values to continue!!
if ([myObject,[Tag_ifc_myInterface1],true] call fnc_checkInterface) then { call an interface method/property...}; //This does allow nil values to continue!!

I would happily add this as a note to the wiki but, my wiki account (Crashdome) never got converted because I was away from arma like a decade ago or so.

Sorry if I'm bothering you guys. Glad it's come this far. Hope I'm giving good feedback. I've been using this since I found out about it and can't wait for it to go beyond dev branch

dedmen added a comment.Sep 4 2023, 3:19 PM

createhashmapobject (_baseClass merge [_newClass, true]) //true means can overwrite existing but only if valid as already implemented (e.g. not compileFinal'd, sealed, etc..)

You could implement that in script by manually checking what you consider as "already implemented"

Edit: I know you can have a base array definition and simply merge or insert an array to get a new class definition but, you lose any benefits of the flags by doing so. As it is simply a similar but completely new class i.e. no way to control inheritance and making it very fragile

Yes, but you could implement your "merge" script to consider flags. That of course then would mean only your subclasses would be compliant (considering sealed), which is meh but.. That's how it is now.
I saw inhertiance being a problem, but it seemed simple enough to solve manually via script and I didn't have the time to make a engine side thing of that.
I only now got to read your comments from back then

Reading from config can be done in a self-implemented function but, to have it natively would be awesome.

Yep, again time limitation. I intentionally didn't do what I thought would be simple to solve in script.

Note, compileFinal on an array is not working for me (expecting String,Code,Hashmap).

Yeah that was an oversight. You can still do it by compileFinal'ing a hashmap with a array inside, and then grabbing the array out of it. I don't know if we'll still fix the missing compileFinal ARRAY.

_finalDefArray toarray false; //Expensive

It is worse than the true variant, but still not really that bad. You don't have hundreds of elements here.

#interfaces is a nice idea, but we probably won't have the time for that. Also there is usually more to an interface than "this is a function", like input/output arguments which I would like to have but we can't really do.

dedmen closed this task as Resolved.Sep 26 2023, 2:15 PM
dedmen claimed this task.

I saw inhertiance being a problem, but it seemed simple enough to solve manually via script and I didn't have the time to make a engine side thing of that.
I only now got to read your comments from back then

Reading from config can be done in a self-implemented function but, to have it natively would be awesome.

Yep, again time limitation. I intentionally didn't do what I thought would be simple to solve in script.

#interfaces is a nice idea, but we probably won't have the time for that. Also there is usually more to an interface than "this is a function", like input/output arguments which I would like to have but we can't really do.

Completely understand all of your responses. Especially about interfaces. I have been working on a little project to somewhat 'standardize' some extended features like scripted preprocessing, caching, basic interfaces, and such. Should be releasing it publicly soon. In any case, what you have done so far and the functionality, as it is now, is certainly better than trying to shoehorn in OO via just hashmaps themselves. Your work is much appreciated. If I could vote for best added feature, it would be this. Thank you.

TL;DR will review some of the comments eventually... however, can I suggest a more elaborate page detailing just _what is a HashMapObject_, particularly nuances along the lines of #Flags, what they are, what can they be, "sealed" for starters. thank you...

michaelwplde added a comment.EditedMon, Feb 5, 6:34 AM

Have my eyes on the XPS OOP effort going on, aimed at leveraging the HMO work. I understand couple of improvements, corrections, in the dev queue around that effort. Meanwhile, trying to make a somewhat uninformed usage involving some forms of method, ctor chaining. I wonder if the internals are prohibitiing some key liberties I am taking in order to achieve that.

My #create function looks something like this:

// DICE_fnc_hmo_onCreateRoll
params [
    ["_sides", 6, [0]]
    , ["_times", 1, [0]]
    , ["_summary", true, [true]]
    , ["_base", 0, [0]]
];

// Based on precompiled module functions
_self set ["#base.#create", DICE_fnc_hmo_onCreate];

_self call ["#base.#create", [_sides]];

_self set ["_times", _times];
_self set ["_summary", _summary];
_self set ["_base", _base];

systemChat format ["[_sides, _times, _summary, _base]: %1", str [_sides, _times, _summary, _base]];

true

Does not report the system chat, although curious that the times, summary, base attributes are all set. The base create chaining does not appear to be set, however.

Which obviously, maybe those names keywords, in any form, are prohibited. I wonder, in general HMO, specifically XPS, what is the disposition towards a method chaining naming convention, in any variety?

Cheers, best.

Have my eyes on the XPS OOP effort going on, aimed at leveraging the HMO work. I understand couple of improvements, corrections, in the dev queue around that effort. Meanwhile, trying to make a somewhat uninformed usage involving some forms of method, ctor chaining. I wonder if the internals are prohibitiing some key liberties I am taking in order to achieve that.

AH, I see what I did (or did not do). DICE_fnc_hmo_onCreateRoll lacking module function declaration. Works perfectly, thus far.

Probably been covered before, having had a couple of learning moments doing appropriate method, ctor, dtor chaining. #base. prefixing works for academic use cases, IAnimal to ICat or IDog, for instance.

But if your hierarchy is deeper than a couple of levels, then I think perhaps prefixing to the interface is probably best. Say, IAnimal to IDog to IAustralianCattleDog. For instance, IDie (loosely, [_sides]) to IHeroSystemBody ([_sides, _zeroes, _twos]) to IHeroSystemBodyRoll ([_sides, _times, _summary, _zeroes, _twos]), attributes loosely illustrated. Plus "Roll" method behavior comprehension, i.e. "IDie.#create", "IDie.Roll", "IHeroSystemBody.#create", and so on

Such chaining may be set during "#create" ctor, or perhaps could be part of the formal type decl itself, in order to ensure appropriate declarations are being made. Would need to tinker with that what works, seems best, etc.

Probably been covered before, having had a couple of learning moments doing appropriate method, ctor, dtor chaining. #base. prefixing works for academic use cases, IAnimal to ICat or IDog, for instance.

But if your hierarchy is deeper than a couple of levels, then I think perhaps prefixing to the interface is probably best. Say, IAnimal to IDog to IAustralianCattleDog. For instance, IDie (loosely, [_sides]) to IHeroSystemBody ([_sides, _zeroes, _twos]) to IHeroSystemBodyRoll ([_sides, _times, _summary, _zeroes, _twos]), attributes loosely illustrated. Plus "Roll" method behavior comprehension, i.e. "IDie.#create", "IDie.Roll", "IHeroSystemBody.#create", and so on

Such chaining may be set during "#create" ctor, or perhaps could be part of the formal type decl itself, in order to ensure appropriate declarations are being made. Would need to tinker with that what works, seems best, etc.

Starting to notice what others have reported concerning spurious, possible scheduled unscheduled, #create callbacks. Put some systemChat reports into my #create callbacks to examine the method chaining, and I am definitely seeing multiple "default" callbacks going on.

As observed, for instance with reports such as, meaning, should SHOULD report what I passed during createHashMapObject, correct? Ultimately... I see one such report on up the chain. Then there is a second pairing involving a _this "default" what looks like default arguments.

systemChat format ["[fn_hmo_onCreateHeroSystemBodyRoll] [_this]: %1", str [_this]];

That buggerboo is going to need addressing before HMO could be considered "ready" for prime time IMO, FWIW.

The pattern even looks a bit combinatorial, IOW (in other words), as you hike the callbacks on up the method chain, that introduces another combination to the resolution.

Definitely not what I would expect be happening creating a single instance HMO.

Probably been covered before, having had a couple of learning moments doing appropriate method, ctor, dtor chaining. #base. prefixing works for academic use cases, IAnimal to ICat or IDog, for instance.

But if your hierarchy is deeper than a couple of levels, then I think perhaps prefixing to the interface is probably best. Say, IAnimal to IDog to IAustralianCattleDog. For instance, IDie (loosely, [_sides]) to IHeroSystemBody ([_sides, _zeroes, _twos]) to IHeroSystemBodyRoll ([_sides, _times, _summary, _zeroes, _twos]), attributes loosely illustrated. Plus "Roll" method behavior comprehension, i.e. "IDie.#create", "IDie.Roll", "IHeroSystemBody.#create", and so on

Such chaining may be set during "#create" ctor, or perhaps could be part of the formal type decl itself, in order to ensure appropriate declarations are being made. Would need to tinker with that what works, seems best, etc.

Starting to notice what others have reported concerning spurious, possible scheduled unscheduled, #create callbacks. Put some systemChat reports into my #create callbacks to examine the method chaining, and I am definitely seeing multiple "default" callbacks going on.

As observed, for instance with reports such as, meaning, should SHOULD report what I passed during createHashMapObject, correct? Ultimately... I see one such report on up the chain. Then there is a second pairing involving a _this "default" what looks like default arguments.

systemChat format ["[fn_hmo_onCreateHeroSystemBodyRoll] [_this]: %1", str [_this]];

That buggerboo is going to need addressing before HMO could be considered "ready" for prime time IMO, FWIW.

The pattern even looks a bit combinatorial, IOW (in other words), as you hike the callbacks on up the method chain, that introduces another combination to the resolution.

Definitely not what I would expect be happening creating a single instance HMO.

It looks almost like the HMO framework is calling each of the decl hierarchy with the same top level arguments. Definitely not what should happen, I think, in a purely OOP pattern.

_die = [6, 10, false] call dice_fnc_hmo_createHeroSystemBodyRoll;
_res = _die call ['Roll'];
_raw = _die getordefault ['_raw', -2];
[_res, _raw]
// [[1,1,1,0,0,1,1,1,2,2],[4,2,4,1,1,5,5,4,6,6]]

The results look sensible, but the systemChat trace looks anything but.

bit of insight conveyed to me, apparently has to do with the #create mechanism and #constrList being formed. which is a bit tricky when the ctor arguments are a bit differently shaped from a base class type to derived one. the workaround is tricky, selecting around #create when specifying a #base and doing the ctor chaining manually, which seems to resolve the issue.

// i.e. ...
, ["#base", _decl select { _x#0 != "#create" }]
// ...

_self set ["#base.#create", DICE_fnc_hmo_onCreate];

The documentation is pretty clear on what #base is

"#base": Array or HashMap - declaration of base class for inheritance

"#base" variable is a thing, "#base.#create" is not.

Anything with # is supposed to be reserved.

Does not report the system chat,

Tried it, does print to system chat, looks all correct.

That buggerboo is going to need addressing before HMO could be considered "ready" for prime time IMO, FWIW.

I have absolutely no idea what you are talking about. I see a lot of word salad.

Definitely not what should happen

Can you provide a repro, I don't know what you're talking about.