Harmony: plugin interoprability API

Discussion in 'WIP and Development Status' started by RoboPhred, Feb 23, 2011.

Thread Status:
Not open for further replies.
  1. Offline

    RoboPhred

    We have magic plugins, npc plugins, alternate world plugins, and various ability plugins. But what if you want to require the player to talk to an NPC to learn magic? Or want a magic spell that sends you to an alternate world? What if a mod comes with everything and the kitchen sink, but another plugin comes with everything and a bathtub? How do you choose? Which features do you forgo in your effort to clobber together various mods of different usages and actions?

    Such is what I realized when I set out to start working on a skill system plugin , one that would let the user choose what skills to make, how you get them, what they do, and what experience is relevant to them. Sure, I could hard code a bunch of filters in, but that would mean I have to reinvent every plugin anyone wants to use.

    In solution to this problem, I present the Harmony API, an effort to provide a robust interoprable set of functions to make it trivial for plugins to export actions, filters, and data, to be used by any other plugin.
    With this proposal, the "skill problem" would be solved by an experience plugin providing filters for experience, a magic plugin providing actions to use spells, and a skills plugin would provide the user a means of defining skills leveling based on filters to perform actions.

    For example, consiter the Spells plugin, which provides an API for creating your own spells to use with wands. A hypothetical developer decides to add a new spell (let's call it "levitation"). Easily done, they create and submit the plugin.

    Now, someone comes looking for a levitation ability for their server, except they perfer runes rather than the wands of the spells plugin. Obviously, the new levitation plugin won't work, as Runecraft has no API (that I know of... this is hypothetical!). What options do they have? They can either make a private modded copy of runecraft, pettition the runecraft developer to add in a levitation ability, or try to convince the runecraft dev to make a new API for runecraft. Once the new API is in, then either a new levitation plugin must be made, or the previous one must be modified to support 2 APIs at once. Time passes, then someone develops an rpg mod, and another user wants a levitation skill. However, the developer wants this to be a realistic plugin, and denies it. The user can go to a different skill plugin, but they will loose all the skills offered by the realistic plugin. If they are really lucky, both plugins might not overlap or interfere with each other, but then the players have to deal with multiple skill commands with different arguments.

    This heavily fragmented approach is what is currently in use; many plugins providing many APIs and fixed ability sets for what is essentially a generic "do something to a specific player" concept.

    Now consiter if all of the modules used the Harmony API to share their actions (providing a configuration method to bind actions to runes/wands/commands/whatnot). As before, a developer decides to make a levitation ability for the spells plugin. This time, the spells plugin uses Harmony, so the developer codes the levitation ability and exports it to the Actions API. Another user comes along, looking to give runecraft a levitation rune. They find the levitation plugin, drop it in, configure runecraft, and now have levitation. Later, someone comes along and makes a new plugin leveraging the Harmony API that grants you different abilities based on what world you are in. A server owner wants people to fly in the nether, so he looks for a Harmony-compatible levitation plugin, finds it, and leaves happy.

    One plugin, one feature, used anywhere. Even on plugins that were created after the original. In essence, the Harmony API will act as a repository for actions, events, and other components as they come (such as my pet-project, a fully user-defined skill tree and API)

    With this in place, I envision the plugin scene migrating away from large, fixed purpose plugins down to pluggable featureset plugins, which ultimately serve as configurable connections between user actions and plugin-defined results. With this in place, users will customize their servers how they want them to run, rather than by what plugins provide what feature packs, and which play nicely with other plugins. Those that do not play nicely with others plugins or which do not allow customization will fall by the wayside. The popular plugins will be those that give the user the most control to customize their server, and support the wealth of other features made available through the API.


    (Note: while reading this, please consider problems and improvements, and post them in the thread.)

    APIs I am seeking to provide
    * Data API with pluggable backends
    - Provide a way for plugins to define data sets via an access class
    - Provide transparent caching, loading, and saving of data.
    - Allow other plugins to provide their own data backend
    - Will ship with SQL and XML backends by default.
    * Filter API
    - Provide a way for plugins to define true/false checks on entities (players, npcs, items, ...).
    - Provide a way for plugins to list and use filters.
    - Provide logical grouping (AND, OR, NOT) for multi-filter checks.
    * Action API
    - Provide a way for plugins to provide actions that can be performed on entities (players, npcs, items, ...).
    - Provide a way for plugins to list and use actions.
    * Trigger API (generic entity-based, builds upon Bukkit events)
    - Provide a way for plugins to define triggers which signify events are occurring.
    - Provide a way for plugins to register for events.
    - Provide logical grouping (AND, OR, NOT) for multi-trigger responses.
    * Entity API
    - General purpose entity system for arbitrary objects
    - Provides a means to target filters, actions, and triggers.


    Here are some examples of using the API, although they are currently in active development and will probably change

    Data API:

    @DataAdaptor.RootToken(token="mymod")
    public class MyData extends PlayerDataAdaptor

    public MyData(DataEntity entity) {
    super(entity);
    }

    @DataEntity.Key(token="somevalue")
    private String mSomeValue;

    public String getSomeValue() {
    return mSomeValue;
    }

    public void setSomeValue(String value) {
    mSomeValue = value;
    update();
    }

    public void setDynamicValue(String dynamicKey, String value) {
    fRootEntity.setValue(dynamicKey, value);
    }

    }

    This will be used by:
    MyData data = player.getDataAdapter(MyData.class);

    Data adaptors are entity-agnostic (PlayerDataAdapter is just a specialization), so this can work with any entity type:
    MyItemData data = item.getDataAdapter(MyItemData.class);


    Filter API:

    public class HoldingItemFilter extends PlayerFilter {
    public boolean check(Player player) {
    return player.isHoldingItem(..);
    }
    }

    Class<PlayerFilter > playerFilters = FilterAPI.getFilters(Player.class);

    filter = new HoldingItemFilter();
    if(filter.check(player))
    doSomething()

    The Action and Trigger API usage is similar to filter.

    Note that its a function that can be instantiated, this is as most requirements will have arguments (ex: item to hold, skill to check level), and will be stored for long term use (such as in skill definitions)


    While this is currently in active development, I plan to have a release within the next two weeks. I haven't decided on a license, but it will most likely be GPL (although I am considering making it no-derivative, to prevent spin-offs fragmenting compatibility).

    I am open (and want!) any new ideas for API that will help plugins work together, and am open to those who want to contribute code. Currently, I am focusing all my time on getting a release ready, but will take the time to set up a source control repository if anyone wants in. I will be asking for proof of your ability (I want to keep this code clean and extensible).


    Current progress:
    Data API: 75% (lacks backend support, API in flux)
    Filter API: 95% (Completed, pending design review)
    Action API: 95% (Completed, pending design review)
    Trigger API: 15% (stub classes)


    What I am doing with the API
    * Skill API (85% complete, only missing experience system)
    - Provides a means for the server owner to set up a skill tree with dependencies through Filter API and actions though Action API.
    - Provides an experience system; experience is gained by Trigger API events, and skills depend on it via the Filter API.
    * Re-Action (5% complete, concept work)
    - An in-game scripting engine and interface to connect complex actions and events to redstone circuits.

    What I would like to see use this API:
    NPCs (dialog system with triggers)
    Magic (actions)
    Runes (triggers)
    Worlds (filters)
    ...and your plugin. Yes, you!


    I'm willing to incorporate this with Bukkit itself, if the developers approve. However, this isn't a requirement, and it will be some time before I approach the developers myself.
     
  2. Offline

    dark navi

    Holy crap. +1up
     
  3. Offline

    NathanWolf

    Great thoughts!

    I'm not trying to say "Persistence does this", but expanding on plugin interoperability is one of my main goals with Persistence.

    At the simplest level, anyone can interact with Persistence to get DAOs.

    I try to write all my DAO's to be fairly self-contained. So, for instance, a PlayerData DAO has an update(Player) function that updates all of its data. It also could have a hasPermission() function, or a denyPermission, etc.

    So, all another plugin has to do to interact with (for instance) a spell in Spells 1.0 (this is what I've been working on ... for a long time now ...) would be something like this:

    Code:
    SpellVariant mySpell = persistence.get("kablooie", SpellVariant.class);
    if (mySpell != null)
    {
        mySpell.cast(player);
    }
    And you can cast the spell - without ever directly hooking into the Spells API at all. For that matter, another plugin could be managing spells- as long as they share the DAO classes.

    Now, this may not be the way bukkit will recommend you use persistence, when it's an internal bukkit thing. I think they prefer the standard form of API-based inter-plugin communication- which is a lot more tightly coupled, and has a lot more of the issues you're trying to address with Harmony.

    However, my personal thinking is that sharing DAOs is a great way to go. We could make a few common libraries of nothing but "shared" DAOs that we all decide on- so, how does a Spell look? What about Money? Can we all decided on a common class to use, and then go off and build our own separate plugins around those?

    It could be a wonderful, beautiful world if so... and something like Harmony might not even be needed, if that were the case.

    I'm going to watch this thread, I'd love to see more discussion on this!

    Inter-plugin communication is Huge and powerful, and I do it myself whenever possible- but only within my own plugins.

    It's way too overwhelming right now to try to interact with other plugin dev's work- there's just too much out there, and everyone wants you to support something different.

    Some common ground to stand on could go along way- whether that takes the form of a separate plugin, or ends up being something like core persistence allowing us to share common entities easily, without even talking to other plugins.
     
  4. Offline

    RoboPhred

    Interesting idea, although I can't say i'm in favor of using a method to store data as a generalized class repository. I would argue for a well-defined API built around the relationships around entities.

    While the DAO method solves the problem of plugin interoprability, it does not solve what I am aiming at with Harmony: the abstraction of plugin features from each other to facilitate 'dynamic' integration.

    Consider the case where a server owner wants to create a magic spell called "intellomatica", which sets a certain skill to 5. Since the skill system is not a component of spells, and is in another plugin, the cross-plugin API comes into play.

    First off, if we assume that the spell plugin has no user-facing spell configuration method, then the spell name has to come from the plugin providing the spell. Because of this, the plugin providing the spell needs to be spell-specific, and will not be compatible with other plugins (such as runecraft, which will require a block pattern). This is the primary reason for Harmony; plugins shouldn't have to target every plugin-specific API they can find, even for differentiating things such as spells, chat commands, block triggers, redstone, and whatnot. It becomes the responsibility of the spell plugin to get the list of actions it can support, rather than having other plugins provide a spell-specific method, a chat-specific method, a block-specific method, and a redstone-specific (craftbook, perhaps) method.

    Next, assume Spells imports its actions based on its own config file. The DAO method would be a very poor choice for this, as a plugin would have to define an explicit action for every wanted spell, such as a SetTheTargetedPlayerIntelloSkillToLevelFive action, which is doable, but silly. Instead, the user should be able to specify the skill and level directly in the spell definition.
    Even more telling, take the "perfect API" test and consider the spells plugin as a pure consumer of actions; providing the logic to run actions for spells, but providing no actions of its own. If DAO was used as shown, many simple spells would suffer the same fate of highly explicit actions, such as a spell to give a player an item. To get this method working, the spells plugin would need to allow an unknown number of arguments of various types to be sent to the execution function of the action.

    By providing a structured API to identify, prepare, and use actions, Harmony provides a solution to this. Through it, plugins can locate and make use of actions that require parameters or other initialization without the plugin needing to understand the classes those actions are implemented with. The idea isn't to abstract the "service" (the base plugins that manage everything), but to abstract the actions used by the service.

    To ensure extentiability, Harmony also provides an API for defining the entities on which actions, events, and data requests run on. This ensures a common ground for these actions based on their targets (player, block, item, chest, npc, ...), not the idea behind them (magic, money, specific player ability, portal, ...). This way, plugins that want to use "events from players" and "actions on players" (such as my ulterior motive: the skills plugin) would not have to be designed with those abstract ideas in mind, and the server host will be able to tell it to use any action that works on a player.


    As an example of how I propose this should work, here is some code that will get and use an action to set a skill to a certain level, as mentioned above:
    Code:
    ActionBuilder<PlayerEntity> actionBuilder = EntityAPI.getActionBuilder(PlayerEntity.class, "skills.grant");
    EntityBuilder<SkillEntity> entityBuilder = EntityAPI.getEntityBuilder(SkillEntity.class, "myNewSkill");
    
    entityBuilder.addParameter("displayName", "My new skill");
    entityBuilder.addParameter("maxLevel", 15);
    SkillEntity skill = entityBuilder.build();
    
    // Set the parameters for this action.  We could check up on the existance
    //  or type of parameters through other methods for actionBuilder if we need to.
    // addParameter works on both primitive types and Harmony-defined entities.
    actionBuilder.addParameter("skill", skill);
    actionBuilder.addParameter("level", 5);
    
    actionBuilder.build().execute(player); // the action could also be stored in a variable for future use.
    
    As both the action name and its parameters are named and settable without relying on prior knowledge of the action, it is possible to create and run actions purely by user-supplied data.
    For example, the spell plugin could provide a config file allowing the user to define actions ran when certain phrases are sent in chat. The function would look something like this:
    Code:
    private static class ArgValue {
      public String key;
      public String value;
    }
    boolean addSpell(String magicWord, String action, ArgValue[] args) {
      ActionBuilder<PlayerEntity> actionBuilder = EntityAPI.getActionBuilder(PlayerEntity.class, action);
      if(actionBuilder == null)
        return false;
      int i;
      for(i = 0; i < args.length; i++) {
        try {
          // If the parameter is a basic type, addParameter will parse the string value
          // If the parameter is an entity, addParameter will attempt to load it
          // using the provider for the entity type in question (more on this later).
          actionBuilder.addParameter(args[i].key, args[i].value);
        } catch(ActionBuilder<?>.ParameterException e) {
          // No such parameter, or cannot load the value from the string.
          return false;
        }
      }
      fPhraseSpells.put(magicWord, actionBuilder.build());
    }
    
    And the config file would look like
    Code:
    spell: intellomatica
        action: skills.grant
          player: self
          skill: myNewSkill
          level: 5
    
    ("player: self" requires a way to resolve relative entities on action execution, rather than on action building. This is being worked on).



    Defining an action is easy, here is the class definition for the skills.grant action
    Code:
    // The short name for this action.  If more than one action tries to use the same
    //   short name, the name will be disabled for all classes that use it.
    //  The classes will still be accessible by their full class path and name.
    @Action.ShortName("skills.grant")
    public class SkillGrantAction extends Action<PlayerEntity> {
    
      @Action.Parameter(name="skill")
      private SkillElement mSkill;
    
      @Action.Parameter(name="level")
      private int mLevel;
    
      // Constructor required by the API for the builder.
      public SkillGrantAction(ActionParameterMap parameters) {
        // We could read other parameters in here,
        // as long as the parameter names were defined on
        // a field or the class-wide @Action.Parameters({"p1", "p2", ...})
      }
    
      // We can provide constructors for direct use, if desired.
      public SkillGrantAction(SkillEntity skill, int level) {
        mSkill = skill;
        mLevel = level;
      }
    
      public void execute(PlayerEntity target) {
        PlayerSkillsAdapter adapter = target.getAdapter(PlayerSkillsAdapter.class);
        adapter.setSkillLevel(mSkill, mLevel);
      }
    }
    


    Most actions, especially those providing minecraft-specific actions, will not require anything else. However, this action provides both a new entity type for its skills, as well as an entity adapter to allow the skills to operate and store data on players entities.


    First, a clarification on the definition I am using for 'entity': In this API, an entity defines discrete types on which actions can run and data can be appended to.
    Only data that is strictly a direct component (such as a player's name) should be attached to an entity, while other 'concepts' (such as a player's inventory) of data should ideally be attached using adapters. This keeps the entity as simple as possible and offloads complex components to purpose-built classes, which then have the benefit of being able to provide helper methods that would otherwise clutter up the root entity. Additionally, adapters are the only way for other plugins to attach data to entities they do not define, and as such using adapters where they make sense brings about a more consistent API.


    Here is the entity defining the skill type. It is important to note that this skill entity /defines/ a skill (name, max level, ...), rather than an /instance/ of a skill (a player's skill level).
    Code:
    // Specify this entity can be created on the fly via the api.
    //   This annotation might also accepts a class extending EntityBuilder<thisClass> as
    //   a parameter to specify a custom builder, in case this entity has a
    //   non-trivial constructor.
    @Entity.Buildable
    
    // Specify instances of this entity can be fetched directly using the API
    //   Sometimes it is desirable to never permit the entity from being obtained
    //   directly, usually in the case of instance-entities (such as inventory items).
    //   For this reason, it is recommended that neither this nor Buildable is used
    //   when implementing DependantEntity<? extends Entity>.
    @Entity.Fetchable
    
    // Specify this entity has a provider to load entities from external sources.
    //   This annotation takes an argument implementing the EntityProvider<?>
    //   interface, and should be used to provide persistence.
    //   Any loading of entities be handled by the provider, and
    //   EntityAPI.getEntityBuilder will throw an exception if it gets an id that
    //   the provider claims is in use.  Additionally, any new entities will be passed
    //   to the provider in a callback.
    // You can use this to define a custom class for entities that are pulled externally,
    //    such as those defined in config files or gotten from minecraft itself.
    // The argument will default to StaticEntityProvider<?>, which will store
    //   new entities in a linked list, and provides no persistence (the data will be
    //   lost on server shutdown).
    // Also available is PersistentEntityProvider<?>, which will use the persistence
    //   engine the server has configured Harmony to use.
    // Note: If both Provider is left as static, and Buildable is not specified, then
    //    this entity will not be usable
    //     (should rearrange these attributes for default-buildable with no persistence)
    @Entity.Provider(StaticEntityProvider<?>)
    
    public class SkillEntity extends Entity {
    
      @Entity.ID
      private String fId;
    
      @Entity.Parameter(name="displayName", defaultValue="skill")
      private String mDisplayName;
    
      @Entity.Parameter(name="maxLevel", defaultValue="0")
      private String mMaxLevel;
    
      public String getDisplayName() {
        return mDisplayName;
      }
    
      public int getMaxLevel() {
        return maxLevel;
      }
    
    
      // Methods to apply skills to players
    
      public boolean canLevel(PlayerEntity player) {
        ...
      }
    
      public boolean levelUp(PlayerEntity player) {
        if(canLevel(player) == false)
          return false;
        ...
      }
    }
    
    A skill entity could then be made by
    Code:
    EntityBuilder<SkillEntity> skillBuilder = EntityAPI.getEntityBuilder(SkillEntity.class, "aNewSkillID");
    skillBuilder.addParameter("displayName", "A new skill");
    skillBuilder.build();
    
    Now that the skill is in place, we need a method to track the skill levels for each player. This is where the Data API comes in. Here, an 'adapter' (which acts like a plugin to an existing entity) is used to provide storage and access to the skill levels.
    Note that this adapter provides direct, unbounded access to the levels, and is only intended for use by the skill entity itself. Plugins that need to do this should also provide a public adapter that acts as a proxy.
    (This is the 'new' Data API, and supersedes the API mentioned in the first post).
    Code:
    // Some persistency thing, Harmony will provide its own user-configurable
    //   persistence framework.  Haven't done any work in this area.
    //   Might want to provide a structured loader/saver similar to @Entity.Provider.
    @PersistentClass
    // This class is default scope, as it is for use by SkillEntity only.
    class InternalPlayerSkillsAdapter extends EntityAdapter<PlayerEntity>
    
      @PersistentList
      private final HashMap<SkillEntity, Integer> fSkills = new HashMap<SkillEntity, Integer>();
    
      // This should always be inacessable for normal use, as
      //   the Harmony API provides caching
      private InternalPlayerSkillsAdapter(EntityAdaptorParent parent) {
        // EntityAdaptorParent provides access to the entity this adapter
        //  is for.  The seperate type is more or less just a means of
        // ensuring adapters are not created outside the API.
        super(parent);
      }
    
      public int getSkillLevel(SkillEntity skill) {
        if(fSkills.containsKey(skill) == false)
          return 0;
        return fSkills.get(skill);
      }
    
      // Directly set the skill.  Again, this is for internal use.
      public bool setSkillLevel(SkillEntity skill, int level) {
        fSkills.put(skill, level);
      }
    
    }
    
    Now that we have an adapter set up for skills, we can then go back and give SkillEntity its remaining methods:
    Code:
    ...
    
      public boolean canLevel(PlayerEntity player) {
    
        // Check other filters
    
        if(mMaxLevel == 0)
          return true;
    
        final InternalPlayerSkillsAdaptor adaptor = player.getAdaptor(InternalPlayerSkillsAdaptor.class);
        if(adaptor.getSkillLevel(this) >= mMaxLevel)
          return false;
    
        return true;
      }
    
      public boolean levelUp(PlayerEntity player) {
        if(canLevel(player) == false)
          return false;
    
    
        final InternalPlayerSkillsAdaptor adaptor = player.getAdaptor(InternalPlayerSkillsAdaptor.class);
        adaptor.setSkillLevel(this, adaptor.getSkillLevel(this) + 1);
    
        // Run actions
        // Fire triggers
      }
    
    ...
    
    And finally, to play nicely with others and provide a robust API, we should provide an adapter to let other plugins get skill levels, and ease the direct use of skills.
    Code:
    public class PlayerSkillsAdapter extends EntityAdapter<PlayerEntity>
    {
    
      private PlayerSkillsAdaptor(EntityAdaptorParent parent) {
        super(parent);
      }
    
      public int getSkillLevel(SkillEntity skill) {
        final InternalPlayerSkillsAdaptor adaptor = player.getAdaptor(InternalPlayerSkillsAdaptor.class);
        return adaptor.getSkillLevel(fParent.get());
      }
    
      public boolean canLevel(SkillEntity skill) {
        return skill.canLevel(fParent.get());
      }
    
      public boolean levelUp(SkillEntity skill) {
        return skill.levelUp(fParent.get());
      }
    }
    
    Progress update: The Entity API is now about 30% complete. The other APIs need to be tweaked a bit to accept the new Entity API, but not much.
     
  5. Offline

    NathanWolf

    Wow, I'm not sure what to say- I definitely do not recommend going off and implementing your own persistence framework- that's a tremendous amount of work on its own, and bukkit wants it as core tech- so you'd be off on your own, once that's available.

    As for the rest - well, to be frank- it sounds like you're asking devs to to trade off tightly coupling to specific plugins with tightly coupling to your API- you're going to have to make something very elegant and general-purpose for that to sell, at all.

    Basically, I guess what I'm saying is- devs are going to use whatever plugin interop is available in core bukkit, and I don't think you're going to get much buy-off on people writing their plugins around another plugin's API.

    People have been willing to do it with Persistence, to some extent, but that's largely, I think, for these three reasons:
    1. I had already proven it before release, to a degree, by having a "published" plugin using it
    2. It provides a necessary service that is currently missing from core Bukkit- saving data
    3. I made, and am making, every effort to get it integrated into bukkit, and have promised that, if it is not integrated, I've made (or will modify) the API to be as close to bukkit as possible- and that something like this will exist in bukkit, so it really doesn't make sense to go off and do it on your own.
    I think you need at least all three of these elements for another plugin dev to use your plugin.

    And, even with all of this- I still have very few users, and not too many happy ones at that!

    It's very hard to make a solid API, and stick to it- the bukkit devs have a much harder job than most people give them credit for. People whine and complain when the devs "break" their plugins- but the fact is, they do so much work behind the scenes without ever affecting us at an API level- and that is a testament to their strong design skills.

    You're basically going to have to match that with Harmony for it to be something people will use- it's going to have to be of the caliber that a bukkit dev might notice it, and possibly consider it for core integration.

    Otherwise, it's going to be a hard sell.

    My 3c- take them with a grain of salt :)
     
  6. Offline

    RoboPhred

    Whatever the end result is, I am going to be using something like this for my Skills plugin. From what I have seen, a plugin that lets the server be set up an MMO style skill system exactly how they want it has a decently high demand already, and that is without players being used to customizing their servers to that extent. If successful, other devs will probably start getting petitioned to support it. And once plugins are providing the actions, there's no real reason why other plugins shouldn't take advantage of it.

    As for persistance, I am no longer including that in this project; instead, Bukkit's own persistence will be added to PersistentEntityProvider, and users of the API will use Bukkit persistence directly with the EntityAdapter where they need it. The code in the second post shows this, even if a few comments are out of date.

    Also, I am aware right now that the API is complex, perhaps a bit too much. Right now I am focusing on an exploratory design, making the API alongside a few projects to use it, and seeing which features need to be supported or what should be cut out.
     
  7. Offline

    NathanWolf

    Again, don't meant to say "I'm doing this"... but I'm doing this :)

    I started a "Classes" plugin a while ago- it's going to be called Experience now.

    Only really been waiting on a decent permissions system so I can use built-in groups as classes.
    --- merged: Mar 1, 2011 12:33 PM ---
    Just to you're aware of the situation, the persistence engine from within the Persistence plugin is currently under code review to be accepted as bukkit.persistence. I can't, of course, guarantee that they'll pick it- but, quite honestly, it's either that, or they write one from scratch.

    That's definitely how I got started with Persistence- it quickly became something I thought other people might want and need- and I think I was right.

    Hopefully the same will happen to you! Just make sure you're not doing a lot of redundant work...
     
  8. Offline

    RoboPhred

    At risk of derailing the topic...

    I have implemented skill systems before in several games, and what I have found is that the only thing you need are skills (supporting dependencies and subskills) and experience. Classes are little more than skills near the root of the skill tree that provide their own levelup points. Essentially, as long as you have these:
    Code:
    experience
      -- triggers for xp gain
    skill
     -- optional visiblity to player (always, never, or when levelable)
     -- display path (for subskills in the interface)
     -- ability to auto-levelup when requirements are met
     -- requirements (xp and presense/absense of other skills)
     -- actions (set skill level, xp, other player attributes)
    
    you can make any skill tree rivaling the most complex MMOs. 'classes' are just skills that other skills are dependent on, and any decent skill system will support dependent skills further up in the tree (ex: magic->earth magic->open portal, magic->life magic->summon creeper). 'Class' skills would directly affect a private experience pool to give levelup points for their subskills, and use skill dependencies to implement class selection rules.
    If you want to see how I set it up in the past, go to http://static.robophreddev.net/euka.txt and search for "Professions, Skills, and Experience". While this is from a very old project and treated professions differently at the configuration level, internally it used skills for professions and experience for levels. It was successful in its time as well, powering a large, complex skill tree for the community I developed it for.

    Anyway, I certainly would like to work with you on your skill plugin as long as you are aiming for this level of configurability, the idea of sub-skills and skill dependencies are the most important features to me. Until then, back to my mad experiments!
     
  9. Offline

    NathanWolf

    Nah, I wouldn't consider this derailing, exactly- you may decide it makes sense to narrow the focus of Harmony, for instance- so it's good to explore specific use cases.

    For Experience, this is a good example of using persistence and bukkit integration. My plugin is going to be very "thin", in the sense of managing leveling trees- just like Groups is very thin about user/group management.

    Making a leveling tree is fairly easy- as you say, and much like with users/groups, it's a fairly well-trodden path.

    And this is exactly why I'm so much in favor of a shared DAO repository. This is how to encapsulate such functionality, in an object-oriented way. Each object should be its own entity, capable of being used completely on its own (or in conjunction with references to other public DAO's).

    It's arguable whether these should be implemented purely in terms of an interface- but I'm guessing so. If the bukkit team takes any of my DAO's, my assumption is they'll either integrate the functionality directly into the corresponding Bukkit API class (e.g. PlayerData goes to bukkit.entity.Player), or they will make them interfaces and move the actual functionality to CraftBukkit.

    However it's done, this is IMO the right way to do it- and I think the Bukkit team generally agrees, except that we might differ on the best way to access these DAO's. I know @Cogito, at least, would prefer an API for everything- so you would talk to a (perhaps generic) Levelling or Classes API for this, as opposed to just using persistence to get a Level or Class DAO directly.

    Anyway- back to the subject at hand.

    Experience, my plugin, is going to be mainly about that- EXP. Hence the name change. Classes and leveling are largely handled by users/groups- there's no need to re-invent that wheel.

    The hard part is putting together a clean API for what basically amounts to a cost/decision tree- the player has a certain amount of any number of types of EXP- and we can define what it "costs" in terms of those EXP values to move from one group to a related (dependent) group.

    That's basically it, for leveling.

    The real "meat" of Experience will be its built-in EXP types (something that other devs could expand on in their own plugins, of course!)

    I plan on tacking exploration first- I want to use persistence to track, at a coarse level, everywhere each player has been (including y values for below-ground exploration- possibly with bonuses for this, in fact).

    Then I'll move on to the "simpler" stuff like construction, mining, combat, farming, hunting, etc. Any of these could be mapped to any type of EXP for your own system (or all mapped to a single generic XP, if you prefer a more traditional style to the Fable-type approach I'm allowing).

    And that's basically it. It's up to the plugin dev and/or admin to decide how to use all this- I just want to give people tools.

    Give people good tools, and they might use them. They may even thank you for giving them the tools, and possibly recognize what it took for you to build those tools.

    However, if you try to force an interface of some kind on people, they'll reject it, find a way to subvert it (if necessary), and do their own thing the way they want to do it... that's just how programmers are. We like things the way we like them.
     
  10. Offline

    RoboPhred

    That is what my proposal tries to solve; rather than having specific interface requirements, it provides a 'normalized' way. If a plugin wants to work with another, it's going to have to work with some interface. This way, a common interface for entity-based actions is used that hides the implementation while still providing information on parameters the actions will need.
    Of course, this is still aimed straight at the idea that plugins can provide discrete functionality.


    Anyway, aside from Harmony as an action repository, there is a specific API method that I want to draw your attention to for feedback:

    Code:
    GroupList groups = player.getAdapter(GroupList.class);
    
    While this is more syntactic than anything, I believe the entity adapter method would be a good addition, even without the rest of the entity API proposal. This would mirror DAOs in that it would provide a means of easily attaching a set of API functions to an entity through a straightforward function, without having to worry about what the string identifier would be for the given entity. It would have essentially the same effect as persistence.get(player.getName(), GroupList.class), but I feel it will simplify DAO usage and make it easier for developers to work with.

    The classes returned don't even have to be a DAO (in the main Harmony API, the default adapter is not persistent), although if they were it would make adding new attributes to players downright trivial:
    Code:
    @Persistent
    public class MyNumberAdapter extends PlayerAdapter {
      public int MyNumber;
    }
    
    ...
    // Set a number to the player, which will automagically save!
    player.getAdapter(MyNumberAdapter.class).MyNumber = 3;
    ...
    
    The benefit of this is that the developer no longer has to deal with managing and mapping the player IDs to their provided data classes. Instead, bukkit's entity layer deals with creating and returning the requested adapter linked to specific IDs, eliminating the usual boilerplate code to connect their data values to players.
     
  11. Offline

    NathanWolf

    I'm not sure I "get" this- can you provide a use case?

    What would an "adapter" do that you couldn't just do with a static function on the DAO?

    I'm not really sure why you'd want to do that, even- which is why I want to see a use case. I may be misunderstanding what you're after.
     
  12. Offline

    RoboPhred

    This won't change what you are able to do, what it does is make it easier for developers to connect an api or data to existing entities. The same code done strictly via DAO would be:
    Code:
    GroupList groups = persistence.get(player.getName(), GroupList.class);
    
    Not much difference, but by connecting API classes directly to the players, rather than leaving it up for interpretation via a string ID, it would provide a more robust relationship between them and make it less ambiguous for developers.
    Additionally, if GroupList was called for a new player, it would have to pull in the player name it was created for and seek out the right player entity. As GroupList is intended to run directly on a player entity, why not parent it to them in design rather than keeping them separate of anything in the persistence layer.

    The adapter method isn't another persistence mechanism though, its purpose is to initialize a given class on the entity that class works with. While these classes can support persistence, its not a requirement.

    Consider for example a plugin which provides the post count of a forum user with the same name as the player entity. It needs no persistent data of its own, as it pulls the information live (and possibly caches it for the lifetime of the server). If DAO was used, a new instance of this plugin class would get created for every player whenever it was accessed, despite the fact that there is nothing to store; DAO just acts like an expensive class registry.

    Anyway, this comes down to an API usability enhancement, and not a new feature.
     
Thread Status:
Not open for further replies.

Share This Page