EasyRPG / Player

RPG Maker 2000/2003 and EasyRPG games interpreter
https://easyrpg.org/player/
GNU General Public License v3.0
965 stars 183 forks source link

New Command: 2002 - Trigger Event At [x,y] #3183

Closed jetrotal closed 2 months ago

jetrotal commented 5 months ago

@raw 2002, "", UseVarX,x UseVarY,y

Activate an event in map remotely, based on its x and y coordinates. image

image

Command made by @MackValentine, I only refactored it to fit the player specifications.

Ghabry commented 4 months ago

The latest Maniac stuff is finally becoming useful for us: This is all kinda undocumented but after 2 hours I got this beautiful Ui working.

grafik

jetrotal commented 4 months ago

How hard is to start fiddling with those custom menus, do they only work on the japanese Maniacs?

Here some quick mockup I put together for this command. image

Ghabry commented 4 months ago

I tried two days ago to patch the editor back to English. I didn't post about it so you can guess the result... The editor itself was mostly English again but the entire command window stayed Japanese. :/ So this relies on BingShan again to make it usable. Closed Source... Yeah......

The Ui is just Windows Forms. So they are just drag-and-dropped using the Visual Studio Designer. So they can contain anything you want.


Though the question is if that is worth the time. Another solution which would be also useful for our editor later is our own open source TPC replacement.

(2 commands of 154 done xD)

Teaser

grafik

jetrotal commented 4 months ago

Oh! that's cool! Do those scripting commands require a lot of handmade code?


Another question: Will it be interpreted as string by the player or will it be compiled as commands, as tpc currently do? Being read by the player would make it sucha powerful scripting language...

Ghabry commented 4 months ago

In needed two days to become used to the API and write wrappers so creating the binding code is not too ugly.

The script language is chaiscript (https://chaiscript.com/) with some custom modifications by me to make the syntax nicer. The syntax is JavaScript like. But simpler.

I will share some code when I found more event commands. Still figuring out some edge cases.

Currently this will be a code generator like TPC but I don't see a reason why this shouldn't be executable by the player directly later.

Code gen first is good for finding bugs :)

Ghabry commented 4 months ago

Example how to add a command (Not too useful yet as code is not released but just so you can see it).

Header:

#pragma once

#include "event_command.h"
#include "types.h"

namespace EasyScript {

class TriggerEventAt {
public:
    TriggerEventAt();
    TriggerEventAt X(const chaiscript::Boxed_Value& value);
    TriggerEventAt Y(const chaiscript::Boxed_Value& value);

    static void Register(chaiscript::ChaiScript& chai, EventCommand::List& commands);

    std::shared_ptr<EventCommand> cmd = std::make_shared<EventCommand>();
};

}

Source:

#include "trigger_event_at.h"
#include "chaiscript/chaiscript.hpp"
#include "dynamic_object.h"
#include "easyscript/event_command.h"
#include <lcf/rpg/eventcommand.h>

EasyScript::TriggerEventAt::TriggerEventAt() {
    cmd->SetDefaults(static_cast<EventCommand::Code>(2002), "", { 0, 0, 0, 0 });
}

EasyScript::TriggerEventAt EasyScript::TriggerEventAt::X(const chaiscript::Boxed_Value& value) {
    cmd->SetValueAndMode(0, 1, value);
    return *this;
}

EasyScript::TriggerEventAt EasyScript::TriggerEventAt::Y(const chaiscript::Boxed_Value& value) {
    cmd->SetValueAndMode(2, 3, value);
    return *this;
}

void EasyScript::TriggerEventAt::Register(chaiscript::ChaiScript& chai, EventCommand::List& commands) {
    chaiscript::ModulePtr m = std::make_shared<chaiscript::Module>();
    chaiscript::utility::add_class<TriggerEventAt>(*m, "__cls_TriggerEventAt",
    {
        chaiscript::constructor<TriggerEventAt()>()
    },
    {
        {chaiscript::fun(&TriggerEventAt::X), "x"},
        {chaiscript::fun(&TriggerEventAt::Y), "y"}
    }
    );
    chai.add(m);

    chaiscript::dispatch::Dynamic_Object o;
    o["trigger"] = chaiscript::var(chaiscript::fun([&](){
        auto evt = TriggerEventAt();
        commands.push_back(evt.cmd);
        return evt;
    }));
    chai.set_global(chaiscript::var(o), "@map");
}

Invocation in the script:

@map.trigger.x(123).y(345)
@map.trigger.y($v(222)).x($vv(333))
jetrotal commented 4 months ago

Looks interesting. I supose those commands could be generated from a .csv file. Just combining what is "type" or "...IsVar" with the respective values parameters

Ghabry commented 4 months ago

yeah currently I'm not auto-generating it. But will do it for most of the simple commands. Still streamlining the syntax a bit.

Before:

void EasyScript::TriggerEventAt::Register(chaiscript::ChaiScript& chai, EventCommand::List& commands) {
    chaiscript::ModulePtr m = std::make_shared<chaiscript::Module>();
    chaiscript::utility::add_class<TriggerEventAt>(*m, "__cls_TriggerEventAt",
    {
        chaiscript::constructor<TriggerEventAt()>()
    },
    {
        {chaiscript::fun(&TriggerEventAt::X), "x"},
        {chaiscript::fun(&TriggerEventAt::Y), "y"}
    }
    );
    chai.add(m);

    chaiscript::dispatch::Dynamic_Object o;
    o["trigger"] = chaiscript::var(chaiscript::fun([&](){
        auto evt = TriggerEventAt();
        commands.push_back(evt.cmd);
        return evt;
    }));
    chai.set_global(chaiscript::var(o), "@map");
}

After:

void EasyScript::TriggerEventAt::Register(chaiscript::ChaiScript& chai, EventCommand::List& commands) {
    Bind<TriggerEventAt, void, TriggerEventAt()>(
        chai, commands,
        "TriggerEventAt", "map", "trigger",
        &TriggerEventAt::X, "x",
        &TriggerEventAt::Y, "y"
    );
}
jetrotal commented 4 months ago

looking better! Dummy question, what is value at cmd->SetValueAndMode(0, 1, value);

Probably the value from user input? 🤔

Ghabry commented 4 months ago

That's the argument passed in to the function

@map.trigger.x(1).y($v(2))

Function X receives number 1 and Function Y receives Variable(2).

SetValueAndMode checks the type of the passed in value and sets the correct mode automatically based on it.

Ghabry commented 4 months ago

Okay and I enhanced the API again: The binding API "introspects" now the class to bind everything. Of course will only work for events that are quite "simple" but this is the case for 90% of them ;).

Example for TriggerEventAt:

Header:

#pragma once

#include "easyscript/forward.h"
#include "easyscript/parameter.h"

namespace EasyScript {

class TriggerEventAt {
public:
    TriggerEventAt();

    std::shared_ptr<EventCommand> cmd = std::make_shared<EventCommand>();

    static void Register(chaiscript::ChaiScript& chai, EventCommandList& commands);

    static constexpr std::array name = { "TriggerEventAt", "map", "trigger" };

    static constexpr const std::array param = std::to_array<Parameter>({
        { "x", 0, 1, 0 },
        { "y", 0, 3, 2 },
    });
};

}

Source:

#include "trigger_event_at.h"
#include "chaiscript/chaiscript.hpp"
#include "easyscript/binding.h"
#include "easyscript/event_command.h"

EasyScript::TriggerEventAt::TriggerEventAt() {
    cmd->SetDefaults(static_cast<Code>(2002), "", { 0, 0, 0, 0 });
}

void EasyScript::TriggerEventAt::Register(chaiscript::ChaiScript& chai, EventCommandList& commands) {
    BindAuto<TriggerEventAt, void, TriggerEventAt()>(chai, commands);
}

More complex example: PlayBgm

Header:

#pragma once

#include "easyscript/forward.h"
#include "easyscript/parameter.h"

namespace EasyScript {

class PlayBgm {
public:
    PlayBgm(StringArg value);

    std::shared_ptr<EventCommand> cmd = std::make_shared<EventCommand>();

    static void Register(chaiscript::ChaiScript& chai, EventCommandList& commands);

    static constexpr std::array name = { "PlayBgm", "music", "play" };

    static constexpr const std::array param = std::to_array<Parameter>({
        { "fadein", 0, 0, 4, 1 },
        { "volume", 100, 1, 4, 2 },
        { "tempo", 100, 2, 4, 3 },
        { "balance", 50, 3, 4, 4 }
    });
    static constexpr const StringParameter string_param = {nullptr, 5, 4, 0};

    static std::string FromCommand(const EventCommand& command);
};

}

Source:

#include "play_bgm.h"
#include "chaiscript/chaiscript.hpp"
#include "easyscript/binding.h"
#include "easyscript/event_command.h"
#include "easyscript/forward.h"

EasyScript::PlayBgm::PlayBgm(StringArg value) {
    cmd->SetDefaults(Code::PlayBGM, "", { 0, 100, 100, 50 });
    string_param.Set(*cmd, value);
}

void EasyScript::PlayBgm::Register(chaiscript::ChaiScript& chai, EventCommandList& commands) {
    BindAuto<PlayBgm, StringArg, PlayBgm(StringArg)>(chai, commands);

    BindNamespaceFunctions(
        chai, "music",
        [&](){
            auto evt = PlayBgm(chaiscript::Boxed_Value(std::make_shared<const std::string>("(OFF)")));
            commands.push_back(evt.cmd);
            return evt;
        }, "stop"
    );
}

std::string EasyScript::PlayBgm::FromCommand(const EventCommand& command) {
    if (string_param.GetMode(command) == 0 && command.string == "(OFF)") {
        return "@music.stop";
    }

    return {};
}
jetrotal commented 3 months ago

@Ghabry We have ways of activating commands right now, So it's no problem aproving those new commands. TriggerEventAt is also a good name!

Ghabry commented 2 months ago

Going to merge this when the build passed.

Note that starting from Player 0.8.1 this command will be subject to "opt-in" for EasyRPG extensions.

Please add this to your INI file to be future proof:

[Patch]
EasyRPG=1
florianessl commented 1 month ago

I'm prototyping some enhanced debugging flags and stumbled upon this. One thing that sticks out, that this behaves exactly, as if it was the player was in front of the event and pressed the decision key:

_SaveEvent::triggered_by_decision_key is set to true, so any condition branch that checks for this, will evaluate to true. Is this wanted? Game_Player::CheckEventTriggerThere_ always will set the event to automatically face the player, right before the commands are scheduled for the interpreter. Maybe also unwanted behavior?

And now for the reason why I looked at this command...: It might be useful, if a new field was defined for "SaveEvent" which indicates that the event was triggered this way. Something like "easyrpg_triggered_indirectly" or maybe even a bitflag field which might hold several values. Otherwise there would be no indication in the callstack how this code was started.

Ghabry commented 1 month ago

You are right, both of these behaviours should not happen (or at least make it possible to configure them). This shows that even simple commands need lots of testing.

I'm currently creating a TestGame to test the new commands before doing further inclusion (next one is "Wait for Single Movement" so this doesn't happen again. Sorry :/