Jump to content

Achievements Plugin: How to contribute achievements from other mods


blizzy78

Recommended Posts

Hi, some of you may know that I've made an Achievements Plugin. I figured that with now over a hundred achievements, maybe other mod authors might want to chime in and contribute new achievements related to their mods. I have now opened up the API of the Achievements Plugin to allow just that.

Requirements

To contribute new achievements, you only need the Achievements.dll from the plugin, starting with version 1.4.6. You need to reference that in your project.

Note that you probably don't want to make the Achievements.dll a direct dependency of your mod's DLL. Because of that, it is best to start a new DLL project that has dependencies on both your mod's DLL and the Achievements.dll. If the player doesn't have the Achievements Plugin installed, only your achievements DLL will fail to load, but your base mod DLL will run fine.

Code

To add a new achievement, you need to write a bit of code. Don't worry, it's not that much. Most of it goes into checking for achievement requirements, such as "is landed" or "is in orbit", and things like that.

Please consider the following complete and annotated example. It should readily compile and be usable in the game.

Note: The following example source code is hereby placed into the public domain, for obvious reasons. Feel free to use it as you wish. I do not reserve any rights to it whatsoever.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;

// This is an achievements factory. It is used to create actual achievement instances and to provide
// information about the category of those achievements.
//
// Achievements factory classes are searched for in all assemblies. When found, an achievements factory
// is instantiated automatically using its no-args constructor.
//
// You can have as many achievements factories as you like, but in general, you need at least one per
// category.
class NorthPoleFactory : AchievementFactory {
// Returns the actual achievements.
public IEnumerable<Achievement> getAchievements() {
return new Achievement[] {
new NorthPole()
};
}

// Returns the category of the achievements returned by getAchievements().
public Category getCategory() {
return Category.LANDING;
}
}


// An actual achievement.
//
// While achievements must only implement the Achievement interface, it is best to subclass from
// the AchievementBase class. This class provides several helper functions to register event
// callbacks.
//
// An achievement is basically a finite state machine. The main method being used is check(), which
// will be invoked in a specific interval. In this method, you should check for any requirements
// that your achievement has. If check() returns true, this means that the player has earned the
// achievement, and the method will never be invoked again. As long as it returns false, the method
// will be invoked again at a later time.
//
// Other interesting classes to look at (check the Achievement Plugin's source code):
//
// - Body (celestial bodies, also try Vessel.getCurrentBody() from Extensions)
// - Category (achievement categories)
// - CountingAchievement (base class for achievements that count up)
// - Extensions (various extension methods)
// - Location (surface locations)
class NorthPole : AchievementBase {
// You can register event callbacks in the constructor. Most of the time, this will be
// registerOnVesselChange() to reset achievement state when the player changes the vessel
// being actively controlled.
//
// See the AchievementBase class for other event handling methods.
public NorthPole() {
registerOnVesselChange(onVesselChange);
}

// While check() is invoked in specific intervals, update() will be invoked every frame.
// You can use this method to check for achievement requirements, too.
//
// USE ONLY IF check() DOESN'T ABSOLUTELY MEET YOUR NEEDS.
public override void update() {
}

// This is the main method to check for achievement requirements. If check() returns true,
// this means that the player has earned the achievement, and check() will never be invoked again.
//
// vessel is the actively controlled vessel. Note that this can be null, for instance if the
// player is in the tracking station. You should always check for null if you need the current
// vessel.
public override bool check(Vessel vessel) {
return (vessel != null) && vessel.isOnSurface() && (vessel.horizontalSrfSpeed < 1d) &&
Location.KERBIN_NORTH_POLE.isAtLocation(vessel);
}

// The vessel change event callback that was registered in the constructor.
private void onVesselChange(Vessel vessel) {
// reset achievement state here
}

// Initialize achievement state from the achievements save file, as saved earlier in save().
//
// node is a config node specific to this achievement. You can't accidentally interfere with
// other achievements.
public override void init(ConfigNode node) {
}

// Save achievement state to the achievements save file. State can be loaded again in init().
//
// node is a config node specific to this achievement. You can't accidentally interfere with
// other achievements.
//
// Note that the config node values "flight" and "time" are reserved. If you set them, they will
// be overwritten.
public override void save(ConfigNode node) {
}

// Returns this achievement's title. Keep short so that it fits into the "toast" texture.
// Always use "Headline Style".
public override string getTitle() {
return "North Pole Landing";
}

// Returns this achievement's text. Keep short (two lines of text) so that it fits into the
// "toast" texture.
public override string getText() {
return "Land at Kerbin's north pole.";
}

// Returns this achievement's key. The key must be unique across all achievements.
//
// This key MUST NOT change once an achievement has been made public. It is used to check if
// the player has already earned the achievement. If the key changes, the achievement will need to
// be earned again by the player, which makes them unhappy.
//
// It is best to use some sort of ad-hoc categories, for example: landing.kerbin.surface.northPole
public override string getKey() {
return "landing.northPole";
}
}

For more example code, check out the source code of the builtin achievements.

Title and Text Guidelines

Titles should always use "Headline Style". Keep them rather short and memorable, preferably humorous, but related to the achievement and its text. If you can't come up with something neat, ask someone else! Most people will love to toss ideas at you.

- Good example: I See What You Did There

- Bad example: I see what you did there

Achievement text must not exceed two lines of text, or it will be longer than the "toast" texture. Always check with the achievements list in the game to see if your text is short enough.

Also, the text should not be too verbose. Try to keep the list of requirements short. You can leave out obvious requirements, even though your achievement code is checking for them.

Spell out acronyms, such as "Kerbal Space Center" instead of "KSC".

To keep the player immersed, do not use real-world names and phrases. Bad example: Send a probe on a Voyager mission.

Note that the Sun's name is not Kerbol in the game, is is "Sun".

- Good example: Land on the Kerbal Space Center runway from an altitude of at least 10000 m.

- Bad example: Launch, get up to 10000 m, then land on the KSC runway, not crashing and no parts breaking off.

Hidden Achievements

Achievements are usually visible in the achievements list window, even if the player has not earned them yet. But you can "hide" an achievement, so that it remains hidden unless the player has earned it. After that, it will stay visible.

Please use hiding of achievements only sparingly, if at all. It should remain reserved for special "surprise" achievements that are not obvious. For example, an achievement that is rewarded for extracting Kethane from the ground of another celestial body should not be hidden because the act of doing so is pretty obvious.

Edited by blizzy78
Link to comment
Share on other sites

Nice work! I'll be giving this a good look for both Kethane and KAS.

The API feels very Java-like, all the way from the class structure to the naming conventions. You might consider using attributes to avoid AchievementFactory which, while serving a purpose in your code, doesn't do much for API users.

I'm a huge fan of modding APIs, and I'm glad you opted for the slightly more complex structure. Here's to this getting a lot of use!

Link to comment
Share on other sites

You might consider using attributes to avoid AchievementFactory which, while serving a purpose in your code, doesn't do much for API users.

It may look like that at first glance, but it actually isn't that simple. For example, consider the following - simplified - snippet that returns multiple achievements, one for each body in the system:


public IEnumerable<Achievement> getAchievements() {
List<Achievement> achievements = new List<Achievement>();
foreach (Body body in Body.ALL_LANDABLE)) {
achievements.Add(new BodyLanding(body, "One Small Step - " + body.name));
}
return achievements;
}

In this example, the BodyLanding class can be used over and over again, with a different body parameter resulting in a different achievement requirement (the body being landed on.)

I don't quite see how to achieve that degree of flexibility using a declarative approach.

Link to comment
Share on other sites

  • 4 weeks later...
This thread is quite old. Please consider starting a new thread rather than reviving this one.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...