Add preset code support to !build and !autobuild

Two new tags have been added for `!autobuild`:
"-nonotes"/"-nn" = Prevents generating sub notes
"-nopotentials"/"-np" = Prevents generating potentials

Example:
`!ab lv20 103 -nn` = Generates a level 20 record, but no sub notes will be generated.
This commit is contained in:
Melledy
2026-04-18 04:15:09 -07:00
parent 58b89ee4a2
commit ea97c091df
5 changed files with 187 additions and 26 deletions
@@ -1,8 +1,11 @@
package emu.nebula.command;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.List;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.AffinityLevelDef;
import emu.nebula.game.character.GameCharacter;
import emu.nebula.game.character.GameDisc;
@@ -10,6 +13,7 @@ import emu.nebula.game.player.Player;
import emu.nebula.util.Utils;
import it.unimi.dsi.fastutil.ints.Int2IntLinkedOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import it.unimi.dsi.fastutil.objects.ObjectSet;
import lombok.Getter;
@@ -235,4 +239,120 @@ public class CommandArgs {
return hasChanged;
}
/**
* Converts a preset code back into a map of ids
* @param code
* @return
*/
public Int2IntMap parsePresetCode(String code) {
// Init map
var items = new Int2IntOpenHashMap();
// Decode preset data from base64
byte[] data = Utils.base64Decode(code);
var preset = ByteBuffer.wrap(data);
preset.order(ByteOrder.BIG_ENDIAN);
// Parse character ids
int[] charIds = new int[3];
try {
for (int i = 0; i < 3; i++) {
int id = preset.getInt();
charIds[i] = id;
items.put(id, 1);
}
} catch (Exception e) {
// Ignored
return null;
}
// Parse potentials
try {
for (int i = 0; i < 3; i++) {
int charId = charIds[i];
// Get potential data
var potentials = GameData.getCharPotentialDataTable().get(charId);
if (potentials == null) {
return null;
}
// Setup potentials
int[] specificPotentialIds;
int[] normalPotentialIds;
int[] commonPotentialIds = potentials.getCommonPotentialIds();
// Check if main trekker or not
if (i == 1) {
specificPotentialIds = potentials.getMasterSpecificPotentialIds();
normalPotentialIds = potentials.getMasterNormalPotentialIds();
} else {
specificPotentialIds = potentials.getAssistSpecificPotentialIds();
normalPotentialIds = potentials.getAssistNormalPotentialIds();
}
// Parser
int size = (int) Math.ceil((specificPotentialIds.length + (normalPotentialIds.length * 3) + (commonPotentialIds.length * 3)) / 8D);
byte[] potentialBytes = new byte[size];
preset.get(potentialBytes);
var potentialData = new BitReader(potentialBytes);
// Calculate levels of potentials
for (int id : specificPotentialIds) {
int level = potentialData.readBits(1);
if (level > 0) {
items.put(id, level);
}
}
for (int id : normalPotentialIds) {
int level = potentialData.readBits(3);
if (level > 0) {
items.put(id, level);
}
}
for (int id : commonPotentialIds) {
int level = potentialData.readBits(3);
if (level > 0) {
items.put(id, level);
}
}
}
} catch (Exception e) {
// Ignored
return null;
}
// Finished
return items;
}
@Getter
private static class BitReader {
private byte[] bits;
private int arrayIndex;
private int bitIndex;
public BitReader(byte[] bits) {
this.bits = bits;
}
public int readBits(int count) {
int result = 0;
for (int i = 0; i < count; i++) {
result <<= 1;
result |= (this.bits[arrayIndex] >> (7 - bitIndex)) & 1;
bitIndex++;
if (bitIndex >= 8) {
bitIndex = 0;
arrayIndex++;
}
}
return result;
}
}
}
@@ -14,7 +14,6 @@ import emu.nebula.game.character.GameDisc;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.net.NetMsgId;
import emu.nebula.util.Utils;
import it.unimi.dsi.fastutil.ints.IntArrayList;
@Command(
@@ -40,16 +39,19 @@ public class AutoBuildCommand implements CommandHandler {
int id = Utils.parseSafeInt(arg);
int count = 1;
if (id != 0) {
builder.parseItem(id, count);
} else if (!Utils.isNumeric(arg)) {
// Might be a preset code
var items = args.parsePresetCode(arg);
if (items != null) {
builder.parseItems(items);
}
}
}
if (args.getMap() != null) {
for (var entry : args.getMap().int2IntEntrySet()) {
int id = entry.getIntKey();
int count = entry.getIntValue();
builder.parseItem(id, count);
}
builder.parseItems(args.getMap());
}
// Remove extra characters/discs
@@ -104,8 +106,27 @@ public class AutoBuildCommand implements CommandHandler {
}
}
// Calcluate score
builder.toBuild();
// Pick random potentials and sub notes
this.generate(builder, targetScore);
boolean shouldGenerateNotes = !args.hasFlag("-nn") && !args.hasFlag("-nonotes");
boolean shouldGeneratePotentials = !args.hasFlag("-np") && !args.hasFlag("-nopotentials") ;
if (shouldGenerateNotes) {
int score = Math.max(targetScore - builder.getBuild().getScore(), 0);
if (shouldGeneratePotentials) {
score = (int) (score * .4D);
}
this.generateSubNotes(builder, score);
}
if (shouldGeneratePotentials) {
int score = Math.max(targetScore - builder.getBuild().getScore(), 0);
this.generatePotentials(builder, score);
}
// Create record
var build = builder.toBuild();
@@ -246,10 +267,7 @@ public class AutoBuildCommand implements CommandHandler {
builder.getDiscs().add(list.get(0));
}
private void generate(StarTowerBuildData builder, int targetScore) {
// Get possible sub notes
int subNoteScore = (int) (targetScore * .4D);
private void generateSubNotes(StarTowerBuildData builder, int subNoteScore) {
// Get possible drops
var drops = new IntArrayList();
@@ -343,11 +361,10 @@ public class AutoBuildCommand implements CommandHandler {
}
// Calcluate score
builder.toBuild().calculateScore();
// Get target potential score
int potentialScore = Math.max(targetScore - builder.getBuild().getScore(), 0);
builder.toBuild();
}
private void generatePotentials(StarTowerBuildData builder, int potentialScore) {
// Init weighted list of characters
var characters = new ArrayList<GameCharacter>();
@@ -13,6 +13,7 @@ import emu.nebula.game.player.Player;
import emu.nebula.game.tower.StarTowerBuild;
import emu.nebula.net.NetMsgId;
import emu.nebula.util.Utils;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import lombok.Getter;
@Command(
@@ -35,16 +36,19 @@ public class BuildCommand implements CommandHandler {
int id = Utils.parseSafeInt(arg);
int count = 1;
if (id != 0) {
builder.parseItem(id, count);
} else if (!Utils.isNumeric(arg)) {
// Might be a preset code
var items = args.parsePresetCode(arg);
if (items != null) {
builder.parseItems(items);
}
}
}
if (args.getMap() != null) {
for (var entry : args.getMap().int2IntEntrySet()) {
int id = entry.getIntKey();
int count = entry.getIntValue();
builder.parseItem(id, count);
}
builder.parseItems(args.getMap());
}
// Check if build is valid
@@ -133,6 +137,15 @@ public class BuildCommand implements CommandHandler {
}
}
public void parseItems(Int2IntMap items) {
for (var entry : items.int2IntEntrySet()) {
int id = entry.getIntKey();
int count = entry.getIntValue();
this.parseItem(id, count);
}
}
public void addCharacter(GameCharacter character) {
if (this.characters.contains(character)) {
return;
@@ -175,6 +188,9 @@ public class BuildCommand implements CommandHandler {
build.getCharPots().add(data.getCharId(), 1);
}
// Reset secondary skills
build.refreshSecondarySkills();
// Calculate score
build.calculateScore();
@@ -135,6 +135,10 @@ public class StarTowerBuild implements GameDatabaseObject {
Nebula.getGameDatabase().update(this, this.getUid(), "preference", this.isPreference());
}
public void refreshSecondarySkills() {
this.secondarySkills = SecondarySkillDef.calculateSecondarySkills(this.getDiscIds(), this.getSubNoteSkills());
}
// Score
public int calculateScore() {
@@ -161,7 +165,7 @@ public class StarTowerBuild implements GameDatabaseObject {
// Check secondary skills
if (this.getSecondarySkills() == null) {
this.secondarySkills = SecondarySkillDef.calculateSecondarySkills(this.getDiscIds(), this.getSubNoteSkills());
this.refreshSecondarySkills();
}
// Add score from secondary skills
+4
View File
@@ -169,6 +169,10 @@ public class Utils {
return (int) sum;
}
public static boolean isNumeric(String str) {
return str.matches("-?\\d+(\\.\\d+)?");
}
public static double generateRandomDouble() {
return ThreadLocalRandom.current().nextDouble();
}