This is an idea Faw and I were chatting about some years back, and I never got around to implementing it in
MLLE. Before I do any coding, though, it'd be good to make sure this is a workable design, so please sing out if you see any potential problems.
Currently MLLE has a "Configure Weapons" window hidden under the "JJ2+ Properties > Weapons" dropdown menu. If you change any weapons in the level (e.g. to
one of these), MLLE takes the following steps (
also described here) to make that happen:
- Instead of MLLE::Setup(), your level script calls MLLE::Setup(array<MLLEWeaponApply@> = { ... }); where the array contains one or more class constructors. This has to happen in the j2as, not in the j2l, because byte data can't specify which class to construct: each class has its own name, and there's no eval() in AngelScript.
- The j2as is also different in that it auto-includes a different library, for example MLLE-Include-1.8w.asc instead of MLLE-Include-1.8.asc. The "w" is short for "weapon," and the "w" libraries are different in that they include code for processing the array<MLLEWeaponApply@>, including these lines:
Code:
#include 'SEweapon.asc'
#pragma require 'SEweapon.asc'
shared interface MLLEWeaponApply { bool Apply(uint, se::WeaponHook@ = null, jjSTREAM@ = null, uint8 = 0); }
(That shared interface line also appears, verbatim, in MLLE-Weapons.asc.)
- Certain hook functions, such as onMain, may be modified to include calls to functions or methods in the MLLE namespace, to support certain weapons. For example, some weapons force a call like this:
Code:
void onPlayer(jjPLAYER@ play) {
MLLE::WeaponHook.processPlayer(play);
If the script does not otherwise contain an onPlayer function, MLLE will add one to the script (and give it a default argument name).
If multiple weapons in the same level each require something to happen in onPlayer, there is still only one MLLE namespace call, because WeaponHook::processPlayer is defined to care of each weapon.
- The "Data5" series of bytes that MLLE adds to j2l files includes a section for specifying custom weapons, corresponding to each MLLEWeaponApply constructor. This includes the weapon's name, used by MLLE (but not by JJ2+), and a series of zero more bytes which are used by both MLLE and JJ2+ for any level-specific options for this weapon.
What does that mean? Consider the Duo Bouncers weapon. In its .ini file in MLLE's Weapons folder, it's defined as follows:
Code:
Name = Duo Bouncers
ImageFilename = duo bouncers.gif
LibraryFilename = DuoBouncers.asc
Initialization = DuoBouncers::Weapon()
Options = Reverse behaviors:bool | Bounce through walls:bool:true
The "Name" line is used by MLLE to match weapon data inside Data5 bytes with weapon definitions in .ini files. The "Options" line says there are two "bool" properties for this weapon which may be determined on a per-level (or per-instance, technically) basis. This means that the Data5s of levels including this weapon contain jjSTREAMs that are two bytes long for this weapon.
The weapon's constructor passes a function (well, a delegate)
DetermineReversedness to
WeaponInterface::WeaponInterface's
apply argument, which looks like this, where
parameter corresponds to those two bytes in Data5:
Code:
bool DetermineReversedness(uint, se::WeaponHook@, jjSTREAM@ parameter) {
if (parameter !is null && parameter.getSize() >= 2) {
parameter.pop(ReverseBehaviors);
parameter.pop(BounceThroughWalls);
}
return true;
}
(Weapons are encouraged to have default configurations if no
apply function is passed/called, which is particularly likely when weapon class instances are constructed in contexts other than
MLLE::Setup.)
For custom
objects, instead of weapons, I think a similar system could work, but with a much simpler coding interface, because a) objects are far more variable than weapons and b) objects tend to only need a single
jjObjectPresets entry, whereas a weapon needs to configure the bullet, the powered-up bullet, the pickup, the powerup monitor, and the +15 crate.
For an interface, I think this should suffice:
Code:
shared interface MLLEObject { bool Apply(uint8 eventID, jjSTREAM@ arguments = null); }
The custom object par excellance would be recolorable springs, but that would take a lot of thought, so let's imagine a custom pickup instead, one that temporarily disables the player's run key.
The library code would look something like this:
Code:
namespace SlowFeet {
array PlayersAreSlow(jjLocalPlayerCount, 0);
class Pickup : MLLEObject {
bool Apply(uint8 eventID, jjSTREAM@ arguments = null) {
uint howManyTicksShouldPlayersBeSlow = 70; //default
if (arguments != null && arguments.getSize() >= 4)
arguments.pop(howManyTicksShouldPlayersBeSlow);
jjOBJ@ newPickup = jjObjectPresets[eventID];
newPickup.behavior = SlowFeetPickup(howManyTicksShouldPlayersBeSlow);
newPickup.determineCurAnim(whatever);
newPickup.playerHandling = HANDLING::PICKUP;
newPickup.scriptedCollisions = true;
}
}
class SlowFeetPickup : jjBEHAVIORINTERFACE {
uint HowManyTicksShouldPlayersBeSlow;
SlowFeetPickup(uint howManyTicksShouldPlayersBeSlow) {
HowManyTicksShouldPlayersBeSlow = howManyTicksShouldPlayersBeSlow;
}
void onBehave(jjOBJ@ obj) {
obj.behave(BEHAVIOR::PICKUP);
}
bool onObjectHit(jjOBJ@ obj, jjOBJ@, jjPLAYER@ player, int) {
PlayersAreSlow[player.localPlayerID] = HowManyTicksShouldPlayersBeSlow;
obj.scriptedCollisions = false;
obj.eventID = OBJECT::FASTFEET;
player.objectHit(obj, 0, HANDLING::PICKUP);
return true;
}
}
void onPlayer(jjPLAYER@ player) {
if (PlayersAreSlow[player.localPlayerID] != 0) {
PlayersAreSlow[player.localPlayerID] = PlayersAreSlow[player.localPlayerID] - 1;
player.keyRun = false;
}
}
}
And an .ini file for MLLE to read would look something like this:
Code:
Name = Slow Feet
LibraryFilename = SlowFeet.asc
Initialization = SlowFeet::Pickup()
Options = Ticks:uint:70
Hooks = onPlayer:SlowFeet::onPlayer
Event = Slow Feet|+|Goodies|Slow|Feet
This would auto-generate a j2as that looked something like this:
Code:
#include "MLLE-Include-1.9o.asc"
#include "SlowFeet.asc"
const bool MLLESetupSuccessful = MLLE::Setup(array = {SlowFeet::Pickup()});
void onPlayer(jjPLAYER@ play) {
SlowFeet::onPlayer(play);
}
...where the "o" suffix on the library filename indicates that it has code for handling custom objects, including the shared interface line. Or maybe this wouldn't be sufficiently complicated to need its own suffix, I haven't thought that through yet.
On the MLLE side of things, there'd be some window for assigning custom objects to event IDs, which would have room for defining object parameters, here the "Ticks" option. Then the "Event" line in the .ini would be read by MLLE and used in place of that event ID's normal event definition, so for example if you wanted to use event 100 for Slow Feet pickups, MLLE would use
100=Slow Feet|+|Goodies|Slow|Feet instead of
100=Tuf Turtle|-|Enemy|Tuf|Turt like normal.
Should that window have limitations? Should events 1-32 not be allowed, or just show a warning if you try?
What about when the script (or an included library) already overwrites the ini event for a given event ID? Like
///@Event 164=Beams|-|Trigger|Beams? Should you be allowed to try to overwrite the same event ID in both ways?
I can think of objects that are more complicated... for example, the poison/antidote system from
CloneV would need two
jjObjectPresets entries, not just one. But that could be accomplished using the
jjSTREAM argument to pass a second uint8. Other times you might want to have multiple instances of the same class, for different event IDs, and then they might have to rely more on delegates than the above example code. But that's all stuff that the library writer would need to handle.
Another design decision is how to handle multiple objects that rely on the same standard hook function. Remember that if multiple
weapons need
onPlayer, you still only see
MLLE::WeaponHook.processPlayer(play);, and that one method handles all the different weapons. But under the above model, for multiple
objects, you'd see something like:
Code:
void onPlayer(jjPLAYER@ play) {
SlowFeet::onPlayer(play);
Poison::sometimesInjurePlayer(play);
RecolorableSprings::checkYSpeed(play);
MLLE::WeaponHook.processPlayer(play);
}
You could get cleaner code using multiple interfaces, like this:
Code:
interface MLLEObject { bool Apply(uint8, jjSTREAM@); }
interface MLLEObject_onPlayer { void onPlayer(jjPLAYER@) }
interface MLLEObject_onMain { void onMain() }
...and so on, for all the other standard hooks.
Then
MLLE-Include-1.9o.asc would do something like this in the Setup function:
Code:
uint8 eventID;
jjSTREAM arguments;
data5.pop(eventID);
data5.pop(arguments);
MLLEObject@ instance = initializerArray[0];
instance.Apply(eventID, arguments);
MLLEObject_onPlayer@ instanceOnPlayer = cast<MLLEObject_onPlayer@>(instance);
if (instanceOnPlayer !is null)
someArrayOfOnPlayerFunctions.insertLast(jjVOIDFUNCPLAYER(instanceOnPlayer.onPlayer));
...and have another function somewhere, similar to the WeaponHook code, like this:
Code:
void ProcessPlayer(jjPLAYER@ player) {
for (uint i = 0; i < SomeArrayOfOnPlayerFunctions.length; ++i)
SomeArrayOfOnPlayerFunctions[i](player);
}
Then the j2as would only look like this, instead of showing four separate calls:
Code:
void onPlayer(jjPLAYER@ play) {
MLLE::ProcessPlayer(play);
}
That would require the library writer to have the hooks all be delegatable methods, and inherit one interface per hook, like this:
Code:
namespace SlowFeet {
array PlayersAreSlow(jjLocalPlayerCount, 0);
class Pickup : MLLEObject, MLLEObject_onPlayer {
bool Apply(uint8 eventID, jjSTREAM@ arguments = null) {
...
}
void onPlayer(jjPLAYER@ player) {
...
}
}
class SlowFeetPickup : jjBEHAVIORINTERFACE {
...
}
}
That's not too big an ask, probably? But I don't know if it's even desirable for all the different functions (
SlowFeet::onPlayer,
Poison::sometimesInjurePlayer,
RecolorableSprings::checkYSpeed) to be hidden like that, or if the level maker might want to be able to order them manually, or sometimes not call them, and so on. Having them fully visible in the j2as gives the scriptwriter more control, which is good if they know what they're doing and bad if they make mistakes.
On a related note, one idea Faw suggested was not using .asc files for custom objects at all, but embedding
all their code in the j2as, so the scriptwriter could easily make any level-specific changes they wanted. But again, that introduces a lot of room for the scriptwriter to make mistakes...
(I know I could use
the issues tracker, but the JCF should get more eyeballs.)