Modding Snippets
[edit]
Please help us build the Modding community by sharing some of your examples. Feel free to update this page via the wiki editor!
When adding code snippets to the wiki, follow this example:
<pre>
// here is some code!
System.out.println("Necesse is cool");
</pre>
Utilities[edit]
Post your Utilities examples here.
Getting All Textures - Author: Kamikaze[edit]
Put this loop into the postInit() function. run buildModJar and then runClient from the gradle tray. The resource folder will output to your project directory.
for (Map.Entry<String, GameTexture> tex : GameTexture.getLoadedTextures()) {
tex.getValue().saveTextureImage("resources/" + tex.getKey());
}
Getting server object - Author: kiriharu[edit]
Server object can be obtained from ServerStartEvent. To simple access to it I set server instance to static variable.
import necesse.engine.GameEventListener;
import necesse.engine.GameEvents;
import necesse.engine.events.ServerStartEvent;
import necesse.engine.modLoader.annotations.ModEntry;
import necesse.engine.network.server.Server;
@ModEntry
public class TestMod {
public static Server SERVER;
public void init() {
// Register ServerStartEvent event listener to obtain server instance
GameEvents.addListener(ServerStartEvent.class, new GameEventListener<ServerStartEvent>() {
@Override
public void onEvent(ServerStartEvent e) {
// Setting server instance to static variable SERVER, so it can be accessible everywhere in code
TestMod.SERVER = e.server;
}
});
}
}
});
Get access to to death messages - Author: kiriharu[edit]
To get access to death messages you can patch DeathMessageTable.getDeathMessage().
public class DeathMessagePath {
@ModMethodPatch(
target = DeathMessageTable.class,
name = "getDeathMessage",
arguments = {Attacker.class, GameMessage.class}
)
public static class MethodPatch {
@Advice.OnMethodExit
public static void onExit(@Advice.Return(typing = Assigner.Typing.DYNAMIC) LocalMessage returned) {
// do everything what you want with death message
}
}
}
Config - Author: kiriharu[edit]
Fair said mod framework have an official way to create config, so add initSettings method in ModEntry class like this:
public static int myInt = 0; // Setting default value to 0
public ModSettings initSettings() {
return new ModSettings() {
@Override
public void addSaveData(SaveData saveData) {
// Save data here
saveData.addInt("myInt", myInt);
}
@Override
public void applyLoadData(LoadData loadData) {
// Load data here
savedInt = loadData.getInt("myInt", myInt);
}
};
}
It will create a file in %APPDATA%/cfg/mods/modid.cfg file like this:
{
SETTINGS = {
myInt = 0
}
}
Getting the client from anywhere[edit]
Method 1 - Author: DimitarBogdanov/m4trixglitch
Sometimes, it's quite difficult to find an instance of Client (e.g. in a Control's activate method). This snippet should work, as long as the user is ingame (shocker, I know...) Returns null if the user isn't in-game. Not tested thoroughly, but it appears to work.
public static Client getClient()
{
State gameState = GlobalData.getCurrentState();
if (!(gameState instanceof MainGame))
{
return null;
}
MainGame mainGame = (MainGame) GlobalData.getCurrentState();
Client client = mainGame.getClient();
return client;
}
Method 2 - Author: FerrenF
If the above method does not work for your use-case, then the following method is more robust in the sense that it will return the client regardless of being in game or not.
static Client getGameClient() {
State currentState = necesse.engine.GlobalData.getCurrentState();
return currentState instanceof MainGame ? ((MainGame) currentState).getClient() : ((MainMenu) currentState).getClient();
}
Including external dependencies in your .JAR[edit]
Author: FerrenF
There might be a case where you need access to code that is provided by an external java library that is NOT included in the Necesse base game libraries. For example, you might want to include com.google.gson as a JSON parser to read formatted data you have included in your mod.
1. Prerequisites
To do this, we need the library we want to include first:
Store a copy of the library that is compatible with the java language level used by Necesse within your project. For Necesse 0.30.0, you would need to use gson 2.8.9 - far below the latest release. If your project directory is C:/Users/User/eclipse-workspace/ProjectName/* then you might put this library at C:/Users/User/eclipse-workspace/ProjectName/libs/. If the folder does not exist, then create it. The name of the sub-folder is arbitrary.
2. Modifying build.gradle
Let's assume that you are using the build.gradle file contained within the ExampleProject provided by @Fair. Below the all-caps warning to not edit below this line (A suggestion!), you will find a section of code preceded by the word 'dependencies':
dependencies {
implementation files(gameDirectory + "/Necesse.jar")
implementation fileTree(gameDirectory + "/lib/")
implementation fileTree("./mods/") // Add all mods located in local mods folder
// Add any third party library dependencies here. Remember to use libDepends, so that they will be added to your jar on build
// These are some examples:
// libDepends group: 'com.google.guava', name: 'guava', version: '31.1-jre'
// libDepends files("path/to/library/jar.jar")
}
We are not going to use libDepends, because it is much easier to use implementation and a small modification to the buildModJar routine. First, add the library file (in .jar format) that you have downloaded and moved to your project's directory:
dependencies {
implementation files(gameDirectory + "/Necesse.jar")
implementation fileTree(gameDirectory + "/lib/")
implementation fileTree("./mods/") // Add all mods located in local mods folder
...
implementation files('libs/gson-2.8.9.jar')
}
We added: `implementation files('libs/gson-2.8.9.jar')` here. Next, farther down in your build.gradle script will be a task called buildModJar. This task is responsible for producing your mod's .jar file that will eventually end up in the game's mod folder.
task buildModJar(type: Jar) {
group "necesse"
description "Generates the mod jar into the build folder"
dependsOn "classes"
...
archiveName "${jarName}.jar"
destinationDir file(buildLocation)
Here, we are going to instruct gradle to package the external libraries necessary for your project into the same .jar file that will be your mod. This is called making a 'fat jar'. Additionally, we are going to instruct gradle to exclude duplicate meta-info/module-info files that may conflict when copying the external library into the .jar being built.
task buildModJar(type: Jar) {
...
// Add only gson-2.12.1.jar, but exclude module-info.class if it exists
from {
zipTree(file('libs/gson-2.8.9.jar')).matching {
exclude 'module-info.class', 'META-INF/versions/**'
}
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE // Prevent duplicate conflicts
archiveName "${jarName}.jar"
destinationDir file(buildLocation)
We added:
from {
zipTree(file('libs/gson-2.8.9.jar')).matching {
exclude 'module-info.class', 'META-INF/versions/**'
}
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
3. Adding the library to the project Build Path
Finally, before attempting to utilize the library you just added - make sure that your external library has been added to the project's build path. Depending on the IDE you are using, this might look different.
Using Eclipse to do this, you would right click your project's root in the package explorer, go to 'Properties', then 'Java Build Path', and under the 'libraries' tab you would use the 'Add JARs' button to locate and add the .jar file you moved to your project into the build path.
4. Refresh Gradle and Build
You are almost done. clean, refresh, and then build your gradle project. Assuming there are no compile errors, it's then time to test that this library has been appropriately added to your project. In your main script, import the package name of the library you added to the build path. For gson, importing the Gson class - it looks like this:
package YourProject; ... import com.google.gson.Gson; ...
If your IDE isn't complaining, then it is probably a good sign. Try using some of your libraries' class definitions and then rebuild. If your compiler does not throw any additional errors, then you have successfully added an external library dependency into your project. You do not need to distribute this library with your mod, because it's bundled into your mod.
5. Things to consider
- Don't use unfamiliar libraries that may execute unsafe code. Adding them into your mod means that arbitrary code can be executed any time the game is running.
- Be mindful of the size of the library you include. Your mod's final .jar file will increase in size directly proportional to the size of the library you are including. Be careful not to use libraries that include javadocs, because they are not compressed.
Equipment[edit]
Post your Equipment examples here.
Adding a sword - Author: Quack
To get started start by placing the texture of your sword in /resources/player/weapons/ as well as /resources/items/. Be sure that the name of the image is the same as it's itemStringID. The item icon placed in the items folder should be 32x32 but the actual texture of the sword places in weapons can be bigger.
Within your mod folder (in this example it's named examplemod) create a new folder or "Package". Within this new folder create a new Java class and name it after the weapon. In this case the weapon class is within a package called "tests" and the class itself is named TestSword.java.
The following code is all that it needed to set the properties of you sword. There are also things like attackXOffset and attackYOffset which can be set if the texture doesn't fit in the character's hand quite right.
package examplemod.tests; // Change this to the package name where you have your weapon class
import necesse.inventory.item.Item;
import necesse.inventory.item.toolItem.swordToolItem.SwordToolItem;
// Extends SwordToolItem
public class TestSword extends SwordToolItem {
// Weapon attack textures are loaded from resources/player/weapons/<itemStringID>
public TestSword() {
super(400);
rarity = Item.Rarity.COMMON;
attackAnimTime.setBaseValue(300); // attack time
attackDamage.setBaseValue(20) // Base sword damage
.setUpgradedValue(1, 95); // Upgraded tier 1 damage
attackRange.setBaseValue(60); // range
knockback.setBaseValue(100); // knockback
}
}
In the example above the weapon class is located in /examplemod/tests/TestSword.java.
Now within the main mod java file you want to include three new lines. The first one at the very top to import the newly created weapon class:
import examplemod.tests.*; // Change this to fit your package name
Then in the init() function we need to add:
public void init() {
ItemRegistry.registerItem("testsword", new TestSword(), 20, true);
}
This registers the weapon in the item registry and sets it's barter value.
Lastly in the postInit() function we need to add:
public void postInit() {
Recipes.registerModRecipe(new Recipe("testsword",
1,
RecipeTechRegistry.NONE, // Here you define the crafting technology needed to craft
new Ingredient[]{
new Ingredient("anylog", 5),
new Ingredient("anystone", 5) // Here you define which ingredients are needded to craft
}).showAfter("woodboat")); // Change this to which item you want it to show after in the menu
}
In this example we have added the test sword as an item which is craftable everywhere for just 5 logs and 5 stones. And it shows after the wood boat item.
NPCs[edit]
Example Shopkeeper - Author: Eryr[edit]
Implementing a new shop NPC requires creating a new file, and registering the new NPC in the registry. This example NPC is unrecruitable.
You'll have to set your NPC to spawn somewhere, or otherwise it will only be accessible via Creative Mode. That is outside of the scope of this tutorial.
import java.util.ArrayList;
import necesse.engine.localization.message.GameMessage;
import necesse.engine.network.server.ServerClient;
import necesse.entity.mobs.friendly.human.humanShop.HumanShop;
import necesse.entity.mobs.friendly.human.humanShop.SellingShopItem;
import necesse.gfx.drawOptions.human.HumanDrawOptions;
import necesse.inventory.InventoryItem;
import necesse.inventory.lootTable.LootItemInterface;
import necesse.inventory.lootTable.LootTable;
import necesse.inventory.lootTable.lootItem.LootItem;
// This is an example shopkeeper, who cannot be recruited, but you can buy a few items from them!
public class ExampleShopkeeper extends HumanShop
{
public ExampleShopkeeper()
{
super(400, 400, "example_shopkeeper"); // Set the shopkeeper's health and ID.
this.attackCooldown = 500;
this.attackAnimTime = 500;
this.setSwimSpeed(1.0F);
this.equipmentInventory.setItem(6, new InventoryItem("katana")); // Set the shopkeeper's weapon, a katana in this instance.
/* This shopkeeper will sell a variety of health potions, and a recall potion. Each item is limited to a stock of 20, with stock
* being restored by +5 per day, up to the maximum of 20. We set the random price in a range according to the item's typical price.
* The higher level items require the interacting player (i.e the player buying from the shop keeper) to have defeated a specific
* boss to be available. This is usually bosses, but you can set it to be any mob, or any mob from a list of mobs, or all mobs from
* a given list.
*/
this.shop.addSellingItem("healthpotion", new SellingShopItem(20, 5)).setRandomPrice(15, 20);
this.shop.addSellingItem("greaterhealthpotion", new SellingShopItem(20, 5)).setRandomPrice(40, 50).addKilledMobRequirement("piratecaptain");
this.shop.addSellingItem("superiorhealthpotion", new SellingShopItem(20, 5)).setRandomPrice(150, 200).addKilledMobRequirement("fallenwizard");
this.shop.addSellingItem("recallscroll", new SellingShopItem(10, 5)).setRandomPrice(25, 50);
}
// Determines the shopkeeper's loot dropped on death. In this instance, they'll drop 100 coins on death.
public LootTable getLootTable()
{
return new LootTable( new LootItemInterface[] { new LootItem("coin", 100)} );
}
// Determines the default 'cosmetic' items used by the shopkeeper. We'll use leather armour for convenience's sake.
public void setDefaultArmor(HumanDrawOptions drawOptions)
{
drawOptions.helmet(new InventoryItem("leatherhood"));
drawOptions.chestplate(new InventoryItem("leathershirt"));
drawOptions.boots(new InventoryItem("leatherboots"));
}
// Determines the 'greeting' messages used by this NPC. This is a very basic implementation.
protected ArrayList<GameMessage> getMessages(ServerClient client)
{
/* Returns a list of messages from the user's selected language file matching the associated keys. In layman's terms, this means
* that the game will look at the user's selected language file for the following keys under the category 'mobmsg':
* example_shopkeeper_greeting1, example_shopkeeper_greeting2, example_shopkeeper_greeting3
*
* This is a very basic implementation. If you can do a lot of interesting stuff if you return a custom list of messages!
*/
return this.getLocalMessages("example_shopkeeper_greeting", 3);
}
}
With our new shopkeeper NPC defined, we can register them in our mob registry.
import ExampleMod.Mobs.Friendly.Human.ExampleShopkeeper;
import necesse.engine.modLoader.annotations.ModEntry;
import necesse.engine.registries.MobRegistry;
import necesse.engine.registries.WorldPresetRegistry;
import necesse.engine.world.worldPresets.WorldPreset;
@ModEntry
public class ExampleMain
{
public void init()
{
MobRegistry.registerMob("example_shopkeeper", new ExampleShopkeeper(), true);
}
}
One last thing before we're finished! We need to also define the example shopkeeper's greeting dialogue. Let's start by creating an english language file (en.lang), and placing it in our resources/locale folder (e.g examplemod/src/main/resources/locale/en.lang), and then adding the required entries.
[mobmsg] example_shopkeeper_greeting1=Exp1! example_shopkeeper_greeting2=Exp2! example_shopkeeper_greeting3=Exp3!
With our shopkeeper defined, registered, and with our greetings defined, our shopkeeper mob is ready to be used. It's up to you as to where it should spawn. Perhaps it should spawn in a new world preset?
Mobs[edit]
Post your Mob examples here.
World Generation[edit]
Creating a new biome - Author: Eryr[edit]
Creating a new biome is a fairly simple process once you understand how it works.
The first step is to create your biome file. This file will contain all of the important details for your biome, including terrain tiles, object generation, entity spawns, music selection, and so on. Something that veteran modders from before 1.0 might notice is that there's no mention of presets! We'll focus on those later, but for now, what you need to know is that they're not handled here, but via a separate system that we'll cover later.
Once you've created your biome file, you should register it and assign it a generation weight to ensure that it'll spawn naturally in a world.
Once you've completed both steps, you'll have successfully created a new biome!
Here's a full example biome, based on the Forest Biome:
import java.util.concurrent.atomic.AtomicInteger;
import necesse.engine.AbstractMusicList;
import necesse.engine.MusicList;
import necesse.engine.registries.MusicRegistry;
import necesse.engine.registries.ObjectRegistry;
import necesse.engine.registries.TileRegistry;
import necesse.engine.sound.GameMusic;
import necesse.engine.sound.SoundSettings;
import necesse.engine.sound.SoundSettingsRegistry;
import necesse.engine.util.GameRandom;
import necesse.engine.util.LevelIdentifier;
import necesse.engine.world.biomeGenerator.BiomeGeneratorStack;
import necesse.entity.mobs.PlayerMob;
import necesse.inventory.lootTable.LootTablePresets;
import necesse.level.gameTile.GameTile;
import necesse.level.maps.Level;
import necesse.level.maps.biomes.Biome;
import necesse.level.maps.biomes.FishingLootTable;
import necesse.level.maps.biomes.FishingSpot;
import necesse.level.maps.biomes.MobSpawnTable;
import necesse.level.maps.presets.RandomCaveChestRoom;
import necesse.level.maps.presets.caveRooms.CaveRuins;
import necesse.level.maps.presets.set.ChestRoomSet;
import necesse.level.maps.presets.set.FurnitureSet;
import necesse.level.maps.presets.set.VillageSet;
import necesse.level.maps.presets.set.WallSet;
import necesse.level.maps.regionSystem.Region;
// This is an example biome, with content for surface, cave, and deep cave layers.
public class ExampleBiome extends Biome
{
// Our fishing loot tables determine what types of fish can be caught in water.
public static FishingLootTable SurfaceFish = new FishingLootTable().addAll(Biome.defaultSurfaceFish);
public static FishingLootTable CaveFish = new FishingLootTable().addAll(Biome.defaultCaveFish);
public static FishingLootTable DeepCaveFish = new FishingLootTable().addAll(Biome.defaultCaveFish);
// These mob spawn tables determine what hostile mobs spawn in a given layer.
public static MobSpawnTable SurfaceMobs = new MobSpawnTable().add(100, "zombie");
public static MobSpawnTable CaveMobs = new MobSpawnTable().add(100, "skeleton");
public static MobSpawnTable DeepCaveMobs = new MobSpawnTable().add(100, "ancientskeleton");
// These mob spawn tables determine what critters spawn in a given layer.
public static MobSpawnTable SurfaceCritters = new MobSpawnTable().add(100, "frog");
public static MobSpawnTable CaveCritters = new MobSpawnTable().add(100, "frog");
public static MobSpawnTable DeepCaveCritters = new MobSpawnTable().add(100, "frog");
// This should be left empty! An empty constructor is necessary for biomes.
public ExampleBiome() {}
// Determines if rain is allowed in this biome. We want rain in this biome, so we'll set it as true.
public boolean canRain(Level level)
{
return true;
}
// Determines possible fishing loot based on layer.
public FishingLootTable getFishingLootTable(FishingSpot Spot)
{
if (Spot.tile.level.getIdentifier() == LevelIdentifier.DEEP_CAVE_IDENTIFIER) // Deep cave
{
return DeepCaveFish;
}
else if (Spot.tile.level.isCave) // Cave
{
return CaveFish;
}
else // Surface
{
return SurfaceFish;
}
}
// Determines critter spawns based on layer.
public MobSpawnTable getCritterSpawnTable(Level Level)
{
if (Level.getIdentifier() == LevelIdentifier.DEEP_CAVE_IDENTIFIER) // Deep cave
{
return DeepCaveCritters;
}
else if (Level.isCave) // Cave
{
return CaveCritters;
}
else // Surface
{
return SurfaceCritters;
}
}
public MobSpawnTable getMobSpawnTable(Level Level)
{
if (Level.getIdentifier() == LevelIdentifier.DEEP_CAVE_IDENTIFIER)
{
return DeepCaveMobs;
}
else if (Level.isCave)
{
return CaveMobs;
}
else
{
return SurfaceMobs;
}
}
// Determines the music playing at a given layer.
public AbstractMusicList getLevelMusic(Level Level, PlayerMob perspective)
{
if (Level.getIdentifier() == LevelIdentifier.DEEP_CAVE_IDENTIFIER)
{
return (AbstractMusicList) new MusicList(new GameMusic[] {MusicRegistry.ForgottenDepths});
}
else if (Level.isCave)
{
return (AbstractMusicList) new MusicList(new GameMusic[] {MusicRegistry.Away});
}
else
{
return (AbstractMusicList) new MusicList(new GameMusic[] {MusicRegistry.ByTheField});
}
}
// Determines the tile texture that goes underneath water tiles.
public GameTile getUnderLiquidTile(Level Level, int tileX, int tileY)
{
if (Level.isCave)
{
return TileRegistry.getTile(TileRegistry.dirtID);
}
return TileRegistry.getTile(TileRegistry.sandID);
}
// Determines what tile is used for water in biome generation.
public int getGenerationWaterTileID()
{
return TileRegistry.waterID;
}
// Determines what tile is used for cave lava in biome generation.
public int getGenerationCaveLavaTileID()
{
return TileRegistry.lavaID;
}
// Determines what tile is used for deep cave lava in biome generation.
public int getGenerationDeepCaveLavaTileID()
{
return TileRegistry.lavaID;
}
// Determines what tile is used for surface terrain in biome generation.
public int getGenerationTerrainTileID()
{
return TileRegistry.grassID;
}
// Determines what object is used for cave rocks in biome generation. Not to be confused with smaller rocks scattered around on tiles!
public int getGenerationCaveRockObjectID()
{
return ObjectRegistry.rockID;
}
// Same as that last method, but for deep cave rocks!
public int getGenerationDeepCaveRockObjectID()
{
return ObjectRegistry.deepRockID;
}
// Determines what tile is used for cave terrain in biome generation.
public int getGenerationCaveTileID()
{
return TileRegistry.rockID;
}
// Determines what tile is used for the boundary between water and terrain tiles.
public int getGenerationBeachTileID()
{
return TileRegistry.sandID;
}
public SoundSettings getWindSound(Level level)
{
return SoundSettingsRegistry.wind;
}
// Determines the types of materials used by villages that spawn in this biome. If you don't want villages in this biome, return null.
public VillageSet[] getVillageSets()
{
return new VillageSet[] { VillageSet.pine, VillageSet.oak, VillageSet.spruce };
}
/* You should define your biome generation 'branches' here! Think of them as 'rulesets' that determine how specific objects/tiles
* should be generated in your biome. The first parameter is the branch ID, which MUST be unique to prevent crashes. The remaining
* parameters will define how your branch generates tiles and objects accordingly.
*/
public void initializeGeneratorStack(BiomeGeneratorStack stack)
{
super.initializeGeneratorStack(stack);
// We'll use this branch for objects that need to be placed consistently but with some distance between them (i.e trees).
stack.addRandomSimplexVeinsBranch("biomeTrees", 2.0F, 0.2F, 0.4F, 0);
// We'll use this branch for objects that should be placed in groups, like blueberry bushes.
stack.addRandomVeinsBranch("biomeBushes", 0.045F, 4, 8, 0.6F, 0, false);
// We'll use these branches for spawning some basic ores in the caves.
stack.addRandomVeinsBranch("biomeCopper", 0.72F, 3, 6, 0.4F, 2, false);
stack.addRandomVeinsBranch("biomeIron", 0.56F, 3, 6, 0.4F, 2, false);
stack.addRandomVeinsBranch("biomeGold", 0.16F, 3, 6, 0.4F, 2, false);
// Same as before, but with modified spawn rates to reflect those used by deep caves.
stack.addRandomVeinsBranch("biomeDeepCopper", 0.08F, 3, 6, 0.4F, 2, false);
stack.addRandomVeinsBranch("biomeDeepIron", 0.4F, 3, 6, 0.4F, 2, false);
stack.addRandomVeinsBranch("BiomeDeepGold", 0.24F, 3, 6, 0.4F, 2, false);
stack.addRandomVeinsBranch("biomeDeepTungsten", 0.32F, 3, 6, 0.4F, 2, false);
}
// This function is used for generating additional objects and tiles on the surface layer of your biome.
public void generateRegionSurfaceTerrain(Region region, BiomeGeneratorStack stack, GameRandom random)
{
super.generateRegionSurfaceTerrain(region, stack, random);
/* Let's start by placing a nice even mix of oak trees and pine trees. We'll use our 'biomeTrees' branch for this, as intended.
* Given that trees can't spawn in water, and shouldn't reasonably spawn on sand, we'll set our tree generation rules to only place
* them on grass tiles. From there, we determine the chance of our trees spawning (this roughly determines how many there should be),
* and then select the specific object to be placed (i.e oak trees and pine trees).
*/
stack.startPlaceOnVein(this, region, random, "biomeTrees").onlyOnTile(TileRegistry.grassID).chance(0.15).placeObject("oaktree");
stack.startPlaceOnVein(this, region, random, "biomeTrees").onlyOnTile(TileRegistry.grassID).chance(0.15).placeObject("pinetree");
// Now we can determine the placement of our blueberry bushes, using our other defined generation branch ('biomeBushes').
stack.startPlaceOnVein(this, region, random, "biomeBushes").onlyOnTile(TileRegistry.grassID).placeObjectFruitGrower("blueberrybush");
// We can add small rocks to provide a little variety to our surface layer, without needing a generation branch.
stack.startPlace(this, region, random).chance(0.003).placeObject("surfacerock");
stack.startPlace(this, region, random).chance(0.005).placeObject("surfacerocksmall");
// Make sure you run this to ensure that liquids are handled properly.
region.updateLiquidManager();
}
@Override
public void generateRegionCaveTerrain(Region region, BiomeGeneratorStack stack, GameRandom random) {
super.generateRegionCaveTerrain(region, stack, random);
// We use our biome ore generation branches to determine how our ores should be distributed across our rock objects.
stack.startPlaceOnVein(this, region, random, "biomeCopper").onlyOnObject(ObjectRegistry.rockID).placeObjectForced("copperorerock");
stack.startPlaceOnVein(this, region, random, "biomeIron").onlyOnObject(ObjectRegistry.rockID).placeObjectForced("ironorerock");
stack.startPlaceOnVein(this, region, random, "biomeGold").onlyOnObject(ObjectRegistry.rockID).placeObjectForced("goldorerock");
stack.startPlace(this, region, random).chance(0.002).placeObject("surfacerock");
stack.startPlace(this, region, random).chance(0.004).placeObject("surfacerocksmall");
}
@Override
public void generateRegionDeepCaveTerrain(Region region, BiomeGeneratorStack stack, GameRandom random) {
super.generateRegionDeepCaveTerrain(region, stack, random);
// Same as before, but with modified spawning rules and the addition of tungsten.
stack.startPlaceOnVein(this, region, random, "biomeDeepCopper").onlyOnObject(ObjectRegistry.deepRockID).placeObjectForced("copperoredeeprock");
stack.startPlaceOnVein(this, region, random, "biomeDeepIron").onlyOnObject(ObjectRegistry.deepRockID).placeObjectForced("ironoredeeprock");
stack.startPlaceOnVein(this, region, random, "biomeDeepGold").onlyOnObject(ObjectRegistry.deepRockID).placeObjectForced("goldoredeeprock");
stack.startPlaceOnVein(this, region, random, "biomeDeepTungsten").onlyOnObject(ObjectRegistry.deepRockID).placeObjectForced("tungstenoredeeprock");
stack.startPlace(this, region, random).chance(0.002).placeObject("surfacerock");
stack.startPlace(this, region, random).chance(0.004).placeObject("surfacerocksmall");
}
// Determines the density of rocks within the cave layer (i.e the lower the chance, the more spacious the cave layer is).
public float getGenerationCaveRockObjectChance()
{
return 0.37F; // Default vanilla value.
}
// Determines the density of rocks within the deep cave layer (i.e the lower the chance, the more spacious the deep cave layer is).
public float getGenerationDeepCaveRockObjectChance()
{
return 0.4F; // Default vanilla value.
}
// Used for constructing cave treasure rooms.
public RandomCaveChestRoom getNewCaveChestRoomPreset(GameRandom random, AtomicInteger lootRotation)
{
// The chest room set determines what materials are to be used for the construction of the treasure room (i.e stone or wood, here).
RandomCaveChestRoom chestRoom = new RandomCaveChestRoom(random, LootTablePresets.basicCaveChest, lootRotation, new ChestRoomSet[] { ChestRoomSet.stone, ChestRoomSet.wood });
chestRoom.replaceTile(TileRegistry.stoneFloorID, ((Integer)random.getOneOf((Object[])new Integer[] { Integer.valueOf(TileRegistry.stoneFloorID), Integer.valueOf(TileRegistry.stoneBrickFloorID) })).intValue());
return chestRoom;
}
// Used for constructing deep cave treasure rooms.
public RandomCaveChestRoom getNewDeepCaveChestRoomPreset(GameRandom random, AtomicInteger lootRotation)
{
RandomCaveChestRoom chestRoom = new RandomCaveChestRoom(random, LootTablePresets.deepCaveChest, lootRotation, new ChestRoomSet[] { ChestRoomSet.deepStone, ChestRoomSet.obsidian });
chestRoom.replaceTile(TileRegistry.deepStoneFloorID, ((Integer)random.getOneOf((Object[])new Integer[] { Integer.valueOf(TileRegistry.deepStoneFloorID), Integer.valueOf(TileRegistry.deepStoneBrickFloorID) })).intValue());
return chestRoom;
}
// Used for constructing cave ruins.
public CaveRuins getNewCaveRuinsPreset(GameRandom random, AtomicInteger lootRotation)
{
WallSet wallSet = (WallSet)random.getOneOf((Object[])new WallSet[] { WallSet.stone, WallSet.wood });
FurnitureSet furnitureSet = (FurnitureSet)random.getOneOf((Object[])new FurnitureSet[] { FurnitureSet.oak, FurnitureSet.spruce });
String floorStringID = (String)random.getOneOf((Object[])new String[] { "woodfloor", "stonefloor", "stonebrickfloor" });
/* Returns a random cave ruin preset, using the given wall set and furniture set to determine what wall objects should be
* used for the walls, and what material the furniture should use (i.e oak or spruce). The floor string ID determines what type
* of floor tile should be used (i.e wood floor, stone floor, or stone brick floor). We select the loot table to determine what
* items should spawn in the chest, and pass the lootRotation as the final parameter.
*/
return ((CaveRuins.CaveRuinGetter)random.getOneOf(CaveRuins.caveRuinGetters))
.get(random, wallSet, furnitureSet, floorStringID, LootTablePresets.basicCaveRuinsChest, lootRotation);
}
// Used for constructing deep cave ruins.
public CaveRuins getNewDeepCaveRuinsPreset(GameRandom random, AtomicInteger lootRotation)
{
WallSet wallSet = (WallSet)random.getOneOf((Object[])new WallSet[] { WallSet.deepStone, WallSet.obsidian });
FurnitureSet furnitureSet = (FurnitureSet)random.getOneOf((Object[])new FurnitureSet[] { FurnitureSet.oak, FurnitureSet.spruce });
String floorStringID = (String)random.getOneOf((Object[])new String[] { "deepstonefloor", "deepstonebrickfloor" });
return ((CaveRuins.CaveRuinGetter)random.getOneOf(CaveRuins.caveRuinGetters))
.get(random, wallSet, furnitureSet, floorStringID, LootTablePresets.basicDeepCaveRuinsChest, lootRotation);
}
}
With the biome file complete, we're almost finished implementing the new biome! Now, we just need to register the new biome, and assign it a generation weight, so that the world generation system knows that it should spawn. We'll register it in our mod entry file for convenience's sake.
import ExampleMod.World.Biomes.ExampleBiome;
import necesse.engine.modLoader.annotations.ModEntry;
import necesse.engine.registries.BiomeRegistry;
@ModEntry
public class ExampleMain
{
public void init()
{
/* Generation weight determines the likelihood of a biome spawning in the world. 1.0f is the typical vanilla value for biomes.
* The generation weight MUST be set for the biome to spawn naturally in the world. You can, however, avoid setting it if you
* don't want it to spawn naturally, as is the case with the Temple or Dungeon biomes.
*/
BiomeRegistry.registerBiome("example_biome", new ExampleBiome().setGenerationWeight(1.0f), true);
}
}
With your biome registered and assigned a generation weight, it'll spawn naturally in the world.
Congratulations, you've just created a new biome!
Creating a world preset - Author: Eryr[edit]
Creating presets and configuring them to spawn in the world is another fairly simple task once you understand how it works!
The first step is to create your preset structure in game. Start by opening any world in creative mode, and building your preset. When your preset is ready, open the Tools menu in Creative Mode to access the Preset tool, and use the selection tool to select the boundaries of your preset. When your selection is complete, your device clipboard will automatically be overwritten with an alphanumeric string that represents the data of your preset!



Now, before you proceed, make sure you save that string from the Preset tool somewhere safe, in case you accidentally copy anything else!
Our next steps require creating two files:
1. Our preset file, which defines our preset using the string from the Preset tool, and allows us to spawn entities, modify tiles/objects, and so on.
2. Our world preset file, which defines the rules of where our preset should spawn.
Let's start by creating our preset file:
import necesse.engine.util.GameRandom;
import necesse.entity.mobs.friendly.human.humanShop.PawnBrokerHumanMob;
import necesse.inventory.lootTable.LootTablePresets;
import necesse.level.maps.presets.Preset;
public class ExamplePreset extends Preset
{
public ExamplePreset()
{
// Use the string generated by the preset tool here!
super("eNrlVc1u3CAQfhUegIPBWa998KnJoadUaaUeqj1gm41pqVlhVlu16ruXGZB_N82mUatGkQYbD998DPOD393dvL_5UP44qca1JUtpK9V968qCOqXl2-u-_JRuKek7cwIFJWxDyUF1cq-NsTtERcy_HODFciz1_5PNOY7n7vNYfJ4X4x011WdZOyyBhBKhLCVFFpJ_Ul1jTpRs2FVQfDXNUQvrRIU1wjdJqJmDLw8GuFg1dYs8HsBGQAKAyFzJrm4p4UVKSW2MVt19Zb5RkmcFJa30W9TCHqQDk-3EhHsF50HRmKN3o5IN6NKFbm-MA4_41WIBGTZn0LgQ3Wus7HtpQZPHE4mukVpUVlCyzQFmnJPNQYsOLNPU--0Xvyuw2hbJDODhLIFY-aN-8UftnXDKdD7QLAZaaI0YtsR47mI4L3RiSFdI1oUCu-DIFm-2_h4tMBgYbAxi_MzDYjhOcBiTOLHN8IUpwUiPlMkCM6ox7-NjhV-yn8HzB_nRa6xNLGSczUiHY10YLz7OQ4lHGqhurFcsh_FzmozQNMmOWhMy_KRUBuGTJ1BOZACsJQD4HDaAB82ajc05FzueZfsNbPAB2mbCn8x9Y49JMkfylT5K-HndhrZ5o_3dUjp7lBR67lrWxq6X8H6brj09RZcIXhNxmmd_YYOXJLtJ1O_-vDUub5_p5NXGvIaiv3WttB-V_-eVe6F7-fMX5y4i-g==");
/* We can add mobs to the preset using the addMob() method. Let's add a pawnbroker human mob here, at the position X = 4, Y = 4. This
* corresponds to just below the bed. You should be mindful of where you place mobs, to ensure that they don't end up
* trapped in walls or furniture. In this context, our co-ordinates are based on the boundaries of the preset itself, so
* the top left tile is X = 0, Y = 0, and our bottom right tile is X = 12, Y = 8.
*/
this.addMob("pawnbrokerhuman", 4, 4, PawnBrokerHumanMob.class, (pwn) ->
{
// Set the pawnbroker's home tile to within the preset itself, to ensure they don't wander off!
pwn.setHome(pwn.getTileX(), pwn.getTileY());
});
/* Let's also add some loot! We'll use an existing loot table for convenience, and add the items to the cooling box
* at X = 10, Y = 2.
*/
this.addInventory(LootTablePresets.hunterCookedFoodLootTable, GameRandom.globalRandom, 10, 2, new Object[0]);
}
}
With our preset defined, it's now time to set up our world preset file:
import java.awt.Dimension;
import java.awt.Point;
import ExampleMod.World.Presets.ExamplePreset;
import necesse.engine.gameLoop.tickManager.PerformanceTimerManager;
import necesse.engine.registries.BiomeRegistry;
import necesse.engine.util.GameRandom;
import necesse.engine.util.LevelIdentifier;
import necesse.engine.world.biomeGenerator.BiomeGeneratorStack;
import necesse.engine.world.worldPresets.LevelPresetsRegion;
import necesse.engine.world.worldPresets.WorldPreset;
import necesse.level.maps.Level;
import necesse.level.maps.presets.Preset;
import necesse.level.maps.presets.PresetUtils;
public class ExampleCabinWorldPreset extends WorldPreset
{
// Define the size of the preset here.
protected Dimension size = new Dimension(13, 9);
/* The core set of rules to determine if your preset should be added to a given region. In this instance, our preset should only spawn
* on the surface layer of the Snow biome. If both of those criteria are true, we return true to indicate that our preset can spawn
* there. If either criteria is identified as false (e.g not on the surface, or in the wrong biome), we return false, to indicate that
* our preset shouldn't spawn there.
*/
public boolean shouldAddToRegion(LevelPresetsRegion presetsRegion)
{
return (presetsRegion.identifier.equals(LevelIdentifier.SURFACE_IDENTIFIER) && presetsRegion.hasAnyOfBiome(BiomeRegistry.SNOW.getID()));
}
/* This is the most difficult part of creating a world preset! Here, we define the spawn rate for our preset, additional spawn rules
* to ensure that it does not spawn on water, and then perform the actual initialisation and placing of the preset itself.
*/
public void addToRegion(GameRandom random, LevelPresetsRegion presetsRegion, final BiomeGeneratorStack generatorStack, PerformanceTimerManager performanceTimer)
{
// Our points per region value is set to 0.016F, which means it should spawn at a fairly regular rate.
int total = getTotalBiomePoints(random, presetsRegion, BiomeRegistry.DESERT, 0.016F);
for (int i = 0; i < total; i++)
{
final Point tile = findRandomBiomePresetTile
(
random,
presetsRegion,
generatorStack,
BiomeRegistry.SNOW, // Must be set to our intended biome.
50, // This defines how many attempts should be made to spawn this preset.
this.size,
new String[] { "loot", "villages" }, // Checks to make sure we're not overlapping with villages or other loot presets.
new WorldPreset.ValidTilePredicate()
{
// Check to make sure our potential preset spawn tile is valid (i.e isn't occupied by water, and is in a Snow biome).
public boolean isValidPosition(int tileX, int tileY)
{
return (!generatorStack.isSurfaceExpensiveWater(tileX, tileY) && generatorStack
.getLazyBiomeID(tileX, tileY) == BiomeRegistry.SNOW.getID());
}
}
);
if (tile != null)
{
presetsRegion.addPreset
(
this,
tile.x,
tile.y,
this.size,
new String[] { "loot", "villages" },
new LevelPresetsRegion.WorldPresetPlaceFunction()
{
public void place(GameRandom random, Level level, PerformanceTimerManager timer)
{
WorldPreset.ensureRegionsAreGenerated(level, tile.x, tile.y, 11, 9);
ExamplePreset preset = new ExamplePreset(); // Initialise a new instance of your preset here!
PresetUtils.clearMobsInPreset((Preset)preset, level, tile.x, tile.y);
preset.applyToLevel(level, tile.x, tile.y); // Applies your preset to the level at the given tile.
}
}
)
.setRemoveIfWithinSpawnRegionRange(1);
}
}
}
}
And finally, all that's left is to register our world preset registry to allow it to spawn.
import ExampleMod.World.WorldPresets.ExampleCabinWorldPreset;
import necesse.engine.modLoader.annotations.ModEntry;
import necesse.engine.registries.WorldPresetRegistry;
import necesse.engine.world.worldPresets.WorldPreset;
@ModEntry
public class ExampleMain
{
public void init()
{
WorldPresetRegistry.registerPreset("example_preset", (WorldPreset) new ExampleCabinWorldPreset());
}
}
Now, your world preset will spawn on the Snow Biome surface.