Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Event system #39

Closed
wants to merge 6 commits into from
Closed

Event system #39

wants to merge 6 commits into from

Conversation

Akjosch
Copy link
Contributor

@Akjosch Akjosch commented Mar 30, 2016

This is a simple, stand-alone event system. Its functions are modelled after the one in MinecraftForge, though it doesn't need to meet its performance or mod loading requirements, so no ASM and class loader trickery is required.

Example usage is marking event handling instance methods somewhere. This could be a separate class, but doesn't have to be - they work anywhere.

public class MyEventHandler {
    @Subscribe
    public void someEventHandler(ScenarioStartedEvent event) {
        // Do something with event.getScenario() or whatever
    }

    private boolean someNonHandlerMethod() {
        /* do stuff */
        return maybe;
    }

    @Subscribe
    public void IRanOutOfMethodNames(PlanetaryEvent event) {
        // Something happened on event.getPlanet()!
    }

    @Subscribe
    public void nope(UnitBoughtEvent wantToBuySomething) {
        wantToBuySomething.cancel(); // Not today!
    }
}

The event classes are (to be defined) subclasses of mekhq.event.HQEvent. Registering the handler is simple enough:

EventBus.registerHandler(new MyEventHandler());

And so is firing events:

EventBus.triggerEvent(new ScenarioStartedEvent(scenario));

And checking if it got cancelled:

boolean eventWasCancelled = EventBus.triggerEvent(new UnitBoughtEvent(getCampaign(), unit));

And you can have your own private EventBus too for specific needs:

private static final EventBus CAMPAIGN_EVENT_BUS = new EventBus();
/* ... */
CAMPAIGN_EVENT_BUS.trigger(new UnitBoughtEvent(campaign, unit));

Impact on other code: Exactly zero - it doesn't modify any existing class. This is meant to ease further development (in particular of the planetary and stellar events, but anywhere you need an event you can use it), and possibly moved whole into MegaMek if it is deemed a suitable replacement or enhancement of the existing systems there.

@Dylan-M
Copy link
Contributor

Dylan-M commented Mar 30, 2016

This is a great idea, however... it will need to wait until after the stable release.

That said, without pushing away your wonderful work, could I convince you to try and concentrate on bug fixes for now (in both MM and MHQ) so that we can get the stable out and start adding all these good features?

@Akjosch
Copy link
Contributor Author

Akjosch commented Mar 30, 2016

Well, now - the whole of PR #37 was essentially one big bug fix. :) This is just something I threw up in a bit of a spare time between my "real" work.

There's no need to hurry this either. As I wrote, I'm not sure if it is even in the right project. It might be more useful being a part of MegaMek instead.

@pheonixstorm
Copy link
Collaborator

To add to ralgiths comment, I would say start with the oldest bug reports on sourceforge and close any that are no longer valid. I do think at least a quarter of them have either been fixed or are by now invalid.

Or if you haven't been added to those projects at least leave a message for arlith or ralgith to close them.

I will have a few bug fixes up and ready by the end of the weekend I hope. Two I posted on my fork and one I posted here.

@Akjosch
Copy link
Contributor Author

Akjosch commented Mar 31, 2016

I'm not actually in the dev team over at SF, so I can at most comment there.

A whole bunch of bugs deal with how AtB does things too, and I'm sadly not nearly versed enough in that to be able to help, nor do I know who I'd need to ask to make rule clarifications for it.

@pheonixstorm
Copy link
Collaborator

Just grab a copy of the latest AtB spreadsheet. Most of the AtB stuff in HQ is rather ad-hoc or a best guess on how to apply it to a computer simulation. neoancient wrote the atb code for HQ but hasn't had time for it.

Best advice, find the rule in the atb spreadsheet and best guess on how it should be coded. Thats about all you can do really. But when in doubt post a question about it.

@Dylan-M
Copy link
Contributor

Dylan-M commented Mar 31, 2016

@Akjosch As to AtB Bugs: They're not stable blocking. So don't worry about them for stable plugging away. Worry about bugs with official rules, game breaking errors, and the like.

@arlith
Copy link
Member

arlith commented Mar 31, 2016

We may want to consider adding an AtB wiki entry to the MHQ wiki. It could be a central location to catalog the AtB rules and how they get implemented in MHQ.

@arlith
Copy link
Member

arlith commented Mar 31, 2016

@Akjosch I also added you to the developer group for the MM, MML, and MHQ projects on SF.

@Dylan-M
Copy link
Contributor

Dylan-M commented Mar 31, 2016

@arlith I think a wiki entry would be a good idea... now where is our AtB developer when we need him ;)

Also, I believe that something like that (being specific to MHQ) should be on the MHQ Wiki rather than our normal policy of everything going on the MM wiki. However, that may make it harder for newbies to find... so I'd appreciate other opinions there.

@arlith
Copy link
Member

arlith commented Mar 31, 2016

I had the MHQ wiki in mind. The same thought crossed my mind, and since this is definitely specifically MHQ, I think it should go. It would be possible to link it, I think.

@Akjosch
Copy link
Contributor Author

Akjosch commented Apr 4, 2016

Since @arlith wanted examples, here's one for the scenario resolution.

Goal: Have a scenario where Side A has to:

  • kill X enemy 'Mechs by turn T
  • don't let building B be destroyed

That's a bunch of events and two event handlers.

public class EntityKilledEvent extends MMEvent /* or whatever sublass of it */ {
    public Entity victim;
    public Entity killer;
    /* some other data about the event */
}

public class TurnFinishedEvent extends MMEvent {
    public int turn;
}

public class BuildingDestroyedEvent extends MMEvent {
    public int buildingId;
    public Entity destroyer;
}

// There are two events here, since the game might involve more than two parties
public class ScenarioWonEvent extends ScenarioEvent {
    public IPlayer winner;
    public ScenarioWonEvent(IPlayer winner) { this.winner = winner; }
}

public class ScenarioLostEvent extends ScenarioEvent {
    public IPlayer loser;
    public ScenarioLostEvent(IPlayer loser) { this.loser = loser; }
}

Now the event handlers for determining when a scenario is won/lost. They get created and registered to the event handler when a game starts, and unregistered once it's done. There's one for the 'Mechs destroyed (which can be written even more generally than in this example) and one for building destroyed.

public class MechDestroyedHandler {
    public IPlayer who;
    public int howManyMechs;
    public int untilWhichTurn;
    public int mechsSoFar = 0;

    public MechDestroyedHandler(IPlayer who, int howManyMechs, int untilWhichTurn) {
        this.who = who;
        this.howManyMechs = howManyMechs;
        this.untilWhichTurn = untilWhichTurn;
    }

    @Subscribe
    public void mechKills(EntityKilledEvent event) {
        if(event.victim instanceof Mech
            && event.killer.isOwnedBy(who) && event.victim.isEnemyOf(who)) {
            ++ mechsSoFar;
            if(mechsSoFar >= howManyMechs) {
                EventBus.triggerEvent(new ScenarioWonEvent(who));
            }
        }
    }

    @Subscribe
    public void turns(TurnFinishedEvent event) {
        if(event.turn > untilWhichTurn) {
            EventBus.triggerEvent(new ScenarioLostEvent(who));
        }
    }
}

public class BuildingProtectorHandler {
    public IPlayer who;
    public int whichBuilding;

    public BuildingProtectorHandler(IPlayer who, int whichBuilding) {
        this.who = who;
        this.whichBuilding = whichBuilding;
    }

    @Subscribe
    public ohNoes(BuildingDestroyedEvent event) {
        if(event.buildingId == whichBuilding) {
            EventBus.triggerEvent(new ScenarioLostEvent(who));
        }
    }
}

There's also a handler for ScenarioEvent, which gets both ScenarioWonEvent and ScenarioLostEvent events and keeps score as to who won and lost and can also deal with unregistering those two. All of those are then registered by a scenario loader/creator:

/* List<Object> */myHandlers = Arrays.asList(
    new MechDestroyedHandler(player, x, t),
    new BuildingProtectorHandler(player, b)
    new ScenarioResolverHandler(game)
);
/* ... */
for(Object handler : myHandlers) {
    EventBus.registerHandler(handler);
}
/* The ScenarioResolverHandler then unregisters all the handlers once all is done */

@Akjosch
Copy link
Contributor Author

Akjosch commented Apr 4, 2016

Another example, story creation. Somebody gets killed by the unit. Their family swears revenge.

public class KillEvent extends StoryEvent {
    public Person victim;
    public Unit responsibleUnit;
    /* circumstances and so on */
}

The code marking the person as "killed" (scenario resolution, some random event, throwing prisoners out of an airlock, ...) calls EventBus.triggerEvent(new KillEvent(person, unit, ...)); Some other code, possibly a loaded "story mode campaign" code, registered an event handler for that earlier:

public class YouKilledMyFather {
    private Campaign campaign;
    private DateTime lastScenarioDate;
    public YouKilledMyFather(Campaign campaign) {
        this.campaign = campaign;
        this.lastScenarioDate = campaign.getDateTime().minusDays(1); // Yesterday
    }

    @Subscribe
    public void prepareToDie(KillEvent event) {
        if(Compute.d6(3) > 16) {
            // 17 or 18 on 3d6
            DateTime when = campaign.getDateTime();
            if(when.isBefore(lastScenarioDate)) {
                return; // There's already one revenge running ...
            }
            when = when.plusMonths(Compute.d6()).plusWeeks(Compute.d6(3));
            Unit enemy = campaign.getUnitGenerator().newFamilyUnitFor(event.victim);
            Scenario sc = campaign.getScenarioGenerator().newScenario(enemy, when);
            sc.setDescription("The family of " + event.getFullName() + " set up a trap for you. You. Will. PAY!!!");
            campaign.addScenario(sc);
        }
    }
}

@Dylan-M
Copy link
Contributor

Dylan-M commented Apr 5, 2016

How about an example of a dynamically loaded module that creates an event and manages it?

@Akjosch
Copy link
Contributor Author

Akjosch commented Apr 5, 2016

That dynamically loaded module is just like any other bunch of classes. The most work consists in creating the framework for dynamically loading it, not in the event system.

So a mod class looks something like this, with some custom annotations to make our mod classloader aware of what methods in which class to call to initialise and shutdown the mod.

package akjosch.modules.example;
import megamek.common.event.EventBus;
import megamek.common.mod.MegaMekMod;
import megamek.common.mod.ModInit;
import megamek.common.mod.ModShutdown;

@MegaMekMod
public class MyDynamicModule {
    private Object myHandler;

    @ModInit
    public void init() {
        myHandler = new GameStartStuff();
        EventBus.registerHandler(myHandler);
    }

    @ModShutdown
    public void shutdown() {
        EventBus.unregisterHandler(myHandler);
    }
}

And the actual event handler is as above, dealing with whatever game events we want.

package akjosch.modules.example;
import megamek.common.event.Subscribe;
import megamek.common.event.game.GameStartEvent;
import megamek.common.event.game.RNGLoadedEvent;

public class GameStartStuff {
    @Subscribe
    public void doSomethingFirst(GameStartEvent event) {
        /* Whatever we want, like setting up some initial data structures
           or adding some game options */
    }

    @Subscribe
    public void generateNames(RNGLoadedEvent event) {
        /* Pre-generate some names for later using the random names generator */
    }
}

The main job here is the actual mod loader, which needs to have its own class loader (to disallow any mod which includes classes in the java., javax., sun., megamek. and mekhq.* namespaces, for example), defined mod lifetime hooks (the ModInit and ModShutdown annotations above as an example) and possibly provide the mods with additional tools to simplify the mod writer's life, like a centralised resource loader/manager which can load both global resources from the game's main data directory and ones embedded into the module's JAR file itself transparently.

@Akjosch
Copy link
Contributor Author

Akjosch commented Apr 8, 2016

Resubmitted as a MegaMek PR.

@Akjosch Akjosch closed this Apr 8, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants