From 6a3674dd7eda66de6f0043b1422193d8254bbb74 Mon Sep 17 00:00:00 2001 From: Rose Hall Date: Tue, 31 Dec 2024 15:09:38 -0500 Subject: [PATCH] works :3 --- LICENSE | 121 ------- README.md | 9 - gradle.properties | 6 +- .../java/com/example/ExampleModClient.java | 10 - .../mixin/client/ExampleClientMixin.java | 15 - .../rose/healthsync/HealthSyncClient.java | 204 ++++++++++++ .../ly/hall/rose/healthsync/HeartType.java | 71 +++++ .../rose/healthsync/PlayerDamageInstance.java | 36 +++ .../rose/healthsync/PlayerDamageOverlay.java | 167 ++++++++++ .../rose/healthsync/PlayerIconRenderer.java | 42 +++ ...dSupportRenderingHealthsyncObjectives.java | 23 ++ src/client/resources/modid.client.mixins.json | 2 +- src/main/java/com/example/ExampleMod.java | 22 -- .../java/com/example/mixin/ExampleMixin.java | 15 - .../hall/rose/healthsync/DamageCallback.java | 8 + .../ly/hall/rose/healthsync/HealCallback.java | 7 + .../ly/hall/rose/healthsync/HealthSync.java | 294 ++++++++++++++++++ .../healthsync/LivingEntityDamageHandler.java | 61 ++++ .../healthsync/LivingEntityHealHandler.java | 53 ++++ .../healthsync/LivingEntityHealReason.java | 10 + .../LivingEntityPostDamageEvent.java | 38 +++ .../LivingEntityPostDamageHandler.java | 5 + .../hall/rose/healthsync/ModDamageTypes.java | 17 + .../rose/healthsync/NetworkingConstants.java | 8 + .../LivingEntityDamageCalculationMixin.java | 23 ++ .../heal/AbsorptionStatusEffectMixin.java | 18 ++ .../mixin/heal/HungerManagerMixin.java | 23 ++ .../mixin/heal/LivingEntityMixin.java | 38 +++ .../mixin/heal/PlayerEntityMixin.java | 33 ++ .../mixin/heal/PlayerManagerMixin.java | 27 ++ .../mixin/heal/StatusEffectMixin.java | 38 +++ src/main/resources/assets/healthsync/icon.png | Bin 0 -> 36138 bytes src/main/resources/assets/modid/icon.png | Bin 453 -> 0 bytes .../damage_type/absorption_changed.json | 5 + .../damage_type/synchronization_damage.json | 5 + src/main/resources/fabric.mod.json | 34 +- src/main/resources/healthsync.mixins.json | 17 + src/main/resources/modid.mixins.json | 11 - 38 files changed, 1286 insertions(+), 230 deletions(-) delete mode 100644 LICENSE delete mode 100644 README.md delete mode 100644 src/client/java/com/example/ExampleModClient.java delete mode 100644 src/client/java/com/example/mixin/client/ExampleClientMixin.java create mode 100644 src/client/java/ly/hall/rose/healthsync/HealthSyncClient.java create mode 100644 src/client/java/ly/hall/rose/healthsync/HeartType.java create mode 100644 src/client/java/ly/hall/rose/healthsync/PlayerDamageInstance.java create mode 100644 src/client/java/ly/hall/rose/healthsync/PlayerDamageOverlay.java create mode 100644 src/client/java/ly/hall/rose/healthsync/PlayerIconRenderer.java create mode 100644 src/client/java/ly/hall/rose/healthsync/mixin/client/InGameHudSupportRenderingHealthsyncObjectives.java delete mode 100644 src/main/java/com/example/ExampleMod.java delete mode 100644 src/main/java/com/example/mixin/ExampleMixin.java create mode 100644 src/main/java/ly/hall/rose/healthsync/DamageCallback.java create mode 100644 src/main/java/ly/hall/rose/healthsync/HealCallback.java create mode 100644 src/main/java/ly/hall/rose/healthsync/HealthSync.java create mode 100644 src/main/java/ly/hall/rose/healthsync/LivingEntityDamageHandler.java create mode 100644 src/main/java/ly/hall/rose/healthsync/LivingEntityHealHandler.java create mode 100644 src/main/java/ly/hall/rose/healthsync/LivingEntityHealReason.java create mode 100644 src/main/java/ly/hall/rose/healthsync/LivingEntityPostDamageEvent.java create mode 100644 src/main/java/ly/hall/rose/healthsync/LivingEntityPostDamageHandler.java create mode 100644 src/main/java/ly/hall/rose/healthsync/ModDamageTypes.java create mode 100644 src/main/java/ly/hall/rose/healthsync/NetworkingConstants.java create mode 100644 src/main/java/ly/hall/rose/healthsync/mixin/LivingEntityDamageCalculationMixin.java create mode 100644 src/main/java/ly/hall/rose/healthsync/mixin/heal/AbsorptionStatusEffectMixin.java create mode 100644 src/main/java/ly/hall/rose/healthsync/mixin/heal/HungerManagerMixin.java create mode 100644 src/main/java/ly/hall/rose/healthsync/mixin/heal/LivingEntityMixin.java create mode 100644 src/main/java/ly/hall/rose/healthsync/mixin/heal/PlayerEntityMixin.java create mode 100644 src/main/java/ly/hall/rose/healthsync/mixin/heal/PlayerManagerMixin.java create mode 100644 src/main/java/ly/hall/rose/healthsync/mixin/heal/StatusEffectMixin.java create mode 100644 src/main/resources/assets/healthsync/icon.png delete mode 100644 src/main/resources/assets/modid/icon.png create mode 100644 src/main/resources/data/healthsync/damage_type/absorption_changed.json create mode 100644 src/main/resources/data/healthsync/damage_type/synchronization_damage.json create mode 100644 src/main/resources/healthsync.mixins.json delete mode 100644 src/main/resources/modid.mixins.json diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 1625c17..0000000 --- a/LICENSE +++ /dev/null @@ -1,121 +0,0 @@ -Creative Commons Legal Code - -CC0 1.0 Universal - - CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE - LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN - ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS - INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES - REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS - PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM - THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED - HEREUNDER. - -Statement of Purpose - -The laws of most jurisdictions throughout the world automatically confer -exclusive Copyright and Related Rights (defined below) upon the creator -and subsequent owner(s) (each and all, an "owner") of an original work of -authorship and/or a database (each, a "Work"). - -Certain owners wish to permanently relinquish those rights to a Work for -the purpose of contributing to a commons of creative, cultural and -scientific works ("Commons") that the public can reliably and without fear -of later claims of infringement build upon, modify, incorporate in other -works, reuse and redistribute as freely as possible in any form whatsoever -and for any purposes, including without limitation commercial purposes. -These owners may contribute to the Commons to promote the ideal of a free -culture and the further production of creative, cultural and scientific -works, or to gain reputation or greater distribution for their Work in -part through the use and efforts of others. - -For these and/or other purposes and motivations, and without any -expectation of additional consideration or compensation, the person -associating CC0 with a Work (the "Affirmer"), to the extent that he or she -is an owner of Copyright and Related Rights in the Work, voluntarily -elects to apply CC0 to the Work and publicly distribute the Work under its -terms, with knowledge of his or her Copyright and Related Rights in the -Work and the meaning and intended legal effect of CC0 on those rights. - -1. Copyright and Related Rights. A Work made available under CC0 may be -protected by copyright and related or neighboring rights ("Copyright and -Related Rights"). Copyright and Related Rights include, but are not -limited to, the following: - - i. the right to reproduce, adapt, distribute, perform, display, - communicate, and translate a Work; - ii. moral rights retained by the original author(s) and/or performer(s); -iii. publicity and privacy rights pertaining to a person's image or - likeness depicted in a Work; - iv. rights protecting against unfair competition in regards to a Work, - subject to the limitations in paragraph 4(a), below; - v. rights protecting the extraction, dissemination, use and reuse of data - in a Work; - vi. database rights (such as those arising under Directive 96/9/EC of the - European Parliament and of the Council of 11 March 1996 on the legal - protection of databases, and under any national implementation - thereof, including any amended or successor version of such - directive); and -vii. other similar, equivalent or corresponding rights throughout the - world based on applicable law or treaty, and any national - implementations thereof. - -2. Waiver. To the greatest extent permitted by, but not in contravention -of, applicable law, Affirmer hereby overtly, fully, permanently, -irrevocably and unconditionally waives, abandons, and surrenders all of -Affirmer's Copyright and Related Rights and associated claims and causes -of action, whether now known or unknown (including existing as well as -future claims and causes of action), in the Work (i) in all territories -worldwide, (ii) for the maximum duration provided by applicable law or -treaty (including future time extensions), (iii) in any current or future -medium and for any number of copies, and (iv) for any purpose whatsoever, -including without limitation commercial, advertising or promotional -purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each -member of the public at large and to the detriment of Affirmer's heirs and -successors, fully intending that such Waiver shall not be subject to -revocation, rescission, cancellation, termination, or any other legal or -equitable action to disrupt the quiet enjoyment of the Work by the public -as contemplated by Affirmer's express Statement of Purpose. - -3. Public License Fallback. Should any part of the Waiver for any reason -be judged legally invalid or ineffective under applicable law, then the -Waiver shall be preserved to the maximum extent permitted taking into -account Affirmer's express Statement of Purpose. In addition, to the -extent the Waiver is so judged Affirmer hereby grants to each affected -person a royalty-free, non transferable, non sublicensable, non exclusive, -irrevocable and unconditional license to exercise Affirmer's Copyright and -Related Rights in the Work (i) in all territories worldwide, (ii) for the -maximum duration provided by applicable law or treaty (including future -time extensions), (iii) in any current or future medium and for any number -of copies, and (iv) for any purpose whatsoever, including without -limitation commercial, advertising or promotional purposes (the -"License"). The License shall be deemed effective as of the date CC0 was -applied by Affirmer to the Work. Should any part of the License for any -reason be judged legally invalid or ineffective under applicable law, such -partial invalidity or ineffectiveness shall not invalidate the remainder -of the License, and in such case Affirmer hereby affirms that he or she -will not (i) exercise any of his or her remaining Copyright and Related -Rights in the Work or (ii) assert any associated claims and causes of -action with respect to the Work, in either case contrary to Affirmer's -express Statement of Purpose. - -4. Limitations and Disclaimers. - - a. No trademark or patent rights held by Affirmer are waived, abandoned, - surrendered, licensed or otherwise affected by this document. - b. Affirmer offers the Work as-is and makes no representations or - warranties of any kind concerning the Work, express, implied, - statutory or otherwise, including without limitation warranties of - title, merchantability, fitness for a particular purpose, non - infringement, or the absence of latent or other defects, accuracy, or - the present or absence of errors, whether or not discoverable, all to - the greatest extent permissible under applicable law. - c. Affirmer disclaims responsibility for clearing rights of other persons - that may apply to the Work or any use thereof, including without - limitation any person's Copyright and Related Rights in the Work. - Further, Affirmer disclaims responsibility for obtaining any necessary - consents, permissions or other rights required for any use of the - Work. - d. Affirmer understands and acknowledges that Creative Commons is not a - party to this document and has no duty or obligation with respect to - this CC0 or use of the Work. \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index fd96346..0000000 --- a/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Fabric Example Mod - -## Setup - -For setup instructions please see the [fabric wiki page](https://fabricmc.net/wiki/tutorial:setup) that relates to the IDE that you are using. - -## License - -This template is available under the CC0 license. Feel free to learn from it and incorporate it in your own projects. diff --git a/gradle.properties b/gradle.properties index acdf74b..59ba403 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,8 +10,8 @@ loader_version=0.15.10 # Mod Properties mod_version=1.0.0 -maven_group=com.example -archives_base_name=modid +maven_group=ly.hall.rose.healthsync +archives_base_name=healthsync # Dependencies -fabric_version=0.97.8+1.20.6 \ No newline at end of file +fabric_version=0.97.8+1.20.6 diff --git a/src/client/java/com/example/ExampleModClient.java b/src/client/java/com/example/ExampleModClient.java deleted file mode 100644 index e2b0436..0000000 --- a/src/client/java/com/example/ExampleModClient.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.example; - -import net.fabricmc.api.ClientModInitializer; - -public class ExampleModClient implements ClientModInitializer { - @Override - public void onInitializeClient() { - // This entrypoint is suitable for setting up client-specific logic, such as rendering. - } -} \ No newline at end of file diff --git a/src/client/java/com/example/mixin/client/ExampleClientMixin.java b/src/client/java/com/example/mixin/client/ExampleClientMixin.java deleted file mode 100644 index 061b0ef..0000000 --- a/src/client/java/com/example/mixin/client/ExampleClientMixin.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.mixin.client; - -import net.minecraft.client.MinecraftClient; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(MinecraftClient.class) -public class ExampleClientMixin { - @Inject(at = @At("HEAD"), method = "run") - private void run(CallbackInfo info) { - // This code is injected into the start of MinecraftClient.run()V - } -} \ No newline at end of file diff --git a/src/client/java/ly/hall/rose/healthsync/HealthSyncClient.java b/src/client/java/ly/hall/rose/healthsync/HealthSyncClient.java new file mode 100644 index 0000000..f307332 --- /dev/null +++ b/src/client/java/ly/hall/rose/healthsync/HealthSyncClient.java @@ -0,0 +1,204 @@ +package ly.hall.rose.healthsync; + +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.EquipmentSlot; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.damage.DamageSources; +import net.minecraft.entity.damage.DamageType; +import net.minecraft.entity.damage.DamageTypes; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.text.Texts; +import net.minecraft.util.Arm; +import net.minecraft.util.math.Vec3d; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; + +public class HealthSyncClient implements ClientModInitializer { + private Text getReasonString(LivingEntityHealReason r) { + if (r == null) + return Texts.join(Text.of("Generic").getWithStyle(Style.EMPTY.withItalic(true)), Text.of("")); + + return switch (r) { + case Generic -> Texts.join(Text.of("Generic").getWithStyle(Style.EMPTY.withItalic(true)), Text.of("")); + case NaturalRegeneration -> Text.of("Natural Regeneration"); + case RegenerationStatusEffect -> Text.of("Regeneration"); + case InstantHealthStatusEffect -> Text.of("Instant Health"); + case AbsorptionStatusEffect -> Text.of("Absorption"); + case Synchronization -> null; + }; + } + + private String snakeToNormal(String snake) { + String[] strings = snake.split("_"); + + strings[0] = Character.toUpperCase(strings[0].charAt(0)) + strings[0].substring(1); + + return String.join(" ", strings); + } + + private Text getReasonString(Optional optSource, Text damagedUser) { + if (optSource.isEmpty()) + return Texts.join(Text.of("Generic").getWithStyle(Style.EMPTY.withItalic(true)), Text.of("")); + + var source = optSource.get(); + + String string = "death.attack." + source.getType().msgId(); + + if (source.getAttacker() == null && source.getSource() == null) + return Text.translatable(string, damagedUser); + + Text text = source.getAttacker() == null ? source.getSource().getDisplayName() : source.getAttacker().getDisplayName(); + Entity entity = source.getAttacker(); + ItemStack itemStack = entity instanceof LivingEntity + ? ((LivingEntity)entity).getMainHandStack() + : ItemStack.EMPTY; + + if (!itemStack.isEmpty() && itemStack.hasCustomName()) { + return Text.translatable(string + ".item", damagedUser, text, itemStack.toHoverableText()); + } + + return Text.translatable(string, damagedUser, text); + + +// var s = source.getType().msgId(); +// +// if (s.endsWith(".item")) +// s = s.substring(0, s.length()-5); +// +// if (s.endsWith(".player")) +// s = s.substring(0, s.length()-7); +// +// return Text.of(snakeToNormal(s)); + } + + @Override + public void onInitializeClient() { + var o = new PlayerDamageOverlay(); + HudRenderCallback.EVENT.register(o); + +// var random = new Random(); +// +// AtomicInteger i = new AtomicInteger(); + +// ClientTickEvents.END_CLIENT_TICK.register(client -> { +// i.getAndIncrement(); +// +// if (i.get() >= 20) { +// o.pushInstance(new PlayerDamageInstance( +// client.getGameProfile(), +// HeartType.NORMAL, +// Text.of("Demo"), +// random.ints(-10, 11).findFirst().getAsInt() < 0 ? -7 : 7 +// )); +// i.set(0); +// } +// }); + + ClientPlayNetworking.registerGlobalReceiver(NetworkingConstants.HEAL_EVENT_PACKET_ID, (client, handler, buf, responseSender) -> { + var uuid = buf.readUuid(); + var reason = buf.readOptional(r -> LivingEntityHealReason.valueOf(r.readString())); + var deltaHealth = buf.readFloat(); + var deltaAbsorption = buf.readFloat(); + + if (client.world == null) + return; + + if (client.getNetworkHandler() == null) + return; + + var listEntry = Objects.requireNonNull(client.getNetworkHandler()).getPlayerListEntry(uuid); + + if (listEntry == null) + return; + + if (deltaHealth != 0) { + o.pushInstance(new PlayerDamageInstance( + Objects.requireNonNull(client.getNetworkHandler()).getPlayerListEntry(uuid).getProfile(), + HeartType.NORMAL, + getReasonString(reason.orElse(null)), + deltaHealth + )); + } + + if (deltaAbsorption != 0) { + o.pushInstance(new PlayerDamageInstance( + Objects.requireNonNull(client.getNetworkHandler()).getPlayerListEntry(uuid).getProfile(), + HeartType.ABSORBING, + getReasonString(reason.orElse(null)), + deltaHealth + )); + } + }); + + ClientPlayNetworking.registerGlobalReceiver(NetworkingConstants.DAMAGE_EVENT_PACKET_ID, (client, handler, buf, responseSender) -> { + var uuid = buf.readUuid(); + var opt = buf.readOptional(bufx -> { + var sourceTypeId = bufx.readInt(); + var attackerId = bufx.readInt(); + var sourceId = bufx.readInt(); + var storedPosition = buf.readOptional(bufxx -> new Vec3d( + bufxx.readDouble(), + bufxx.readDouble(), + bufxx.readDouble() + )); + + assert client.world != null; + RegistryEntry entry = client.world.getRegistryManager().get(RegistryKeys.DAMAGE_TYPE).getEntry(sourceTypeId).orElseThrow(); + + Entity entity = attackerId == 0 ? null : client.world.getEntityById(attackerId - 1); + Entity entity2 = sourceId == 0 ? null : client.world.getEntityById(sourceId - 1); + + return storedPosition + .map(vec3d -> new DamageSource(entry, vec3d)) + .orElseGet(() -> new DamageSource(entry, entity, entity2)); + }); + var deltaTotal = buf.readFloat() + buf.readFloat(); + var isPoison = buf.readBoolean(); + var isWither = buf.readBoolean(); + var isFrozen = buf.readBoolean(); + + if (client.world == null) + return; + + if (client.getNetworkHandler() == null) + return; + + var listEntry = Objects.requireNonNull(client.getNetworkHandler()).getPlayerListEntry(uuid); + + if (listEntry == null) + return; + + if (opt.isPresent() && opt.get().isOf(DamageTypes.PLAYER_ATTACK) && opt.get().getAttacker() != null) { + o.pushInstance(new PlayerDamageInstance( + ((PlayerEntity)opt.get().getAttacker()).getGameProfile(), + HeartType.CONTAINER, + Text.of("Homicide"), + deltaTotal + )); + } + + o.pushInstance(new PlayerDamageInstance( + listEntry.getProfile(), + isPoison ? HeartType.POISONED : isWither ? HeartType.WITHERED : isFrozen ? HeartType.FROZEN : HeartType.CONTAINER, + getReasonString(opt, listEntry.getDisplayName() == null ? Text.of(listEntry.getProfile().getName()) : listEntry.getDisplayName()), + deltaTotal + )); + }); + } +} \ No newline at end of file diff --git a/src/client/java/ly/hall/rose/healthsync/HeartType.java b/src/client/java/ly/hall/rose/healthsync/HeartType.java new file mode 100644 index 0000000..dbd3478 --- /dev/null +++ b/src/client/java/ly/hall/rose/healthsync/HeartType.java @@ -0,0 +1,71 @@ +package ly.hall.rose.healthsync; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.render.*; +import net.minecraft.client.util.Icons; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.Identifier; +import org.joml.Matrix4f; + +public enum HeartType { + CONTAINER(0, false, true), + NORMAL(2, true, false), + POISONED(4, true, false), + WITHERED(6, true, false), + ABSORBING(8, false, false), + FROZEN(9, false, false); + + private final int textureIndex; + private final boolean hasBlinkingTexture; + private final boolean shouldCrop; + + private HeartType(int textureIndex, boolean hasBlinkingTexture, boolean shouldCrop) { + this.textureIndex = textureIndex; + this.hasBlinkingTexture = hasBlinkingTexture; + this.shouldCrop = shouldCrop; + } + + public int getU(boolean halfHeart, boolean blinking) { + int i; + if (this == CONTAINER) { + i = blinking ? 1 : 0; + } else { + int j = halfHeart ? 1 : 0; + int k = this.hasBlinkingTexture && blinking ? 2 : 0; + i = j + k; + } + return 16 + (this.textureIndex * 2 + i) * 9; + } + + public void draw(DrawContext ctx, int x, int y, boolean hardcore, boolean half, boolean blinking, boolean last, float opacity) { + int width = (half && this.shouldCrop) ? 5 : (last ? 9 : 8); + int height = 9; + + var ta = new Identifier("textures/gui/icons.png"); + + int x1 = x; + int y1 = y; + int x2 = x + width; + int y2 = y + height; + + float u1 = this.getU(half, blinking); + float v1 = (hardcore ? 5 : 0) * 9; + float u2 = u1 + width; + float v2 = v1 + height; + + RenderSystem.setShaderTexture(0, ta); + RenderSystem.setShader(GameRenderer::getPositionColorTexProgram); + RenderSystem.enableBlend(); + Matrix4f matrix4f = (new MatrixStack()).peek().getPositionMatrix(); + BufferBuilder bufferBuilder = Tessellator.getInstance().getBuffer(); + bufferBuilder.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_COLOR_TEXTURE); + bufferBuilder.vertex(matrix4f, x1, y1, 0).color(1.0F, 1.0F, 1.0F, opacity).texture(u1/256, v1/256).next(); + bufferBuilder.vertex(matrix4f, x1, y2, 0).color(1.0F, 1.0F, 1.0F, opacity).texture(u1/256, v2/256).next(); + bufferBuilder.vertex(matrix4f, x2, y2, 0).color(1.0F, 1.0F, 1.0F, opacity).texture(u2/256, v2/256).next(); + bufferBuilder.vertex(matrix4f, x2, y1, 0).color(1.0F, 1.0F, 1.0F, opacity).texture(u2/256, v1/256).next(); + BufferRenderer.drawWithGlobalProgram(bufferBuilder.end()); + RenderSystem.disableBlend(); + } +} \ No newline at end of file diff --git a/src/client/java/ly/hall/rose/healthsync/PlayerDamageInstance.java b/src/client/java/ly/hall/rose/healthsync/PlayerDamageInstance.java new file mode 100644 index 0000000..133574c --- /dev/null +++ b/src/client/java/ly/hall/rose/healthsync/PlayerDamageInstance.java @@ -0,0 +1,36 @@ +package ly.hall.rose.healthsync; + +import com.mojang.authlib.GameProfile; +import net.minecraft.text.Text; + +public class PlayerDamageInstance { + public GameProfile profile; + public Text damageReason; + public float health; + public float lifetime; + public HeartType heartType; + + PlayerDamageInstance(GameProfile profile, HeartType heartType, Text damageReason, float health) { + this.profile = profile; + this.heartType = heartType; + this.damageReason = damageReason; + this.health = health; + } + + public void addTime(float time) { + this.lifetime += time; + } + + public void updateHealth(float health) { + this.health += health; + this.lifetime = 0; + } + + public int getHalfHeartCount() { + return health < 0 ? -((int) Math.ceil(Math.abs(health))) : ((int) Math.ceil(Math.abs(health))); + } + + public boolean matches(PlayerDamageInstance other) { + return this.profile.equals(other.profile) && this.damageReason.getString().equals(other.damageReason.getString()) && this.heartType.equals(other.heartType); + } +} diff --git a/src/client/java/ly/hall/rose/healthsync/PlayerDamageOverlay.java b/src/client/java/ly/hall/rose/healthsync/PlayerDamageOverlay.java new file mode 100644 index 0000000..bbfe675 --- /dev/null +++ b/src/client/java/ly/hall/rose/healthsync/PlayerDamageOverlay.java @@ -0,0 +1,167 @@ +package ly.hall.rose.healthsync; + +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import com.mojang.blaze3d.systems.RenderSystem; +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +import net.minecraft.block.SculkShriekerBlock; +import net.minecraft.block.entity.SculkShriekerWarningManager; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.PlayerSkinDrawer; +import net.minecraft.client.gui.hud.InGameHud; +import net.minecraft.client.network.PlayerListEntry; +import net.minecraft.client.texture.PlayerSkinProvider; +import net.minecraft.entity.effect.StatusEffects; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.text.TextColor; +import net.minecraft.text.Texts; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.MathHelper; + +import java.util.*; + +public class PlayerDamageOverlay implements HudRenderCallback { + public static final int MAX_LIFETIME = 20 * 30; + public static final int FADE_START_TIME = 20 * 25; + public static final int FADE_LENGTH = MAX_LIFETIME - FADE_START_TIME; + + private final List instances = new ArrayList<>(); + + private final Object o = new Object(); + + int animationOffset = 0; + float ticks = 0; + + @Override + public void onHudRender(DrawContext drawContext, float tickDelta) { + ticks += tickDelta; + + synchronized (o) { + var client = MinecraftClient.getInstance(); + + if (client == null) + return; + + instances.sort(Comparator.comparingDouble(o -> o.lifetime)); + + int i = 0; + + Set toRemove = new HashSet<>(); + + if (animationOffset > 0) + animationOffset--; + + for (PlayerDamageInstance instance : instances) { + instance.addTime(tickDelta); + + if (instance.getHalfHeartCount() == 0) { + continue; + } + + if (animationOffset == 0 || i == 0) { + this.renderLine(drawContext, instance, i++ * 11); + } else { + this.renderLine(drawContext, instance, (i++ * 11) - animationOffset); + } + + if (instance.lifetime >= MAX_LIFETIME) + toRemove.add(instance); + } + + for (PlayerDamageInstance playerDamageInstance : toRemove) { + instances.remove(playerDamageInstance); + } + } + } + + public void pushInstance(PlayerDamageInstance i) { + synchronized (o) { + for (PlayerDamageInstance instance : this.instances) { + if (instance.matches(i)) { + instance.updateHealth(i.health); + return; + } + } + + this.instances.add(i); + animationOffset = 11; + } + } + + private float flip(float x) { return 1 - x; } + private float easeIn(float t) { return t * t; } + private float easeOut(float t) { return flip(easeIn(flip(t))); } + private void renderLine(DrawContext drawContext, PlayerDamageInstance instance, int rawBaseY) { + var client = MinecraftClient.getInstance(); + var window = client.getWindow(); + var world = Objects.requireNonNull(client.getCameraEntity()).getWorld(); + + var hardcore = world.getLevelProperties().isHardcore(); + var blinking = instance.lifetime < 20 && Math.floor(ticks / 3.0F) % 2 == 1; + + float alpha = 1; + + if (instance.lifetime > FADE_START_TIME) { + float progress = (instance.lifetime - FADE_START_TIME) / FADE_LENGTH; + alpha = MathHelper.clampedLerp(1, 0, easeOut(progress)); + } + + int completelyEmptyHeartCount; + + if (instance.getHalfHeartCount() == 0) { + completelyEmptyHeartCount = 0; + } else if (instance.getHalfHeartCount() < 0) { + completelyEmptyHeartCount = Math.floorDiv(-instance.getHalfHeartCount(), 2); + } else { + completelyEmptyHeartCount = (int) Math.ceil(((double) instance.getHalfHeartCount()) / 2.0F); + } + + assert MinecraftClient.getInstance().player != null; + int baseHeartRows = (int) (Math.ceil((Math.ceil(MinecraftClient.getInstance().player.getMaxHealth() / 2) + Math.ceil(MinecraftClient.getInstance().player.getAbsorptionAmount() / 2)) / 10)); + + int baseX = (window.getScaledWidth() / 2) - 91; + int baseY = window.getScaledHeight() - 40 - rawBaseY - (baseHeartRows * 10); + + for (int i = 0; i < completelyEmptyHeartCount; i++) { + HeartType.CONTAINER.draw(drawContext, baseX + (i * 8), baseY, hardcore, false, blinking, i == (completelyEmptyHeartCount - 1), alpha); + } + + if (instance.heartType.equals(HeartType.CONTAINER) && (-instance.getHalfHeartCount()) % 2 == 1) { + HeartType.CONTAINER.draw(drawContext, baseX + (completelyEmptyHeartCount * 8), baseY, hardcore, true, blinking, true, alpha); + } + + if (!instance.heartType.equals(HeartType.CONTAINER)) { + if (Math.abs(instance.getHalfHeartCount()) % 2 == 1) { + for (int i = 0; i < (Math.abs(completelyEmptyHeartCount) - 1); i++) { + instance.heartType.draw(drawContext, baseX + (i * 8), baseY, hardcore, false, blinking, false, alpha); + } + + instance.heartType.draw(drawContext, baseX + ((Math.abs(completelyEmptyHeartCount) - 1) * 8), baseY, hardcore, true, blinking, true, alpha); + } else { + for (int i = 0; i < Math.abs(completelyEmptyHeartCount); i++) { + instance.heartType.draw(drawContext, baseX + (i * 8), baseY, hardcore, false, blinking, i == Math.abs(completelyEmptyHeartCount) - 1, alpha); + } + } + } + + PlayerIconRenderer.draw(client.getSkinProvider().loadSkin(instance.profile), baseX - 10, baseY + 1, 8, true, alpha); + + Text t = Texts.join(List.of(new Text[] { + instance.damageReason, +// Optional.ofNullable(world.getPlayerByUuid(instance.profile.getId())) +// .map(PlayerEntity::getDisplayName) +// .orElse(Text.of(instance.profile.getName())), + }), Text.of(" / ")); + + var leftOffset = baseX - client.textRenderer.getWidth(t) - 12; + + int color = (((int) (alpha * 255)) << 24) | 0xFFFFFF; + + if ((alpha * 255) > 4) { + drawContext.drawText(client.textRenderer, t, leftOffset, baseY + 1, color, true); + } + } +} diff --git a/src/client/java/ly/hall/rose/healthsync/PlayerIconRenderer.java b/src/client/java/ly/hall/rose/healthsync/PlayerIconRenderer.java new file mode 100644 index 0000000..9f9de59 --- /dev/null +++ b/src/client/java/ly/hall/rose/healthsync/PlayerIconRenderer.java @@ -0,0 +1,42 @@ +package ly.hall.rose.healthsync; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.PlayerSkinDrawer; +import net.minecraft.client.render.*; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.Identifier; +import org.joml.Matrix4f; + +public class PlayerIconRenderer { + public static void draw(Identifier texture, int x, int y, int size, boolean hatVisible, float opacity) { + render(texture, x, y, size, size, 8.0f, 8, 8, 8, opacity); + if (hatVisible) { + drawHat(texture, x, y, size, opacity); + } + } + + private static void drawHat(Identifier texture, int x, int y, int size, float opacity) { + render(texture, x, y, size, size, 40.0f, 8, 8, 8, opacity); + } + + public static void render(Identifier texture, int x, int y, int width, int height, float u, float v, int regionWidth, int regionHeight, float opacity) { + float u1 = u/64.0F; + float v1 = v/64.0F; + float u2 = u1 + (8.0F/64.0F); + float v2 = v1 + (8.0F/64.0F); + + RenderSystem.setShaderTexture(0, texture); + RenderSystem.setShader(GameRenderer::getPositionColorTexProgram); + RenderSystem.enableBlend(); + Matrix4f matrix4f = (new MatrixStack()).peek().getPositionMatrix(); + BufferBuilder bufferBuilder = Tessellator.getInstance().getBuffer(); + bufferBuilder.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_COLOR_TEXTURE); + bufferBuilder.vertex(matrix4f, x, y, 0).color(1.0F, 1.0F, 1.0F, opacity).texture(u1, v1).next(); + bufferBuilder.vertex(matrix4f, x, y + height, 0).color(1.0F, 1.0F, 1.0F, opacity).texture(u1, v2).next(); + bufferBuilder.vertex(matrix4f, x + width, y + height, 0).color(1.0F, 1.0F, 1.0F, opacity).texture(u2, v2).next(); + bufferBuilder.vertex(matrix4f, x + width, y, 0).color(1.0F, 1.0F, 1.0F, opacity).texture(u2, v1).next(); + BufferRenderer.drawWithGlobalProgram(bufferBuilder.end()); + RenderSystem.disableBlend(); + } +} diff --git a/src/client/java/ly/hall/rose/healthsync/mixin/client/InGameHudSupportRenderingHealthsyncObjectives.java b/src/client/java/ly/hall/rose/healthsync/mixin/client/InGameHudSupportRenderingHealthsyncObjectives.java new file mode 100644 index 0000000..bcdbe94 --- /dev/null +++ b/src/client/java/ly/hall/rose/healthsync/mixin/client/InGameHudSupportRenderingHealthsyncObjectives.java @@ -0,0 +1,23 @@ +package ly.hall.rose.healthsync.mixin.client; + +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.hud.InGameHud; +import net.minecraft.scoreboard.ScoreboardObjective; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Objects; + +@Mixin(InGameHud.class) +public class InGameHudSupportRenderingHealthsyncObjectives { + @Inject(at = @At("HEAD"), method = "renderScoreboardSidebar") + private void run(DrawContext context, ScoreboardObjective objective, CallbackInfo ci) { +// if (Objects.equals(objective.getCriterion().getName(), "healthsync_health_contributed") || Objects.equals(objective.getCriterion().getName(), "healthsync_damage_contributed")) { +// ci.cancel(); +// +// +// } + } +} \ No newline at end of file diff --git a/src/client/resources/modid.client.mixins.json b/src/client/resources/modid.client.mixins.json index 9341450..20ce235 100644 --- a/src/client/resources/modid.client.mixins.json +++ b/src/client/resources/modid.client.mixins.json @@ -3,7 +3,7 @@ "package": "com.example.mixin.client", "compatibilityLevel": "JAVA_21", "client": [ - "ExampleClientMixin" + "ly.hall.rose.healthsync.mixin.client.ExampleClientMixin" ], "injectors": { "defaultRequire": 1 diff --git a/src/main/java/com/example/ExampleMod.java b/src/main/java/com/example/ExampleMod.java deleted file mode 100644 index f97cce9..0000000 --- a/src/main/java/com/example/ExampleMod.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.example; - -import net.fabricmc.api.ModInitializer; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ExampleMod implements ModInitializer { - // This logger is used to write text to the console and the log file. - // It is considered best practice to use your mod id as the logger's name. - // That way, it's clear which mod wrote info, warnings, and errors. - public static final Logger LOGGER = LoggerFactory.getLogger("modid"); - - @Override - public void onInitialize() { - // This code runs as soon as Minecraft is in a mod-load-ready state. - // However, some things (like resources) may still be uninitialized. - // Proceed with mild caution. - - LOGGER.info("Hello Fabric world!"); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/mixin/ExampleMixin.java b/src/main/java/com/example/mixin/ExampleMixin.java deleted file mode 100644 index 3c4212c..0000000 --- a/src/main/java/com/example/mixin/ExampleMixin.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.mixin; - -import net.minecraft.server.MinecraftServer; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(MinecraftServer.class) -public class ExampleMixin { - @Inject(at = @At("HEAD"), method = "loadWorld") - private void init(CallbackInfo info) { - // This code is injected into the start of MinecraftServer.loadWorld()V - } -} \ No newline at end of file diff --git a/src/main/java/ly/hall/rose/healthsync/DamageCallback.java b/src/main/java/ly/hall/rose/healthsync/DamageCallback.java new file mode 100644 index 0000000..7cf98ff --- /dev/null +++ b/src/main/java/ly/hall/rose/healthsync/DamageCallback.java @@ -0,0 +1,8 @@ +package ly.hall.rose.healthsync; + +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.server.network.ServerPlayerEntity; + +public interface DamageCallback { + void handle(ServerPlayerEntity e, DamageSource source, float deltaHealth, float deltaAbsorption); +} diff --git a/src/main/java/ly/hall/rose/healthsync/HealCallback.java b/src/main/java/ly/hall/rose/healthsync/HealCallback.java new file mode 100644 index 0000000..a2df59d --- /dev/null +++ b/src/main/java/ly/hall/rose/healthsync/HealCallback.java @@ -0,0 +1,7 @@ +package ly.hall.rose.healthsync; + +import net.minecraft.server.network.ServerPlayerEntity; + +public interface HealCallback { + void handle(ServerPlayerEntity e, LivingEntityHealReason reason, float deltaHealth, float deltaAbsorption); +} diff --git a/src/main/java/ly/hall/rose/healthsync/HealthSync.java b/src/main/java/ly/hall/rose/healthsync/HealthSync.java new file mode 100644 index 0000000..734e958 --- /dev/null +++ b/src/main/java/ly/hall/rose/healthsync/HealthSync.java @@ -0,0 +1,294 @@ +package ly.hall.rose.healthsync; + +import com.mojang.authlib.GameProfile; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.fabricmc.api.ModInitializer; + +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents; +import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents; +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.command.CommandSource; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.attribute.EntityAttributes; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.effect.StatusEffects; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.scoreboard.ScoreboardCriterion; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.world.GameMode; +import net.minecraft.world.PersistentState; +import net.minecraft.world.PersistentStateManager; +import net.minecraft.world.World; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.mojang.brigadier.builder.LiteralArgumentBuilder.literal; + +class PlayerStats { + public float TotalDamageTaken; + public float TotalHealed; +} + +record TaggedPlayerCoarseStats(String tag, int TotalHealthDamageTaken, int TotalHealthHealed) {} + +class GlobalHealthSaverLoader extends PersistentState { + public static GlobalHealthSaverLoader getServerState(MinecraftServer server) { + PersistentStateManager psm = server.getWorld(World.OVERWORLD).getPersistentStateManager(); + GlobalHealthSaverLoader state = psm.getOrCreate(GlobalHealthSaverLoader::createFromNbt, GlobalHealthSaverLoader::new, "healthsync"); + state.markDirty(); + + return state; + } + + public static PlayerStats getPlayerStats(LivingEntity player) { + var serverState = getServerState(Objects.requireNonNull(player.getWorld().getServer())); + + return serverState.players.computeIfAbsent(player.getUuid(), uuid -> new PlayerStats()); + } + + public static Stream> getAllPlayerStats(MinecraftServer server) { + return getServerState(server).players.entrySet().stream(); + } + + public float GlobalHealth = 20; + public float GlobalAbsorption = 0; + + public HashMap players = new HashMap<>(); + + public static GlobalHealthSaverLoader createFromNbt(NbtCompound tag) { + var s = new GlobalHealthSaverLoader(); + s.GlobalHealth = tag.getFloat("healthsync:globalHealth"); + s.GlobalAbsorption = tag.getFloat("healthsync:globalAbsorption"); + NbtCompound playersNbt = tag.getCompound("healthsync:players"); + playersNbt.getKeys().forEach(key -> { + PlayerStats playerData = new PlayerStats(); + + playerData.TotalHealed = playersNbt.getCompound(key).getFloat("totalhealed"); + playerData.TotalDamageTaken = playersNbt.getCompound(key).getFloat("totaldamagetaken"); + + UUID uuid = UUID.fromString(key); + s.players.put(uuid, playerData); + }); + return s; + } + + public NbtCompound writeNbt(NbtCompound nbt) { + nbt.putFloat("healthsync:globalHealth", GlobalHealth); + nbt.putFloat("healthsync:globalAbsorption", GlobalAbsorption); + NbtCompound playersNbt = new NbtCompound(); + players.forEach((uuid, playerData) -> { + NbtCompound playerNbt = new NbtCompound(); + + playerNbt.putFloat("totalhealed", playerData.TotalHealed); + playerNbt.putFloat("totaldamagetaken", playerData.TotalDamageTaken); + + playersNbt.put(uuid.toString(), playerNbt); + }); + nbt.put("healthsync:players", playersNbt); + return nbt; + } +} + +public class HealthSync implements ModInitializer { + // This logger is used to write text to the console and the log file. + // It is considered best practice to use your mod id as the logger's name. + // That way, it's clear which mod wrote info, warnings, and errors. + public static final Logger LOGGER = LoggerFactory.getLogger("healthsync"); + + @Override + public void onInitialize() { + var healthHealedCriterion = ScoreboardCriterion.create("healthsync_health_contributed", true, ScoreboardCriterion.RenderType.INTEGER); + var damageTakenCriterion = ScoreboardCriterion.create("healthsync_damage_contributed", true, ScoreboardCriterion.RenderType.INTEGER); + + CommandRegistrationCallback.EVENT.register((CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) -> { + LiteralArgumentBuilder builder = literal("healthsync"); + LiteralArgumentBuilder builder2 = literal("scoreboard"); + + dispatcher.register(builder.then(builder2.executes(c -> { + try { + var source = c.getSource(); + + var stats = GlobalHealthSaverLoader.getAllPlayerStats(source.getServer()) + .map(v -> new TaggedPlayerCoarseStats( + Objects.requireNonNull(source.getServer().getUserCache()).getByUuid(v.getKey()).map(GameProfile::getName).orElse(v.getKey().toString()), + (int) Math.ceil(v.getValue().TotalDamageTaken), + (int) Math.ceil(v.getValue().TotalHealed) + )) + .sorted(Comparator.comparingInt(TaggedPlayerCoarseStats::TotalHealthDamageTaken)) + .toList(); + + int maxNameLength = 0; + int maxHealLength = 0; + int maxDamageLength = 0; + + for (TaggedPlayerCoarseStats stat : stats) { + maxNameLength = Math.max(maxNameLength, stat.tag().length()); + maxHealLength = Math.max(maxHealLength, Float.toString(stat.TotalHealthHealed() / 2.0F).length()); + maxDamageLength = Math.max(maxDamageLength, Float.toString(stat.TotalHealthDamageTaken() / 2.0F).length()); + } + + for (TaggedPlayerCoarseStats stat : stats) { + source.sendMessage(Text.of(StringUtils.rightPad(stat.tag(), maxNameLength) + " | " + StringUtils.leftPad(Float.toString(stat.TotalHealthHealed() / 2.0F), maxHealLength) + "\uD83D\uDDA4 | " + StringUtils.leftPad(Float.toString(stat.TotalHealthDamageTaken() / 2.0F), maxDamageLength) + "\uD83D\uDC94")); + } + + return 1; + } catch (Exception e) { + HealthSync.LOGGER.error(e.toString()); + + return 1; + } + }))); + }); + + LivingEntityDamageHandler.initializeGeneric(); + + LivingEntityHealHandler.handle((e, reason, deltaHealth, deltaAbsorption) -> { + var state = GlobalHealthSaverLoader.getServerState(Objects.requireNonNull(e.getServer())); + + state.GlobalHealth += deltaHealth; + state.GlobalAbsorption += deltaAbsorption; + + if (state.GlobalAbsorption < 0) + state.GlobalAbsorption = 0; + + var stats = GlobalHealthSaverLoader.getPlayerStats(e); + + stats.TotalHealed += deltaHealth + deltaAbsorption; + + e.getScoreboard().forEachScore(healthHealedCriterion, e.getEntityName(), innerScore -> innerScore.setScore((int) stats.TotalHealed)); + + synchronizeHealth(Objects.requireNonNull(e.getServer())); + + for (ServerPlayerEntity serverPlayerEntity : e.getServer().getPlayerManager().getPlayerList()) { + var buf = PacketByteBufs.create(); + + buf.writeUuid(e.getUuid()); + + buf.writeOptional(Optional.ofNullable(reason), (bufx, pos) -> { + assert reason != null; + bufx.writeString(reason.toString()); + }); + + buf.writeFloat(deltaHealth); + buf.writeFloat(deltaAbsorption); + + ServerPlayNetworking.send(serverPlayerEntity, NetworkingConstants.HEAL_EVENT_PACKET_ID, buf); + } + }); + + LivingEntityDamageHandler.handle((e, source, deltaHealth, deltaAbsorption) -> { + var state = GlobalHealthSaverLoader.getServerState(Objects.requireNonNull(e.getServer())); + + state.GlobalHealth += deltaHealth; + state.GlobalAbsorption += deltaAbsorption; + + if (state.GlobalAbsorption < 0) + state.GlobalAbsorption = 0; + + var stats = GlobalHealthSaverLoader.getPlayerStats(e); + + stats.TotalDamageTaken += (-1 * deltaHealth) + (-1 * deltaAbsorption); + + e.getScoreboard().forEachScore(damageTakenCriterion, e.getEntityName(), innerScore -> innerScore.setScore((int) stats.TotalDamageTaken)); + + synchronizeHealth(Objects.requireNonNull(e.getServer())); + + for (ServerPlayerEntity serverPlayerEntity : e.getServer().getPlayerManager().getPlayerList()) { + var buf = PacketByteBufs.create(); + + buf.writeUuid(e.getUuid()); + + buf.writeOptional(Optional.ofNullable(source), (bufx, pos) -> { + assert source != null; + + int sourceTypeId = e.getWorld().getRegistryManager().get(RegistryKeys.DAMAGE_TYPE).getRawId(source.getType()); + int attackerId = source.getAttacker() == null ? 0 : (source.getAttacker().getId() + 1); + int sourceId = source.getSource() == null ? 0 : (source.getSource().getId() + 1); + + bufx.writeInt(sourceTypeId); + bufx.writeInt(attackerId); + bufx.writeInt(sourceId); + + bufx.writeOptional(Optional.ofNullable(source.getStoredPosition()), (bufxx, poss) -> { + bufxx.writeDouble(source.getStoredPosition().getX()); + bufxx.writeDouble(source.getStoredPosition().getY()); + bufxx.writeDouble(source.getStoredPosition().getZ()); + }); + }); + + buf.writeFloat(deltaHealth); + buf.writeFloat(deltaAbsorption); + buf.writeBoolean(e.hasStatusEffect(StatusEffects.POISON)); + buf.writeBoolean(e.hasStatusEffect(StatusEffects.WITHER)); + buf.writeBoolean(e.isFrozen()); + + ServerPlayNetworking.send(serverPlayerEntity, NetworkingConstants.DAMAGE_EVENT_PACKET_ID, buf); + } + }); + + Object o = new Object(); + + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { + synchronized(o) { + var state = GlobalHealthSaverLoader.getServerState(server); + + LivingEntityHealHandler.applyReason(LivingEntityHealReason.Synchronization); + handler.player.setHealth(state.GlobalHealth); + + LivingEntityHealHandler.applyReason(LivingEntityHealReason.Synchronization); + handler.player.setAbsorptionAmount(state.GlobalAbsorption); + } + }); + + ServerPlayerEvents.AFTER_RESPAWN.register((oldPlayer, newPlayer, alive) -> { + if (newPlayer.getWorld().getPlayers().stream().allMatch(e -> e.isDead() || e.getUuid().equals(newPlayer.getUuid()))) { + var state = GlobalHealthSaverLoader.getServerState(Objects.requireNonNull(newPlayer.getServer())); + + state.GlobalHealth = 20; + state.GlobalAbsorption = 0; + } + }); + } + + private void synchronizeHealth(MinecraftServer server) { + for (ServerPlayerEntity serverPlayerEntity : server.getPlayerManager().getPlayerList()) { + var state = GlobalHealthSaverLoader.getServerState(Objects.requireNonNull(server)); + + if (serverPlayerEntity.getHealth() < state.GlobalHealth) { + LivingEntityHealHandler.applyReason(LivingEntityHealReason.Synchronization); + serverPlayerEntity.setHealth(state.GlobalHealth); + } + + if (serverPlayerEntity.getHealth() > state.GlobalHealth) { + serverPlayerEntity.setHealth(state.GlobalHealth); + } + + if (serverPlayerEntity.getAbsorptionAmount() < state.GlobalAbsorption) { + LivingEntityHealHandler.applyReason(LivingEntityHealReason.Synchronization); + serverPlayerEntity.setAbsorptionAmount(state.GlobalAbsorption); + } + + if (serverPlayerEntity.getAbsorptionAmount() > state.GlobalAbsorption) { + serverPlayerEntity.setAbsorptionAmount(state.GlobalAbsorption); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/ly/hall/rose/healthsync/LivingEntityDamageHandler.java b/src/main/java/ly/hall/rose/healthsync/LivingEntityDamageHandler.java new file mode 100644 index 0000000..781425f --- /dev/null +++ b/src/main/java/ly/hall/rose/healthsync/LivingEntityDamageHandler.java @@ -0,0 +1,61 @@ +package ly.hall.rose.healthsync; + +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; + +import java.util.HashSet; +import java.util.Set; + +public class LivingEntityDamageHandler { + private static Set callbackSet = new HashSet<>(); + private static Set entitiesTakingDamage = new HashSet<>(); + + public static void initializeGeneric() { + ServerLivingEntityEvents.ALLOW_DAMAGE.register((LivingEntity e, DamageSource s, float a) -> { + var oldHealth = e.getHealth(); + var oldAbsorption = e.getAbsorptionAmount(); + + entitiesTakingDamage.add(e); + + LivingEntityPostDamageEvent.once(e, () -> { + var healthDelta = e.getHealth() - oldHealth; + var absorptionDelta = e.getAbsorptionAmount() - oldAbsorption; + + entitiesTakingDamage.remove(e); + + LivingEntityDamageHandler.fire(e, s, healthDelta, absorptionDelta); + }); + + return true; + }); + } + + public static void fire(LivingEntity e, DamageSource source, float healthAmount, float absorptionAmount) { + if (!(e instanceof ServerPlayerEntity)) + return; + + if (source.isOf(ModDamageTypes.SYNCHRONIZATION_DAMAGE)) + return; + + callbackSet.forEach(cb -> cb.handle((ServerPlayerEntity) e, source, healthAmount, absorptionAmount)); + } + + public static void fireFromAbsorption(PlayerEntity e, float absorptionAmount) { + if (entitiesTakingDamage.contains(e)) + return; + + fire(e, ModDamageTypes.of(e.getWorld(), ModDamageTypes.ABSORPTION_CHANGED), 0, absorptionAmount); + } + + public static void handle(DamageCallback cb) { + callbackSet.add(cb); + } + + public static void unhandle(DamageCallback cb) { + callbackSet.remove(cb); + } +} diff --git a/src/main/java/ly/hall/rose/healthsync/LivingEntityHealHandler.java b/src/main/java/ly/hall/rose/healthsync/LivingEntityHealHandler.java new file mode 100644 index 0000000..0eb02e9 --- /dev/null +++ b/src/main/java/ly/hall/rose/healthsync/LivingEntityHealHandler.java @@ -0,0 +1,53 @@ +package ly.hall.rose.healthsync; + +import net.minecraft.entity.LivingEntity; +import net.minecraft.server.network.ServerPlayerEntity; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +public class LivingEntityHealHandler { + private static Set callbackSet = new HashSet<>(); + private static LivingEntityHealReason appliedReason = null; + public static Set loadingPlayers = new HashSet<>(); + + public static void applyReason(LivingEntityHealReason reason) { + appliedReason = reason; + } + + private static LivingEntityHealReason consumeReason() { + var reason = appliedReason; + appliedReason = null; + return reason; + } + + public static void fire(LivingEntity e, float healthAmount, float absorptionAmount) { + var reason = consumeReason(); + + if (!(e instanceof ServerPlayerEntity)) + return; + + // handle setting health during construction + if (((ServerPlayerEntity) e).getGameProfile() == null) + return; + + if (reason == LivingEntityHealReason.Synchronization) + return; + + if (loadingPlayers.contains(e)) + return; + + for (HealCallback healCallback : callbackSet) { + healCallback.handle((ServerPlayerEntity) e, reason, healthAmount, absorptionAmount); + } + } + + public static void handle(HealCallback cb) { + callbackSet.add(cb); + } + + public static void unhandle(HealCallback cb) { + callbackSet.remove(cb); + } +} diff --git a/src/main/java/ly/hall/rose/healthsync/LivingEntityHealReason.java b/src/main/java/ly/hall/rose/healthsync/LivingEntityHealReason.java new file mode 100644 index 0000000..487e52c --- /dev/null +++ b/src/main/java/ly/hall/rose/healthsync/LivingEntityHealReason.java @@ -0,0 +1,10 @@ +package ly.hall.rose.healthsync; + +public enum LivingEntityHealReason { + Generic, + NaturalRegeneration, + RegenerationStatusEffect, + InstantHealthStatusEffect, + AbsorptionStatusEffect, + Synchronization +} diff --git a/src/main/java/ly/hall/rose/healthsync/LivingEntityPostDamageEvent.java b/src/main/java/ly/hall/rose/healthsync/LivingEntityPostDamageEvent.java new file mode 100644 index 0000000..ef08407 --- /dev/null +++ b/src/main/java/ly/hall/rose/healthsync/LivingEntityPostDamageEvent.java @@ -0,0 +1,38 @@ +package ly.hall.rose.healthsync; + +import net.minecraft.entity.LivingEntity; +import java.util.HashSet; +import java.util.Set; + +class LivingEntityPostDamageEventHandlerKeyValuePair { + public LivingEntity entity; + public LivingEntityPostDamageHandler handler; + + public LivingEntityPostDamageEventHandlerKeyValuePair(LivingEntity e, LivingEntityPostDamageHandler h) { + this.entity = e; + this.handler = h; + } +} + +public class LivingEntityPostDamageEvent { + private static Set handlers = new HashSet(); + + public static void once(LivingEntity e, LivingEntityPostDamageHandler handler) { + var keyValuePair = new LivingEntityPostDamageEventHandlerKeyValuePair(e, null); + + keyValuePair.handler = () -> { + handlers.remove(keyValuePair); + handler.handle(); + }; + + handlers.add(keyValuePair); + } + + public static void emit(LivingEntity e) { + for (var handler : handlers) { + if (handler.entity.equals(e)) { + handler.handler.handle(); + } + } + } +} diff --git a/src/main/java/ly/hall/rose/healthsync/LivingEntityPostDamageHandler.java b/src/main/java/ly/hall/rose/healthsync/LivingEntityPostDamageHandler.java new file mode 100644 index 0000000..fe5b334 --- /dev/null +++ b/src/main/java/ly/hall/rose/healthsync/LivingEntityPostDamageHandler.java @@ -0,0 +1,5 @@ +package ly.hall.rose.healthsync; + +public interface LivingEntityPostDamageHandler { + public void handle(); +} diff --git a/src/main/java/ly/hall/rose/healthsync/ModDamageTypes.java b/src/main/java/ly/hall/rose/healthsync/ModDamageTypes.java new file mode 100644 index 0000000..2d1176b --- /dev/null +++ b/src/main/java/ly/hall/rose/healthsync/ModDamageTypes.java @@ -0,0 +1,17 @@ +package ly.hall.rose.healthsync; + +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.damage.DamageType; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.util.Identifier; +import net.minecraft.world.World; + +public class ModDamageTypes { + public static final RegistryKey SYNCHRONIZATION_DAMAGE = RegistryKey.of(RegistryKeys.DAMAGE_TYPE, new Identifier("healthsync", "synchronization_damage")); + public static final RegistryKey ABSORPTION_CHANGED = RegistryKey.of(RegistryKeys.DAMAGE_TYPE, new Identifier("healthsync", "absorption_changed")); + + public static DamageSource of(World world, RegistryKey key) { + return new DamageSource(world.getRegistryManager().get(RegistryKeys.DAMAGE_TYPE).entryOf(key)); + } +} diff --git a/src/main/java/ly/hall/rose/healthsync/NetworkingConstants.java b/src/main/java/ly/hall/rose/healthsync/NetworkingConstants.java new file mode 100644 index 0000000..6760c77 --- /dev/null +++ b/src/main/java/ly/hall/rose/healthsync/NetworkingConstants.java @@ -0,0 +1,8 @@ +package ly.hall.rose.healthsync; + +import net.minecraft.util.Identifier; + +public class NetworkingConstants { + public static final Identifier HEAL_EVENT_PACKET_ID = new Identifier("healthsync", "heal_event"); + public static final Identifier DAMAGE_EVENT_PACKET_ID = new Identifier("healthsync", "damage_event"); +} diff --git a/src/main/java/ly/hall/rose/healthsync/mixin/LivingEntityDamageCalculationMixin.java b/src/main/java/ly/hall/rose/healthsync/mixin/LivingEntityDamageCalculationMixin.java new file mode 100644 index 0000000..d543fbd --- /dev/null +++ b/src/main/java/ly/hall/rose/healthsync/mixin/LivingEntityDamageCalculationMixin.java @@ -0,0 +1,23 @@ +package ly.hall.rose.healthsync.mixin; + +import ly.hall.rose.healthsync.LivingEntityPostDamageEvent; +import ly.hall.rose.healthsync.LivingEntityPostDamageHandler; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.server.MinecraftServer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.HashSet; +import java.util.Set; + +@Mixin(LivingEntity.class) +public class LivingEntityDamageCalculationMixin { + @Inject(at = @At("RETURN"), method = "damage") + private void postDamage(DamageSource source, float amount, CallbackInfoReturnable cir) { + LivingEntityPostDamageEvent.emit((LivingEntity)(Object)this); + } +} \ No newline at end of file diff --git a/src/main/java/ly/hall/rose/healthsync/mixin/heal/AbsorptionStatusEffectMixin.java b/src/main/java/ly/hall/rose/healthsync/mixin/heal/AbsorptionStatusEffectMixin.java new file mode 100644 index 0000000..bf1f8d5 --- /dev/null +++ b/src/main/java/ly/hall/rose/healthsync/mixin/heal/AbsorptionStatusEffectMixin.java @@ -0,0 +1,18 @@ +package ly.hall.rose.healthsync.mixin.heal; + +import ly.hall.rose.healthsync.LivingEntityHealHandler; +import ly.hall.rose.healthsync.LivingEntityHealReason; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.attribute.AttributeContainer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(targets = "net.minecraft.entity.effect.AbsorptionStatusEffect") +public class AbsorptionStatusEffectMixin { + @Inject(at = @At(value = "HEAD"), method = "onApplied") + private void handleRegen(LivingEntity entity, AttributeContainer attributes, int amplifier, CallbackInfo ci) { + LivingEntityHealHandler.applyReason(LivingEntityHealReason.AbsorptionStatusEffect); + } +} \ No newline at end of file diff --git a/src/main/java/ly/hall/rose/healthsync/mixin/heal/HungerManagerMixin.java b/src/main/java/ly/hall/rose/healthsync/mixin/heal/HungerManagerMixin.java new file mode 100644 index 0000000..7b49641 --- /dev/null +++ b/src/main/java/ly/hall/rose/healthsync/mixin/heal/HungerManagerMixin.java @@ -0,0 +1,23 @@ +package ly.hall.rose.healthsync.mixin.heal; + +import ly.hall.rose.healthsync.LivingEntityHealHandler; +import ly.hall.rose.healthsync.LivingEntityHealReason; +import net.minecraft.entity.player.HungerManager; +import net.minecraft.entity.player.PlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(HungerManager.class) +public class HungerManagerMixin { + @Inject(at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/player/PlayerEntity;heal(F)V", ordinal = 0), method = "update") + private void handleRegen1(PlayerEntity player, CallbackInfo ci) { + LivingEntityHealHandler.applyReason(LivingEntityHealReason.NaturalRegeneration); + } + + @Inject(at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/player/PlayerEntity;heal(F)V", ordinal = 1), method = "update") + private void handleRegen2(PlayerEntity player, CallbackInfo ci) { + LivingEntityHealHandler.applyReason(LivingEntityHealReason.NaturalRegeneration); + } +} diff --git a/src/main/java/ly/hall/rose/healthsync/mixin/heal/LivingEntityMixin.java b/src/main/java/ly/hall/rose/healthsync/mixin/heal/LivingEntityMixin.java new file mode 100644 index 0000000..f66a2aa --- /dev/null +++ b/src/main/java/ly/hall/rose/healthsync/mixin/heal/LivingEntityMixin.java @@ -0,0 +1,38 @@ +package ly.hall.rose.healthsync.mixin.heal; + +import ly.hall.rose.healthsync.*; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.entity.effect.StatusEffects; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.math.MathHelper; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(LivingEntity.class) +public class LivingEntityMixin { + @Inject(at = @At(value = "HEAD"), method = "setHealth") + private void handleHealthUpdate(float targetHealth, CallbackInfo ci) { + var currentHealth = ((LivingEntity)(Object)this).getHealth(); + var difference = Float.valueOf(MathHelper.clamp(targetHealth, 0.0f, ((LivingEntity)(Object)this).getMaxHealth())) - currentHealth; + + if (difference > 0) { + LivingEntityHealHandler.fire((LivingEntity)(Object)this, difference, 0); + } + } + + @Inject(at = @At("HEAD"), method = "onStatusEffectRemoved") + private void handleStatsuEffectRemoved(StatusEffectInstance effect, CallbackInfo ci) { + var self = (LivingEntity) (Object) this; + + if (self instanceof ServerPlayerEntity) { + if (effect.getEffectType().equals(StatusEffects.ABSORPTION)) { + LivingEntityDamageHandler.fireFromAbsorption((ServerPlayerEntity) self, -self.getAbsorptionAmount()); + } + } + } +} diff --git a/src/main/java/ly/hall/rose/healthsync/mixin/heal/PlayerEntityMixin.java b/src/main/java/ly/hall/rose/healthsync/mixin/heal/PlayerEntityMixin.java new file mode 100644 index 0000000..94848e2 --- /dev/null +++ b/src/main/java/ly/hall/rose/healthsync/mixin/heal/PlayerEntityMixin.java @@ -0,0 +1,33 @@ +package ly.hall.rose.healthsync.mixin.heal; + +import ly.hall.rose.healthsync.*; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.player.PlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(PlayerEntity.class) +public class PlayerEntityMixin { + @Inject(at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/player/PlayerEntity;heal(F)V"), method = "tickMovement") + private void handleRegen(CallbackInfo ci) { + LivingEntityHealHandler.applyReason(LivingEntityHealReason.NaturalRegeneration); + } + + @Inject(at = @At(value = "HEAD"), method = "setAbsorptionAmount") + private void handleAbsorptionUpdate(float targetAbsorption, CallbackInfo ci) { + var currentAbsorption = ((PlayerEntity)(Object)this).getAbsorptionAmount(); + var difference = targetAbsorption - currentAbsorption; + + if (difference > 0) { + LivingEntityHealHandler.fire((PlayerEntity)(Object)this, 0, difference); + } + + if (difference < 0) { + LivingEntityDamageHandler.fireFromAbsorption((PlayerEntity)(Object)this, difference); + } + } +} diff --git a/src/main/java/ly/hall/rose/healthsync/mixin/heal/PlayerManagerMixin.java b/src/main/java/ly/hall/rose/healthsync/mixin/heal/PlayerManagerMixin.java new file mode 100644 index 0000000..19135e2 --- /dev/null +++ b/src/main/java/ly/hall/rose/healthsync/mixin/heal/PlayerManagerMixin.java @@ -0,0 +1,27 @@ +package ly.hall.rose.healthsync.mixin.heal; + +import ly.hall.rose.healthsync.LivingEntityHealHandler; +import ly.hall.rose.healthsync.LivingEntityHealReason; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.server.PlayerManager; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.HashSet; +import java.util.Set; + +@Mixin(PlayerManager.class) +public class PlayerManagerMixin { + @Inject(at = @At("HEAD"), method = "loadPlayerData") + public void onPlayerDataStart(ServerPlayerEntity player, CallbackInfoReturnable cir) { + LivingEntityHealHandler.loadingPlayers.add(player); + } + + @Inject(at = @At("RETURN"), method = "loadPlayerData") + public void onPlayerDataEnd(ServerPlayerEntity player, CallbackInfoReturnable cir) { + LivingEntityHealHandler.loadingPlayers.remove(player); + } +} diff --git a/src/main/java/ly/hall/rose/healthsync/mixin/heal/StatusEffectMixin.java b/src/main/java/ly/hall/rose/healthsync/mixin/heal/StatusEffectMixin.java new file mode 100644 index 0000000..0f16104 --- /dev/null +++ b/src/main/java/ly/hall/rose/healthsync/mixin/heal/StatusEffectMixin.java @@ -0,0 +1,38 @@ +package ly.hall.rose.healthsync.mixin.heal; + +import ly.hall.rose.healthsync.LivingEntityDamageHandler; +import ly.hall.rose.healthsync.LivingEntityHealHandler; +import ly.hall.rose.healthsync.LivingEntityHealReason; +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.attribute.*; +import net.minecraft.entity.effect.StatusEffect; +import net.minecraft.entity.effect.StatusEffects; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Map; + +@Mixin(StatusEffect.class) +public class StatusEffectMixin { + @Inject(at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/LivingEntity;heal(F)V"), method = "applyInstantEffect") + public void h1(Entity source, Entity attacker, LivingEntity target, int amplifier, double proximity, CallbackInfo ci) { + if (target instanceof ServerPlayerEntity) { + LivingEntityHealHandler.applyReason(LivingEntityHealReason.InstantHealthStatusEffect); + } + } + + @Inject(at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/LivingEntity;heal(F)V"), method = "applyUpdateEffect") + public void h1(LivingEntity entity, int amplifier, CallbackInfo ci) { + if (entity instanceof ServerPlayerEntity) { + if ((Object) this == StatusEffects.REGENERATION) { + LivingEntityHealHandler.applyReason(LivingEntityHealReason.RegenerationStatusEffect); + } else { + LivingEntityHealHandler.applyReason(LivingEntityHealReason.InstantHealthStatusEffect); + } + } + } +} diff --git a/src/main/resources/assets/healthsync/icon.png b/src/main/resources/assets/healthsync/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5e6d24f11768adde3803c6ae7f5b4f09ae60256f GIT binary patch literal 36138 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�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�S`MO|{X| z15JpJmXT`KbAUb2g!lkEuBkR!dY}pM(K1radJeD$nh+mg$2HYPOAj<5K3YbqS*fF0LV8!bK1g!pI~sb)O~*aQCuTvpPal%d^G P00000NkvXXu0mjff3r-< literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/modid/icon.png b/src/main/resources/assets/modid/icon.png deleted file mode 100644 index 047b91f2347de5cf95f23284476fddbe21ba23fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 453 zcmV;$0XqJPP)QAFYGys`80vegN0XDFh0OXKz&i8?Le#x7{1X)R+00000NkvXXu0mjf73i~T diff --git a/src/main/resources/data/healthsync/damage_type/absorption_changed.json b/src/main/resources/data/healthsync/damage_type/absorption_changed.json new file mode 100644 index 0000000..80bff32 --- /dev/null +++ b/src/main/resources/data/healthsync/damage_type/absorption_changed.json @@ -0,0 +1,5 @@ +{ + "message_id": "absorption_changed", + "exhaustion": 0, + "scaling": "never" +} \ No newline at end of file diff --git a/src/main/resources/data/healthsync/damage_type/synchronization_damage.json b/src/main/resources/data/healthsync/damage_type/synchronization_damage.json new file mode 100644 index 0000000..79df744 --- /dev/null +++ b/src/main/resources/data/healthsync/damage_type/synchronization_damage.json @@ -0,0 +1,5 @@ +{ + "message_id": "synchronization_damage", + "exhaustion": 0, + "scaling": "never" +} \ No newline at end of file diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 73d4a76..61fe7f4 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -1,41 +1,29 @@ { "schemaVersion": 1, - "id": "modid", + "id": "healthsync", "version": "${version}", - "name": "Example mod", - "description": "This is an example description! Tell everyone what your mod is about!", + "name": "HealthSync", + "description": "Sync your health with other players", "authors": [ - "Me!" + "Rose Kodsi-Hall" ], - "contact": { - "homepage": "https://fabricmc.net/", - "sources": "https://github.com/FabricMC/fabric-example-mod" - }, - "license": "CC0-1.0", - "icon": "assets/modid/icon.png", + "icon": "assets/healthsync/icon.png", "environment": "*", "entrypoints": { "main": [ - "com.example.ExampleMod" + "ly.hall.rose.healthsync.HealthSync" ], "client": [ - "com.example.ExampleModClient" + "ly.hall.rose.healthsync.HealthSyncClient" ] }, "mixins": [ - "modid.mixins.json", - { - "config": "modid.client.mixins.json", - "environment": "client" - } + "healthsync.mixins.json" ], "depends": { - "fabricloader": ">=0.15.10", - "minecraft": "~1.20.6", - "java": ">=21", + "fabricloader": ">=0.14.23", + "minecraft": "~1.20.1", + "java": ">=17", "fabric-api": "*" - }, - "suggests": { - "another-mod": "*" } } \ No newline at end of file diff --git a/src/main/resources/healthsync.mixins.json b/src/main/resources/healthsync.mixins.json new file mode 100644 index 0000000..d191fc7 --- /dev/null +++ b/src/main/resources/healthsync.mixins.json @@ -0,0 +1,17 @@ +{ + "required": true, + "package": "ly.hall.rose.healthsync.mixin", + "compatibilityLevel": "JAVA_17", + "mixins": [ + "LivingEntityDamageCalculationMixin", + "heal.AbsorptionStatusEffectMixin", + "heal.HungerManagerMixin", + "heal.LivingEntityMixin", + "heal.PlayerEntityMixin", + "heal.PlayerManagerMixin", + "heal.StatusEffectMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} \ No newline at end of file diff --git a/src/main/resources/modid.mixins.json b/src/main/resources/modid.mixins.json deleted file mode 100644 index f7fc0a3..0000000 --- a/src/main/resources/modid.mixins.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "required": true, - "package": "com.example.mixin", - "compatibilityLevel": "JAVA_21", - "mixins": [ - "ExampleMixin" - ], - "injectors": { - "defaultRequire": 1 - } -} \ No newline at end of file