Register FAQ Search Today's Posts Mark Forums Read
Go Back   JazzJackrabbit Community Forums » Open Forums » JCS & Scripting

Custom objects in MLLE, how to design

Violet CLM

Administrator

Joined: Mar 2001

Posts: 11,287

Violet CLM has disabled reputation

Mar 4, 2026, 12:57 PM
Violet CLM is offline
Reply With Quote
Custom objects in MLLE, how to design

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.)

Last edited by Violet CLM; Mar 4, 2026 at 01:18 PM.
Violet CLM

Administrator

Joined: Mar 2001

Posts: 11,287

Violet CLM has disabled reputation

Mar 19, 2026, 11:57 AM
Violet CLM is offline
Reply With Quote
Hey! It's been a couple weeks, still looking for opinions on these questions
cooba

JCF Veteran

Joined: Jan 2004

Posts: 7,835

cooba is a glorious beacon of lightcooba is a glorious beacon of lightcooba is a glorious beacon of lightcooba is a glorious beacon of lightcooba is a glorious beacon of lightcooba is a glorious beacon of light

Mar 21, 2026, 03:11 AM
cooba is offline
Reply With Quote
I don't feel 100% competent in answering this, but here's some opinions:
Quote:
Originally Posted by Violet CLM View Post
Should that window have limitations? Should events 1-32 not be allowed, or just show a warning if you try?
Well, nothing would be broken permanently. Maybe just a note in the documentation to avoid messing with events 1-32 would be enough.
Quote:
Originally Posted by Violet CLM View Post
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?
Usually this kind of stuff is handled by order of the code being loaded, right? I imagine it's not going to be a common occurence for two (or more) such libraries to be clashing.
Quote:
Originally Posted by Violet CLM View Post
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);
}
I like this one, even if it's not the cleanest option - it's probably good to have a discrete list of what exactly is going on in the code, step by step.
Quote:
Originally Posted by Violet CLM View Post
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...
.asc files are good in theory (i.e. easy to distribute, just by uploading to J2O), and are good practice for organised releases i.e. HH packs; but also: for singular levels, we've been doing just fine with copy-and-pasting all the relevant code from one j2as to the other. And in this case, resolving event ID clashes as mentioned above would be a lot easier (I guess).
DoubleGJ

JCF Member

Joined: Sep 2002

Posts: 3,053

DoubleGJ has disabled reputation

Mar 21, 2026, 05:13 AM
DoubleGJ is offline
Reply With Quote
One thing I'd like to see - without knowing how doable this is, mind you - would be support for an additional list layer for the event window and its related UI.

The episode I'm working on is very large in scope with many new objects added, and in the end I have every intention to not lock out a level from using the original behavior of an object (etc.) next to the additional one that uses its ID.

Example: see how much food I'm adding in the demo levels. You might think no one in their right mind would use more than 255 different food items in a single level, and yes, that's definitely reasonable. But the logical, clean-looking and conveniently usable end goal would be to have the same object asc for all the episode levels, similarly to BL18 or HH24. Even at this very moment there's 92 already, that's over 1/3 of all the event IDs available. Imagine if MLLE supported branching out events further so that the selection list could go, for example...
  • Pickups
  • Food
  • Apples
  • Red
  • Green
  • Yellow
  • Mixed
  • Exotic
  • Banana
  • Mango
  • Pineapple
  • Fast Food
  • Hamburger
  • Cheeseburger
  • Fries
  • Hot Dog

(I sure hope this displays in a readable manner...)

You might suggest that there's already a solution in that I can use event parameters to introduce an additional layer of selection, but here's the thing: it's not quite as readable in MLLE's level view. The squares are limited to the event name, and the parameter is only a small piece of text on the bottom bar - and that's a generic number for anything custom-made. It'd be far more readable if the event display could read what it should say from a label one layer deeper than the event ID name.

And an innate support for keeping the original object behavior there as an option from the built-in custom object editor would surely be more inviting for script newbies. Even having done this for a good while now, the logistics of deciding which objects can I overwrite since I won't be using them can be a pain in the butt.
__________________
my stuff
Reply

Thread Tools

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is On

Forum Jump

All times are GMT -8. The time now is 01:22 PM.