Files
OpenPrestige/src/main/java/dev/zprestige/prestige/config/ConfigManager.java
T
2026-10-23 00:28:57 +01:00

684 lines
30 KiB
Java

package dev.zprestige.prestige.config;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import dev.zprestige.prestige.ColorSetting;
import dev.zprestige.prestige.KeybindSetting;
import dev.zprestige.prestige.Module;
import dev.zprestige.prestige.MultiSetting;
import dev.zprestige.prestige.RangeSetting;
import dev.zprestige.prestige.Setting;
import dev.zprestige.prestige.annotation.Chainable;
import dev.zprestige.prestige.client.Prestige;
import dev.zprestige.prestige.data.PendingModuleActivation;
import dev.zprestige.prestige.hud.HudElement;
import dev.zprestige.prestige.math.SettingValueParser;
import dev.zprestige.prestige.render.ConfigReport;
import dev.zprestige.prestige.type.NotificationType;
import dev.zprestige.prestige.type.HudAlignment;
import dev.zprestige.prestige.type.HudAnchor;
import dev.zprestige.prestige.util.NotificationManager;
import java.awt.Color;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Map.Entry;
import net.minecraft.client.MinecraftClient;
public class ConfigManager {
private static final Gson GSON = new Gson();
private String lastCompleteSnapshot;
private String lastLoadReport;
private static final long NOTIFICATION_DURATION_MS = 8000L;
@Chainable
public String saveToJson() {
ConfigReport report = new ConfigReport("save");
JsonObject root = new JsonObject();
try {
if (Prestige.moduleManager == null) {
report.error("Config", "module manager is not ready", null);
} else {
HashSet sectionNames = new HashSet();
sectionNames.add("Menu");
JsonObject menuSection = new JsonObject();
for (Setting menuSetting : (Iterable<Setting>)(Prestige.moduleManager.getMenu().settings)) {
this.saveSetting(menuSection, menuSetting, "Menu", report);
}
root.add("Menu", menuSection);
for (Module module : (Iterable<Module>)(Prestige.moduleManager.getModules())) {
String moduleName = module.getName();
if (moduleName == null || moduleName.isBlank()) {
report.error("Unnamed module", "module has no config name", null);
} else if (!sectionNames.add(moduleName)) {
report.error(moduleName, "duplicate config section name", null);
} else {
JsonObject moduleSection = new JsonObject();
for (Setting coreSetting : (Iterable<Setting>)(module.getCoreSettings())) {
this.saveSetting(moduleSection, coreSetting, moduleName, report);
}
for (Setting setting : (Iterable<Setting>)(module.settings)) {
this.saveSetting(moduleSection, setting, moduleName, report);
}
root.add(moduleName, moduleSection);
}
}
if (Prestige.hudEditorScreen == null) {
report.error("HUD", "HUD editor is not ready", null);
} else {
for (HudElement element : Prestige.hudEditorScreen.getElements()) {
String elementName = element.getName();
if (elementName == null || elementName.isBlank()) {
report.error("HUD", "component has no config name", null);
} else if (!sectionNames.add(elementName)) {
report.error("HUD > " + elementName, "duplicate config section name", null);
} else {
try {
JsonObject elementSection = new JsonObject();
elementSection.addProperty("Enabled", element.isEnabled());
elementSection.addProperty("X", element.getX());
elementSection.addProperty("Y", element.getY());
if (element.isPositionPending()) {
report.addValues(3);
} else {
elementSection.addProperty("RuleX", element.anchorX.name());
elementSection.addProperty("RuleY", element.anchorY.name());
elementSection.addProperty("OffX", element.offsetX);
elementSection.addProperty("OffY", element.offsetY);
report.addValues(7);
}
root.add(elementName, elementSection);
} catch (Throwable elementError) {
rethrowIfFatal(elementError);
report.error("HUD > " + elementName, describeError(elementError), elementError);
}
}
}
}
}
} catch (Throwable saveError) {
rethrowIfFatal(saveError);
report.error("Config", "unexpected save failure: " + describeError(saveError), saveError);
}
String json;
try {
json = root.toString();
} catch (Throwable serializeError) {
rethrowIfFatal(serializeError);
report.error("Config", "could not serialize snapshot: " + describeError(serializeError), serializeError);
json = "{}";
}
if (report.hasErrors()) {
if (this.lastCompleteSnapshot != null) {
report.warn("Config", "save was aborted; the previous complete snapshot was kept", null);
json = this.lastCompleteSnapshot;
} else {
report.error("Config", "save was aborted; no complete fallback snapshot exists", null);
json = "{}";
}
} else {
this.lastCompleteSnapshot = json;
}
notifyResult(report);
return json;
}
@Chainable
public void loadFromJson(String json) {
ConfigReport report = new ConfigReport("load");
try {
JsonObject root = this.parseConfigJson(json, report);
if (root == null) {
return;
}
if (Prestige.moduleManager != null) {
HashSet sectionNames = new HashSet();
sectionNames.add("Menu");
ArrayList pendingStates = new ArrayList();
Set predisabled = Collections.newSetFromMap(new IdentityHashMap());
this.disableModulesBeforeLoad(root, predisabled, report);
this.loadMenuSection(root, report);
for (Module module : (Iterable<Module>)(Prestige.moduleManager.getModules())) {
String moduleName = module.getName();
if (moduleName != null && !moduleName.isBlank()) {
if (!sectionNames.add(moduleName)) {
report.error(moduleName, "duplicate module config name; section was skipped", null);
} else {
this.loadModuleSection(root, module, moduleName, predisabled, pendingStates, report);
}
}
}
if (Prestige.hudEditorScreen == null) {
report.warn("HUD", "HUD editor is not ready; HUD layout was skipped", null);
} else {
for (HudElement element : Prestige.hudEditorScreen.getElements()) {
String elementName = element.getName();
if (elementName == null || elementName.isBlank()) {
report.error("HUD", "component has no config name", null);
} else if (!sectionNames.add(elementName)) {
report.error("HUD > " + elementName, "duplicate config section name; section was skipped", null);
} else {
this.loadHudSection(root, element, elementName, report);
}
}
}
this.reportUnknownKeys(root, sectionNames, "", "module or HUD section", report);
for (PendingModuleActivation pending : (Iterable<PendingModuleActivation>)(pendingStates)) {
this.applyModuleEnabledState(pending.enabledValue(), pending.module(), pending.sectionName(), pending.alreadyApplied(), report);
}
if (report.sectionCount == 0) {
report.error("Config", "no recognized config sections were found", null);
}
return;
}
report.error("Config", "module manager is not ready", null);
} catch (Throwable error) {
rethrowIfFatal(error);
report.error("Config", "unexpected load failure: " + describeError(error), error);
return;
} finally {
this.lastLoadReport = report.getSummary();
notifyResult(report);
}
}
@Chainable
public String consumeLoadReport() {
String report = this.lastLoadReport;
this.lastLoadReport = null;
return report;
}
private void disableModulesBeforeLoad(JsonObject root, Set predisabled, ConfigReport report) {
HashSet seenNames = new HashSet();
seenNames.add("Menu");
Iterator moduleIterator = Prestige.moduleManager.getModules().iterator();
while (true) {
Module module;
String name;
boolean currentlyEnabled;
while (true) {
boolean configEnabled;
while (true) {
if (!moduleIterator.hasNext()) {
return;
}
module = (Module)moduleIterator.next();
name = module.getName();
if (name != null && !name.isBlank() && seenNames.add(name)) {
JsonElement section = root.get(name);
if (section != null && section.isJsonObject()) {
JsonObject sectionObject = section.getAsJsonObject();
if (sectionObject.has("Enabled")) {
try {
configEnabled = SettingValueParser.parseBoolean(this.readScalarString(sectionObject.get("Enabled")));
break;
} catch (Throwable parseError) {
rethrowIfFatal(parseError);
}
}
}
}
}
if (!configEnabled) {
try {
currentlyEnabled = module.isEnabled();
break;
} catch (Throwable stateError) {
rethrowIfFatal(stateError);
report.error(name + " > Enabled", "could not inspect current module state: " + describeError(stateError), stateError);
}
}
}
if (currentlyEnabled) {
predisabled.add(module);
String settingPath = name + " > Enabled";
try {
module.toggle();
if (module.isEnabled()) {
module.toggle();
}
if (!module.isEnabled()) {
report.addValues(1);
} else {
report.warn(settingPath, "module changed its own state while being disabled", null);
}
} catch (Throwable toggleError) {
rethrowIfFatal(toggleError);
report.error(settingPath, this.restoreModuleState(module, true, toggleError) ? "disable lifecycle failed; previous state was restored" : "disable lifecycle failed; state may be inconsistent", toggleError);
}
}
}
}
private void loadMenuSection(JsonObject root, ConfigReport report) {
if (root.has("Menu")) {
report.sectionCount++;
JsonObject section = this.requireSectionObject(root.get("Menu"), "Menu", report);
if (section != null) {
HashSet seenSettings = new HashSet();
for (Setting setting : (Iterable<Setting>)(Prestige.moduleManager.getMenu().settings)) {
this.loadSetting(section, setting, "Menu", seenSettings, report);
}
this.reportUnknownKeys(section, seenSettings, "Menu", "setting", report);
}
}
}
private void loadModuleSection(JsonObject root, Module module, String name, Set predisabled, List pendingStates, ConfigReport report) {
if (root.has(name)) {
report.sectionCount++;
JsonObject section = this.requireSectionObject(root.get(name), name, report);
if (section != null) {
HashSet seenSettings = new HashSet();
boolean hasEnabledSetting = false;
for (Setting coreSetting : (Iterable<Setting>)(module.getCoreSettings())) {
if ("Enabled".equals(coreSetting.getName())) {
hasEnabledSetting = true;
seenSettings.add("Enabled");
} else {
this.loadSetting(section, coreSetting, name, seenSettings, report);
}
}
for (Setting setting : (Iterable<Setting>)(module.settings)) {
this.loadSetting(section, setting, name, seenSettings, report);
}
this.reportUnknownKeys(section, seenSettings, name, "setting", report);
if (hasEnabledSetting && section.has("Enabled")) {
pendingStates.add(new PendingModuleActivation(module, name, section.get("Enabled"), predisabled.contains(module)));
}
}
}
}
private void applyModuleEnabledState(JsonElement enabledValue, Module module, String name, boolean alreadyDisabled, ConfigReport report) {
String settingPath = name + " > Enabled";
if (!alreadyDisabled) {
try {
boolean wantEnabled = SettingValueParser.parseBoolean(this.readScalarString(enabledValue));
boolean wasEnabled = module.isEnabled();
if (wantEnabled == wasEnabled) {
report.addValues(1);
return;
}
if (wantEnabled && !module.isPersistent()) {
report.warn(settingPath, "activation is blocked for downloaded configs; settings were still loaded", null);
return;
}
try {
module.toggle();
if (module.isEnabled() == wantEnabled) {
report.addValues(1);
} else {
report.warn(settingPath, "module changed its own state during activation", null);
}
} catch (Throwable toggleError) {
rethrowIfFatal(toggleError);
report.error(settingPath, this.restoreModuleState(module, wasEnabled, toggleError) ? "module lifecycle failed; previous state was restored" : "module lifecycle failed; state may be inconsistent", toggleError);
}
} catch (Throwable error) {
rethrowIfFatal(error);
report.error(settingPath, describeError(error), error);
}
}
}
private boolean restoreModuleState(Module module, boolean expectedEnabled, Throwable failure) {
try {
if (module.isEnabled() != expectedEnabled) {
module.toggle();
}
return module.isEnabled() == expectedEnabled;
} catch (Throwable restoreError) {
rethrowIfFatal(restoreError);
failure.addSuppressed(restoreError);
return false;
}
}
private void loadHudSection(JsonObject root, HudElement element, String name, ConfigReport report) {
if (root.has(name)) {
report.sectionCount++;
String sectionPath = "HUD > " + name;
JsonObject section = this.requireSectionObject(root.get(name), sectionPath, report);
if (section != null) {
Set knownKeys = Set.of("Enabled", "X", "Y", "Anchor", "RuleX", "RuleY", "OffX", "OffY");
this.reportUnknownKeys(section, knownKeys, sectionPath, "setting", report);
boolean hasRules = section.has("RuleX") && section.has("RuleY") && section.has("OffX") && section.has("OffY");
boolean hasX = section.has("X");
boolean hasY = section.has("Y");
if (hasRules) {
try {
element.setAnchors(HudAlignment.valueOf(this.readScalarString(section.get("RuleX"))), HudAlignment.valueOf(this.readScalarString(section.get("RuleY"))), SettingValueParser.parseFloat(this.readScalarString(section.get("OffX"))), SettingValueParser.parseFloat(this.readScalarString(section.get("OffY"))));
report.addValues(4);
} catch (IllegalArgumentException unknownRule) {
report.error(sectionPath + " > Position", "unknown positioning rule; position was skipped", null);
} catch (Throwable ruleError) {
rethrowIfFatal(ruleError);
report.error(sectionPath + " > Position", describeError(ruleError), ruleError);
}
} else if (hasX != hasY) {
report.error(sectionPath + " > Position", "X and Y must both be present; position was skipped", null);
} else if (hasX) {
try {
label128: {
float x = SettingValueParser.parseFloat(this.readScalarString(section.get("X")));
float y = SettingValueParser.parseFloat(this.readScalarString(section.get("Y")));
label129: {
{
if (section.has("Anchor")) {
try {
element.setCornerAnchor(HudAnchor.valueOf(this.readScalarString(section.get("Anchor"))), x, y);
report.addValues(3);
break label128;
} catch (IllegalArgumentException unknownAnchor) {
break label129;
}
}
}
element.setPosition(x, y);
report.addValues(2);
break label128;
}
report.error(sectionPath + " > Anchor", "unknown anchor; position was skipped", null);
}
} catch (Throwable positionError) {
rethrowIfFatal(positionError);
report.error(sectionPath + " > Position", describeError(positionError), positionError);
}
}
if (section.has("Enabled")) {
try {
if (SettingValueParser.parseBoolean(this.readScalarString(section.get("Enabled"))) != element.isEnabled()) {
boolean wasEnabled = element.isEnabled();
try {
element.toggle();
} catch (Throwable toggleError) {
rethrowIfFatal(toggleError);
report.error(sectionPath + " > Enabled", this.restoreHudState(element, wasEnabled, toggleError) ? "HUD lifecycle failed; previous state was restored" : "HUD lifecycle failed; state may be inconsistent", toggleError);
return;
}
}
report.addValues(1);
} catch (Throwable enabledError) {
rethrowIfFatal(enabledError);
report.error(sectionPath + " > Enabled", describeError(enabledError), enabledError);
}
}
}
}
}
private boolean restoreHudState(HudElement element, boolean expectedEnabled, Throwable failure) {
try {
if (element.isEnabled() != expectedEnabled) {
element.toggle();
}
return element.isEnabled() == expectedEnabled;
} catch (Throwable restoreError) {
rethrowIfFatal(restoreError);
failure.addSuppressed(restoreError);
return false;
}
}
private void loadSetting(JsonObject section, Setting setting, String sectionName, Set seenSettings, ConfigReport report) {
if (setting != null && setting.getName() != null && !setting.getName().isBlank()) {
String settingName = setting.getName();
if (!seenSettings.add(settingName)) {
report.error(sectionName + " > " + settingName, "duplicate registered setting name", null);
} else if (section.has(settingName)) {
String settingPath = sectionName + " > " + settingName;
try {
SettingValueParser.applyValue(setting, this.readScalarString(section.get(settingName)));
report.addValues(1);
} catch (Throwable error) {
rethrowIfFatal(error);
report.error(settingPath, describeError(error), error);
}
}
} else {
report.warn(sectionName, "an unnamed setting was skipped", null);
}
}
private void saveSetting(JsonObject section, Setting setting, String sectionName, ConfigReport report) {
if (setting != null && setting.getName() != null && !setting.getName().isBlank()) {
String settingPath = sectionName + " > " + setting.getName();
if (section.has(setting.getName())) {
report.error(settingPath, "duplicate registered setting name", null);
} else {
try {
section.addProperty(setting.getName(), this.serializeSetting(setting));
report.addValues(1);
} catch (Throwable error) {
rethrowIfFatal(error);
report.error(settingPath, describeError(error), error);
}
}
} else {
report.warn(sectionName, "an unnamed setting was skipped", null);
}
}
private JsonObject parseConfigJson(String json, ConfigReport report) {
if (json != null && !json.isBlank()) {
if (json.length() > 100000) {
report.error("Config", "downloaded config is too large", null);
return null;
} else if (this.isNestedTooDeeply(json)) {
report.error("Config", "downloaded config is nested too deeply", null);
return null;
} else {
JsonObject root;
try {
root = GSON.fromJson(json, JsonObject.class);
} catch (Exception parseError) {
report.error("Config", "downloaded config is not valid JSON", parseError);
return null;
}
if (root == null) {
report.error("Config", "downloaded config contained JSON null", null);
return null;
} else {
return root;
}
}
} else {
report.error("Config", "downloaded config was empty", null);
return null;
}
}
private boolean isNestedTooDeeply(String json) {
int depth = 0;
boolean inString = false;
boolean escaped = false;
for (int i = 0; i < json.length(); i++) {
char ch = json.charAt(i);
if (inString) {
if (escaped) {
escaped = false;
} else if (ch == '\\') {
escaped = true;
} else if (ch == '"') {
inString = false;
}
} else if (ch == '"') {
inString = true;
} else if (ch == '{' || ch == '[') {
if (++depth > 64) {
return true;
}
} else if (ch == '}' || ch == ']') {
depth--;
}
}
return false;
}
private JsonObject requireSectionObject(JsonElement element, String path, ConfigReport report) {
if (element == null || element.isJsonNull()) {
report.error(path, "section is null", null);
return null;
} else if (!element.isJsonObject()) {
report.error(path, "section must be a JSON object", null);
return null;
} else {
return element.getAsJsonObject();
}
}
private String readScalarString(JsonElement element) {
if (element == null || element.isJsonNull()) {
throw new IllegalArgumentException("value is null");
} else if (!element.isJsonPrimitive()) {
throw new IllegalArgumentException("expected a single value");
} else {
return element.getAsString();
}
}
private void reportUnknownKeys(JsonObject section, Set knownKeys, String pathPrefix, String kind, ConfigReport report) {
for (Entry entry : section.entrySet()) {
if (!knownKeys.contains(entry.getKey())) {
report.warn(pathPrefix.isEmpty() ? (String)entry.getKey() : pathPrefix + " > " + (String)entry.getKey(), kind + " is not available in this version; skipped", null);
}
}
}
private String serializeSetting(Setting setting) {
if (setting instanceof ColorSetting colorSetting) {
Color color = colorSetting.getValue();
if (color == null) {
throw new IllegalStateException("color is null");
} else {
return color.getRed() + "." + color.getGreen() + "." + color.getBlue() + "." + color.getAlpha();
}
} else if (setting instanceof KeybindSetting keybindSetting) {
if (keybindSetting.getValue() == null) {
throw new IllegalStateException("bind is null");
} else {
return keybindSetting.getValue() + "." + keybindSetting.isHoldMode();
}
} else if (setting instanceof MultiSetting multiSetting) {
StringBuilder builder = new StringBuilder();
for (String option : multiSetting.getOptions()) {
if (!option.isEmpty()) {
builder.append(option).append(".").append(multiSetting.isSelected(option)).append("&");
}
}
return builder.toString();
} else if (setting instanceof RangeSetting rangeSetting) {
return rangeSetting.getLow() + "|" + rangeSetting.getHigh();
} else {
Object value = setting.getValue();
if (value == null) {
throw new IllegalStateException("value is null");
} else {
return value.toString();
}
}
}
private static void notifyResult(ConfigReport report) {
if (!"load".equals(report.operation) && (report.hasErrors() || report.warningCount != 0)) {
Runnable showResult = () -> {
if (Prestige.notificationManager != null) {
String verb = "load".equals(report.operation) ? "loaded" : "saved";
NotificationManager notifications = Prestige.notificationManager;
String title = "Config " + verb;
String summary = formatSummary(report.errorCount, report.warningCount, report.valueCount, verb);
long durationMs = NOTIFICATION_DURATION_MS;
notifications.addNotification(title, summary, durationMs, (NotificationType)(report.errorCount > 0 ? NotificationType.ERROR : (report.warningCount > 0 ? NotificationType.WARNING : NotificationType.SUCCESS)));
}
};
try {
MinecraftClient client = MinecraftClient.getInstance();
if (client != null && !client.isOnThread()) {
client.execute(showResult);
} else {
showResult.run();
}
} catch (Throwable error) {
rethrowIfFatal(error);
}
}
}
public static String formatSummary(int errors, int skipped, int applied, String verb) {
return errors + " errors, " + skipped + " skipped, " + applied + " " + verb;
}
private static String describeError(Throwable error) {
String message = error.getMessage();
return message != null && !message.isBlank() ? message : error.getClass().getSimpleName();
}
private static void rethrowIfFatal(Throwable error) {
if (error instanceof ThreadDeath threadDeath) {
throw threadDeath;
} else if (error instanceof VirtualMachineError vmError) {
throw vmError;
} else {
if (error instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
}
}
}