You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 15:06:19 +00:00
Merge pull request #103 from medievalshell/Dev
feat(furnieditor): split-aware FurniDataManager + JSON5 tolerance
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- 020_auth_ticket_ttl.sql
|
||||||
|
--
|
||||||
|
-- Adds an explicit expiry timestamp to the SSO auth_ticket on `users`.
|
||||||
|
--
|
||||||
|
-- The CMS issuing the ticket is expected to populate auth_ticket_expires_at
|
||||||
|
-- (e.g. NOW() + INTERVAL 60 SECOND) on every login redirect. The emulator-
|
||||||
|
-- side SELECT queries that look up a user by auth_ticket have been changed to
|
||||||
|
--
|
||||||
|
-- WHERE auth_ticket = ?
|
||||||
|
-- AND (auth_ticket_expires_at IS NULL OR auth_ticket_expires_at >= NOW())
|
||||||
|
--
|
||||||
|
-- The NULL branch keeps backward-compatibility with CMS deployments that do
|
||||||
|
-- not populate the column yet: existing rows continue to authenticate the
|
||||||
|
-- same way they always did, and the TTL kicks in only once the CMS starts
|
||||||
|
-- writing the expiry value.
|
||||||
|
--
|
||||||
|
-- Idempotent: skips the ALTER if the column already exists.
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
SET @col_exists = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'users'
|
||||||
|
AND COLUMN_NAME = 'auth_ticket_expires_at'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @ddl = IF(@col_exists = 0,
|
||||||
|
'ALTER TABLE `users` ADD COLUMN `auth_ticket_expires_at` TIMESTAMP NULL DEFAULT NULL AFTER `auth_ticket`',
|
||||||
|
'SELECT ''auth_ticket_expires_at already present, skipping'' AS info'
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE stmt FROM @ddl;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
@@ -30682,6 +30682,7 @@ CREATE TABLE IF NOT EXISTS `users` (
|
|||||||
`points` int(11) NOT NULL DEFAULT 10,
|
`points` int(11) NOT NULL DEFAULT 10,
|
||||||
`online` enum('0','1','2') CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '0',
|
`online` enum('0','1','2') CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '0',
|
||||||
`auth_ticket` varchar(256) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT '',
|
`auth_ticket` varchar(256) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT '',
|
||||||
|
`auth_ticket_expires_at` timestamp NULL DEFAULT NULL,
|
||||||
`remember_token_hash` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '',
|
`remember_token_hash` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '',
|
||||||
`remember_token_expires_at` int(11) unsigned NOT NULL DEFAULT 0,
|
`remember_token_expires_at` int(11) unsigned NOT NULL DEFAULT 0,
|
||||||
`ip_register` varchar(45) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
|
`ip_register` varchar(45) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<groupId>com.eu.habbo</groupId>
|
<groupId>com.eu.habbo</groupId>
|
||||||
<artifactId>Habbo</artifactId>
|
<artifactId>Habbo</artifactId>
|
||||||
<version>4.1.16</version>
|
<version>4.2.7</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ public class HabboManager {
|
|||||||
int userId = 0;
|
int userId = 0;
|
||||||
|
|
||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||||
PreparedStatement statement = connection.prepareStatement("SELECT id FROM users WHERE auth_ticket = ? LIMIT 1")) {
|
PreparedStatement statement = connection.prepareStatement("SELECT id FROM users WHERE auth_ticket = ? AND (auth_ticket_expires_at IS NULL OR auth_ticket_expires_at >= NOW()) LIMIT 1")) {
|
||||||
statement.setString(1, sso);
|
statement.setString(1, sso);
|
||||||
try (ResultSet s = statement.executeQuery()) {
|
try (ResultSet s = statement.executeQuery()) {
|
||||||
if (s.next()) {
|
if (s.next()) {
|
||||||
@@ -121,7 +121,7 @@ public class HabboManager {
|
|||||||
|
|
||||||
|
|
||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||||
PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE auth_ticket = ? LIMIT 1")) {
|
PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE auth_ticket = ? AND (auth_ticket_expires_at IS NULL OR auth_ticket_expires_at >= NOW()) LIMIT 1")) {
|
||||||
statement.setString(1, sso);
|
statement.setString(1, sso);
|
||||||
try (ResultSet set = statement.executeQuery()) {
|
try (ResultSet set = statement.executeQuery()) {
|
||||||
if (set.next()) {
|
if (set.next()) {
|
||||||
|
|||||||
+271
-61
@@ -13,107 +13,317 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages reading and writing of FurnitureData.json entries.
|
* Manages reading and writing of FurnitureData entries.
|
||||||
* Resolves the file path from emulator config keys.
|
*
|
||||||
|
* Accepts both legacy single-file layouts (FurnitureData.json) and the split
|
||||||
|
* directory layout introduced by the split-aware loader on the Nitro V3 side:
|
||||||
|
*
|
||||||
|
* <base>/
|
||||||
|
* manifest.json5 OPTIONAL { "tiers": ["core", "custom", "seasonal"] }
|
||||||
|
* core/manifest.json5 REQUIRED { "files": ["floor-001.json5", ...] }
|
||||||
|
* core/*.json5
|
||||||
|
* custom/manifest.json5 OPTIONAL
|
||||||
|
* seasonal/manifest.json5 OPTIONAL
|
||||||
|
*
|
||||||
|
* The path is resolved from the emulator config:
|
||||||
|
*
|
||||||
|
* furni.editor.renderer.config.path -> renderer-config.json (read for the
|
||||||
|
* furnidata.url value)
|
||||||
|
* furni.editor.asset.base.path -> filesystem base used to derive the
|
||||||
|
* local path from an http(s) URL
|
||||||
*/
|
*/
|
||||||
public class FurniDataManager {
|
public class FurniDataManager {
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(FurniDataManager.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(FurniDataManager.class);
|
||||||
|
|
||||||
|
private static final List<String> DEFAULT_TIERS = Arrays.asList("core", "custom", "seasonal");
|
||||||
|
private static final List<String> MANIFEST_NAMES = Arrays.asList("manifest.json5", "manifest.json");
|
||||||
|
private static final List<String> SECTIONS = Arrays.asList("roomitemtypes", "wallitemtypes");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the JSON string for a specific item from FurnitureData.json.
|
* Get the JSON string for a specific item.
|
||||||
* Returns "{}" if not found or on error.
|
* Returns "{}" if not found or on error.
|
||||||
*/
|
*/
|
||||||
public static String getItemJson(int itemId) {
|
public static String getItemJson(int itemId) {
|
||||||
try {
|
try {
|
||||||
Path furniDataPath = resolveFurniDataPath();
|
ResolvedSource source = resolveSource();
|
||||||
if (furniDataPath == null || !Files.exists(furniDataPath)) {
|
if (source == null) return "{}";
|
||||||
return "{}";
|
|
||||||
|
if (source.directory) {
|
||||||
|
return findItemInSplitDir(source.path, itemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
String content = Files.readString(furniDataPath, StandardCharsets.UTF_8);
|
if (!Files.exists(source.path)) return "{}";
|
||||||
JsonObject root = JsonParser.parseString(content).getAsJsonObject();
|
|
||||||
|
|
||||||
// Search in both "roomitemtypes" and "wallitemtypes"
|
String content = readJson5(source.path);
|
||||||
for (String section : new String[]{"roomitemtypes", "wallitemtypes"}) {
|
return findItemInRoot(JsonParser.parseString(content).getAsJsonObject(), itemId);
|
||||||
if (!root.has(section)) continue;
|
|
||||||
JsonObject sectionObj = root.getAsJsonObject(section);
|
|
||||||
if (!sectionObj.has("furnitype")) continue;
|
|
||||||
JsonArray types = sectionObj.getAsJsonArray("furnitype");
|
|
||||||
|
|
||||||
for (JsonElement el : types) {
|
|
||||||
JsonObject obj = el.getAsJsonObject();
|
|
||||||
if (obj.has("id") && obj.get("id").getAsInt() == itemId) {
|
|
||||||
return obj.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOGGER.warn("Failed to read FurnitureData.json for item " + itemId, e);
|
LOGGER.warn("Failed to read FurnitureData for item " + itemId, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return "{}";
|
return "{}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String findItemInRoot(JsonObject root, int itemId) {
|
||||||
|
for (String section : SECTIONS) {
|
||||||
|
if (!root.has(section)) continue;
|
||||||
|
JsonObject sectionObj = root.getAsJsonObject(section);
|
||||||
|
if (!sectionObj.has("furnitype")) continue;
|
||||||
|
JsonArray types = sectionObj.getAsJsonArray("furnitype");
|
||||||
|
|
||||||
|
for (JsonElement el : types) {
|
||||||
|
JsonObject obj = el.getAsJsonObject();
|
||||||
|
if (obj.has("id") && obj.get("id").getAsInt() == itemId) {
|
||||||
|
return obj.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the path to FurnitureData.json from emulator config.
|
* Walk the split directory layout looking for an item by id.
|
||||||
|
* Later tiers (custom, then seasonal) override earlier ones.
|
||||||
*/
|
*/
|
||||||
private static Path resolveFurniDataPath() {
|
private static String findItemInSplitDir(Path baseDir, int itemId) {
|
||||||
|
if (!Files.isDirectory(baseDir)) return "{}";
|
||||||
|
|
||||||
|
List<String> tiers = readTiersManifest(baseDir);
|
||||||
|
String found = null;
|
||||||
|
|
||||||
|
for (String tier : tiers) {
|
||||||
|
Path tierDir = baseDir.resolve(tier);
|
||||||
|
if (!Files.isDirectory(tierDir)) continue;
|
||||||
|
|
||||||
|
List<String> files = readFilesManifest(tierDir);
|
||||||
|
for (String fileName : files) {
|
||||||
|
Path file = tierDir.resolve(fileName);
|
||||||
|
if (!Files.exists(file)) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
String content = readJson5(file);
|
||||||
|
JsonObject obj = JsonParser.parseString(content).getAsJsonObject();
|
||||||
|
String match = findItemInRoot(obj, itemId);
|
||||||
|
if (match != null) found = match;
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("Failed to parse split gamedata file " + file, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return found != null ? found : "{}";
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static List<String> readTiersManifest(Path baseDir) {
|
||||||
|
Path manifest = firstExisting(baseDir, MANIFEST_NAMES);
|
||||||
|
if (manifest == null) return DEFAULT_TIERS;
|
||||||
|
|
||||||
|
try {
|
||||||
|
String content = readJson5(manifest);
|
||||||
|
JsonObject obj = JsonParser.parseString(content).getAsJsonObject();
|
||||||
|
if (obj.has("tiers") && obj.get("tiers").isJsonArray()) {
|
||||||
|
JsonArray arr = obj.getAsJsonArray("tiers");
|
||||||
|
List<String> out = new java.util.ArrayList<>();
|
||||||
|
for (JsonElement el : arr) out.add(el.getAsString());
|
||||||
|
if (!out.isEmpty()) return out;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("Failed to read root manifest " + manifest + ", falling back to default tiers", e);
|
||||||
|
}
|
||||||
|
return DEFAULT_TIERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> readFilesManifest(Path tierDir) {
|
||||||
|
Path manifest = firstExisting(tierDir, MANIFEST_NAMES);
|
||||||
|
if (manifest == null) return java.util.Collections.emptyList();
|
||||||
|
|
||||||
|
try {
|
||||||
|
String content = readJson5(manifest);
|
||||||
|
JsonObject obj = JsonParser.parseString(content).getAsJsonObject();
|
||||||
|
if (obj.has("files") && obj.get("files").isJsonArray()) {
|
||||||
|
JsonArray arr = obj.getAsJsonArray("files");
|
||||||
|
List<String> out = new java.util.ArrayList<>();
|
||||||
|
for (JsonElement el : arr) out.add(el.getAsString());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("Failed to read tier manifest " + manifest, e);
|
||||||
|
}
|
||||||
|
return java.util.Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path firstExisting(Path dir, List<String> names) {
|
||||||
|
for (String name : names) {
|
||||||
|
Path p = dir.resolve(name);
|
||||||
|
if (Files.exists(p)) return p;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a JSON or JSON5 file. Strips line and block comments and trailing
|
||||||
|
* commas so Gson can parse the result. String contents are preserved
|
||||||
|
* verbatim; comments embedded inside strings are not removed.
|
||||||
|
*/
|
||||||
|
private static String readJson5(Path path) throws IOException {
|
||||||
|
String raw = Files.readString(path, StandardCharsets.UTF_8);
|
||||||
|
return stripJson5(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String stripJson5(String content) {
|
||||||
|
if (content == null || content.isEmpty()) return content;
|
||||||
|
|
||||||
|
StringBuilder out = new StringBuilder(content.length());
|
||||||
|
int i = 0;
|
||||||
|
int len = content.length();
|
||||||
|
boolean inString = false;
|
||||||
|
char stringChar = 0;
|
||||||
|
boolean escape = false;
|
||||||
|
|
||||||
|
while (i < len) {
|
||||||
|
char c = content.charAt(i);
|
||||||
|
|
||||||
|
if (inString) {
|
||||||
|
out.append(c);
|
||||||
|
if (escape) {
|
||||||
|
escape = false;
|
||||||
|
} else if (c == '\\') {
|
||||||
|
escape = true;
|
||||||
|
} else if (c == stringChar) {
|
||||||
|
inString = false;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == '"' || c == '\'') {
|
||||||
|
inString = true;
|
||||||
|
stringChar = c;
|
||||||
|
out.append(c);
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == '/' && i + 1 < len) {
|
||||||
|
char next = content.charAt(i + 1);
|
||||||
|
if (next == '/') {
|
||||||
|
int eol = content.indexOf('\n', i + 2);
|
||||||
|
if (eol < 0) { i = len; break; }
|
||||||
|
i = eol;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (next == '*') {
|
||||||
|
int end = content.indexOf("*/", i + 2);
|
||||||
|
if (end < 0) { i = len; break; }
|
||||||
|
i = end + 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.append(c);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
String stripped = out.toString();
|
||||||
|
// Remove trailing commas before } or ]
|
||||||
|
stripped = stripped.replaceAll(",(\\s*[}\\]])", "$1");
|
||||||
|
return stripped;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the resolved location of the furnidata source: either a single
|
||||||
|
* file or a directory in split-layout mode.
|
||||||
|
*/
|
||||||
|
private static class ResolvedSource {
|
||||||
|
final Path path;
|
||||||
|
final boolean directory;
|
||||||
|
|
||||||
|
ResolvedSource(Path path, boolean directory) {
|
||||||
|
this.path = path;
|
||||||
|
this.directory = directory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the location of the furnidata source. Returns null if no
|
||||||
|
* candidate can be found.
|
||||||
|
*/
|
||||||
|
private static ResolvedSource resolveSource() {
|
||||||
try {
|
try {
|
||||||
String configPath = Emulator.getConfig().getValue("furni.editor.renderer.config.path", "");
|
String configPath = Emulator.getConfig().getValue("furni.editor.renderer.config.path", "");
|
||||||
|
|
||||||
if (configPath.isEmpty()) {
|
if (configPath.isEmpty()) {
|
||||||
// Fallback: try common locations
|
Path fallback = fallbackToBasePath();
|
||||||
String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
|
return fallback != null ? new ResolvedSource(fallback, Files.isDirectory(fallback)) : null;
|
||||||
if (!basePath.isEmpty()) {
|
|
||||||
Path candidate = Paths.get(basePath, "FurnitureData.json");
|
|
||||||
if (Files.exists(candidate)) return candidate;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the renderer config to find the furnidata URL/path
|
|
||||||
Path rendererConfig = Paths.get(configPath);
|
Path rendererConfig = Paths.get(configPath);
|
||||||
if (!Files.exists(rendererConfig)) return null;
|
if (!Files.exists(rendererConfig)) return null;
|
||||||
|
|
||||||
String rendererContent = Files.readString(rendererConfig, StandardCharsets.UTF_8);
|
String rendererContent = readJson5(rendererConfig);
|
||||||
JsonObject rendererObj = JsonParser.parseString(rendererContent).getAsJsonObject();
|
JsonObject rendererObj = JsonParser.parseString(rendererContent).getAsJsonObject();
|
||||||
|
|
||||||
if (rendererObj.has("furnidata.url")) {
|
if (!rendererObj.has("furnidata.url")) return null;
|
||||||
String furniUrl = rendererObj.get("furnidata.url").getAsString();
|
|
||||||
|
|
||||||
// Skip unresolved placeholders like ${gamedata.url}
|
String furniUrl = rendererObj.get("furnidata.url").getAsString();
|
||||||
if (furniUrl.contains("${")) {
|
|
||||||
String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
|
|
||||||
if (!basePath.isEmpty()) {
|
|
||||||
Path candidate = Paths.get(basePath, "FurnitureData.json");
|
|
||||||
if (Files.exists(candidate)) return candidate;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip query string (?v=1 etc.)
|
if (furniUrl.contains("${")) {
|
||||||
String cleanUrl = furniUrl.contains("?") ? furniUrl.substring(0, furniUrl.indexOf('?')) : furniUrl;
|
Path fallback = fallbackToBasePath();
|
||||||
|
return fallback != null ? new ResolvedSource(fallback, Files.isDirectory(fallback)) : null;
|
||||||
// If it's a local file path (not http), use it directly
|
|
||||||
if (!cleanUrl.startsWith("http")) {
|
|
||||||
return Paths.get(cleanUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For http URLs, try to derive local path from base path
|
|
||||||
String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
|
|
||||||
if (!basePath.isEmpty()) {
|
|
||||||
// Extract filename from URL (without query string)
|
|
||||||
String filename = cleanUrl.substring(cleanUrl.lastIndexOf('/') + 1);
|
|
||||||
return Paths.get(basePath, filename);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Strip query string and fragment (e.g. ?v=123 or #anchor)
|
||||||
|
String cleanUrl = furniUrl;
|
||||||
|
int q = cleanUrl.indexOf('?');
|
||||||
|
if (q >= 0) cleanUrl = cleanUrl.substring(0, q);
|
||||||
|
int h = cleanUrl.indexOf('#');
|
||||||
|
if (h >= 0) cleanUrl = cleanUrl.substring(0, h);
|
||||||
|
|
||||||
|
boolean splitMode = cleanUrl.endsWith("/");
|
||||||
|
|
||||||
|
// Local file path (not http) — return as-is, the caller will check
|
||||||
|
// whether it points at a file or a directory.
|
||||||
|
if (!cleanUrl.startsWith("http")) {
|
||||||
|
Path local = Paths.get(cleanUrl);
|
||||||
|
return new ResolvedSource(local, splitMode || Files.isDirectory(local));
|
||||||
|
}
|
||||||
|
|
||||||
|
String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
|
||||||
|
if (basePath.isEmpty()) return null;
|
||||||
|
|
||||||
|
if (splitMode) {
|
||||||
|
// Derive the directory name from the URL: take the last non-empty
|
||||||
|
// segment before the trailing slash. e.g. https://x/y/furnidata/ -> "furnidata"
|
||||||
|
String trimmed = cleanUrl.endsWith("/") ? cleanUrl.substring(0, cleanUrl.length() - 1) : cleanUrl;
|
||||||
|
String dirName = trimmed.substring(trimmed.lastIndexOf('/') + 1);
|
||||||
|
Path candidate = Paths.get(basePath, dirName);
|
||||||
|
return new ResolvedSource(candidate, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
String filename = cleanUrl.substring(cleanUrl.lastIndexOf('/') + 1);
|
||||||
|
Path candidate = Paths.get(basePath, filename);
|
||||||
|
return new ResolvedSource(candidate, false);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOGGER.warn("Failed to resolve FurnitureData.json path", e);
|
LOGGER.warn("Failed to resolve FurnitureData source", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Path fallbackToBasePath() {
|
||||||
|
String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
|
||||||
|
if (basePath.isEmpty()) return null;
|
||||||
|
Path dir = Paths.get(basePath);
|
||||||
|
// Prefer the split layout if it exists, then the legacy file.
|
||||||
|
Path splitCandidate = dir.resolve("furnidata");
|
||||||
|
if (Files.isDirectory(splitCandidate)) return splitCandidate;
|
||||||
|
Path legacy = dir.resolve("FurnitureData.json");
|
||||||
|
if (Files.exists(legacy)) return legacy;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -104,7 +104,7 @@ public class SecureLoginEvent extends MessageHandler {
|
|||||||
// First, look up the user ID to check for ghost sessions
|
// First, look up the user ID to check for ghost sessions
|
||||||
int lookupUserId = 0;
|
int lookupUserId = 0;
|
||||||
try (java.sql.Connection conn = Emulator.getDatabase().getDataSource().getConnection();
|
try (java.sql.Connection conn = Emulator.getDatabase().getDataSource().getConnection();
|
||||||
java.sql.PreparedStatement stmt = conn.prepareStatement("SELECT id FROM users WHERE auth_ticket = ? LIMIT 1")) {
|
java.sql.PreparedStatement stmt = conn.prepareStatement("SELECT id FROM users WHERE auth_ticket = ? AND (auth_ticket_expires_at IS NULL OR auth_ticket_expires_at >= NOW()) LIMIT 1")) {
|
||||||
stmt.setString(1, sso);
|
stmt.setString(1, sso);
|
||||||
try (java.sql.ResultSet rs = stmt.executeQuery()) {
|
try (java.sql.ResultSet rs = stmt.executeQuery()) {
|
||||||
if (rs.next()) {
|
if (rs.next()) {
|
||||||
|
|||||||
+2
-2
@@ -50,7 +50,7 @@ final class SessionEndpoints {
|
|||||||
|
|
||||||
if (ssoTicket != null && !ssoTicket.isEmpty()) {
|
if (ssoTicket != null && !ssoTicket.isEmpty()) {
|
||||||
try (PreparedStatement lookup = conn.prepareStatement(
|
try (PreparedStatement lookup = conn.prepareStatement(
|
||||||
"SELECT id FROM users WHERE auth_ticket = ? LIMIT 1")) {
|
"SELECT id FROM users WHERE auth_ticket = ? AND (auth_ticket_expires_at IS NULL OR auth_ticket_expires_at >= NOW()) LIMIT 1")) {
|
||||||
lookup.setString(1, ssoTicket);
|
lookup.setString(1, ssoTicket);
|
||||||
try (ResultSet rs = lookup.executeQuery()) {
|
try (ResultSet rs = lookup.executeQuery()) {
|
||||||
if (rs.next()) userId = rs.getInt("id");
|
if (rs.next()) userId = rs.getInt("id");
|
||||||
@@ -134,7 +134,7 @@ final class SessionEndpoints {
|
|||||||
|
|
||||||
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
|
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
|
||||||
PreparedStatement lookup = conn.prepareStatement(
|
PreparedStatement lookup = conn.prepareStatement(
|
||||||
"SELECT id, username FROM users WHERE auth_ticket = ? LIMIT 1")) {
|
"SELECT id, username FROM users WHERE auth_ticket = ? AND (auth_ticket_expires_at IS NULL OR auth_ticket_expires_at >= NOW()) LIMIT 1")) {
|
||||||
lookup.setString(1, ssoTicket);
|
lookup.setString(1, ssoTicket);
|
||||||
try (ResultSet rs = lookup.executeQuery()) {
|
try (ResultSet rs = lookup.executeQuery()) {
|
||||||
if (!rs.next()) {
|
if (!rs.next()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user