Page MenuHomeFeedback Tracker

[Feature Request] OOP support for SQF using hashmaps (with destructor support)
New, NormalPublic

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.EditedWed, Mar 1, 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.Wed, Mar 1, 12:55 PM
Leopard20 added a comment.EditedWed, Mar 1, 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.EditedFri, Mar 3, 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.EditedFri, Mar 3, 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.EditedWed, Mar 22, 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.EditedThu, Mar 23, 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.EditedMon, Mar 27, 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.Wed, Mar 29, 9:44 PM