Merge branch 'mcMMO-Dev:master' into master

This commit is contained in:
albert4719
2026-06-07 12:04:32 +08:00
committed by GitHub
9 changed files with 229 additions and 25 deletions

View File

@ -1,3 +1,7 @@
Version 2.2.054
Fixed party/admin chat allowing player color code tokens without the 'mcmmo.chat.colors' permission
Fixed diminished returns resetting in certain situations when players reconnected
Version 2.2.053
!! -- This build has important fixes for anyone using Paper (or forks of Paper), please read the notes carefully.
It is completely safe to update to this version of mcMMO.
@ -11,7 +15,6 @@ Version 2.2.053
Fixed KnockOnWood XP orbs never spawning on nether/warped tree cap blocks during Tree Feller
Fixed Impale (Tridents) damage bonus formula applying one fewer rank of the multiplier than intended
Fixed melee attack strength scale resolving to near-zero after Paper fixed a vanilla attack cooldown bug in 26.1.2 (See notes)
Fixed server-side diminished returns state being evicted too early, allowing reconnects to bypass the DR window (See notes)
Added option to allow Magic Hunter (Fishing) to grant items with conflicting enchantments; disabled by default (Thanks Warriorrrr)
Added 'Skills.Fishing.Allow_Conflicting_Enchants' to config.yml (Thanks Warriorrrr)
(Codebase) Removed 27 dead JSON.* locale keys from all locale files (See notes)
@ -5523,3 +5526,4 @@ Version 0.1
Releasing my awesome plugin

View File

@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.gmail.nossr50.mcMMO</groupId>
<artifactId>mcMMO</artifactId>
<version>2.2.053-SNAPSHOT</version>
<version>2.2.053</version>
<name>mcMMO</name>
<url>https://github.com/mcMMO-Dev/mcMMO</url>
<scm>

View File

@ -1,5 +1,9 @@
package com.gmail.nossr50.chat.mailer;
import com.gmail.nossr50.locale.LocaleLoader;
import com.gmail.nossr50.util.text.TextUtils;
import java.util.UUID;
import net.kyori.adventure.text.TextComponent;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
@ -10,4 +14,21 @@ public abstract class AbstractChatMailer implements ChatMailer {
public AbstractChatMailer(@NotNull Plugin pluginRef) {
this.pluginRef = pluginRef;
}
protected final @NotNull TextComponent formatLocaleStyleWithLiteralMessage(
@NotNull String localeKey,
@NotNull String authoredName,
@NotNull String literalMessage) {
final String messageStartMarker = createLiteralMessageMarker("START");
final String messageEndMarker = createLiteralMessageMarker("END");
final String formattedTemplate = LocaleLoader.getString(localeKey, authoredName,
messageStartMarker + messageEndMarker);
return TextUtils.insertLiteralTextAtMarkers(formattedTemplate, messageStartMarker,
messageEndMarker, literalMessage);
}
private static @NotNull String createLiteralMessageMarker(@NotNull String markerRole) {
return "\u0002MCMMO_" + markerRole + "_" + UUID.randomUUID() + "\u0003";
}
}

View File

@ -63,9 +63,9 @@ public class AdminChatMailer extends AbstractChatMailer {
"Chat.Style.Admin", author.getAuthoredName(ChatChannel.ADMIN),
message);
} else {
return TextUtils.ofLegacyTextRaw(
LocaleLoader.getString("Chat.Style.Admin",
author.getAuthoredName(ChatChannel.ADMIN), message));
final String literalMessage = TextUtils.literalizeLegacyColorCodes(message);
return formatLocaleStyleWithLiteralMessage("Chat.Style.Admin",
author.getAuthoredName(ChatChannel.ADMIN), literalMessage);
}
}

View File

@ -77,15 +77,14 @@ public class PartyChatMailer extends AbstractChatMailer {
message);
}
} else {
final String literalMessage = TextUtils.literalizeLegacyColorCodes(message);
if (isLeader) {
return TextUtils.ofLegacyTextRaw(
LocaleLoader.getString(
"Chat.Style.Party.Leader",
author.getAuthoredName(ChatChannel.PARTY), message));
return formatLocaleStyleWithLiteralMessage("Chat.Style.Party.Leader",
author.getAuthoredName(ChatChannel.PARTY), literalMessage);
} else {
return TextUtils.ofLegacyTextRaw(
LocaleLoader.getString("Chat.Style.Party",
author.getAuthoredName(ChatChannel.PARTY), message));
return formatLocaleStyleWithLiteralMessage("Chat.Style.Party",
author.getAuthoredName(ChatChannel.PARTY), literalMessage);
}
}
}

View File

@ -98,8 +98,7 @@ public class PlayerProfile {
this.playerName = playerName;
this.uuid = uuid;
this.scoreboardTipsShown = scoreboardTipsShown;
// This constructor is used for save copies only — do not pull DR state from cache.
this.diminishedReturnsState = DiminishedReturnsCache.getOrCreate(null);
this.diminishedReturnsState = DiminishedReturnsCache.getOrCreate(uuid);
skills.putAll(levelData);
skillsXp.putAll(xpData);

View File

@ -136,4 +136,51 @@ public class TextUtils {
TextComponent componentForm = ofLegacyTextRaw(string);
return customLegacySerializer.serialize(componentForm);
}
/**
* Inserts literal message text between two unique markers in a pre-formatted legacy template.
*
* <p>Everything before and after the marker pair is parsed as legacy text, while the inserted
* message is kept literal so legacy color tokens are not interpreted.
*/
public static @NotNull TextComponent insertLiteralTextAtMarkers(
@NotNull String formattedTemplate,
@NotNull String startMarker,
@NotNull String endMarker,
@NotNull String literalMessage) {
final int startMarkerIndex = formattedTemplate.indexOf(startMarker);
if (startMarkerIndex < 0) {
return ofLegacyTextRaw(formattedTemplate);
}
final int afterStartMarkerIndex = startMarkerIndex + startMarker.length();
final int endMarkerIndex = formattedTemplate.indexOf(endMarker, afterStartMarkerIndex);
if (endMarkerIndex < 0) {
return Component.text()
.append(ofLegacyTextRaw(formattedTemplate.replace(startMarker, "")))
.append(Component.text(literalMessage))
.build();
}
final String prefixText = formattedTemplate.substring(0, startMarkerIndex);
final String suffixText = formattedTemplate.substring(endMarkerIndex + endMarker.length());
return Component.text()
.append(ofLegacyTextRaw(prefixText))
.append(Component.text(literalMessage))
.append(ofLegacyTextRaw(suffixText))
.build();
}
/**
* Converts section-sign legacy color prefixes to ampersands so formatting tokens are displayed
* as plain text instead of being applied.
*
* <p>This is intended for message inputs when a sender lacks permission to use chat colors.
*/
public static @NotNull String literalizeLegacyColorCodes(@NotNull String text) {
return text.replace('\u00A7', '&');
}
}

View File

@ -11,7 +11,13 @@ import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import com.gmail.nossr50.config.experience.ExperienceConfig;
import com.gmail.nossr50.datatypes.player.PlayerProfile;
import com.gmail.nossr50.datatypes.player.UniqueDataType;
import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
import com.gmail.nossr50.datatypes.skills.SuperAbilityType;
import java.lang.reflect.Field;
import java.util.EnumMap;
import java.util.Map;
import java.util.UUID;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@ -288,4 +294,29 @@ class DiminishedReturnsCacheTest {
"state with a future latestExpiryTimeMillis should report active entries");
}
}
@Nested
class PlayerProfileConstructorIntegration {
@Test
void mapBasedConstructorShouldUseUuidCacheState() {
// Given - a player UUID with an existing cached DR state
final UUID playerUuid = UUID.randomUUID();
final DiminishedReturnsState cachedState = DiminishedReturnsCache.getOrCreate(playerUuid);
final Map<PrimarySkillType, Integer> levelData = new EnumMap<>(PrimarySkillType.class);
final Map<PrimarySkillType, Float> xpData = new EnumMap<>(PrimarySkillType.class);
final Map<SuperAbilityType, Integer> cooldownData = new EnumMap<>(SuperAbilityType.class);
final Map<UniqueDataType, Integer> uniqueData = new EnumMap<>(UniqueDataType.class);
// When - profile is created using the map-based constructor used by DB loaders
final PlayerProfile loadedProfile = new PlayerProfile("TestPlayer", playerUuid,
levelData, xpData, cooldownData, 0, uniqueData, null);
loadedProfile.registerXpGain(PrimarySkillType.MINING, 50F);
// Then - cached state should reflect the XP registered by that loaded profile
assertEquals(50F, cachedState.getRegisteredXpGain(PrimarySkillType.MINING),
"loaded profiles with UUID must use the UUID-cached DR state");
}
}
}

View File

@ -1,8 +1,13 @@
package com.gmail.nossr50.util.text;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.ArrayList;
import java.util.List;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
/**
@ -15,17 +20,115 @@ import org.junit.jupiter.api.Test;
*/
class TextUtilsTest {
@Test
void testColorizeText() {
String inputText = "&4This text should be red.";
@Nested
class ColorizeText {
/*
* If this method raises an exception, we know Adventure is not set up correctly.
* This will also make the test fail and warn us about it.
*/
TextComponent component = TextUtils.colorizeText(inputText);
@Test
void parsesLegacyColorCodes() {
// Given
final String inputText = "&4This text should be red.";
String message = "Looks like Adventure is not working correctly. We likely need to update our dependency!";
Assertions.assertEquals(NamedTextColor.DARK_RED, component.color(), message);
// When
final TextComponent component = TextUtils.colorizeText(inputText);
// Then
final String failureMessage = "Looks like Adventure is not working correctly. We likely need to update our dependency!";
assertThat(component.color()).withFailMessage(failureMessage)
.isEqualTo(NamedTextColor.DARK_RED);
}
}
@Nested
class LiteralizeLegacyColorCodes {
@Test
void keepsAmpersandFormattingCodesLiteral() {
// Given
final String rawMessage = "&aHello &lWorld&r!";
// When
final String literalizedMessage = TextUtils.literalizeLegacyColorCodes(rawMessage);
// Then
assertThat(literalizedMessage).isEqualTo("&aHello &lWorld&r!");
}
@Test
void convertsSectionSignFormattingCodesToAmpersands() {
// Given
final String rawMessage = "\u00A7aHello \u00A7lWorld\u00A7r!";
// When
final String literalizedMessage = TextUtils.literalizeLegacyColorCodes(rawMessage);
// Then
assertThat(literalizedMessage).isEqualTo("&aHello &lWorld&r!");
}
@Test
void preservesHexTokensAndNormalAmpersandsAsLiteralText() {
// Given
final String rawMessage = "Color: &#12AB9FHello & welcome";
// When
final String literalizedMessage = TextUtils.literalizeLegacyColorCodes(rawMessage);
// Then
assertThat(literalizedMessage).isEqualTo("Color: &#12AB9FHello & welcome");
}
}
@Nested
class InsertLiteralTextAtMarkers {
@Test
void preservesLiteralColorTokensInInsertedMessage() {
// Given
final String startMarker = "\u0002START\u0003";
final String endMarker = "\u0002END\u0003";
final String template = "\u00A7aPrefix " + startMarker + endMarker + " \u00A7bSuffix";
final String literalMessage = "&cMessage &lTokens";
// When
final TextComponent formattedComponent = TextUtils.insertLiteralTextAtMarkers(template,
startMarker, endMarker, literalMessage);
final String aggregatedContent = aggregateTextContent(formattedComponent);
// Then
assertThat(aggregatedContent).isEqualTo("Prefix &cMessage &lTokens Suffix");
}
@Test
void onlyReplacesTextBetweenMarkerBoundaries() {
// Given
final String startMarker = "\u0002START\u0003";
final String endMarker = "\u0002END\u0003";
final String template = "Header START " + startMarker + endMarker + " END Footer";
final String literalMessage = "payload";
// When
final TextComponent formattedComponent = TextUtils.insertLiteralTextAtMarkers(template,
startMarker, endMarker, literalMessage);
final String aggregatedContent = aggregateTextContent(formattedComponent);
// Then
assertThat(aggregatedContent).isEqualTo("Header START payload END Footer");
}
private String aggregateTextContent(final Component rootComponent) {
final List<String> parts = new ArrayList<>();
collectTextContent(rootComponent, parts);
return String.join("", parts);
}
private void collectTextContent(final Component component, final List<String> parts) {
if (component instanceof TextComponent textComponent) {
parts.add(textComponent.content());
}
for (final Component childComponent : component.children()) {
collectTextContent(childComponent, parts);
}
}
}
}