Spherical forums

User-Made => Libraries => Topic started by: Radnen on June 28, 2013, 01:36:00 am

Title: analogue.js
Post by: Radnen on June 28, 2013, 01:36:00 am
Introducing: analogue.js

Ever used tung's system but found it to be too slow, or that it doesn't work right at times? Well have no fear since this sleek and lightweight alternative has arrived. I call it "analogue". It's a direct code analogue of the entities on the map, that loads persist scripts and can interact with scripts exactly like how persist can.

Source: revision 10


This can be used a a direct replacement of tung's 'persist.js'.

How to use:

You make a script and store it at "../scripts/maps/your_map.js" for the map 'your_map.rmp' in the /maps directory. This logic holds true for maps in subfolders. "/maps/town/inside.rmp" would need a code stored in "/scripts/maps/town/inside.js" to function.

The code for a town looks like this:
Code: (javascript) [Select]
({
    enter: function() {
        /* code to run on entry of a map */
    },

    leave: function() {
        /* code to run on leave */
    },

    leaveNorth: function() {
        /* code to run when you leave north of the map */
        /* also: South, West, and East variants. */
    },

    // people:
    Bob: {
        talk: function() {
            /* code to run when talked to Bob */
        },

        create: function() {
            /* code to run when Bob is created */
        },

        destroy: function() {
            /* code to run when Bob is destroyed */
        },

        generate: function() {
            /* code to run when Bob generates commands */
        },

        touch: function() {
            /* code to run when Bob is touched by the player */
        },
       
    }
})

When you write code in those 'handlers', it'll directly add it to the person on the map. People also persist values, so you can give bob a property and he'll retain it, like if he were talked to and said something different on the first visit:

Code: (javascript) [Select]
    Bob: {
        visited: false,

        talk: function() {
            if (!this.visited) {
                /* do something when he wasn't visited */
            }

            this.visited = true;
        },
    }

In the code above, visiting Bob means he'll do the action in talk once. Have fun and don't shy away with persisting variables!

Alternatively you can reach the world, map, and person scopes by using three passed variables to the function calls:
Code: (javascript) [Select]
({
    event: 21324,  // some event number, or whatever

    Bob: {
        visited: false,

        talk: function(me, map, world) {
            if (!me.visited) {
                me.visited = true;
                /* do something when he wasn't visited. */
            }
            else if (world.majorEvent) {
                /* talk about some major event in the world. */
            }
            else if (map.event == 21324) {
                /* or perhaps something local to just this map. */
            }
        },
    }
})
Title: Re: analogue.js
Post by: Fat Cerberus on June 28, 2013, 01:40:49 am
Nice, I'll have to drop this into Specs tomorrow and give it a proper test drive! :)
Title: Re: analogue.js
Post by: Radnen on June 28, 2013, 01:45:36 am
It needs testing. I put it here because dropping it in as a direct replacement in my Blockman game seems to still work. Persons still have their properties, shops work (!), and maps and person events are being called like they used to. But by all means, tell me if you notice a problem. Tung isn't here to update persist, so at least we can guarantee that this will be up-to-date.

I should give some credit to tung: I even used some of his code (the mapEvents and personEvents were directly copied). :)
Title: Re: analogue.js
Post by: Fat Cerberus on June 28, 2013, 01:49:32 am
Just looking at your code, I notice something suspect--SetWorld only sets map data.  What about when a user adds custom properties directly to their world object (party data, e.g.) and wants that serialized as well?
Title: Re: analogue.js
Post by: Radnen on June 28, 2013, 02:00:48 am

Just looking at your code, I notice something suspect--SetWorld only sets map data.  What about when a user adds custom properties directly to their world object (party data, e.g.) and wants that serialized as well?


Well, you can.

Code: (javascript) [Select]

analogue.map().Bob.newProp = [true, "hi", 5];

// or

analogue.world.party = ["Jenny", "Lars", "Bob"];

// or

analogue.map().visited = true;


There, then when you JSON the analogue.world object, and the use SetWorld on load, all the new custom members will stay. It's made with that in mind. That the world is a playground, and so are the maps and people.
Title: Re: analogue.js
Post by: Fat Cerberus on June 28, 2013, 02:13:32 am
I'm confused though... I only see Absorb calls for the maps... Before those you just do world = {}.  So if you set world.party, JSON it and then later do SetWorld(), where exactly is world.party being deserialized?
Title: Re: analogue.js
Post by: Radnen on June 28, 2013, 02:16:01 am

I'm confused though... I only see Absorb calls for the maps... Before those you just do world = {}.  So if you set world.party, JSON it and then later do SetWorld(), where exactly is world.party being deserialized?


Oh, I see, global world scripts won't get added even though I intended for it, my mistake. Yeah, only the map level and below was being saved. And I can see how that can be a problem for those that saved stuff to the world object. x.x

Edit: all fixed, and some other issues as well. Now it'll work right... It wasn't before due to referencing the older named "RadPersist".
Title: Re: analogue.js
Post by: Fat Cerberus on June 28, 2013, 08:30:51 am
Nice, just dropped analogue.js into Specs.  Quick modification to two lines and it worked with no modification to my persist scripts!

Incidentally, I figured out what the state ensuring in persist was meant to do: lazy initialization.  Seems like an unnecessary optimization though, persisted data is lightweight enough that it just complicates things for not much gain.  One thing that does bug me in analogue though (other than the name, seems pretty obtuse) is the semantics of the world setter.  If I see an assignment like this:

Code: (javascript) [Select]
analogue.world = { someVar: "maggie" };


I expect it to completely replace the object with what's on the right of the equal sign.  If I weren't familiar with the library in question, I would consider it very odd behavior indeed to merge the objects!  Unless I'm misunderstanding things and that's not actually what happens?

Edit: Wow, not sure what the heck happened there.  Just for everyone's amusement, this is what originally got posted:
Quote
Nice, just dropped analogue.js into Spe


Something went wrong with the forums I think, since I typed way more than that before I clicked post!
Title: Re: analogue.js
Post by: Radnen on June 28, 2013, 01:08:08 pm
Well there is analogue.setWorld() which I think is better than using the .world setter. Some people like using '.prop' setters while others use functions so I put in both methods.

I that analogue was a clever name!  ;D Certainly better than the "RadPersist" I had before.
Title: Re: analogue.js
Post by: Fat Cerberus on June 28, 2013, 04:15:10 pm
Wait, I see what's happening here.  It does do a full replacement of the state, it just doesn't do it the way I had expected.  What confused me was the Absorb() calls, so I assumed setWorld() was merging the old world object with the new, which isn't the case (hence the initial world = {};).  I am still a bit confused by why you handle the map data as a special case (i.e. the GetMap() calls in setWorld() ), instead of just letting the Absorb() call handle that by itself?  Is there a specific reason?  I assume it has something to do with your comment in the code about there "not being maps in maps", but that statement doesn't make any sense to me. ???
Title: Re: analogue.js
Post by: Radnen on June 28, 2013, 05:39:00 pm
Well, say you modified a file after you saved a game. It needs to update the new map code so that when it replaces it doesn't leave things out. Useful for when you are playing and editing at the same time!
Title: Re: analogue.js
Post by: Radnen on June 28, 2013, 11:07:45 pm
Revision 5
Compatibility update and world switching

New Features
- analogue.map([map]) now takes a map name as persist did.
- analogue.person(name [, map]) added, takes a person name and an optional mapname.
- analogue.changeWorld(num) added to change the world you are using.

The last feature is cool. Instead of working with a gigantic file for your entire game, you can block up the world spaces, this is useful for games that may have more than one world. This also makes lazy initialization not that important since if a world does get too big you can split it up. In this way you can choose to lazy-load based on your save file format and where you are in the world.

It must be used with care. If you are on world 2, and need info from world 1, then you'll have to do a simple switch:
Code: (javascript) [Select]

analogue.setWorld(0);
var data = analogue.world.map("name.rmp").data;
analogue.setWorld(1);
// do something with the data... //


It can also be useful for saved games, where each saved file is a separate world space. Makes loading potentially faster. You can, for example, use world 0 for all autosaving. For very fast autosave or quick-save functionality.
Title: Re: analogue.js
Post by: Fat Cerberus on June 28, 2013, 11:24:54 pm
Neat, that multi-world feature should come in handy once I finally get to the point of implementing game saving in Spectacles. :)  Not sure I'm a big fan of this API setup though, why not just do what you did with the maps and set up the API like this:
Code: (javascript) [Select]
var data = analogue.world(0).map("name.rmp").data;


Seems like a better setup from a consistency standpoint and would prevent bugs due to missed calls to changeWorld().
Title: Re: analogue.js
Post by: Radnen on June 28, 2013, 11:54:50 pm
Good idea, will look into it next revision. :)
Title: Re: analogue.js
Post by: Radnen on June 29, 2013, 02:37:44 am
Revision 8 - Breaking Change

New world handling features and some helper functions in the world object.

New Features
- world objects now have methods .map(), and .person()
- analogue.mergeWorld(data [, world_num]) will merge a saved world into the existing world or world num
- analogue.getWorld([world_num]) will get the numbered world
- analogue.setWorld(data [, world_num]) will replace the world; don't do this if you intend to merge.
- analogue.clear() clears the current world by replacing it with a new one.

Bug Fixes
- Merge's Absorb will respond to updated objects if a member had changed from an object to something else.
- analogue.setWorld() now Absorbs the replacement into an empty world to preserve .map() and .person().

etc.
- Some code cleaning and commenting.

So, to remind the users: if you want to use multiple worlds in-game you must use .changeWorld(). .getWorld() and .setWorld() only retrieve or replace information. analogue will only run automatically on the current world (the world set by .changeWorld()). Of course you can keep the current world at 0, not use .changeWorld(), and instead replace world 0 whenever you load games. So in other words: .changeWorld() is really optional if you stick to world 0 for the game and world 1 and on as strictly data.

So, now you can do this:
Code: (javascript) [Select]

var data = analogue.getWorld(2).map("map.rmp").data;


Oh and about the world .person() and .map() calls, well, that means you shouldn't put 'person' or 'map' members into the world object. If you do, they will indeed persist, but overwrite the function calls. Small price to pay for convenience.
Title: Re: analogue.js
Post by: Fat Cerberus on June 29, 2013, 10:40:04 am
With the latest version of analogue, I get an "invalid 'in' operand event" exception when loading a map with no accompanying JS file (for example, the test map I use for prototyping).  Previous versions handled this just fine (and so did persist, I think), so I'm guessing this is a regression?
Title: Re: analogue.js
Post by: Radnen on June 29, 2013, 02:13:39 pm
Revision 9

Minor update.

Bug Fixes
- Fixes empty script evaluation regression.
Title: Re: analogue.js
Post by: Fat Cerberus on June 30, 2013, 12:57:24 am
Damn, I forgot about this little wrinkle with persist/analogue, it's been so long since I wrote an actual persist script...

You can't change maps in the enter function, otherwise you get an exception: "ChangeMap() failed: Default enter map script already running!"  This breaks my opening cutscene, which is initiated from a blank starting map (so that the opening narration is on a black background), then switches to another map for a flashback, and finally switches to the starting town before giving the player control.  Know of any way around this, Radnen?
Title: Re: analogue.js
Post by: Radnen on June 30, 2013, 01:15:55 am
Oh, well, that's impossible. If you change map when you enter a map you'll be doing it forever. That's what the default map script does. When you save it with the file it does a run-once on the enter, after it does the default.

1. All default map scripts are ran.
2. All local map scripts are ran.

Since persist changes the default map script, you can't do some things since, well, it tries to stop you from an endless cycle. Using ChangeMap() in a default map script means another map is loaded, which means another entry, which means another ChangeMap() call (and repeat). Of course, persist and analogue work by changing the map script out each time a new map is loaded and so you wouldn't have that problem. However, the engine writers never foresaw that.

The workaround: SetDelayScript(1, "ChangeMap('map.rmp');");

Interestingly, if you set the delay to 0 you'll hang the engine! XD But at 1 it might blink the map for a frame...
Title: Re: analogue.js
Post by: Fat Cerberus on June 30, 2013, 02:25:03 am
Thanks.  I had to use a slightly different method, though.  I can't call ChangeMap() directly (I'm using Scenario so that would break the scene coordination), instead I have a teleport scenelet defined for Scenario that does the ChangeMap internally.  So what I had to do instead was, I still used SetDelayScript, but instead of just queuing the ChangeMap call, I created a function to run the whole cutscene, and set a delay script to call that function.  Works like a charm!
Title: Re: analogue.js
Post by: Radnen on June 30, 2013, 05:39:33 am
Revision 10

Stability update.

Bug Fixes
- Properly fixes long paths for Linux machines running Sphere.
Title: Re: analogue.js
Post by: Fat Cerberus on June 30, 2013, 10:51:05 am
Are you sure using 'this' works properly? I haven't tested it yet, but I just skimmed through the code and discovered that you're calling event functions like this:

Code: (javascript) [Select]
map[event](map, world)


...which passes the global object as 'this'.  You should be using the .call method...
Title: Re: analogue.js
Post by: Radnen on June 30, 2013, 01:14:59 pm
No, that works perfectly fine. Calling a function first uses the prototype it's bound to before anything else: it's calling a method of the map object. In many languages, if you call an object's method 'this' is implicitly set to the owner of the method, therefore you (usually) don't have to do something like this: myObject.call(myObject, params) each time you call a method.
Title: Re: analogue.js
Post by: Fat Cerberus on June 30, 2013, 02:44:42 pm
No, I knew that, I just wasn't sure if obj["func"] had the same semantics as obj.func.  E.g. If you do:
Code: [Select]
var f = obj.func;
f();


Then 'this' isn't set correctly (it's the global object).  I figured the array syntax would have the same problem.
Title: Re: analogue.js
Post by: Radnen on June 30, 2013, 03:02:11 pm
Oh yeah obj["func"] is the same thing as obj.func, in fact I can just call map.enter() directly, but I'd have to do a separate call per method whereas the [] accessor can just loop through the calls. JS can be really neat sometimes!
Title: Re: analogue.js
Post by: alpha123 on June 30, 2013, 10:07:52 pm

Are you sure using 'this' works properly? I haven't tested it yet, but I just skimmed through the code and discovered that you're calling event functions like this:

Code: (javascript) [Select]
map[event](map, world)


...which passes the global object as 'this'.  You should be using the .call method...

That will actually pass map as this. Basically, here's how this works in JavaScript:







Invocation type                         Syntaxthis value
functionfn(arg);global object1
methodobj.fn(arg);
obj["fn"](arg);
obj
applyfn.call(self, arg);
fn.apply(self, argArray);
obj.fn.call(self, arg);
self
bind2var f = fn.bind(thing); f(arg);
var f = obj.fn.bind(thing); f(arg);
thing


It does not matter what happened prior to the invocation; e.g.
Code: (javascript) [Select]

var fn = obj.fn;
fn(1, 2, 3);

will behave as the first invocation pattern. Another example:
Code: (javascript) [Select]

fn(1, 2, 3);  // `this` is the global object (see footnote 1)
obj.fn = fn;
obj.fn(1, 2, 3);  // `this` is obj


obj.fn is exactly equivalent to and is merely syntactic sugar for obj["fn"].

Title: Re: analogue.js
Post by: Fat Cerberus on June 30, 2013, 10:37:43 pm
Good to know, thanks alpha.  For some reason I always assumed that the interpreter converted this:
Code: (javascript) [Select]
obj["func"]();

...to this:
Code: (javascript) [Select]
var tmp = obj["func"];
tmp();


That is, that the indexing operation was evaluated before the function call, rather than the whole thing being atomic.  Must be my C/C++ instincts kicking in again...

So yeah, thanks for clearing that up! :)
Title: Re: analogue.js
Post by: alpha123 on July 01, 2013, 12:07:49 am
You're not entirely wrong, although that varies from JS engine to JS engine.

Basically the runtime does
Code: (javascript) [Select]
var tmp = obj["func"];
tmp.call(obj);


No problem. this is one of those weird hairy corners of JS that I didn't fully understand for a while.
Title: Re: analogue.js
Post by: Fat Cerberus on April 13, 2015, 10:58:48 am
I found a bug:

Code: (javascript) [Select]
({
enter: function(map, world) { world.cows_eat_kitties = "Meoowww--MOOOOoooooo! ...*munch*"; },
kitty: {
    talk: function(self, world) {
        Abort(world.cows_eat_kitties);  // undefined?! wtf
    }
})


If I change all references to the world argument to analogue.world it works fine, but apparently analogue is passing different world objects to each handler...?

Edit: Come to think of it, didn't persist.js have this same issue?  This exact bug is why you wrote analogue.js in the first place... kind of ironic. ;)