Skip to content

Instantly share code, notes, and snippets.

@Quackster
Created September 5, 2025 04:19
Show Gist options
  • Save Quackster/7bd3bbab5af3eef040b90c1f82211c16 to your computer and use it in GitHub Desktop.
Save Quackster/7bd3bbab5af3eef040b90c1f82211c16 to your computer and use it in GitHub Desktop.
Avatara4j figuredata conversion
package net.h4bbo.avatara4j.figure.converter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.h4bbo.avatara4j.figure.readers.FiguredataReader;
import net.h4bbo.avatara4j.figure.types.FigureColor;
import org.json.JSONObject;
import org.json.XML;
import org.json.XMLParserConfiguration;
import java.io.*;
import java.net.URL;
import java.nio.file.Files;
import java.util.*;
/**
* Figuredata converter originally written by Alcosmos.
*/
public class FigureConverter {
private static volatile FigureConverter instance;
private static final Object lock = new Object();
public static FigureConverter getInstance() {
if (instance == null) {
synchronized (lock) {
if (instance == null) {
instance = new FigureConverter();
}
}
}
return instance;
}
private final String oldFigureDataPath = "figuredata/converter/oldfiguredata.json";
// private final String newFigureDataPath = "figuredata/converter/newfiguredata.json";
private volatile JsonNode oldFigureData;
// private volatile JsonNode newFigureData;
private final Object oldLock = new Object();
// private final Object newLock = new Object();
private final ObjectMapper mapper = new ObjectMapper();
// Private constructor for singleton
private FigureConverter() {
/*
try {
// Read JSON from the URL
URL url = new URL("https://raw.githubusercontent.com/Alcosmos/habbo-old-figure-converter/refs/heads/main/imager/oldfiguredata.json");
BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
StringBuilder sb = new StringBuilder();
String inputLine;
while ((inputLine = in.readLine()) != null) {
sb.append(inputLine);
}
in.close();
// Parse JSON
JSONObject jsonObject = new JSONObject(sb.toString());
// Convert to XML
String xmlString = XML.toString(jsonObject, null, XMLParserConfiguration.ORIGINAL, 2);
try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.xml"))) {
writer.write(xmlString);
}
} catch (Exception e) {
e.printStackTrace();
}
*/
// getOldschoolJsonData();
ObjectMapper mapper = new ObjectMapper();
// Read JSON as Map
Map<String, Object> originalData = null;
try {
originalData = mapper.readValue(getOldschoolJsonData(), Map.class);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
// Mapping of old keys to new keys
Map<String, String> renameMap = new HashMap<>();
renameMap.put("p", "parts");
renameMap.put("c", "colors");
renameMap.put("s","sprite");
renameMap.put("M", "male");
renameMap.put("F", "female");
// Renaming process
Map<String, Object> renamedData = (Map<String, Object>) renameKeysRecursive(originalData, renameMap);
// Output result as JSON
String resultJson = null;
try {
resultJson = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(renamedData);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
/*
String xmlString = XML.toString(jsonObject, null, XMLParserConfiguration.ORIGINAL, 2);
try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.xml"))) {
writer.write(xmlString);
}
*/
try (BufferedWriter writer = new BufferedWriter(new FileWriter("formatted_figuredata.json"))) {
writer.write(resultJson);
} catch (IOException e) {
throw new RuntimeException(e);
}
try (BufferedWriter writer = new BufferedWriter(new FileWriter("formatted_figuredata.xml"))) {
writer.write(XML.toString(new JSONObject(resultJson), null, XMLParserConfiguration.ORIGINAL, 2).replaceAll("<\\s*array\\s*>", "")
.replaceAll("<\\s*/\\s*array\\s*>", ""));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
public static Object renameKeysRecursive(Object data, Map<String, String> renameMap) {
if (data instanceof Map) {
Map<String, Object> map = (Map<String, Object>) data;
Map<String, Object> result = new HashMap<>();
for (Map.Entry<String, Object> entry : map.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
// Flatten/remove "array" key, merge its map elements into parent
if ("array".equals(key)) {
Object arrValue = renameKeysRecursive(value, renameMap);
if (arrValue instanceof List) {
for (Object item : (List<?>) arrValue) {
if (item instanceof Map) {
result.putAll((Map<? extends String, ?>) item);
}
// If not a Map, skip or handle as desired
}
}
// If not a list, skip
continue;
}
// Convert "c" to colours:[{colour:...}]
if ("c".equals(key)) {
List<Object> coloursList = new ArrayList<>();
Map<String, Object> colourObj = new HashMap<>();
colourObj.put("colour", renameKeysRecursive(value, renameMap));
coloursList.add(colourObj);
Object existingColours = result.get("colours");
if (existingColours instanceof List) {
((List<Object>) existingColours).addAll(coloursList);
} else {
result.put("colours", coloursList);
}
continue;
}
// Handle "parts" that is a List: merge to single map if each is a single-key map
String newKey = renameMap.getOrDefault(key, key);
Object newValue = renameKeysRecursive(value, renameMap);
if ("parts".equals(newKey) && newValue instanceof List) {
List<?> list = (List<?>) newValue;
boolean allSingleKeyMaps = list.stream().allMatch(
item -> item instanceof Map && ((Map<?, ?>) item).size() == 1
);
if (allSingleKeyMaps && !list.isEmpty()) {
Map<String, Object> merged = new HashMap<>();
for (Object item : list) {
merged.putAll((Map<? extends String, ?>) item);
}
result.put("parts", merged);
} else {
result.put(newKey, newValue);
}
} else {
result.put(newKey, newValue);
}
}
return result;
} else if (data instanceof List) {
List<Object> list = (List<Object>) data;
List<Object> result = new ArrayList<>();
for (Object item : list) {
result.add(renameKeysRecursive(item, renameMap));
}
return result;
} else {
return data;
}
}
private String getOldschoolJsonData() {
StringBuilder sb = new StringBuilder();
try {
URL url = new URL("https://raw.githubusercontent.com/Alcosmos/habbo-old-figure-converter/refs/heads/main/imager/oldfiguredata.json");
BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
String inputLine;
while ((inputLine = in.readLine()) != null) {
sb.append(inputLine);
}
in.close();
} catch (IOException e) {
}
return sb.toString();
}
/**
* Converts an old figure format string to the new avatarimage format.
*/
public String convertOldToNew(String oldFigure) {
if (oldFigure == null || oldFigure.length() < 22) {
throw new IllegalArgumentException("Invalid figure string");
}
String[] partsString = new String[10];
int start = 0;
for (int i = 0; i < 10; i++) {
int length = (i == 0 || i == 2 || i == 4 || i == 6 || i == 8) ? 3 : 2;
partsString[i] = oldFigure.substring(start, start + length);
start += length;
}
int[] parts = new int[10];
for (int i = 0; i < 10; i++) {
parts[i] = Integer.parseInt(partsString[i]);
}
String hrColor = convertOldColorToNew("hr", parts[0], parts[1]);
StringBuilder result = new StringBuilder();
result.append("hr-").append(parts[0]).append("-").append(hrColor);
result.append(".hd-").append(parts[2]).append("-").append(convertOldColorToNew("hd", parts[2], parts[3]));
result.append(".ch-").append(parts[8]).append("-").append(convertOldColorToNew("ch", parts[8], parts[9]));
result.append(".lg-").append(parts[4]).append("-").append(convertOldColorToNew("lg", parts[4], parts[5]));
result.append(".sh-").append(parts[6] == 730 ? 3206 : parts[6]).append("-").append(convertOldColorToNew("sh", parts[6], parts[7]));
result.append(takeCareOfHats(parts[0], Integer.parseInt(hrColor)));
return result.toString();
}
private JsonNode getOldFigureData() {
if (oldFigureData == null) {
synchronized (oldLock) {
if (oldFigureData == null) {
try {
oldFigureData = mapper.readTree(new File(oldFigureDataPath));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
return oldFigureData;
}
/*
private JsonNode getNewFigureData() {
if (newFigureData == null) {
synchronized (newLock) {
if (newFigureData == null) {
try {
newFigureData = mapper.readTree(new File(newFigureDataPath));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
return newFigureData;
}*/
private String getOldColorFromFigureList(String part, int sprite, int colorIndex) {
JsonNode colorsJson = getOldFigureData();
JsonNode genders = colorsJson.get("genders");
if (genders == null) return null;
for (JsonNode gender : genders) {
for (JsonNode partType : gender) {
if (partType.has(part)) {
JsonNode partArray = partType.get(part);
for (JsonNode dataArray : partArray) {
for (JsonNode dataObj : dataArray) {
if (dataObj != null && dataObj.has("s") && dataObj.has("c")) {
if (dataObj.get("s").asInt() == sprite) {
JsonNode spriteColorsArray = mapper.createArrayNode();
try {
spriteColorsArray = mapper.readTree(dataObj.get("c").toString());
} catch (IOException e) {
throw new RuntimeException(e);
}
if (colorIndex - 1 < spriteColorsArray.size()) {
return spriteColorsArray.get(colorIndex - 1).asText();
}
}
}
}
}
}
}
}
return null;
}
private String convertOldColorToNew(String part, int sprite, int colorIndex) {
String oldColor = getOldColorFromFigureList(part, sprite, colorIndex);
if (oldColor == null) return null;
for (List<FigureColor> colourPalettes : FiguredataReader.getInstance().getFigurePalettes().values()) {
Optional<FigureColor> colour = colourPalettes.stream()
.filter(x -> x.getHexColor().equalsIgnoreCase(oldColor))
.findFirst();
if (colour.isPresent()) {
return colour.get().getColourId();
}
}
/*
JsonNode paletteJson = getNewFigureData();
JsonNode palette = paletteJson.get("palette");
if (palette == null) return null;
for (JsonNode pal : palette) {
if (pal.isObject()) {
for (java.util.Iterator<String> it = pal.fieldNames(); it.hasNext(); ) {
String colorName = it.next();
JsonNode colorObj = pal.get(colorName);
if (colorObj != null && colorObj.has("color")) {
if (colorObj.get("color").asText().equals(oldColor)) {
return colorName;
}
}
}
}
}*/
return null;
}
private String takeCareOfHats(int spriteId, int colorId) {
switch (spriteId) {
case 120: return ".ha-1001-0";
case 525:
case 140: return ".ha-1002-" + colorId;
case 150:
case 535: return ".ha-1003-" + colorId;
case 160:
case 565: return ".ha-1004-" + colorId;
case 570: return ".ha-1005-" + colorId;
case 585:
case 175: return ".ha-1006-0";
case 580:
case 176: return ".ha-1007-0.fa-1202-70";
case 590:
case 177: return ".ha-1008-0";
case 595:
case 178: return ".ha-1009-1321";
case 130: return ".ha-1010-" + colorId;
case 801: return ".hr-829-" + colorId + ".fa-1201-62.ha-1011-" + colorId;
case 800:
case 810: return ".ha-1012-" + colorId;
case 802:
case 811: return ".ha-1013-" + colorId;
default: return ""; // this is the same as below but takes up less memory and is official Habbo behaviour :^) - Quackster
// default: return ".ha-0-" + colorId;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment