Skip to content

Commit 6fcbefe

Browse files
committed
Keep FlatFile players unloaded when their data cannot be read at login
When a player's profile did not load at login, the loading task asked the database for a new user. On FlatFile, newUser(Player) always returned a loaded profile at starting levels, and saveUser writes over any row with that player's UUID. A users file that could not be read at that moment, or a row whose cooldowns could not be parsed, therefore wiped the player's stored progress on the first save. newUser(Player) now reads mcmmo.users and only returns a loaded profile when the file was read and has no row with the player's UUID. Otherwise the profile stays unloaded and the login keeps retrying the load, the same way the SQL database behaves when its insert fails. newUser(String, UUID) no longer appends a second row for a UUID that is already stored. The load queries skip a row that fails to parse and use the next row with the same UUID, so a fresh duplicate would be loaded in place of a damaged original and then saved over both. A row that fails to parse is now logged once per player, naming them, instead of the exception being swallowed. Rows that end before the newer XP columns load with those columns at zero, as the startup health check pads them, instead of failing to load.
1 parent 584fa08 commit 6fcbefe

4 files changed

Lines changed: 660 additions & 47 deletions

File tree

‎Changelog.txt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
Version 2.3.002
22
Fixed mcMMO failing to start on Spigot 26.3 servers
33
Fixed a read error while updating the FlatFile database overwriting mcmmo.users with only the players read before the error (Thanks cyrne1-7208)
4+
Fixed FlatFile players losing their progress when their data could not be read as they joined (See notes)
45
Fixed FlatFile database saves reporting success when mcmmo.users could not be written
56
Improved performance of repairing items on an anvil
67
Lowered the default Pale_Hanging_Moss XP for Herbalism in experience.yml (See notes)
7-
(API) DatabaseManager.newUser(String, UUID) on the FlatFile database now returns an unloaded profile when mcmmo.users cannot be read, matching the SQL database
8+
(API) DatabaseManager.newUser(Player) and newUser(String, UUID) on the FlatFile database now return an unloaded profile when mcmmo.users cannot be read or already has a row with that UUID
89
(Codebase) Crossbow ricochet reflection no longer modifies the arrow velocity and block face normal vectors it is given (Thanks cyrne1-7208)
910

1011
NOTES:
1112
We believe this is a bug in Spigot 26.3 rather than an intended API change, the error comes from Spigot itself whenever a plugin looks through the server's recipes. mcMMO now works around it on our side so it keeps running until Spigot fixes it.
1213
If you run Spigot 26.3 you need this update, no config changes are required.
14+
A FlatFile player no longer starts over with a new profile when mcmmo.users can't be read as they join, or when their row in it can't be loaded. mcMMO keeps retrying the load instead, and a damaged row is logged once with the player's name. Restarting the server repairs a damaged row by resetting the values that can't be read. This never affected SQL databases, SQL already keeps the player unloaded and retries the load in this situation.
1315
To get the lower Pale_Hanging_Moss XP, set 'Experience_Values.Herbalism.Pale_Hanging_Moss' to 1 in experience.yml, or delete experience.yml to generate a new one.
1416

1517
Version 2.3.001

‎src/main/java/com/gmail/nossr50/database/FlatFileDatabaseManager.java‎

Lines changed: 85 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@
2828
import java.util.HashMap;
2929
import java.util.List;
3030
import java.util.Map;
31+
import java.util.Set;
3132
import java.util.TreeSet;
3233
import java.util.UUID;
34+
import java.util.concurrent.ConcurrentHashMap;
3335
import java.util.concurrent.atomic.AtomicLong;
3436
import java.util.function.Consumer;
3537
import java.util.function.Function;
@@ -57,6 +59,7 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
5759
// check-then-act window and performing duplicate full-file scans. Only a successful rebuild
5860
// keeps the claimed timestamp; failures roll it back so retries are not throttled.
5961
private final @NotNull AtomicLong lastUpdate = new AtomicLong(0L);
62+
private final @NotNull Set<UUID> reportedUnloadableRows = ConcurrentHashMap.newKeySet();
6063

6164
private final @NotNull String usersFilePath;
6265
private final @NotNull File usersFile;
@@ -540,8 +543,45 @@ public void writeUserToLine(@NotNull PlayerProfile profile,
540543
out.append(LINE_ENDING);
541544
}
542545

546+
/**
547+
* Called when the player's profile did not load. The profile returned here is saved over
548+
* their stored row, so it is only loaded when the file was read and has no row with their
549+
* UUID. Otherwise it is unloaded, and the login retries the load.
550+
*/
543551
public @NotNull PlayerProfile newUser(@NotNull Player player) {
544-
return new PlayerProfile(player.getName(), player.getUniqueId(), true, startingLevel);
552+
final UUID uuid = player.getUniqueId();
553+
return new PlayerProfile(player.getName(), uuid, isConfirmedNewPlayer(uuid),
554+
startingLevel);
555+
}
556+
557+
/** True only when the users file was read and has no row for {@code uuid}. */
558+
private boolean isConfirmedNewPlayer(@NotNull UUID uuid) {
559+
synchronized (fileWritingLock) {
560+
try (BufferedReader in = newBufferedReader()) {
561+
String line;
562+
while ((line = in.readLine()) != null) {
563+
if (isRowFor(line, uuid)) {
564+
return false;
565+
}
566+
}
567+
return true;
568+
} catch (IOException e) {
569+
logger.severe("Could not read " + usersFilePath + " to look for " + uuid + ": "
570+
+ e);
571+
return false;
572+
}
573+
}
574+
}
575+
576+
/** Whether {@code line} is a row for {@code uuid}, matched the way the load queries match. */
577+
private boolean isRowFor(@NotNull String line, @NotNull UUID uuid) {
578+
if (line.startsWith("#")) {
579+
return false;
580+
}
581+
582+
// Nothing past the UUID is needed, so the rest of the row stays unsplit
583+
final String[] fields = line.split(":", UUID_INDEX + 2);
584+
return fields.length > UUID_INDEX && uuid.equals(parseUuidOrNull(fields[UUID_INDEX]));
545585
}
546586

547587
public @NotNull PlayerProfile newUser(@NotNull String playerName, @NotNull UUID uuid) {
@@ -553,6 +593,13 @@ public void writeUserToLine(@NotNull PlayerProfile profile,
553593
try (BufferedReader bufferedReader = newBufferedReader()) {
554594
String line;
555595
while ((line = bufferedReader.readLine()) != null) {
596+
// A second row would be loaded instead of a first that fails to parse,
597+
// and the next save would overwrite both
598+
if (isRowFor(line, uuid)) {
599+
logger.warning("Not adding " + playerName + " (" + uuid + ") to "
600+
+ usersFilePath + ", it already has a row for that UUID");
601+
return new PlayerProfile(playerName, uuid, false, startingLevel);
602+
}
556603
stringBuilder.append(line).append(LINE_ENDING);
557604
}
558605
} catch (IOException e) {
@@ -653,17 +700,15 @@ public void writeUserToLine(@NotNull PlayerProfile profile,
653700

654701
String[] rawSplitData = line.split(":");
655702

656-
if (rawSplitData.length < (UUID_INDEX + 1)) {
703+
if (rawSplitData.length < (UUID_INDEX + 1)
704+
|| !uuid.equals(parseUuidOrNull(rawSplitData[UUID_INDEX]))) {
657705
continue;
658706
}
659707

660708
try {
661-
UUID fromDataUUID = UUID.fromString(rawSplitData[UUID_INDEX]);
662-
if (fromDataUUID.equals(uuid)) {
663-
return loadFromLine(rawSplitData);
664-
}
665-
} catch (Exception e) {
666-
// Ignore malformed UUIDs
709+
return loadFromLine(rawSplitData);
710+
} catch (RuntimeException e) {
711+
logUnloadableRow(rawSplitData[USERNAME_INDEX], uuid, e);
667712
}
668713
}
669714
} catch (Exception e) {
@@ -690,28 +735,25 @@ public void writeUserToLine(@NotNull PlayerProfile profile,
690735

691736
String[] rawSplitData = line.split(":");
692737

693-
if (rawSplitData.length < (UUID_INDEX + 1)) {
738+
if (rawSplitData.length < (UUID_INDEX + 1)
739+
|| !uuid.equals(parseUuidOrNull(rawSplitData[UUID_INDEX]))) {
694740
continue;
695741
}
696742

743+
final String dbPlayerName = rawSplitData[USERNAME_INDEX];
744+
final boolean matchingName = dbPlayerName.equalsIgnoreCase(playerName);
745+
746+
if (!matchingName) {
747+
logger.warning("When loading user: " + playerName + " with UUID of ("
748+
+ uuid + ") we found a mismatched name, the name in the DB will"
749+
+ " be replaced (DB name: " + dbPlayerName + ")");
750+
rawSplitData[USERNAME_INDEX] = playerName;
751+
}
752+
697753
try {
698-
UUID fromDataUUID = UUID.fromString(rawSplitData[UUID_INDEX]);
699-
if (fromDataUUID.equals(uuid)) {
700-
String dbPlayerName = rawSplitData[USERNAME_INDEX];
701-
boolean matchingName = dbPlayerName.equalsIgnoreCase(playerName);
702-
703-
if (!matchingName) {
704-
logger.warning(
705-
"When loading user: " + playerName + " with UUID of ("
706-
+ uuid + ") we found a mismatched name, the name in the DB will be replaced (DB name: "
707-
+ dbPlayerName + ")");
708-
rawSplitData[USERNAME_INDEX] = playerName;
709-
}
710-
711-
return loadFromLine(rawSplitData);
712-
}
713-
} catch (Exception e) {
714-
// Ignore malformed UUIDs
754+
return loadFromLine(rawSplitData);
755+
} catch (RuntimeException e) {
756+
logUnloadableRow(playerName, uuid, e);
715757
}
716758
}
717759
} catch (IOException e) {
@@ -723,6 +765,21 @@ public void writeUserToLine(@NotNull PlayerProfile profile,
723765
return grabUnloadedProfile(uuid, playerName);
724766
}
725767

768+
/**
769+
* Logged once per player, since the login retries and API lookups repeat the load. Callers
770+
* keep scanning afterwards, as a later row for the same player may still load.
771+
*/
772+
private void logUnloadableRow(@NotNull String playerName, @NotNull UUID uuid,
773+
@NotNull RuntimeException cause) {
774+
if (!reportedUnloadableRows.add(uuid)) {
775+
return;
776+
}
777+
778+
logger.log(Level.SEVERE, "Could not load " + playerName + " (" + uuid + ") from "
779+
+ usersFilePath + ", their row has data that cannot be read. They will not"
780+
+ " load until it is fixed, restarting the server repairs it.", cause);
781+
}
782+
726783
private @NotNull PlayerProfile grabUnloadedProfile(@NotNull UUID uuid,
727784
@Nullable String playerName) {
728785
String name = (playerName == null) ? "" : playerName;
@@ -1221,6 +1278,8 @@ private void tryLoadSkillFloatValuesFromRawData(@NotNull Map<PrimarySkillType, F
12211278
try {
12221279
float valueFromString = Integer.parseInt(character[index]);
12231280
skillMap.put(primarySkillType, valueFromString);
1281+
} catch (ArrayIndexOutOfBoundsException e) {
1282+
skillMap.put(primarySkillType, 0F);
12241283
} catch (NumberFormatException e) {
12251284
skillMap.put(primarySkillType, 0F);
12261285
logger.severe("Data corruption when trying to load the value for skill "

0 commit comments

Comments
 (0)