Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.hibiscusmc.hmccosmetics.packets.CosmeticPacketInterface;
import com.hibiscusmc.hmccosmetics.user.CosmeticUser;
import com.hibiscusmc.hmccosmetics.user.CosmeticUsers;
import com.hibiscusmc.hmccosmetics.user.manager.BalloonSmoothingTask;
import com.hibiscusmc.hmccosmetics.util.search.PlayerSearchManager;
import com.hibiscusmc.hmccosmetics.util.MessagesUtil;
import com.hibiscusmc.hmccosmetics.util.TranslationUtil;
Expand Down Expand Up @@ -53,11 +54,16 @@

public final class HMCCosmeticsPlugin extends HibiscusPlugin {

/** Bump whenever an existing config key changes meaning, and add the matching step to migrateConfig. */
private static final int CONFIG_VERSION = 2;

private static HMCCosmeticsPlugin instance;
private static YamlConfigurationLoader configLoader;

@Getter
private PlayerSearchManager playerSearchManager;
@Getter
private final BalloonSmoothingTask balloonSmoothingTask = new BalloonSmoothingTask();

public HMCCosmeticsPlugin() {
super(13873, 1879);
Expand Down Expand Up @@ -170,6 +176,8 @@ public void onLoad() {
@Override
public void onEnd() {
// Plugin shutdown logic
balloonSmoothingTask.stop();
com.hibiscusmc.hmccosmetics.util.BalloonStressTest.stop();
for (Player player : Bukkit.getOnlinePlayers()) {
CosmeticUser user = CosmeticUsers.getUser(player);
if (user == null) continue;
Expand All @@ -186,6 +194,7 @@ public static HMCCosmeticsPlugin getInstance() {

public static void setup() {
getInstance().reloadConfig();
migrateConfig();

// Configuration setup
final File file = Path.of(getInstance().getDataFolder().getPath(), "config.yml").toFile();
Expand Down Expand Up @@ -271,6 +280,33 @@ public static void setup() {
getInstance().getLogger().info(WardrobeSettings.getWardrobes().size() + " Wardrobes Successfully Setup");
getInstance().getLogger().info("Data storage is set to " + DatabaseSettings.getDatabaseType());

// Runs here rather than in onStart so /hmccosmetics reload picks up a changed balloon-lerp-period.
// start() cancels any previous task first, so calling it again is safe.
getInstance().getBalloonSmoothingTask().start(getInstance());

Bukkit.getPluginManager().callEvent(new HMCCosmeticSetupEvent());
}

/**
* Brings an existing config.yml forward to {@link #CONFIG_VERSION}. The comment sync run on enable adds
* keys that are missing but never rewrites ones that are present, so a shipped default that changes
* meaning between versions has to be migrated here or upgraded servers silently keep the old behaviour.
*/
private static void migrateConfig() {
final var config = getInstance().getConfig();
final int version = config.getInt("config-version", 0);
// 0 means the key is absent entirely, which Settings already reports as a broken config. Rewriting
// something we can't identify would do more harm than leaving it be.
if (version <= 0 || version >= CONFIG_VERSION) return;

if (version < 2) {
// balloon-head-unmoving flipped to true alongside the balloon smoothing rework: the smoothing
// task drives the balloon's own pitch, so mirroring the player's head pitch fights it.
config.set("cosmetic-settings.balloon-head-unmoving", true);
}

config.set("config-version", CONFIG_VERSION);
getInstance().saveConfig();
getInstance().getLogger().info("Migrated config.yml from config-version " + version + " to " + CONFIG_VERSION);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
import com.hibiscusmc.hmccosmetics.cosmetic.Cosmetic;
import com.hibiscusmc.hmccosmetics.cosmetic.CosmeticSlot;
import com.hibiscusmc.hmccosmetics.cosmetic.Cosmetics;
import com.hibiscusmc.hmccosmetics.cosmetic.types.CosmeticBalloonType;
import com.hibiscusmc.hmccosmetics.database.Database;
import com.hibiscusmc.hmccosmetics.gui.Menu;
import com.hibiscusmc.hmccosmetics.gui.Menus;
import com.hibiscusmc.hmccosmetics.gui.special.DyeMenuProvider;
import com.hibiscusmc.hmccosmetics.user.CosmeticUser;
import com.hibiscusmc.hmccosmetics.user.CosmeticUsers;
import com.hibiscusmc.hmccosmetics.util.BalloonStressTest;
import com.hibiscusmc.hmccosmetics.util.MessagesUtil;
import com.hibiscusmc.hmccosmetics.util.HMCCServerUtils;
import me.lojosho.hibiscuscommons.HibiscusCommonsPlugin;
Expand Down Expand Up @@ -572,6 +574,44 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command
user.clearHiddenReasons();
return true;
}

case "stresstest" -> {
if (!sender.hasPermission("hmccosmetics.cmd.stresstest") && !sender.isOp()) {
if (!silent) MessagesUtil.sendMessage(sender, "no-permission");
return true;
}
if (args.length >= 2 && args[1].equalsIgnoreCase("stop")) {
BalloonStressTest.stop();
sender.sendMessage("Balloon stress test stopped.");
return true;
}
if (player == null) {
sender.sendMessage("Run /hmccosmetics stresstest as a player (it spawns balloons at your location).");
return true;
}
int count = 100;
if (args.length >= 2) {
try {
count = Math.max(1, Math.min(2000, Integer.parseInt(args[1])));
} catch (NumberFormatException e) {
sender.sendMessage("Usage: /hmccosmetics stresstest <count|stop>");
return true;
}
}
// Clone the balloon the caller has equipped so each stress entity renders the real
// model (ModelEngine or item), not a placeholder. Falls back to a visible helmet if none.
CosmeticUser stressUser = CosmeticUsers.getUser(player.getUniqueId());
CosmeticBalloonType balloon = stressUser != null
&& stressUser.getCosmetic(CosmeticSlot.BALLOON) instanceof CosmeticBalloonType b ? b : null;
ItemStack fallback = player.getInventory().getItemInMainHand();
if (fallback.getType().isAir()) fallback = new ItemStack(org.bukkit.Material.CARVED_PUMPKIN);
BalloonStressTest.start(HMCCosmeticsPlugin.getInstance(), player.getLocation(), count, balloon, fallback, sender);
sender.sendMessage("Spawned " + count + " stress balloons"
+ (balloon != null ? " of your equipped cosmetic" : " (no balloon equipped, using fallback item)")
+ " (period=" + Math.max(1, Settings.getBalloonLerpPeriod())
+ "). Timing prints every ~5s. Run /hmccosmetics stresstest stop to end.");
return true;
}
}
return true;
}
Expand Down
103 changes: 101 additions & 2 deletions common/src/main/java/com/hibiscusmc/hmccosmetics/config/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,24 @@ public class Settings {
private static final String COSMETIC_BACKPACK_FORCE_RIDING_PACKET_PATH = "backpack-force-riding-packet";
private static final String COSMETIC_BACKPACK_INTERCEPT_PASSENGER_PACKET_PATH = "backpack-intercept-passenger-packets";
private static final String COSMETIC_DESTROY_LOOSE_COSMETIC_PATH = "destroy-loose-cosmetics";
private static final String COSMETIC_BALLOON_HEAD_FORWARD_PATH = "balloon-head-forward";
private static final String COSMETIC_BALLOON_HEAD_FORWARD_PATH = "balloon-head-unmoving";
private static final String COSMETIC_BALLOON_DEFAULT_SHOW_LEAD_PATH = "balloon-lead-default";
private static final String COSMETIC_BALLOON_LERP_PERIOD_PATH = "balloon-lerp-period";
private static final String COSMETIC_BALLOON_POSITION_LERP_FACTOR_PATH = "balloon-position-lerp-factor";
private static final String COSMETIC_BALLOON_VERTICAL_LERP_FACTOR_PATH = "balloon-vertical-lerp-factor";
private static final String COSMETIC_BALLOON_YAW_LERP_FACTOR_PATH = "balloon-yaw-lerp-factor";
private static final String COSMETIC_BALLOON_TILT_LERP_FACTOR_PATH = "balloon-tilt-lerp-factor";
private static final String COSMETIC_BALLOON_MAX_TILT_ANGLE_PATH = "balloon-max-tilt-angle";
private static final String COSMETIC_BALLOON_TILT_FORWARD_FACTOR_PATH = "balloon-tilt-forward-factor";
private static final String COSMETIC_BALLOON_TILT_SIDE_FACTOR_PATH = "balloon-tilt-side-factor";
private static final String COSMETIC_BALLOON_BOB_AMPLITUDE_PATH = "balloon-bob-amplitude";
private static final String COSMETIC_BALLOON_BOB_PERIOD_PATH = "balloon-bob-period";
private static final String COSMETIC_BALLOON_SWAY_ANGLE_PATH = "balloon-sway-angle";
private static final String COSMETIC_BALLOON_SWAY_PERIOD_PATH = "balloon-sway-period";
private static final String COSMETIC_BALLOON_IDLE_YAW_ANGLE_PATH = "balloon-idle-yaw-angle";
private static final String COSMETIC_BALLOON_IDLE_YAW_PERIOD_PATH = "balloon-idle-yaw-period";
private static final String COSMETIC_BALLOON_SAG_FACTOR_PATH = "balloon-sag-factor";
private static final String COSMETIC_BALLOON_MAX_SAG_PATH = "balloon-max-sag";
private static final String COSMETIC_OFFHAND_PREVENT_SWAPPING = "offhand-prevent-swapping";
private static final String MENU_SETTINGS_PATH = "menu-settings";
private static final String MENU_CLICK_COOLDOWN_PATH = "click-cooldown";
Expand Down Expand Up @@ -124,6 +140,38 @@ public class Settings {
@Getter
private static boolean balloonDefaultShowLead;
@Getter
private static int balloonLerpPeriod;
@Getter
private static double balloonPositionLerpFactor;
@Getter
private static double balloonVerticalLerpFactor;
@Getter
private static double balloonYawLerpFactor;
@Getter
private static double balloonTiltLerpFactor;
@Getter
private static double balloonMaxTiltAngle;
@Getter
private static double balloonTiltForwardFactor;
@Getter
private static double balloonTiltSideFactor;
@Getter
private static double balloonBobAmplitude;
@Getter
private static int balloonBobPeriod;
@Getter
private static double balloonSwayAngle;
@Getter
private static int balloonSwayPeriod;
@Getter
private static double balloonIdleYawAngle;
@Getter
private static int balloonIdleYawPeriod;
@Getter
private static double balloonSagFactor;
@Getter
private static double balloonMaxSag;
@Getter
private static boolean backpackPreventDarkness;
@Getter
private static boolean preferHMCColorDyeMenu;
Expand Down Expand Up @@ -239,8 +287,26 @@ public static void load(ConfigurationNode source) {
tickPeriod = cosmeticSettings.node(TICK_PERIOD_PATH).getInt(-1);
engine = PlayerSearchManager.SearchEngine.valueOf(cosmeticSettings.node(PLAYER_SEARCH_IMPLEMENTATION).getString("BUKKIT").toUpperCase());
viewDistance = cosmeticSettings.node(VIEW_DISTANCE_PATH).getInt(-3);
balloonHeadForward = cosmeticSettings.node(COSMETIC_BALLOON_HEAD_FORWARD_PATH).getBoolean(false);
balloonHeadForward = cosmeticSettings.node(COSMETIC_BALLOON_HEAD_FORWARD_PATH).getBoolean(true);
balloonDefaultShowLead = cosmeticSettings.node(COSMETIC_BALLOON_DEFAULT_SHOW_LEAD_PATH).getBoolean(true);
// A period of 0 or less disables the balloon smoothing task entirely, matching the -1 sentinel
// that tick-period and view-distance already use.
balloonLerpPeriod = cosmeticSettings.node(COSMETIC_BALLOON_LERP_PERIOD_PATH).getInt(2);
balloonPositionLerpFactor = loadLerpFactor(cosmeticSettings, COSMETIC_BALLOON_POSITION_LERP_FACTOR_PATH, 0.35);
balloonVerticalLerpFactor = loadLerpFactor(cosmeticSettings, COSMETIC_BALLOON_VERTICAL_LERP_FACTOR_PATH, 0.15);
balloonYawLerpFactor = loadLerpFactor(cosmeticSettings, COSMETIC_BALLOON_YAW_LERP_FACTOR_PATH, 0.15);
balloonTiltLerpFactor = loadLerpFactor(cosmeticSettings, COSMETIC_BALLOON_TILT_LERP_FACTOR_PATH, 0.25);
balloonMaxTiltAngle = loadNonNegative(cosmeticSettings, COSMETIC_BALLOON_MAX_TILT_ANGLE_PATH, 25.0);
balloonTiltForwardFactor = loadFinite(cosmeticSettings, COSMETIC_BALLOON_TILT_FORWARD_FACTOR_PATH, 4.0);
balloonTiltSideFactor = loadFinite(cosmeticSettings, COSMETIC_BALLOON_TILT_SIDE_FACTOR_PATH, 6.0);
balloonBobAmplitude = loadNonNegative(cosmeticSettings, COSMETIC_BALLOON_BOB_AMPLITUDE_PATH, 0.0);
balloonBobPeriod = Math.max(1, cosmeticSettings.node(COSMETIC_BALLOON_BOB_PERIOD_PATH).getInt(70));
balloonSwayAngle = loadNonNegative(cosmeticSettings, COSMETIC_BALLOON_SWAY_ANGLE_PATH, 0.0);
balloonSwayPeriod = Math.max(1, cosmeticSettings.node(COSMETIC_BALLOON_SWAY_PERIOD_PATH).getInt(90));
balloonIdleYawAngle = loadNonNegative(cosmeticSettings, COSMETIC_BALLOON_IDLE_YAW_ANGLE_PATH, 0.0);
balloonIdleYawPeriod = Math.max(1, cosmeticSettings.node(COSMETIC_BALLOON_IDLE_YAW_PERIOD_PATH).getInt(160));
balloonSagFactor = loadNonNegative(cosmeticSettings, COSMETIC_BALLOON_SAG_FACTOR_PATH, 0.6);
balloonMaxSag = loadNonNegative(cosmeticSettings, COSMETIC_BALLOON_MAX_SAG_PATH, 0.4);
backpackPreventDarkness = cosmeticSettings.node(BACKPACK_PREVENT_DARKNESS_PATH).getBoolean(true);

ConfigurationNode menuSettings = source.node(MENU_SETTINGS_PATH);
Expand Down Expand Up @@ -305,6 +371,39 @@ public static Vector loadVector(final ConfigurationNode config) {
return new Vector(config.node("x").getDouble(), config.node("y").getDouble(), config.node("z").getDouble());
}

/**
* Reads a lerp factor, clamped into [0, 1]. Non-finite values fall back to the default rather than
* being clamped: NaN survives every clamp and would propagate through the smoothing maths into
* Location#setX, making the balloon teleport throw on every tick.
*/
private static double loadLerpFactor(final ConfigurationNode config, final String path, final double def) {
final double value = config.node(path).getDouble(def);
if (!Double.isFinite(value)) return warnAndDefault(path, value, def);
return Math.clamp(value, 0.0, 1.0);
}

/**
* Reads a double that has no meaning below zero. A negative max tilt angle in particular would invert
* the bounds handed to {@link Math#clamp} and pin every balloon at full lean.
*/
private static double loadNonNegative(final ConfigurationNode config, final String path, final double def) {
final double value = config.node(path).getDouble(def);
if (!Double.isFinite(value) || value < 0) return warnAndDefault(path, value, def);
return value;
}

private static double loadFinite(final ConfigurationNode config, final String path, final double def) {
final double value = config.node(path).getDouble(def);
if (!Double.isFinite(value)) return warnAndDefault(path, value, def);
return value;
}

private static double warnAndDefault(final String path, final double value, final double def) {
HMCCosmeticsPlugin.getInstance().getLogger().warning(
"Invalid value for " + path + " (" + value + "), falling back to " + def);
return def;
}

public static SlotOptionConfig getSlotOption(EquipmentSlot slot) {
if (!slotOptions.containsKey(slot)) slotOptions.put(slot, new SlotOptionConfig(slot, false, false, false, false));
return slotOptions.get(slot);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@

import com.hibiscusmc.hmccosmetics.config.Settings;
import com.hibiscusmc.hmccosmetics.cosmetic.Cosmetic;
import com.hibiscusmc.hmccosmetics.cosmetic.behavior.CosmeticMovementBehavior;
import com.hibiscusmc.hmccosmetics.cosmetic.behavior.CosmeticUpdateBehavior;
import com.hibiscusmc.hmccosmetics.user.CosmeticUser;
import com.hibiscusmc.hmccosmetics.user.manager.UserBalloonManager;
import com.hibiscusmc.hmccosmetics.util.MessagesUtil;
import com.hibiscusmc.hmccosmetics.util.packets.HMCCPacketManager;
import lombok.Getter;
import me.lojosho.shaded.configurate.ConfigurationNode;
Expand All @@ -20,7 +18,9 @@

import java.util.List;

public class CosmeticBalloonType extends Cosmetic implements CosmeticUpdateBehavior, CosmeticMovementBehavior {
// Balloons deliberately do not implement CosmeticMovementBehavior: BalloonSmoothingTask owns their
// position and runs far more often than move events fire, so a move handler has nothing left to do.
public class CosmeticBalloonType extends Cosmetic implements CosmeticUpdateBehavior {

@Getter
private final String modelName;
Expand Down Expand Up @@ -72,6 +72,18 @@ public void dispatchUpdate(@NotNull CosmeticUser user) {
newLocation = newLocation.clone().add(getBalloonOffset());
if (Settings.isBalloonHeadForward()) newLocation.setPitch(0);

// Smoothing task is off (balloon-lerp-period <= 0): it is the only thing that moves the balloon
// AND its lead, so drive both from this low-frequency tick instead of leaving them frozen at spawn.
// Gated on <= 0 so it never fights the smoothing task while that is running.
if (Settings.getBalloonLerpPeriod() <= 0) {
userBalloonManager.snapTo(newLocation);
// snapTo only moves the model entity; the lead is anchored to the pufferfish, which the
// smoothing task would normally teleport. Move it here for existing viewers so the lead follows.
if (!userBalloonManager.getPufferfish().getViewers().isEmpty()) {
userBalloonManager.getPufferfish().teleport(newLocation);
}
}

if (!user.isHidden() && showLead) {
List<Player> sendTo = userBalloonManager.getPufferfish().refreshViewers(newLocation);
if (sendTo.isEmpty()) return;
Expand All @@ -80,42 +92,6 @@ public void dispatchUpdate(@NotNull CosmeticUser user) {
}
}

@Override
public void dispatchMove(@NotNull CosmeticUser user, @NotNull Location from, @NotNull Location to) {
Entity entity = Bukkit.getEntity(user.getUniqueId());
UserBalloonManager userBalloonManager = user.getBalloonManager();

if (entity == null || userBalloonManager == null) return;
if (user.isInWardrobe()) return;

if (!userBalloonManager.getModelEntity().isValid()) {
return;
}

Location newLocation = entity.getLocation();
Location currentLocation = user.getBalloonManager().getLocation();
newLocation = newLocation.clone().add(getBalloonOffset());
if (Settings.isBalloonHeadForward()) newLocation.setPitch(0);

List<Player> viewers = HMCCPacketManager.getViewers(entity.getLocation());

if (entity.getLocation().getWorld() != userBalloonManager.getLocation().getWorld()) {
userBalloonManager.getModelEntity().teleport(newLocation);
HMCCPacketManager.sendTeleportPacket(userBalloonManager.getPufferfishBalloonId(), newLocation, false, viewers);
return;
}

//Vector velocity = newLocation.toVector().subtract(currentLocation.toVector());
userBalloonManager.setLocation(newLocation);

MessagesUtil.sendDebugMessages("Balloon Cosmetic Update for " + user.getEntity().getName());
MessagesUtil.sendDebugMessages("Ballon previous location is " + currentLocation);
MessagesUtil.sendDebugMessages("Balloon location set to " + newLocation);

HMCCPacketManager.sendTeleportPacket(userBalloonManager.getPufferfishBalloonId(), newLocation, false, viewers);
HMCCPacketManager.sendLeashPacket(userBalloonManager.getPufferfishBalloonId(), entity.getEntityId(), viewers);
}

public boolean isDyeablePart(String name) {
// If player does not define parts, dye whole model
if (dyeableParts == null) return true;
Expand Down
Loading
Loading