From 5276e52f4589c22cbe04f3ae0280c0751db26b79 Mon Sep 17 00:00:00 2001 From: Rongmario Date: Sun, 28 May 2023 03:55:27 +0100 Subject: [PATCH 01/10] Formatting + some extra documentation --- build.gradle | 171 +++++++++++++++++++++++++----------------------- settings.gradle | 14 ++-- 2 files changed, 96 insertions(+), 89 deletions(-) diff --git a/build.gradle b/build.gradle index ecadeb0..9e82e74 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,13 @@ -import com.gtnewhorizons.retrofuturagradle.mcp.ReobfuscatedJar import org.jetbrains.gradle.ext.Gradle plugins { - id("java") - id("java-library") - id("maven-publish") - id("org.jetbrains.gradle.plugin.idea-ext") version "1.1.7" - id("eclipse") - id("com.gtnewhorizons.retrofuturagradle") version "1.3.9" - id("com.matthewprenger.cursegradle") version "1.4.0" + id 'java' + id 'java-library' + id 'maven-publish' + id 'org.jetbrains.gradle.plugin.idea-ext' version '1.1.7' + id 'eclipse' + id 'com.gtnewhorizons.retrofuturagradle' version '1.3.16' + id 'com.matthewprenger.cursegradle' version '1.4.0' } version = project.mod_version @@ -24,11 +23,11 @@ java { } // Generate sources and javadocs jars when building and publishing withSourcesJar() - withJavadocJar() + // withJavadocJar() } tasks.withType(JavaCompile).configureEach { - options.encoding = "UTF-8" + options.encoding = 'UTF-8' } configurations { @@ -38,6 +37,18 @@ configurations { minecraft { mcVersion = '1.12.2' + + // MCP Mappings + mcpMappingChannel = 'stable' + mcpMappingVersion = '39' + + // Set username here, the UUID will be looked up automatically + username = 'Developer' + + // Add any additional tweaker classes here + // extraTweakClasses.add('org.spongepowered.asm.launch.MixinTweaker') + + // Add various JVM arguments here for runtime def args = ["-ea:${project.group}"] if (project.use_coremod.toBoolean()) { args << '-Dfml.coreMods.load=' + coremod_plugin_class_name @@ -49,26 +60,36 @@ minecraft { } extraRunJvmArguments.addAll(args) + // Include and use dependencies' Access Transformer files useDependencyAccessTransformers = true - - injectedTags.put("VERSION", project.version) + + // Add any properties you want to swap out for a dynamic value at build time here + // Any properties here will be added to a class at build time, the name can be configured below + // Example: + // injectedTags.put('VERSION', project.version) + // injectedTags.put('MOD_ID', project.archives_base_name) } -// Generate a my.project.Tags class with the version number as a field +// Generate a group.archives_base_name.Tags class tasks.injectTags.configure { - outputClassName.set("${project.group}.Tags") + // Change Tags class' name here: + outputClassName.set("${project.group}.${project.archives_base_name}.Tags") } repositories { maven { - url = 'https://maven.cleanroommc.com' + name 'CleanroomMC Maven' + url 'https://maven.cleanroommc.com' } - maven { url = "https://repo.spongepowered.org/maven" } - //maven { url "https://maven.mcmoddev.com/" } maven { - url "https://cursemaven.com" + name 'SpongePowered Maven' + url 'https://repo.spongepowered.org/maven' + } + maven { + name 'CurseMaven' + url 'https://cursemaven.com' content { - includeGroup "curse.maven" + includeGroup 'curse.maven' } } mavenLocal() // Must be last for caching to work @@ -76,47 +97,32 @@ repositories { dependencies { if (project.use_assetmover.toBoolean()) { - implementation 'com.cleanroommc:assetmover:2.0' + implementation 'com.cleanroommc:assetmover:2.5' } if (project.use_mixins.toBoolean()) { - implementation 'zone.rong:mixinbooter:7.0' + implementation 'zone.rong:mixinbooter:7.1' } - // Example deobf dependency - // compileOnly rfg.deobf("curse.maven:endercore-231868:2972849:") + // Example of deobfuscating a dependency + // implementation rfg.deobf('curse.maven:had-enough-items-557549:4543375') if (project.use_mixins.toBoolean()) { - api ("org.spongepowered:mixin:0.8.3") {transitive = false} - annotationProcessor('org.ow2.asm:asm-debug-all:5.2') - annotationProcessor('com.google.guava:guava:24.1.1-jre') - annotationProcessor('com.google.code.gson:gson:2.8.6') - annotationProcessor ("org.spongepowered:mixin:0.8.3") {transitive = false} - } - -} - -def mixinConfigRefMap = 'mixins.' + project.archives_base_name + '.refmap.json' -def mixinTmpDir = buildDir.path + File.separator + 'tmp' + File.separator + 'mixins' -def refMap = "${mixinTmpDir}" + File.separator + mixinConfigRefMap -def mixinSrg = "${mixinTmpDir}" + File.separator + "mixins.srg" - -if (project.use_mixins.toBoolean()) { - tasks.named("reobfJar", ReobfuscatedJar).configure { - extraSrgFiles.from(mixinSrg) - } - - tasks.named("compileJava", JavaCompile).configure { - doFirst { - new File(mixinTmpDir).mkdirs() + // Change your mixin refmap name here: + String mixin = modUtils.enableMixins('org.spongepowered:mixin:0.8.3', "mixins.${project.archives_base_name}.refmap.json") + api (mixin) { + transitive = false + } + annotationProcessor 'org.ow2.asm:asm-debug-all:5.2' + annotationProcessor 'com.google.guava:guava:24.1.1-jre' + annotationProcessor 'com.google.code.gson:gson:2.8.6' + annotationProcessor (mixin) { + transitive = false } - options.compilerArgs += [ - "-AreobfSrgFile=${tasks.reobfJar.srg.get().asFile}", - "-AoutSrgFile=${mixinSrg}", - "-AoutRefMapFile=${refMap}", - ] } + } +// Adds Access Transformer files to tasks if (project.use_access_transformer.toBoolean()) { for (File at : sourceSets.getByName("main").resources.files) { if (at.name.toLowerCase().endsWith("_at.cfg")) { @@ -127,12 +133,13 @@ if (project.use_access_transformer.toBoolean()) { } processResources { - // this will ensure that this task is redone when the versions change. + // This will ensure that this task is redone when the versions change inputs.property 'version', project.version inputs.property 'mcversion', project.minecraft.version - // replace stuff in mcmod.info, nothing else + + // Replace various properties in mcmod.info and pack.mcmeta if applicable filesMatching(['mcmod.info', 'pack.mcmeta']) { fcd -> - // replace version and mcversion + // Replace version and mcversion fcd.expand ( 'version': project.version, 'mcversion': project.minecraft.version @@ -140,13 +147,7 @@ processResources { } if (project.use_access_transformer.toBoolean()) { - rename '(.+_at.cfg)', 'META-INF/$1' // Access Transformers - } - - if (project.use_mixins.toBoolean()) { - // Embed mixin refmap - from refMap - dependsOn("compileJava") + rename '(.+_at.cfg)', 'META-INF/$1' // Make sure Access Transformer files are in META-INF folder } } @@ -160,7 +161,7 @@ jar { attribute_map['ForceLoadAsMod'] = project.gradle.startParameter.taskNames[0] == "build" } } - if (project.use_access_transformer.toBoolean()) { + if (project.use_access_transformer.toBoolean()) { attribute_map['FMLAT'] = project.archives_base_name + '_at.cfg' } attributes(attribute_map) @@ -170,31 +171,35 @@ jar { } idea { - module { inheritOutputDirs = true } - project { settings { - runConfigurations { - "1. Run Client"(Gradle) { - taskNames = ["runClient"] + module { + inheritOutputDirs = true + } + project { + settings { + runConfigurations { + "1. Run Client"(Gradle) { + taskNames = ["runClient"] + } + "2. Run Server"(Gradle) { + taskNames = ["runServer"] + } + "3. Run Obfuscated Client"(Gradle) { + taskNames = ["runObfClient"] + } + "4. Run Obfuscated Server"(Gradle) { + taskNames = ["runObfServer"] + } } - "2. Run Server"(Gradle) { - taskNames = ["runServer"] - } - "3. Run Obfuscated Client"(Gradle) { - taskNames = ["runObfClient"] - } - "4. Run Obfuscated Server"(Gradle) { - taskNames = ["runObfServer"] + compiler.javac { + afterEvaluate { + javacAdditionalOptions = "-encoding utf8" + moduleJavacAdditionalOptions = [ + (project.name + ".main"): tasks.compileJava.options.compilerArgs.collect { '"' + it + '"' }.join(' ') + ] + } } } - compiler.javac { - afterEvaluate { - javacAdditionalOptions = "-encoding utf8" - moduleJavacAdditionalOptions = [ - (project.name + ".main"): tasks.compileJava.options.compilerArgs.collect { '"' + it + '"' }.join(' ') - ] - } - } - }} + } } tasks.named("processIdeaSettings").configure { diff --git a/settings.gradle b/settings.gradle index 112969a..4d208c5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,12 +2,12 @@ pluginManagement { repositories { maven { // RetroFuturaGradle - name = "GTNH Maven" - url = uri("http://jenkins.usrv.eu:8081/nexus/content/groups/public/") + name 'GTNH Maven' + url 'http://jenkins.usrv.eu:8081/nexus/content/groups/public/' allowInsecureProtocol = true mavenContent { - includeGroup("com.gtnewhorizons") - includeGroup("com.gtnewhorizons.retrofuturagradle") + includeGroup 'com.gtnewhorizons' + includeGroup 'com.gtnewhorizons.retrofuturagradle' } } gradlePluginPortal() @@ -18,7 +18,9 @@ pluginManagement { plugins { // Automatic toolchain provisioning - id("org.gradle.toolchains.foojay-resolver-convention") version "0.4.0" + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.4.0' } -rootProject.name = archives_base_name +// Due to an IntelliJ bug, this has to be done +// rootProject.name = archives_base_name +rootProject.name = rootProject.projectDir.getName() From 3102d3c033b6c6bb77b799d5686339fff2acb4f4 Mon Sep 17 00:00:00 2001 From: Rongmario Date: Fri, 3 Nov 2023 12:36:00 +0000 Subject: [PATCH 02/10] Include RFG-compatible MinecraftDev fork's link in the README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index ea287ad..ae122d0 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,7 @@ With **coremod and mixin support** that is easy to configure. 4. Open the project folder in IDEA. 5. Right-click in IDEA `build.gradle` of your project, and select `Link Gradle Project`, after completion, hit `Refresh All` in the gradle tab on the right. 6. Run `gradlew runClient` and `gradlew runServer`, or use the auto-imported run configurations in IntelliJ like `1. Run Client`. + +### Mixins: + +- When writing Mixins on IntelliJ, it is advisable to use latest [MinecraftDev Fork for RetroFuturaGradle](https://github.com/eigenraven/MinecraftDev/releases). From 7db468db1eff2bde9bd066414018748564bd5b40 Mon Sep 17 00:00:00 2001 From: Rongmario Date: Tue, 9 Jan 2024 02:28:31 +0000 Subject: [PATCH 03/10] Update RFG to 1.3.27 and update the maven url also --- README.md | 2 +- build.gradle | 2 +- settings.gradle | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ae122d0..1b7c029 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Template workspace for modding Minecraft 1.12.2. Licensed under MIT, it is made for public use. -This template currently utilizies **Gradle 8.1.1** + **[RetroFuturaGradle](https://github.com/GTNewHorizons/RetroFuturaGradle) 1.3.6** + **Forge 14.23.5.2847**. +This template currently utilizies **Gradle 8.1.1** + **[RetroFuturaGradle](https://github.com/GTNewHorizons/RetroFuturaGradle) 1.3.27** + **Forge 14.23.5.2847**. With **coremod and mixin support** that is easy to configure. diff --git a/build.gradle b/build.gradle index 9e82e74..514e540 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { id 'maven-publish' id 'org.jetbrains.gradle.plugin.idea-ext' version '1.1.7' id 'eclipse' - id 'com.gtnewhorizons.retrofuturagradle' version '1.3.16' + id 'com.gtnewhorizons.retrofuturagradle' version '1.3.27' id 'com.matthewprenger.cursegradle' version '1.4.0' } diff --git a/settings.gradle b/settings.gradle index 4d208c5..2c64da6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,8 +3,7 @@ pluginManagement { maven { // RetroFuturaGradle name 'GTNH Maven' - url 'http://jenkins.usrv.eu:8081/nexus/content/groups/public/' - allowInsecureProtocol = true + url 'https://nexus.gtnewhorizons.com/repository/public/' mavenContent { includeGroup 'com.gtnewhorizons' includeGroup 'com.gtnewhorizons.retrofuturagradle' From c82fcee8aaffb3c511bbca78d0b1516f4b3c9b51 Mon Sep 17 00:00:00 2001 From: Rongmario Date: Sun, 7 Apr 2024 18:39:25 +0100 Subject: [PATCH 04/10] Merge overhaul branch to be upstream (#24) * Debug mode for publishing artifacts * Jabel * Generation of mod meta, pack meta and mixin jsons * Fixed runObfServer using 1.7's main class rather than 1.12's - Allows changing of source without it being regenerated in dev * ExampleMod + fixing tag collection * Template expanding for mcmod.info + pack.mcmeta + remove redundant tasks - Now supports arbitrary script blocks to retrieve value `${{ }}` from directly in gradle.properties * Deployment via tasks/actions + changelog support + script folder * Fixed mixin json generating condition * Fix ATs not being applied * Allow mixinbooter & configanytime to be prioritized in obf runs * Remove redundant coremod arg addition as manifest is read at runtime * Allow processResources to work correctly * refactor: make parser changelog as method instead of job * fix: ensure correct header parser for changelog (2to2 and 3to3) * fix: no env available due to Github don't automatic inject env value to GHA * refactor: standardize mod version with SemVer, remove unnecessary changelog block in `build.gradle` * refactor: mixin config template and generator, resource filter * Updated Gradle to 8.7 + RetroFuturaGradle to 1.3.35 * Update MixinBooter to 9.1 + provide wiki link Co-authored-by: Oganesson897 <101081378+Darknight123MC@users.noreply.github.com> Co-authored-by: Li Co-authored-by: Li --- .github/workflows/deploy.yml | 61 ++++ CHANGELOG.md | 6 + README.md | 2 +- build.gradle | 315 ++++++++++++------ gradle.properties | 128 ++++++- gradle/scripts/dependencies.gradle | 62 ++++ gradle/scripts/extra.gradle | 5 + gradle/scripts/helpers.gradle | 96 ++++++ gradle/scripts/publishing.gradle | 107 ++++++ gradle/wrapper/gradle-wrapper.properties | 2 +- src/main/java/com/cleanroommc/README.md | 2 - .../java/com/example/modid/ExampleMod.java | 24 ++ src/main/resources/mcmod.info | 28 +- src/main/resources/mixins.modid.json | 11 + src/main/resources/pack.mcmeta | 11 +- tags.properties | 3 + 16 files changed, 730 insertions(+), 133 deletions(-) create mode 100644 .github/workflows/deploy.yml create mode 100644 CHANGELOG.md create mode 100644 gradle/scripts/dependencies.gradle create mode 100644 gradle/scripts/extra.gradle create mode 100644 gradle/scripts/helpers.gradle create mode 100644 gradle/scripts/publishing.gradle delete mode 100644 src/main/java/com/cleanroommc/README.md create mode 100644 src/main/java/com/example/modid/ExampleMod.java create mode 100644 src/main/resources/mixins.modid.json create mode 100644 tags.properties diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..e037b80 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,61 @@ +# A deployment template that works out of the box +# It supports these objectives: +# - Deploy to Maven (Build Job) [Secrets: MAVEN_USER, MAVEN_PASS] +# - Deploy to CurseForge (Upload Job) [Secrets: CURSEFORGE_TOKEN] +# - Deploy to Modrinth (Upload Job) [Secrets: MODRINTH_TOKEN] + +name: Deploy + +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Grant Execute Permission for gradlew + run: chmod +x gradlew + + - name: Read gradle.properties + uses: BrycensRanch/read-properties-action@v1 + id: properties + with: + file: gradle.properties + all: true + + - name: Setup Java + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'zulu' + cache: gradle + + - name: Publish to Maven + if: steps.properties.outputs.publish_to_maven == 'true' && steps.properties.outputs.publish_to_local_maven == 'true' + uses: gradle/gradle-build-action@v2 + with: + arguments: | + publish + -P${{ steps.properties.outputs.maven_name }}Username=${{ secrets.MAVEN_USER }} + -P${{ steps.properties.outputs.maven_name }}Password=${{ secrets.MAVEN_PASS }} + + - name: Publish to CurseForge + if: steps.properties.outputs.publish_to_curseforge == 'true' + uses: gradle/gradle-build-action@v2 + env: + CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }} + with: + arguments: curseforge + + - name: Publish to Modrinth + if: steps.properties.outputs.publish_to_modrinth == 'true' + uses: gradle/gradle-build-action@v2 + env: + MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} + with: + arguments: modrinth diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a5c40b2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +## [1.0.0] - 2023-09-15 + +### Added +- This is a default template changelog that follows the [KeepAChangelog Convention](https://keepachangelog.com/en/1.1.0/) \ No newline at end of file diff --git a/README.md b/README.md index 1b7c029..08e33f2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Template workspace for modding Minecraft 1.12.2. Licensed under MIT, it is made for public use. -This template currently utilizies **Gradle 8.1.1** + **[RetroFuturaGradle](https://github.com/GTNewHorizons/RetroFuturaGradle) 1.3.27** + **Forge 14.23.5.2847**. +This template currently utilizies **Gradle 8.7** + **[RetroFuturaGradle](https://github.com/GTNewHorizons/RetroFuturaGradle) 1.3.35** + **Forge 14.23.5.2847**. With **coremod and mixin support** that is easy to configure. diff --git a/build.gradle b/build.gradle index 514e540..478c5ac 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,8 @@ +/** + * It is advised that you do not edit anything in the build.gradle; unless you are sure of what you are doing + */ +import com.gtnewhorizons.retrofuturagradle.mcp.InjectTagsTask +import org.jetbrains.changelog.Changelog import org.jetbrains.gradle.ext.Gradle plugins { @@ -5,29 +10,58 @@ plugins { id 'java-library' id 'maven-publish' id 'org.jetbrains.gradle.plugin.idea-ext' version '1.1.7' - id 'eclipse' - id 'com.gtnewhorizons.retrofuturagradle' version '1.3.27' - id 'com.matthewprenger.cursegradle' version '1.4.0' + id 'com.gtnewhorizons.retrofuturagradle' version '1.3.35' + id 'com.matthewprenger.cursegradle' version '1.4.0' apply false + id 'com.modrinth.minotaur' version '2.+' apply false + id 'org.jetbrains.changelog' version '2.2.0' } -version = project.mod_version -group = project.maven_group -archivesBaseName = project.archives_base_name +apply from: 'gradle/scripts/helpers.gradle' + +// Early Assertions +assertProperty 'mod_version' +assertProperty 'root_package' +assertProperty 'mod_id' +assertProperty 'mod_name' + +assertSubProperties 'use_tags', 'tag_class_name' +assertSubProperties 'use_access_transformer', 'access_transformer_locations' +assertSubProperties 'use_mixins', 'mixin_booter_version', 'mixin_refmap' +assertSubProperties 'is_coremod', 'coremod_includes_mod', 'coremod_plugin_class_name' +assertSubProperties 'use_asset_mover', 'asset_mover_version' + +setDefaultProperty 'use_modern_java_syntax', false, false +setDefaultProperty 'generate_sources_jar', true, false +setDefaultProperty 'generate_javadocs_jar', true, false +setDefaultProperty 'mapping_channel', true, 'stable' +setDefaultProperty 'mapping_version', true, '39' +setDefaultProperty 'use_dependency_at_files', true, true +setDefaultProperty 'minecraft_username', true, 'Developer' +setDefaultProperty 'extra_jvm_args', false, '' +setDefaultProperty 'extra_tweak_classes', false, '' +setDefaultProperty 'change_minecraft_sources', false, false + +version = propertyString('mod_version') +group = propertyString('root_package') + +base { + archivesName.set(propertyString('mod_id')) +} + +tasks.decompressDecompiledSources.enabled !propertyBool('change_minecraft_sources') -// Set the toolchain version to decouple the Java we run Gradle with from the Java used to compile and run the mod java { toolchain { - languageVersion.set(JavaLanguageVersion.of(8)) + languageVersion.set(JavaLanguageVersion.of(propertyBool('use_modern_java_syntax') ? 16 : 8)) // Azul covers the most platforms for Java 8 toolchains, crucially including MacOS arm64 - vendor.set(org.gradle.jvm.toolchain.JvmVendorSpec.AZUL) + vendor.set(JvmVendorSpec.AZUL) + } + if (propertyBool('generate_sources_jar')) { + withSourcesJar() + } + if (propertyBool('generate_javadocs_jar')) { + withJavadocJar() } - // Generate sources and javadocs jars when building and publishing - withSourcesJar() - // withJavadocJar() -} - -tasks.withType(JavaCompile).configureEach { - options.encoding = 'UTF-8' } configurations { @@ -36,44 +70,36 @@ configurations { } minecraft { - mcVersion = '1.12.2' + mcVersion.set('1.12.2') - // MCP Mappings - mcpMappingChannel = 'stable' - mcpMappingVersion = '39' - - // Set username here, the UUID will be looked up automatically - username = 'Developer' + mcpMappingChannel.set(propertyString('mapping_channel')) + mcpMappingVersion.set(propertyString('mapping_version')) + + useDependencyAccessTransformers.set(propertyBool('use_dependency_at_files')) + + username.set(propertyString('minecraft_username')) // Add any additional tweaker classes here - // extraTweakClasses.add('org.spongepowered.asm.launch.MixinTweaker') - + extraTweakClasses.addAll(propertyStringList('extra_tweak_classes')) + // Add various JVM arguments here for runtime - def args = ["-ea:${project.group}"] - if (project.use_coremod.toBoolean()) { - args << '-Dfml.coreMods.load=' + coremod_plugin_class_name - } - if (project.use_mixins.toBoolean()) { + def args = ['-ea:' + group] + if (propertyBool('use_mixins')) { args << '-Dmixin.hotSwap=true' args << '-Dmixin.checks.interfaces=true' args << '-Dmixin.debug.export=true' } extraRunJvmArguments.addAll(args) + extraRunJvmArguments.addAll(propertyStringList('extra_jvm_args')) - // Include and use dependencies' Access Transformer files - useDependencyAccessTransformers = true - - // Add any properties you want to swap out for a dynamic value at build time here - // Any properties here will be added to a class at build time, the name can be configured below - // Example: - // injectedTags.put('VERSION', project.version) - // injectedTags.put('MOD_ID', project.archives_base_name) -} - -// Generate a group.archives_base_name.Tags class -tasks.injectTags.configure { - // Change Tags class' name here: - outputClassName.set("${project.group}.${project.archives_base_name}.Tags") + if (propertyBool('use_tags')) { + if (file('tags.properties').exists()) { + Properties props = new Properties().tap { it.load(file('tags.properties').newInputStream()); it } + if (!props.isEmpty()) { + injectedTags.set(props.collectEntries { k, v -> [(k): interpolate(v)] }) + } + } + } } repositories { @@ -81,34 +107,29 @@ repositories { name 'CleanroomMC Maven' url 'https://maven.cleanroommc.com' } - maven { - name 'SpongePowered Maven' - url 'https://repo.spongepowered.org/maven' - } - maven { - name 'CurseMaven' - url 'https://cursemaven.com' - content { - includeGroup 'curse.maven' - } - } - mavenLocal() // Must be last for caching to work } dependencies { - if (project.use_assetmover.toBoolean()) { - implementation 'com.cleanroommc:assetmover:2.5' + if (propertyBool('use_modern_java_syntax')) { + annotationProcessor 'com.github.bsideup.jabel:jabel-javac-plugin:1.0.0' + // Workaround for https://github.com/bsideup/jabel/issues/174 + annotationProcessor 'net.java.dev.jna:jna-platform:5.13.0' + compileOnly ('com.github.bsideup.jabel:jabel-javac-plugin:1.0.0') { + transitive = false + } + // Allow jdk.unsupported classes like sun.misc.Unsafe, workaround for JDK-8206937 and fixes crashes in tests + patchedMinecraft 'me.eigenraven.java8unsupported:java-8-unsupported-shim:1.0.0' + // Include for tests + testAnnotationProcessor 'com.github.bsideup.jabel:jabel-javac-plugin:1.0.0' + testCompileOnly('com.github.bsideup.jabel:jabel-javac-plugin:1.0.0') { + transitive = false // We only care about the 1 annotation class + } } - if (project.use_mixins.toBoolean()) { - implementation 'zone.rong:mixinbooter:7.1' + if (propertyBool('use_asset_mover')) { + implementation "com.cleanroommc:assetmover:${propertyString('asset_mover_version')}" } - - // Example of deobfuscating a dependency - // implementation rfg.deobf('curse.maven:had-enough-items-557549:4543375') - - if (project.use_mixins.toBoolean()) { - // Change your mixin refmap name here: - String mixin = modUtils.enableMixins('org.spongepowered:mixin:0.8.3', "mixins.${project.archives_base_name}.refmap.json") + if (propertyBool('use_mixins')) { + String mixin = modUtils.enableMixins("zone.rong:mixinbooter:${propertyString('mixin_booter_version')}", propertyString('mixin_refmap')) api (mixin) { transitive = false } @@ -119,50 +140,65 @@ dependencies { transitive = false } } - } +apply from: 'gradle/scripts/dependencies.gradle' + // Adds Access Transformer files to tasks -if (project.use_access_transformer.toBoolean()) { - for (File at : sourceSets.getByName("main").resources.files) { - if (at.name.toLowerCase().endsWith("_at.cfg")) { - tasks.deobfuscateMergedJarToSrg.accessTransformerFiles.from(at) - tasks.srgifyBinpatchedJar.accessTransformerFiles.from(at) +if (propertyBool('use_access_transformer')) { + for (def location : propertyStringList('access_transformer_locations')) { + def fileLocation = file("${projectDir}/src/main/resources/${location}") + if (fileLocation.exists()) { + tasks.deobfuscateMergedJarToSrg.accessTransformerFiles.from(fileLocation) + tasks.srgifyBinpatchedJar.accessTransformerFiles.from(fileLocation) + } else { + throw new GradleException("Access Transformer file [$fileLocation] does not exist!") } } } processResources { - // This will ensure that this task is redone when the versions change - inputs.property 'version', project.version - inputs.property 'mcversion', project.minecraft.version - - // Replace various properties in mcmod.info and pack.mcmeta if applicable - filesMatching(['mcmod.info', 'pack.mcmeta']) { fcd -> - // Replace version and mcversion - fcd.expand ( - 'version': project.version, - 'mcversion': project.minecraft.version + + def filterList = ['mcmod.info', 'pack.mcmeta'] + filterList.addAll(propertyStringList('mixin_configs').collect(config -> "mixins.${config}.json" as String)) + + filesMatching(filterList) { fcd -> + fcd.expand( + 'mod_id': propertyString('mod_id'), + 'mod_name': propertyString('mod_name'), + 'mod_version': propertyString('mod_version'), + 'mod_description': propertyString('mod_description'), + 'mod_authors': "[${propertyStringList('mod_authors', ',').join(', ')}]", + 'mod_credits': propertyString('mod_credits'), + 'mod_url': propertyString('mod_url'), + 'mod_update_json': propertyString('mod_update_json'), + 'mod_logo_path': propertyString('mod_logo_path'), + 'mixin_refmap': propertyString('mixin_refmap'), + 'mixin_package': propertyString('mixin_package') ) } - if (project.use_access_transformer.toBoolean()) { - rename '(.+_at.cfg)', 'META-INF/$1' // Make sure Access Transformer files are in META-INF folder + if (propertyBool('use_access_transformer')) { + rename '(.+_at.cfg)', 'META-INF/$1' } + } jar { manifest { def attribute_map = [:] - if (project.use_coremod.toBoolean()) { - attribute_map['FMLCorePlugin'] = project.coremod_plugin_class_name - if (project.include_mod.toBoolean()) { + if (propertyBool('is_coremod')) { + attribute_map['FMLCorePlugin'] = propertyString('coremod_plugin_class_name') + if (propertyBool('coremod_includes_mod')) { attribute_map['FMLCorePluginContainsFMLMod'] = true - attribute_map['ForceLoadAsMod'] = project.gradle.startParameter.taskNames[0] == "build" + def currentTasks = gradle.startParameter.taskNames + if (currentTasks[0] == 'build' || currentTasks[0] == 'prepareObfModsFolder' || currentTasks[0] == 'runObfClient') { + attribute_map['ForceLoadAsMod'] = true + } } } - if (project.use_access_transformer.toBoolean()) { - attribute_map['FMLAT'] = project.archives_base_name + '_at.cfg' + if (propertyBool('use_access_transformer')) { + attribute_map['FMLAT'] = propertyString('access_transformer_locations') } attributes(attribute_map) } @@ -202,6 +238,99 @@ idea { } } -tasks.named("processIdeaSettings").configure { - dependsOn("injectTags") +compileTestJava { + sourceCompatibility = targetCompatibility = 8 } + +test { + javaLauncher.set(javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(8) + }) +} + +String parserChangelog() { + if (!file('CHANGELOG.md').exists()) { + throw new GradleException('publish_with_changelog is true, but CHANGELOG.md does not exist in the workspace!') + } + String parsedChangelog = changelog.renderItem( + changelog.get(propertyString('mod_version')).withHeader(false).withEmptySections(false), + Changelog.OutputType.MARKDOWN) + if (parsedChangelog.isEmpty()) { + throw new GradleException('publish_with_changelog is true, but the changelog for the latest version is empty!') + } + return parsedChangelog +} + +tasks.register('generateMixinJson') { + group 'cleanroom helpers' + def missingConfig = propertyStringList('mixin_configs').findAll(config -> !file("src/main/resources/mixins.${config}.json").exists()) + onlyIf { + if (propertyBool('use_mixins') && propertyBool('generate_mixins_json')) { + return !missingConfig.empty + } + return false + } + doLast { + for (String mixinConfig : missingConfig) { + def file = file("src/main/resources/mixins.${mixinConfig}.json") + file << """{\n\t"package": "",\n\t"required": true,\n\t"refmap": "${mixin_refmap}",\n\t"target": "@env(DEFAULT)",\n\t"minVersion": "0.8.5",\n\t"compatibilityLevel": "JAVA_8",\n\t"mixins": [],\n\t"server": [],\n\t"client": []\n}""" + } + } +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + if (propertyBool('use_modern_java_syntax')) { + if (it.name in ['compileMcLauncherJava', 'compilePatchedMcJava']) { + return + } + sourceCompatibility = 17 + options.release.set(8) + javaCompiler.set(javaToolchains.compilerFor { + languageVersion.set(JavaLanguageVersion.of(16)) + vendor.set(JvmVendorSpec.AZUL) + }) + } +} + +tasks.register('cleanroomAfterSync') { + group 'cleanroom helpers' + dependsOn 'injectTags', 'generateMixinJson' +} + +if (propertyBool('use_modern_java_syntax')) { + tasks.withType(Javadoc).configureEach { + sourceCompatibility = 17 + } +} + +tasks.named('injectTags', InjectTagsTask).configure { + onlyIf { + return propertyBool('use_tags') && !it.getTags().get().isEmpty() + } + it.outputClassName.set(propertyString('tag_class_name')) +} + +tasks.named('prepareObfModsFolder').configure { + finalizedBy 'prioritizeCoremods' +} + +tasks.register('prioritizeCoremods') { + dependsOn 'prepareObfModsFolder' + doLast { + fileTree('run/obfuscated').forEach { + if (it.isFile() && it.name =~ '(mixinbooter|configanytime)(-)([0-9])+\\.+([0-9])+(.jar)') { + it.renameTo(new File(it.parentFile, "!${it.name}")) + } + } + } +} + +idea.project.settings { + taskTriggers { + afterSync 'cleanroomAfterSync' + } +} + +apply from: 'gradle/scripts/publishing.gradle' +apply from: 'gradle/scripts/extra.gradle' diff --git a/gradle.properties b/gradle.properties index c8318b8..2381506 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,22 +1,122 @@ -# Sets default memory used for gradle commands. Can be overridden by user or command line properties. -# This is required to provide enough memory for the Minecraft decompilation process. +# Gradle Properties org.gradle.jvmargs = -Xmx3G +# Source Options +# Use Modern Java(9+) Syntax (Courtesy of Jabel) +use_modern_java_syntax = false + +# Compilation Options +generate_sources_jar = true +generate_javadocs_jar = false + # Mod Information -mod_version = 1.0 -maven_group = com.cleanroommc -archives_base_name = modid +# HIGHLY RECOMMEND complying with SemVer for mod_version: https://semver.org/ +mod_version = 1.0.0 +root_package = com.example +mod_id = modid +mod_name = Mod Name -# If any properties changes below this line, run `gradlew setupDecompWorkspace` and refresh gradle again to ensure everything is working correctly. +# Mod Metadata (Optional) +mod_description = +mod_url = +mod_update_json = +# Delimit authors with commas +mod_authors = +mod_credits = +mod_logo_path = -# Boilerplate Options -use_mixins = false -use_coremod = false -use_assetmover = false +# Mapping Properties +mapping_channel = stable +mapping_version = 39 +use_dependency_at_files = true -# Access Transformer files should be in the root of `resources` folder and with the filename formatted as: `{archives_base_name}_at.cfg` +# Run Configurations +# If multiple arguments/tweak classes are stated, use spaces as the delimiter +minecraft_username = Developer +extra_jvm_args = +extra_tweak_classes = + +# Maven Publishing (Provide secret: MAVEN_USER, MAVEN_PASS) +publish_to_maven = false +# Good for debugging artifacts before uploading to remote maven +# GitHub actions won't run if this is true, test this by running the task `publishToMavenLocal` +publish_to_local_maven = false +maven_name = ${mod_name} +maven_url = + +# Publishing +# release_type can only be: release, beta or alpha (applies to CurseForge / Modrinth) +release_type = release +publish_with_changelog = ${{ it.file('CHANGELOG.md').exists() }} + +# Publishing to CurseForge (Provide secret: CURSEFORGE_TOKEN) +# To configure dependencies, head to publishing.gradle's curseforge block +publish_to_curseforge = false +# CurseForge project ID must be the numerical ID and not the slug +curseforge_project_id = +curseforge_debug = false + +# Publishing to Modrinth (Provide secret: MODRINTH_TOKEN), the token must have the `CREATE_VERSION` and `PROJECT_WRITE` permissions +# To configure dependencies, head to publishing.gradle's modrinth block +publish_to_modrinth = false +modrinth_project_id = +# Allows gradle to publish updated READMEs to the project body (via the modrinthSyncBody task) +modrinth_sync_readme = false +modrinth_debug = false + +# If any properties changes below this line, refresh gradle again to ensure everything is working correctly. + +# Modify Minecraft Sources +# RetroFuturaGradle allows Minecraft sources to be edited, and have the changes reflected upon running it +# Good for previews when coremodding, or generally seeing how behaviours can change with certain code applied/unapplied +# Turning this on allows Minecraft sources to persist and not regenerate +change_minecraft_sources = false + +# Tags +# A RetroFuturaGradle concept akin to Ant ReplaceTokens +# A class is generated at build-time for compilation, to describe properties that have values that could change at build time such as versioning +# Class name is configurable with the `tag_class_name` property +# Tag properties can be stated in the `tags.properties` file, references are allowed +use_tags = true +tag_class_name = ${root_package}.${mod_id}.Tags + +# Access Transformers +# A way to change visibility of Minecraft's classes, methods and fields +# An example access transformer file is given in the path: `src/main/resources/example_at.cfg` +# AT files should be in the root of src/main/resources with the filename formatted as: `mod_id_at.cfg` +# Use the property `access_transformer_locations` to state custom AT files if you aren't using the default `mod_id_at.cfg` location +# If multiple locations are stated, use spaces as the delimiter use_access_transformer = false +access_transformer_locations = ${mod_id}_at.cfg -# Coremod Arguments -include_mod = true -coremod_plugin_class_name = \ No newline at end of file +# Mixins +# Powerful tool to do runtime description changes of classes +# Wiki: https://github.com/SpongePowered/Mixin/wiki + https://github.com/CleanroomMC/MixinBooter/ + https://cleanroommc.com/wiki/forge-mod-development/mixin/preface +# Only use mixins once you understand the underlying structure +use_mixins = false +mixin_booter_version = 9.1 +# A configuration defines a mixin set, and you may have as many mixin sets as you require for your application. +# Each config can only have one and only one package root. +# Generate missing configs, obtain from mixin_configs and generate file base on name convention: "mixins.config_name.json" +# You should change package root once they are generated +generate_mixins_json = true +# Delimit configs with spaces. Should only put configs name instead of full file name +mixin_configs = ${mod_id} +# A refmap is a json that denotes mapping conversions, this json is generated automatically, with the name `mixins.mod_id.refmap.json` +# Use the property `mixin_refmap` if you want it to use a different name, only one name is accepted +mixin_refmap = mixins.${mod_id}.refmap.json + +# Coremods +# The most powerful way to change java classes at runtime, it is however very primitive with little documentation. +# Only make a coremod if you are absolutely sure of what you are doing +# Change the property `coremod_includes_mod` to false if your coremod doesn't have a @Mod annotation +# You MUST state a class name for `coremod_plugin_class_name` if you are making a coremod, the class should implement `IFMLLoadingPlugin` +is_coremod = false +coremod_includes_mod = true +coremod_plugin_class_name = + +# AssetMover +# Convenient way to allow downloading of assets from official vanilla Minecraft servers, CurseForge, or any direct links +# Documentation: https://github.com/CleanroomMC/AssetMover +use_asset_mover = false +asset_mover_version = 2.5 \ No newline at end of file diff --git a/gradle/scripts/dependencies.gradle b/gradle/scripts/dependencies.gradle new file mode 100644 index 0000000..557c777 --- /dev/null +++ b/gradle/scripts/dependencies.gradle @@ -0,0 +1,62 @@ +apply from: 'gradle/scripts/helpers.gradle' + +repositories { + // Other repositories described by default: + // CleanroomMC: https://maven.cleanroommc.com + exclusiveContent { + forRepository { + maven { + name 'CurseMaven' + url 'https://cursemaven.com' + } + } + filter { + includeGroup 'curse.maven' + } + } + exclusiveContent { + forRepository { + maven { + name 'Modrinth' + url 'https://api.modrinth.com/maven' + } + } + filter { + includeGroup 'maven.modrinth' + } + } + mavenLocal() // Must be last for caching to work +} + +dependencies { + // Example - Dependency descriptor: + // 'com.google.code.gson:gson:2.8.6' << group: com.google.code.gson, name:gson, version:2.8.6 + // 'group:name:version:classifier' where classifier is optional + + // Example - Deobfuscating dependencies: + // rfg.deobf('curse.maven:had-enough-items-557549:4543375') + // By wrapping a dependency descriptor in rfg.deobf() method call, the dependency is queued for deobfuscation + // When deobfuscating, RFG respects the mapping_channel + mapping_version stated in gradle.properties + + // Example - CurseMaven dependencies: + // 'curse.maven:had-enough-items-557549:4543375' << had-enough-items = project slug, 557549 = project id, 4543375 = file id + // Full documentation: https://cursemaven.com/ + + // Example - Modrinth dependencies: + // 'maven.modrinth:jei:4.16.1.1000' << jei = project name, 4.16.1.1000 = file version + // Full documentation: https://docs.modrinth.com/docs/tutorials/maven/ + + // Common dependency types (configuration): + // implementation = dependency available at both compile time and runtime + // runtimeOnly = runtime dependency + // compileOnly = compile time dependency + // annotationProcessor = annotation processing dependencies + + // Transitive dependencies: + // (Dependencies that your dependency depends on) + // If you wish to exclude transitive dependencies in the described dependencies + // Use a closure as such: + // implementation ('com.google.code.gson:gson:2.8.6') { + // transitive = false + // } +} \ No newline at end of file diff --git a/gradle/scripts/extra.gradle b/gradle/scripts/extra.gradle new file mode 100644 index 0000000..a44cb8e --- /dev/null +++ b/gradle/scripts/extra.gradle @@ -0,0 +1,5 @@ +// You may write any gradle buildscript component in this file +// This file is automatically applied after build.gradle + dependencies.gradle is ran + +// If you wish to use the default helper methods, uncomment the line below +// apply from: 'gradle/scripts/helpers.gradle' diff --git a/gradle/scripts/helpers.gradle b/gradle/scripts/helpers.gradle new file mode 100644 index 0000000..0b3f2ee --- /dev/null +++ b/gradle/scripts/helpers.gradle @@ -0,0 +1,96 @@ +import groovy.text.SimpleTemplateEngine +import org.codehaus.groovy.runtime.MethodClosure + +ext.propertyString = this.&propertyString as MethodClosure +ext.propertyBool = this.&propertyBool as MethodClosure +ext.propertyStringList = this.&propertyStringList as MethodClosure +ext.interpolate = this.&interpolate as MethodClosure +ext.assertProperty = this.&assertProperty as MethodClosure +ext.assertSubProperties = this.&assertSubProperties as MethodClosure +ext.setDefaultProperty = this.&setDefaultProperty as MethodClosure +ext.assertEnvironmentVariable = this.&assertEnvironmentVariable as MethodClosure + +String propertyString(String key) { + return $property(key).toString() +} + +boolean propertyBool(String key) { + return propertyString(key).toBoolean() +} + +Collection propertyStringList(String key) { + return propertyStringList(key, ' ') +} + +Collection propertyStringList(String key, String delimit) { + return propertyString(key).split(delimit).findAll { !it.isEmpty() } +} + +private Object $property(String key) { + def value = project.findProperty(key) + if (value instanceof String) { + return interpolate(value) + } + return value +} + +String interpolate(String value) { + if (value.startsWith('${{') && value.endsWith('}}')) { + value = value.substring(3, value.length() - 2) + Binding newBinding = new Binding(this.binding.getVariables()) + newBinding.setProperty('it', this) + return new GroovyShell(this.getClass().getClassLoader(), newBinding).evaluate(value) + } + if (value.contains('${')) { + return new SimpleTemplateEngine().createTemplate(value).make(project.properties).toString() + } + return value +} + +void assertProperty(String propertyName) { + def property = property(propertyName) + if (property == null) { + throw new GradleException("Property ${propertyName} is not defined!") + } + if (property.isEmpty()) { + throw new GradleException("Property ${propertyName} is empty!") + } +} + +void assertSubProperties(String propertyName, String... subPropertyNames) { + assertProperty(propertyName) + if (propertyBool(propertyName)) { + for (String subPropertyName : subPropertyNames) { + assertProperty(subPropertyName) + } + } +} + +void setDefaultProperty(String propertyName, boolean warn, defaultValue) { + def property = property(propertyName) + def exists = true + if (property == null) { + exists = false + if (warn) { + project.logger.log(LogLevel.WARN, "Property ${propertyName} is not defined!") + } + } else if (property.isEmpty()) { + exists = false + if (warn) { + project.logger.log(LogLevel.WARN, "Property ${propertyName} is empty!") + } + } + if (!exists) { + project.setProperty(propertyName, defaultValue.toString()) + } +} + +void assertEnvironmentVariable(String propertyName) { + def property = System.getenv(propertyName) + if (property == null) { + throw new GradleException("System Environment Variable $propertyName is not defined!") + } + if (property.isEmpty()) { + throw new GradleException("Property $propertyName is empty!") + } +} diff --git a/gradle/scripts/publishing.gradle b/gradle/scripts/publishing.gradle new file mode 100644 index 0000000..c7897c9 --- /dev/null +++ b/gradle/scripts/publishing.gradle @@ -0,0 +1,107 @@ +apply from: 'gradle/scripts/helpers.gradle' + +setDefaultProperty('publish_to_maven', true, false) +setDefaultProperty('publish_to_curseforge', true, false) +setDefaultProperty('publish_to_modrinth', true, false) + +if (propertyBool('publish_to_maven')) { + assertProperty('maven_name') + assertProperty('maven_url') + publishing { + repositories { + maven { + name propertyString('maven_name').replaceAll("\\s", "") + url propertyString('maven_url') + credentials(PasswordCredentials) + } + } + publications { + mavenJava(MavenPublication) { + from components.java // Publish with standard artifacts + setGroupId(propertyString('root_package'))// Publish with root package as maven group + setArtifactId(propertyString('mod_id')) // Publish artifacts with mod id as the artifact id + + // Custom artifact: + // If you want to publish a different artifact to the one outputted when building normally + // Create a different gradle task (Jar task), in extra.gradle + // Remove the 'from components.java' line above + // Add this line (change the task name): + // artifacts task_name + } + } + } +} + +// Documentation here: https://github.com/matthewprenger/CurseGradle/wiki/ +if (propertyBool('publish_to_curseforge')) { + apply plugin: 'com.matthewprenger.cursegradle' + assertProperty('curseforge_project_id') + assertProperty('release_type') + setDefaultProperty('curseforge_debug', false, false) + curseforge { + apiKey = System.getenv('CURSEFORGE_TOKEN') == null ? "" : System.getenv('CURSEFORGE_TOKEN') + // noinspection GroovyAssignabilityCheck + project { + id = propertyString('curseforge_project_id') + addGameVersion 'Java 8' + addGameVersion 'Forge' + addGameVersion '1.12.2' + releaseType = propertyString('release_type') + if (!propertyBool('publish_with_changelog')) { + changelog = parserChangelog() + changelogType = 'markdown' + } + mainArtifact tasks.reobfJar, { + displayName = "${propertyString('mod_name')} ${propertyString('mod_version')}" + if (propertyBool('use_mixins')) { + relations { + requiredDependency 'mixin-booter' + } + } + if (propertyBool('use_asset_mover')) { + relations { + requiredDependency 'assetmover' + } + } + } + options { + debug = propertyBool('curseforge_debug') + } + } + } +} + +// Documentation here: https://github.com/modrinth/minotaur +if (propertyBool('publish_to_modrinth')) { + apply plugin: 'com.modrinth.minotaur' + assertProperty('modrinth_project_id') + assertProperty('release_type') + setDefaultProperty('modrinth_debug', false, false) + modrinth { + token = System.getenv('MODRINTH_TOKEN') ? "" : System.getenv('MODRINTH_TOKEN') + projectId = propertyString('modrinth_project_id') + versionNumber = propertyString('mod_version') + versionType = propertyString('release_type') + uploadFile = tasks.reobfJar + gameVersions = ['1.12.2'] + loaders = ['forge'] + debugMode = propertyBool('modrinth_debug') + if (propertyBool('use_mixins') || propertyBool('use_asset_mover')) { + dependencies { + if (propertyBool('use_mixins')) { + required.project 'mixinbooter' + } + if (propertyBool('use_asset_mover')) { + required.project 'assetmover' + } + } + } + if (!propertyBool('publish_with_changelog')) { + changelog = parserChangelog() + } + if (propertyBool('modrinth_sync_readme')) { + syncBodyFrom = file('README.md').text + tasks.modrinth.dependsOn(tasks.modrinthSyncBody) + } + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37aef8d..20db9ad 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/java/com/cleanroommc/README.md b/src/main/java/com/cleanroommc/README.md deleted file mode 100644 index de1e64c..0000000 --- a/src/main/java/com/cleanroommc/README.md +++ /dev/null @@ -1,2 +0,0 @@ -- Here lies the root of the `io.github.cleanroommc` package, add another level with your mod id and use that as the root for your mod classes. - diff --git a/src/main/java/com/example/modid/ExampleMod.java b/src/main/java/com/example/modid/ExampleMod.java new file mode 100644 index 0000000..d249994 --- /dev/null +++ b/src/main/java/com/example/modid/ExampleMod.java @@ -0,0 +1,24 @@ +package com.example.modid; + +import com.example.modid.Tags; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@Mod(modid = Tags.MOD_ID, name = Tags.MOD_NAME, version = Tags.VERSION) +public class ExampleMod { + + public static final Logger LOGGER = LogManager.getLogger(Tags.MOD_NAME); + + /** + * + * Take a look at how many FMLStateEvents you can listen to via the @Mod.EventHandler annotation here + * + */ + @Mod.EventHandler + public void preInit(FMLPreInitializationEvent event) { + LOGGER.info("Hello From {}!", Tags.MOD_NAME); + } + +} diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info index 7d30553..402f70a 100644 --- a/src/main/resources/mcmod.info +++ b/src/main/resources/mcmod.info @@ -1,16 +1,12 @@ -[ -{ - "modid": "examplemod", - "name": "Example Mod", - "description": "Example placeholder mod.", - "version": "${version}", - "mcversion": "${mcversion}", - "url": "", - "updateUrl": "", - "authorList": ["CleanroomMC"], - "credits": "Authors of this project", - "logoFile": "", - "screenshots": [], - "dependencies": [] -} -] +[{ + "modid": "${mod_id}", + "name": "${mod_name}", + "version": "${mod_version}", + "mcversion": "1.12.2", + "description": "${mod_description}", + "authorList": ${mod_authors}, + "credits": "${mod_credits}", + "url": "${mod_url}", + "updateJSON": "${mod_update_json}", + "logoFile": "${mod_logo_path}" +}] \ No newline at end of file diff --git a/src/main/resources/mixins.modid.json b/src/main/resources/mixins.modid.json new file mode 100644 index 0000000..ffbbdee --- /dev/null +++ b/src/main/resources/mixins.modid.json @@ -0,0 +1,11 @@ +{ + "package": "", + "required": true, + "refmap": "${mixin_refmap}", + "target": "@env(DEFAULT)", + "minVersion": "0.8.5", + "compatibilityLevel": "JAVA_8", + "mixins": [], + "server": [], + "client": [] +} \ No newline at end of file diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta index 4018267..daa17a8 100644 --- a/src/main/resources/pack.mcmeta +++ b/src/main/resources/pack.mcmeta @@ -1,7 +1,6 @@ { - "pack": { - "description": "examplemod resources", - "pack_format": 3, - "_comment": "A pack_format of 3 should be used starting with Minecraft 1.11. All resources, including language files, should be lowercase (eg: en_us.lang). A pack_format of 2 will load your mod resources with LegacyV2Adapter, which requires language files to have uppercase letters (eg: en_US.lang)." - } -} + "pack": { + "description": "${mod_name} Resources", + "pack_format": 3 + } +} \ No newline at end of file diff --git a/tags.properties b/tags.properties new file mode 100644 index 0000000..d795ef9 --- /dev/null +++ b/tags.properties @@ -0,0 +1,3 @@ +VERSION = ${mod_version} +MOD_ID = ${mod_id} +MOD_NAME = ${mod_name} \ No newline at end of file From 83757cd9bc3966abd1cf195a126e7951b3f3b9d1 Mon Sep 17 00:00:00 2001 From: Riley Brown <30506033+X-Niter@users.noreply.github.com> Date: Mon, 8 Apr 2024 08:20:17 -0500 Subject: [PATCH 05/10] Update Wiki link for issue #25 (#26) --- src/main/java/com/example/modid/ExampleMod.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/modid/ExampleMod.java b/src/main/java/com/example/modid/ExampleMod.java index d249994..4dd9e46 100644 --- a/src/main/java/com/example/modid/ExampleMod.java +++ b/src/main/java/com/example/modid/ExampleMod.java @@ -12,7 +12,7 @@ public class ExampleMod { public static final Logger LOGGER = LogManager.getLogger(Tags.MOD_NAME); /** - * + * * Take a look at how many FMLStateEvents you can listen to via the @Mod.EventHandler annotation here * */ From 4cf81e171cb99a4a07d966d366887069dc8c2a83 Mon Sep 17 00:00:00 2001 From: Rongmario Date: Sat, 27 Apr 2024 00:25:58 +0100 Subject: [PATCH 06/10] Enable testing --- build.gradle | 4 ++++ gradle.properties | 3 +++ 2 files changed, 7 insertions(+) diff --git a/build.gradle b/build.gradle index 478c5ac..080c1d5 100644 --- a/build.gradle +++ b/build.gradle @@ -140,6 +140,10 @@ dependencies { transitive = false } } + if (propertyBool('enable_junit_testing')) { + testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + } } apply from: 'gradle/scripts/dependencies.gradle' diff --git a/gradle.properties b/gradle.properties index 2381506..1d72361 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,6 +9,9 @@ use_modern_java_syntax = false generate_sources_jar = true generate_javadocs_jar = false +# Testing +enable_junit_testing = true + # Mod Information # HIGHLY RECOMMEND complying with SemVer for mod_version: https://semver.org/ mod_version = 1.0.0 From a91692ded79813815bad08fec7c56c1c789920e4 Mon Sep 17 00:00:00 2001 From: Rongmario Date: Sat, 27 Apr 2024 00:48:57 +0100 Subject: [PATCH 07/10] useJUnitPlatform + show_testing_output option --- build.gradle | 6 ++++++ gradle.properties | 1 + 2 files changed, 7 insertions(+) diff --git a/build.gradle b/build.gradle index 080c1d5..9ba85ba 100644 --- a/build.gradle +++ b/build.gradle @@ -247,9 +247,15 @@ compileTestJava { } test { + useJUnitPlatform() javaLauncher.set(javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(8) }) + if (propertyBool('show_testing_output')) { + testLogging { + showStandardStreams = true + } + } } String parserChangelog() { diff --git a/gradle.properties b/gradle.properties index 1d72361..861338a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,6 +11,7 @@ generate_javadocs_jar = false # Testing enable_junit_testing = true +show_testing_output = false # Mod Information # HIGHLY RECOMMEND complying with SemVer for mod_version: https://semver.org/ From f1c52e437194891aafeb63e225e861d0be22a6fb Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 15 Jul 2024 11:22:53 +0500 Subject: [PATCH 08/10] 1.0.0 --- gradle.properties | 16 +- .../java/com/example/modid/ExampleMod.java | 24 -- .../knockdowns/client/ClientProxy.java | 15 ++ .../client/communication/CalloutManager.java | 34 +++ .../KnockedNotificationManager.java | 28 ++ .../event/KnockdownsClientEventListener.java | 71 ++++++ .../client/event/KnockdownsKeyListener.java | 56 ++++ .../client/gui/CommunicationGui.java | 175 +++++++++++++ .../client/gui/KnockdownsBaseGui.java | 25 ++ .../client/gui/KnockedNotificationGui.java | 64 +++++ .../knockdowns/client/gui/ReviveGui.java | 47 ++++ .../KnockdownsClientPacketHandler.java | 145 +++++++++++ .../knockdowns/client/util/Callout.java | 31 +++ .../client/util/DirectionalCallSound.java | 42 +++ .../client/util/KnockedPlayerData.java | 27 ++ .../knockdowns/common/IClientProxy.java | 13 + .../common/KnockdownsCommonEventListener.java | 239 ++++++++++++++++++ .../common/KnockdownsFMLLoadingPlugin.java | 43 ++++ .../knockdowns/common/KnockdownsMod.java | 21 ++ .../knockdowns/common/KnockdownsUtils.java | 43 ++++ .../knockdowns/common/ReviverTracker.java | 32 +++ .../common/communication/CalloutType.java | 35 +++ .../common/data/IKnockdownsPlayerData.java | 21 ++ .../common/data/KnockdownsCapability.java | 65 +++++ .../common/data/KnockdownsPlayerData.java | 59 +++++ .../common/mixins/EntityLivingBaseMixin.java | 31 +++ .../common/mixins/EntityPlayerMixin.java | 22 ++ .../common/network/KnockdownsNetwork.java | 71 ++++++ .../KnockdownsServerPacketHandler.java | 51 ++++ .../packets/c2s/CancelReviveC2SPacket.java | 17 ++ .../packets/c2s/PlayerCalloutC2SPacket.java | 25 ++ .../packets/s2c/PlayerCalloutS2CPacket.java | 37 +++ .../s2c/PlayerKnockedDownS2CPacket.java | 36 +++ .../s2c/SynchronizePlayerDataS2CPacket.java | 85 +++++++ .../s2c/SynchronizeReviversS2CPacket.java | 56 ++++ .../registry/KnockdownsSoundEvents.java | 17 ++ .../assets/knockdowns/lang/ru_ru.lang | 64 +++++ ...xins.modid.json => mixins.knockdowns.json} | 6 +- 38 files changed, 1854 insertions(+), 35 deletions(-) delete mode 100644 src/main/java/com/example/modid/ExampleMod.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/client/ClientProxy.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/client/communication/CalloutManager.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/client/communication/KnockedNotificationManager.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/client/event/KnockdownsClientEventListener.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/client/event/KnockdownsKeyListener.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/client/gui/CommunicationGui.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/client/gui/KnockdownsBaseGui.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/client/gui/KnockedNotificationGui.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/client/gui/ReviveGui.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/client/network/KnockdownsClientPacketHandler.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/client/util/Callout.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/client/util/DirectionalCallSound.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/client/util/KnockedPlayerData.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/common/IClientProxy.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsCommonEventListener.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsFMLLoadingPlugin.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsMod.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsUtils.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/common/ReviverTracker.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/common/communication/CalloutType.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/common/data/IKnockdownsPlayerData.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/common/data/KnockdownsCapability.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/common/data/KnockdownsPlayerData.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/common/mixins/EntityLivingBaseMixin.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/common/mixins/EntityPlayerMixin.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/common/network/KnockdownsNetwork.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/common/network/KnockdownsServerPacketHandler.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/common/network/packets/c2s/CancelReviveC2SPacket.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/common/network/packets/c2s/PlayerCalloutC2SPacket.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/common/network/packets/s2c/PlayerCalloutS2CPacket.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/common/network/packets/s2c/PlayerKnockedDownS2CPacket.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/common/network/packets/s2c/SynchronizePlayerDataS2CPacket.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/common/network/packets/s2c/SynchronizeReviversS2CPacket.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/common/registry/KnockdownsSoundEvents.java create mode 100644 src/main/resources/assets/knockdowns/lang/ru_ru.lang rename src/main/resources/{mixins.modid.json => mixins.knockdowns.json} (58%) diff --git a/gradle.properties b/gradle.properties index 861338a..3a3daea 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,16 +16,16 @@ show_testing_output = false # Mod Information # HIGHLY RECOMMEND complying with SemVer for mod_version: https://semver.org/ mod_version = 1.0.0 -root_package = com.example -mod_id = modid -mod_name = Mod Name +root_package = ru.octol1ttle +mod_id = knockdowns +mod_name = Knockdowns (Legacy) # Mod Metadata (Optional) mod_description = mod_url = mod_update_json = # Delimit authors with commas -mod_authors = +mod_authors = Octol1ttle mod_credits = mod_logo_path = @@ -97,7 +97,7 @@ access_transformer_locations = ${mod_id}_at.cfg # Powerful tool to do runtime description changes of classes # Wiki: https://github.com/SpongePowered/Mixin/wiki + https://github.com/CleanroomMC/MixinBooter/ + https://cleanroommc.com/wiki/forge-mod-development/mixin/preface # Only use mixins once you understand the underlying structure -use_mixins = false +use_mixins = true mixin_booter_version = 9.1 # A configuration defines a mixin set, and you may have as many mixin sets as you require for your application. # Each config can only have one and only one package root. @@ -115,12 +115,12 @@ mixin_refmap = mixins.${mod_id}.refmap.json # Only make a coremod if you are absolutely sure of what you are doing # Change the property `coremod_includes_mod` to false if your coremod doesn't have a @Mod annotation # You MUST state a class name for `coremod_plugin_class_name` if you are making a coremod, the class should implement `IFMLLoadingPlugin` -is_coremod = false +is_coremod = true coremod_includes_mod = true -coremod_plugin_class_name = +coremod_plugin_class_name = ru.octol1ttle.knockdowns.common.KnockdownsFMLLoadingPlugin # AssetMover # Convenient way to allow downloading of assets from official vanilla Minecraft servers, CurseForge, or any direct links # Documentation: https://github.com/CleanroomMC/AssetMover use_asset_mover = false -asset_mover_version = 2.5 \ No newline at end of file +asset_mover_version = 2.5 diff --git a/src/main/java/com/example/modid/ExampleMod.java b/src/main/java/com/example/modid/ExampleMod.java deleted file mode 100644 index 4dd9e46..0000000 --- a/src/main/java/com/example/modid/ExampleMod.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.example.modid; - -import com.example.modid.Tags; -import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -@Mod(modid = Tags.MOD_ID, name = Tags.MOD_NAME, version = Tags.VERSION) -public class ExampleMod { - - public static final Logger LOGGER = LogManager.getLogger(Tags.MOD_NAME); - - /** - * - * Take a look at how many FMLStateEvents you can listen to via the @Mod.EventHandler annotation here - * - */ - @Mod.EventHandler - public void preInit(FMLPreInitializationEvent event) { - LOGGER.info("Hello From {}!", Tags.MOD_NAME); - } - -} diff --git a/src/main/java/ru/octol1ttle/knockdowns/client/ClientProxy.java b/src/main/java/ru/octol1ttle/knockdowns/client/ClientProxy.java new file mode 100644 index 0000000..1a0fbf1 --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/client/ClientProxy.java @@ -0,0 +1,15 @@ +package ru.octol1ttle.knockdowns.client; + +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; +import ru.octol1ttle.knockdowns.client.event.KnockdownsKeyListener; +import ru.octol1ttle.knockdowns.common.IClientProxy; + +@SideOnly(Side.CLIENT) +public class ClientProxy implements IClientProxy { + @Override + public void onFMLInit(FMLInitializationEvent event) { + KnockdownsKeyListener.registerKeyBindings(); + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/client/communication/CalloutManager.java b/src/main/java/ru/octol1ttle/knockdowns/client/communication/CalloutManager.java new file mode 100644 index 0000000..a6ecefe --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/client/communication/CalloutManager.java @@ -0,0 +1,34 @@ +package ru.octol1ttle.knockdowns.client.communication; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import net.minecraft.client.Minecraft; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; +import ru.octol1ttle.knockdowns.client.util.Callout; +import ru.octol1ttle.knockdowns.client.util.DirectionalCallSound; +import ru.octol1ttle.knockdowns.common.network.packets.s2c.PlayerCalloutS2CPacket; +import ru.octol1ttle.knockdowns.common.registry.KnockdownsSoundEvents; + +@SideOnly(Side.CLIENT) +public class CalloutManager { + private static final Minecraft client = Minecraft.getMinecraft(); + private static final Map callouts = new HashMap<>(); + + public static Set> getCallouts() { + return callouts.entrySet(); + } + + public static boolean addOrUpdateCallout(PlayerCalloutS2CPacket message) { + return callouts.put(message.playerId, new Callout(message.position, message.type, client.world.getTotalWorldTime())) == null; + } + + public static void playCalloutSound(PlayerCalloutS2CPacket message) { + client.getSoundHandler().playSound(new DirectionalCallSound(KnockdownsSoundEvents.CALLOUT, client.world.getEntityByID(message.playerId), message.position)); + } + + public static void clearCallouts() { + callouts.clear(); + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/client/communication/KnockedNotificationManager.java b/src/main/java/ru/octol1ttle/knockdowns/client/communication/KnockedNotificationManager.java new file mode 100644 index 0000000..923e768 --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/client/communication/KnockedNotificationManager.java @@ -0,0 +1,28 @@ +package ru.octol1ttle.knockdowns.client.communication; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import net.minecraft.client.Minecraft; +import net.minecraft.util.math.Vec3d; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; +import ru.octol1ttle.knockdowns.client.util.KnockedPlayerData; + +@SideOnly(Side.CLIENT) +public class KnockedNotificationManager { + private static final Minecraft client = Minecraft.getMinecraft(); + private static final List knockedDatas = new ArrayList<>(); + + public static void addKnockedNotification(int playerId, Vec3d position) { + knockedDatas.add(new KnockedPlayerData(playerId, position, client.world.getTotalWorldTime())); + } + + public static Collection getKnockedPlayerDatas() { + return knockedDatas; + } + + public static void clearDatas() { + knockedDatas.clear(); + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/client/event/KnockdownsClientEventListener.java b/src/main/java/ru/octol1ttle/knockdowns/client/event/KnockdownsClientEventListener.java new file mode 100644 index 0000000..ebf5b56 --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/client/event/KnockdownsClientEventListener.java @@ -0,0 +1,71 @@ +package ru.octol1ttle.knockdowns.client.event; + +import java.util.List; +import net.minecraft.client.Minecraft; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraftforge.client.event.RenderGameOverlayEvent; +import net.minecraftforge.client.event.RenderWorldLastEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.InputEvent; +import net.minecraftforge.fml.common.gameevent.TickEvent; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; +import ru.octol1ttle.knockdowns.Tags; +import ru.octol1ttle.knockdowns.client.communication.CalloutManager; +import ru.octol1ttle.knockdowns.client.communication.KnockedNotificationManager; +import ru.octol1ttle.knockdowns.client.gui.CommunicationGui; +import ru.octol1ttle.knockdowns.client.gui.KnockedNotificationGui; +import ru.octol1ttle.knockdowns.client.gui.ReviveGui; +import ru.octol1ttle.knockdowns.common.ReviverTracker; +import ru.octol1ttle.knockdowns.common.network.KnockdownsNetwork; +import ru.octol1ttle.knockdowns.common.network.packets.c2s.CancelReviveC2SPacket; + +@SideOnly(Side.CLIENT) +@Mod.EventBusSubscriber(value = Side.CLIENT, modid = Tags.MOD_ID) +public class KnockdownsClientEventListener { + private static final Minecraft client = Minecraft.getMinecraft(); + private static final CommunicationGui communicationGui = new CommunicationGui(); + private static final KnockedNotificationGui notificationGui = new KnockedNotificationGui(); + private static final ReviveGui reviveGui = new ReviveGui(); + + @SubscribeEvent + public static void onTick(TickEvent.ClientTickEvent event) { + if (event.phase == TickEvent.Phase.START) { + return; + } + if (client.world == null) { + CalloutManager.clearCallouts(); + KnockedNotificationManager.clearDatas(); + return; + } + CalloutManager.getCallouts().removeIf(callout -> client.world.getTotalWorldTime() - callout.getValue().getReceiveTime() > 60); + KnockedNotificationManager.getKnockedPlayerDatas().removeIf(notification -> client.world.getTotalWorldTime() - notification.getReceiveTime() > 100); + } + + @SubscribeEvent + public static void onPlayerTick(TickEvent.PlayerTickEvent event) { + List revivers = ReviverTracker.getRevivers(event.player); + if (revivers.contains(client.player) && !event.player.equals(client.pointedEntity)) { + KnockdownsNetwork.sendToServer(new CancelReviveC2SPacket()); + revivers.remove(client.player); + } + } + + @SubscribeEvent + public static void onRenderWorldLast(RenderWorldLastEvent event) { + communicationGui.renderCallouts(event.getPartialTicks()); + notificationGui.renderNotifications(event.getPartialTicks()); + } + + @SubscribeEvent + public static void onRenderGameOverlay(RenderGameOverlayEvent.Chat event) { + communicationGui.render(event.getPartialTicks(), event.getResolution()); + reviveGui.render(event.getPartialTicks(), event.getResolution()); + } + + @SubscribeEvent + public static void onKeyInput(InputEvent.KeyInputEvent event) { + KnockdownsKeyListener.tickKeys(); + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/client/event/KnockdownsKeyListener.java b/src/main/java/ru/octol1ttle/knockdowns/client/event/KnockdownsKeyListener.java new file mode 100644 index 0000000..e2c1057 --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/client/event/KnockdownsKeyListener.java @@ -0,0 +1,56 @@ +package ru.octol1ttle.knockdowns.client.event; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; +import net.minecraft.client.Minecraft; +import net.minecraft.client.settings.KeyBinding; +import net.minecraftforge.fml.client.registry.ClientRegistry; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; +import org.lwjgl.input.Keyboard; +import ru.octol1ttle.knockdowns.Tags; +import ru.octol1ttle.knockdowns.common.communication.CalloutType; +import ru.octol1ttle.knockdowns.common.data.IKnockdownsPlayerData; +import ru.octol1ttle.knockdowns.common.network.KnockdownsNetwork; +import ru.octol1ttle.knockdowns.common.network.packets.c2s.PlayerCalloutC2SPacket; + +@SideOnly(Side.CLIENT) +@Mod.EventBusSubscriber(value = Side.CLIENT, modid = Tags.MOD_ID) +public class KnockdownsKeyListener { + private static final Minecraft client = Minecraft.getMinecraft(); + public static final Map> calloutBindings = new HashMap<>(); + + public static void registerKeyBindings() { + calloutBindings.put( + new KeyBinding("knockdowns.key.callout.danger", Keyboard.KEY_LEFT, "knockdowns.key.category"), + () -> CalloutType.DANGER + ); + calloutBindings.put( + new KeyBinding("knockdowns.key.callout.booyah", Keyboard.KEY_DOWN, "knockdowns.key.category"), + () -> CalloutType.BOOYAH + ); + calloutBindings.put( + new KeyBinding("knockdowns.key.callout.this_way_help", Keyboard.KEY_UP, "knockdowns.key.category"), + () -> IKnockdownsPlayerData.get(client.player).isKnockedDown() ? CalloutType.HELP : CalloutType.THIS_WAY + ); + calloutBindings.put( + new KeyBinding("knockdowns.key.callout.ouch", Keyboard.KEY_RIGHT, "knockdowns.key.category"), + () -> CalloutType.OUCH + ); + + for (KeyBinding binding : calloutBindings.keySet()) { + ClientRegistry.registerKeyBinding(binding); + } + } + + public static void tickKeys() { + for (KeyBinding binding : calloutBindings.keySet()) { + if (binding.isPressed()) { + KnockdownsNetwork.sendToServer(new PlayerCalloutC2SPacket(calloutBindings.get(binding).get())); + break; + } + } + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/client/gui/CommunicationGui.java b/src/main/java/ru/octol1ttle/knockdowns/client/gui/CommunicationGui.java new file mode 100644 index 0000000..ac1920c --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/client/gui/CommunicationGui.java @@ -0,0 +1,175 @@ +package ru.octol1ttle.knockdowns.client.gui; + +import java.util.Map; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.resources.I18n; +import net.minecraft.client.settings.KeyBinding; +import net.minecraft.entity.Entity; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.Vec3d; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; +import org.lwjgl.input.Keyboard; +import org.lwjgl.opengl.GL11; +import ru.octol1ttle.knockdowns.Tags; +import ru.octol1ttle.knockdowns.client.communication.CalloutManager; +import ru.octol1ttle.knockdowns.client.event.KnockdownsKeyListener; +import ru.octol1ttle.knockdowns.client.util.Callout; + +@SideOnly(Side.CLIENT) +public class CommunicationGui extends KnockdownsBaseGui { + private static final Minecraft client = Minecraft.getMinecraft(); + private static final ResourceLocation LEFT_ARROW = new ResourceLocation(Tags.MOD_ID, "textures/gui/left_arrow.png"); + private static final ResourceLocation DOWN_ARROW = new ResourceLocation(Tags.MOD_ID, "textures/gui/down_arrow.png"); + private static final ResourceLocation UP_ARROW = new ResourceLocation(Tags.MOD_ID, "textures/gui/up_arrow.png"); + private static final ResourceLocation RIGHT_ARROW = new ResourceLocation(Tags.MOD_ID, "textures/gui/right_arrow.png"); + private static final int SCREEN_EDGE_MARGIN = 5; + private static final int SEPARATOR_MARGIN = 2; + private static final int KEY_SIZE = 17; + private float totalPartialTicks; + + @Override + public void render(float partialTicks, ScaledResolution resolution) { + FontRenderer font = client.fontRenderer; + + int x = SCREEN_EDGE_MARGIN; + int y = resolution.getScaledHeight() - SCREEN_EDGE_MARGIN - font.FONT_HEIGHT; + + KeyBinding[] sortedBindings = new KeyBinding[4]; + for (KeyBinding binding : KnockdownsKeyListener.calloutBindings.keySet()) + { + switch (binding.getKeyCode()) { + case Keyboard.KEY_LEFT: + sortedBindings[0] = binding; + break; + case Keyboard.KEY_DOWN: + sortedBindings[1] = binding; + break; + case Keyboard.KEY_UP: + sortedBindings[2] = binding; + break; + case Keyboard.KEY_RIGHT: + sortedBindings[3] = binding; + break; + } + } + + KeyBinding leftCallout = sortedBindings[0]; + if (leftCallout != null) { + String text = I18n.format(KnockdownsKeyListener.calloutBindings.get(leftCallout).get().getTextKey()); + font.drawStringWithShadow( + text, + x, + y - 12, + 0xFFFFFF + ); + + x += font.getStringWidth(text) + SEPARATOR_MARGIN; + client.getTextureManager().bindTexture(LEFT_ARROW); + this.drawTexture( + x, + y - KEY_SIZE, + KEY_SIZE, + KEY_SIZE + ); + x += KEY_SIZE + SEPARATOR_MARGIN; + } + + KeyBinding downCallout = sortedBindings[1]; + if (downCallout != null) { + String text = I18n.format(KnockdownsKeyListener.calloutBindings.get(downCallout).get().getTextKey()); + font.drawStringWithShadow( + text, + x + KEY_SIZE * 0.5f - font.getStringWidth(text) * 0.5f, + y + SEPARATOR_MARGIN, + 0xFFFFFF + ); + client.getTextureManager().bindTexture(DOWN_ARROW); + this.drawTexture( + x, + y - KEY_SIZE, + KEY_SIZE, + KEY_SIZE + ); + } + + KeyBinding upCallout = sortedBindings[2]; + if (upCallout != null) { + String text = I18n.format(KnockdownsKeyListener.calloutBindings.get(upCallout).get().getTextKey()); + font.drawStringWithShadow( + text, + x + KEY_SIZE * 0.5f - font.getStringWidth(text) * 0.5f, + y - KEY_SIZE * 2 - 12, + 0xFFFFFF + ); + + client.getTextureManager().bindTexture(UP_ARROW); + this.drawTexture( + x, + y - KEY_SIZE * 2 - 2, + KEY_SIZE, + KEY_SIZE + ); + } + + KeyBinding rightCallout = sortedBindings[3]; + if (rightCallout != null) { + x += KEY_SIZE + SEPARATOR_MARGIN; + String text = I18n.format(KnockdownsKeyListener.calloutBindings.get(rightCallout).get().getTextKey()); + font.drawStringWithShadow( + text, + x + KEY_SIZE + SEPARATOR_MARGIN, + y - 12, + 0xFFFFFF + ); + + client.getTextureManager().bindTexture(RIGHT_ARROW); + this.drawTexture( + x, + y - KEY_SIZE, + KEY_SIZE, + KEY_SIZE + ); + } + } + + public void renderCallouts(float partialTicks) { + totalPartialTicks += partialTicks; + for (Map.Entry calloutEntry : CalloutManager.getCallouts()) { + Entity entity = client.world.getEntityByID(calloutEntry.getKey()); + renderCallout( + I18n.format(calloutEntry.getValue().getType().getTextKey()), + entity != null ? entity.getPositionEyes(partialTicks).add(0, 1, 0) : calloutEntry.getValue().getPosition(), + client.getRenderManager().playerViewX, + client.getRenderManager().playerViewY, + client.getRenderManager().options.thirdPersonView == 2 + ); + } + } + + private void renderCallout(String text, Vec3d position, float pitch, float yaw, boolean isThirdPersonFrontal) { + FontRenderer font = client.fontRenderer; + + Vec3d deltaPos = position.subtract(client.getRenderManager().viewerPosX, client.getRenderManager().viewerPosY, client.getRenderManager().viewerPosZ); + + GlStateManager.pushMatrix(); + GlStateManager.translate(deltaPos.x, deltaPos.y, deltaPos.z); + GlStateManager.rotate(-yaw, 0.0F, 1.0F, 0.0F); + GlStateManager.rotate((float)(isThirdPersonFrontal ? -1 : 1) * pitch, 1.0F, 0.0F, 0.0F); + float scale = (float) Math.max(deltaPos.length() / 8.0f, 1.0f) * (0.75f + MathHelper.abs((float) (MathHelper.sin(totalPartialTicks / 10F) / Math.PI))); + GlStateManager.scale(-0.025F * scale, -0.025F * scale, 0.025F * scale); + GlStateManager.color(1f, 1f, 1f, 1f); + GlStateManager.disableCull(); + GlStateManager.depthFunc(GL11.GL_ALWAYS); + + font.drawStringWithShadow(text, -font.getStringWidth(text) * 0.5f, -font.FONT_HEIGHT * 0.5f, 0xFFFFFF); + + GlStateManager.depthFunc(GL11.GL_LEQUAL); + GlStateManager.enableCull(); + GlStateManager.popMatrix(); + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/client/gui/KnockdownsBaseGui.java b/src/main/java/ru/octol1ttle/knockdowns/client/gui/KnockdownsBaseGui.java new file mode 100644 index 0000000..d2e635e --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/client/gui/KnockdownsBaseGui.java @@ -0,0 +1,25 @@ +package ru.octol1ttle.knockdowns.client.gui; + +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.renderer.BufferBuilder; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.vertex.DefaultVertexFormats; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; + +@SideOnly(Side.CLIENT) +public abstract class KnockdownsBaseGui extends Gui { + public abstract void render(float partialTicks, ScaledResolution resolution); + + protected void drawTexture(int x, int y, int width, int height) { + Tessellator tessellator = Tessellator.getInstance(); + BufferBuilder bufferbuilder = tessellator.getBuffer(); + bufferbuilder.begin(7, DefaultVertexFormats.POSITION_TEX); + bufferbuilder.pos(x, y, this.zLevel).tex(0, 0).endVertex(); + bufferbuilder.pos(x, y + height, this.zLevel).tex(0, 1).endVertex(); + bufferbuilder.pos(x + width, y + height, this.zLevel).tex(1, 1).endVertex(); + bufferbuilder.pos(x + width, y, this.zLevel).tex(1, 0).endVertex(); + tessellator.draw(); + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/client/gui/KnockedNotificationGui.java b/src/main/java/ru/octol1ttle/knockdowns/client/gui/KnockedNotificationGui.java new file mode 100644 index 0000000..c8055fb --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/client/gui/KnockedNotificationGui.java @@ -0,0 +1,64 @@ +package ru.octol1ttle.knockdowns.client.gui; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.entity.Entity; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.Vec3d; +import org.lwjgl.opengl.GL11; +import ru.octol1ttle.knockdowns.Tags; +import ru.octol1ttle.knockdowns.client.communication.KnockedNotificationManager; +import ru.octol1ttle.knockdowns.client.util.KnockedPlayerData; + +public class KnockedNotificationGui extends KnockdownsBaseGui { + private static final Minecraft client = Minecraft.getMinecraft(); + private static final int KNOCKED_ICON_SIZE = 18; + private static final ResourceLocation KNOCKED_ICON = new ResourceLocation(Tags.MOD_ID, "textures/gui/knocked_icon.png"); + private float totalPartialTicks; + + @Deprecated + @Override + public void render(float partialTicks, ScaledResolution resolution) { + } + + public void renderNotifications(float partialTicks) { + totalPartialTicks += partialTicks; + for (KnockedPlayerData data : KnockedNotificationManager.getKnockedPlayerDatas()) { + Entity entity = client.world.getEntityByID(data.getPlayerId()); + renderKnockedNotification( + entity != null ? entity.getPositionEyes(partialTicks).add(0, 1, 0) : data.getPosition(), + client.getRenderManager().playerViewX, + client.getRenderManager().playerViewY, + client.getRenderManager().options.thirdPersonView == 2 + ); + } + } + + private void renderKnockedNotification(Vec3d position, float pitch, float yaw, boolean isThirdPersonFrontal) { + Vec3d deltaPos = position.subtract(client.getRenderManager().viewerPosX, client.getRenderManager().viewerPosY, client.getRenderManager().viewerPosZ); + + GlStateManager.pushMatrix(); + GlStateManager.translate(deltaPos.x, deltaPos.y, deltaPos.z); + GlStateManager.rotate(-yaw, 0.0F, 1.0F, 0.0F); + GlStateManager.rotate((float)(isThirdPersonFrontal ? -1 : 1) * pitch, 1.0F, 0.0F, 0.0F); + float scale = (float) Math.max(deltaPos.length() / 8.0f, 1.0f) * (0.75f + MathHelper.abs((float) (MathHelper.sin(totalPartialTicks / 10F) / Math.PI))); + GlStateManager.scale(0.05F * scale, 0.05F * scale, 0.05F * scale); + GlStateManager.color(1f, 1f, 1f, 1f); + GlStateManager.disableCull(); + GlStateManager.depthFunc(GL11.GL_ALWAYS); + + client.getTextureManager().bindTexture(KNOCKED_ICON); + this.drawTexture( + -KNOCKED_ICON_SIZE / 2, + -KNOCKED_ICON_SIZE / 2, + KNOCKED_ICON_SIZE, + KNOCKED_ICON_SIZE + ); + + GlStateManager.depthFunc(GL11.GL_LEQUAL); + GlStateManager.enableCull(); + GlStateManager.popMatrix(); + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/client/gui/ReviveGui.java b/src/main/java/ru/octol1ttle/knockdowns/client/gui/ReviveGui.java new file mode 100644 index 0000000..e8df745 --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/client/gui/ReviveGui.java @@ -0,0 +1,47 @@ +package ru.octol1ttle.knockdowns.client.gui; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.util.text.TextFormatting; +import ru.octol1ttle.knockdowns.common.KnockdownsUtils; +import ru.octol1ttle.knockdowns.common.ReviverTracker; +import ru.octol1ttle.knockdowns.common.data.IKnockdownsPlayerData; + +public class ReviveGui extends KnockdownsBaseGui { + private static final Minecraft client = Minecraft.getMinecraft(); + + @Override + public void render(float partialTicks, ScaledResolution resolution) { + EntityPlayer reviving = + client.pointedEntity instanceof EntityPlayer && ReviverTracker.getRevivers((EntityPlayer) client.pointedEntity).contains(client.player) + ? (EntityPlayer) client.pointedEntity + : client.player; + if (IKnockdownsPlayerData.get(reviving).getReviveTimeLeft() == KnockdownsUtils.INITIAL_REVIVE_TIME_LEFT) { + return; + } + IKnockdownsPlayerData data = IKnockdownsPlayerData.get(reviving); + + FontRenderer font = client.fontRenderer; + + String timerText = String.format("%.1f", data.getReviveTimeLeft() / 20.0f); + float timerX = (resolution.getScaledWidth() - font.getStringWidth(timerText)) * 0.5f; + + int reviverCount = ReviverTracker.getRevivers(reviving).size(); + TextFormatting color; + if (reviverCount == 0) { + color = TextFormatting.RED; + } else if (reviverCount == 1) { + color = TextFormatting.WHITE; + } else { + color = TextFormatting.GREEN; + } + + String reviverCountText = "x" + reviverCount; + float reviveCountX = (resolution.getScaledWidth() - font.getStringWidth(reviverCountText)) * 0.5f; + + font.drawStringWithShadow(color + timerText, timerX, resolution.getScaledHeight() * 0.5f + 5, 553648127); + font.drawStringWithShadow(color + reviverCountText, reviveCountX, resolution.getScaledHeight() * 0.5f + 14, 553648127); + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/client/network/KnockdownsClientPacketHandler.java b/src/main/java/ru/octol1ttle/knockdowns/client/network/KnockdownsClientPacketHandler.java new file mode 100644 index 0000000..4770827 --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/client/network/KnockdownsClientPacketHandler.java @@ -0,0 +1,145 @@ +package ru.octol1ttle.knockdowns.client.network; + +import net.minecraft.client.Minecraft; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraftforge.fml.common.network.simpleimpl.IMessage; +import net.minecraftforge.fml.common.network.simpleimpl.IMessageHandler; +import net.minecraftforge.fml.common.network.simpleimpl.MessageContext; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; +import ru.octol1ttle.knockdowns.client.communication.CalloutManager; +import ru.octol1ttle.knockdowns.client.communication.KnockedNotificationManager; +import ru.octol1ttle.knockdowns.client.util.DirectionalCallSound; +import ru.octol1ttle.knockdowns.common.ReviverTracker; +import ru.octol1ttle.knockdowns.common.data.IKnockdownsPlayerData; +import ru.octol1ttle.knockdowns.common.network.packets.s2c.PlayerCalloutS2CPacket; +import ru.octol1ttle.knockdowns.common.network.packets.s2c.PlayerKnockedDownS2CPacket; +import ru.octol1ttle.knockdowns.common.network.packets.s2c.SynchronizePlayerDataS2CPacket; +import ru.octol1ttle.knockdowns.common.network.packets.s2c.SynchronizeReviversS2CPacket; +import ru.octol1ttle.knockdowns.common.registry.KnockdownsSoundEvents; + +import static ru.octol1ttle.knockdowns.common.KnockdownsUtils.INITIAL_REVIVE_TIME_LEFT; + +public class KnockdownsClientPacketHandler { + public static class Callout implements IMessageHandler { + private static final Minecraft client = Minecraft.getMinecraft(); + + @SideOnly(Side.CLIENT) + @Override + public IMessage onMessage(PlayerCalloutS2CPacket message, MessageContext ctx) { + client.addScheduledTask(() -> { + if (CalloutManager.addOrUpdateCallout(message)) { + CalloutManager.playCalloutSound(message); + } + }); + return null; + } + } + + public static class PlayerKnockedDown implements IMessageHandler { + private static final Minecraft client = Minecraft.getMinecraft(); + + @SideOnly(Side.CLIENT) + @Override + public IMessage onMessage(PlayerKnockedDownS2CPacket message, MessageContext ctx) { + client.addScheduledTask(() -> { + EntityPlayer entity = (EntityPlayer) client.world.getEntityByID(message.playerId); + if (entity != null) { + IKnockdownsPlayerData data = IKnockdownsPlayerData.get(entity); + data.setKnockedDown(true); + data.setReviveTimeLeft(INITIAL_REVIVE_TIME_LEFT); + } + + if (client.player.dimension == message.dimensionId) { + client.getSoundHandler().playSound(new DirectionalCallSound(KnockdownsSoundEvents.KNOCKED_DOWN, entity, message.position)); + KnockedNotificationManager.addKnockedNotification(message.playerId, message.position); + } + }); + return null; + } + } + + public static class SynchronizePlayerData { + private static final Minecraft client = Minecraft.getMinecraft(); + + public static class KnockedDown implements IMessageHandler { + @SideOnly(Side.CLIENT) + @Override + public IMessage onMessage(SynchronizePlayerDataS2CPacket.KnockedDown message, MessageContext ctx) { + client.addScheduledTask(() -> { + EntityPlayer entity = (EntityPlayer) client.world.getEntityByID(message.playerId); + if (entity != null) { + IKnockdownsPlayerData data = IKnockdownsPlayerData.get(entity); + data.setKnockedDown(message.knockedDown); + } + }); + return null; + } + } + + public static class ReviveTimeLeft implements IMessageHandler { + @SideOnly(Side.CLIENT) + @Override + public IMessage onMessage(SynchronizePlayerDataS2CPacket.ReviveTimeLeft message, MessageContext ctx) { + client.addScheduledTask(() -> { + EntityPlayer entity = (EntityPlayer) client.world.getEntityByID(message.playerId); + if (entity != null) { + IKnockdownsPlayerData data = IKnockdownsPlayerData.get(entity); + data.setReviveTimeLeft(message.reviveTimeLeft); + } + }); + return null; + } + } + + public static class Full implements IMessageHandler { + @SideOnly(Side.CLIENT) + @Override + public IMessage onMessage(SynchronizePlayerDataS2CPacket.Full message, MessageContext ctx) { + client.addScheduledTask(() -> { + EntityPlayer entity = (EntityPlayer) client.world.getEntityByID(message.playerId); + if (entity != null) { + IKnockdownsPlayerData data = IKnockdownsPlayerData.get(entity); + data.setKnockedDown(message.knockedDown); + data.setReviveTimeLeft(message.reviveTimeLeft); + } + }); + return null; + } + } + } + + public static class SynchronizeRevivers { + private static final Minecraft client = Minecraft.getMinecraft(); + + public static class Add implements IMessageHandler { + @SideOnly(Side.CLIENT) + @Override + public IMessage onMessage(SynchronizeReviversS2CPacket.Add message, MessageContext ctx) { + client.addScheduledTask(() -> { + EntityPlayer knocked = (EntityPlayer) client.world.getEntityByID(message.knockedId); + EntityPlayer reviver = (EntityPlayer) client.world.getEntityByID(message.reviverId); + if (knocked != null && reviver != null) { + ReviverTracker.startReviving(knocked, reviver); + } + }); + return null; + } + } + + public static class Remove implements IMessageHandler { + @SideOnly(Side.CLIENT) + @Override + public IMessage onMessage(SynchronizeReviversS2CPacket.Remove message, MessageContext ctx) { + client.addScheduledTask(() -> { + EntityPlayer knocked = (EntityPlayer) client.world.getEntityByID(message.knockedId); + EntityPlayer reviver = (EntityPlayer) client.world.getEntityByID(message.reviverId); + if (knocked != null && reviver != null) { + ReviverTracker.stopReviving(knocked, reviver); + } + }); + return null; + } + } + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/client/util/Callout.java b/src/main/java/ru/octol1ttle/knockdowns/client/util/Callout.java new file mode 100644 index 0000000..aa82df3 --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/client/util/Callout.java @@ -0,0 +1,31 @@ +package ru.octol1ttle.knockdowns.client.util; + +import net.minecraft.util.math.Vec3d; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; +import ru.octol1ttle.knockdowns.common.communication.CalloutType; + +@SideOnly(Side.CLIENT) +public class Callout { + private final Vec3d position; + private final CalloutType type; + private final long receiveTime; + + public Callout(Vec3d position, CalloutType type, long receiveTime) { + this.position = position; + this.type = type; + this.receiveTime = receiveTime; + } + + public Vec3d getPosition() { + return position; + } + + public CalloutType getType() { + return type; + } + + public long getReceiveTime() { + return receiveTime; + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/client/util/DirectionalCallSound.java b/src/main/java/ru/octol1ttle/knockdowns/client/util/DirectionalCallSound.java new file mode 100644 index 0000000..c5089b9 --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/client/util/DirectionalCallSound.java @@ -0,0 +1,42 @@ +package ru.octol1ttle.knockdowns.client.util; + +import javax.annotation.Nullable; +import net.minecraft.client.Minecraft; +import net.minecraft.client.audio.MovingSound; +import net.minecraft.entity.Entity; +import net.minecraft.util.SoundCategory; +import net.minecraft.util.SoundEvent; +import net.minecraft.util.math.Vec3d; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; + +@SideOnly(Side.CLIENT) +public class DirectionalCallSound extends MovingSound { + private final @Nullable Entity entity; + private final Vec3d position; + private int time; + + public DirectionalCallSound(SoundEvent event, @Nullable Entity entity, Vec3d position) { + super(event, SoundCategory.PLAYERS); + this.entity = entity; + this.position = position; + } + + @Override + public void update() { + this.time++; + if (this.time > 40 || this.entity != null && this.entity.isDead) { + this.donePlaying = true; + return; + } + + Minecraft client = Minecraft.getMinecraft(); + Vec3d calloutPos = this.entity != null ? this.entity.getPositionVector() : this.position; + Vec3d directionVec = calloutPos.subtract(client.player.getPositionVector()).normalize(); + Vec3d finalPos = client.player.getPositionVector().add(directionVec); + + this.xPosF = (float) finalPos.x; + this.yPosF = (float) finalPos.y; + this.zPosF = (float) finalPos.z; + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/client/util/KnockedPlayerData.java b/src/main/java/ru/octol1ttle/knockdowns/client/util/KnockedPlayerData.java new file mode 100644 index 0000000..8cebdde --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/client/util/KnockedPlayerData.java @@ -0,0 +1,27 @@ +package ru.octol1ttle.knockdowns.client.util; + +import net.minecraft.util.math.Vec3d; + +public class KnockedPlayerData { + private final int playerId; + private final Vec3d position; + private final long receiveTime; + + public KnockedPlayerData(int playerId, Vec3d position, long receiveTime) { + this.playerId = playerId; + this.position = position; + this.receiveTime = receiveTime; + } + + public int getPlayerId() { + return playerId; + } + + public Vec3d getPosition() { + return position; + } + + public long getReceiveTime() { + return receiveTime; + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/IClientProxy.java b/src/main/java/ru/octol1ttle/knockdowns/common/IClientProxy.java new file mode 100644 index 0000000..448ccd8 --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/common/IClientProxy.java @@ -0,0 +1,13 @@ +package ru.octol1ttle.knockdowns.common; + +import net.minecraftforge.fml.common.event.FMLInitializationEvent; + +public interface IClientProxy { + void onFMLInit(FMLInitializationEvent event); + + class Dummy implements IClientProxy { + @Override + public void onFMLInit(FMLInitializationEvent event) { + } + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsCommonEventListener.java b/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsCommonEventListener.java new file mode 100644 index 0000000..1b8b252 --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsCommonEventListener.java @@ -0,0 +1,239 @@ +package ru.octol1ttle.knockdowns.common; + +import java.util.List; +import net.minecraft.client.resources.I18n; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLiving; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.init.MobEffects; +import net.minecraft.potion.PotionEffect; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.DamageSource; +import net.minecraft.util.EnumActionResult; +import net.minecraft.util.SoundEvent; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.text.TextComponentTranslation; +import net.minecraftforge.event.AttachCapabilitiesEvent; +import net.minecraftforge.event.RegistryEvent; +import net.minecraftforge.event.entity.living.LivingDeathEvent; +import net.minecraftforge.event.entity.living.LivingSetAttackTargetEvent; +import net.minecraftforge.event.entity.player.AttackEntityEvent; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.event.entity.player.PlayerInteractEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.PlayerEvent.PlayerLoggedInEvent; +import net.minecraftforge.fml.common.gameevent.PlayerEvent.PlayerLoggedOutEvent; +import net.minecraftforge.fml.common.gameevent.TickEvent; +import ru.octol1ttle.knockdowns.Tags; +import ru.octol1ttle.knockdowns.common.data.IKnockdownsPlayerData; +import ru.octol1ttle.knockdowns.common.data.KnockdownsCapability; +import ru.octol1ttle.knockdowns.common.network.KnockdownsNetwork; +import ru.octol1ttle.knockdowns.common.network.packets.s2c.PlayerKnockedDownS2CPacket; +import ru.octol1ttle.knockdowns.common.network.packets.s2c.SynchronizePlayerDataS2CPacket; +import ru.octol1ttle.knockdowns.common.network.packets.s2c.SynchronizeReviversS2CPacket; +import ru.octol1ttle.knockdowns.common.registry.KnockdownsSoundEvents; + +import static ru.octol1ttle.knockdowns.common.KnockdownsUtils.INITIAL_REVIVE_TIME_LEFT; +import static ru.octol1ttle.knockdowns.common.KnockdownsUtils.KNOCKED_HURT_PERIOD; +import static ru.octol1ttle.knockdowns.common.KnockdownsUtils.KNOCKED_INVULNERABILITY_TICKS; +import static ru.octol1ttle.knockdowns.common.KnockdownsUtils.KNOCKED_TENACITY; +import static ru.octol1ttle.knockdowns.common.KnockdownsUtils.allPlayersKnocked; +import static ru.octol1ttle.knockdowns.common.KnockdownsUtils.resetKnockedState; + +@Mod.EventBusSubscriber(modid = Tags.MOD_ID) +public class KnockdownsCommonEventListener { + public static void onFMLInit(FMLInitializationEvent event) { + KnockdownsNetwork.registerPackets(); + KnockdownsCapability.register(); + } + + @SubscribeEvent + public static void onSoundsRegister(RegistryEvent.Register event) { + event.getRegistry().register(KnockdownsSoundEvents.CALLOUT); + } + + @SubscribeEvent + public static void onCapabilitiesAttach(AttachCapabilitiesEvent event) { + if (event.getObject() instanceof EntityPlayer) { + event.addCapability(KnockdownsCapability.ID, new KnockdownsCapability()); + } + } + + @SubscribeEvent + public static void onPlayerTick(TickEvent.PlayerTickEvent event) { + MinecraftServer server = event.player.getServer(); + if (event.phase == TickEvent.Phase.START || server == null) { + return; + } + EntityPlayerMP player = (EntityPlayerMP) event.player; + IKnockdownsPlayerData data = IKnockdownsPlayerData.get(player); + if (!data.isKnockedDown()) { + return; + } + + if (allPlayersKnocked(server, player)) { + player.attackEntityFrom(DamageSource.GENERIC, player.getMaxHealth()); + return; + } + + List revivers = ReviverTracker.getRevivers(player); + if (!revivers.isEmpty()) { + data.setReviveTimeLeft(data.getReviveTimeLeft() - revivers.size()); + KnockdownsNetwork.sendToMultiple( + new SynchronizePlayerDataS2CPacket.ReviveTimeLeft(player.getEntityId(), data.getReviveTimeLeft()), + revivers, + player + ); + + if (data.getReviveTimeLeft() <= 0) { + resetKnockedState(player, data); + + player.setEntityInvulnerable(false); + player.setHealth(player.getMaxHealth() * 0.3f); + player.setAbsorptionAmount(0.0f); + } + return; + } + + int oldReviveTimeLeft = data.getReviveTimeLeft(); + data.setReviveTimeLeft(Math.min(INITIAL_REVIVE_TIME_LEFT, oldReviveTimeLeft + 1)); + if (data.getReviveTimeLeft() != oldReviveTimeLeft) { + KnockdownsNetwork.sendToPlayer( + new SynchronizePlayerDataS2CPacket.ReviveTimeLeft(player.getEntityId(), data.getReviveTimeLeft()), + player + ); + } + + data.setTicksKnocked(data.getTicksKnocked() + 1); + + int period = MathHelper.floor(KNOCKED_HURT_PERIOD * 20); + if (data.getTicksKnocked() >= KNOCKED_INVULNERABILITY_TICKS && data.getTicksKnocked() % period == 0) { + player.setEntityInvulnerable(false); + player.attackEntityFrom(DamageSource.GENERIC, player.getMaxHealth() / (KNOCKED_TENACITY / KNOCKED_HURT_PERIOD)); + } + } + + @SubscribeEvent + public static void onPlayerDeath(LivingDeathEvent event) { + if (!(event.getEntityLiving() instanceof EntityPlayerMP)) { + return; + } + + EntityPlayerMP player = (EntityPlayerMP) event.getEntityLiving(); + IKnockdownsPlayerData data = player.getCapability(KnockdownsCapability.CAPABILITY, null); + if (data == null) { + return; + } + + if (data.isKnockedDown() || allPlayersKnocked(player.getServer(), player)) { + ReviverTracker.clearRevivers(player); + return; + } + + player.clearActivePotions(); + player.setEntityInvulnerable(true); + player.setHealth(1.0f); + player.setAbsorptionAmount(player.getMaxHealth() - 1.0f); + player.extinguish(); + player.setAir(300); + player.clearElytraFlying(); + + player.addPotionEffect(new PotionEffect(MobEffects.SLOWNESS, 6000, 3)); + + Entity trueSource = event.getSource().getTrueSource(); + if (trueSource instanceof EntityLiving) { + ((EntityLiving) trueSource).setAttackTarget(null); + } + + data.setKnockedDown(true); + data.setReviveTimeLeft(INITIAL_REVIVE_TIME_LEFT); + data.setTicksKnocked(0); + + KnockdownsNetwork.sendToAll(new PlayerKnockedDownS2CPacket(player.getEntityId(), player.dimension, player.getPositionEyes(1).add(0, 1, 0))); + + TextComponentTranslation deathMessage = (TextComponentTranslation) player.getCombatTracker().getDeathMessage(); + + String knockdownKey = deathMessage.getKey().replace("death.", "knockdown."); + player.getServer().getPlayerList().sendMessage( + I18n.hasKey(knockdownKey) + ? new TextComponentTranslation(knockdownKey, deathMessage.getFormatArgs()) + : deathMessage, + true + ); + + event.setCanceled(true); + } + + @SubscribeEvent + public static void onMobTarget(LivingSetAttackTargetEvent event) { + if (event.getTarget() instanceof EntityPlayer && IKnockdownsPlayerData.get((EntityPlayer) event.getTarget()).isKnockedDown()) { + ((EntityLiving)event.getEntityLiving()).setAttackTarget(null); + } + } + + @SubscribeEvent + public static void onPlayerStartTracking(PlayerEvent.StartTracking event) { + if (event.getTarget() instanceof EntityPlayerMP) { + IKnockdownsPlayerData data = IKnockdownsPlayerData.get((EntityPlayer) event.getTarget()); + if (data.isKnockedDown()) { + KnockdownsNetwork.sendToPlayer( + new SynchronizePlayerDataS2CPacket.KnockedDown( + event.getTarget().getEntityId(), + data.isKnockedDown() + ), + (EntityPlayerMP) event.getEntityPlayer() + ); + } + } + } + + @SubscribeEvent + public static void onKnockedAttack(AttackEntityEvent event) { + if (IKnockdownsPlayerData.get(event.getEntityPlayer()).isKnockedDown()) { + event.setCanceled(true); + } + } + + @SubscribeEvent + public static void onKnockedInteraction(PlayerInteractEvent event) { + if (IKnockdownsPlayerData.get(event.getEntityPlayer()).isKnockedDown()) { + if (!(event instanceof PlayerInteractEvent.RightClickBlock) && event.isCancelable()) { + event.setCanceled(true); + event.setCancellationResult(EnumActionResult.FAIL); + } + } else if (event instanceof PlayerInteractEvent.EntityInteract) { + onPlayerInteraction((PlayerInteractEvent.EntityInteract) event); + } + } + + public static void onPlayerInteraction(PlayerInteractEvent.EntityInteract event) { + if (event.getEntityLiving() instanceof EntityPlayerMP) { + EntityPlayerMP knocked = (EntityPlayerMP) event.getEntityLiving(); + if (IKnockdownsPlayerData.get(knocked).isKnockedDown()) { + KnockdownsNetwork.sendToMultiple( + new SynchronizeReviversS2CPacket.Add(event.getEntityLiving().getEntityId(), event.getEntityPlayer().getEntityId()), + ReviverTracker.getRevivers(knocked), + knocked + ); + ReviverTracker.startReviving(knocked, event.getEntityPlayer()); + } + } + } + + @SubscribeEvent + public static void onPlayerJoin(PlayerLoggedInEvent event) { + IKnockdownsPlayerData data = IKnockdownsPlayerData.get(event.player); + KnockdownsNetwork.sendToPlayer( + new SynchronizePlayerDataS2CPacket.Full(event.player.getEntityId(), data.isKnockedDown(), data.getReviveTimeLeft()), + (EntityPlayerMP) event.player + ); + } + + @SubscribeEvent + public static void onPlayerLeave(PlayerLoggedOutEvent event) { + ReviverTracker.clearRevivers(event.player); + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsFMLLoadingPlugin.java b/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsFMLLoadingPlugin.java new file mode 100644 index 0000000..77e881b --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsFMLLoadingPlugin.java @@ -0,0 +1,43 @@ +package ru.octol1ttle.knockdowns.common; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin; +import zone.rong.mixinbooter.IEarlyMixinLoader; + +@IFMLLoadingPlugin.MCVersion("1.12.2") +public class KnockdownsFMLLoadingPlugin implements IFMLLoadingPlugin, IEarlyMixinLoader { + @Override + public String[] getASMTransformerClass() { + return null; + } + + @Override + public String getModContainerClass() { + return null; + } + + @Nullable + @Override + public String getSetupClass() { + return null; + } + + @Override + public void injectData(Map data) { + } + + @Override + public String getAccessTransformerClass() { + return null; + } + + @Override + public List getMixinConfigs() { + ArrayList list = new ArrayList<>(); + list.add("mixins.knockdowns.json"); + return list; + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsMod.java b/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsMod.java new file mode 100644 index 0000000..d9e27c0 --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsMod.java @@ -0,0 +1,21 @@ +package ru.octol1ttle.knockdowns.common; + +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.SidedProxy; +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import ru.octol1ttle.knockdowns.Tags; + +@Mod(modid = Tags.MOD_ID, name = Tags.MOD_NAME, version = Tags.VERSION) +public class KnockdownsMod { + public static final Logger LOGGER = LogManager.getLogger(Tags.MOD_NAME); + @SidedProxy(clientSide = "ru.octol1ttle.knockdowns.client.ClientProxy", serverSide = "ru.octol1ttle.knockdowns.common.IClientProxy$Dummy") + public static IClientProxy clientProxy; + + @Mod.EventHandler + public void onFMLInit(FMLInitializationEvent event) { + clientProxy.onFMLInit(event); + KnockdownsCommonEventListener.onFMLInit(event); + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsUtils.java b/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsUtils.java new file mode 100644 index 0000000..207a0ab --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsUtils.java @@ -0,0 +1,43 @@ +package ru.octol1ttle.knockdowns.common; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.init.MobEffects; +import net.minecraft.server.MinecraftServer; +import ru.octol1ttle.knockdowns.common.data.IKnockdownsPlayerData; +import ru.octol1ttle.knockdowns.common.network.KnockdownsNetwork; +import ru.octol1ttle.knockdowns.common.network.packets.s2c.SynchronizePlayerDataS2CPacket; + +public class KnockdownsUtils { + public static final int INITIAL_REVIVE_TIME_LEFT = 200; + public static final float KNOCKED_INVULNERABILITY_TICKS = 3.0f * 20.0f; + public static final float KNOCKED_HURT_PERIOD = 1.2f; + public static final float KNOCKED_TENACITY = 60.0f; + + public static boolean allPlayersKnocked(MinecraftServer server, EntityPlayer except) { + for (EntityPlayer player : server.getPlayerList().getPlayers()) { + if (player.equals(except)) { + continue; + } + IKnockdownsPlayerData data = IKnockdownsPlayerData.get(player); + if (player.isEntityAlive() && !data.isKnockedDown()) { + return false; + } + } + return false; + } + + public static void resetKnockedState(EntityPlayerMP player, IKnockdownsPlayerData data) { + player.removePotionEffect(MobEffects.SLOWNESS); + data.setKnockedDown(false); + data.setReviveTimeLeft(INITIAL_REVIVE_TIME_LEFT); + data.setTicksKnocked(0); + + KnockdownsNetwork.sendToTrackingAndSelf( + new SynchronizePlayerDataS2CPacket.KnockedDown(player.getEntityId(), data.isKnockedDown()), + player + ); + + ReviverTracker.clearRevivers(player); + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/ReviverTracker.java b/src/main/java/ru/octol1ttle/knockdowns/common/ReviverTracker.java new file mode 100644 index 0000000..ba4736c --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/common/ReviverTracker.java @@ -0,0 +1,32 @@ +package ru.octol1ttle.knockdowns.common; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import net.minecraft.entity.player.EntityPlayer; + +public class ReviverTracker { + private static final Map> knockedToReviversMap = new HashMap<>(); + + public static void startReviving(EntityPlayer knocked, EntityPlayer reviver) { + getRevivers(knocked).add(reviver); + } + + public static void stopReviving(EntityPlayer knocked, EntityPlayer reviver) { + getRevivers(knocked).remove(reviver); + } + + public static void clearRevivers(EntityPlayer knocked) { + getRevivers(knocked).clear(); + } + + public static List getRevivers(EntityPlayer knocked) { + return knockedToReviversMap.computeIfAbsent(knocked, player -> new ArrayList<>()); + } + + public static Set>> getAllRevivers() { + return knockedToReviversMap.entrySet(); + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/communication/CalloutType.java b/src/main/java/ru/octol1ttle/knockdowns/common/communication/CalloutType.java new file mode 100644 index 0000000..56ecb03 --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/common/communication/CalloutType.java @@ -0,0 +1,35 @@ +package ru.octol1ttle.knockdowns.common.communication; + +public enum CalloutType { + DANGER((byte) 0, "knockdowns.callout.danger"), + BOOYAH((byte) 1, "knockdowns.callout.booyah"), + THIS_WAY((byte) 2, "knockdowns.callout.this_way"), + OUCH((byte) 3, "knockdowns.callout.ouch"), + HELP((byte) 4, "knockdowns.callout.help"); + + private final byte id; + private final String textKey; + + CalloutType(byte id, String textKey) { + this.id = id; + this.textKey = textKey; + } + + public byte getId() { + return id; + } + + public String getTextKey() { + return textKey; + } + + public static CalloutType byId(byte id) { + for (CalloutType type : CalloutType.values()) { + if (id == type.getId()) { + return type; + } + } + + throw new IllegalArgumentException(); + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/data/IKnockdownsPlayerData.java b/src/main/java/ru/octol1ttle/knockdowns/common/data/IKnockdownsPlayerData.java new file mode 100644 index 0000000..f0e98c9 --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/common/data/IKnockdownsPlayerData.java @@ -0,0 +1,21 @@ +package ru.octol1ttle.knockdowns.common.data; + +import java.util.Objects; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraftforge.common.util.INBTSerializable; + +public interface IKnockdownsPlayerData extends INBTSerializable { + boolean isKnockedDown(); + void setKnockedDown(boolean knockedDown); + + int getReviveTimeLeft(); + void setReviveTimeLeft(int reviveTimeLeft); + + int getTicksKnocked(); + void setTicksKnocked(int ticksKnocked); + + static IKnockdownsPlayerData get(EntityPlayer player) { + return Objects.requireNonNull(player.getCapability(KnockdownsCapability.CAPABILITY, null)); + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/data/KnockdownsCapability.java b/src/main/java/ru/octol1ttle/knockdowns/common/data/KnockdownsCapability.java new file mode 100644 index 0000000..d25ad94 --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/common/data/KnockdownsCapability.java @@ -0,0 +1,65 @@ +package ru.octol1ttle.knockdowns.common.data; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import net.minecraft.nbt.NBTBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.LazyLoadBase; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.capabilities.CapabilityInject; +import net.minecraftforge.common.capabilities.CapabilityManager; +import net.minecraftforge.common.capabilities.ICapabilitySerializable; +import ru.octol1ttle.knockdowns.Tags; + +public class KnockdownsCapability implements ICapabilitySerializable { + @CapabilityInject(IKnockdownsPlayerData.class) + public static Capability CAPABILITY; + public static final ResourceLocation ID = new ResourceLocation(Tags.MOD_ID, "data"); + + private KnockdownsPlayerData playerData = null; + private final LazyLoadBase lazy = new LazyLoadBase() { + @Override + protected KnockdownsPlayerData load() { + return playerData == null ? (playerData = new KnockdownsPlayerData()) : playerData; + } + }; + + public static void register() { + CapabilityManager.INSTANCE.register(IKnockdownsPlayerData.class, new Capability.IStorage() { + @Nullable + @Override + public NBTBase writeNBT(Capability capability, IKnockdownsPlayerData instance, EnumFacing side) { + return instance.serializeNBT(); + } + + @Override + public void readNBT(Capability capability, IKnockdownsPlayerData instance, EnumFacing side, NBTBase nbt) { + instance.deserializeNBT((NBTTagCompound) nbt); + } + }, KnockdownsPlayerData::new); + } + + @Override + public boolean hasCapability(@Nonnull Capability capability, @Nullable EnumFacing facing) { + return capability == CAPABILITY; + } + + @SuppressWarnings("unchecked") + @Nullable + @Override + public T getCapability(@Nonnull Capability capability, @Nullable EnumFacing facing) { + return capability == CAPABILITY ? (T) lazy.getValue() : null; + } + + @Override + public NBTTagCompound serializeNBT() { + return lazy.getValue().serializeNBT(); + } + + @Override + public void deserializeNBT(NBTTagCompound nbt) { + lazy.getValue().deserializeNBT(nbt); + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/data/KnockdownsPlayerData.java b/src/main/java/ru/octol1ttle/knockdowns/common/data/KnockdownsPlayerData.java new file mode 100644 index 0000000..7eac143 --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/common/data/KnockdownsPlayerData.java @@ -0,0 +1,59 @@ +package ru.octol1ttle.knockdowns.common.data; + +import net.minecraft.nbt.NBTTagCompound; +import ru.octol1ttle.knockdowns.common.KnockdownsUtils; + +public class KnockdownsPlayerData implements IKnockdownsPlayerData { + private static final String KEY_KNOCKED_DOWN = "KnockedDown"; + private static final String KEY_REVIVE_TIME_LEFT = "ReviveTimeLeft"; + private static final String KEY_TICKS_KNOCKED = "TicksKnocked"; + private boolean knockedDown = false; + private int reviveTimeLeft = KnockdownsUtils.INITIAL_REVIVE_TIME_LEFT; + private int ticksKnocked = 0; + + @Override + public boolean isKnockedDown() { + return this.knockedDown; + } + + @Override + public void setKnockedDown(boolean knockedDown) { + this.knockedDown = knockedDown; + } + + @Override + public int getReviveTimeLeft() { + return this.reviveTimeLeft; + } + + @Override + public void setReviveTimeLeft(int reviveTimeLeft) { + this.reviveTimeLeft = reviveTimeLeft; + } + + @Override + public int getTicksKnocked() { + return this.ticksKnocked; + } + + @Override + public void setTicksKnocked(int ticksKnocked) { + this.ticksKnocked = ticksKnocked; + } + + @Override + public NBTTagCompound serializeNBT() { + NBTTagCompound nbt = new NBTTagCompound(); + nbt.setBoolean(KEY_KNOCKED_DOWN, this.knockedDown); + nbt.setInteger(KEY_REVIVE_TIME_LEFT, this.reviveTimeLeft); + nbt.setInteger(KEY_TICKS_KNOCKED, this.ticksKnocked); + return nbt; + } + + @Override + public void deserializeNBT(NBTTagCompound nbt) { + this.knockedDown = nbt.getBoolean(KEY_KNOCKED_DOWN); + this.reviveTimeLeft = nbt.getInteger(KEY_REVIVE_TIME_LEFT); + this.ticksKnocked = nbt.getInteger(KEY_TICKS_KNOCKED); + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/mixins/EntityLivingBaseMixin.java b/src/main/java/ru/octol1ttle/knockdowns/common/mixins/EntityLivingBaseMixin.java new file mode 100644 index 0000000..8bcea94 --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/common/mixins/EntityLivingBaseMixin.java @@ -0,0 +1,31 @@ +package ru.octol1ttle.knockdowns.common.mixins; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.world.World; +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 ru.octol1ttle.knockdowns.common.KnockdownsUtils; +import ru.octol1ttle.knockdowns.common.data.IKnockdownsPlayerData; + +@SuppressWarnings("ConstantValue") +@Mixin(EntityLivingBase.class) +public abstract class EntityLivingBaseMixin extends Entity { + public EntityLivingBaseMixin(World worldIn) { + super(worldIn); + } + + @Inject(method = "checkTotemDeathProtection", at = @At("RETURN")) + public void onTotemActivation(CallbackInfoReturnable cir) { + if (cir.getReturnValue() && ((Object) this) instanceof EntityPlayerMP) { + EntityPlayerMP player = (EntityPlayerMP) (Object) this; + IKnockdownsPlayerData data = IKnockdownsPlayerData.get(player); + if (data.isKnockedDown()) { + KnockdownsUtils.resetKnockedState(player, data); + } + } + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/mixins/EntityPlayerMixin.java b/src/main/java/ru/octol1ttle/knockdowns/common/mixins/EntityPlayerMixin.java new file mode 100644 index 0000000..e1684ac --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/common/mixins/EntityPlayerMixin.java @@ -0,0 +1,22 @@ +package ru.octol1ttle.knockdowns.common.mixins; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import net.minecraft.entity.player.EntityPlayer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import ru.octol1ttle.knockdowns.common.data.IKnockdownsPlayerData; + +@SuppressWarnings("ConstantValue") +@Mixin(EntityPlayer.class) +public class EntityPlayerMixin { + @ModifyReturnValue(method = "shouldHeal", at = @At("RETURN")) + private boolean dontHealIfKnockedDown(boolean original) { + if (((Object) this) instanceof EntityPlayer) { + EntityPlayer player = (EntityPlayer) (Object) this; + if (IKnockdownsPlayerData.get(player).isKnockedDown()) { + return false; + } + } + return original; + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/network/KnockdownsNetwork.java b/src/main/java/ru/octol1ttle/knockdowns/common/network/KnockdownsNetwork.java new file mode 100644 index 0000000..4542e68 --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/common/network/KnockdownsNetwork.java @@ -0,0 +1,71 @@ +package ru.octol1ttle.knockdowns.common.network; + +import java.util.List; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraftforge.fml.common.network.NetworkRegistry; +import net.minecraftforge.fml.common.network.simpleimpl.IMessage; +import net.minecraftforge.fml.common.network.simpleimpl.MessageContext; +import net.minecraftforge.fml.common.network.simpleimpl.SimpleNetworkWrapper; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; +import ru.octol1ttle.knockdowns.Tags; +import ru.octol1ttle.knockdowns.client.network.KnockdownsClientPacketHandler; +import ru.octol1ttle.knockdowns.common.network.packets.c2s.CancelReviveC2SPacket; +import ru.octol1ttle.knockdowns.common.network.packets.c2s.PlayerCalloutC2SPacket; +import ru.octol1ttle.knockdowns.common.network.packets.s2c.PlayerCalloutS2CPacket; +import ru.octol1ttle.knockdowns.common.network.packets.s2c.PlayerKnockedDownS2CPacket; +import ru.octol1ttle.knockdowns.common.network.packets.s2c.SynchronizePlayerDataS2CPacket; +import ru.octol1ttle.knockdowns.common.network.packets.s2c.SynchronizeReviversS2CPacket; + +public class KnockdownsNetwork { + private static final SimpleNetworkWrapper INSTANCE = NetworkRegistry.INSTANCE.newSimpleChannel(Tags.MOD_ID); + private static int packetId = 0; + + public static void registerPackets() { + INSTANCE.registerMessage(KnockdownsServerPacketHandler.Callout.class, PlayerCalloutC2SPacket.class, packetId++, Side.SERVER); + INSTANCE.registerMessage(KnockdownsServerPacketHandler.CancelRevive.class, CancelReviveC2SPacket.class, packetId++, Side.SERVER); + + INSTANCE.registerMessage(KnockdownsClientPacketHandler.Callout.class, PlayerCalloutS2CPacket.class, packetId++, Side.CLIENT); + INSTANCE.registerMessage(KnockdownsClientPacketHandler.PlayerKnockedDown.class, PlayerKnockedDownS2CPacket.class, packetId++, Side.CLIENT); + INSTANCE.registerMessage(KnockdownsClientPacketHandler.SynchronizePlayerData.KnockedDown.class, SynchronizePlayerDataS2CPacket.KnockedDown.class, packetId++, Side.CLIENT); + INSTANCE.registerMessage(KnockdownsClientPacketHandler.SynchronizePlayerData.ReviveTimeLeft.class, SynchronizePlayerDataS2CPacket.ReviveTimeLeft.class, packetId++, Side.CLIENT); + INSTANCE.registerMessage(KnockdownsClientPacketHandler.SynchronizePlayerData.Full.class, SynchronizePlayerDataS2CPacket.Full.class, packetId++, Side.CLIENT); + INSTANCE.registerMessage(KnockdownsClientPacketHandler.SynchronizeRevivers.Add.class, SynchronizeReviversS2CPacket.Add.class, packetId++, Side.CLIENT); + INSTANCE.registerMessage(KnockdownsClientPacketHandler.SynchronizeRevivers.Remove.class, SynchronizeReviversS2CPacket.Remove.class, packetId++, Side.CLIENT); + } + + @SideOnly(Side.CLIENT) + public static void sendToServer(IMessage message) { + INSTANCE.sendToServer(message); + } + + public static void sendToAll(IMessage message) { + INSTANCE.sendToAll(message); + } + + public static void sendToDimension(IMessage message, MessageContext context) { + INSTANCE.sendToDimension(message, context.getServerHandler().player.dimension); + } + + public static void sendToTrackingAndSelf(IMessage message, EntityPlayerMP player) { + sendToPlayer(message, player); + sendToTracking(message, player); + } + + public static void sendToMultiple(IMessage message, List players, EntityPlayerMP player) { + sendToPlayer(message, player); + for (EntityPlayer listed : players) { + sendToPlayer(message, (EntityPlayerMP) listed); + } + } + + public static void sendToTracking(IMessage message, Entity entity) { + INSTANCE.sendToAllTracking(message, entity); + } + + public static void sendToPlayer(IMessage message, EntityPlayerMP player) { + INSTANCE.sendTo(message, player); + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/network/KnockdownsServerPacketHandler.java b/src/main/java/ru/octol1ttle/knockdowns/common/network/KnockdownsServerPacketHandler.java new file mode 100644 index 0000000..d73f461 --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/common/network/KnockdownsServerPacketHandler.java @@ -0,0 +1,51 @@ +package ru.octol1ttle.knockdowns.common.network; + +import java.util.List; +import java.util.Map; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraftforge.fml.common.network.simpleimpl.IMessage; +import net.minecraftforge.fml.common.network.simpleimpl.IMessageHandler; +import net.minecraftforge.fml.common.network.simpleimpl.MessageContext; +import ru.octol1ttle.knockdowns.common.ReviverTracker; +import ru.octol1ttle.knockdowns.common.network.packets.c2s.CancelReviveC2SPacket; +import ru.octol1ttle.knockdowns.common.network.packets.c2s.PlayerCalloutC2SPacket; +import ru.octol1ttle.knockdowns.common.network.packets.s2c.PlayerCalloutS2CPacket; +import ru.octol1ttle.knockdowns.common.network.packets.s2c.SynchronizeReviversS2CPacket; + +public class KnockdownsServerPacketHandler { + public static class Callout implements IMessageHandler { + @Override + public IMessage onMessage(PlayerCalloutC2SPacket message, MessageContext ctx) { + KnockdownsNetwork.sendToDimension( + new PlayerCalloutS2CPacket( + ctx.getServerHandler().player.getEntityId(), + ctx.getServerHandler().player.getPositionEyes(1).add(0, 1, 0), + message.type + ), + ctx + ); + return null; + } + } + + public static class CancelRevive implements IMessageHandler { + @Override + public IMessage onMessage(CancelReviveC2SPacket message, MessageContext ctx) { + EntityPlayerMP player = ctx.getServerHandler().player; + for (Map.Entry> revivers : ReviverTracker.getAllRevivers()) { + if (revivers.getValue().contains(player)) { + revivers.getValue().remove(player); + KnockdownsNetwork.sendToMultiple( + new SynchronizeReviversS2CPacket.Remove(revivers.getKey().getEntityId(), player.getEntityId()), + revivers.getValue(), + (EntityPlayerMP) revivers.getKey() + ); + break; + } + } + + return null; + } + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/network/packets/c2s/CancelReviveC2SPacket.java b/src/main/java/ru/octol1ttle/knockdowns/common/network/packets/c2s/CancelReviveC2SPacket.java new file mode 100644 index 0000000..dce5e9a --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/common/network/packets/c2s/CancelReviveC2SPacket.java @@ -0,0 +1,17 @@ +package ru.octol1ttle.knockdowns.common.network.packets.c2s; + +import io.netty.buffer.ByteBuf; +import net.minecraftforge.fml.common.network.simpleimpl.IMessage; + +public class CancelReviveC2SPacket implements IMessage { + public CancelReviveC2SPacket() { + } + + @Override + public void toBytes(ByteBuf buf) { + } + + @Override + public void fromBytes(ByteBuf buf) { + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/network/packets/c2s/PlayerCalloutC2SPacket.java b/src/main/java/ru/octol1ttle/knockdowns/common/network/packets/c2s/PlayerCalloutC2SPacket.java new file mode 100644 index 0000000..580d5fa --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/common/network/packets/c2s/PlayerCalloutC2SPacket.java @@ -0,0 +1,25 @@ +package ru.octol1ttle.knockdowns.common.network.packets.c2s; + +import io.netty.buffer.ByteBuf; +import net.minecraftforge.fml.common.network.simpleimpl.IMessage; +import ru.octol1ttle.knockdowns.common.communication.CalloutType; + +public class PlayerCalloutC2SPacket implements IMessage { + public PlayerCalloutC2SPacket() { + } + + public CalloutType type; + public PlayerCalloutC2SPacket(CalloutType type) { + this.type = type; + } + + @Override + public void toBytes(ByteBuf buf) { + buf.writeByte(this.type.getId()); + } + + @Override + public void fromBytes(ByteBuf buf) { + this.type = CalloutType.byId(buf.readByte()); + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/network/packets/s2c/PlayerCalloutS2CPacket.java b/src/main/java/ru/octol1ttle/knockdowns/common/network/packets/s2c/PlayerCalloutS2CPacket.java new file mode 100644 index 0000000..34752dd --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/common/network/packets/s2c/PlayerCalloutS2CPacket.java @@ -0,0 +1,37 @@ +package ru.octol1ttle.knockdowns.common.network.packets.s2c; + +import io.netty.buffer.ByteBuf; +import net.minecraft.util.math.Vec3d; +import net.minecraftforge.fml.common.network.simpleimpl.IMessage; +import ru.octol1ttle.knockdowns.common.communication.CalloutType; + +public class PlayerCalloutS2CPacket implements IMessage { + public PlayerCalloutS2CPacket() { + } + + public int playerId; + public Vec3d position; + public CalloutType type; + + public PlayerCalloutS2CPacket(int playerId, Vec3d position, CalloutType type) { + this.playerId = playerId; + this.position = position; + this.type = type; + } + + @Override + public void toBytes(ByteBuf buf) { + buf.writeInt(this.playerId); + buf.writeDouble(this.position.x); + buf.writeDouble(this.position.y); + buf.writeDouble(this.position.z); + buf.writeByte(this.type.getId()); + } + + @Override + public void fromBytes(ByteBuf buf) { + this.playerId = buf.readInt(); + this.position = new Vec3d(buf.readDouble(), buf.readDouble(), buf.readDouble()); + this.type = CalloutType.byId(buf.readByte()); + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/network/packets/s2c/PlayerKnockedDownS2CPacket.java b/src/main/java/ru/octol1ttle/knockdowns/common/network/packets/s2c/PlayerKnockedDownS2CPacket.java new file mode 100644 index 0000000..017bb4a --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/common/network/packets/s2c/PlayerKnockedDownS2CPacket.java @@ -0,0 +1,36 @@ +package ru.octol1ttle.knockdowns.common.network.packets.s2c; + +import io.netty.buffer.ByteBuf; +import net.minecraft.util.math.Vec3d; +import net.minecraftforge.fml.common.network.simpleimpl.IMessage; + +public class PlayerKnockedDownS2CPacket implements IMessage { + public PlayerKnockedDownS2CPacket() { + } + + public int playerId; + public int dimensionId; + public Vec3d position; + + public PlayerKnockedDownS2CPacket(int playerId, int dimensionId, Vec3d position) { + this.playerId = playerId; + this.dimensionId = dimensionId; + this.position = position; + } + + @Override + public void toBytes(ByteBuf buf) { + buf.writeInt(this.playerId); + buf.writeInt(this.dimensionId); + buf.writeDouble(this.position.x); + buf.writeDouble(this.position.y); + buf.writeDouble(this.position.z); + } + + @Override + public void fromBytes(ByteBuf buf) { + this.playerId = buf.readInt(); + this.dimensionId = buf.readInt(); + this.position = new Vec3d(buf.readDouble(), buf.readDouble(), buf.readDouble()); + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/network/packets/s2c/SynchronizePlayerDataS2CPacket.java b/src/main/java/ru/octol1ttle/knockdowns/common/network/packets/s2c/SynchronizePlayerDataS2CPacket.java new file mode 100644 index 0000000..c5846ab --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/common/network/packets/s2c/SynchronizePlayerDataS2CPacket.java @@ -0,0 +1,85 @@ +package ru.octol1ttle.knockdowns.common.network.packets.s2c; + +import io.netty.buffer.ByteBuf; +import net.minecraftforge.fml.common.network.simpleimpl.IMessage; + +public class SynchronizePlayerDataS2CPacket { + public static class KnockedDown implements IMessage { + public KnockedDown() { + } + + public int playerId; + public boolean knockedDown; + + public KnockedDown(int playerId, boolean knockedDown) { + this.playerId = playerId; + this.knockedDown = knockedDown; + } + + @Override + public void toBytes(ByteBuf buf) { + buf.writeInt(this.playerId); + buf.writeBoolean(this.knockedDown); + } + + @Override + public void fromBytes(ByteBuf buf) { + this.playerId = buf.readInt(); + this.knockedDown = buf.readBoolean(); + } + } + + public static class ReviveTimeLeft implements IMessage { + public ReviveTimeLeft() { + } + + public int playerId; + public int reviveTimeLeft; + + public ReviveTimeLeft(int playerId, int reviveTimeLeft) { + this.playerId = playerId; + this.reviveTimeLeft = reviveTimeLeft; + } + + @Override + public void toBytes(ByteBuf buf) { + buf.writeInt(this.playerId); + buf.writeInt(this.reviveTimeLeft); + } + + @Override + public void fromBytes(ByteBuf buf) { + this.playerId = buf.readInt(); + this.reviveTimeLeft = buf.readInt(); + } + } + + public static class Full implements IMessage { + public Full() { + } + + public int playerId; + public boolean knockedDown; + public int reviveTimeLeft; + + public Full(int playerId, boolean knockedDown, int reviveTimeLeft) { + this.playerId = playerId; + this.knockedDown = knockedDown; + this.reviveTimeLeft = reviveTimeLeft; + } + + @Override + public void toBytes(ByteBuf buf) { + buf.writeInt(this.playerId); + buf.writeBoolean(this.knockedDown); + buf.writeInt(this.reviveTimeLeft); + } + + @Override + public void fromBytes(ByteBuf buf) { + this.playerId = buf.readInt(); + this.knockedDown = buf.readBoolean(); + this.reviveTimeLeft = buf.readInt(); + } + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/network/packets/s2c/SynchronizeReviversS2CPacket.java b/src/main/java/ru/octol1ttle/knockdowns/common/network/packets/s2c/SynchronizeReviversS2CPacket.java new file mode 100644 index 0000000..480c701 --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/common/network/packets/s2c/SynchronizeReviversS2CPacket.java @@ -0,0 +1,56 @@ +package ru.octol1ttle.knockdowns.common.network.packets.s2c; + +import io.netty.buffer.ByteBuf; +import net.minecraftforge.fml.common.network.simpleimpl.IMessage; + +public class SynchronizeReviversS2CPacket { + public static class Add implements IMessage { + public Add() { + } + + public int knockedId; + public int reviverId; + + public Add(int knockedId, int reviverId) { + this.knockedId = knockedId; + this.reviverId = reviverId; + } + + @Override + public void toBytes(ByteBuf buf) { + buf.writeInt(this.knockedId); + buf.writeInt(this.reviverId); + } + + @Override + public void fromBytes(ByteBuf buf) { + this.knockedId = buf.readInt(); + this.reviverId = buf.readInt(); + } + } + + public static class Remove implements IMessage { + public Remove() { + } + + public int knockedId; + public int reviverId; + + public Remove(int knockedId, int reviverId) { + this.knockedId = knockedId; + this.reviverId = reviverId; + } + + @Override + public void toBytes(ByteBuf buf) { + buf.writeInt(this.knockedId); + buf.writeInt(this.reviverId); + } + + @Override + public void fromBytes(ByteBuf buf) { + this.knockedId = buf.readInt(); + this.reviverId = buf.readInt(); + } + } +} diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/registry/KnockdownsSoundEvents.java b/src/main/java/ru/octol1ttle/knockdowns/common/registry/KnockdownsSoundEvents.java new file mode 100644 index 0000000..c90437d --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/common/registry/KnockdownsSoundEvents.java @@ -0,0 +1,17 @@ +package ru.octol1ttle.knockdowns.common.registry; + +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.SoundEvent; +import ru.octol1ttle.knockdowns.Tags; + +public class KnockdownsSoundEvents { + public static final SoundEvent CALLOUT; + public static final SoundEvent KNOCKED_DOWN; + static { + ResourceLocation callout = new ResourceLocation(Tags.MOD_ID, "callout"); + CALLOUT = new SoundEvent(callout).setRegistryName(callout); + + ResourceLocation knockedDown = new ResourceLocation(Tags.MOD_ID, "knocked_down"); + KNOCKED_DOWN = new SoundEvent(knockedDown).setRegistryName(knockedDown); + } +} diff --git a/src/main/resources/assets/knockdowns/lang/ru_ru.lang b/src/main/resources/assets/knockdowns/lang/ru_ru.lang new file mode 100644 index 0000000..a45d2e2 --- /dev/null +++ b/src/main/resources/assets/knockdowns/lang/ru_ru.lang @@ -0,0 +1,64 @@ +knockdowns.key.category=Knockdowns +knockdowns.key.callout.danger="Опасность!" +knockdowns.key.callout.booyah="Йо-хо!" +knockdowns.key.callout.this_way_help="Сюда!" ("SOS!" когда тяжело ранен) +knockdowns.key.callout.ouch="Непруха!" + +knockdowns.callout.danger=Опасность! +knockdowns.callout.booyah=Йо-хо! +knockdowns.callout.this_way=Сюда! +knockdowns.callout.ouch=Непруха! +knockdowns.callout.help=SOS! + +knockdowns.subtitles.callout=Игрок зовёт союзников +knockdowns.subtitles.knocked_down=Игрок тяжело ранен + +knockdown.attack.anvil=%1$s тяжело ранен упавшей наковальней +knockdown.attack.arrow=%1$s тяжело ранен стрелой %2$s +knockdown.attack.arrow.item=%1$s тяжело ранен стрелой %2$s с помощью %3$s +knockdown.attack.cactus=%1$s исколот до тяжелого ранения +knockdown.attack.cactus.player=%1$s тяжело ранен кактусом, спасаясь от %2$s +knockdown.attack.cramming=%1$s расплющен до тяжелого ранения +knockdown.attack.dragonBreath=%1$s тяжело ранен в драконьем дыхании +knockdown.attack.drown=%1$s тяжело ранен от нехватки воздуха +knockdown.attack.drown.player=%1$s тяжело ранен от нехватки воздуха, спасаясь от %2$s +knockdown.attack.explosion=%1$s тяжело ранен взрывом +knockdown.attack.explosion.player=%1$s был тяжело ранен взрывом %2$s +knockdown.attack.fall=%1$s разбился до тяжелого ранения +knockdown.attack.fallingBlock=%1$s тяжело ранен упавшим блоком +knockdown.attack.fireball=%1$s тяжело ранен файерболом %2$s +knockdown.attack.fireball.item=%1$s тяжело ранен файерболом %2$s с помощью %3$s +knockdown.attack.fireworks=%1$s с треском тяжело ранен +knockdown.attack.flyIntoWall=%1$s преобразовал кинетическую энергию в тяжелое ранение +knockdown.attack.generic=%1$s тяжело ранен +knockdown.attack.hotFloor=%1$s тяжело ранен, обнаружив под ногами лаву +knockdown.attack.hotFloor.player=%1$s зашёл в опасную зону тяжелого ранения из-за %2$s +knockdown.attack.inFire=%1$s сгорел до тяжелого ранения +knockdown.attack.inFire.player=%1$s тяжело ранен в огне, борясь с %2$s +knockdown.attack.inWall=%1$s погребён до тяжелого ранения +knockdown.attack.indirectMagic=%1$s был тяжело ранен %2$s с помощью магии +knockdown.attack.indirectMagic.item=%1$s был тяжело ранен %2$s с помощью %3$s +knockdown.attack.lava=%1$s решил получить тяжелое ранение в лаве +knockdown.attack.lava.player=%1$s получил тяжелое ранение от лавы, убегая от %2$s +knockdown.attack.lightningBolt=%1$s был тяжело ранен поражением молнией +knockdown.attack.magic=%1$s был тяжело ранен магией +knockdown.attack.mob=%1$s был тяжело ранен %2$s +knockdown.attack.onFire=%1$s сгорел до тяжелого ранения +knockdown.attack.onFire.player=%1$s был сожжён до тяжелого ранения, пока боролся с %2$s +knockdown.attack.outOfWorld=%1$s тяжело ранен отсутствием земли под собой +knockdown.attack.player=%1$s был тяжело ранен %2$s +knockdown.attack.player.item=%1$s был тяжело ранен %2$s с помощью %3$s +knockdown.attack.starve=%1$s тяжело ранен от голода +knockdown.attack.thorns=%1$s был тяжело ранен, пытаясь навредить %2$s +knockdown.attack.thrown=%1$s был избит до тяжелого ранения %2$s +knockdown.attack.thrown.item=%1$s был избит до тяжелого ранения %2$s с помощью %3$s +knockdown.attack.wither=%1$s тяжело ранен иссушением +knockdown.fell.accident.generic=%1$s разбился до тяжелого ранения +knockdown.fell.accident.ladder=%1$s свалился с лестницы и был тяжело ранен +knockdown.fell.accident.vines=%1$s сорвался с лианы и был тяжело ранен +knockdown.fell.accident.water=%1$s выпал из воды и был тяжело ранен +knockdown.fell.assist=%1$s свалился и был тяжело ранен благодаря %2$s +knockdown.fell.assist.item=%1$s был обречён на тяжелое ранение %2$s с помощью %3$s +knockdown.fell.finish=%1$s упал с высоты и был тяжело ранен %2$s +knockdown.fell.finish.item=%1$s упал с высоты и был тяжело ранен %2$s с помощью %3$s +knockdown.fell.killer=%1$s был обречён на тяжелое ранение diff --git a/src/main/resources/mixins.modid.json b/src/main/resources/mixins.knockdowns.json similarity index 58% rename from src/main/resources/mixins.modid.json rename to src/main/resources/mixins.knockdowns.json index ffbbdee..7e58f0a 100644 --- a/src/main/resources/mixins.modid.json +++ b/src/main/resources/mixins.knockdowns.json @@ -1,11 +1,11 @@ { - "package": "", + "package": "ru.octol1ttle.knockdowns.common.mixins", "required": true, "refmap": "${mixin_refmap}", "target": "@env(DEFAULT)", "minVersion": "0.8.5", "compatibilityLevel": "JAVA_8", - "mixins": [], + "mixins": [ "EntityLivingBaseMixin", "EntityPlayerMixin" ], "server": [], "client": [] -} \ No newline at end of file +} From 5fee6404b0af85f5abde01486901f7d287d6e910 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 15 Jul 2024 11:25:37 +0500 Subject: [PATCH 09/10] fixup: thanks git --- .../knockdowns/client/ClientProxy.java | 2 + .../common/KnockdownsCommonEventListener.java | 2 + .../knockdowns/common/KnockdownsMod.java | 1 + .../knockdowns/common/KnockdownsUtils.java | 2 +- .../assets/knockdowns/lang/en_us.lang | 65 ++++++++++++++++++ .../resources/assets/knockdowns/sounds.json | 18 +++++ .../assets/knockdowns/sounds/callout.ogg | Bin 0 -> 35645 bytes .../assets/knockdowns/sounds/knocked_down.ogg | Bin 0 -> 19188 bytes .../knockdowns/textures/gui/down_arrow.png | Bin 0 -> 5557 bytes .../knockdowns/textures/gui/knocked_icon.png | Bin 0 -> 172 bytes .../knockdowns/textures/gui/left_arrow.png | Bin 0 -> 5405 bytes .../knockdowns/textures/gui/right_arrow.png | Bin 0 -> 5401 bytes .../knockdowns/textures/gui/up_arrow.png | Bin 0 -> 5714 bytes 13 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/assets/knockdowns/lang/en_us.lang create mode 100755 src/main/resources/assets/knockdowns/sounds.json create mode 100644 src/main/resources/assets/knockdowns/sounds/callout.ogg create mode 100755 src/main/resources/assets/knockdowns/sounds/knocked_down.ogg create mode 100644 src/main/resources/assets/knockdowns/textures/gui/down_arrow.png create mode 100755 src/main/resources/assets/knockdowns/textures/gui/knocked_icon.png create mode 100644 src/main/resources/assets/knockdowns/textures/gui/left_arrow.png create mode 100644 src/main/resources/assets/knockdowns/textures/gui/right_arrow.png create mode 100644 src/main/resources/assets/knockdowns/textures/gui/up_arrow.png diff --git a/src/main/java/ru/octol1ttle/knockdowns/client/ClientProxy.java b/src/main/java/ru/octol1ttle/knockdowns/client/ClientProxy.java index 1a0fbf1..bb97d96 100644 --- a/src/main/java/ru/octol1ttle/knockdowns/client/ClientProxy.java +++ b/src/main/java/ru/octol1ttle/knockdowns/client/ClientProxy.java @@ -5,11 +5,13 @@ import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; import ru.octol1ttle.knockdowns.client.event.KnockdownsKeyListener; import ru.octol1ttle.knockdowns.common.IClientProxy; +import ru.octol1ttle.knockdowns.common.KnockdownsMod; @SideOnly(Side.CLIENT) public class ClientProxy implements IClientProxy { @Override public void onFMLInit(FMLInitializationEvent event) { + KnockdownsMod.LOGGER.info("Registering key bindings"); KnockdownsKeyListener.registerKeyBindings(); } } diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsCommonEventListener.java b/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsCommonEventListener.java index 1b8b252..5328da7 100644 --- a/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsCommonEventListener.java +++ b/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsCommonEventListener.java @@ -46,7 +46,9 @@ import static ru.octol1ttle.knockdowns.common.KnockdownsUtils.resetKnockedState; @Mod.EventBusSubscriber(modid = Tags.MOD_ID) public class KnockdownsCommonEventListener { public static void onFMLInit(FMLInitializationEvent event) { + KnockdownsMod.LOGGER.info("Registering network packets"); KnockdownsNetwork.registerPackets(); + KnockdownsMod.LOGGER.info("Registering capability"); KnockdownsCapability.register(); } diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsMod.java b/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsMod.java index d9e27c0..49e365c 100644 --- a/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsMod.java +++ b/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsMod.java @@ -15,6 +15,7 @@ public class KnockdownsMod { @Mod.EventHandler public void onFMLInit(FMLInitializationEvent event) { + LOGGER.info("Initializing"); clientProxy.onFMLInit(event); KnockdownsCommonEventListener.onFMLInit(event); } diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsUtils.java b/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsUtils.java index 207a0ab..d87763f 100644 --- a/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsUtils.java +++ b/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsUtils.java @@ -24,7 +24,7 @@ public class KnockdownsUtils { return false; } } - return false; + return true; } public static void resetKnockedState(EntityPlayerMP player, IKnockdownsPlayerData data) { diff --git a/src/main/resources/assets/knockdowns/lang/en_us.lang b/src/main/resources/assets/knockdowns/lang/en_us.lang new file mode 100644 index 0000000..a9042b8 --- /dev/null +++ b/src/main/resources/assets/knockdowns/lang/en_us.lang @@ -0,0 +1,65 @@ +knockdowns.key.category=Knockdowns +knockdowns.key.callout.danger=Call out "Danger!" +knockdowns.key.callout.booyah=Call out "Booyah!" +knockdowns.key.callout.this_way_help=Call out "This way!" ("Help!" when knocked down) +knockdowns.key.callout.ouch=Call out "Ouch..." + +knockdowns.callout.danger=Danger! +knockdowns.callout.booyah=Booyah! +knockdowns.callout.this_way=This way! +knockdowns.callout.ouch=Ouch... +knockdowns.callout.help=Help! + +knockdowns.subtitles.callout=Player calls out +knockdowns.subtitles.knocked_down=Player knocked down + +knockdown.fell.accident.ladder=%1$s was knocked down by falling off a ladder +knockdown.fell.accident.vines=%1$s was knocked down by falling off some vines +knockdown.fell.accident.water=%1$s was knocked down by falling out of the water +knockdown.fell.accident.generic=%1$s was knocked down by a fall +knockdown.fell.killer=%1$s was doomed to get knocked down +knockdown.fell.assist=%1$s was doomed to get knocked down by %2$s +knockdown.fell.assist.item=%1$s was doomed to get knocked down by %2$s using %3$s +knockdown.fell.finish=%1$s fell too far and was knocked down by %2$s +knockdown.fell.finish.item=%1$s fell too far and was knocked down by %2$s using %3$s + +knockdown.attack.lightningBolt=%1$s was knocked down by lightning +knockdown.attack.inFire=%1$s was knocked down by the fire below +knockdown.attack.inFire.player=%1$s was knocked down by the fire below whilst fighting %2$s +knockdown.attack.onFire=%1$s was knocked down by fire +knockdown.attack.onFire.player=%1$s was knocked down by fire whilst fighting %2$s +knockdown.attack.lava=%1$s was knocked down by lava +knockdown.attack.lava.player=%1$s was knocked down by lava to escape %2$s +knockdown.attack.hotFloor=%1$s was knocked down by the floor +knockdown.attack.hotFloor.player=%1$s was knocked down by the floor due to %2$s +knockdown.attack.inWall=%1$s was knocked down by a wall +knockdown.attack.cramming=%1$s was knocked down by social anxiety +knockdown.attack.drown=%1$s was knocked down by lack of air +knockdown.attack.drown.player=%1$s was knocked down by lack of air whilst trying to escape %2$s +knockdown.attack.starve=%1$s was knocked down by hunger +knockdown.attack.cactus=%1$s was knocked down by a cactus +knockdown.attack.cactus.player=%1$s was knocked down by a cactus whilst trying to escape %2$s +knockdown.attack.generic=%1$s was knocked down +knockdown.attack.explosion=%1$s was knocked down by an explosion +knockdown.attack.explosion.player=%1$s was knocked down by an explosion due to %2$s +knockdown.attack.magic=%1$s was knocked down by magic +knockdown.attack.wither=%1$s was knocked down by withering +knockdown.attack.anvil=%1$s was knocked down by a falling anvil +knockdown.attack.fallingBlock=%1$s was knocked down by a falling block +knockdown.attack.mob=%1$s was knocked down by %2$s +knockdown.attack.player=%1$s was knocked down by %2$s +knockdown.attack.player.item=%1$s was knocked down by %2$s using %3$s +knockdown.attack.arrow=%1$s was knocked down by an arrow shot by %2$s +knockdown.attack.arrow.item=%1$s was knocked down by an arrow shot by %2$s using %3$s +knockdown.attack.fireball=%1$s was knocked down by a fireball shot by %2$s +knockdown.attack.fireball.item=%1$s was knocked down by a fireball shot by %2$s using %3$s +knockdown.attack.thrown=%1$s was knocked down after being pummeled by %2$s +knockdown.attack.thrown.item=%1$s was knocked down after being pummeled by %2$s using %3$s +knockdown.attack.indirectMagic=%1$s was knocked down by %2$s using magic +knockdown.attack.indirectMagic.item=%1$s was knocked down by %2$s using %3$s +knockdown.attack.thorns=%1$s was knocked down trying to hurt %2$s +knockdown.attack.fall=%1$s was knocked down by the ground below +knockdown.attack.outOfWorld=%1$s was knocked down by the void +knockdown.attack.dragonBreath=%1$s was knocked down by dragon breath +knockdown.attack.flyIntoWall=%1$s was knocked down by physics +knockdown.attack.fireworks=%1$s was knocked down by a firework diff --git a/src/main/resources/assets/knockdowns/sounds.json b/src/main/resources/assets/knockdowns/sounds.json new file mode 100755 index 0000000..fabd199 --- /dev/null +++ b/src/main/resources/assets/knockdowns/sounds.json @@ -0,0 +1,18 @@ +{ + "callout": { + "subtitle": "knockdowns.subtitles.callout", + "sounds": [ + { + "name": "knockdowns:callout" + } + ] + }, + "knocked_down": { + "subtitle": "knockdowns.subtitles.knocked_down", + "sounds": [ + { + "name": "knockdowns:knocked_down" + } + ] + } +} diff --git a/src/main/resources/assets/knockdowns/sounds/callout.ogg b/src/main/resources/assets/knockdowns/sounds/callout.ogg new file mode 100644 index 0000000000000000000000000000000000000000..93c71daa0283d3e82504e9a0c1df8f0866f7ac87 GIT binary patch literal 35645 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 WuXNQOkGHN{Szh#@bZ)NhY$gEt2xPPX literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/knockdowns/sounds/knocked_down.ogg b/src/main/resources/assets/knockdowns/sounds/knocked_down.ogg new file mode 100755 index 0000000000000000000000000000000000000000..742eb8463a1083da3ea896b2884d2e752e754107 GIT binary patch literal 19188 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 CMAoeU literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/knockdowns/textures/gui/down_arrow.png b/src/main/resources/assets/knockdowns/textures/gui/down_arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..44a550d0608aff556b3da3cf151fc2f18b2f2842 GIT binary patch literal 5557 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/knockdowns/textures/gui/knocked_icon.png b/src/main/resources/assets/knockdowns/textures/gui/knocked_icon.png new file mode 100755 index 0000000000000000000000000000000000000000..d61f41cd2ed91db2872f6bce853183f5b313d350 GIT binary patch literal 172 zcmeAS@N?(olHy`uVBq!ia0y~yU=RXfCT0c(2JHoDM;I6wcmjMvT>t<7Z^_J@!_2&g znR%jB*$M^*2F8*gzhDN3XE)Lq7#RFKT^vIy;*t{_n0&Y;OeAMWI7uvH@bpt-XIob9 zJMVtu8`kJ|3@62TSRFon-_L9i$u^Z;xZz0)k0_g(+V0pn4&T22=XEG%l5m-Lwv~zD Zcevok>02bD85kHCJYD@<);T3K0RY+~GU@;T literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/knockdowns/textures/gui/left_arrow.png b/src/main/resources/assets/knockdowns/textures/gui/left_arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..4badc50c1e0495f9f4f36f148d25c7071ca36d9d GIT binary patch literal 5405 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/knockdowns/textures/gui/up_arrow.png b/src/main/resources/assets/knockdowns/textures/gui/up_arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..56e803bc7ea9dd7e544c1197ef24b8c28aa8b021 GIT binary patch literal 5714 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 aPD%F_xSi9K&wmdZ`}1`5b6Mw<&;$SwOr4Ve literal 0 HcmV?d00001 From ac41344a3c9aad460c2c9e0acdd9f47da05e0024 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 15 Jul 2024 13:18:14 +0500 Subject: [PATCH 10/10] fix all of the bullshit!! Signed-off-by: Octol1ttle --- LICENSE | 18 +-- .../knockdowns/client/ClientProxy.java | 96 ++++++++++++ .../event/KnockdownsClientEventListener.java | 6 +- .../knockdowns/client/gui/ReviveGui.java | 10 +- .../KnockdownsClientPacketHandler.java | 145 ------------------ .../knockdowns/common/IClientProxy.java | 8 + .../common/KnockdownsCommonEventListener.java | 72 +++++---- .../knockdowns/common/KnockdownsUtils.java | 2 +- .../knockdowns/common/ReviverTracker.java | 32 ---- .../common/data/IKnockdownsPlayerData.java | 3 + .../common/data/KnockdownsPlayerData.java | 8 + .../common/network/KnockdownsNetwork.java | 18 ++- .../KnockdownsServerPacketHandler.java | 16 +- .../common/util/UniqueOnlyList.java | 23 +++ 14 files changed, 214 insertions(+), 243 deletions(-) delete mode 100644 src/main/java/ru/octol1ttle/knockdowns/client/network/KnockdownsClientPacketHandler.java delete mode 100644 src/main/java/ru/octol1ttle/knockdowns/common/ReviverTracker.java create mode 100644 src/main/java/ru/octol1ttle/knockdowns/common/util/UniqueOnlyList.java diff --git a/LICENSE b/LICENSE index fefacc2..99a3357 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,11 @@ -MIT License +All Rights Reserved -Copyright (c) 2022 CleanroomMC - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +Copyright (c) 2024 Octol1ttle THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/main/java/ru/octol1ttle/knockdowns/client/ClientProxy.java b/src/main/java/ru/octol1ttle/knockdowns/client/ClientProxy.java index bb97d96..80a6603 100644 --- a/src/main/java/ru/octol1ttle/knockdowns/client/ClientProxy.java +++ b/src/main/java/ru/octol1ttle/knockdowns/client/ClientProxy.java @@ -1,17 +1,113 @@ package ru.octol1ttle.knockdowns.client; +import net.minecraft.client.Minecraft; +import net.minecraft.entity.player.EntityPlayer; import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.network.simpleimpl.IMessage; import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; +import ru.octol1ttle.knockdowns.client.communication.CalloutManager; +import ru.octol1ttle.knockdowns.client.communication.KnockedNotificationManager; import ru.octol1ttle.knockdowns.client.event.KnockdownsKeyListener; +import ru.octol1ttle.knockdowns.client.util.DirectionalCallSound; import ru.octol1ttle.knockdowns.common.IClientProxy; import ru.octol1ttle.knockdowns.common.KnockdownsMod; +import ru.octol1ttle.knockdowns.common.data.IKnockdownsPlayerData; +import ru.octol1ttle.knockdowns.common.network.packets.s2c.PlayerCalloutS2CPacket; +import ru.octol1ttle.knockdowns.common.network.packets.s2c.PlayerKnockedDownS2CPacket; +import ru.octol1ttle.knockdowns.common.network.packets.s2c.SynchronizePlayerDataS2CPacket; +import ru.octol1ttle.knockdowns.common.network.packets.s2c.SynchronizeReviversS2CPacket; +import ru.octol1ttle.knockdowns.common.registry.KnockdownsSoundEvents; + +import static ru.octol1ttle.knockdowns.common.KnockdownsUtils.INITIAL_REVIVE_TIME_LEFT; @SideOnly(Side.CLIENT) public class ClientProxy implements IClientProxy { + private static final Minecraft client = Minecraft.getMinecraft(); + @Override public void onFMLInit(FMLInitializationEvent event) { KnockdownsMod.LOGGER.info("Registering key bindings"); KnockdownsKeyListener.registerKeyBindings(); } + + @Override + public T handleMessage(IMessage message) { + if (message instanceof PlayerCalloutS2CPacket) { + PlayerCalloutS2CPacket packet = (PlayerCalloutS2CPacket) message; + client.addScheduledTask(() -> { + if (CalloutManager.addOrUpdateCallout(packet)) { + CalloutManager.playCalloutSound(packet); + } + }); + } else if (message instanceof PlayerKnockedDownS2CPacket) { + PlayerKnockedDownS2CPacket packet = (PlayerKnockedDownS2CPacket) message; + client.addScheduledTask(() -> { + EntityPlayer entity = (EntityPlayer) client.world.getEntityByID(packet.playerId); + if (entity != null) { + IKnockdownsPlayerData data = IKnockdownsPlayerData.get(entity); + data.setKnockedDown(true); + data.setReviveTimeLeft(INITIAL_REVIVE_TIME_LEFT); + data.getRevivers().clear(); + } + + if (client.player.dimension == packet.dimensionId) { + client.getSoundHandler().playSound(new DirectionalCallSound(KnockdownsSoundEvents.KNOCKED_DOWN, entity, packet.position)); + KnockedNotificationManager.addKnockedNotification(packet.playerId, packet.position); + } + }); + } else if (message instanceof SynchronizePlayerDataS2CPacket.KnockedDown) { + SynchronizePlayerDataS2CPacket.KnockedDown packet = (SynchronizePlayerDataS2CPacket.KnockedDown) message; + client.addScheduledTask(() -> { + EntityPlayer entity = (EntityPlayer) client.world.getEntityByID(packet.playerId); + if (entity != null) { + IKnockdownsPlayerData data = IKnockdownsPlayerData.get(entity); + data.setKnockedDown(packet.knockedDown); + data.setReviveTimeLeft(INITIAL_REVIVE_TIME_LEFT); + data.getRevivers().clear(); + } + }); + } else if (message instanceof SynchronizePlayerDataS2CPacket.ReviveTimeLeft) { + SynchronizePlayerDataS2CPacket.ReviveTimeLeft packet = (SynchronizePlayerDataS2CPacket.ReviveTimeLeft) message; + client.addScheduledTask(() -> { + EntityPlayer entity = (EntityPlayer) client.world.getEntityByID(packet.playerId); + if (entity != null) { + IKnockdownsPlayerData data = IKnockdownsPlayerData.get(entity); + data.setReviveTimeLeft(packet.reviveTimeLeft); + } + }); + } else if (message instanceof SynchronizePlayerDataS2CPacket.Full) { + SynchronizePlayerDataS2CPacket.Full packet = (SynchronizePlayerDataS2CPacket.Full) message; + client.addScheduledTask(() -> { + EntityPlayer entity = (EntityPlayer) client.world.getEntityByID(packet.playerId); + if (entity != null) { + IKnockdownsPlayerData data = IKnockdownsPlayerData.get(entity); + data.setKnockedDown(packet.knockedDown); + data.setReviveTimeLeft(packet.reviveTimeLeft); + } + }); + } else if (message instanceof SynchronizeReviversS2CPacket.Add) { + SynchronizeReviversS2CPacket.Add packet = (SynchronizeReviversS2CPacket.Add) message; + client.addScheduledTask(() -> { + EntityPlayer knocked = (EntityPlayer) client.world.getEntityByID(packet.knockedId); + EntityPlayer reviver = (EntityPlayer) client.world.getEntityByID(packet.reviverId); + if (knocked != null && reviver != null) { + IKnockdownsPlayerData.get(knocked).getRevivers().add(reviver); + } + }); + } else if (message instanceof SynchronizeReviversS2CPacket.Remove) { + SynchronizeReviversS2CPacket.Remove packet = (SynchronizeReviversS2CPacket.Remove) message; + client.addScheduledTask(() -> { + EntityPlayer knocked = (EntityPlayer) client.world.getEntityByID(packet.knockedId); + EntityPlayer reviver = (EntityPlayer) client.world.getEntityByID(packet.reviverId); + if (knocked != null && reviver != null) { + IKnockdownsPlayerData.get(knocked).getRevivers().remove(reviver); + } + }); + } else { + throw new IllegalStateException("Unknown packet received on the client: " + message.getClass().getName()); + } + + return null; + } } diff --git a/src/main/java/ru/octol1ttle/knockdowns/client/event/KnockdownsClientEventListener.java b/src/main/java/ru/octol1ttle/knockdowns/client/event/KnockdownsClientEventListener.java index ebf5b56..0aa8db7 100644 --- a/src/main/java/ru/octol1ttle/knockdowns/client/event/KnockdownsClientEventListener.java +++ b/src/main/java/ru/octol1ttle/knockdowns/client/event/KnockdownsClientEventListener.java @@ -17,7 +17,7 @@ import ru.octol1ttle.knockdowns.client.communication.KnockedNotificationManager; import ru.octol1ttle.knockdowns.client.gui.CommunicationGui; import ru.octol1ttle.knockdowns.client.gui.KnockedNotificationGui; import ru.octol1ttle.knockdowns.client.gui.ReviveGui; -import ru.octol1ttle.knockdowns.common.ReviverTracker; +import ru.octol1ttle.knockdowns.common.data.IKnockdownsPlayerData; import ru.octol1ttle.knockdowns.common.network.KnockdownsNetwork; import ru.octol1ttle.knockdowns.common.network.packets.c2s.CancelReviveC2SPacket; @@ -45,7 +45,7 @@ public class KnockdownsClientEventListener { @SubscribeEvent public static void onPlayerTick(TickEvent.PlayerTickEvent event) { - List revivers = ReviverTracker.getRevivers(event.player); + List revivers = IKnockdownsPlayerData.get(event.player).getRevivers(); if (revivers.contains(client.player) && !event.player.equals(client.pointedEntity)) { KnockdownsNetwork.sendToServer(new CancelReviveC2SPacket()); revivers.remove(client.player); @@ -54,8 +54,8 @@ public class KnockdownsClientEventListener { @SubscribeEvent public static void onRenderWorldLast(RenderWorldLastEvent event) { - communicationGui.renderCallouts(event.getPartialTicks()); notificationGui.renderNotifications(event.getPartialTicks()); + communicationGui.renderCallouts(event.getPartialTicks()); } @SubscribeEvent diff --git a/src/main/java/ru/octol1ttle/knockdowns/client/gui/ReviveGui.java b/src/main/java/ru/octol1ttle/knockdowns/client/gui/ReviveGui.java index e8df745..ad78dbb 100644 --- a/src/main/java/ru/octol1ttle/knockdowns/client/gui/ReviveGui.java +++ b/src/main/java/ru/octol1ttle/knockdowns/client/gui/ReviveGui.java @@ -6,7 +6,6 @@ import net.minecraft.client.gui.ScaledResolution; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.util.text.TextFormatting; import ru.octol1ttle.knockdowns.common.KnockdownsUtils; -import ru.octol1ttle.knockdowns.common.ReviverTracker; import ru.octol1ttle.knockdowns.common.data.IKnockdownsPlayerData; public class ReviveGui extends KnockdownsBaseGui { @@ -15,7 +14,7 @@ public class ReviveGui extends KnockdownsBaseGui { @Override public void render(float partialTicks, ScaledResolution resolution) { EntityPlayer reviving = - client.pointedEntity instanceof EntityPlayer && ReviverTracker.getRevivers((EntityPlayer) client.pointedEntity).contains(client.player) + client.pointedEntity instanceof EntityPlayer && IKnockdownsPlayerData.get((EntityPlayer) client.pointedEntity).getRevivers().contains(client.player) ? (EntityPlayer) client.pointedEntity : client.player; if (IKnockdownsPlayerData.get(reviving).getReviveTimeLeft() == KnockdownsUtils.INITIAL_REVIVE_TIME_LEFT) { @@ -28,7 +27,8 @@ public class ReviveGui extends KnockdownsBaseGui { String timerText = String.format("%.1f", data.getReviveTimeLeft() / 20.0f); float timerX = (resolution.getScaledWidth() - font.getStringWidth(timerText)) * 0.5f; - int reviverCount = ReviverTracker.getRevivers(reviving).size(); + data.getRevivers().removeIf(reviver -> reviver.isDead || !reviver.isEntityAlive() || IKnockdownsPlayerData.get(reviver).isKnockedDown()); + int reviverCount = data.getRevivers().size(); TextFormatting color; if (reviverCount == 0) { color = TextFormatting.RED; @@ -41,7 +41,7 @@ public class ReviveGui extends KnockdownsBaseGui { String reviverCountText = "x" + reviverCount; float reviveCountX = (resolution.getScaledWidth() - font.getStringWidth(reviverCountText)) * 0.5f; - font.drawStringWithShadow(color + timerText, timerX, resolution.getScaledHeight() * 0.5f + 5, 553648127); - font.drawStringWithShadow(color + reviverCountText, reviveCountX, resolution.getScaledHeight() * 0.5f + 14, 553648127); + font.drawStringWithShadow(color + timerText, timerX, resolution.getScaledHeight() * 0.5f + 5, 0xFFFFFF); + font.drawStringWithShadow(color + reviverCountText, reviveCountX, resolution.getScaledHeight() * 0.5f + 14, 0xFFFFFF); } } diff --git a/src/main/java/ru/octol1ttle/knockdowns/client/network/KnockdownsClientPacketHandler.java b/src/main/java/ru/octol1ttle/knockdowns/client/network/KnockdownsClientPacketHandler.java deleted file mode 100644 index 4770827..0000000 --- a/src/main/java/ru/octol1ttle/knockdowns/client/network/KnockdownsClientPacketHandler.java +++ /dev/null @@ -1,145 +0,0 @@ -package ru.octol1ttle.knockdowns.client.network; - -import net.minecraft.client.Minecraft; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraftforge.fml.common.network.simpleimpl.IMessage; -import net.minecraftforge.fml.common.network.simpleimpl.IMessageHandler; -import net.minecraftforge.fml.common.network.simpleimpl.MessageContext; -import net.minecraftforge.fml.relauncher.Side; -import net.minecraftforge.fml.relauncher.SideOnly; -import ru.octol1ttle.knockdowns.client.communication.CalloutManager; -import ru.octol1ttle.knockdowns.client.communication.KnockedNotificationManager; -import ru.octol1ttle.knockdowns.client.util.DirectionalCallSound; -import ru.octol1ttle.knockdowns.common.ReviverTracker; -import ru.octol1ttle.knockdowns.common.data.IKnockdownsPlayerData; -import ru.octol1ttle.knockdowns.common.network.packets.s2c.PlayerCalloutS2CPacket; -import ru.octol1ttle.knockdowns.common.network.packets.s2c.PlayerKnockedDownS2CPacket; -import ru.octol1ttle.knockdowns.common.network.packets.s2c.SynchronizePlayerDataS2CPacket; -import ru.octol1ttle.knockdowns.common.network.packets.s2c.SynchronizeReviversS2CPacket; -import ru.octol1ttle.knockdowns.common.registry.KnockdownsSoundEvents; - -import static ru.octol1ttle.knockdowns.common.KnockdownsUtils.INITIAL_REVIVE_TIME_LEFT; - -public class KnockdownsClientPacketHandler { - public static class Callout implements IMessageHandler { - private static final Minecraft client = Minecraft.getMinecraft(); - - @SideOnly(Side.CLIENT) - @Override - public IMessage onMessage(PlayerCalloutS2CPacket message, MessageContext ctx) { - client.addScheduledTask(() -> { - if (CalloutManager.addOrUpdateCallout(message)) { - CalloutManager.playCalloutSound(message); - } - }); - return null; - } - } - - public static class PlayerKnockedDown implements IMessageHandler { - private static final Minecraft client = Minecraft.getMinecraft(); - - @SideOnly(Side.CLIENT) - @Override - public IMessage onMessage(PlayerKnockedDownS2CPacket message, MessageContext ctx) { - client.addScheduledTask(() -> { - EntityPlayer entity = (EntityPlayer) client.world.getEntityByID(message.playerId); - if (entity != null) { - IKnockdownsPlayerData data = IKnockdownsPlayerData.get(entity); - data.setKnockedDown(true); - data.setReviveTimeLeft(INITIAL_REVIVE_TIME_LEFT); - } - - if (client.player.dimension == message.dimensionId) { - client.getSoundHandler().playSound(new DirectionalCallSound(KnockdownsSoundEvents.KNOCKED_DOWN, entity, message.position)); - KnockedNotificationManager.addKnockedNotification(message.playerId, message.position); - } - }); - return null; - } - } - - public static class SynchronizePlayerData { - private static final Minecraft client = Minecraft.getMinecraft(); - - public static class KnockedDown implements IMessageHandler { - @SideOnly(Side.CLIENT) - @Override - public IMessage onMessage(SynchronizePlayerDataS2CPacket.KnockedDown message, MessageContext ctx) { - client.addScheduledTask(() -> { - EntityPlayer entity = (EntityPlayer) client.world.getEntityByID(message.playerId); - if (entity != null) { - IKnockdownsPlayerData data = IKnockdownsPlayerData.get(entity); - data.setKnockedDown(message.knockedDown); - } - }); - return null; - } - } - - public static class ReviveTimeLeft implements IMessageHandler { - @SideOnly(Side.CLIENT) - @Override - public IMessage onMessage(SynchronizePlayerDataS2CPacket.ReviveTimeLeft message, MessageContext ctx) { - client.addScheduledTask(() -> { - EntityPlayer entity = (EntityPlayer) client.world.getEntityByID(message.playerId); - if (entity != null) { - IKnockdownsPlayerData data = IKnockdownsPlayerData.get(entity); - data.setReviveTimeLeft(message.reviveTimeLeft); - } - }); - return null; - } - } - - public static class Full implements IMessageHandler { - @SideOnly(Side.CLIENT) - @Override - public IMessage onMessage(SynchronizePlayerDataS2CPacket.Full message, MessageContext ctx) { - client.addScheduledTask(() -> { - EntityPlayer entity = (EntityPlayer) client.world.getEntityByID(message.playerId); - if (entity != null) { - IKnockdownsPlayerData data = IKnockdownsPlayerData.get(entity); - data.setKnockedDown(message.knockedDown); - data.setReviveTimeLeft(message.reviveTimeLeft); - } - }); - return null; - } - } - } - - public static class SynchronizeRevivers { - private static final Minecraft client = Minecraft.getMinecraft(); - - public static class Add implements IMessageHandler { - @SideOnly(Side.CLIENT) - @Override - public IMessage onMessage(SynchronizeReviversS2CPacket.Add message, MessageContext ctx) { - client.addScheduledTask(() -> { - EntityPlayer knocked = (EntityPlayer) client.world.getEntityByID(message.knockedId); - EntityPlayer reviver = (EntityPlayer) client.world.getEntityByID(message.reviverId); - if (knocked != null && reviver != null) { - ReviverTracker.startReviving(knocked, reviver); - } - }); - return null; - } - } - - public static class Remove implements IMessageHandler { - @SideOnly(Side.CLIENT) - @Override - public IMessage onMessage(SynchronizeReviversS2CPacket.Remove message, MessageContext ctx) { - client.addScheduledTask(() -> { - EntityPlayer knocked = (EntityPlayer) client.world.getEntityByID(message.knockedId); - EntityPlayer reviver = (EntityPlayer) client.world.getEntityByID(message.reviverId); - if (knocked != null && reviver != null) { - ReviverTracker.stopReviving(knocked, reviver); - } - }); - return null; - } - } - } -} diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/IClientProxy.java b/src/main/java/ru/octol1ttle/knockdowns/common/IClientProxy.java index 448ccd8..d1db4ea 100644 --- a/src/main/java/ru/octol1ttle/knockdowns/common/IClientProxy.java +++ b/src/main/java/ru/octol1ttle/knockdowns/common/IClientProxy.java @@ -1,13 +1,21 @@ package ru.octol1ttle.knockdowns.common; import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.network.simpleimpl.IMessage; public interface IClientProxy { void onFMLInit(FMLInitializationEvent event); + T handleMessage(IMessage message); + class Dummy implements IClientProxy { @Override public void onFMLInit(FMLInitializationEvent event) { } + + @Override + public T handleMessage(IMessage message) { + return null; + } } } diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsCommonEventListener.java b/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsCommonEventListener.java index 5328da7..30ad689 100644 --- a/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsCommonEventListener.java +++ b/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsCommonEventListener.java @@ -1,7 +1,6 @@ package ru.octol1ttle.knockdowns.common; import java.util.List; -import net.minecraft.client.resources.I18n; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLiving; import net.minecraft.entity.player.EntityPlayer; @@ -70,32 +69,33 @@ public class KnockdownsCommonEventListener { if (event.phase == TickEvent.Phase.START || server == null) { return; } - EntityPlayerMP player = (EntityPlayerMP) event.player; - IKnockdownsPlayerData data = IKnockdownsPlayerData.get(player); + EntityPlayerMP knocked = (EntityPlayerMP) event.player; + IKnockdownsPlayerData data = IKnockdownsPlayerData.get(knocked); if (!data.isKnockedDown()) { return; } - if (allPlayersKnocked(server, player)) { - player.attackEntityFrom(DamageSource.GENERIC, player.getMaxHealth()); + if (allPlayersKnocked(server, knocked)) { + knocked.attackEntityFrom(DamageSource.GENERIC, knocked.getMaxHealth()); return; } - List revivers = ReviverTracker.getRevivers(player); + List revivers = data.getRevivers(); + revivers.removeIf(reviver -> reviver.isDead || !reviver.isEntityAlive() || IKnockdownsPlayerData.get(reviver).isKnockedDown()); if (!revivers.isEmpty()) { data.setReviveTimeLeft(data.getReviveTimeLeft() - revivers.size()); KnockdownsNetwork.sendToMultiple( - new SynchronizePlayerDataS2CPacket.ReviveTimeLeft(player.getEntityId(), data.getReviveTimeLeft()), + new SynchronizePlayerDataS2CPacket.ReviveTimeLeft(knocked.getEntityId(), data.getReviveTimeLeft()), revivers, - player + knocked ); if (data.getReviveTimeLeft() <= 0) { - resetKnockedState(player, data); + resetKnockedState(knocked, data); - player.setEntityInvulnerable(false); - player.setHealth(player.getMaxHealth() * 0.3f); - player.setAbsorptionAmount(0.0f); + knocked.setEntityInvulnerable(false); + knocked.setHealth(knocked.getMaxHealth() * 0.3f); + knocked.setAbsorptionAmount(0.0f); } return; } @@ -104,8 +104,8 @@ public class KnockdownsCommonEventListener { data.setReviveTimeLeft(Math.min(INITIAL_REVIVE_TIME_LEFT, oldReviveTimeLeft + 1)); if (data.getReviveTimeLeft() != oldReviveTimeLeft) { KnockdownsNetwork.sendToPlayer( - new SynchronizePlayerDataS2CPacket.ReviveTimeLeft(player.getEntityId(), data.getReviveTimeLeft()), - player + new SynchronizePlayerDataS2CPacket.ReviveTimeLeft(knocked.getEntityId(), data.getReviveTimeLeft()), + knocked ); } @@ -113,8 +113,8 @@ public class KnockdownsCommonEventListener { int period = MathHelper.floor(KNOCKED_HURT_PERIOD * 20); if (data.getTicksKnocked() >= KNOCKED_INVULNERABILITY_TICKS && data.getTicksKnocked() % period == 0) { - player.setEntityInvulnerable(false); - player.attackEntityFrom(DamageSource.GENERIC, player.getMaxHealth() / (KNOCKED_TENACITY / KNOCKED_HURT_PERIOD)); + knocked.setEntityInvulnerable(false); + knocked.attackEntityFrom(DamageSource.GENERIC, knocked.getMaxHealth() / (KNOCKED_TENACITY / KNOCKED_HURT_PERIOD)); } } @@ -131,7 +131,7 @@ public class KnockdownsCommonEventListener { } if (data.isKnockedDown() || allPlayersKnocked(player.getServer(), player)) { - ReviverTracker.clearRevivers(player); + data.getRevivers().clear(); return; } @@ -159,10 +159,9 @@ public class KnockdownsCommonEventListener { TextComponentTranslation deathMessage = (TextComponentTranslation) player.getCombatTracker().getDeathMessage(); String knockdownKey = deathMessage.getKey().replace("death.", "knockdown."); + TextComponentTranslation knockdownTranslation = new TextComponentTranslation(knockdownKey, deathMessage.getFormatArgs()); player.getServer().getPlayerList().sendMessage( - I18n.hasKey(knockdownKey) - ? new TextComponentTranslation(knockdownKey, deathMessage.getFormatArgs()) - : deathMessage, + !knockdownTranslation.getUnformattedComponentText().equals(knockdownKey) ? knockdownTranslation : deathMessage, true ); @@ -212,15 +211,23 @@ public class KnockdownsCommonEventListener { } public static void onPlayerInteraction(PlayerInteractEvent.EntityInteract event) { - if (event.getEntityLiving() instanceof EntityPlayerMP) { - EntityPlayerMP knocked = (EntityPlayerMP) event.getEntityLiving(); - if (IKnockdownsPlayerData.get(knocked).isKnockedDown()) { + if (event.getTarget() instanceof EntityPlayerMP) { + EntityPlayerMP knocked = (EntityPlayerMP) event.getTarget(); + IKnockdownsPlayerData data = IKnockdownsPlayerData.get(knocked); + if (data.isKnockedDown() && !data.getRevivers().contains(event.getEntityPlayer())) { + for (EntityPlayer reviver : data.getRevivers()) { + KnockdownsNetwork.sendToPlayer( + new SynchronizeReviversS2CPacket.Add(event.getTarget().getEntityId(), reviver.getEntityId()), + (EntityPlayerMP) event.getEntityPlayer() + ); + } + + data.getRevivers().add(event.getEntityPlayer()); KnockdownsNetwork.sendToMultiple( - new SynchronizeReviversS2CPacket.Add(event.getEntityLiving().getEntityId(), event.getEntityPlayer().getEntityId()), - ReviverTracker.getRevivers(knocked), + new SynchronizeReviversS2CPacket.Add(event.getTarget().getEntityId(), event.getEntityPlayer().getEntityId()), + data.getRevivers(), knocked ); - ReviverTracker.startReviving(knocked, event.getEntityPlayer()); } } } @@ -236,6 +243,17 @@ public class KnockdownsCommonEventListener { @SubscribeEvent public static void onPlayerLeave(PlayerLoggedOutEvent event) { - ReviverTracker.clearRevivers(event.player); + for (EntityPlayer knocked : event.player.world.playerEntities) { + List revivers = IKnockdownsPlayerData.get(knocked).getRevivers(); + if (revivers.contains(event.player)) { + revivers.remove(event.player); + KnockdownsNetwork.sendToMultiple( + new SynchronizeReviversS2CPacket.Remove(knocked.getEntityId(), event.player.getEntityId()), + revivers, + (EntityPlayerMP) knocked + ); + break; + } + } } } diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsUtils.java b/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsUtils.java index d87763f..4b577f2 100644 --- a/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsUtils.java +++ b/src/main/java/ru/octol1ttle/knockdowns/common/KnockdownsUtils.java @@ -38,6 +38,6 @@ public class KnockdownsUtils { player ); - ReviverTracker.clearRevivers(player); + data.getRevivers().clear(); } } diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/ReviverTracker.java b/src/main/java/ru/octol1ttle/knockdowns/common/ReviverTracker.java deleted file mode 100644 index ba4736c..0000000 --- a/src/main/java/ru/octol1ttle/knockdowns/common/ReviverTracker.java +++ /dev/null @@ -1,32 +0,0 @@ -package ru.octol1ttle.knockdowns.common; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import net.minecraft.entity.player.EntityPlayer; - -public class ReviverTracker { - private static final Map> knockedToReviversMap = new HashMap<>(); - - public static void startReviving(EntityPlayer knocked, EntityPlayer reviver) { - getRevivers(knocked).add(reviver); - } - - public static void stopReviving(EntityPlayer knocked, EntityPlayer reviver) { - getRevivers(knocked).remove(reviver); - } - - public static void clearRevivers(EntityPlayer knocked) { - getRevivers(knocked).clear(); - } - - public static List getRevivers(EntityPlayer knocked) { - return knockedToReviversMap.computeIfAbsent(knocked, player -> new ArrayList<>()); - } - - public static Set>> getAllRevivers() { - return knockedToReviversMap.entrySet(); - } -} diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/data/IKnockdownsPlayerData.java b/src/main/java/ru/octol1ttle/knockdowns/common/data/IKnockdownsPlayerData.java index f0e98c9..820c8a0 100644 --- a/src/main/java/ru/octol1ttle/knockdowns/common/data/IKnockdownsPlayerData.java +++ b/src/main/java/ru/octol1ttle/knockdowns/common/data/IKnockdownsPlayerData.java @@ -4,6 +4,7 @@ import java.util.Objects; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; import net.minecraftforge.common.util.INBTSerializable; +import ru.octol1ttle.knockdowns.common.util.UniqueOnlyList; public interface IKnockdownsPlayerData extends INBTSerializable { boolean isKnockedDown(); @@ -15,6 +16,8 @@ public interface IKnockdownsPlayerData extends INBTSerializable int getTicksKnocked(); void setTicksKnocked(int ticksKnocked); + UniqueOnlyList getRevivers(); + static IKnockdownsPlayerData get(EntityPlayer player) { return Objects.requireNonNull(player.getCapability(KnockdownsCapability.CAPABILITY, null)); } diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/data/KnockdownsPlayerData.java b/src/main/java/ru/octol1ttle/knockdowns/common/data/KnockdownsPlayerData.java index 7eac143..cbff961 100644 --- a/src/main/java/ru/octol1ttle/knockdowns/common/data/KnockdownsPlayerData.java +++ b/src/main/java/ru/octol1ttle/knockdowns/common/data/KnockdownsPlayerData.java @@ -1,7 +1,9 @@ package ru.octol1ttle.knockdowns.common.data; +import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; import ru.octol1ttle.knockdowns.common.KnockdownsUtils; +import ru.octol1ttle.knockdowns.common.util.UniqueOnlyList; public class KnockdownsPlayerData implements IKnockdownsPlayerData { private static final String KEY_KNOCKED_DOWN = "KnockedDown"; @@ -10,6 +12,7 @@ public class KnockdownsPlayerData implements IKnockdownsPlayerData { private boolean knockedDown = false; private int reviveTimeLeft = KnockdownsUtils.INITIAL_REVIVE_TIME_LEFT; private int ticksKnocked = 0; + private final UniqueOnlyList revivers = new UniqueOnlyList<>(); @Override public boolean isKnockedDown() { @@ -41,6 +44,11 @@ public class KnockdownsPlayerData implements IKnockdownsPlayerData { this.ticksKnocked = ticksKnocked; } + @Override + public UniqueOnlyList getRevivers() { + return revivers; + } + @Override public NBTTagCompound serializeNBT() { NBTTagCompound nbt = new NBTTagCompound(); diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/network/KnockdownsNetwork.java b/src/main/java/ru/octol1ttle/knockdowns/common/network/KnockdownsNetwork.java index 4542e68..6e5fa60 100644 --- a/src/main/java/ru/octol1ttle/knockdowns/common/network/KnockdownsNetwork.java +++ b/src/main/java/ru/octol1ttle/knockdowns/common/network/KnockdownsNetwork.java @@ -6,12 +6,13 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraftforge.fml.common.network.NetworkRegistry; import net.minecraftforge.fml.common.network.simpleimpl.IMessage; +import net.minecraftforge.fml.common.network.simpleimpl.IMessageHandler; import net.minecraftforge.fml.common.network.simpleimpl.MessageContext; import net.minecraftforge.fml.common.network.simpleimpl.SimpleNetworkWrapper; import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; import ru.octol1ttle.knockdowns.Tags; -import ru.octol1ttle.knockdowns.client.network.KnockdownsClientPacketHandler; +import ru.octol1ttle.knockdowns.common.KnockdownsMod; import ru.octol1ttle.knockdowns.common.network.packets.c2s.CancelReviveC2SPacket; import ru.octol1ttle.knockdowns.common.network.packets.c2s.PlayerCalloutC2SPacket; import ru.octol1ttle.knockdowns.common.network.packets.s2c.PlayerCalloutS2CPacket; @@ -27,13 +28,14 @@ public class KnockdownsNetwork { INSTANCE.registerMessage(KnockdownsServerPacketHandler.Callout.class, PlayerCalloutC2SPacket.class, packetId++, Side.SERVER); INSTANCE.registerMessage(KnockdownsServerPacketHandler.CancelRevive.class, CancelReviveC2SPacket.class, packetId++, Side.SERVER); - INSTANCE.registerMessage(KnockdownsClientPacketHandler.Callout.class, PlayerCalloutS2CPacket.class, packetId++, Side.CLIENT); - INSTANCE.registerMessage(KnockdownsClientPacketHandler.PlayerKnockedDown.class, PlayerKnockedDownS2CPacket.class, packetId++, Side.CLIENT); - INSTANCE.registerMessage(KnockdownsClientPacketHandler.SynchronizePlayerData.KnockedDown.class, SynchronizePlayerDataS2CPacket.KnockedDown.class, packetId++, Side.CLIENT); - INSTANCE.registerMessage(KnockdownsClientPacketHandler.SynchronizePlayerData.ReviveTimeLeft.class, SynchronizePlayerDataS2CPacket.ReviveTimeLeft.class, packetId++, Side.CLIENT); - INSTANCE.registerMessage(KnockdownsClientPacketHandler.SynchronizePlayerData.Full.class, SynchronizePlayerDataS2CPacket.Full.class, packetId++, Side.CLIENT); - INSTANCE.registerMessage(KnockdownsClientPacketHandler.SynchronizeRevivers.Add.class, SynchronizeReviversS2CPacket.Add.class, packetId++, Side.CLIENT); - INSTANCE.registerMessage(KnockdownsClientPacketHandler.SynchronizeRevivers.Remove.class, SynchronizeReviversS2CPacket.Remove.class, packetId++, Side.CLIENT); + IMessageHandler clientProxyHandler = (message, ctx) -> KnockdownsMod.clientProxy.handleMessage(message); + INSTANCE.registerMessage(clientProxyHandler, PlayerCalloutS2CPacket.class, packetId++, Side.CLIENT); + INSTANCE.registerMessage(clientProxyHandler, PlayerKnockedDownS2CPacket.class, packetId++, Side.CLIENT); + INSTANCE.registerMessage(clientProxyHandler, SynchronizePlayerDataS2CPacket.KnockedDown.class, packetId++, Side.CLIENT); + INSTANCE.registerMessage(clientProxyHandler, SynchronizePlayerDataS2CPacket.ReviveTimeLeft.class, packetId++, Side.CLIENT); + INSTANCE.registerMessage(clientProxyHandler, SynchronizePlayerDataS2CPacket.Full.class, packetId++, Side.CLIENT); + INSTANCE.registerMessage(clientProxyHandler, SynchronizeReviversS2CPacket.Add.class, packetId++, Side.CLIENT); + INSTANCE.registerMessage(clientProxyHandler, SynchronizeReviversS2CPacket.Remove.class, packetId++, Side.CLIENT); } @SideOnly(Side.CLIENT) diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/network/KnockdownsServerPacketHandler.java b/src/main/java/ru/octol1ttle/knockdowns/common/network/KnockdownsServerPacketHandler.java index d73f461..d0beb64 100644 --- a/src/main/java/ru/octol1ttle/knockdowns/common/network/KnockdownsServerPacketHandler.java +++ b/src/main/java/ru/octol1ttle/knockdowns/common/network/KnockdownsServerPacketHandler.java @@ -1,13 +1,12 @@ package ru.octol1ttle.knockdowns.common.network; import java.util.List; -import java.util.Map; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraftforge.fml.common.network.simpleimpl.IMessage; import net.minecraftforge.fml.common.network.simpleimpl.IMessageHandler; import net.minecraftforge.fml.common.network.simpleimpl.MessageContext; -import ru.octol1ttle.knockdowns.common.ReviverTracker; +import ru.octol1ttle.knockdowns.common.data.IKnockdownsPlayerData; import ru.octol1ttle.knockdowns.common.network.packets.c2s.CancelReviveC2SPacket; import ru.octol1ttle.knockdowns.common.network.packets.c2s.PlayerCalloutC2SPacket; import ru.octol1ttle.knockdowns.common.network.packets.s2c.PlayerCalloutS2CPacket; @@ -33,13 +32,14 @@ public class KnockdownsServerPacketHandler { @Override public IMessage onMessage(CancelReviveC2SPacket message, MessageContext ctx) { EntityPlayerMP player = ctx.getServerHandler().player; - for (Map.Entry> revivers : ReviverTracker.getAllRevivers()) { - if (revivers.getValue().contains(player)) { - revivers.getValue().remove(player); + for (EntityPlayer knocked : player.world.playerEntities) { + List revivers = IKnockdownsPlayerData.get(knocked).getRevivers(); + if (revivers.contains(player)) { + revivers.remove(player); KnockdownsNetwork.sendToMultiple( - new SynchronizeReviversS2CPacket.Remove(revivers.getKey().getEntityId(), player.getEntityId()), - revivers.getValue(), - (EntityPlayerMP) revivers.getKey() + new SynchronizeReviversS2CPacket.Remove(knocked.getEntityId(), player.getEntityId()), + revivers, + (EntityPlayerMP) knocked ); break; } diff --git a/src/main/java/ru/octol1ttle/knockdowns/common/util/UniqueOnlyList.java b/src/main/java/ru/octol1ttle/knockdowns/common/util/UniqueOnlyList.java new file mode 100644 index 0000000..de5fcb4 --- /dev/null +++ b/src/main/java/ru/octol1ttle/knockdowns/common/util/UniqueOnlyList.java @@ -0,0 +1,23 @@ +package ru.octol1ttle.knockdowns.common.util; + +import java.util.ArrayList; + +public class UniqueOnlyList extends ArrayList { + @Override + public void add(int index, T element) { + throw new IllegalStateException(); + } + + @Override + public boolean add(T t) { + if (this.contains(t)) { + return false; + } + return super.add(t); + } + + @Override + public T set(int index, T element) { + throw new IllegalStateException(); + } +}