1.渲染由客户端世界Tick进行
2.优化了些细节实现(如拴绳交互)
3.提供可调配的实体配置(用于过滤拴绳拉动和拴绳传送)
fix:
1.移除旧的技术实体实现,转为Tick计算力
2.修复C/S网络同步问题
This commit is contained in:
叁玖领域 2025-09-09 00:00:33 +08:00
parent bf0fb8665d
commit ec373ee1cf
79 changed files with 3870 additions and 481 deletions

View File

@ -4,5 +4,6 @@
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true">
<option name="METHOD_MATCHER_CONFIG" value="java.util.Formatter,format,java.io.Writer,append,com.google.common.base.Preconditions,checkNotNull,org.hibernate.Session,close,java.io.PrintWriter,printf,java.io.PrintStream,printf,net.minecraft.world.entity.Entity,level" />
</inspection_tool>
<inspection_tool class="ClassCanBeRecord" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
</profile>
</component>

View File

@ -29,4 +29,18 @@ This project is licensed under the **GNU GPL v3** open source license.
**相同许可证** - 使用相同的GPL v3协议
**Full Terms:** [View LICENSE file](LICENSE.GPL3) | [GPL v3 Details](https://www.gnu.org/licenses/gpl-3.0.html)
**完整条款:** [查看LICENSE文件](LICENSE.GPL3) | [GPL v3详情](https://www.gnu.org/licenses/gpl-3.0.html)
**完整条款:** [查看LICENSE文件](LICENSE.GPL3) | [GPL v3详情](https://www.gnu.org/licenses/gpl-3.0.html)
# 🎵 Audio Resources / 音效资源声明
This mod includes supplemental audio assets from **Minecraft (by Mojang Studios / Microsoft)**,
which are **not** covered by the GPL license of this project.
本模组中包含部分来自 **MinecraftMojang Studios / Microsoft** 的补充音效资源,
这些资源 **不属于** 本项目 GPL 协议授权的范围。
- Copyright of these audio files belongs to **Mojang Studios / Microsoft**.
这些音效文件的版权归 **Mojang Studios / Microsoft** 所有。
- They are provided here solely for compatibility and gameplay purposes.
这些音效仅为兼容性和游戏功能提供。
- The **GPL v3 license applies only to the original code and assets of this mod**,
and does **not** apply to the bundled Minecraft audio files.
**GPL v3 协议仅适用于本模组的原创代码与资源**,不适用于其中包含的 Minecraft 音效文件。

View File

@ -1,2 +1,2 @@
// 1.20.1 2025-08-02T18:29:50.5044172 Languages: zh_tw
d8984a2a501a148017582d1745d24136f24840a6 assets/superleadrope/lang/zh_tw.json
// 1.20.1 2025-09-08T23:59:30.214676 Languages: zh_tw
01bf653cea7c3be88445a248753b4cc75e5ac45e assets/superleadrope/lang/zh_tw.json

View File

@ -1,2 +1,2 @@
// 1.20.1 2025-08-02T18:29:50.4900479 Languages: zh_cn
1d742b4bdaf09cf0bb27b838c05b37d404f9d805 assets/superleadrope/lang/zh_cn.json
// 1.20.1 2025-09-08T23:59:30.214676 Languages: zh_cn
6c4c5affb9dae3253eae9155ee4fb58deb5c4b92 assets/superleadrope/lang/zh_cn.json

View File

@ -1,3 +1,4 @@
// 1.20.1 2025-08-02T12:46:53.5980773 Item Models: superleadrope
// 1.20.1 2025-09-05T21:41:43.7307014 Item Models: superleadrope
c982e91b60c03a6460d1cc7b516628cf09a28417 assets/superleadrope/models/item/broken_super_lead_rope.json
7b072a8cc70b53d54e37e5fa72d705bd07780943 assets/superleadrope/models/item/eternal_potato.json
4fb737a5f8f15642212aa581a02a81cf649fc36f assets/superleadrope/models/item/super_lead_rope.json

View File

@ -1,2 +1,2 @@
// 1.20.1 2025-08-02T18:29:50.5014197 Languages: lzh
b9129899dc35bdeb8acf2451acab08ce358dfd8d assets/superleadrope/lang/lzh.json
// 1.20.1 2025-09-08T23:59:30.214676 Languages: lzh
e7d897e78d2a100c249948780b7f1c07ad94e9b9 assets/superleadrope/lang/lzh.json

View File

@ -1 +1,3 @@
// 1.20.1 2025-08-02T18:29:50.496049 Recipes
// 1.20.1 2025-09-07T22:44:37.7874227 Recipes
0e55db960b9b165ac2a40e2ec1f417a5affa88b9 data/superleadrope/advancements/recipes/tools/super_lead_rope.json
4777e7678c6fa5b253dc58b84730eac7d331ed5a data/superleadrope/recipes/super_lead_rope.json

View File

@ -1 +1,2 @@
// 1.20.1 2025-07-29T13:13:42.0601667 Tags for minecraft:item mod id superleadrope
// 1.20.1 2025-09-07T22:23:20.2618005 Tags for minecraft:item mod id superleadrope
d0e9808a4ffe703b1a69a402d7b88980a775eb06 data/superleadrope/tags/items/lead.json

View File

@ -1,2 +1,2 @@
// 1.20.1 2025-08-02T18:29:50.4980437 Languages: en_us
4cf775c9c9c637391296805dbd65edea025bf959 assets/superleadrope/lang/en_us.json
// 1.20.1 2025-09-08T23:59:30.214676 Languages: en_us
9055e381f25bb65f46b6c4489800326812fd3659 assets/superleadrope/lang/en_us.json

View File

@ -1,6 +1,24 @@
{
"death.attack.eternal_potato_not_complete": "§c%1$s was not the rightful owner, struck by lightning!",
"death.attack.eternal_potato_not_owner": "§c%1$s was not the rightful owner, struck by lightning!",
"entity.superleadrope.super_lead_knot": "Super Lead Knot",
"entity.superleadrope.super_leash": "Super Leash",
"item.eternal_potato.msg.bind_msg": "§6Bound to you as the server-wide shared person.",
"item.eternal_potato.msg.cannot_drop": "§cThe Eternal Potato cannot be dropped! +%d punishments.",
"item.eternal_potato.msg.obligation_countdown": "Punish Countdown: §a%d §fseconds remaining",
"item.eternal_potato.msg.obligation_done": "§eObligation completed, remaining: §a%d§e",
"item.eternal_potato.msg.obligation_full": "§aAll obligations completed today!",
"item.eternal_potato.msg.obligation_info": "§e[Eternal Potato] §fThis is the server-wide shared person, remaining obligations today: §a%d§f.",
"item.eternal_potato.msg.pickup_not_owner": "§cYou are not the rightful owner and cannot pick this up!",
"item.eternal_potato.msg.potato_heal": "§aThe power of the Eternal Potato comforts you, it won't disappear.",
"item.eternal_potato.msg.punish_msg": "§cYesterday obligations incomplete, punished!",
"item.eternal_potato.msg.punish_not_owner": "§cYou are not the rightful owner, punished by lightning!",
"item.eternal_potato.tooltip.bind_owner": "§bBound Owner: §f%s",
"item.eternal_potato.tooltip.desc": "§7Symbol of server-wide contract, cannot be discarded",
"item.eternal_potato.tooltip.obligation": "§7Daily obligations remaining: §a%d §c(+%d§c overdue)",
"item.eternal_potato.tooltip.punish": "§cOverdue punishments: §4%d §7(will be applied), grace exceeded: §4%d",
"item.eternal_potato.tooltip.title": "§6Mythical Item §7- §6Eternal Potato",
"item.eternal_potato.tooltip.unbound": "§cUnbound",
"item.superleadrope.eternal_potato": "Eternal Potato",
"item.superleadrope.super_lead_rope": "Super Lead Rope",
"sound.superleadrope.subtitle.lead_break": "Lead Break",
"sound.superleadrope.subtitle.lead_tied": "Lead Tied",

View File

@ -1,6 +1,24 @@
{
"death.attack.eternal_potato_not_complete": "§c%1$s 非汝所主,雷霆降身!",
"death.attack.eternal_potato_not_owner": "§c%1$s 非汝所主,雷霆降身!",
"entity.superleadrope.super_lead_knot": "神駒羈縻索結",
"entity.superleadrope.super_leash": "神駒羈縻索",
"item.eternal_potato.msg.bind_msg": "§6已与汝绑定为全服共享之人。",
"item.eternal_potato.msg.cannot_drop": "§c永恒土豆不可丟棄懲罰數增加%d",
"item.eternal_potato.msg.obligation_countdown": "受罚倒数§a%d §f瞬",
"item.eternal_potato.msg.obligation_done": "§e责务完成尚余 §a%d §e次。",
"item.eternal_potato.msg.obligation_full": "§a今日责务尽矣",
"item.eternal_potato.msg.obligation_info": "§e[永恒土豆] §f此为全服共享之人今日责务尚余§a%d§f次。",
"item.eternal_potato.msg.pickup_not_owner": "§c非汝所主勿取",
"item.eternal_potato.msg.potato_heal": "§a永恒土豆之力慰心永不消逝。",
"item.eternal_potato.msg.punish_msg": "§c昨日之责未尽受罚矣",
"item.eternal_potato.msg.punish_not_owner": "§c非汝所主雷霆降身",
"item.eternal_potato.tooltip.bind_owner": "§b绑定主人: §f%s",
"item.eternal_potato.tooltip.desc": "§7象征全服契约绝不可弃",
"item.eternal_potato.tooltip.obligation": "§7今日责务尚余: §a%d §c(+%d §c逾期未尽)",
"item.eternal_potato.tooltip.punish": "§c逾期责务尚未完成: §4%d §7(將受懲罰),超出寬限數: §4%d",
"item.eternal_potato.tooltip.title": "§6永恒土豆 §7- §6传奇之物",
"item.eternal_potato.tooltip.unbound": "§c尚未绑定主人",
"item.superleadrope.eternal_potato": "不滅薯",
"item.superleadrope.super_lead_rope": "神駒羈縻索",
"sound.superleadrope.subtitle.lead_break": "索絕",
"sound.superleadrope.subtitle.lead_tied": "繫索",

View File

@ -1,6 +1,24 @@
{
"death.attack.eternal_potato_not_complete": "§c%1$s 因使用非自己绑定物品,受到闪电惩罚!",
"death.attack.eternal_potato_not_owner": "§c%1$s 因使用非自己绑定物品,受到闪电惩罚!",
"entity.superleadrope.super_lead_knot": "超级拴绳结",
"entity.superleadrope.super_leash": "超级拴绳",
"item.eternal_potato.msg.bind_msg": "§6已与你绑定成为全服共有之人。",
"item.eternal_potato.msg.cannot_drop": "§c永恒土豆是不可丢弃的惩罚数加%d",
"item.eternal_potato.msg.obligation_countdown": "惩罚倒计时: §a%d §f秒",
"item.eternal_potato.msg.obligation_done": "§e义务完成一次剩余 §a%d §e次。",
"item.eternal_potato.msg.obligation_full": "§a今日义务已全部完成",
"item.eternal_potato.msg.obligation_info": "§e[永恒土豆] §f这是全服共有之人今日义务剩余§a%d§f次。",
"item.eternal_potato.msg.pickup_not_owner": "§c非绑定主人无法拾取此物品",
"item.eternal_potato.msg.potato_heal": "§a永恒土豆的力量抚慰了你但它不会消失。",
"item.eternal_potato.msg.punish_msg": "§c未完成昨日义务受到惩罚",
"item.eternal_potato.msg.punish_not_owner": "§c非绑定主人使用受到闪电惩罚",
"item.eternal_potato.tooltip.bind_owner": "§b绑定主人: §f%s",
"item.eternal_potato.tooltip.desc": "§7象征全服契约不可丢弃",
"item.eternal_potato.tooltip.obligation": "§7今日剩余义务: §a%d §c(+%d §c逾期未完成)",
"item.eternal_potato.tooltip.punish": "§c逾期未完成责务: §4%d §7(将会受罚),超出宽限数: §4%d",
"item.eternal_potato.tooltip.title": "§6神话物品 §7- §6永恒土豆",
"item.eternal_potato.tooltip.unbound": "§c未绑定主人",
"item.superleadrope.eternal_potato": "永恒土豆",
"item.superleadrope.super_lead_rope": "超级拴绳",
"sound.superleadrope.subtitle.lead_break": "拴绳断裂",
"sound.superleadrope.subtitle.lead_tied": "拴绳系上",

View File

@ -1,6 +1,24 @@
{
"death.attack.eternal_potato_not_complete": "§c%1$s 因使用非自己綁定物品,受到閃電懲罰!",
"death.attack.eternal_potato_not_owner": "§c%1$s 因使用非自己綁定物品,受到閃電懲罰!",
"entity.superleadrope.super_lead_knot": "超級拴繩結",
"entity.superleadrope.super_leash": "超級拴繩",
"item.eternal_potato.msg.bind_msg": "§6已與你綁定成為全服共有之人。",
"item.eternal_potato.msg.cannot_drop": "§c永恆土豆不可丟棄懲罰數加%d",
"item.eternal_potato.msg.obligation_countdown": "懲罰倒計時: §a%d §f秒",
"item.eternal_potato.msg.obligation_done": "§e義務完成一次剩餘 §a%d §e次。",
"item.eternal_potato.msg.obligation_full": "§a今日義務已全部完成",
"item.eternal_potato.msg.obligation_info": "§e[永恒土豆] §f這是全服共有之人今日義務剩餘§a%d§f次。",
"item.eternal_potato.msg.pickup_not_owner": "§c非綁定主人無法拾取此物品",
"item.eternal_potato.msg.potato_heal": "§a永恆土豆的力量撫慰了你但它不會消失。",
"item.eternal_potato.msg.punish_msg": "§c未完成昨日義務受到懲罰",
"item.eternal_potato.msg.punish_not_owner": "§c非綁定主人使用受到閃電懲罰",
"item.eternal_potato.tooltip.bind_owner": "§b綁定主人: §f%s",
"item.eternal_potato.tooltip.desc": "§7象徵全服契約不可丟棄",
"item.eternal_potato.tooltip.obligation": "§7今日剩餘義務: §a%d §c(+%d §c逾期未完成)",
"item.eternal_potato.tooltip.punish": "§c逾期未完成责務: §4%d §7(將會受罰),超出寬限數: §4%d",
"item.eternal_potato.tooltip.title": "§6神話物品 §7- §6永恒土豆",
"item.eternal_potato.tooltip.unbound": "§c未綁定主人",
"item.superleadrope.eternal_potato": "永恆馬鈴薯",
"item.superleadrope.super_lead_rope": "超級拴繩",
"sound.superleadrope.subtitle.lead_break": "拴繩斷裂",
"sound.superleadrope.subtitle.lead_tied": "拴繩係上",

View File

@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "superleadrope:item/eternal_potato"
}
}

View File

@ -0,0 +1,35 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_lead": {
"conditions": {
"items": [
{
"items": [
"minecraft:lead"
]
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipe": "superleadrope:super_lead_rope"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"requirements": [
[
"has_lead",
"has_the_recipe"
]
],
"rewards": {
"recipes": [
"superleadrope:super_lead_rope"
]
},
"sends_telemetry_event": false
}

View File

@ -0,0 +1,27 @@
{
"type": "minecraft:crafting_shaped",
"category": "equipment",
"key": {
"E": {
"item": "minecraft:experience_bottle"
},
"I": {
"item": "minecraft:string"
},
"L": {
"item": "minecraft:lead"
},
"S": {
"item": "minecraft:slime_ball"
}
},
"pattern": [
"SL ",
"LE ",
" I"
],
"result": {
"item": "superleadrope:super_lead_rope"
},
"show_notification": true
}

View File

@ -0,0 +1,7 @@
{
"values": [
"minecraft:lead",
"minecraft:slime_ball",
"minecraft:string"
]
}

View File

@ -23,6 +23,7 @@ import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Pose;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.vehicle.Boat;
import net.minecraft.world.entity.vehicle.Minecart;
@ -38,22 +39,40 @@ import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.EntityJoinLevelEvent;
import net.minecraftforge.event.entity.EntityLeaveLevelEvent;
import net.minecraftforge.event.entity.EntityTeleportEvent;
import net.minecraftforge.event.entity.item.ItemTossEvent;
import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
import net.minecraftforge.event.level.LevelEvent;
import net.minecraftforge.event.server.ServerStartingEvent;
import net.minecraftforge.event.server.ServerStoppingEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import org.jetbrains.annotations.NotNull;
import top.r3944realms.superleadrope.content.capability.CapabilityHandler;
import top.r3944realms.superleadrope.content.capability.CapabilityRemainder;
import top.r3944realms.superleadrope.content.capability.LeashDataImpl;
import top.r3944realms.superleadrope.content.capability.impi.LeashDataImpl;
import top.r3944realms.superleadrope.content.capability.inter.IEternalPotato;
import top.r3944realms.superleadrope.content.capability.inter.ILeashDataCapability;
import top.r3944realms.superleadrope.content.entity.SuperLeashEntity;
import top.r3944realms.superleadrope.content.entity.SuperLeashKnotEntity;
import top.r3944realms.superleadrope.content.item.EternalPotatoItem;
import top.r3944realms.superleadrope.content.item.SuperLeadRopeItem;
import top.r3944realms.superleadrope.core.leash.LeashInteractHandler;
import top.r3944realms.superleadrope.core.leash.LeashSyncManager;
import top.r3944realms.superleadrope.core.potato.EternalPotatoFacade;
import top.r3944realms.superleadrope.core.register.SLPItems;
import top.r3944realms.superleadrope.core.util.PotatoMode;
import top.r3944realms.superleadrope.core.util.PotatoModeHelper;
import top.r3944realms.superleadrope.datagen.data.SLPLangKeyValue;
import top.r3944realms.superleadrope.util.model.RidingRelationship;
import top.r3944realms.superleadrope.util.riding.RidingApplier;
import top.r3944realms.superleadrope.util.riding.RidingDismounts;
import top.r3944realms.superleadrope.util.riding.RidingFinder;
import top.r3944realms.superleadrope.util.riding.RidingSaver;
import java.util.*;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
public class CommonEventHandler {
@ -65,7 +84,6 @@ public class CommonEventHandler {
if (entity.level().isClientSide) return;
if (entity instanceof LivingEntity || entity instanceof Boat || entity instanceof Minecart) {
entity.getCapability(CapabilityHandler.LEASH_DATA_CAP).ifPresent(LeashSyncManager::track);
entity.level().addFreshEntity(new SuperLeashEntity(entity.level(), entity));
}
}
@ -77,6 +95,23 @@ public class CommonEventHandler {
entity.getCapability(CapabilityHandler.LEASH_DATA_CAP).ifPresent(LeashSyncManager::untrack);
}
}
@SubscribeEvent
public void onPlayerLoggedIn(PlayerEvent.PlayerLoggedInEvent event) {
if (!(event.getEntity() instanceof ServerPlayer player)) return; // 只处理服务端
for (ItemStack stack : player.getInventory().items) {
if (stack.getItem() instanceof EternalPotatoItem) {
stack.getCapability(CapabilityHandler.ETERNAL_POTATO_CAP).ifPresent(cap -> {
// 物品已绑定主人时恢复服务端上下文
if (cap.getOwnerUUID() != null && cap.getOwnerUUID().equals(player.getUUID())) {
cap.bindSyncContext(player); // 只绑定服务端玩家
cap.syncToClient(player); // 由服务端发包给客户端
}
});
}
}
}
@SubscribeEvent
public static void onPlayerRightHitOnBlock(PlayerInteractEvent.RightClickBlock event) {
Level level = event.getLevel();
@ -95,76 +130,183 @@ public class CommonEventHandler {
}
}
}
@SubscribeEvent
public static void onServerStarting(ServerStartingEvent event) {
PotatoMode mode = PotatoModeHelper.getCurrentMode();
EternalPotatoFacade.init(mode, true); // 服务端
}
@SubscribeEvent
public static void onWorldLoad(LevelEvent.Load event) {
if (event.getLevel() instanceof ServerLevel serverLevel) {
// 只在主世界挂载 SavedData
if (serverLevel.dimension() == Level.OVERWORLD) {
EternalPotatoFacade.initSavedData(serverLevel);
RidingSaver.setEntityProvider(serverLevel::getEntity);
}
}
}
@SubscribeEvent
public static void onWorldUnload(LevelEvent.Unload event) {
if (event.getLevel() instanceof ServerLevel serverLevel) {
// 只在主世界卸载时清空
if (serverLevel.dimension() == Level.OVERWORLD) {
EternalPotatoFacade.clear();
}
}
}
// 服务器关闭
@SubscribeEvent
public static void onServerStopping(ServerStoppingEvent event) {
EternalPotatoFacade.clear();
}
@SubscribeEvent
public static void onItemDrop(ItemTossEvent event) {
Player player = event.getPlayer();
ItemEntity entityItem = event.getEntity();
ItemStack stack = entityItem.getItem();
if (!(stack.getItem() instanceof EternalPotatoItem)) return;
if (player.level().isClientSide) return; // 只处理服务端
UUID uuid = EternalPotatoItem.getOrCreateItemUUID(stack);
IEternalPotato cap = EternalPotatoFacade.getOrCreate(uuid);
if (player instanceof ServerPlayer serverPlayer) {
cap.bindSyncContext(serverPlayer); // 服务端绑定
}
EternalPotatoItem.ensureItemInInventory(player, stack);
// 移除地面实体
BlockPos spawnPos = entityItem.level().getSharedSpawnPos();
entityItem.setPos(spawnPos.getX(), -394, spawnPos.getZ());
// 处罚逻辑
if (cap.getOwnerUUID() != null
&& cap.getOwnerUUID().equals(player.getUUID())
&& !player.getAbilities().instabuild) {
cap.beginInit();
cap.setPendingPunishments(cap.getPendingPunishments() + 2);
cap.endInit();
player.sendSystemMessage(
net.minecraft.network.chat.Component.translatable(SLPLangKeyValue.EP_CANNOT_DROP.getKey(), 2)
);
}
}
@SubscribeEvent
public static void onItemPickup(@NotNull PlayerEvent.ItemPickupEvent event) {
Player player = event.getEntity();
ItemStack stack = event.getStack();
if (player.level().isClientSide) return; // 客户端不处理
if (!(stack.getItem() instanceof EternalPotatoItem)) return;
if (player.getAbilities().instabuild) return;
UUID uuid = EternalPotatoItem.getOrCreateItemUUID(stack);
IEternalPotato cap = EternalPotatoFacade.getOrCreate(uuid);
if (player instanceof ServerPlayer serverPlayer) {
cap.bindSyncContext(serverPlayer); // 服务端绑定
}
// 如果玩家不是主人显示提示
if (cap.getOwnerUUID() != null && !cap.getOwnerUUID().equals(player.getUUID())) {
player.displayClientMessage(
net.minecraft.network.chat.Component.translatable(SLPLangKeyValue.EP_PICKUP_NOT_OWNER.getKey()),
true
);
}
}
@SubscribeEvent
public static void onEntityTeleport(EntityTeleportEvent event) {
Entity telEntity = event.getEntity();
Vec3 targetPos = event.getTarget();
Level level = telEntity.level();
if (level instanceof ServerLevel serverLevel) {
List<Entity> entities = LeashDataImpl.leashableInArea(telEntity);
// 为每个实体创建状态快照
entities.forEach(beLeashedEntity -> {
// 保存原来的旋转角度
Pose originalPose = beLeashedEntity.getPose();
boolean originalIsSprinting = beLeashedEntity.isSprinting();
float originalYaw = beLeashedEntity.getYRot();
float originalPitch = beLeashedEntity.getXRot();
Vec3 originalDeltaMovement = beLeashedEntity.getDeltaMovement();
AtomicReference<ILeashDataCapability.LeashInfo> originalLeashInfo = new AtomicReference<>();
beLeashedEntity.getCapability(CapabilityHandler.LEASH_DATA_CAP)
.ifPresent(cap -> {
originalLeashInfo.set(cap.getLeashInfo(telEntity).orElse(null));
cap.removeLeash(telEntity);
});
if (!(level instanceof ServerLevel serverLevel)) return;
if (beLeashedEntity.level() == telEntity.level()) {
// 使用空集合表示所有值都是绝对的
if (beLeashedEntity instanceof ServerPlayer serverPlayer) {
serverPlayer.connection.teleport(
targetPos.x, targetPos.y, targetPos.z,
originalYaw, originalPitch,
Collections.emptySet() // 所有值都是绝对的
);
} else {
beLeashedEntity.teleportTo(
serverLevel,
targetPos.x,
targetPos.y,
targetPos.z,
Collections.emptySet(),
originalYaw, originalPitch
);
}
} else {
beLeashedEntity.teleportTo(
serverLevel,
targetPos.x,
targetPos.y,
targetPos.z,
Collections.emptySet(),
originalYaw, originalPitch
);
}
beLeashedEntity.setDeltaMovement(originalDeltaMovement);
beLeashedEntity.setSprinting(originalIsSprinting);
beLeashedEntity.setPose(originalPose);
ILeashDataCapability.LeashInfo leashInfoOrDefault = Optional.ofNullable(originalLeashInfo.get()).map(i -> i.transferHolder(telEntity)).orElse(ILeashDataCapability.LeashInfo.EMPTY);
beLeashedEntity.getCapability(CapabilityHandler.LEASH_DATA_CAP).ifPresent(
cap -> cap.addLeash(telEntity, leashInfoOrDefault)
);
// 获取范围内可被拴住实体
List<Entity> entities = LeashDataImpl.leashableInArea(telEntity);
for (Entity beLeashedEntity : entities) {
// --- 保存状态快照 ---
Pose originalPose = beLeashedEntity.getPose();
boolean originalIsSprinting = beLeashedEntity.isSprinting();
float originalYaw = beLeashedEntity.getYRot();
float originalPitch = beLeashedEntity.getXRot();
Vec3 originalDeltaMovement = beLeashedEntity.getDeltaMovement();
AtomicReference<ILeashDataCapability.LeashInfo> originalLeashInfo = new AtomicReference<>();
beLeashedEntity.getCapability(CapabilityHandler.LEASH_DATA_CAP).ifPresent(cap -> {
originalLeashInfo.set(cap.getLeashInfo(telEntity).orElse(null));
cap.removeLeash(telEntity);
});
// --- 保存骑乘关系可修改列表 ---
RidingRelationship originalRidingRelationship = RidingSaver.save(beLeashedEntity, true);
// --- 解除骑乘 ---
List<Entity> allPassengers = RidingFinder.getEntityFromRidingShip(originalRidingRelationship, serverLevel::getEntity);
RidingDismounts.dismountEntities(allPassengers);
// --- 传送实体及乘客 ---
for (Entity entity : allPassengers) {
if (entity.level() != serverLevel) {
entity.teleportTo(serverLevel, targetPos.x, targetPos.y, targetPos.z,
Collections.emptySet(), originalYaw, originalPitch);
} else if (entity instanceof ServerPlayer player) {
player.connection.teleport(targetPos.x, targetPos.y, targetPos.z,
originalYaw, originalPitch, Collections.emptySet());
} else {
entity.teleportTo(serverLevel, targetPos.x, targetPos.y, targetPos.z,
Collections.emptySet(), originalYaw, originalPitch);
}
}
// --- 恢复状态 ---
for (Entity entity : allPassengers) {
entity.setDeltaMovement(originalDeltaMovement);
entity.setSprinting(originalIsSprinting);
entity.setPose(originalPose);
}
// --- 恢复拴绳 ---
ILeashDataCapability.LeashInfo leashInfo = Optional.ofNullable(originalLeashInfo.get())
.map(info -> info.transferHolder(telEntity))
.orElse(ILeashDataCapability.LeashInfo.EMPTY);
beLeashedEntity.getCapability(CapabilityHandler.LEASH_DATA_CAP).ifPresent(
cap -> cap.addLeash(telEntity, leashInfo)
);
// --- 重新应用骑乘关系仅保留白名单根载具 ---
RidingRelationship filteredRelationship = RidingSaver.filterByWhitelistRoot(originalRidingRelationship);
RidingApplier.applyRidingRelationship(filteredRelationship, serverLevel::getEntity);
}
}
@SubscribeEvent
public static void onPlayerClone(PlayerEvent.Clone event) {
CapabilityRemainder.onPlayerClone(event);
}
private static int tickCounter = 0;
@SubscribeEvent
public static void onServerTick(TickEvent.ServerTickEvent event) {
if (event.phase == TickEvent.Phase.END) {
LeashSyncManager.forEach((i -> ((LeashDataImpl) i).checkSync()));
tickCounter++;
// 每10 tick标记为脏needsSync
if (tickCounter % 10 == 0) {
LeashSyncManager.forEach(ILeashDataCapability::markForSync);
}
// 定期同步检查
LeashSyncManager.forEach(ILeashDataCapability::checkSync);
// 应用物理拉力/效果
LeashSyncManager.forEach(ILeashDataCapability::applyLeashForces);
}
}

View File

@ -16,15 +16,18 @@
package top.r3944realms.superleadrope;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.fml.ModLoadingContext;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.config.ModConfig;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import top.r3944realms.superleadrope.config.LeashCommonConfig;
import top.r3944realms.superleadrope.core.register.SLPEntityTypes;
import top.r3944realms.superleadrope.core.register.SLPItems;
import top.r3944realms.superleadrope.core.register.SLPSoundEvents;
import top.r3944realms.superleadrope.network.NetworkHandler;
import top.r3944realms.superleadrope.util.file.ConfigUtil;
@Mod(value = SuperLeadRope.MOD_ID)
public class SuperLeadRope {
@ -36,5 +39,13 @@ public class SuperLeadRope {
SLPEntityTypes.register(eventBus);
SLPSoundEvents.register(eventBus);
NetworkHandler.register();
initialize();
}
public static void initialize() {
logger.info("Initializing SuperLeadRope");
String c = "common";
ConfigUtil.createFile(new String[]{c});
ModLoadingContext modLoadingContext = ModLoadingContext.get();
ConfigUtil.registerConfig(modLoadingContext, ModConfig.Type.COMMON, LeashCommonConfig.SPEC, c, "leash");
}
}

View File

@ -20,6 +20,7 @@ import net.minecraft.client.renderer.item.ItemProperties;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.client.event.ClientPlayerNetworkEvent;
import net.minecraftforge.client.event.EntityRenderersEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
@ -27,15 +28,23 @@ import top.r3944realms.superleadrope.SuperLeadRope;
import top.r3944realms.superleadrope.client.model.SuperLeashKnotModel;
import top.r3944realms.superleadrope.client.model.geom.SLPModelLayers;
import top.r3944realms.superleadrope.client.renderer.entity.SuperLeashKnotRenderer;
import top.r3944realms.superleadrope.client.renderer.entity.SuperLeashRenderer;
import top.r3944realms.superleadrope.core.potato.EternalPotatoFacade;
import top.r3944realms.superleadrope.core.register.SLPEntityTypes;
import top.r3944realms.superleadrope.core.register.SLPItems;
import top.r3944realms.superleadrope.core.util.PotatoMode;
import static top.r3944realms.superleadrope.core.util.PotatoModeHelper.getCurrentMode;
@OnlyIn(Dist.CLIENT)
public class ClientEventHandler {
// @net.minecraftforge.fml.common.Mod.EventBusSubscriber(modid = SuperLeadRope.MOD_ID, value = Dist.CLIENT, bus = net.minecraftforge.fml.common.Mod.EventBusSubscriber.Bus.FORGE)
@net.minecraftforge.fml.common.Mod.EventBusSubscriber(modid = SuperLeadRope.MOD_ID, value = Dist.CLIENT, bus = net.minecraftforge.fml.common.Mod.EventBusSubscriber.Bus.FORGE)
public static class Game {
// 未使用-注释
@SubscribeEvent
public static void onPlayerLoggedOut(ClientPlayerNetworkEvent.LoggingOut event) {
// 清理客户端缓存数据
EternalPotatoFacade.clear();
}
}
@net.minecraftforge.fml.common.Mod.EventBusSubscriber(modid = SuperLeadRope.MOD_ID, value = Dist.CLIENT, bus = net.minecraftforge.fml.common.Mod.EventBusSubscriber.Bus.MOD)
public static class Mod {
@ -50,6 +59,8 @@ public class ClientEventHandler {
return itemStack.getDamageValue() > 1024 - 50 ? 1.0F : 0.0F; // 损坏 返回 1.0触发 override
}
);
PotatoMode mode = getCurrentMode();
EternalPotatoFacade.init(mode, false); // 客户端
});
}
@SubscribeEvent
@ -59,7 +70,6 @@ public class ClientEventHandler {
@SubscribeEvent
public static void onRegisterRenderer (EntityRenderersEvent.RegisterRenderers event) {
event.registerEntityRenderer(SLPEntityTypes.SUPER_LEAD_KNOT.get(), SuperLeashKnotRenderer::new);
event.registerEntityRenderer(SLPEntityTypes.SUPER_LEASH.get(), SuperLeashRenderer::new);
}
}
}

View File

@ -0,0 +1,234 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.client.renderer;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.client.event.RenderLevelStageEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import org.joml.Matrix4f;
import top.r3944realms.superleadrope.SuperLeadRope;
import top.r3944realms.superleadrope.client.renderer.resolver.SuperLeashStateResolver;
import top.r3944realms.superleadrope.client.renderer.state.SuperLeashRenderState;
import top.r3944realms.superleadrope.content.capability.CapabilityHandler;
import top.r3944realms.superleadrope.content.capability.inter.ILeashDataCapability;
import top.r3944realms.superleadrope.content.entity.SuperLeashKnotEntity;
import java.util.Optional;
import java.util.UUID;
@Mod.EventBusSubscriber(modid = SuperLeadRope.MOD_ID, value = Dist.CLIENT)
public class LeashRenderHandler {
@OnlyIn(Dist.CLIENT)
@SubscribeEvent
public static void onRenderLevelStage(RenderLevelStageEvent event) {
if (event.getStage() != RenderLevelStageEvent.Stage.AFTER_ENTITIES) {
return;
}
renderAllCustomLeashes(event.getPoseStack(), event.getPartialTick());
}
private static void renderAllCustomLeashes(PoseStack poseStack, float partialTick) {
Minecraft minecraft = Minecraft.getInstance();
Level level = minecraft.level;
Entity cameraEntity = minecraft.getCameraEntity();
if (level == null || cameraEntity == null) return;
MultiBufferSource.BufferSource bufferSource = minecraft.renderBuffers().bufferSource();
for (Entity entity : level.getEntitiesOfClass(Entity.class,
cameraEntity.getBoundingBox().inflate(50))) {
entity.getCapability(CapabilityHandler.LEASH_DATA_CAP).ifPresent(leashData -> {
for (ILeashDataCapability.LeashInfo leashInfo : leashData.getAllLeashes()) {
renderLeashFromInfo(entity, leashInfo, poseStack, bufferSource, partialTick);
}
});
}
}
private static void renderLeashFromInfo(Entity entity, ILeashDataCapability.LeashInfo leashInfo,
PoseStack poseStack, MultiBufferSource bufferSource,
float partialTick) {
try {
Optional<Entity> holderOpt = getHolderFromLeashInfo((ClientLevel) entity.level(), leashInfo);
if (holderOpt.isEmpty()) return;
Entity holder = holderOpt.get();
Optional<SuperLeashRenderState> stateOpt = SuperLeashStateResolver.resolve(
holder, entity, leashInfo, partialTick
);
stateOpt.ifPresent(state -> {
// 每个渲染都创建自己的 PoseStack避免污染全局
PoseStack localStack = new PoseStack();
localStack.mulPoseMatrix(poseStack.last().pose());
renderSingleLeash(state, localStack, bufferSource, partialTick);
});
} catch (Exception e) {
SuperLeadRope.logger.error("Error rendering leash: {}", e.getMessage());
}
}
private static Optional<Entity> getHolderFromLeashInfo(ClientLevel level, ILeashDataCapability.LeashInfo leashInfo) {
// 根据LeashInfo类型获取持有者
if (leashInfo.blockPosOpt().isPresent()) {
// 拴绳结持有者
BlockPos pos = leashInfo.blockPosOpt().get();
SuperLeashKnotEntity knot = SuperLeashKnotEntity.getOrCreateKnot(level, pos);
return Optional.of(knot);
} else if (leashInfo.holderUUIDOpt().isPresent()) {
// 实体持有者
UUID holderUUID = leashInfo.holderUUIDOpt().get();
// 在客户端我们需要通过ID查找实体
if (leashInfo.holderIdOpt().isPresent()) {
int holderId = leashInfo.holderIdOpt().get();
Entity holder = level.getEntity(holderId);
if (holder != null) {
return Optional.of(holder);
}
}
// 备用方案通过UUID查找可能效率较低
for (Entity entity : level.entitiesForRendering()) {
if (entity.getUUID().equals(holderUUID)) {
return Optional.of(entity);
}
}
}
return Optional.empty();
}
private static void renderSingleLeash(SuperLeashRenderState state, PoseStack poseStack,
MultiBufferSource bufferSource, float partialTick) {
poseStack.pushPose();
try {
// 相机位置平移
Vec3 cameraPos = Minecraft.getInstance().gameRenderer.getMainCamera().getPosition();
poseStack.translate(-cameraPos.x, -cameraPos.y, -cameraPos.z);
// 获取缓冲区
VertexConsumer consumer = bufferSource.getBuffer(SLPRenderType.leashType());
// 绘制拴绳
renderLeashAsTriangleStrip(state, consumer, poseStack, partialTick);
} finally {
// 保证无论发生什么都会弹栈
poseStack.popPose();
}
}
private static void renderLeashAsTriangleStrip(SuperLeashRenderState state,
VertexConsumer consumer,
PoseStack poseStack,
float partialTick) {
Vec3 start = state.startPos();
Vec3 end = state.endPos();
double length = start.distanceTo(end);
if (length < 0.001) return;
Vec3 direction = end.subtract(start).normalize();
// 计算垂直向量用于厚度
Vec3 perpendicular = calculatePerpendicularVector(direction);
Matrix4f pose = poseStack.last().pose();
int color = state.color();
float r = ((color >> 16) & 0xFF) / 255.0f;
float g = ((color >> 8) & 0xFF) / 255.0f;
float b = (color & 0xFF) / 255.0f;
float a = state.isCritical() ? 0.8f : 1.0f;
// 分段渲染使用三角形带
final int SEGMENTS = 12;
for (int i = 0; i <= SEGMENTS; i++) {
float progress = (float) i / SEGMENTS;
// 计算当前段中心位置包含摆动效果
Vec3 center = start.add(direction.scale(length * progress));
Vec3 swingOffset = state.getSwingOffset(progress, partialTick);
center = center.add(swingOffset);
// 应用张力拉伸效果
if (state.stretchRatio() > 1.0f) {
float stretchFactor = (float)Math.sin(progress * Math.PI) * 0.2f * (state.stretchRatio() - 1.0f);
center = center.add(direction.scale(stretchFactor * length));
}
// 计算当前厚度
float currentThickness = state.thickness() * (1 - progress * 0.3f);
// 添加两个顶点形成带子
Vec3 top = center.add(perpendicular.scale(currentThickness));
Vec3 bottom = center.subtract(perpendicular.scale(currentThickness));
consumer.vertex(pose, (float)top.x, (float)top.y, (float)top.z)
.color(r, g, b, a)
.uv(0.0F, 0.0F) // 固定UV随便填
.uv2(0xF000F0) // 光照全亮
.normal(0.0F, 1.0F, 0.0F) // 法线朝上
.endVertex();
consumer.vertex(pose, (float)bottom.x, (float)bottom.y, (float)bottom.z)
.color(r, g, b, a)
.uv(1.0F, 0.0F) // 对应另一边UV
.uv2(0xF000F0)
.normal(0.0F, 1.0F, 0.0F)
.endVertex();
}
}
private static Vec3 calculatePerpendicularVector(Vec3 direction) {
// 计算一个与方向向量垂直的向量
Vec3 perpendicular = new Vec3(-direction.z, 0, direction.x).normalize();
if (perpendicular.lengthSqr() < 1.0E-7D) {
// 如果方向是垂直的使用另一个计算方法
perpendicular = new Vec3(0, -direction.z, direction.y).normalize();
}
return perpendicular;
}
// 距离和视野检查性能优化
private static boolean shouldRenderLeash(Entity entity, Entity holder) {
Minecraft minecraft = Minecraft.getInstance();
Entity cameraEntity = minecraft.getCameraEntity();
if (cameraEntity == null) return false;
// 距离检查只渲染50格内
double distanceToEntity = entity.distanceToSqr(cameraEntity);
double distanceToHolder = holder.distanceToSqr(cameraEntity);
return !(distanceToEntity > 50 * 50) || !(distanceToHolder > 50 * 50);
}
}

View File

@ -15,35 +15,44 @@
package top.r3944realms.superleadrope.client.renderer;
import com.google.common.collect.ImmutableMap;
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import com.mojang.blaze3d.vertex.VertexFormat;
import com.mojang.blaze3d.vertex.VertexFormatElement;
import net.minecraft.client.renderer.RenderType;
import java.util.OptionalDouble;
public class SLPRenderType extends RenderType {
public SLPRenderType(String name, VertexFormat format, VertexFormat.Mode mode, int bufferSize, boolean affectsCrumbling, boolean sortOnUpload, Runnable setupState, Runnable clearState) {
super(name, format, mode, bufferSize, affectsCrumbling, sortOnUpload, setupState, clearState);
}
static RenderType SUPER_LEASH;
public static final VertexFormat POSITION_COLOR_LIGHTMAP_NORMAL;
public static RenderType leashType() {
return SUPER_LEASH;
}
static {
POSITION_COLOR_LIGHTMAP_NORMAL = new VertexFormat(
ImmutableMap.<String, VertexFormatElement>builder()
.put("Position", DefaultVertexFormat.ELEMENT_POSITION)
.put("Color", DefaultVertexFormat.ELEMENT_COLOR)
.put("UV2", DefaultVertexFormat.ELEMENT_UV2) // 光照
.put("Normal", DefaultVertexFormat.ELEMENT_NORMAL) // 法线
.put("Padding", DefaultVertexFormat.ELEMENT_PADDING)
.build()
);
SUPER_LEASH = RenderType.create("super_leash",
DefaultVertexFormat.POSITION_COLOR_LIGHTMAP,
VertexFormat.Mode.LINE_STRIP,
POSITION_COLOR_LIGHTMAP_NORMAL, // 注意需要 NORMAL法线信息
VertexFormat.Mode.TRIANGLE_STRIP, // 改为三角形带模式
256,
false,
false,
false, // not used for crumbling
false, // sortOnUpload
CompositeState.builder()
.setShaderState(RENDERTYPE_LINES_SHADER)
.setLineState(new LineStateShard(OptionalDouble.empty()))
.setLayeringState(VIEW_OFFSET_Z_LAYERING)
.setShaderState(RENDERTYPE_LEASH_SHADER) // 使用实体着色器
.setTextureState(NO_TEXTURE) // 无纹理
.setTransparencyState(TRANSLUCENT_TRANSPARENCY)
.setOutputState(ITEM_ENTITY_TARGET)
.setWriteMaskState(COLOR_DEPTH_WRITE)
.setCullState(NO_CULL)
.createCompositeState(false));
.setLightmapState(LIGHTMAP) // 启用光照
.setCullState(NO_CULL) // 双面渲染
.createCompositeState(false)
);
}
}

View File

@ -1,153 +0,0 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.client.renderer.entity;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.entity.EntityRenderer;
import net.minecraft.client.renderer.entity.EntityRendererProvider;
import net.minecraft.client.renderer.texture.OverlayTexture;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.NotNull;
import top.r3944realms.superleadrope.client.renderer.SLPRenderType;
import top.r3944realms.superleadrope.client.renderer.resolver.SuperLeashStateResolver;
import top.r3944realms.superleadrope.client.renderer.state.SuperLeashRenderState;
import top.r3944realms.superleadrope.content.capability.CapabilityHandler;
import top.r3944realms.superleadrope.content.capability.LeashDataImpl;
import top.r3944realms.superleadrope.content.entity.SuperLeashEntity;
import java.util.List;
public class SuperLeashRenderer extends EntityRenderer<SuperLeashEntity> {
// 渲染参数
private static final int SEGMENTS = 24; // 绳子分段数
private static final float BASE_THICKNESS = 0.08f; // 基础线宽
private static final float CRITICAL_PULSE_SPEED = 0.2f; // 临界状态脉冲速度
public SuperLeashRenderer(EntityRendererProvider.Context context) {
super(context);
}
@Override
public void render(@NotNull SuperLeashEntity entity, float entityYaw, float partialTick,
@NotNull PoseStack poseStack, @NotNull MultiBufferSource buffer, int packedLight) {
Entity controlled = entity.getControlled();
if (!LeashDataImpl.isLeashable(controlled)) return;
// 创建渲染状态
List<SuperLeashRenderState> superLeashRenderStates = SuperLeashStateResolver.resolveAll(
controlled,
(LeashDataImpl) controlled.getCapability(CapabilityHandler.LEASH_DATA_CAP).orElseThrow(NullPointerException::new),
partialTick
);
// 渲染绳子
superLeashRenderStates.forEach(s -> renderLeash(s, poseStack, buffer, packedLight, partialTick));
}
private void renderLeash(SuperLeashRenderState state, PoseStack poseStack,
MultiBufferSource buffer, int packedLight, float partialTicks) {
poseStack.pushPose();
VertexConsumer vertexBuilder = buffer.getBuffer(SLPRenderType.leashType());
Vec3 start = state.startPos();
Vec3 end = state.endPos();
// 计算绳子总长度和方向
double length = start.distanceTo(end);
Vec3 direction = end.subtract(start).normalize();
// 临界状态闪烁效果
float alpha = state.isCritical() ?
(Mth.sin(partialTicks * CRITICAL_PULSE_SPEED) * 0.5f + 0.5f) :
1.0f;
// 分段渲染绳子
for (int i = 0; i <= SEGMENTS; i++) {
float progress = (float)i / SEGMENTS;
// 计算当前段位置和摆动偏移
Vec3 segmentPos = start.add(direction.scale(length * progress));
Vec3 swingOffset = state.getSwingOffset(progress, partialTicks);
// 应用张力拉伸效果
if (state.stretchRatio() > 1.0f) {
float stretchFactor = (float)Math.sin(progress * Math.PI) * 0.2f * (state.stretchRatio() - 1.0f);
segmentPos = segmentPos.add(direction.scale(stretchFactor * length));
}
// 添加摆动偏移
segmentPos = segmentPos.add(swingOffset);
// 计算颜色渐变 (从正常色到紧张色)
int color = interpolateColor(
SuperLeashRenderState.COLOR_NORMAL,
state.isCritical() ? SuperLeashRenderState.COLOR_CRITICAL : SuperLeashRenderState.COLOR_TENSION,
state.tension());
// 添加顶点
addVertex(vertexBuilder, poseStack, segmentPos,
state.thickness() * (1 - progress * 0.3f), // 末端稍细
color, alpha, packedLight);
}
poseStack.popPose();
}
private void addVertex(VertexConsumer builder, PoseStack poseStack, Vec3 pos,
float thickness, int color, float alpha, int packedLight) {
// 解包颜色
float r = ((color >> 16) & 0xFF) / 255.0f;
float g = ((color >> 8) & 0xFF) / 255.0f;
float b = (color & 0xFF) / 255.0f;
// 转换到相机空间
Vec3 viewPos = pos.subtract(this.entityRenderDispatcher.camera.getPosition());
// 添加顶点 (使用LINE_STRIP渲染)
builder.vertex(poseStack.last().pose(), (float)viewPos.x, (float)viewPos.y, (float)viewPos.z)
.color(r, g, b, alpha)
.uv2(packedLight)
.overlayCoords(OverlayTexture.NO_OVERLAY)
.normal(poseStack.last().normal(), 0, 1, 0)
.endVertex();
}
private int interpolateColor(int color1, int color2, float ratio) {
int r1 = (color1 >> 16) & 0xFF;
int g1 = (color1 >> 8) & 0xFF;
int b1 = color1 & 0xFF;
int r2 = (color2 >> 16) & 0xFF;
int g2 = (color2 >> 8) & 0xFF;
int b2 = color2 & 0xFF;
int r = (int)(r1 + (r2 - r1) * ratio);
int g = (int)(g1 + (g2 - g1) * ratio);
int b = (int)(b1 + (b2 - b1) * ratio);
return (r << 16) | (g << 8) | b;
}
@Override
public @NotNull ResourceLocation getTextureLocation(@NotNull SuperLeashEntity entity) {
return new ResourceLocation("unknown"); // 使用自定义渲染类型不需要纹理
}
}

View File

@ -20,7 +20,7 @@ import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
import top.r3944realms.superleadrope.SuperLeadRope;
import top.r3944realms.superleadrope.client.renderer.state.SuperLeashRenderState;
import top.r3944realms.superleadrope.content.capability.LeashDataImpl;
import top.r3944realms.superleadrope.content.capability.impi.LeashDataImpl;
import top.r3944realms.superleadrope.content.capability.inter.ILeashDataCapability;
import top.r3944realms.superleadrope.content.entity.SuperLeashKnotEntity;
@ -173,7 +173,6 @@ public class SuperLeashStateResolver {
/**
* 获取实体所有拴绳的渲染状态
*/
@SuppressWarnings("unchecked")
public static List<SuperLeashRenderState> resolveAll(
Entity leashedEntity,
LeashDataImpl leashData,

View File

@ -0,0 +1,57 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.config;
import net.minecraftforge.common.ForgeConfigSpec;
import java.util.List;
public class LeashCommonConfig {
public static ForgeConfigSpec.Builder BUILDER = new ForgeConfigSpec.Builder();
public static ForgeConfigSpec SPEC;
public static final Common COMMON;
static {
BUILDER.comment("Leash Common Config");
COMMON = new Common(BUILDER);
SPEC = BUILDER.build();
}
public static class Common {
public final ForgeConfigSpec.ConfigValue<List<? extends String>> teleportWhitelist;
public Common(ForgeConfigSpec.Builder builder) {
builder.push("leash");
teleportWhitelist = builder
.comment("Entity teleport whitelist.",
"Use `#modid` to allow teleporting to all entities from a mod.",
"Use `modid:entity_name` to allow teleporting to a specific entity.")
.defineListAllowEmpty(
List.of("allowedTeleportEntities"),
List.of("#minecraft", "modernlife:bicycle", "modernlife:motorboat"),
o -> o instanceof String s && isValidFormat(s)
);
builder.pop();
}
private static boolean isValidFormat(String s) {
if (s.startsWith("#")) {
return s.length() > 1 && s.substring(1).matches("[a-z0-9_]+");
}
return s.matches("[a-z0-9_]+:[a-z0-9_/]+");
}
}
}

View File

@ -0,0 +1,41 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.content;
import net.minecraft.world.damagesource.DamageEffects;
import net.minecraft.world.damagesource.DamageScaling;
import net.minecraft.world.damagesource.DamageType;
import net.minecraft.world.damagesource.DeathMessageType;
public class SLPDamageTypes {
// 非绑定主人使用惩罚
public static final DamageType ETERNAL_POTATO_NOT_OWNER =
new DamageType(
"eternal_potato_not_owner",
DamageScaling.ALWAYS,
0f,
DamageEffects.HURT,
DeathMessageType.DEFAULT
);
public static final DamageType ETERNAL_POTATO_NOT_COMPLETE =
new DamageType(
"eternal_potato_not_complete",
DamageScaling.ALWAYS,
0f,
DamageEffects.HURT,
DeathMessageType.DEFAULT
);
}

View File

@ -13,24 +13,21 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.datagen.data;
package top.r3944realms.superleadrope.content;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.TagKey;
import net.minecraft.world.item.Item;
import top.r3944realms.superleadrope.SuperLeadRope;
public final class SLPItemTags {
private SLPItemTags() {
public class SLPTags {
public static class Items {
public static final TagKey<Item> LEAD = tag("lead");
private static TagKey<net.minecraft.world.item.Item> tag(String name)
{
return TagKey.create(Registries.ITEM, new ResourceLocation(SuperLeadRope.MOD_ID, name));
}
}
private static TagKey<Item> bind(String name) {
return TagKey.create(Registries.ITEM, new ResourceLocation(name));
}
public static TagKey<Item> create(final ResourceLocation name) {
return TagKey.create(Registries.ITEM, name);
}
}

View File

@ -0,0 +1,71 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.content;
import net.minecraft.world.item.Tier;
import net.minecraft.world.item.crafting.Ingredient;
import org.jetbrains.annotations.NotNull;
import java.util.function.Supplier;
public enum SLPToolTier implements Tier {
STRING(24, 1.0F, 0.0F, 15, () -> Ingredient.of(SLPTags.Items.LEAD))
;
private final int uses;
private final float speed;
private final float attackDamageBonus;
private final int enchantmentValue;
private final Supplier<Ingredient> repairIngredient;
SLPToolTier(int uses, float speed, float attackDamageBonus, int enchantmentValue, Supplier<Ingredient> repairIngredient) {
this.uses = uses;
this.speed = speed;
this.attackDamageBonus = attackDamageBonus;
this.enchantmentValue = enchantmentValue;
this.repairIngredient = repairIngredient;
}
@Override
public int getUses() {
return this.uses;
}
@Override
public float getSpeed() {
return this.speed;
}
@Override
public float getAttackDamageBonus() {
return this.attackDamageBonus;
}
@Override
public int getLevel() {
return 0;
}
@Override
public int getEnchantmentValue() {
return this.enchantmentValue;
}
@Override
public @NotNull Ingredient getRepairIngredient() {
return this.repairIngredient.get();
}
}

View File

@ -16,18 +16,25 @@
package top.r3944realms.superleadrope.content.capability;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.CapabilityManager;
import net.minecraftforge.common.capabilities.CapabilityToken;
import net.minecraftforge.common.capabilities.RegisterCapabilitiesEvent;
import net.minecraftforge.event.AttachCapabilitiesEvent;
import top.r3944realms.superleadrope.content.capability.impi.LeashDataImpl;
import top.r3944realms.superleadrope.content.capability.inter.IEternalPotato;
import top.r3944realms.superleadrope.content.capability.inter.ILeashDataCapability;
import top.r3944realms.superleadrope.content.capability.provider.EternalPotatoProvider;
import top.r3944realms.superleadrope.content.capability.provider.LeashDataProvider;
import top.r3944realms.superleadrope.content.item.EternalPotatoItem;
public class CapabilityHandler {
public static final Capability<ILeashDataCapability> LEASH_DATA_CAP = CapabilityManager.get(new CapabilityToken<>(){});
public static Capability<IEternalPotato> ETERNAL_POTATO_CAP = CapabilityManager.get(new CapabilityToken<>() {});
public static void registerCapability(RegisterCapabilitiesEvent event) {
event.register(ILeashDataCapability.class);
event.register(IEternalPotato.class);
}
public static void attachCapability(AttachCapabilitiesEvent<?> event) {
@ -36,6 +43,9 @@ public class CapabilityHandler {
(LeashDataImpl.isLeashable(entity))//只对活体 矿车添加CAP
) {
event.addCapability(LeashDataProvider.LEASH_DATA_REL, new LeashDataProvider(entity));
} else if (object instanceof ItemStack stack && stack.getItem() instanceof EternalPotatoItem) {
event.addCapability(EternalPotatoProvider.ETERNAL_POTATO_DATA_REL, new EternalPotatoProvider(stack));
}
}
}

View File

@ -0,0 +1,306 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.content.capability.impi;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.server.ServerLifecycleHooks;
import top.r3944realms.superleadrope.content.capability.inter.IEternalPotato;
import top.r3944realms.superleadrope.core.punishment.IObligationCompletion;
import top.r3944realms.superleadrope.core.punishment.PunishmentDefinition;
import top.r3944realms.superleadrope.core.register.ObligationCompletionRegistry;
import top.r3944realms.superleadrope.network.NetworkHandler;
import top.r3944realms.superleadrope.network.toClient.EternalPotatoSyncCapPacket;
import java.util.UUID;
public class EternalPotatoImpl implements IEternalPotato {
private ItemStackSync itemStackSync;
private Player boundPlayer;
private UUID ownerUUID;
private volatile UUID itemUUID;
private String ownerName;
private boolean initializing = false;
private boolean dirtyDuringInit = false;
/**
* 每日任务次数
*/
private int dailyObligations = 0;
private int gracePeriod = 0;
/**
* 累计未完成触发的惩罚次数
*/
private int pendingPunishments = 0;
private String lastReset = "";
private String lastPunishDate = ""; // yyyy-MM-dd
private PunishmentDefinition punishment = PunishmentDefinition.DEFAULT;
private IObligationCompletion completionRule = IObligationCompletion.NONE;
// NBT Keys
public static final String TAG_LAST_PUNISH_DATE = "last_punish_date";
public static final String TAG_PENDING_PUNISHMENTS = "pending_punishments";
public static final String TAG_GRACE_PERIOD = "grace_period";
public static final String TAG_OWNER_UUID = "owner_uuid";
public static final String TAG_ITEM_UUID = "item_uuid";
public static final String TAG_OWNER_NAME = "owner_name";
public static final String TAG_OBLIGATIONS = "obligations";
public static final String TAG_LAST_RESET = "last_reset";
public static final String TAG_PUNISHMENT_TYPE = "punishment_type";
public static final String TAG_PUNISHMENT_STRENGTH = "punishment_strength";
public static final String TAG_PUNISHMENT_AFFECT_OTHERS = "punishment_affect_others";
public static final String TAG_COMPLETION_ID = "completion_id";
@Override
public void bindItemStackSync(ItemStackSync callback) {
this.itemStackSync = callback;
}
/**
* 开始初始化禁止立即发包
*/
public void beginInit() {
initializing = true;
dirtyDuringInit = false;
}
/**
* 结束初始化如果期间有改动就统一发一次包
*/
public void endInit() {
initializing = false;
if (dirtyDuringInit) {
// 同步到玩家
if (boundPlayer != null) {
syncToClient(boundPlayer);
}
// 写回 ItemStack NBT
if (itemStackSync != null) {
itemStackSync.markDirtyForItem();
}
// 重置脏标记
dirtyDuringInit = false;
}
}
public void bindSyncContext(Player player) {
if (isNetworkSyncNonRequired()) return;
this.boundPlayer = player;
}
public Player getBoundPlayer() {
return boundPlayer;
}
private void markDirty() {
if (initializing) {
dirtyDuringInit = true;
} else {
if (boundPlayer != null) {
syncToClient(boundPlayer);
}
if (itemStackSync != null) {
itemStackSync.markDirtyForItem();
}
}
}
@Override
public void setItemUUID(UUID uuid) {
if (uuid == null) throw new IllegalArgumentException("Item UUID cannot be null");
this.itemUUID = uuid;
}
@Override
public UUID getItemUUID() {
return itemUUID;
}
@Override
public void setOwner(UUID uuid, String name) {
if (uuid == null) throw new IllegalArgumentException("Owner UUID cannot be null");
if (name == null) name = "Unknown";
this.ownerUUID = uuid;
this.ownerName = name;
markDirty();
}
@Override
public UUID getOwnerUUID() {
return ownerUUID;
}
@Override
public String getOwnerName() {
return ownerName != null ? ownerName : "Unknown";
}
@Override
public void setDailyObligations(int count) {
if (count < 0) count = 0;
this.dailyObligations = count;
markDirty();
}
@Override
public int getDailyObligations() {
return dailyObligations;
}
@Override
public int getPendingPunishments() {
return pendingPunishments;
}
@Override
public void setPendingPunishments(int count) {
if (count < 0) count = 0;
this.pendingPunishments = count;
markDirty();
}
@Override
public int getGracePunishments() {
return gracePeriod;
}
@Override
public void setGracePunishments(int count) {
if (count < 0) count = 0;
this.gracePeriod = count;
markDirty();
}
@Override
public void syncToClient(Player player) {
if (isNetworkSyncNonRequired() || player.level().isClientSide) return;
EternalPotatoSyncCapPacket packet =
new EternalPotatoSyncCapPacket(
itemUUID, ownerUUID, ownerName,
dailyObligations,pendingPunishments, gracePeriod,
lastReset, lastPunishDate, punishment, completionRule
);
MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
if (server != null) {
for (ServerPlayer p : server.getPlayerList().getPlayers()) {
NetworkHandler.sendToPlayer(packet, p);
}
}
}
@Override
public void setLastReset(String date) {
this.lastReset = date;
markDirty();
}
@Override
public String getLastReset() { return lastReset; }
@Override
public PunishmentDefinition getPunishment() { return punishment; }
@Override
public void setPunishment(PunishmentDefinition definition) {
this.punishment = definition != null ? definition : PunishmentDefinition.DEFAULT;
markDirty();
}
@Override
public void setLastPunishDate(String date) {
this.lastPunishDate = date;
markDirty();
}
@Override
public String getLastPunishDate() {
return lastPunishDate;
}
@Override
public IObligationCompletion getCompletionRule() {
return completionRule;
}
@Override
public void setCompletionRule(IObligationCompletion completion) {
this.completionRule = completion != null ? completion : IObligationCompletion.NONE;
markDirty();
}
@Override
public CompoundTag serializeNBT() {
CompoundTag tag = new CompoundTag();
if (ownerUUID != null) tag.putUUID(TAG_OWNER_UUID, ownerUUID);
if (itemUUID != null) tag.putUUID(TAG_ITEM_UUID, itemUUID);
if (ownerName != null) tag.putString(TAG_OWNER_NAME, ownerName);
tag.putInt(TAG_OBLIGATIONS, dailyObligations);
tag.putInt(TAG_GRACE_PERIOD, gracePeriod);
if (lastReset != null) tag.putString(TAG_LAST_RESET, lastReset);
// 序列化 PunishmentDefinition record
if (punishment != null && punishment != PunishmentDefinition.DEFAULT) {
tag.putString(TAG_PUNISHMENT_TYPE, punishment.type().name());
tag.putFloat(TAG_PUNISHMENT_STRENGTH, punishment.strength());
tag.putBoolean(TAG_PUNISHMENT_AFFECT_OTHERS, punishment.affectOthers());
}
// 序列化任务完成规则
if (completionRule != null && completionRule != IObligationCompletion.NONE) {
tag.putString(TAG_COMPLETION_ID, completionRule.getId());
}
if (lastPunishDate != null) tag.putString(TAG_LAST_PUNISH_DATE, lastPunishDate);
tag.putInt(TAG_PENDING_PUNISHMENTS, pendingPunishments);
return tag;
}
@Override
public void deserializeNBT(CompoundTag nbt) {
if (nbt == null) return; // 卫语句防空
if (nbt.hasUUID(TAG_OWNER_UUID)) ownerUUID = nbt.getUUID(TAG_OWNER_UUID);
if (nbt.contains(TAG_ITEM_UUID)) itemUUID = nbt.getUUID(TAG_ITEM_UUID);
if (nbt.contains(TAG_OWNER_NAME)) ownerName = nbt.getString(TAG_OWNER_NAME);
if (nbt.contains(TAG_GRACE_PERIOD)) gracePeriod = Math.max(0, nbt.getInt(TAG_GRACE_PERIOD));
if (nbt.contains(TAG_OBLIGATIONS)) dailyObligations = Math.max(0, nbt.getInt(TAG_OBLIGATIONS));
if (nbt.contains(TAG_PENDING_PUNISHMENTS)) pendingPunishments = Math.max(0, nbt.getInt(TAG_PENDING_PUNISHMENTS));
if (nbt.contains(TAG_LAST_RESET)) lastReset = nbt.getString(TAG_LAST_RESET);
if (nbt.contains(TAG_LAST_PUNISH_DATE)) lastPunishDate = nbt.getString(TAG_LAST_PUNISH_DATE);
// PunishmentDefinition 反序列化
if (nbt.contains(TAG_PUNISHMENT_TYPE) && nbt.contains(TAG_PUNISHMENT_STRENGTH) && nbt.contains(TAG_PUNISHMENT_AFFECT_OTHERS)) {
try {
PunishmentDefinition.Type type = PunishmentDefinition.Type.valueOf(nbt.getString(TAG_PUNISHMENT_TYPE));
float strength = nbt.getFloat(TAG_PUNISHMENT_STRENGTH);
boolean affectOthers = nbt.getBoolean(TAG_PUNISHMENT_AFFECT_OTHERS);
punishment = new PunishmentDefinition(type, strength, affectOthers);
} catch (IllegalArgumentException e) {
punishment = PunishmentDefinition.DEFAULT;
}
}
// 完成规则反序列化
if (nbt.contains(TAG_COMPLETION_ID)) {
String id = nbt.getString(TAG_COMPLETION_ID);
IObligationCompletion rule = ObligationCompletionRegistry.byId(id);
completionRule = rule != null ? rule : IObligationCompletion.NONE;
}
}
}

View File

@ -13,20 +13,18 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.content.capability;
package top.r3944realms.superleadrope.content.capability.impi;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.*;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.ai.goal.RandomLookAroundGoal;
import net.minecraft.world.entity.ai.goal.RandomStrollGoal;
import net.minecraft.world.entity.animal.Animal;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.animal.horse.Llama;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.vehicle.Boat;
import net.minecraft.world.entity.vehicle.Minecart;
import net.minecraft.world.item.ItemStack;
@ -37,17 +35,19 @@ import net.minecraftforge.network.PacketDistributor;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import top.r3944realms.superleadrope.SuperLeadRope;
import top.r3944realms.superleadrope.content.capability.CapabilityHandler;
import top.r3944realms.superleadrope.content.capability.inter.ILeashDataCapability;
import top.r3944realms.superleadrope.content.entity.SuperLeashKnotEntity;
import top.r3944realms.superleadrope.network.NetworkHandler;
import top.r3944realms.superleadrope.network.toClient.LeashDataSyncPacket;
import top.r3944realms.superleadrope.network.toClient.UpdatePlayerMovementPacket;
import top.r3944realms.superleadrope.util.riding.RindingLeash;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 预期行为
@ -90,48 +90,49 @@ public class LeashDataImpl implements ILeashDataCapability {
private final Map<UUID, LeashInfo> leashHolders = new ConcurrentHashMap<>();
// 引入解决 绳结不保存导致第二进入持有者不存在的问题
private final Map<BlockPos, LeashInfo> leashKnots = new ConcurrentHashMap<>();
private CompoundTag lastSyncedData = new CompoundTag();
// private CompoundTag lastSyncedData = new CompoundTag();
public LeashDataImpl(Entity entity) {
this.entity = entity;
}
private void markForSync() {
@Override
public void markForSync() {
if (!entity.level().isClientSide) {
needsSync = true;
immediateSync();
immediateSync(); // 立即同步一次
}
}
private void immediateSync() {
NetworkHandler.INSTANCE.send(
PacketDistributor.TRACKING_ENTITY_AND_SELF.with(() -> entity),
new LeashDataSyncPacket(entity.getId(), serializeNBT())
/** 立即同步,无视时间间隔 */
@Override
public void immediateSync() {
syncNow();
}
/** 定期调用,每 tick 或每几秒检测 */
@Override
public void checkSync() {
if (!needsSync || entity.level().isClientSide) return;
long now = System.currentTimeMillis();
// 每隔 2 秒同步一次
if (now - lastSyncTime > 2000) {
syncNow();
}
}
/** 内部统一同步方法,避免重复逻辑 */
private void syncNow() {
CompoundTag currentData = serializeNBT();
NetworkHandler.sendToPlayer(
new LeashDataSyncPacket(entity.getId(), currentData),
entity,
PacketDistributor.TRACKING_ENTITY_AND_SELF
);
lastSyncTime = System.currentTimeMillis();
needsSync = false;
}
public void sync() {
if (!needsSync || entity.level().isClientSide) return;
CompoundTag currentData = serializeNBT();
if (!currentData.equals(lastSyncedData)) {
NetworkHandler.INSTANCE.send(
PacketDistributor.TRACKING_ENTITY_AND_SELF.with(() -> entity),
new LeashDataSyncPacket(entity.getId(), currentData)
);
lastSyncTime = System.currentTimeMillis();
lastSyncedData = currentData;
needsSync = false;
}
}
// 定期同步检查
public void checkSync() {
if (!needsSync) return;
// 距离上次同步超过0.1秒才同步
if (System.currentTimeMillis() - lastSyncTime > 100) {
sync();
}
}
// 添加拴绳支持自定义最大长度和弹性距离
@Override
@ -366,34 +367,25 @@ public class LeashDataImpl implements ILeashDataCapability {
}
}
// 应用合力
if (!combinedForce.equals(Vec3.ZERO)) {
if(entity instanceof ServerPlayer serverPlayer) { //对于玩家发包交给客户端处理移动
NetworkHandler.sendToPlayer(
new UpdatePlayerMovementPacket(
UpdatePlayerMovementPacket.Operation.ADD,
combinedForce
), serverPlayer
);
return; //后面的逻辑肯定与该分支无关直接返回
boolean hasForce = !combinedForce.equals(Vec3.ZERO);
Entity finalApplyEntity = RindingLeash.getFinalEntityForLeashIfForce(entity, hasForce);
if (hasForce) {
if (finalApplyEntity instanceof ServerPlayer player) {
RindingLeash.applyForceToPlayer(player, combinedForce);
return;
} else {
entity.setDeltaMovement(entity.getDeltaMovement().add(combinedForce));
entity.hurtMarked = true;
finalApplyEntity.setDeltaMovement(finalApplyEntity.getDeltaMovement().add(combinedForce));
finalApplyEntity.hurtMarked = true;
}
// 有拴绳时禁用移动控制
if (entity instanceof Animal mob) {
mob.goalSelector.disableControlFlag(Goal.Flag.MOVE);
entity.resetFallDistance();
}
RindingLeash.protectAnimalMovement(finalApplyEntity, true);
} else {
// 无拴绳时恢复移动控制
if (entity instanceof Animal mob) {
mob.goalSelector.enableControlFlag(Goal.Flag.MOVE);
}
RindingLeash.protectAnimalMovement(finalApplyEntity, false);
}
}
/**
* 为UUID拴绳计算力
*/
@ -403,7 +395,8 @@ public class LeashDataImpl implements ILeashDataCapability {
if (uuidHolder != null) {
return calculateLeashForce(uuidHolder, entry);
} else {
SuperLeadRope.logger.error("Could not apply leash forces for {}, because it is not found.", uuid);
SuperLeadRope.logger.error("Could not apply leash forces for {}, because it is not found(it will be removed from list).", uuid);
leashHolders.remove(uuid);
return null;
}
}
@ -608,9 +601,10 @@ public class LeashDataImpl implements ILeashDataCapability {
// 获取所有拴绳信息
@Override
public Collection<LeashInfo> getAllLeashes() {
Collection<LeashInfo> values = leashHolders.values();
values.addAll(leashKnots.values());
return values;
return Stream.concat(
leashHolders.values().stream(),
leashKnots.values().stream()
).collect(Collectors.toList());
}
@Override
@ -785,20 +779,12 @@ public class LeashDataImpl implements ILeashDataCapability {
}
public static boolean isLeashHolder(@NotNull Entity pEntity, UUID pHolderUUID) {
AtomicBoolean isTarget = new AtomicBoolean(false);
pEntity.getCapability(CapabilityHandler.LEASH_DATA_CAP).ifPresent(i -> {
if (i instanceof LeashDataImpl li) {
isTarget.set(li.isLeashedBy(pHolderUUID));
}
});
pEntity.getCapability(CapabilityHandler.LEASH_DATA_CAP).ifPresent(i -> isTarget.set(i.isLeashedBy(pHolderUUID)));
return isTarget.get();
}
public static boolean isLeashHolder(@NotNull Entity pEntity, BlockPos pKnotPos) {
AtomicBoolean isTarget = new AtomicBoolean(false);
pEntity.getCapability(CapabilityHandler.LEASH_DATA_CAP).ifPresent(i -> {
if (i instanceof LeashDataImpl li) {
isTarget.set(li.isLeashedBy(pKnotPos));
}
});
pEntity.getCapability(CapabilityHandler.LEASH_DATA_CAP).ifPresent(i -> isTarget.set(i.isLeashedBy(pKnotPos)));
return isTarget.get();
}
public static boolean isLeashHolder(@NotNull Entity pEntity, Entity pTestHolder) {

View File

@ -0,0 +1,86 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.content.capability.inter;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.entity.player.Player;
import top.r3944realms.superleadrope.core.potato.EternalPotatoFacade;
import top.r3944realms.superleadrope.core.punishment.IObligationCompletion;
import top.r3944realms.superleadrope.core.punishment.PunishmentDefinition;
import java.util.UUID;
public interface IEternalPotato {
interface ItemStackSync {
void markDirtyForItem();
}
void bindItemStackSync(ItemStackSync callback);
void beginInit();
void endInit();
void setItemUUID(UUID uuid);
UUID getItemUUID();
void setOwner(UUID uuid, String name);
UUID getOwnerUUID();
String getOwnerName();
void setDailyObligations(int count);
int getDailyObligations();
int getPendingPunishments();
void setPendingPunishments(int count);
/**
* 计算今日总任务次数 = 未完成惩罚次数 + 当日剩余次数
*/
default int getFinalTaskCount() {
return getPendingPunishments() + getDailyObligations();
}
default boolean isNetworkSyncNonRequired() {
return !EternalPotatoFacade.isServer();
}
default boolean isGlobalEffect() {
// 根据当前惩罚任务规则决定
return getPunishment() != null && getPunishment().affectOthers();
}
/**
* 是否在宽限期内
*/
default boolean isWithinGracePeriod() {
return getPendingPunishments() <= getGracePunishments();
}
/**
* 宽限惩罚数
*/
int getGracePunishments();
void setGracePunishments(int count);
void syncToClient(Player player);
void bindSyncContext(Player player);
Player getBoundPlayer();
void setLastReset(String date);
String getLastReset();
PunishmentDefinition getPunishment();
void setPunishment(PunishmentDefinition definition);
void setLastPunishDate(String date);
String getLastPunishDate();
IObligationCompletion getCompletionRule();
void setCompletionRule(IObligationCompletion completion);
CompoundTag serializeNBT();
void deserializeNBT(CompoundTag nbt);
}

View File

@ -70,7 +70,9 @@ public interface ILeashDataCapability extends INBTSerializable<CompoundTag> {
boolean canBeLeashed();
boolean canBeAttachedTo(Entity pEntity);
void markForSync();
void immediateSync();
void checkSync();
record LeashInfo(
Optional<BlockPos> blockPosOpt,

View File

@ -0,0 +1,106 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.content.capability.provider;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.ICapabilitySerializable;
import net.minecraftforge.common.util.LazyOptional;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import top.r3944realms.superleadrope.SuperLeadRope;
import top.r3944realms.superleadrope.content.capability.CapabilityHandler;
import top.r3944realms.superleadrope.content.capability.inter.IEternalPotato;
import top.r3944realms.superleadrope.content.item.EternalPotatoItem;
import top.r3944realms.superleadrope.core.potato.EternalPotatoFacade;
import java.util.UUID;
public class EternalPotatoProvider implements ICapabilitySerializable<CompoundTag> {
public static final ResourceLocation ETERNAL_POTATO_DATA_REL =
new ResourceLocation(SuperLeadRope.MOD_ID, "eternal_potato_data");
private final UUID uuid;
private volatile IEternalPotato instance;
private final LazyOptional<IEternalPotato> optional;
// 新增引用 ItemStack
private final ItemStack stack;
public EternalPotatoProvider(ItemStack stack) {
this.stack = stack;
this.uuid = EternalPotatoItem.getOrCreateItemUUID(stack);
this.instance = null;
this.optional = LazyOptional.of(this::resolveInstance);
}
private IEternalPotato resolveInstance() {
if (instance == null) {
synchronized (this) {
if (instance == null) {
instance = EternalPotatoFacade.getOrCreate(uuid);
// 绑定写回回调只绑定一次
instance.bindItemStackSync(() -> {
if (!stack.isEmpty()) {
stack.setTag(instance.serializeNBT());
}
});
}
}
}
return instance;
}
@Override
public @NotNull <T> LazyOptional<T> getCapability(@NotNull Capability<T> cap, @Nullable Direction side) {
return CapabilityHandler.ETERNAL_POTATO_CAP.orEmpty(cap, optional);
}
@Override
public CompoundTag serializeNBT() {
return resolveInstance() != null ? resolveInstance().serializeNBT() : new CompoundTag();
}
@Override
public void deserializeNBT(CompoundTag compoundTag) {
if (compoundTag == null) return;
// 只在首次 attach 时初始化 UUID
if (instance == null) {
UUID loaded = compoundTag.hasUUID("item_uuid")
? compoundTag.getUUID("item_uuid")
: UUID.randomUUID();
IEternalPotato shared = EternalPotatoFacade.getOrCreate(loaded);
// 不再调用 shared.deserializeNBT(compoundTag) Manager 统一负责持久化
this.instance = shared;
// 绑定写回回调
shared.bindItemStackSync(() -> {
if (!stack.isEmpty()) {
stack.setTag(shared.serializeNBT());
}
});
}
}
}

View File

@ -13,7 +13,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.content.capability;
package top.r3944realms.superleadrope.content.capability.provider;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
@ -25,6 +25,8 @@ import net.minecraftforge.common.util.LazyOptional;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import top.r3944realms.superleadrope.SuperLeadRope;
import top.r3944realms.superleadrope.content.capability.CapabilityHandler;
import top.r3944realms.superleadrope.content.capability.impi.LeashDataImpl;
import top.r3944realms.superleadrope.content.capability.inter.ILeashDataCapability;
public class LeashDataProvider implements ICapabilitySerializable<CompoundTag> {

View File

@ -1,74 +0,0 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.content.entity;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.NotNull;
import top.r3944realms.superleadrope.content.capability.CapabilityHandler;
import top.r3944realms.superleadrope.content.capability.LeashDataImpl;
import top.r3944realms.superleadrope.content.capability.inter.ILeashDataCapability;
import top.r3944realms.superleadrope.core.register.SLPEntityTypes;
public class SuperLeashEntity extends Entity {
private Entity controlled;
public SuperLeashEntity(EntityType<? extends SuperLeashEntity> entityType, Level level) {
super(entityType, level);
}
public SuperLeashEntity(Level level, Entity controlled) {
super(SLPEntityTypes.SUPER_LEASH.get(), level);
if (!LeashDataImpl.isLeashable(controlled)) {
throw new IllegalArgumentException("Controlled entity " + controlled.getClass().getName() + "is not a leashable entity");
}
this.controlled = controlled;
}
public Entity getControlled() {
return controlled;
}
@Override
public void tick() {
super.tick();
if (controlled == null || !controlled.isAlive()) {
this.discard();
}
this.setPos(controlled.getX(), controlled.getY(), controlled.getZ());
if(!level().isClientSide) controlled.getCapability(CapabilityHandler.LEASH_DATA_CAP).ifPresent(ILeashDataCapability::applyLeashForces);
}
@Override
public void kill() {
}
@Override
protected void defineSynchedData() {
}
@Override
protected void readAdditionalSaveData(@NotNull CompoundTag compoundTag) {
}
@Override
protected void addAdditionalSaveData(@NotNull CompoundTag compoundTag) {
}
}

View File

@ -16,6 +16,8 @@
package top.r3944realms.superleadrope.content.entity;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.tags.BlockTags;
import net.minecraft.tags.TagKey;
import net.minecraft.world.InteractionHand;
@ -25,6 +27,7 @@ import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.decoration.LeashFenceKnotEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
@ -32,7 +35,7 @@ import net.minecraft.world.level.gameevent.GameEvent;
import net.minecraft.world.phys.AABB;
import org.jetbrains.annotations.NotNull;
import top.r3944realms.superleadrope.content.capability.CapabilityHandler;
import top.r3944realms.superleadrope.content.capability.LeashDataImpl;
import top.r3944realms.superleadrope.content.capability.impi.LeashDataImpl;
import top.r3944realms.superleadrope.core.register.SLPEntityTypes;
import java.util.Arrays;
@ -71,8 +74,12 @@ public class SuperLeashKnotEntity extends LeashFenceKnotEntity {
return false;
} else {
if (!this.isRemoved() && !this.level().isClientSide) {
if (source.getEntity() instanceof ServerPlayer player && player.gameMode.getGameModeForPlayer() == GameType.ADVENTURE) {
return false;
}
this.kill();
this.markHurt();
this.playSound(SoundEvents.LEASH_KNOT_BREAK);
List<Entity> entities = LeashDataImpl.leashableInArea(this.level(), pos.getCenter(), entity -> LeashDataImpl.isLeashHolder(entity, this));
entities.forEach(entity -> entity
.getCapability(CapabilityHandler.LEASH_DATA_CAP)
@ -169,9 +176,11 @@ public class SuperLeashKnotEntity extends LeashFenceKnotEntity {
}
AtomicBoolean isRemoveLeashKnot = new AtomicBoolean(false);
if (!isTransferLeash.get()) {
this.discard();
if (player.getAbilities().instabuild) {
entities.forEach(
if (((ServerPlayer) player).gameMode.getGameModeForPlayer() != GameType.ADVENTURE) {
this.playSound(SoundEvents.LEASH_KNOT_BREAK);
this.discard();
List<Entity> entities1 = LeashDataImpl.leashableInArea(this);
entities1.forEach(
entity -> entity
.getCapability(CapabilityHandler.LEASH_DATA_CAP)
.ifPresent(iLeashDataCapability -> {
@ -180,6 +189,8 @@ public class SuperLeashKnotEntity extends LeashFenceKnotEntity {
}
));
}
} else {
this.playSound(SoundEvents.LEASH_KNOT_PLACE);
}
if (isTransferLeash.get() || isRemoveLeashKnot.get()) {
this.gameEvent(GameEvent.BLOCK_ATTACH, player);

View File

@ -0,0 +1,245 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.content.item;
import net.minecraft.core.Holder;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.*;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.Level;
import net.minecraftforge.common.capabilities.ICapabilityProvider;
import org.jetbrains.annotations.NotNull;
import top.r3944realms.superleadrope.content.SLPDamageTypes;
import top.r3944realms.superleadrope.content.capability.CapabilityHandler;
import top.r3944realms.superleadrope.content.capability.provider.EternalPotatoProvider;
import top.r3944realms.superleadrope.datagen.data.SLPLangKeyValue;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
public class EternalPotatoItem extends Item {
private static final String ITEM_UUID_TAG = "item_uuid";
public EternalPotatoItem(Properties properties) {
super(properties.rarity(Rarity.EPIC).stacksTo(1));
}
@Override
public ICapabilityProvider initCapabilities(ItemStack stack, CompoundTag nbt) {
return new EternalPotatoProvider(stack);
}
/** 获取或生成 ItemStack 的唯一 UUID */
public static UUID getOrCreateItemUUID(ItemStack stack) {
CompoundTag tag = stack.getOrCreateTag();
if (tag.hasUUID(ITEM_UUID_TAG)) {
return tag.getUUID(ITEM_UUID_TAG);
} else {
UUID uuid = UUID.randomUUID();
tag.putUUID(ITEM_UUID_TAG, uuid);
return uuid;
}
}
/** 使用契约物品完成义务 */
@Override
public @NotNull InteractionResult useOn(UseOnContext context) {
Player player = context.getPlayer();
if (player == null || context.getLevel().isClientSide) return InteractionResult.PASS;
ItemStack stack = context.getItemInHand();
stack.getCapability(CapabilityHandler.ETERNAL_POTATO_CAP).ifPresent(cap -> {
if (cap.getCompletionRule().isCompleted((ServerPlayer) player, stack)) {
cap.getCompletionRule().onCompleted((ServerPlayer) player, stack);
cap.beginInit();
cap.setPendingPunishments(Math.max(0, cap.getPendingPunishments() - 1));
cap.endInit();
}
});
return InteractionResult.sidedSuccess(player.level().isClientSide);
}
@Override
public void appendHoverText(@NotNull ItemStack stack, Level world, @NotNull List<Component> tooltip, @NotNull TooltipFlag flag) {
stack.getCapability(CapabilityHandler.ETERNAL_POTATO_CAP).ifPresent(cap -> {
tooltip.add(Component.translatable(SLPLangKeyValue.EP_TOOLTIP_TITLE.getKey()));
tooltip.add(Component.translatable(SLPLangKeyValue.EP_DESC_TOOLTIP.getKey()));
if (cap.getOwnerUUID() != null) {
tooltip.add(Component.translatable(SLPLangKeyValue.EP_OBLIGATION_TOOLTIP.getKey(), cap.getFinalTaskCount(), cap.getPendingPunishments()));
if (cap.getPendingPunishments() > cap.getGracePunishments())
tooltip.add(Component.translatable(SLPLangKeyValue.EP_PUNISH_TOOLTIP.getKey(), cap.getPendingPunishments(), cap.getGracePunishments()));
tooltip.add(Component.translatable(SLPLangKeyValue.EP_BIND_OWNER.getKey(), cap.getOwnerName()));
} else {
tooltip.add(Component.translatable(SLPLangKeyValue.EP_UNBOUND.getKey()));
}
});
}
@Override
public @NotNull InteractionResultHolder<ItemStack> use(@NotNull Level level, @NotNull Player player, @NotNull InteractionHand hand) {
ItemStack stack = player.getItemInHand(hand);
if (level.isClientSide) {
// 客户端只返回结果不修改任何数据
return InteractionResultHolder.consume(stack);
}
// 服务端逻辑
if (player.getCooldowns().isOnCooldown(this)) return InteractionResultHolder.fail(stack);
stack.getCapability(CapabilityHandler.ETERNAL_POTATO_CAP).ifPresent(cap -> {
// 首次绑定
if (cap.getOwnerUUID() == null) {
cap.beginInit();
if (player instanceof ServerPlayer serverPlayer) cap.bindSyncContext(serverPlayer);
cap.setOwner(player.getUUID(), player.getDisplayName().getString());
cap.setDailyObligations(99);
cap.setPendingPunishments(0);
cap.endInit();
player.displayClientMessage(Component.translatable(SLPLangKeyValue.EP_BIND_MSG.getKey()), true);
cap.syncToClient(player); // 只在服务端发包
return;
}
// 已绑定 只在服务端更新
if (cap.getBoundPlayer() == null || !cap.getBoundPlayer().getUUID().equals(player.getUUID())) {
if (player instanceof ServerPlayer serverPlayer) cap.bindSyncContext(serverPlayer);
assert player instanceof ServerPlayer;
cap.syncToClient(player);
}
boolean isOwner = cap.getOwnerUUID().equals(player.getUUID());
if (!isOwner) {
cap.getPunishment().execute(
(ServerPlayer) player,
new DamageSource(Holder.direct(SLPDamageTypes.ETERNAL_POTATO_NOT_OWNER), player),
Component.translatable(SLPLangKeyValue.EP_PUNISH_NOT_OWNER.getKey())
);
} else {
player.startUsingItem(hand);
player.heal(50.0F);
player.getFoodData().eat(18, 8);
player.displayClientMessage(Component.translatable(SLPLangKeyValue.EP_OBLIGATION_INFO.getKey(), cap.getDailyObligations()), true);
player.getCooldowns().addCooldown(this, 20 * 3);
}
});
return InteractionResultHolder.sidedSuccess(stack, false);
}
@Override
public int getUseDuration(@NotNull ItemStack stack) {
return 32;
}
@Override
public @NotNull UseAnim getUseAnimation(@NotNull ItemStack stack) {
return UseAnim.EAT;
}
@Override
public @NotNull ItemStack finishUsingItem(@NotNull ItemStack stack, @NotNull Level level, @NotNull LivingEntity entity) {
if (entity instanceof Player player && !level.isClientSide) {
player.heal(50.0F);
player.getFoodData().eat(18, 8);
player.displayClientMessage(Component.translatable(SLPLangKeyValue.EP_POTATO_HEAL.getKey()), true);
player.getCooldowns().addCooldown(this, 20 * 3);
}
return stack.copy();
}
public static String getDescKey(String name) {
return "item.eternal_potato.tooltip." + name;
}
public static String getMsgKey(String name) {
return "item.eternal_potato.msg." + name;
}
@Override
public boolean canBeHurtBy(@NotNull DamageSource source) {
return false;
}
@Override
public boolean canFitInsideContainerItems() {
return false;
}
@Override
public boolean isFoil(@NotNull ItemStack stack) {
return true;
}
@Override
public void inventoryTick(@NotNull ItemStack stack, @NotNull Level level, @NotNull Entity entity, int slot, boolean selected) {
super.inventoryTick(stack, level, entity, slot, selected);
// 延迟初始化客户端逻辑
// if (level.isClientSide) { // 只在客户端执行
// // 直接通过 ItemStack UUID 调用 Manager
// UUID uuid = EternalPotatoItem.getOrCreateItemUUID(stack);
// EternalPotatoFacade.getOrCreate(uuid);
// }
}
public static void ensureItemInInventory(Player player, ItemStack stack) {
if (player.getInventory().add(stack)) return;
List<ItemStack> nonEmptySlots = new ArrayList<>();
for (ItemStack s : player.getInventory().items) {
if (!s.isEmpty() && s != stack) nonEmptySlots.add(s);
}
if (!nonEmptySlots.isEmpty()) {
ItemStack toDrop = nonEmptySlots.get(player.getRandom().nextInt(nonEmptySlots.size()));
player.drop(toDrop.copy(), true, false);
player.getInventory().removeItem(toDrop);
player.getInventory().add(stack);
return;
}
ItemStack currentHelmet = player.getInventory().armor.get(3);
if (!currentHelmet.isEmpty()) {
Objects.requireNonNull(player.drop(currentHelmet.copy(), true, false))
.setPos(player.getX(), player.getY() + 1, player.getZ());
}
player.getInventory().armor.set(3, stack);
}
@Override
public boolean canBeDepleted() {
return false;
}
}

View File

@ -22,7 +22,7 @@ import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.LeadItem;
import net.minecraft.world.item.TieredItem;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
@ -30,8 +30,9 @@ import net.minecraft.world.level.gameevent.GameEvent;
import net.minecraftforge.common.extensions.IForgeItem;
import net.minecraftforge.common.util.LazyOptional;
import org.jetbrains.annotations.NotNull;
import top.r3944realms.superleadrope.content.SLPToolTier;
import top.r3944realms.superleadrope.content.capability.CapabilityHandler;
import top.r3944realms.superleadrope.content.capability.LeashDataImpl;
import top.r3944realms.superleadrope.content.capability.impi.LeashDataImpl;
import top.r3944realms.superleadrope.content.capability.inter.ILeashDataCapability;
import top.r3944realms.superleadrope.content.entity.SuperLeashKnotEntity;
import top.r3944realms.superleadrope.core.register.SLPSoundEvents;
@ -50,7 +51,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
// 6. 不可消耗的但是有耐久需要修复, 通过消耗原版拴绳恢复每次多重绑定就会消耗1点耐久
// 实现拴生物在生物的interact方法里去写相关逻辑
// 尝试0 mixin 实现 加强拴绳逻辑
public class SuperLeadRopeItem extends LeadItem implements IForgeItem {
public class SuperLeadRopeItem extends TieredItem implements IForgeItem {
// 配置常量
// 手动调节,可以通过附魔获取更远抛掷和抛掷距离 - x1.3//TODO:将可抛掷实现留到下次编写
// 可以做个大于一定距离时远距离使用时抛出拴绳的实体击中生物才栓中的
@ -59,12 +60,13 @@ public class SuperLeadRopeItem extends LeadItem implements IForgeItem {
public SuperLeadRopeItem(@NotNull Properties pProperties) {
super(
super(SLPToolTier.STRING,
pProperties
.durability(1024)
.setNoRepair()
);
}
//通过按键 可抛掷启用/关闭实现不会影响use逻辑
@Override
public @NotNull InteractionResultHolder<ItemStack> use(@NotNull Level pLevel, @NotNull Player pPlayer, @NotNull InteractionHand pUsedHand) {
@ -122,17 +124,11 @@ public class SuperLeadRopeItem extends LeadItem implements IForgeItem {
for(Entity e : list) {
AtomicBoolean canBeAttachedTo = new AtomicBoolean(false);
LazyOptional<ILeashDataCapability> iLeashDataCapability = e.getCapability(CapabilityHandler.LEASH_DATA_CAP);
iLeashDataCapability.ifPresent(i -> {
if (i instanceof LeashDataImpl li) {
canBeAttachedTo.set(li.canBeAttachedTo(newHolder));
}
});
iLeashDataCapability.ifPresent(i -> canBeAttachedTo.set(i.canBeAttachedTo(newHolder)));
if(canBeAttachedTo.get()) {//canBeAttachedTo
iLeashDataCapability.ifPresent(i -> {
if(i instanceof LeashDataImpl li) {
li.transferLeash(player.getUUID(), newHolder, leashStack);
i.transferLeash(player.getUUID(), newHolder, leashStack);
isSuccess.set(true);
}
});
}
}
@ -179,10 +175,8 @@ public class SuperLeadRopeItem extends LeadItem implements IForgeItem {
SuperLeashKnotEntity finalKnot1 = knot;
player.getCapability(CapabilityHandler.LEASH_DATA_CAP).ifPresent(i -> {
if(i instanceof LeashDataImpl li) {
li.addLeash(finalKnot1, leashStack, 8D);
i.addLeash(finalKnot1, leashStack, 8D);
isSuccess.set(true);
}
});
}
@ -195,17 +189,11 @@ public class SuperLeadRopeItem extends LeadItem implements IForgeItem {
AtomicBoolean canBeAttachedTo = new AtomicBoolean(false);
SuperLeashKnotEntity finalKnot = knot;
LazyOptional<ILeashDataCapability> iLeashDataCapability = e.getCapability(CapabilityHandler.LEASH_DATA_CAP);
iLeashDataCapability.ifPresent(i -> {
if (i instanceof LeashDataImpl li) {
canBeAttachedTo.set(li.canBeAttachedTo(finalKnot));
}
});
iLeashDataCapability.ifPresent(i -> canBeAttachedTo.set(i.canBeAttachedTo(finalKnot)));
if(canBeAttachedTo.get()) {//canBeAttachedTo
iLeashDataCapability.ifPresent(i -> {
if(i instanceof LeashDataImpl li) {
li.transferLeash(uuid, finalKnot);
i.transferLeash(uuid, finalKnot);
isSuccess.set(true);
}
});
}
}

View File

@ -0,0 +1,40 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.core.exception;
import java.util.UUID;
public class RidingCycleException extends IllegalStateException {
private final UUID entityId;
private final UUID vehicleId;
public RidingCycleException(UUID entityId, UUID vehicleId) {
super(String.format("Cyclic riding reference detected. " +
"Entity %s cannot be added as passenger to vehicle %s " +
"as it would create a circular dependency.",
entityId, vehicleId));
this.entityId = entityId;
this.vehicleId = vehicleId;
}
public UUID getEntityId() {
return entityId;
}
public UUID getVehicleId() {
return vehicleId;
}
}

View File

@ -26,7 +26,7 @@ import net.minecraft.world.level.gameevent.GameEvent;
import net.minecraftforge.common.util.LazyOptional;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
import top.r3944realms.superleadrope.content.capability.CapabilityHandler;
import top.r3944realms.superleadrope.content.capability.LeashDataImpl;
import top.r3944realms.superleadrope.content.capability.impi.LeashDataImpl;
import top.r3944realms.superleadrope.content.capability.inter.ILeashDataCapability;
import top.r3944realms.superleadrope.content.item.SuperLeadRopeItem;
import top.r3944realms.superleadrope.core.register.SLPItems;
@ -39,6 +39,13 @@ public class LeashInteractHandler {
// ===== 卫语句 =====
if (level.isClientSide) {
if (hand == InteractionHand.MAIN_HAND &&
(player.getItemInHand(InteractionHand.MAIN_HAND).is(SLPItems.SUPER_LEAD_ROPE.get()) ||
player.getItemInHand(InteractionHand.OFF_HAND).is(SLPItems.SUPER_LEAD_ROPE.get()))
) {
event.setCanceled(true);
event.setCancellationResult(InteractionResult.CONSUME);
}
return;
}
if (hand == InteractionHand.OFF_HAND) {
@ -69,12 +76,14 @@ public class LeashInteractHandler {
event.setCancellationResult(InteractionResult.CONSUME);
}
} else {
if(LeashDataImpl.isLeashHolder(target, player)) {
if (LeashDataImpl.isLeashHolder(target, player)) {
LeashCap.ifPresent(
iLeashDataCapability -> iLeashDataCapability.removeLeash(player.getUUID())
);
target.gameEvent(GameEvent.ENTITY_INTERACT, player);
target.playSound(SLPSoundEvents.LEAD_UNTIED.get());
event.setCanceled(true);
event.setCancellationResult(InteractionResult.CONSUME);
return;
}
ItemStack itemStack;
@ -92,8 +101,10 @@ public class LeashInteractHandler {
boolean success = iLeashDataCapability.addLeash(player, itemStack, 12);
if (success) {
if(!player.isCreative())
itemStack.hurtAndBreak(50, player, e->{});
itemStack.hurtAndBreak(24, player, e->{});
target.playSound(SLPSoundEvents.LEAD_TIED.get());
event.setCanceled(true);
event.setCancellationResult(InteractionResult.CONSUME);
}
}
});
@ -101,6 +112,5 @@ public class LeashInteractHandler {
}
}
}
}

View File

@ -0,0 +1,108 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.core.potato;
import net.minecraft.server.level.ServerLevel;
import net.minecraftforge.server.ServerLifecycleHooks;
import top.r3944realms.superleadrope.content.capability.inter.IEternalPotato;
import top.r3944realms.superleadrope.core.util.PotatoMode;
import top.r3944realms.superleadrope.core.util.PotatoModeHelper;
import top.r3944realms.superleadrope.network.NetworkHandler;
import top.r3944realms.superleadrope.network.toClient.PacketEternalPotatoRemovePacket;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 外观类统一入口
* 调用方只管 Facade不关心底层是 Local 还是 Synced
*/
public class EternalPotatoFacade {
private static IEternalPotatoManager manager;
private static PotatoSavedData savedData;
// 全局监听器
private static final List<IEternalPotatoChangeListener> listeners = new CopyOnWriteArrayList<>();
public static void addListener(IEternalPotatoChangeListener listener) {
listeners.add(listener);
}
public static void removeListener(IEternalPotatoChangeListener listener) {
listeners.remove(listener);
}
// 内部方法用于通知变化
static void notifyChange(UUID uuid, IEternalPotato potato) {
listeners.forEach(l -> l.onPotatoChanged(uuid, potato));
}
public static IEternalPotatoManager getManager() {
return manager;
}
public static PotatoSavedData getSavedData() {
return savedData;
}
public static void initSavedData(ServerLevel serverLevel) {
savedData = PotatoSavedData.create(serverLevel);
}
/**
* 初始化进入世界时调用
* @param mode 当前运行模式
* @param isServer 是否在服务端
*/
public static void init(PotatoMode mode, boolean isServer) {
if (manager != null) {
return; // 已经有 manager说明重复初始化直接返回
}
switch (mode) {
case INTEGRATED -> manager = new LocalEternalPotatoManager();
case DEDICATED, REMOTE_CLIENT -> manager = new SyncedEternalPotatoManager(isServer);
}
}
public static IEternalPotato getOrCreate(UUID uuid) {
if (manager == null) throw new IllegalStateException("EternalPotatoFacade not initialized!");
IEternalPotato potato = manager.getOrCreate(uuid);
// 绑定统一回调
potato.bindItemStackSync(() -> {
if (savedData != null) savedData.setDirty(); // 标记 SavedData
notifyChange(uuid, potato); // 通知全局监听器
});
return potato;
}
public static void remove(UUID uuid) {
if (manager != null) manager.remove(uuid);
// Synced 模式才发包
if (PotatoModeHelper.getCurrentMode().isSynced() && ServerLifecycleHooks.getCurrentServer() != null) {
ServerLifecycleHooks.getCurrentServer().getPlayerList().getPlayers().forEach(player -> {
PacketEternalPotatoRemovePacket packet = new PacketEternalPotatoRemovePacket(uuid);
NetworkHandler.sendToPlayer(packet, player);
});
}
}
public static void clear() {
if (manager != null) manager.clear();
}
public static boolean isServer() {
return (manager instanceof SyncedEternalPotatoManager synced) && synced.isServer();
}
}

View File

@ -13,7 +13,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.config;
package top.r3944realms.superleadrope.core.potato;
public class CommonConfig {
import top.r3944realms.superleadrope.content.capability.inter.IEternalPotato;
import java.util.UUID;
public interface IEternalPotatoChangeListener {
void onPotatoChanged(UUID uuid, IEternalPotato potato);
}

View File

@ -0,0 +1,31 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.core.potato;
import net.minecraft.nbt.CompoundTag;
import top.r3944realms.superleadrope.content.capability.inter.IEternalPotato;
import java.util.UUID;
public interface IEternalPotatoManager {
IEternalPotato getOrCreate(UUID uuid);
void remove(UUID uuid);
void clear();
CompoundTag saveAll();
void loadAll(CompoundTag tag);
}

View File

@ -0,0 +1,76 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.core.potato;
import net.minecraft.nbt.CompoundTag;
import top.r3944realms.superleadrope.SuperLeadRope;
import top.r3944realms.superleadrope.content.capability.impi.EternalPotatoImpl;
import top.r3944realms.superleadrope.content.capability.inter.IEternalPotato;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* 单人世界 & 局域网主机使用
* <p>
* 因为客户端和服务端在同一 JVM可以共用同一个 Map<UUID, Data>
* <p>
* 特点不需要发网络包直接访问
*/
class LocalEternalPotatoManager implements IEternalPotatoManager {
private final Map<UUID, EternalPotatoImpl> LOCAL_DATA = new ConcurrentHashMap<>();
public IEternalPotato getOrCreate(UUID uuid) {
return LOCAL_DATA.computeIfAbsent(uuid, k -> {
EternalPotatoImpl impl = new EternalPotatoImpl();
impl.setItemUUID(uuid);
return impl;
});
}
@Override
public void remove(UUID uuid) {
LOCAL_DATA.remove(uuid);
}
public void clear() {
LOCAL_DATA.clear();
}
@Override
public CompoundTag saveAll() {
CompoundTag root = new CompoundTag();
LOCAL_DATA.forEach((uuid, impl) -> root.put(uuid.toString(), impl.serializeNBT()));
return root;
}
@Override
public void loadAll(CompoundTag tag) {
LOCAL_DATA.clear();
for (String key : tag.getAllKeys()) {
try {
UUID uuid = UUID.fromString(key);
EternalPotatoImpl impl = new EternalPotatoImpl();
impl.deserializeNBT(tag.getCompound(key));
impl.setItemUUID(uuid);
LOCAL_DATA.put(uuid, impl);
} catch (IllegalArgumentException e) {
SuperLeadRope.logger.error("Could not load UUID: {}", key, e);
}
}
}
}

View File

@ -0,0 +1,51 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.core.potato;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.saveddata.SavedData;
import org.jetbrains.annotations.NotNull;
public class PotatoSavedData extends SavedData {
public static final String DATA_NAME = "eternal_potato";
@Override
public @NotNull CompoundTag save(CompoundTag tag) {
// 把所有数据塞进 tag
tag.put(DATA_NAME, EternalPotatoFacade.getManager().saveAll());
return tag;
}
public static PotatoSavedData load(CompoundTag tag) {
IEternalPotatoManager manager = EternalPotatoFacade.getManager();
PotatoSavedData data = new PotatoSavedData();
if (tag.contains(DATA_NAME)) {
manager.loadAll(tag.getCompound(DATA_NAME));
}
return data;
}
// 工厂方法Forge 推荐写法
public static PotatoSavedData create(ServerLevel level) {
return level.getDataStorage().computeIfAbsent(
PotatoSavedData::load,
PotatoSavedData::new,
DATA_NAME
);
}
}

View File

@ -0,0 +1,89 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.core.potato;
import net.minecraft.nbt.CompoundTag;
import top.r3944realms.superleadrope.SuperLeadRope;
import top.r3944realms.superleadrope.content.capability.impi.EternalPotatoImpl;
import top.r3944realms.superleadrope.content.capability.inter.IEternalPotato;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* 专用服务器 + 远程客户端使用
* <p>
* 服务端维护权威数据变更时发包
* <p>
* 客户端本地缓存收到服务端同步包时更新
*/
class SyncedEternalPotatoManager implements IEternalPotatoManager {
private final Map<UUID, EternalPotatoImpl> GLOBAL_DATA = new ConcurrentHashMap<>();
private final boolean isServer;
public SyncedEternalPotatoManager(boolean isServer) {
this.isServer = isServer;
}
public IEternalPotato getOrCreate(UUID uuid) {
return GLOBAL_DATA.computeIfAbsent(uuid, k -> {
EternalPotatoImpl impl = new EternalPotatoImpl();
impl.setItemUUID(uuid);
return impl;
});
}
// 可选移除数据防止内存泄漏
public void remove(UUID uuid) {
GLOBAL_DATA.remove(uuid);
}
@Override
public void clear() {
GLOBAL_DATA.clear();
}
@Override
public CompoundTag saveAll() {
if (!isServer) {
return new CompoundTag(); // 客户端不存盘
}
CompoundTag root = new CompoundTag();
GLOBAL_DATA.forEach((uuid, impl) -> {
root.put(uuid.toString(), impl.serializeNBT());
});
return root;
}
@Override
public void loadAll(CompoundTag tag) {
GLOBAL_DATA.clear();
for (String key : tag.getAllKeys()) {
try {
UUID uuid = UUID.fromString(key);
EternalPotatoImpl impl = new EternalPotatoImpl();
impl.deserializeNBT(tag.getCompound(key));
impl.setItemUUID(uuid);
GLOBAL_DATA.put(uuid, impl);
} catch (IllegalArgumentException e) {
SuperLeadRope.logger.error("Could not load UUID: {}", key, e);
}
}
}
public boolean isServer() {
return isServer;
}
}

View File

@ -0,0 +1,127 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.core.punishment;
import net.minecraft.core.Holder;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.TextColor;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraftforge.server.ServerLifecycleHooks;
import top.r3944realms.superleadrope.content.SLPDamageTypes;
import top.r3944realms.superleadrope.content.capability.CapabilityHandler;
import top.r3944realms.superleadrope.datagen.data.SLPLangKeyValue;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class DailyPunishmentHandler {
private static long lastProcessedDay = -1;
private static final int COUNTDOWN_SECONDS = 10;
/**
* 玩家倒计时任务存储
*/
private static final Map<UUID, Integer> countdownMap = new ConcurrentHashMap<>();
public static void onServerTick() {
var server = ServerLifecycleHooks.getCurrentServer();
if (server == null) return;
for (ServerLevel level : server.getAllLevels()) {
long currentDay = level.getDayTime() / 24000L; // 24000 tick 为一天
if (currentDay != lastProcessedDay) {
lastProcessedDay = currentDay;
for (ServerPlayer player : level.getPlayers(p -> !(p.isCreative() || p.isSpectator()))) {
player.getInventory().items.stream()
.filter(stack -> !stack.isEmpty())
.forEach(stack -> stack.getCapability(CapabilityHandler.ETERNAL_POTATO_CAP).ifPresent(cap -> {
// 将上日未完成每日任务加入 pendingPunishments
int dailyObl = cap.getDailyObligations();
if (dailyObl > 0) {
cap.beginInit();
cap.setPendingPunishments(cap.getPendingPunishments() + dailyObl);
cap.setDailyObligations(0);
cap.endInit();
}
// 超过可宽恕次数开始倒计时惩罚
if (cap.getPendingPunishments() > cap.getGracePunishments()) {
UUID playerId = player.getUUID();
if (!countdownMap.containsKey(playerId)) {
countdownMap.put(playerId, COUNTDOWN_SECONDS * 20); // 10秒 = 200 tick
}
}
}));
}
}
// tick 更新倒计时
countdownMap.forEach((uuid, ticksLeft) -> {
ServerPlayer player = (ServerPlayer) level.getPlayerByUUID(uuid);
if (player == null) return;
player.getInventory().items.stream()
.filter(stack -> !stack.isEmpty())
.forEach(stack -> stack.getCapability(CapabilityHandler.ETERNAL_POTATO_CAP).ifPresent(cap -> {
// 如果 pending 已经低于宽恕值或者是非生存/冒险模式下的玩家中途取消倒计时
if (cap.getPendingPunishments() <= cap.getGracePunishments() || player.isCreative() || player.isSpectator()) {
countdownMap.remove(uuid);
return;
}
int newTicksLeft = ticksLeft - 1;
countdownMap.put(uuid, newTicksLeft);
int secondsLeft = (newTicksLeft + 19) / 20; // 转成秒
// 颜色渐变->->绿
int color;
if (secondsLeft > 6) color = 0xFF5555; //
else if (secondsLeft > 3) color = 0xFFFF55; //
else color = 0x55FF55; // 绿
player.displayClientMessage(
Component.translatable(SLPLangKeyValue.EP_OBLIGATION_COUNTDOWN.getKey(), secondsLeft)
.withStyle(style -> style.withColor(TextColor.fromRgb(color))),
true
);
if (newTicksLeft <= 0) {
// 执行惩罚
PunishmentDefinition punishment = cap.getPunishment();
if (punishment != null) {
punishment.execute(player,
new DamageSource(Holder.direct(SLPDamageTypes.ETERNAL_POTATO_NOT_COMPLETE), player));
}
// 扣除 2 pendingPunishments
cap.beginInit();
cap.setPendingPunishments(Math.max(0, cap.getPendingPunishments() - 2));
cap.endInit();
countdownMap.remove(uuid);
}
}));
});
}
}
}

View File

@ -0,0 +1,77 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.core.punishment;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
import top.r3944realms.superleadrope.core.register.ObligationCompletionRegistry;
import java.util.Map;
/**
* 判定单次任务是否完成
*/
public interface IObligationCompletion {
/**
* 判断某次操作是否算作完成义务
*
* @param player 执行玩家
* @param stack 操作的物品
* @return true = 完成一次义务
*/
boolean isCompleted(ServerPlayer player, ItemStack stack);
/**
* 当义务完成时执行比如减少计数提示
*
* @param player 执行玩家
* @param stack 操作的物品
*/
void onCompleted(ServerPlayer player, ItemStack stack);
/**
* 获取注册 ID
*/
default String getId() {
for (Map.Entry<String, IObligationCompletion> entry : ObligationCompletionRegistry.getAll().entrySet()) {
if (entry.getValue() == this) return entry.getKey();
}
return "none";
}
// --- 网络序列化 ---
default void toNetwork(FriendlyByteBuf buf) {
buf.writeUtf(this.getId());
}
static IObligationCompletion fromNetwork(FriendlyByteBuf buf) {
String id = buf.readUtf();
return ObligationCompletionRegistry.byId(id); // 如果没找到返回 NONE
}
/**
* 一个便捷的静态空实现默认永不完成
*/
IObligationCompletion NONE = new IObligationCompletion() {
@Override
public boolean isCompleted(ServerPlayer player, ItemStack stack) {
return false;
}
@Override
public void onCompleted(ServerPlayer player, ItemStack stack) {
// no-op
}
};
}

View File

@ -0,0 +1,82 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.core.punishment;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LightningBolt;
import org.jetbrains.annotations.Nullable;
/**
* 定义物品的惩罚方式
*
* @param strength 威力爆炸用 / 效果用
* @param affectOthers 是否影响其他实体
*/
public record PunishmentDefinition(PunishmentDefinition.Type type, float strength,
boolean affectOthers) {
public static final PunishmentDefinition DEFAULT = new PunishmentDefinition(Type.LIGHTNING, 0, false);
public enum Type {
LIGHTNING, // 雷劈
EXPLOSION, // 爆炸
EFFECT // 给予负面效果
}
/** 序列化到网络 */
public void toNetwork(FriendlyByteBuf buf) {
buf.writeEnum(this.type);
buf.writeFloat(this.strength);
buf.writeBoolean(this.affectOthers);
}
/** 从网络反序列化 */
public static PunishmentDefinition fromNetwork(FriendlyByteBuf buf) {
Type type = buf.readEnum(Type.class);
float strength = buf.readFloat();
boolean affectOthers = buf.readBoolean();
return new PunishmentDefinition(type, strength, affectOthers);
}
/**
* 执行惩罚
*/
public void execute(ServerPlayer target, DamageSource cause) {
execute(target, cause, null);
}
public void execute(ServerPlayer target, DamageSource cause,@Nullable Component actionMessage) {
ServerLevel level = (ServerLevel) target.level();
switch (type) {
case LIGHTNING -> {
LightningBolt bolt = new LightningBolt(EntityType.LIGHTNING_BOLT, level);
bolt.setPos(target.getX(), target.getY(), target.getZ());
bolt.setVisualOnly(true);
if(actionMessage != null) target.displayClientMessage(actionMessage, true);
level.addFreshEntity(bolt);
target.hurt(cause, Float.MAX_VALUE);
}
case EXPLOSION -> {
}
case EFFECT -> {
}
}
}
}

View File

@ -0,0 +1,58 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.core.register;
import top.r3944realms.superleadrope.core.punishment.IObligationCompletion;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* IObligationCompletion 注册与反序列化管理器
*/
public class ObligationCompletionRegistry {
/** ID -> IObligationCompletion 实例 */
private static final Map<String, IObligationCompletion> REGISTRY = new HashMap<>();
/**
* 注册一个 IObligationCompletion 实例
* @param id 唯一 ID
* @param completion 实例
*/
public static void register(String id, IObligationCompletion completion) {
if (id == null || id.isEmpty()) throw new IllegalArgumentException("ID cannot be null or empty");
if (completion == null) throw new IllegalArgumentException("Completion cannot be null");
REGISTRY.put(id, completion);
}
/**
* 根据 ID 获取 IObligationCompletion 实例
* @param id ID
* @return 实例如果未注册则返回 NONE
*/
public static IObligationCompletion byId(String id) {
return REGISTRY.getOrDefault(id, IObligationCompletion.NONE);
}
/**
* 获取只读注册表用于调试或枚举
*/
public static Map<String, IObligationCompletion> getAll() {
return Collections.unmodifiableMap(REGISTRY);
}
}

View File

@ -22,7 +22,6 @@ import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.registries.RegistryObject;
import top.r3944realms.superleadrope.SuperLeadRope;
import top.r3944realms.superleadrope.content.entity.SuperLeashEntity;
import top.r3944realms.superleadrope.content.entity.SuperLeashKnotEntity;
public class SLPEntityTypes {
@ -37,18 +36,6 @@ public class SLPEntityTypes {
.updateInterval(Integer.MAX_VALUE)
.build("super_lead_knot")
);
public static RegistryObject<EntityType<SuperLeashEntity>> SUPER_LEASH = ENTITY_TYPES.register(
"super_leash",
() -> EntityType.Builder.<SuperLeashEntity>of(SuperLeashEntity::new, MobCategory.MISC)
.sized(0.01f, 0.01f)
.noSummon()
.noSave()
.clientTrackingRange(0)
.updateInterval(1)
.fireImmune()
.canSpawnFarFromPlayer()
.build("super_leash")
);
public static String getEntityNameKey(String entityName) {
return "entity." + SuperLeadRope.MOD_ID + "." + entityName;
}

View File

@ -21,6 +21,7 @@ import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.registries.RegistryObject;
import top.r3944realms.superleadrope.SuperLeadRope;
import top.r3944realms.superleadrope.content.item.EternalPotatoItem;
import top.r3944realms.superleadrope.content.item.SuperLeadRopeItem;
public class SLPItems {
@ -29,6 +30,13 @@ public class SLPItems {
"super_lead_rope",
() -> new SuperLeadRopeItem(new Item.Properties())
);
public static final RegistryObject<Item> ETERNAL_POTATO =
ITEMS.register("eternal_potato",
() -> new EternalPotatoItem(
new Item.Properties()
.stacksTo(1) // 只能有一颗
.fireResistant() // 防火
));
public static void register(IEventBus bus) {
ITEMS.register(bus);
}

View File

@ -0,0 +1,47 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.core.util;
import java.util.Objects;
public record ImmutablePair<F, S>(F first, S second) {
public static <F, S> ImmutablePair<F, S> of(F first, S second) {
return new ImmutablePair<>(first, second);
}
/**
* 重写equals方法
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ImmutablePair<?, ?> immutablePair = (ImmutablePair<?, ?>) o;
if (!Objects.equals(first, immutablePair.first)) return false;
return Objects.equals(second, immutablePair.second);
}
/**
* 重写toString方法便于调试
*/
@Override
public String toString() {
return "Pair{" + first + ", " + second + "}";
}
}

View File

@ -0,0 +1,35 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.core.util;
public enum PotatoMode {
/**
* 单人 or 局域网主机
*/
INTEGRATED,
/**
* 专用服务器
*/
DEDICATED,
/**
* 远程连接的客户端
*/
REMOTE_CLIENT;
public boolean isSynced() {
// Synced 模式DEDICATED 服务端 + REMOTE_CLIENT 客户端
return this == DEDICATED || this == REMOTE_CLIENT;
}
}

View File

@ -0,0 +1,41 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.core.util;
import net.minecraft.client.Minecraft;
import net.minecraft.server.MinecraftServer;
import net.minecraftforge.fml.loading.FMLEnvironment;
import net.minecraftforge.server.ServerLifecycleHooks;
public class PotatoModeHelper {
public static PotatoMode getCurrentMode() {
if (FMLEnvironment.dist.isClient()) {
Minecraft mc = Minecraft.getInstance();
if (mc.hasSingleplayerServer()) {
return PotatoMode.INTEGRATED;
}
return PotatoMode.REMOTE_CLIENT;
} else {
MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
if (server != null && server.isDedicatedServer()) {
return PotatoMode.DEDICATED;
}
// 如果不是专用服那就是集成服服务端部分
return PotatoMode.INTEGRATED;
}
}
}

View File

@ -25,16 +25,15 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import top.r3944realms.superleadrope.SuperLeadRope;
import top.r3944realms.superleadrope.datagen.provider.*;
import top.r3944realms.superleadrope.utils.lang.LanguageEnum;
import top.r3944realms.superleadrope.util.lang.LanguageEnum;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
@Mod.EventBusSubscriber(modid = SuperLeadRope.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD)
public class SLPDataGenEvent {
static Logger logger = LoggerFactory.getLogger(SLPDataGenEvent.class);
@SubscribeEvent
public static void gatherData(GatherDataEvent event) throws IOException {
public static void gatherData(GatherDataEvent event) {
logger.info("GatherDataEvent thread: {}", Thread.currentThread().getName());
CompletableFuture<HolderLookup.Provider> lookupProvider = event.getLookupProvider();
LanguageGenerator(event, LanguageEnum.English);

View File

@ -18,11 +18,12 @@ package top.r3944realms.superleadrope.datagen.data;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.block.Block;
import org.jetbrains.annotations.NotNull;
import top.r3944realms.superleadrope.content.item.EternalPotatoItem;
import top.r3944realms.superleadrope.core.register.SLPEntityTypes;
import top.r3944realms.superleadrope.core.register.SLPItems;
import top.r3944realms.superleadrope.core.register.SLPSoundEvents;
import top.r3944realms.superleadrope.utils.lang.LanguageEnum;
import top.r3944realms.superleadrope.utils.lang.ModPartEnum;
import top.r3944realms.superleadrope.util.lang.LanguageEnum;
import top.r3944realms.superleadrope.util.lang.ModPartEnum;
import javax.annotation.Nullable;
import java.util.function.Supplier;
@ -34,6 +35,138 @@ public enum SLPLangKeyValue {
"Super Lead Rope", "超级拴绳", "超級拴繩","神駒羈縻索"
),
ITEM_ETERNAL_POTATO(
SLPItems.ETERNAL_POTATO, ModPartEnum.ITEM,
"Eternal Potato", "永恒土豆", "永恆馬鈴薯", "不滅薯", true
),
EP_TOOLTIP_TITLE(EternalPotatoItem.getDescKey("title"), ModPartEnum.DESCRIPTION,
"§6Mythical Item §7- §6Eternal Potato",
"§6神话物品 §7- §6永恒土豆",
"§6神話物品 §7- §6永恒土豆",
"§6永恒土豆 §7- §6传奇之物"
),
EP_DESC_TOOLTIP(EternalPotatoItem.getDescKey("desc"), ModPartEnum.DESCRIPTION,
"§7Symbol of server-wide contract, cannot be discarded",
"§7象征全服契约不可丢弃",
"§7象徵全服契約不可丟棄",
"§7象征全服契约绝不可弃"
),
EP_BIND_OWNER(EternalPotatoItem.getDescKey("bind_owner"), ModPartEnum.DESCRIPTION,
"§bBound Owner: §f%s",
"§b绑定主人: §f%s",
"§b綁定主人: §f%s",
"§b绑定主人: §f%s"
),
EP_UNBOUND(EternalPotatoItem.getDescKey("unbound"), ModPartEnum.DESCRIPTION,
"§cUnbound",
"§c未绑定主人",
"§c未綁定主人",
"§c尚未绑定主人"
),
EP_OBLIGATION_TOOLTIP(EternalPotatoItem.getDescKey("obligation"), ModPartEnum.DESCRIPTION,
"§7Daily obligations remaining: §a%d §c(+%d§c overdue)",
"§7今日剩余义务: §a%d §c(+%d §c逾期未完成)",
"§7今日剩餘義務: §a%d §c(+%d §c逾期未完成)",
"§7今日责务尚余: §a%d §c(+%d §c逾期未尽)"
),
EP_PUNISH_TOOLTIP(EternalPotatoItem.getDescKey("punish"), ModPartEnum.DESCRIPTION,
"§cOverdue punishments: §4%d §7(will be applied), grace exceeded: §4%d",
"§c逾期未完成责务: §4%d §7(将会受罚),超出宽限数: §4%d",
"§c逾期未完成责務: §4%d §7(將會受罰),超出寬限數: §4%d",
"§c逾期责务尚未完成: §4%d §7(將受懲罰),超出寬限數: §4%d"
),
EP_OBLIGATION_INFO(EternalPotatoItem.getMsgKey("obligation_info"), ModPartEnum.MESSAGE,
"§e[Eternal Potato] §fThis is the server-wide shared person, remaining obligations today: §a%d§f.",
"§e[永恒土豆] §f这是全服共有之人今日义务剩余§a%d§f次。",
"§e[永恒土豆] §f這是全服共有之人今日義務剩餘§a%d§f次。",
"§e[永恒土豆] §f此为全服共享之人今日责务尚余§a%d§f次。"
),
EP_POTATO_HEAL(EternalPotatoItem.getMsgKey("potato_heal"), ModPartEnum.MESSAGE,
"§aThe power of the Eternal Potato comforts you, it won't disappear.",
"§a永恒土豆的力量抚慰了你但它不会消失。",
"§a永恆土豆的力量撫慰了你但它不會消失。",
"§a永恒土豆之力慰心永不消逝。"
),
EP_CANNOT_DROP(EternalPotatoItem.getMsgKey("cannot_drop"), ModPartEnum.MESSAGE,
"§cThe Eternal Potato cannot be dropped! +%d punishments.",
"§c永恒土豆是不可丢弃的惩罚数加%d",
"§c永恆土豆不可丟棄懲罰數加%d",
"§c永恒土豆不可丟棄懲罰數增加%d"
),
EP_BIND_MSG(EternalPotatoItem.getMsgKey("bind_msg"), ModPartEnum.MESSAGE,
"§6Bound to you as the server-wide shared person.",
"§6已与你绑定成为全服共有之人。",
"§6已與你綁定成為全服共有之人。",
"§6已与汝绑定为全服共享之人。"
),
EP_OBLIGATION_DONE(EternalPotatoItem.getMsgKey("obligation_done"), ModPartEnum.MESSAGE,
"§eObligation completed, remaining: §a%d§e",
"§e义务完成一次剩余 §a%d §e次。",
"§e義務完成一次剩餘 §a%d §e次。",
"§e责务完成尚余 §a%d §e次。"
),
EP_OBLIGATION_FULL(EternalPotatoItem.getMsgKey("obligation_full"), ModPartEnum.MESSAGE,
"§aAll obligations completed today!",
"§a今日义务已全部完成",
"§a今日義務已全部完成",
"§a今日责务尽矣"
),
EP_PUNISH_MSG(EternalPotatoItem.getMsgKey("punish_msg"), ModPartEnum.MESSAGE,
"§cYesterday obligations incomplete, punished!",
"§c未完成昨日义务受到惩罚",
"§c未完成昨日義務受到懲罰",
"§c昨日之责未尽受罚矣"
),
EP_OBLIGATION_COUNTDOWN(EternalPotatoItem.getMsgKey("obligation_countdown"), ModPartEnum.MESSAGE,
"Punish Countdown: §a%d §fseconds remaining",
"惩罚倒计时: §a%d §f秒",
"懲罰倒計時: §a%d §f秒",
"受罚倒数§a%d §f瞬"
),
EP_PICKUP_NOT_OWNER(EternalPotatoItem.getMsgKey("pickup_not_owner"), ModPartEnum.MESSAGE,
"§cYou are not the rightful owner and cannot pick this up!",
"§c非绑定主人无法拾取此物品",
"§c非綁定主人無法拾取此物品",
"§c非汝所主勿取"
),
EP_PUNISH_NOT_OWNER(EternalPotatoItem.getMsgKey("punish_not_owner"), ModPartEnum.MESSAGE,
"§cYou are not the rightful owner, punished by lightning!",
"§c非绑定主人使用受到闪电惩罚",
"§c非綁定主人使用受到閃電懲罰",
"§c非汝所主雷霆降身"
),
EP_PUNISH_NOT_OWNER_DEATH_MSG(
"death.attack.eternal_potato_not_owner", ModPartEnum.MESSAGE,
"§c%1$s was not the rightful owner, struck by lightning!",
"§c%1$s 因使用非自己绑定物品,受到闪电惩罚!",
"§c%1$s 因使用非自己綁定物品,受到閃電懲罰!",
"§c%1$s 非汝所主,雷霆降身!"
),
EP_PUNISH_NOT_COMPETE_DEATH_MSG(
"death.attack.eternal_potato_not_complete", ModPartEnum.MESSAGE,
"§c%1$s was not the rightful owner, struck by lightning!",
"§c%1$s 因使用非自己绑定物品,受到闪电惩罚!",
"§c%1$s 因使用非自己綁定物品,受到閃電懲罰!",
"§c%1$s 非汝所主,雷霆降身!"
),
SOUND_SUBTITLE_SUPER_LEAD_BREAK(
SLPSoundEvents.getSubTitleTranslateKey("lead_break"), ModPartEnum.SOUND,
"Lead Break", "拴绳断裂", "拴繩斷裂", "索絕"
@ -54,13 +187,6 @@ public enum SLPLangKeyValue {
"Super Lead Knot", "超级拴绳结", "超級拴繩結", "神駒羈縻索結"
),
ENTITY_SUPER_LEASH(
SLPEntityTypes.getEntityNameKey("super_leash"), ModPartEnum.ENTITY,
"Super Leash", "超级拴绳", "超級拴繩","神駒羈縻索"
),
;
private final Supplier<?> supplier;
private String key;

View File

@ -17,8 +17,12 @@ package top.r3944realms.superleadrope.datagen.provider;
import net.minecraft.data.PackOutput;
import net.minecraft.data.recipes.FinishedRecipe;
import net.minecraft.data.recipes.RecipeCategory;
import net.minecraft.data.recipes.RecipeProvider;
import net.minecraft.data.recipes.ShapedRecipeBuilder;
import net.minecraft.world.item.Items;
import org.jetbrains.annotations.NotNull;
import top.r3944realms.superleadrope.core.register.SLPItems;
import java.util.function.Consumer;
@ -29,6 +33,15 @@ public class SLPItemRecipeProvider extends RecipeProvider {
@Override
protected void buildRecipes(@NotNull Consumer<FinishedRecipe> consumer) {
ShapedRecipeBuilder.shaped(RecipeCategory.TOOLS, SLPItems.SUPER_LEAD_ROPE.get())
.pattern("SL ")
.pattern("LE ")
.pattern(" I")
.define('S', Items.SLIME_BALL)
.define('L', Items.LEAD)
.define('E', Items.EXPERIENCE_BOTTLE)
.define('I', Items.STRING)
.unlockedBy("has_lead", has(Items.LEAD))
.save(consumer);
}
}

View File

@ -18,10 +18,12 @@ package top.r3944realms.superleadrope.datagen.provider;
import net.minecraft.core.HolderLookup;
import net.minecraft.data.PackOutput;
import net.minecraft.data.tags.ItemTagsProvider;
import net.minecraft.world.item.Items;
import net.minecraftforge.common.data.ExistingFileHelper;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import top.r3944realms.superleadrope.SuperLeadRope;
import top.r3944realms.superleadrope.content.SLPTags;
import java.util.concurrent.CompletableFuture;
@ -35,7 +37,9 @@ public class SLPItemTagProvider extends ItemTagsProvider {
@Override
protected void addTags(HolderLookup.@NotNull Provider provider) {
tag(SLPTags.Items.LEAD)
.add(Items.LEAD)
.add(Items.SLIME_BALL)
.add(Items.STRING);
}
}

View File

@ -19,7 +19,7 @@ import net.minecraft.data.PackOutput;
import net.minecraftforge.common.data.LanguageProvider;
import top.r3944realms.superleadrope.SuperLeadRope;
import top.r3944realms.superleadrope.datagen.data.SLPLangKeyValue;
import top.r3944realms.superleadrope.utils.lang.LanguageEnum;
import top.r3944realms.superleadrope.util.lang.LanguageEnum;
import java.util.ArrayList;
import java.util.HashMap;

View File

@ -22,9 +22,12 @@ import net.minecraftforge.network.NetworkRegistry;
import net.minecraftforge.network.PacketDistributor;
import net.minecraftforge.network.simple.SimpleChannel;
import top.r3944realms.superleadrope.SuperLeadRope;
import top.r3944realms.superleadrope.network.toClient.EternalPotatoSyncCapPacket;
import top.r3944realms.superleadrope.network.toClient.LeashDataSyncPacket;
import top.r3944realms.superleadrope.network.toClient.PacketEternalPotatoRemovePacket;
import top.r3944realms.superleadrope.network.toClient.UpdatePlayerMovementPacket;
public class NetworkHandler {
private static final String PROTOCOL_VERSION = "1";
private static int cid = 0;
@ -45,12 +48,21 @@ public class NetworkHandler {
.encoder(UpdatePlayerMovementPacket::encode)
.consumerNetworkThread(UpdatePlayerMovementPacket::handle)
.add();
INSTANCE.messageBuilder(EternalPotatoSyncCapPacket.class, cid++, NetworkDirection.PLAY_TO_CLIENT)
.decoder(EternalPotatoSyncCapPacket::decode)
.encoder(EternalPotatoSyncCapPacket::encode)
.consumerNetworkThread(EternalPotatoSyncCapPacket::handle)
.add();
INSTANCE.messageBuilder(PacketEternalPotatoRemovePacket.class, cid++, NetworkDirection.PLAY_TO_CLIENT)
.decoder(PacketEternalPotatoRemovePacket::decode)
.encoder(PacketEternalPotatoRemovePacket::encode)
.consumerNetworkThread(PacketEternalPotatoRemovePacket::handle)
.add();
}
public static <MSG> void sendAllPlayer(MSG message){
INSTANCE.send(PacketDistributor.ALL.noArg(), message);
}
public static <MSG> void sendToPlayer(MSG message, ServerPlayer player){
INSTANCE.send(PacketDistributor.PLAYER.with(() -> player), message);
}
public static <MSG, T> void sendToPlayer(MSG message, T entity, PacketDistributor<T> packetDistributor){
INSTANCE.send(packetDistributor.with(() -> entity), message);
}
}

View File

@ -0,0 +1,95 @@
package top.r3944realms.superleadrope.network.toClient;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.network.NetworkEvent;
import top.r3944realms.superleadrope.content.capability.inter.IEternalPotato;
import top.r3944realms.superleadrope.core.potato.EternalPotatoFacade;
import top.r3944realms.superleadrope.core.punishment.IObligationCompletion;
import top.r3944realms.superleadrope.core.punishment.PunishmentDefinition;
import java.util.UUID;
import java.util.function.Supplier;
public record EternalPotatoSyncCapPacket(
UUID itemUUID,
UUID ownerUUID,
String ownerName,
int dailyObligations,
int pendingPunishments,
int gracePunishments,
String lastReset,
String lastPunishDate,
PunishmentDefinition punishment,
IObligationCompletion completionRule
) {
// 编码
public static void encode(EternalPotatoSyncCapPacket msg, FriendlyByteBuf buf) {
buf.writeUUID(msg.itemUUID);
buf.writeBoolean(msg.ownerUUID != null);
if (msg.ownerUUID != null) buf.writeUUID(msg.ownerUUID);
buf.writeUtf(msg.ownerName != null ? msg.ownerName : "");
buf.writeInt(msg.dailyObligations);
buf.writeInt(msg.pendingPunishments);
buf.writeInt(msg.gracePunishments);
buf.writeUtf(msg.lastReset != null ? msg.lastReset : "");
buf.writeUtf(msg.lastPunishDate != null ? msg.lastPunishDate : "");
buf.writeBoolean(msg.punishment != null);
if (msg.punishment != null) {
msg.punishment.toNetwork(buf);
}
buf.writeBoolean(msg.completionRule != null);
if (msg.completionRule != null) {
msg.completionRule.toNetwork(buf);
}
}
// 解码
public static EternalPotatoSyncCapPacket decode(FriendlyByteBuf buf) {
UUID itemUUID = buf.readUUID();
UUID ownerUUID = buf.readBoolean() ? buf.readUUID() : null;
String name = buf.readUtf();
int daily = buf.readInt();
int pending = buf.readInt();
int grace = buf.readInt();
String lastReset = buf.readUtf();
String lastPunishDate = buf.readUtf();
PunishmentDefinition punishment = null;
if (buf.readBoolean()) {
punishment = PunishmentDefinition.fromNetwork(buf);
}
IObligationCompletion completionRule = null;
if (buf.readBoolean()) {
completionRule = IObligationCompletion.fromNetwork(buf);
}
return new EternalPotatoSyncCapPacket(itemUUID, ownerUUID, name, daily, pending, grace,
lastReset, lastPunishDate, punishment, completionRule);
}
// 处理
public static void handle(EternalPotatoSyncCapPacket msg, Supplier<NetworkEvent.Context> ctx) {
ctx.get().enqueueWork(() -> {
// 获取全局能力实例
IEternalPotato cap = EternalPotatoFacade.getOrCreate(msg.itemUUID);
// 更新数据
cap.beginInit();
cap.setOwner(msg.ownerUUID, msg.ownerName);
cap.setDailyObligations(msg.dailyObligations);
cap.setPendingPunishments(msg.pendingPunishments);
cap.setGracePunishments(msg.gracePunishments);
cap.setLastReset(msg.lastReset);
cap.setLastPunishDate(msg.lastPunishDate);
if (msg.punishment != null) cap.setPunishment(msg.punishment);
if (msg.completionRule != null) cap.setCompletionRule(msg.completionRule);
cap.endInit();
});
ctx.get().setPacketHandled(true);
}
}

View File

@ -19,9 +19,7 @@ import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.protocol.Packet;
import net.minecraft.world.entity.Entity;
import net.minecraftforge.network.ICustomPacket;
import net.minecraftforge.network.NetworkEvent;
import top.r3944realms.superleadrope.content.capability.CapabilityHandler;

View File

@ -0,0 +1,41 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.network.toClient;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.network.NetworkEvent;
import top.r3944realms.superleadrope.core.potato.EternalPotatoFacade;
import java.util.UUID;
import java.util.function.Supplier;
public record PacketEternalPotatoRemovePacket(UUID itemUUID) {
public static void encode(PacketEternalPotatoRemovePacket msg, FriendlyByteBuf buf) {
buf.writeUUID(msg.itemUUID());
}
public static PacketEternalPotatoRemovePacket decode(FriendlyByteBuf buf) {
return new PacketEternalPotatoRemovePacket(buf.readUUID());
}
public static void handle(PacketEternalPotatoRemovePacket msg, Supplier<NetworkEvent.Context> ctx) {
ctx.get().enqueueWork(() -> {
// 客户端收到移除请求
EternalPotatoFacade.remove(msg.itemUUID());
});
ctx.get().setPacketHandled(true);
}
}

View File

@ -13,7 +13,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.utils.coremods;
package top.r3944realms.superleadrope.util.coremods;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;

View File

@ -0,0 +1,63 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.util.file;
import net.minecraftforge.common.ForgeConfigSpec;
import net.minecraftforge.fml.ModLoadingContext;
import net.minecraftforge.fml.config.ModConfig;
import net.minecraftforge.fml.loading.FMLPaths;
import top.r3944realms.superleadrope.SuperLeadRope;
import java.io.File;
import java.util.Optional;
public class ConfigUtil {
public static void createFile(String[] children) {//初始化配置文件目录
File configFile = new File(FMLPaths.CONFIGDIR.get().toFile(), SuperLeadRope.MOD_ID);
if (!configFile.exists()) {
boolean mkdirSuccess = configFile.mkdirs();
if (!mkdirSuccess) {
SuperLeadRope.logger.error("failed to create config directory for whimsicality");
throw new RuntimeException("failed to create config directory for " + SuperLeadRope.MOD_ID);
} else {
for (String child : children) {
File file = new File(configFile, child);
if (!file.exists()) {
boolean mkdirChildrenSuccess = file.mkdirs();
if (!mkdirChildrenSuccess) {
SuperLeadRope.logger.error("failed to create {} directory for +" + SuperLeadRope.MOD_ID, child);
throw new RuntimeException("failed to create " + child + " directory for" +SuperLeadRope.MOD_ID);
}
}
}
}
}
}
public static void registerConfig (
ModLoadingContext context,
ModConfig.Type type,
ForgeConfigSpec configSpec,
String folderName,
String fileName
) {
context.registerConfig(
type,
configSpec,
SuperLeadRope.MOD_ID + "/" + Optional.ofNullable(folderName).map(i-> i + "/").orElse("") + fileName + ".toml"
);
}
}

View File

@ -13,7 +13,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.utils.lang;
package top.r3944realms.superleadrope.util.lang;
public enum LanguageEnum {
English("en_us"),

View File

@ -13,7 +13,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.utils.lang;
package top.r3944realms.superleadrope.util.lang;
public enum ModPartEnum {
DEFAULT,

View File

@ -0,0 +1,100 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.util.model;
import java.util.*;
/**
* 骑乘关系数据结构
*/
public class RidingRelationship {
private UUID entityId;
private UUID vehicleId;
private List<RidingRelationship> passengers;
public RidingRelationship() {
this.passengers = new ArrayList<>();
}
public RidingRelationship(List<RidingRelationship> passengers, UUID vehicleId, UUID entityId) {
this.passengers = passengers != null ? passengers : new ArrayList<>();
this.vehicleId = vehicleId;
this.entityId = entityId;
}
public UUID getEntityId() {
return entityId;
}
public void setEntityId(UUID entityId) {
this.entityId = entityId;
}
public List<RidingRelationship> getPassengers() {
return Collections.unmodifiableList(passengers);
}
public void setPassengers(List<RidingRelationship> passengers) {
this.passengers = passengers != null ? passengers : new ArrayList<>();
}
public void addPassenger(RidingRelationship passenger) {
this.passengers.add(passenger);
}
public UUID getVehicleId() {
return vehicleId;
}
public void setVehicleId(UUID vehicleId) {
this.vehicleId = vehicleId;
}
/**
* 获取所有嵌套乘客的数量
*/
public int getTotalPassengerCount() {
int count = passengers.size();
for (RidingRelationship passenger : passengers) {
count += passenger.getTotalPassengerCount();
}
return count;
}
/**
* 检查是否包含特定实体
*/
public boolean containsEntity(UUID entityId) {
if (Objects.equals(this.entityId, entityId)) {
return true;
}
for (RidingRelationship passenger : passengers) {
if (passenger.containsEntity(entityId)) {
return true;
}
}
return false;
}
@Override
public String toString() {
return "RidingRelationship{" +
"entityId=" + entityId +
", vehicleId=" + vehicleId +
", passengers=" + passengers.size() +
'}';
}
}

View File

@ -0,0 +1,122 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.util.riding;
import net.minecraft.world.entity.Entity;
import top.r3944realms.superleadrope.SuperLeadRope;
import top.r3944realms.superleadrope.core.exception.RidingCycleException;
import top.r3944realms.superleadrope.util.model.RidingRelationship;
import java.util.Collection;
import java.util.LinkedList;
import java.util.Queue;
import java.util.UUID;
import java.util.function.Function;
public class RidingApplier {
/**
* 应用骑乘关系在服务器端调用
* @param relationship 骑乘关系
* @param entityProvider 实体提供器根据UUID获取实体
* @return 应用成功的实体数量
*/
public static int applyRidingRelationship(RidingRelationship relationship,
Function<UUID, Entity> entityProvider) {
if (relationship == null || entityProvider == null) {
return 0;
}
int appliedCount = 0;
Queue<RidingRelationship> queue = new LinkedList<>();
queue.offer(relationship);
while (!queue.isEmpty()) {
RidingRelationship current = queue.poll();
UUID entityId = current.getEntityId();
UUID vehicleId = current.getVehicleId();
// 获取实体和载具
Entity entity = entityProvider.apply(entityId);
Entity vehicle = vehicleId != null ? entityProvider.apply(vehicleId) : null;
if (entity == null) continue;
// ---------- 白名单保护 ----------
if (!RidingValidator.isInWhitelist(entity.getType())) {
// 不在白名单跳过本节点但保留其乘客挂回上层
if (vehicle != null) {
// 将当前节点的乘客挂回上层载具
for (RidingRelationship child : current.getPassengers()) {
child.setVehicleId(vehicle.getUUID());
queue.offer(child);
}
}
continue; // 跳过本实体的骑乘操作
}
appliedCount++;
// 如果实体已经有载具先下车
if (entity.getVehicle() != null) {
entity.stopRiding();
}
// 如果有指定的载具尝试上车
if (vehicle != null && RidingValidator.isInWhitelist(vehicle.getType())) {
if (RidingValidator.wouldCreateCycle(entity, vehicle)) {
throw new RidingCycleException(entityId, vehicleId);
}
boolean success = entity.startRiding(vehicle, true);
if (!success) {
SuperLeadRope.logger.error("Failed to mount entity {} to vehicle {}", entityId, vehicleId);
}
}
// 处理子乘客
queue.addAll(current.getPassengers());
}
return appliedCount;
}
/**
* 批量应用骑乘关系适用于世界加载时
*/
public static void applyRidingRelationships(Collection<RidingRelationship> relationships,
Function<UUID, Entity> entityProvider) {
if (relationships == null || relationships.isEmpty()) {
return;
}
for (RidingRelationship relationship : relationships) {
try {
applyRidingRelationship(relationship, entityProvider);
} catch (RidingCycleException e) {
// 记录循环引用错误但继续处理其他关系
SuperLeadRope.logger.warn("Cyclic riding reference detected and skipped: {}", e.getMessage());
}
}
}
/**
* 从JSON字符串应用骑乘关系
*/
public static int applyRidingRelationshipFromJson(String json,
Function<UUID, Entity> entityProvider) {
RidingRelationship relationship = RidingSerializer.deserialize(json);
return applyRidingRelationship(relationship, entityProvider);
}
}

View File

@ -0,0 +1,208 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.util.riding;
import net.minecraft.world.entity.Entity;
import top.r3944realms.superleadrope.util.model.RidingRelationship;
import java.util.*;
import java.util.function.Function;
public class RidingDismounts {
/**
* 解除单个实体的骑乘关系
*/
public static void dismountEntity(Entity entity) {
if (entity == null) {
return;
}
// 如果实体正在骑乘先下车
if (entity.isPassenger()) {
entity.stopRiding();
}
// 让所有乘客下车
dismountAllPassengers(entity);
}
/**
* 解除实体及其所有乘客的骑乘关系非递归
*/
public static void dismountAllPassengers(Entity entity) {
if (entity == null) {
return;
}
// 使用队列进行广度优先遍历
Queue<Entity> queue = new LinkedList<>();
queue.offer(entity);
while (!queue.isEmpty()) {
Entity current = queue.poll();
// 让当前实体的所有乘客下车
List<Entity> passengers = new ArrayList<>(current.getPassengers());
for (Entity passenger : passengers) {
passenger.stopRiding();
queue.offer(passenger);
}
}
}
/**
* 解除根实体的骑乘关系包括从载具下车
*/
public static void dismountRootEntity(Entity entity) {
if (entity == null) {
return;
}
// 找到根载具
Entity rootVehicle = RidingFinder.findRootVehicle(entity);
if (rootVehicle != null) {
// 让根载具的所有乘客下车
dismountAllPassengers(rootVehicle);
// 根载具本身也下车如果有载具的话
if (rootVehicle.isPassenger()) {
rootVehicle.stopRiding();
}
}
}
/**
* 安全解除骑乘关系带超时保护
*/
public static boolean safeDismountAll(Entity entity, int maxIterations) {
if (entity == null) {
return true;
}
int iteration = 0;
Queue<Entity> queue = new LinkedList<>();
queue.offer(entity);
while (!queue.isEmpty() && iteration < maxIterations) {
Entity current = queue.poll();
iteration++;
// 让当前实体下车如果是乘客
if (current.isPassenger()) {
current.stopRiding();
}
// 处理当前实体的乘客
List<Entity> passengers = new ArrayList<>(current.getPassengers());
for (Entity passenger : passengers) {
passenger.stopRiding();
queue.offer(passenger);
}
}
return queue.isEmpty(); // 如果队列为空表示全部解除成功
}
/**
* 批量解除多个实体的骑乘关系
*/
public static void dismountEntities(Collection<Entity> entities) {
if (entities == null || entities.isEmpty()) {
return;
}
Set<Entity> processed = new HashSet<>();
Queue<Entity> queue = new LinkedList<>(entities);
while (!queue.isEmpty()) {
Entity current = queue.poll();
if (current != null && !processed.contains(current)) {
processed.add(current);
// 让当前实体下车
if (current.isPassenger()) {
current.stopRiding();
}
// 处理乘客
List<Entity> passengers = new ArrayList<>(current.getPassengers());
for (Entity passenger : passengers) {
if (!processed.contains(passenger)) {
queue.offer(passenger);
}
}
}
}
}
/**
* 根据骑乘关系数据结构解除骑乘
*/
public static void dismountByRelationship(RidingRelationship relationship,
Function<UUID, Entity> entityProvider) {
if (relationship == null || entityProvider == null) {
return;
}
// 使用栈进行深度优先遍历解除
Deque<RidingRelationship> stack = new ArrayDeque<>();
stack.push(relationship);
while (!stack.isEmpty()) {
RidingRelationship current = stack.pop();
// 解除当前实体的骑乘
Entity entity = entityProvider.apply(current.getEntityId());
if (entity != null && entity.isPassenger()) {
entity.stopRiding();
}
// 将子乘客加入栈中后进先出深度优先
List<RidingRelationship> passengers = current.getPassengers();
for (int i = passengers.size() - 1; i >= 0; i--) {
stack.push(passengers.get(i));
}
}
}
/**
* 立即解除所有骑乘关系强制方式
*/
public static void forceDismountAll(Entity entity) {
if (entity == null) {
return;
}
// 先让自己下车
if (entity.isPassenger()) {
entity.stopRiding();
}
// 使用广度优先让所有乘客下车
List<Entity> allPassengers = RidingFinder.getAllPassengers(entity, false);
for (Entity passenger : allPassengers) {
if (passenger.isPassenger()) {
passenger.stopRiding();
}
}
// 再次检查并清理确保完全解除
if (!entity.getPassengers().isEmpty()) {
entity.ejectPassengers();
}
}
}

View File

@ -0,0 +1,101 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.util.riding;
import net.minecraft.world.entity.Entity;
import top.r3944realms.superleadrope.util.model.RidingRelationship;
import javax.annotation.Nullable;
import java.util.*;
import java.util.function.Function;
public class RidingFinder {
/**
* 从JSON字符串应用骑乘关系
*/
public static List<Entity> getEntityFromRidingShip(RidingRelationship ship,
Function<UUID, Entity> entityProvider) {
List<Entity> ret = new ArrayList<>();
Queue<RidingRelationship> queue = new LinkedList<>();
queue.offer(ship);
while (!queue.isEmpty()) {
RidingRelationship poll = queue.poll();
ret.add(entityProvider.apply(ship.getEntityId()));
List<RidingRelationship> passengers = poll.getPassengers();
if (!passengers.isEmpty()) {
queue.addAll(passengers);
}
}
return ret;
}
/**
* 查找根载具
*/
@Nullable
public static Entity findRootVehicle(@Nullable Entity entity) {
if (entity == null) {
return null;
}
Entity current = entity;
while (current.getVehicle() != null) {
current = current.getVehicle();
// 安全保护防止意外循环
if (current == entity) {
break;
}
}
return current;
}
/**
* 获取所有乘客包括嵌套乘客
*/
public static List<Entity> getAllPassengers(@Nullable Entity entity) {
return getAllPassengers(entity, true);
}
/**
* 获取所有乘客包括嵌套乘客
*/
public static List<Entity> getAllPassengers(@Nullable Entity entity, boolean findRoot) {
if (entity == null) {
return Collections.emptyList();
}
Entity rootEntity = findRoot ? findRootVehicle(entity) : entity;
if (rootEntity == null) {
return Collections.emptyList();
}
List<Entity> result = new ArrayList<>();
Queue<Entity> queue = new LinkedList<>();
queue.offer(rootEntity);
while (!queue.isEmpty()) {
Entity current = queue.poll();
result.add(current); // 把当前实体加入列表
List<Entity> passengers = current.getPassengers();
if (!passengers.isEmpty()) {
queue.addAll(passengers);
}
}
return Collections.unmodifiableList(result);
}
}

View File

@ -0,0 +1,160 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.util.riding;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import top.r3944realms.superleadrope.core.exception.RidingCycleException;
import top.r3944realms.superleadrope.core.util.ImmutablePair;
import top.r3944realms.superleadrope.util.model.RidingRelationship;
import javax.annotation.Nullable;
import java.util.*;
import java.util.function.Function;
public class RidingSaver {
/**
* 保存骑乘关系
*/
public static RidingRelationship save(@Nullable Entity entity) {
return save(entity, true);
}
/**
* 保存骑乘关系
*/
public static RidingRelationship save(@Nullable Entity entity, boolean findRoot) {
if (entity == null) {
return new RidingRelationship(Collections.emptyList(), null, null);
}
Entity rootEntity = findRoot ? RidingFinder.findRootVehicle(entity) : entity;
if (rootEntity == null) {
return new RidingRelationship(Collections.emptyList(), null, null);
}
RidingRelationship rootRelationship = new RidingRelationship();
rootRelationship.setEntityId(rootEntity.getUUID());
rootRelationship.setVehicleId(null);
rootRelationship.setPassengers(new ArrayList<>());
Queue<ImmutablePair<Entity, RidingRelationship>> queue = new LinkedList<>();
queue.offer(ImmutablePair.of(rootEntity, rootRelationship));
Set<UUID> processedEntities = new HashSet<>();
processedEntities.add(rootEntity.getUUID());
while (!queue.isEmpty()) {
ImmutablePair<Entity, RidingRelationship> current = queue.poll();
Entity currentEntity = current.first();
RidingRelationship currentRelation = current.second();
List<Entity> passengers = currentEntity.getPassengers();
if (!passengers.isEmpty()) {
for (Entity passenger : passengers) {
UUID passengerId = passenger.getUUID();
if (!processedEntities.contains(passengerId)) {
processedEntities.add(passengerId);
// 校验白名单
if (!RidingValidator.isInWhitelist(passenger.getType())) {
// 不在白名单直接截断
continue;
}
// 构建子关系
RidingRelationship passengerRelation = new RidingRelationship();
passengerRelation.setEntityId(passengerId);
passengerRelation.setVehicleId(currentEntity.getUUID());
passengerRelation.setPassengers(new ArrayList<>());
currentRelation.addPassenger(passengerRelation);
queue.offer(ImmutablePair.of(passenger, passengerRelation));
} else {
throw new RidingCycleException(
passengerId,
currentEntity.getUUID()
);
}
}
}
}
return rootRelationship;
}
/**
* 过滤骑乘关系只保留白名单根节点及其合法子树
* 如果根节点不在白名单则回退到第一个合法父节点
*/
public static RidingRelationship filterByWhitelistRoot(RidingRelationship relationship) {
if (relationship == null) return null;
// 如果当前根节点在白名单则直接处理子节点
if (RidingValidator.isInWhitelist(Objects.requireNonNull(getEntityType(relationship.getEntityId())))) {
RidingRelationship filtered = new RidingRelationship();
filtered.setEntityId(relationship.getEntityId());
filtered.setVehicleId(relationship.getVehicleId());
filtered.setPassengers(filterPassengers(relationship.getPassengers()));
return filtered;
} else {
// 根节点不在白名单尝试找到合法的子节点作为新的根
for (RidingRelationship child : relationship.getPassengers()) {
if (RidingValidator.isInWhitelist(Objects.requireNonNull(getEntityType(child.getEntityId())))) {
// 设置父节点为当前节点的父倒二叉逻辑
RidingRelationship newRoot = new RidingRelationship();
newRoot.setEntityId(child.getEntityId());
newRoot.setVehicleId(relationship.getVehicleId());
newRoot.setPassengers(filterPassengers(child.getPassengers()));
return newRoot;
}
}
}
// 如果整个子树都不在白名单返回空关系
return new RidingRelationship(new ArrayList<>(), null, null);
}
private static List<RidingRelationship> filterPassengers(List<RidingRelationship> passengers) {
if (passengers == null || passengers.isEmpty()) return new ArrayList<>();
List<RidingRelationship> filtered = new ArrayList<>();
for (RidingRelationship passenger : passengers) {
RidingRelationship childFiltered = filterByWhitelistRoot(passenger);
if (childFiltered != null && childFiltered.getEntityId() != null) {
filtered.add(childFiltered);
}
}
return filtered;
}
// 传入一个实体提供器 Function<UUID, Entity>通常在服务器侧就是 level::getEntity
private static Function<UUID, Entity> entityProvider;
public static void setEntityProvider(Function<UUID, Entity> provider) {
entityProvider = provider;
}
/**
* 根据UUID获取EntityType
*/
private static EntityType<?> getEntityType(UUID entityId) {
if (entityProvider == null) return null;
Entity entity = entityProvider.apply(entityId);
if (entity == null) return null;
return entity.getType();
}
}

View File

@ -0,0 +1,36 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.util.riding;
import com.google.gson.Gson;
import top.r3944realms.superleadrope.util.model.RidingRelationship;
public class RidingSerializer {
private static final Gson GSON = new Gson();
/**
* 序列化骑乘关系
*/
public static String serialize(RidingRelationship relationship) {
return GSON.toJson(relationship);
}
/**
* 反序列化骑乘关系
*/
public static RidingRelationship deserialize(String json) {
return GSON.fromJson(json, RidingRelationship.class);
}
}

View File

@ -0,0 +1,78 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.util.riding;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import top.r3944realms.superleadrope.config.LeashCommonConfig;
import java.util.LinkedList;
import java.util.Queue;
public class RidingValidator {
/**
* 是否在配置白名单里
*/
public static boolean isInWhitelist(EntityType<?> type) {
//noinspection deprecation
String key = type.builtInRegistryHolder().key().location().toString();
String modid = key.split(":")[0];
for (String entry : LeashCommonConfig.COMMON.teleportWhitelist.get()) {
if (entry.startsWith("#")) {
if (modid.equals(entry.substring(1))) {
return true;
}
} else if (entry.equals(key)) {
return true;
}
}
return false;
}
/**
* 检查骑乘是否会产生循环引用
*/
public static boolean wouldCreateCycle(Entity entity, Entity vehicle) {
// 如果实体就是载具本身直接产生循环
if (entity == vehicle) {
return true;
}
// 检查载具是否已经是实体的乘客直接或间接
return isIndirectPassenger(vehicle, entity);
}
/**
* 检查target是否是entity的间接乘客
*/
public static boolean isIndirectPassenger(Entity target, Entity entity) {
Queue<Entity> queue = new LinkedList<>();
queue.offer(entity);
while (!queue.isEmpty()) {
Entity current = queue.poll();
if (current == target) {
return true;
}
// 检查当前实体的所有乘客
for (Entity passenger : current.getPassengers()) {
queue.offer(passenger);
}
}
return false;
}
}

View File

@ -0,0 +1,101 @@
/*
* Super Lead rope mod
* Copyright (C) 2025 R3944Realms
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package top.r3944realms.superleadrope.util.riding;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.animal.Animal;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.Nullable;
import top.r3944realms.superleadrope.network.NetworkHandler;
import top.r3944realms.superleadrope.network.toClient.UpdatePlayerMovementPacket;
public class RindingLeash {
/**
* 获取乘坐链中第一个在白名单的载具如果没有则返回null
*/
@Nullable
public static Entity getSafeWhitelistRoot(Entity entity) {
if (entity == null) return null;
Entity root = RidingFinder.findRootVehicle(entity);
if (root == null) return null;
Entity current = root;
while (current != null) {
if (RidingValidator.isInWhitelist(current.getType())) {
return current; // 找到白名单载具
}
current = current.getVehicle();
}
return null; // 整条链条没有白名单载具
}
/**
* 获取最终可作用的载具用于拴绳合力应用
* 当链条中没有白名单载具时解除骑乘并返回自身
* 仅在拴绳合力不为零时调用
*/
public static Entity getFinalEntityForLeashIfForce(Entity entity, boolean hasForce) {
if (!hasForce || entity == null) {
return entity; // 没有力时直接返回原实体不做处理
}
Entity root = RidingFinder.findRootVehicle(entity);
if (root == null) return entity;
Entity current = root;
while (current != null) {
if (RidingValidator.isInWhitelist(current.getType())) {
return current; // 找到白名单载具
}
current = current.getVehicle();
}
// 没有白名单载具解除骑乘
RidingDismounts.dismountRootEntity(entity);
return entity; // 返回自身作为最终应用对象
}
/**
* 给动物应用拴绳力前的移动控制保护
*/
public static void protectAnimalMovement(Entity entity, boolean hasLeash) {
if (entity instanceof Animal mob) {
if (hasLeash) {
mob.goalSelector.disableControlFlag(Goal.Flag.MOVE);
entity.resetFallDistance();
} else {
mob.goalSelector.enableControlFlag(Goal.Flag.MOVE);
}
}
}
/**
* 给玩家应用拴绳力前的发包处理
*/
public static void applyForceToPlayer(ServerPlayer player, Vec3 force) {
NetworkHandler.sendToPlayer(
new UpdatePlayerMovementPacket(
UpdatePlayerMovementPacket.Operation.ADD,
force
), player
);
}
}

View File

@ -27,7 +27,7 @@ displayName="${mod_name}" #mandatory
# A file name (in the root of the mod JAR) containing a logo for display
logoFile="superleadrope_logo.png" #optional
# A text field displayed in the mod UI
#credits="" #optional
credits="Leisuretimedock"
# A text field displayed in the mod UI
authors="${mod_authors}" #optional
# Display Test controls the display for your mod in the server connection screen
@ -38,7 +38,15 @@ authors="${mod_authors}" #optional
# IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself.
#displayTest="MATCH_VERSION" # MATCH_VERSION is the default if nothing is specified (#optional)
# The description text for the mod (multi line!) (#mandatory)
description='''${mod_description}'''
description='''
${mod_description}
Audio Notice / :
This mod includes supplemental audio assets from Minecraft (Mojang Studios/Microsoft).
They are not covered by the GPL license of this project, and remain the property of Mojang Studios/Microsoft.
Minecraft (Mojang Studios/Microsoft)
GPL Mojang Studios/Microsoft
'''
# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional.
[[dependencies.${mod_id}]] #optional

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B