Created
September 5, 2025 04:19
-
-
Save Quackster/7bd3bbab5af3eef040b90c1f82211c16 to your computer and use it in GitHub Desktop.
Avatara4j figuredata conversion
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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