Fixed Holocore #1328

This commit is contained in:
Josh-Larson
2024-06-07 20:32:28 -05:00
parent 4dfa51e7f4
commit bd94484b81
3 changed files with 247 additions and 338 deletions

View File

@@ -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 <http://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 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<String, Short> VAR_NAME_TO_ID = new HashMap<>();
private static final Map<Short, String> 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<String, Integer> 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<String, Integer> getVariables() {
return Collections.unmodifiableMap(variables);
}
public void forEach(BiConsumer<? super String, ? super Integer> consumer) {
variables.forEach(consumer);
}
public void clear() {
variables.clear();
}
@Override
public String toString() {
StringBuilder str = new StringBuilder();
boolean first = true;
for (Entry<String, Integer> 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;
}
}

View File

@@ -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 <http:></http:>//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<String, Int> = Collections.synchronizedMap(LinkedHashMap()) // Ordered and synchronized
val variables: Map<String, Int> = 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<in String?, in Int?>?) {
_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<String, Byte> = HashMap()
private val VAR_ID_TO_NAME: MutableMap<Byte, String> = 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
}
}
}

View File

@@ -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)
}
}