From bd94484b81aa71b6e1e4bbeb000e46a9e7cef84d Mon Sep 17 00:00:00 2001 From: Josh-Larson Date: Fri, 7 Jun 2024 20:32:28 -0500 Subject: [PATCH] Fixed Holocore #1328 --- .../customization/CustomizationString.java | 329 ------------------ .../data/customization/CustomizationString.kt | 239 +++++++++++++ .../customization/TestCustomizationString.kt | 17 +- 3 files changed, 247 insertions(+), 338 deletions(-) delete mode 100644 src/main/java/com/projectswg/common/data/customization/CustomizationString.java create mode 100644 src/main/java/com/projectswg/common/data/customization/CustomizationString.kt diff --git a/src/main/java/com/projectswg/common/data/customization/CustomizationString.java b/src/main/java/com/projectswg/common/data/customization/CustomizationString.java deleted file mode 100644 index 4aebe3d..0000000 --- a/src/main/java/com/projectswg/common/data/customization/CustomizationString.java +++ /dev/null @@ -1,329 +0,0 @@ -/*********************************************************************************** - * Copyright (c) 2023 /// Project SWG /// www.projectswg.com * - * * - * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * - * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. * - * Our goal is to create an emulator which will provide a server for players to * - * continue playing a game similar to the one they used to play. We are basing * - * it on the final publish of the game prior to end-game events. * - * * - * This file is part of PSWGCommon. * - * * - * --------------------------------------------------------------------------------* - * * - * PSWGCommon is free software: you can redistribute it and/or modify * - * it under the terms of the GNU Affero General Public License as * - * published by the Free Software Foundation, either version 3 of the * - * License, or (at your option) any later version. * - * * - * PSWGCommon is distributed in the hope that it will be useful, * - * but WITHOUT ANY WARRANTY; without even the implied warranty of * - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * GNU Affero General Public License for more details. * - * * - * You should have received a copy of the GNU Affero General Public License * - * along with PSWGCommon. If not, see . * - ***********************************************************************************/ -package com.projectswg.common.data.customization; - -import com.projectswg.common.data.encodables.mongo.MongoData; -import com.projectswg.common.data.encodables.mongo.MongoPersistable; -import com.projectswg.common.encoding.Encodable; -import com.projectswg.common.network.NetBuffer; -import me.joshlarson.jlcommon.log.Log; -import org.jetbrains.annotations.NotNull; - -import java.io.*; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Map.Entry; -import java.util.function.BiConsumer; - -/** - * The Customization string is used to set special properties - * on objects. This can be lightsaber color, vehicle speed, - * facial hair style... - */ -public class CustomizationString implements Encodable, MongoPersistable { - - private static final Map VAR_NAME_TO_ID = new HashMap<>(); - private static final Map VAR_ID_TO_NAME = new HashMap<>(); - - static { - try (BufferedInputStream bis = new BufferedInputStream(CustomizationString.class.getResourceAsStream("customization_variables.sdb"))) { - StringBuilder buffer = new StringBuilder(); - String key = null; - int c; - while ((c = bis.read()) != -1) { - switch (c) { - case '\t': - key = buffer.toString(); - buffer.setLength(0); - break; - //noinspection HardcodedLineSeparator - case '\r': - //noinspection HardcodedLineSeparator - case '\n': - if (buffer.length() <= 0) - continue; - assert key != null; - VAR_NAME_TO_ID.put(key, Short.valueOf(buffer.toString())); - VAR_ID_TO_NAME.put(Short.valueOf(buffer.toString()), key); - buffer.setLength(0); - break; - default: - buffer.append((char) c); - break; - } - } - if (buffer.length() > 0) { - assert key != null; - VAR_NAME_TO_ID.put(key, Short.valueOf(buffer.toString())); - VAR_ID_TO_NAME.put(Short.valueOf(buffer.toString()), key); - } - } catch (IOException e) { - throw new RuntimeException("could not load customization variables from resources", e); - } - } - - private final Map variables; - - public CustomizationString() { - this.variables = Collections.synchronizedMap(new LinkedHashMap<>()); // Ordered and synchronized - } - - boolean isEmpty() { - return variables.isEmpty(); - } - - @Override - public void saveMongo(MongoData data) { - data.putMap("variables", variables); - } - - @Override - public void readMongo(MongoData data) { - variables.clear(); - variables.putAll(data.getMap("variables", String.class, Integer.class)); - } - - public Integer put(String name, int value) { - return variables.put(name, value); - } - - public Integer remove(String name) { - return variables.remove(name); - } - - public Integer get(String name) { - return variables.get(name); - } - - public Map getVariables() { - return Collections.unmodifiableMap(variables); - } - - public void forEach(BiConsumer consumer) { - variables.forEach(consumer); - } - - public void clear() { - variables.clear(); - } - - @Override - public String toString() { - StringBuilder str = new StringBuilder(); - boolean first = true; - for (Entry e : variables.entrySet()) { - if (!first) - str.append(", "); - first = false; - str.append(e.getKey()).append('=').append(e.getValue()); - } - return str.toString(); - } - - @Override - public void decode(NetBuffer data) { - byte[] stringData = data.getArray(); - - if (stringData.length == 0) { - return; - } - - String string = new String(stringData, StandardCharsets.UTF_8); - int[] codePoints = string.codePoints().toArray(); // Replaces 0xC3BF with 0xFF, because 0xFF is reserved as an escape flag - int position = 0; - byte startOfText = (byte) codePoints[position++]; - - if (startOfText != 0x02) { - Log.w("Expected UTF8 start-of-text in CustomizationString, assuming corruption!"); - return; - } - - short variableCount = (short) codePoints[position++]; - - if (variableCount == 0xFF) { - // SOE decided not to include a variable count for the Twi'lek lekku. No object has anywhere near 255 variables, so we'll be alright. - return; - } - - for (short i = 0; i < variableCount; i++) { - short combinedVariableInfo = (short) codePoints[position++]; - int variableSize = ((combinedVariableInfo & 0x80) != 0) ? 2 : 1; - short variableId = (short) (combinedVariableInfo & 0x7F); - int current = codePoints[position++]; // Read 8-bit value - int variable; - - switch (variableSize) { - case 2: - // Read 16-bit value - byte lowByte = (byte) current; - byte hiByte = (byte) codePoints[position++]; - position++; // Skip escape - - current = ((hiByte << 8) | lowByte) & 0xFF; - break; - } - - if (current == 0xFF) { // This marks an escaped character to follow - int next = codePoints[position++]; - - switch (next) { - case 0x01: // Value is 0 - default: - variable = 0; - break; - case 0x02: // Value is 255 - variable = 0xFF; - break; - case 0x03: // We shouldn't be meeting an end here. Malformed input. - Log.w("Unexpected end of text in CustomizationString, assuming corruption!"); - clear(); // In this case, we'll want to clear whatever we've loaded as it might be corrupted - return; - } - } else { - variable = current; - } - - String variableName = VAR_ID_TO_NAME.get(variableId); - - if (variableName == null) { // Variable ID matched no variable name. - Log.w("Variable ID %d had no name associated", variableId); - position++; // Skip the associated value - continue; - } - - variables.put(variableName, variable); - } - - if ((position + 2) < codePoints.length) { - Log.w("Too much data remaining in CustomizationString, assuming corruption!"); - clear(); // In this case, we'll want to clear whatever we've loaded as it might be corrupted - return; - } - - int escapeFlag = codePoints[position++]; - int endOfText = codePoints[position]; - - if (escapeFlag == 0xFF && endOfText != 0x03) { - Log.w("Invalid UTF-8 ending for CustomizationString, assuming corruption!"); - clear(); // In this case, we'll want to clear whatever we've loaded as it might be corrupted - } - } - - @Override - public byte @NotNull [] encode() { - if (isEmpty()) { - // No need to send more than an empty array in this case - return ByteBuffer.allocate(Short.BYTES).array(); - } - - int encodableLength = getLength(); - ByteArrayOutputStream out = new ByteArrayOutputStream(encodableLength - Short.BYTES); - - try { - out.write(2); // Marks start of text - addCustomizationStringByte(out, variables.size()); - - variables.forEach((variableName, variableValue) -> { - int variableId = VAR_NAME_TO_ID.get(variableName); - boolean variableValueOneByte = variableValue >= 0 && variableValue < 128; - if (!variableValueOneByte) - variableId |= 0x80; - - try { - out.write(variableId); - if (variableValueOneByte) { - addCustomizationStringByte(out, variableValue); - } else { - addCustomizationStringByte(out, variableValue & 0xFF); - addCustomizationStringByte(out, (variableValue >> 8) & 0xFF); - } - } catch (Exception e) { - Log.e(e); - } - }); - - out.write(0xFF); // Escape - out.write(3); // Marks end of text - out.flush(); - - byte[] result = out.toByteArray(); - NetBuffer data = NetBuffer.allocate(encodableLength); - - data.addArray(result); // This will add the array length in little endian order - - return data.array(); - } catch (IOException e) { - Log.e(e); - return NetBuffer.allocate(Short.SIZE).array(); // Returns an array of 0x00, 0x00 indicating that it's empty - } - } - - private void addCustomizationStringByte(ByteArrayOutputStream out, int c) throws IOException { - assert(c >= 0 && c <= 0xFF); - switch (c) { - case 0x00 -> { - out.write(0xFF); // Escape - out.write(0x01); // Put variable value - } - case 0xFF -> { - out.write(0xFF); // Escape - out.write(0x02); // Put variable value - } - default -> out.write(c); - } - } - - @Override - public int getLength() { - int length = Short.BYTES; // Array size declaration field - - length += 3; // UTF-8 start of text, escape, UTF-8 end of text - length += getValueLength(variables.size()); // variable count - for (Integer i : variables.values()) { - length += 1; // variableId - int firstByte = i & 0xFF; - int secondByte = (i >> 8) & 0xFF; - length += getValueLength(firstByte); - if (i < 0 || i >= 128) { // signed short - length += getValueLength(secondByte); - } - } - - return length; - } - - private static int getValueLength(int c) { - if (c == 0x00 || c == 0xFF) - return 2; - return 1; - } - -} diff --git a/src/main/java/com/projectswg/common/data/customization/CustomizationString.kt b/src/main/java/com/projectswg/common/data/customization/CustomizationString.kt new file mode 100644 index 0000000..6f3ef7f --- /dev/null +++ b/src/main/java/com/projectswg/common/data/customization/CustomizationString.kt @@ -0,0 +1,239 @@ +/*********************************************************************************** + * Copyright (c) 2023 /// Project SWG /// www.projectswg.com * + * * + * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * + * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. * + * Our goal is to create an emulator which will provide a server for players to * + * continue playing a game similar to the one they used to play. We are basing * + * it on the final publish of the game prior to end-game events. * + * * + * This file is part of PSWGCommon. * + * * + * --------------------------------------------------------------------------------* + * * + * PSWGCommon is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 of the * + * License, or (at your option) any later version. * + * * + * PSWGCommon is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with PSWGCommon. If not, see //www.gnu.org/licenses/>. * + */ +package com.projectswg.common.data.customization + +import com.projectswg.common.data.encodables.mongo.MongoData +import com.projectswg.common.data.encodables.mongo.MongoPersistable +import com.projectswg.common.encoding.Encodable +import com.projectswg.common.network.NetBuffer +import me.joshlarson.jlcommon.log.Log +import java.io.BufferedReader +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStreamReader +import java.util.* +import java.util.function.BiConsumer + +/** + * The Customization string is used to set special properties + * on objects. This can be lightsaber color, vehicle speed, + * facial hair, etc. + */ +class CustomizationString : Encodable, MongoPersistable { + private val _variables: MutableMap = Collections.synchronizedMap(LinkedHashMap()) // Ordered and synchronized + val variables: Map = Collections.unmodifiableMap(_variables) + + val isEmpty: Boolean + get() = _variables.isEmpty() + + override fun saveMongo(data: MongoData) { + data.putMap("variables", _variables) + } + + override fun readMongo(data: MongoData) { + _variables.clear() + _variables.putAll(data.getMap("variables", String::class.java, Int::class.java)) + } + + fun put(name: String, value: Int): Int? { + assert(name in VAR_NAME_TO_ID) + return _variables.put(name, value) + } + + fun remove(name: String?): Int? { + return _variables.remove(name) + } + + fun get(name: String?): Int? { + return _variables[name] + } + + fun forEach(consumer: BiConsumer?) { + _variables.forEach(consumer!!) + } + + fun clear() { + _variables.clear() + } + + override fun toString(): String { + val str = StringBuilder() + var first = true + for ((key, value) in _variables) { + if (!first) str.append(", ") + first = false + str.append(key).append('=').append(value) + } + return str.toString() + } + + override fun decode(data: NetBuffer) { + val str = NetBuffer.wrap(data.array) + if (str.size() == 0 || str.byte.toInt() != 0x02) return // Start of Text + + val variableCount = getCustomizationStringByte(str) + for (i in 0 until variableCount) { + val combinedVariableId = str.byte + val variableId = VAR_ID_TO_NAME[(combinedVariableId.toInt() and 0x7F).toByte()] + var value = getCustomizationStringByte(str) and 0xFF + if ((combinedVariableId.toInt() and 0x80) != 0) { + value = value or ((getCustomizationStringByte(str) shl 8) and 0xFF00) + } + if (variableId == null) { + Log.w("CustomizationString: Unparsed variableId %d with value %d", (combinedVariableId.toInt() and 0x7F), value) + } else { + _variables[variableId] = value and 0xFFFF + } + } + + // str.byte; 0xFF - Escape + // str.byte; 0x03 - End of Text + } + + override fun encode(): ByteArray { + if (isEmpty) { + // No need to send more than an empty array in this case + return ByteArray(java.lang.Short.BYTES) + } + + val encodableLength = length + val out = ByteArrayOutputStream(encodableLength - java.lang.Short.BYTES) + + try { + out.write(2) // Marks start of text + addCustomizationStringByte(out, _variables.size) + + _variables.forEach { (variableName: String, variableValue: Int) -> + var variableId = VAR_NAME_TO_ID[variableName]!!.toInt() + val variableValueOneByte = variableValue in 0..127 + if (!variableValueOneByte) variableId = variableId or 0x80 + try { + out.write(variableId) + if (variableValueOneByte) { + addCustomizationStringByte(out, variableValue) + } else { + addCustomizationStringByte(out, variableValue and 0xFF) + addCustomizationStringByte(out, (variableValue shr 8) and 0xFF) + } + } catch (e: Exception) { + Log.e(e) + } + } + + out.write(0xFF) // Escape + out.write(3) // Marks end of text + out.flush() + + val result = out.toByteArray() + val data = NetBuffer.allocate(encodableLength) + + data.addArray(result) // This will add the array length in little endian order + + return data.array() + } catch (e: IOException) { + Log.e(e) + return NetBuffer.allocate(java.lang.Short.SIZE).array() // Returns an array of 0x00, 0x00 indicating that it's empty + } + } + + @Throws(IOException::class) + private fun addCustomizationStringByte(out: ByteArrayOutputStream, c: Int) { + assert(c in 0..0xFF) + when (c) { + 0x00 -> { + out.write(0xFF) // Escape + out.write(0x01) // Put variable value + } + + 0xFF -> { + out.write(0xFF) // Escape + out.write(0x02) // Put variable value + } + + else -> out.write(c) + } + } + + private fun getCustomizationStringByte(out: NetBuffer): Int { + val c = out.byte + + if (c == 0xFF.toByte()) { + return when (out.byte) { + 0x01.toByte() -> 0x00 + 0x02.toByte() -> 0xFF + else -> 0x00 + } + } + + return c.toInt() + } + + override val length: Int + get() { + var length = java.lang.Short.BYTES // Array size declaration field + + length += 3 // UTF-8 start of text, escape, UTF-8 end of text + length += getValueLength(_variables.size) // variable count + for (i in _variables.values) { + length += 1 // variableId + val firstByte = i and 0xFF + val secondByte = (i shr 8) and 0xFF + length += getValueLength(firstByte) + if (i < 0 || i >= 128) { // signed short + length += getValueLength(secondByte) + } + } + + return length + } + + companion object { + private val VAR_NAME_TO_ID: MutableMap = HashMap() + private val VAR_ID_TO_NAME: MutableMap = HashMap() + + init { + try { + BufferedReader(InputStreamReader(Objects.requireNonNull(CustomizationString::class.java.getResourceAsStream("customization_variables.sdb")))).use { reader -> + reader.lines().forEach { line: String -> + val tab = line.indexOf('\t') + val key = line.substring(0, tab) + val value = line.substring(tab + 1).toByte() + VAR_NAME_TO_ID[key] = value + VAR_ID_TO_NAME[value] = key + } + } + } catch (e: IOException) { + throw RuntimeException("could not load customization variables from resources", e) + } + } + + private fun getValueLength(c: Int): Int { + if (c == 0x00 || c == 0xFF) return 2 + return 1 + } + } +} diff --git a/src/test/java/com/projectswg/common/data/customization/TestCustomizationString.kt b/src/test/java/com/projectswg/common/data/customization/TestCustomizationString.kt index 792812a..654746b 100644 --- a/src/test/java/com/projectswg/common/data/customization/TestCustomizationString.kt +++ b/src/test/java/com/projectswg/common/data/customization/TestCustomizationString.kt @@ -34,7 +34,7 @@ class TestCustomizationString { @Test fun testPut() { val string = CustomizationString() - val key = "test" + val key = "/private/index_color_pattern" assertNull(string.put(key, 0)) // Nothing should be replaced because string's empty assertEquals(0, string.put(key, 1)) // Same key, so the variable we put earlier should be replaced } @@ -42,7 +42,7 @@ class TestCustomizationString { @Test fun testRemove() { val string = CustomizationString() - val key = "test" + val key = "/private/index_color_pattern" string.put(key, 0) assertEquals(0, string.remove(key)) // Same key, so the variable we put earlier should be returned } @@ -50,7 +50,7 @@ class TestCustomizationString { @Test fun testIsEmpty() { val string = CustomizationString() - val key = "test" + val key = "/private/index_color_pattern" assertTrue(string.isEmpty) string.put(key, 0) assertFalse(string.isEmpty) @@ -71,16 +71,15 @@ class TestCustomizationString { } @Test - fun decoding() { + fun encodeDecode() { val string = CustomizationString() string.put("/private/index_color_1", 237) string.put("/private/index_color_2", 4) - val input = byteArrayOf(10, 0, 2, 2, 2, -61, -83, 1, 4, -61, -65, 3) - - string.decode(NetBuffer.wrap(input)) + val decoded = CustomizationString() + decoded.decode(NetBuffer.wrap(string.encode())) assertEquals(2, string.variables.size) - assertEquals(string.get("/private/index_color_1"), 237) - assertEquals(string.get("/private/index_color_2"), 4) + assertEquals(decoded.get("/private/index_color_1"), 237) + assertEquals(decoded.get("/private/index_color_2"), 4) } }