Finishing Touches on XpBeacons

At the end of last week when I put out an update for Experience Beacons, I only had a small list of improvements left for the project. I managed to pull off all of them and release an update.

What is Experience Beacons?

If you haven't been following the saga, you might not have any clue. Even if you have been following, it has been changing so quickly you might not be sure.

Beacons in Minecraft provide certain buffs, or status effects, when in proximity. There are effects like health regeneration, fast movement speed and others. These effects aren't very strong by default, though. I wanted to increase the effect strength, or "amplitude". If I just changed a few values and cranked up the effect amplitude, it would seem like cheating. There wouldn't be a high enough cost associated with getting those status effects.

XpBeacons originally was created as a way to introduce an option for greater status effects with beacons, at a cost. This cost is experience. Your xp level determines the effect strength, and there is an optional feature that slowly drains xp as you use beacons.

Along the way I ended up sprinkling a few more knobs and dials in. At this point, you can configure almost every aspect of how beacons apply status effects. This includes the effective radius, effect strength, effect duration, max beacon pyramid level, beacon tick rate and xp drain.

And the code is pretty simple, clean and readable if I do say so myself.

Maven

Since I just created a maven repository for minecraft mods like this, I started by adding a publishing configuration to build.gradle. To do this, I added a repositories section in publishing.

publishing {
	publications {
		mavenJava(MavenPublication) {
			artifactId project.archives_base_name
			// add all the jars that should be included when publishing to maven
			artifact(remapJar) {
				builtBy remapJar
			}
			artifact(jar) {
				builtBy remapJar
			}
			artifact(sourcesJar) {
				builtBy remapSourcesJar
			}
		}
	}

	// select the repositories you want to publish to
	repositories {
		if (mavenUsername != null && mavenPassword != null) {
			maven {
				url "https://maven.fracturedcode.net/releases"
				credentials {
					username mavenUsername
					password mavenPassword
				}
				authentication {
					basic(BasicAuthentication)
				}
			}
		}
	}
}

The mavenUsername and mavenPassword credentials are environment variables. That way I can publish the project and its source without having to modify the build file every time to remove that information. It's also just better security.

Lots of New Features

The first feature I added was small, which was a beacon_tick_rate rule that allows you to change how often the beacon does a complete tick, which importantly involves applying the status effects.

@Redirect(method="tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/World;getTime()J"))
private static long customBeaconTickRate(World world) {
    return XpBeaconsSimpleSettings.xpbeacons ?
            (world.getTime() % BeaconSettings.beacon_tick_rate == 0 ? 80L : 1L)
            : world.getTime();
}

This redirects an if statement that looks something like this:

if (world.getTime() % 80L == 0) {
	//do stuff
}

This has the effect of only entering every 80 game ticks, or 4 seconds. By redirecting the call to getTime, the mixin can manipulate the result of the if statement.

Unlimited Beacon Pyramid Level

In order to operate, beacons need to be built on a pyramid of rare materials. The height of this pyramid governs things like the effective reach of the beacon and status effect duration.

https://i.imgur.com/K6IOZXx.png

4 differently sized pyramids powering beacons.

The mixin that provides the main functionality for this looks like the following:

@ModifyConstant(method="updateLevel", constant = @Constant(intValue = 4))
private static int modifyMaxBeaconLevel(int maxLevel) {
    return XpBeaconsSimpleSettings.xpbeacons ? BeaconSettings.beacon_max_pyramid_level : maxLevel;
}

It's pretty easy, because the function that returns the size of the pyramid is just a for loop. I just have to change the upper bound of that loop, which in this case is 4.

for (int i = 0; j < 4; i = j++) {
	// check blocks in this level here
}

And I say "just" have to change the upper bound, but there were repercussions of this change elsewhere. The biggest one being that I had just released xpbeacons 6.0, which had introduced control over beacon reach, but was reliant on having a maximum 4 pyramid levels.

Beacon Reach

https://i.imgur.com/1M3sWAj.png

To replace the old beacon reach feature I created a new beacon_reach_multiplier rule, which essentially exposes a constant like beacon_max_pyramid_level does. In vanilla minecraft beacon reach is determined by the formula 10a + 10 = b, where 'a' is the pyramid level, and 'b' is the reach in meters. Essentially, this feature exposes a's coefficient, so that the beacon can reach further or shorter for each pyramid level.

@ModifyConstant(method="applyPlayerEffects", constant = @Constant(intValue = 10, ordinal = 0))
private static int customReachMultiplier(int value) {
    return XpBeaconsSimpleSettings.xpbeacons ? BeaconSettings.beacon_reach_multiplier : value;
}

Effect Duration

Every beacon tick the beacon applies the status effect. Status effects in minecraft last for a certain duration. Beacons apply the effect in different durations depending on the pyramid level. Effect duration is calculated by (9 + 2a) * 20 = b, where a is the pyramid level, and b is the duration in game ticks. By changing the coefficient of 'a' again (2), we can change the effect duration per pyramid level. This works with essentially the same ModifyConstant annotation as the others.

Organizing the Code

The last thing I did was remove the attrocious XpBeaconsCategorySettings.java file, which was home to over half a dozen classes and too many carpet rules. I split the classes into a nice folder structure.

Publishing

I published to all the relevant places after all these changes, pushed a README update and made a couple wiki pages. You can find the jar in Curseforge, my maven, the project's home in Gitea or Github. If you'd like to see the code changes you can check those out here.

I think this is one of those rare "complete" projects. While there might be some bugs I don't yet know about that will prompt me to release a patch, this mod now has all the features I want for it.

And frankly if I had anything else I wanted to add I would burn out on this project.

I'm exhausted.

-Fractured