完成初次提交

This commit is contained in:
tartaric_acid
2024-05-16 23:59:03 +08:00
commit 019059a83c
1132 changed files with 158417 additions and 0 deletions

5
.gitattributes vendored Normal file
View File

@@ -0,0 +1,5 @@
# Disable autocrlf on generated files, they always generate with LF
# Add any extra files or paths here to make git stop saying they
# are changed when only line endings change.
src/generated/**/.cache/cache text eol=lf
src/generated/**/*.json text eol=lf

27
.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# eclipse
bin
*.launch
.settings
.metadata
.classpath
.project
# idea
out
*.ipr
*.iws
*.iml
.idea
# gradle
build
.gradle
# other
eclipse
run
test
logs
# Files from Forge MDK
forge*changelog.txt

223
build.gradle Normal file
View File

@@ -0,0 +1,223 @@
import java.text.SimpleDateFormat
// 设置日期格式,用于填充快照版版本号
SimpleDateFormat FORMAT = new SimpleDateFormat("MMdd-HHmmss")
FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"))
buildscript {
repositories {
maven { url = 'https://files.minecraftforge.net/maven' }
maven { url = 'https://maven.parchmentmc.org' }
maven { url = 'https://repo.spongepowered.org/maven' }
mavenCentral()
}
dependencies {
classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '5.1.+', changing: true
classpath 'org.parchmentmc:librarian:1.+'
classpath group: 'org.spongepowered', name: 'mixingradle', version: '0.7-SNAPSHOT'
// shadow 插件,用来打包 Apache Commons Math 库
classpath 'gradle.plugin.com.github.johnrengelman:shadow:7.1.2'
}
}
apply plugin: 'net.minecraftforge.gradle'
apply plugin: 'org.parchmentmc.librarian.forgegradle'
apply plugin: 'eclipse'
apply plugin: 'org.spongepowered.mixin'
apply plugin: 'com.github.johnrengelman.shadow'
apply plugin: 'java'
apply plugin: 'maven-publish'
// version = "1.0.0"
// 版本号,正式发布需要修改这一行
version = FORMAT.format(new Date())
group = "com.tacz"
archivesBaseName = "tacz-1.20.1"
java.toolchain.languageVersion = JavaLanguageVersion.of(17)
mixin {
add sourceSets.main, "tacz.refmap.json"
config "tacz.mixins.json"
}
minecraft {
// 使用 parchment 来反混淆变量名
mappings channel: 'parchment', version: '2023.08.20-1.20.1'
// 使用 access transformer 来修改原版的一些方法访问修饰符
accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg')
// 运行参数
runs {
client {
// 使用 JetBrainsRuntime 时需要的参数,否则无法热重载
// 如果你使用别的 JDK那么可以删除这一行
jvmArgs "-XX:+AllowEnhancedClassRedefinition"
// 每个启动单独区分文件夹
workingDirectory project.file('run/client_a')
property 'forge.logging.markers', 'SCAN,REGISTRIES,REGISTRYDUMP'
property 'forge.logging.console.level', 'debug'
property 'mixin.env.remapRefMap', 'true'
property 'mixin.env.refMapRemappingFile', "${buildDir}/createSrgToMcp/output.srg"
mods {
tacz {
source sourceSets.main
}
}
}
client2 {
parent minecraft.runs.client
workingDirectory project.file('run/client_b')
// 设定用户名
args '--username', 'mayday_memory'
mods {
tacz {
source sourceSets.main
}
}
}
server {
workingDirectory project.file('run/server')
property 'mixin.env.disableRefMap', 'true'
property 'forge.logging.markers', 'SCAN,REGISTRIES,REGISTRYDUMP'
property 'forge.logging.console.level', 'debug'
property 'mixin.env.remapRefMap', 'true'
property 'mixin.env.refMapRemappingFile', "${projectDir}/build/createSrgToMcp/output.srg"
mods {
tacz {
source sourceSets.main
}
}
}
data {
workingDirectory project.file('run/data')
property 'mixin.env.disableRefMap', 'true'
property 'forge.logging.markers', 'SCAN,REGISTRIES,REGISTRYDUMP'
property 'forge.logging.console.level', 'debug'
args '--mod', 'tacz', '--all', '--output', file('src/generated/resources/')
mods {
tacz {
source sourceSets.main
}
}
}
}
}
sourceSets.main.resources { srcDir 'src/generated/resources' }
repositories {
maven {
// 阿里云镜像,方便国内开发
url = uri("https://maven.aliyun.com/repository/public/")
}
maven {
// location of the maven that hosts JEI files since January 2023
// Patchouli
name = "Jared's maven"
url = "https://maven.blamejared.com/"
}
maven {
// location of a maven mirror for JEI files, as a fallback
name = "Mod Maven"
url = "https://modmaven.k-4u.nl"
}
maven {
// cloth config api
url "https://maven.shedaniel.me/"
}
maven {
url "https://cursemaven.com"
content {
includeGroup "curse.maven"
}
}
}
dependencies {
minecraft "net.minecraftforge:forge:${forge_version}"
// Apache Commons Math 库,用于进行一些插值运算
minecraftLibrary 'org.apache.commons:commons-math3:3.6.1'
// compile against the JEI API but do not include it at runtime
compileOnly(fg.deobf("mezz.jei:jei-1.20.1-common-api:${jei_version}"))
compileOnly(fg.deobf("mezz.jei:jei-1.20.1-forge-api:${jei_version}"))
// at runtime, use the full JEI jar for Forge
runtimeOnly(fg.deobf("mezz.jei:jei-1.20.1-forge:${jei_version}"))
implementation fg.deobf("me.shedaniel.cloth:cloth-config-forge:${cloth_config_forge}")
compileOnly fg.deobf("org.embeddedt:embeddium-1.20.1:${embeddedt_id}")
compileOnly fg.deobf("curse.maven:oculus-581495:${oculus_id}")
annotationProcessor 'org.spongepowered:mixin:0.8.5:processor'
}
jar {
// 打包时,给 manifest 文件写入一些有用信息
// 这些信息会被游戏调用,从而显示成模组的名称和版本信息
manifest {
attributes([
"Implementation-Title" : project.name,
"Implementation-Version": project.version
])
}
}
java {
// 编译源码文件,方便发布 maven
withSourcesJar()
}
publishing {
// 发布到 jitpack maven方便其他模组开发者
publications.create('mavenJava', MavenPublication) {
from components.java
groupId = project.group
version = project.version
artifactId = project.archivesBaseName
pom {
name = project.archivesBaseName
// 删除所有依赖,不需要
pom.withXml {
asNode().dependencies.dependency.each { dep ->
assert dep.parent().remove(dep)
}
}
}
}
}
// 禁用元数据生成,它生成的 maven 项目有问题
tasks.withType(GenerateModuleMetadata).configureEach {
enabled = false
}
shadowJar {
dependencies {
// 打包 Apache Commons Math 库
include(dependency("org.apache.commons:commons-math3:3.6.1"))
// 排除 Apache Commons Math 库中不需要的说明文件
exclude('META-INF/LICENSE.txt')
exclude('META-INF/NOTICE.txt')
exclude('META-INF/maven/')
}
// 防止与其他模组导致的同名路径冲突
relocate 'org.apache.commons.math3', 'com.tacz.guns.libs.org.apache.commons.math3'
// 最小化
minimize()
}
reobf {
// 与名称的混淆有关的,反正需要 shadowJar 一下
shadowJar {}
}
// 保证编译时的 assemble 在 shadowJar 之后执行
assemble.dependsOn shadowJar
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
}

7
gradle.properties Normal file
View File

@@ -0,0 +1,7 @@
org.gradle.jvmargs=-Xmx4G
org.gradle.daemon=false
forge_version=1.20.1-47.1.0
jei_version=15.0.0.12
cloth_config_forge=11.1.106
embeddedt_id=0.2.18-git.ce29192+mc1.20.1
oculus_id=4767500

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
gradlew vendored Normal file
View File

@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,36 @@
package com.tacz.guns;
import com.tacz.guns.config.ClientConfig;
import com.tacz.guns.config.CommonConfig;
import com.tacz.guns.config.ServerConfig;
import com.tacz.guns.init.*;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.fml.ModLoadingContext;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.config.ModConfig;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@Mod(GunMod.MOD_ID)
public class GunMod {
public static final String MOD_ID = "tacz";
public static final Logger LOGGER = LogManager.getLogger(MOD_ID);
public GunMod() {
ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, CommonConfig.init());
ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, ServerConfig.init());
ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, ClientConfig.init());
IEventBus bus = FMLJavaModLoadingContext.get().getModEventBus();
ModBlocks.BLOCKS.register(bus);
ModBlocks.TILE_ENTITIES.register(bus);
ModItems.ITEMS.register(bus);
ModEntities.ENTITY_TYPES.register(bus);
ModEntities.DATA_SERIALIZERS.register(bus);
ModRecipe.RECIPE_SERIALIZERS.register(bus);
ModContainer.CONTAINER_TYPE.register(bus);
ModSounds.SOUNDS.register(bus);
ModParticles.PARTICLE_TYPES.register(bus);
}
}

View File

@@ -0,0 +1,20 @@
package com.tacz.guns.api;
import com.tacz.guns.GunMod;
import net.minecraft.resources.ResourceLocation;
public final class DefaultAssets {
public static ResourceLocation EMPTY_GUN_ID = new ResourceLocation(GunMod.MOD_ID, "empty");
public static ResourceLocation DEFAULT_GUN_DISPLAY = new ResourceLocation(GunMod.MOD_ID, "ak47_display");
public static ResourceLocation DEFAULT_GUN_DATA = new ResourceLocation(GunMod.MOD_ID, "ak47_data");
public static ResourceLocation DEFAULT_AMMO_ID = new ResourceLocation(GunMod.MOD_ID, "762x39");
public static ResourceLocation DEFAULT_AMMO_DISPLAY = new ResourceLocation(GunMod.MOD_ID, "762x39_display");
public static ResourceLocation EMPTY_AMMO_ID = new ResourceLocation(GunMod.MOD_ID, "empty");
public static ResourceLocation DEFAULT_ATTACHMENT_ID = new ResourceLocation(GunMod.MOD_ID, "sight_sro_dot");
public static ResourceLocation EMPTY_ATTACHMENT_ID = new ResourceLocation(GunMod.MOD_ID, "empty");
public static ResourceLocation DEFAULT_ATTACHMENT_SKIN_ID = new ResourceLocation(GunMod.MOD_ID, "sight_sro_dot_blue");
public static ResourceLocation EMPTY_ATTACHMENT_SKIN_ID = new ResourceLocation(GunMod.MOD_ID, "empty");
}

View File

@@ -0,0 +1,89 @@
package com.tacz.guns.api;
import com.tacz.guns.api.client.other.IThirdPersonAnimation;
import com.tacz.guns.api.client.other.ThirdPersonManager;
import com.tacz.guns.client.resource.ClientGunPackLoader;
import com.tacz.guns.client.resource.index.ClientAmmoIndex;
import com.tacz.guns.client.resource.index.ClientAttachmentIndex;
import com.tacz.guns.client.resource.index.ClientGunIndex;
import com.tacz.guns.crafting.GunSmithTableRecipe;
import com.tacz.guns.resource.CommonAssetManager;
import com.tacz.guns.resource.CommonGunPackLoader;
import com.tacz.guns.resource.index.CommonAmmoIndex;
import com.tacz.guns.resource.index.CommonAttachmentIndex;
import com.tacz.guns.resource.index.CommonGunIndex;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
public final class TimelessAPI {
@OnlyIn(Dist.CLIENT)
public static Optional<ClientGunIndex> getClientGunIndex(ResourceLocation gunId) {
return ClientGunPackLoader.getGunIndex(gunId);
}
@OnlyIn(Dist.CLIENT)
public static Optional<ClientAttachmentIndex> getClientAttachmentIndex(ResourceLocation attachmentId) {
return ClientGunPackLoader.getAttachmentIndex(attachmentId);
}
@OnlyIn(Dist.CLIENT)
public static Optional<ClientAmmoIndex> getClientAmmoIndex(ResourceLocation ammoId) {
return ClientGunPackLoader.getAmmoIndex(ammoId);
}
@OnlyIn(Dist.CLIENT)
public static Set<Map.Entry<ResourceLocation, ClientGunIndex>> getAllClientGunIndex() {
return ClientGunPackLoader.getAllGuns();
}
@OnlyIn(Dist.CLIENT)
public static Set<Map.Entry<ResourceLocation, ClientAmmoIndex>> getAllClientAmmoIndex() {
return ClientGunPackLoader.getAllAmmo();
}
@OnlyIn(Dist.CLIENT)
public static Set<Map.Entry<ResourceLocation, ClientAttachmentIndex>> getAllClientAttachmentIndex() {
return ClientGunPackLoader.getAllAttachments();
}
public static Optional<CommonGunIndex> getCommonGunIndex(ResourceLocation gunId) {
return CommonGunPackLoader.getGunIndex(gunId);
}
public static Optional<CommonAttachmentIndex> getCommonAttachmentIndex(ResourceLocation attachmentId) {
return CommonGunPackLoader.getAttachmentIndex(attachmentId);
}
public static Optional<CommonAmmoIndex> getCommonAmmoIndex(ResourceLocation ammoId) {
return CommonGunPackLoader.getAmmoIndex(ammoId);
}
public static Optional<GunSmithTableRecipe> getRecipe(ResourceLocation recipeId) {
return CommonAssetManager.INSTANCE.getRecipe(recipeId);
}
public static Set<Map.Entry<ResourceLocation, CommonGunIndex>> getAllCommonGunIndex() {
return CommonGunPackLoader.getAllGuns();
}
public static Set<Map.Entry<ResourceLocation, CommonAmmoIndex>> getAllCommonAmmoIndex() {
return CommonGunPackLoader.getAllAmmo();
}
public static Set<Map.Entry<ResourceLocation, CommonAttachmentIndex>> getAllCommonAttachmentIndex() {
return CommonGunPackLoader.getAllAttachments();
}
public static Map<ResourceLocation, GunSmithTableRecipe> getAllRecipes() {
return CommonAssetManager.INSTANCE.getAllRecipes();
}
public static void registerThirdPersonAnimation(String name, IThirdPersonAnimation animation) {
ThirdPersonManager.register(name, animation);
}
}

View File

@@ -0,0 +1,20 @@
package com.tacz.guns.api.client.event;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraftforge.eventbus.api.Event;
/**
* 在调用 ItemInHandRenderer#renderHandsWithItems 方法时触发该事件
* 用于相机动画相关调用
*/
public class BeforeRenderHandEvent extends Event {
private final PoseStack poseStack;
public BeforeRenderHandEvent(PoseStack poseStack) {
this.poseStack = poseStack;
}
public PoseStack getPoseStack() {
return poseStack;
}
}

View File

@@ -0,0 +1,32 @@
package com.tacz.guns.api.client.event;
import net.minecraft.client.Camera;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraftforge.client.event.EntityViewRenderEvent;
/**
* 在调用 GameRenderer#getFov 方法时触发该事件
* 用于瞄准镜瞄准时 FOV 相关变化时调用
*/
public class FieldOfView extends EntityViewRenderEvent {
private final boolean isItemWithHand;
private double fov;
public FieldOfView(GameRenderer renderer, Camera camera, double partialTicks, double fov, boolean isItemWithHand) {
super(renderer, camera, partialTicks);
this.fov = fov;
this.isItemWithHand = isItemWithHand;
}
public double getFOV() {
return fov;
}
public void setFOV(double fov) {
this.fov = fov;
}
public boolean isItemWithHand() {
return isItemWithHand;
}
}

View File

@@ -0,0 +1,25 @@
package com.tacz.guns.api.client.event;
import net.minecraftforge.eventbus.api.Cancelable;
import net.minecraftforge.eventbus.api.Event;
/**
* 当第一人称视角触发摇晃时,玩家手部的摇晃
*/
public class RenderItemInHandBobEvent extends Event {
/**
* 使用注解也可以,但是热重载会导致游戏崩溃
*/
@Override
public boolean isCancelable() {
return true;
}
@Cancelable
public static class BobHurt extends RenderItemInHandBobEvent {
}
@Cancelable
public static class BobView extends RenderItemInHandBobEvent {
}
}

View File

@@ -0,0 +1,25 @@
package com.tacz.guns.api.client.event;
import net.minecraftforge.eventbus.api.Cancelable;
import net.minecraftforge.eventbus.api.Event;
/**
* 当第一人称视角触发摇晃时,世界背景的摇晃
*/
public class RenderLevelBobEvent extends Event {
/**
* 使用注解也可以,但是热重载会导致游戏崩溃
*/
@Override
public boolean isCancelable() {
return true;
}
@Cancelable
public static class BobHurt extends RenderLevelBobEvent {
}
@Cancelable
public static class BobView extends RenderLevelBobEvent {
}
}

View File

@@ -0,0 +1,9 @@
package com.tacz.guns.api.client.event;
import net.minecraftforge.eventbus.api.Event;
/**
* 玩家交换主副手物品时触发该事件
*/
public class SwapItemWithOffHand extends Event {
}

View File

@@ -0,0 +1,75 @@
package com.tacz.guns.api.client.gameplay;
import com.tacz.guns.api.entity.ShootResult;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* 客户端枪械操纵者
* 目前仅用于 LocalPlayer
*/
@OnlyIn(Dist.CLIENT)
public interface IClientPlayerGunOperator {
/**
* LocalPlayer 通过 Mixin 的方式实现了这个接口
*/
static IClientPlayerGunOperator fromLocalPlayer(LocalPlayer player) {
return (IClientPlayerGunOperator) player;
}
/**
* 检查玩家能否开火,并执行客户端开火逻辑。
*
* @return 返回开火的结果
*/
ShootResult shoot();
/**
* 执行客户端切枪逻辑。
*/
void draw(ItemStack lastItem);
/**
* 客户端手动换弹
*/
void bolt();
/**
* 客户端换弹
*/
void reload();
/**
* 客户端检视
*/
void inspect();
/**
* 客户端切换开火模式
*/
void fireSelect();
/**
* 客户端瞄准
*/
void aim(boolean isAim);
/**
* 客户端是否处于瞄准状态
*/
boolean isAim();
/**
* 客户端瞄准进度
*
* @return 0-11 代表开镜进度到 100%
*/
float getClientAimingProgress(float partialTicks);
/**
* 客户端射击冷却时间
*/
long getClientShootCoolDown();
}

View File

@@ -0,0 +1,27 @@
package com.tacz.guns.api.client.other;
import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.world.entity.LivingEntity;
public interface IThirdPersonAnimation {
/**
* 第三人称动画:主手持有枪械时
*
* @param entity 持有枪械的实体
* @param rightArm 右手模型
* @param leftArm 左手模型
* @param head 头部模型
*/
void animateGunHold(LivingEntity entity, ModelPart rightArm, ModelPart leftArm, ModelPart body, ModelPart head);
/**
* 第三人称动画:枪械瞄准时
*
* @param entity 持有枪械的实体
* @param rightArm 右手模型
* @param leftArm 左手模型
* @param head 头部模型
* @param aimProgress 瞄准进度 0-1
*/
void animateGunAim(LivingEntity entity, ModelPart rightArm, ModelPart leftArm, ModelPart body, ModelPart head, float aimProgress);
}

View File

@@ -0,0 +1,21 @@
package com.tacz.guns.api.client.other;
import net.minecraft.world.item.ItemStack;
/**
* 用来在收物品时,让其保持一段时间渲染的接口
*/
public interface KeepingItemRenderer {
/**
* 物品保持渲染的时间
*
* @param itemStack 保持的物品
* @param timeMs 时间,单位毫秒
*/
void keep(ItemStack itemStack, long timeMs);
/**
* 获取当前主手正在渲染的物品
*/
ItemStack getCurrentItem();
}

View File

@@ -0,0 +1,50 @@
package com.tacz.guns.api.client.other;
import com.google.common.collect.Maps;
import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.LivingEntity;
import java.util.Map;
/**
* 简单的第三人称持枪动画 Manager
*/
public final class ThirdPersonManager {
private static final Map<String, IThirdPersonAnimation> CACHE = Maps.newHashMap();
private static final String RESERVED_DEFAULT_NAME = "default";
private static final IThirdPersonAnimation DEFAULT = new IThirdPersonAnimation() {
@Override
public void animateGunHold(LivingEntity entity, ModelPart rightArm, ModelPart leftArm, ModelPart body, ModelPart head) {
rightArm.yRot = -0.3F + head.yRot;
leftArm.yRot = 0.8F + head.yRot;
rightArm.xRot = -1.4F + head.xRot;
leftArm.xRot = -1.4F + head.xRot;
}
@Override
public void animateGunAim(LivingEntity entity, ModelPart rightArm, ModelPart leftArm, ModelPart body, ModelPart head, float aimProgress) {
float lerp1 = Mth.lerp(aimProgress, 0.3f, 0.35f);
float lerp2 = Mth.lerp(aimProgress, 1.4f, 1.6f);
rightArm.yRot = -lerp1 + head.yRot;
leftArm.yRot = 0.8F + head.yRot;
rightArm.xRot = -lerp2 + head.xRot;
leftArm.xRot = -lerp2 + head.xRot;
}
};
public static void registerDefault() {
CACHE.put(RESERVED_DEFAULT_NAME, DEFAULT);
}
public static void register(String name, IThirdPersonAnimation animation) {
if (name.equals(RESERVED_DEFAULT_NAME)) {
return;
}
CACHE.put(name, animation);
}
public static IThirdPersonAnimation getAnimation(String name) {
return CACHE.getOrDefault(name, DEFAULT);
}
}

View File

@@ -0,0 +1,113 @@
package com.tacz.guns.api.entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
import java.util.function.Supplier;
public interface IGunOperator {
/**
* LivingEntity 通过 Mixin 的方式实现了这个接口
*/
static IGunOperator fromLivingEntity(LivingEntity entity) {
return (IGunOperator) entity;
}
/**
* 获取从服务端同步的射击的冷却
*/
long getSynShootCoolDown();
/**
* 获取从服务端同步的切枪的冷却
*/
long getSynDrawCoolDown();
/**
* 获取从服务端同步的手动换弹的冷却
*/
long getSynBoltCoolDown();
/**
* 获取从服务端同步的换弹状态
*/
ReloadState getSynReloadState();
/**
* 获取从服务端同步的瞄准进度
*/
float getSynAimingProgress();
/**
* 获取该实体是否正在瞄准。
* 注意,这个方法并不等价于 getSynAimingProgress() > 0。
* 如果玩家正在瞄准,瞄准进度会增加,否则瞄准进度会减少。
*/
boolean getSynIsAiming();
/**
* 获取玩家持枪奔跑的时长。
* 最大不会大于枪械数据中设置的 sprintTime最小不会小于 0。
*/
float getSynSprintTime();
/**
* 初始化枪械操作的各个数据,如换弹冷却、开火冷却等。
*/
void initialData();
/**
* 服务端切枪逻辑
*/
void draw(Supplier<ItemStack> itemStackSupplier);
/**
* 服务端拉栓逻辑
*/
void bolt();
/**
* 服务端换弹逻辑
*/
void reload();
/**
* 服务端切换开火模式的逻辑
*/
void fireSelect();
/**
* 服务端调整倍镜的逻辑
*/
void zoom();
/**
* 从实体的位置,向指定的方向开枪
*
* @param pitch 开火方向的俯仰角(即 xRot )
* @param yaw 开火方向的偏航角(即 yRot )
* @return 本次射击的结果
*/
ShootResult shoot(float pitch, float yaw);
/**
* 服务端,该操作者是否受弹药数影响
*
* @return 如果为 false那么开火时不会检查弹药无论是玩家背包内还是枪械内的
*/
boolean needCheckAmmo();
/**
* 服务端,开火是否消耗弹药
*
* @return 如果为 false那么开火不会消耗枪械弹药
*/
boolean consumesAmmoOrNot();
/**
* 服务端,应用瞄准的逻辑
*
* @param isAim 是否瞄准
*/
void aim(boolean isAim);
}

View File

@@ -0,0 +1,19 @@
package com.tacz.guns.api.entity;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.phys.EntityHitResult;
/**
* 用于进行一些并非 {@link LivingEntity} 但是可被子弹击中的特殊实体的处理
*/
public interface ITargetEntity {
/**
* @param projectile 弹射物实体
* @param result 击中实体的位置
* @param source 伤害源类型
* @param damage 伤害值
*/
void onProjectileHit(Entity projectile, EntityHitResult result, DamageSource source, float damage);
}

View File

@@ -0,0 +1,31 @@
package com.tacz.guns.api.entity;
import net.minecraft.world.entity.LivingEntity;
/**
* 用于修改实体被子弹击中后的击退效果的设计
* 默认给所有 LivingEntity 添加了此接口
*/
public interface KnockBackModifier {
/**
* LivingEntity 通过 Mixin 的方式实现了这个接口
*/
static KnockBackModifier fromLivingEntity(LivingEntity entity) {
return (KnockBackModifier) entity;
}
/**
* 重置击退效果,实体此时恢复正常原版击退逻辑
*/
void resetKnockBackStrength();
/**
* 获取击退强度
*/
double getKnockBackStrength();
/**
* 设置击退强度
*/
void setKnockBackStrength(double strength);
}

View File

@@ -0,0 +1,111 @@
package com.tacz.guns.api.entity;
public class ReloadState {
/**
* 没有进行换弹操作时,倒计时为 -1
*/
public static final int NOT_RELOADING_COUNTDOWN = -1;
/**
* 换弹状态
*/
protected ReloadState.StateType stateType;
/**
* 换弹状态的剩余时长,毫秒
*/
protected long countDown;
public ReloadState() {
stateType = StateType.NOT_RELOADING;
countDown = NOT_RELOADING_COUNTDOWN;
}
public ReloadState(ReloadState src) {
stateType = src.stateType;
countDown = src.countDown;
}
/**
* @return 返回当前的换弹状态的类型。可用于判断是否正在进行换弹、换弹处在的阶段等。
*/
public StateType getStateType() {
return stateType;
}
public void setStateType(StateType stateType) {
this.stateType = stateType;
}
/**
* @return 如果 StateType 为 NOT_RELOADING则返回 NOT_RELOADING_COUNTDOWN(= -1), 否则返回当前状态剩余的时长,单位为 ms 。
*/
public long getCountDown() {
if (stateType == StateType.NOT_RELOADING) {
return NOT_RELOADING_COUNTDOWN;
}
return countDown;
}
public void setCountDown(long countDown) {
this.countDown = countDown;
}
@Override
public boolean equals(Object o) {
if (o instanceof ReloadState reloadState) {
return reloadState.stateType.equals(stateType) && reloadState.countDown == countDown;
} else {
return false;
}
}
public enum StateType {
/**
* 表示当前玩家未进行换弹。
*/
NOT_RELOADING,
/**
* 表示当前换弹状态为 正在进行空仓换弹 ,并处在填装弹药阶段。
*/
EMPTY_RELOAD_FEEDING,
/**
* 表示当前换弹状态为 正在进行空仓换弹,并处在收尾阶段。
*/
EMPTY_RELOAD_FINISHING,
/**
* 表示当前换弹状态为 正在进行战术快速换弹 ,并处在填装弹药阶段。
*/
TACTICAL_RELOAD_FEEDING,
/**
* 表示当前换弹状态为 正在进行战术快速换弹,并处在收尾阶段。
*/
TACTICAL_RELOAD_FINISHING;
/**
* 判断这个状态是否是空仓换弹过程中的其中一个阶段。包括空仓换弹的收尾阶段。
*/
public boolean isReloadingEmpty() {
return this == EMPTY_RELOAD_FEEDING || this == EMPTY_RELOAD_FINISHING;
}
/**
* 判断这个状态是否是战术换弹过程中的其中一个阶段。包括战术换弹的收尾阶段。
*/
public boolean isReloadingTactical() {
return this == TACTICAL_RELOAD_FEEDING || this == TACTICAL_RELOAD_FINISHING;
}
/**
* 判断这个状态是否是任意换弹过程中的其中一个阶段。包括任意换弹的收尾阶段。
*/
public boolean isReloading() {
return isReloadingEmpty() || isReloadingTactical();
}
/**
* 判断这个状态是否是任意换弹过程中的的收尾阶段。
*/
public boolean isReloadFinishing() {
return this == StateType.EMPTY_RELOAD_FINISHING || this == StateType.TACTICAL_RELOAD_FINISHING;
}
}
}

View File

@@ -0,0 +1,56 @@
package com.tacz.guns.api.entity;
public enum ShootResult {
/**
* 成功
*/
SUCCESS,
/**
* 未知原因失败
*/
UNKNOWN_FAIL,
/**
* 射击冷却时间还没到
*/
COOL_DOWN,
/**
* 无弹药
*/
NO_AMMO,
/**
* 没有执行切枪逻辑
*/
NOT_DRAW,
/**
* 当前物品不是枪
*/
NOT_GUN,
/**
* 枪械 ID 不存在
*/
ID_NOT_EXIST,
/**
* 需要手动上膛
*/
NEED_BOLT,
/**
* 正处于换弹状态
*/
IS_RELOADING,
/**
* 正处于切枪状态
*/
IS_DRAWING,
/**
* 正处于拉拴状态
*/
IS_BOLTING,
/**
* 正处于疾跑状态
*/
IS_SPRINTING,
/**
* Forge 事件原因取消
*/
FORGE_EVENT_CANCEL
}

View File

@@ -0,0 +1,38 @@
package com.tacz.guns.api.event.common;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.LogicalSide;
/**
* 生物的枪击发的事件。与 {@link GunShootEvent}不同的是,扣动一次扳机可能多次触发这个事件(如枪械处于 Burst 模式),但 {@link GunShootEvent} 只会触发一次
*/
public class GunFireEvent extends Event {
private final LivingEntity shooter;
private final ItemStack gunItemStack;
private final LogicalSide logicalSide;
public GunFireEvent(LivingEntity shooter, ItemStack gunItemStack, LogicalSide side) {
this.shooter = shooter;
this.gunItemStack = gunItemStack;
this.logicalSide = side;
}
@Override
public boolean isCancelable() {
return true;
}
public LivingEntity getShooter() {
return shooter;
}
public ItemStack getGunItemStack() {
return gunItemStack;
}
public LogicalSide getLogicalSide() {
return logicalSide;
}
}

View File

@@ -0,0 +1,38 @@
package com.tacz.guns.api.event.common;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.LogicalSide;
/**
* 生物切换枪械开火模式时触发的事件
*/
public class GunFireSelectEvent extends Event {
private final LivingEntity shooter;
private final ItemStack gunItemStack;
private final LogicalSide logicalSide;
public GunFireSelectEvent(LivingEntity shooter, ItemStack gunItemStack, LogicalSide side) {
this.shooter = shooter;
this.gunItemStack = gunItemStack;
this.logicalSide = side;
}
@Override
public boolean isCancelable() {
return true;
}
public LivingEntity getShooter() {
return shooter;
}
public ItemStack getGunItemStack() {
return gunItemStack;
}
public LogicalSide getLogicalSide() {
return logicalSide;
}
}

View File

@@ -0,0 +1,38 @@
package com.tacz.guns.api.event.common;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.LogicalSide;
/**
* 生物开始更换枪械弹药时触发的事件。
*/
public class GunReloadEvent extends Event {
private final LivingEntity entity;
private final ItemStack gunItemStack;
private final LogicalSide logicalSide;
public GunReloadEvent(LivingEntity entity, ItemStack gunItemStack, LogicalSide side) {
this.entity = entity;
this.gunItemStack = gunItemStack;
this.logicalSide = side;
}
@Override
public boolean isCancelable() {
return true;
}
public LivingEntity getEntity() {
return entity;
}
public ItemStack getGunItemStack() {
return gunItemStack;
}
public LogicalSide getLogicalSide() {
return logicalSide;
}
}

View File

@@ -0,0 +1,38 @@
package com.tacz.guns.api.event.common;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.LogicalSide;
/**
* 生物射击时触发的事件。与 {@link GunFireEvent}不同的是,扣动一次扳机只会触发一次这个事件,但可能多次触发 {@link GunFireEvent}(如枪械处于 Burst 模式)
*/
public class GunShootEvent extends Event {
private final LivingEntity shooter;
private final ItemStack gunItemStack;
private final LogicalSide logicalSide;
public GunShootEvent(LivingEntity shooter, ItemStack gunItemStack, LogicalSide side) {
this.shooter = shooter;
this.gunItemStack = gunItemStack;
this.logicalSide = side;
}
@Override
public boolean isCancelable() {
return true;
}
public LivingEntity getShooter() {
return shooter;
}
public ItemStack getGunItemStack() {
return gunItemStack;
}
public LogicalSide getLogicalSide() {
return logicalSide;
}
}

View File

@@ -0,0 +1,55 @@
package com.tacz.guns.api.event.common;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.LivingEntity;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.LogicalSide;
import javax.annotation.Nullable;
/**
* 生物被枪械子弹伤害时触发的事件
*/
public class LivingHurtByGunEvent extends Event {
private final @Nullable LivingEntity hurtEntity;
private final @Nullable LivingEntity attacker;
private final ResourceLocation gunId;
private final float amount;
private final boolean isHeadShot;
private final LogicalSide logicalSide;
public LivingHurtByGunEvent(@Nullable LivingEntity hurtEntity, @Nullable LivingEntity attacker, ResourceLocation gunId, float amount, boolean isHeadShot, LogicalSide logicalSide) {
this.hurtEntity = hurtEntity;
this.attacker = attacker;
this.gunId = gunId;
this.amount = amount;
this.isHeadShot = isHeadShot;
this.logicalSide = logicalSide;
}
@Nullable
public LivingEntity getHurtEntity() {
return hurtEntity;
}
@Nullable
public LivingEntity getAttacker() {
return attacker;
}
public ResourceLocation getGunId() {
return gunId;
}
public float getAmount() {
return amount;
}
public boolean isHeadShot() {
return isHeadShot;
}
public LogicalSide getLogicalSide() {
return logicalSide;
}
}

View File

@@ -0,0 +1,49 @@
package com.tacz.guns.api.event.common;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.LivingEntity;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.LogicalSide;
import javax.annotation.Nullable;
/**
* 生物被枪械子弹击杀时触发的事件
*/
public class LivingKillByGunEvent extends Event {
private final @Nullable LivingEntity killedEntity;
private final @Nullable LivingEntity attacker;
private final ResourceLocation gunId;
private final boolean isHeadShot;
private final LogicalSide logicalSide;
public LivingKillByGunEvent(@Nullable LivingEntity hurtEntity, @Nullable LivingEntity attacker, ResourceLocation gunId, boolean isHeadShot, LogicalSide logicalSide) {
this.killedEntity = hurtEntity;
this.attacker = attacker;
this.gunId = gunId;
this.isHeadShot = isHeadShot;
this.logicalSide = logicalSide;
}
@Nullable
public LivingEntity getKilledEntity() {
return killedEntity;
}
@Nullable
public LivingEntity getAttacker() {
return attacker;
}
public ResourceLocation getGunId() {
return gunId;
}
public boolean isHeadShot() {
return isHeadShot;
}
public LogicalSide getLogicalSide() {
return logicalSide;
}
}

View File

@@ -0,0 +1,40 @@
package com.tacz.guns.api.event.server;
import com.tacz.guns.entity.EntityKineticBullet;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraftforge.eventbus.api.Event;
/**
* 子弹击中方块时触发的事件,目前仅在服务端触发
*/
public class AmmoHitBlockEvent extends Event {
private final Level level;
private final BlockHitResult hitResult;
private final BlockState state;
private final EntityKineticBullet ammo;
public AmmoHitBlockEvent(Level level, BlockHitResult hitResult, BlockState state, EntityKineticBullet ammo) {
this.level = level;
this.hitResult = hitResult;
this.state = state;
this.ammo = ammo;
}
public Level getLevel() {
return level;
}
public BlockHitResult getHitResult() {
return hitResult;
}
public BlockState getState() {
return state;
}
public EntityKineticBullet getAmmo() {
return ammo;
}
}

View File

@@ -0,0 +1,44 @@
package com.tacz.guns.api.item;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nullable;
public interface IAmmo {
/**
* @return 如果物品类型为 IAttachment 则返回显式转换后的实例,否则返回 null。
*/
@Nullable
static IAmmo getIAmmoOrNull(@Nullable ItemStack stack) {
if (stack == null) {
return null;
}
if (stack.getItem() instanceof IAmmo iAmmo) {
return iAmmo;
}
return null;
}
/**
* 获取弹药 ID
*
* @param ammo 输入物品
* @return 弹药 ID
*/
ResourceLocation getAmmoId(ItemStack ammo);
/**
* 设置弹药 ID
*/
void setAmmoId(ItemStack ammo, @Nullable ResourceLocation ammoId);
/**
* 弹药是否属于这把枪
*
* @param gun 检查的枪械物品
* @param ammo 检查的子弹物品
* @return 是否属于这把枪
*/
boolean isAmmoOfGun(ItemStack gun, ItemStack ammo);
}

View File

@@ -0,0 +1,52 @@
package com.tacz.guns.api.item;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
/**
* 子弹盒接口
*/
public interface IAmmoBox {
/**
* 获取子弹盒中的子弹 ID
*
* @param ammoBox 子弹盒
* @return 子弹盒中的子弹 ID
*/
ResourceLocation getAmmoId(ItemStack ammoBox);
/**
* 获取子弹盒中的子弹数量
*
* @param ammoBox 子弹盒
* @return 子弹数量
*/
int getAmmoCount(ItemStack ammoBox);
/**
* 设置子弹盒中子弹的 ID
*/
void setAmmoId(ItemStack ammoBox, ResourceLocation ammoId);
/**
* 设置子弹盒中子弹数量
*/
void setAmmoCount(ItemStack ammoBox, int count);
/**
* 子弹盒中的子弹是否属于这把枪
*
* @param gun 枪
* @param ammoBox 子弹盒
* @return 是否属于这把枪
*/
boolean isAmmoBoxOfGun(ItemStack gun, ItemStack ammoBox);
/**
* 是否是无限子弹盒
*
* @param ammoBox 子弹盒
* @return 是否是无限子弹盒
*/
boolean isCreative(ItemStack ammoBox);
}

View File

@@ -0,0 +1,62 @@
package com.tacz.guns.api.item;
import com.tacz.guns.api.item.attachment.AttachmentType;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public interface IAttachment {
/**
* @return 如果物品类型为 IAttachment 则返回显式转换后的实例,否则返回 null。
*/
@Nullable
static IAttachment getIAttachmentOrNull(@Nullable ItemStack stack) {
if (stack == null) {
return null;
}
if (stack.getItem() instanceof IAttachment iAttachment) {
return iAttachment;
}
return null;
}
/**
* 获取配件 ID
*/
@Nonnull
ResourceLocation getAttachmentId(ItemStack attachmentStack);
/**
* 设置配件 ID
*/
void setAttachmentId(ItemStack attachmentStack, @Nullable ResourceLocation attachmentId);
/**
* 获取配件的皮肤 ID
*/
@Nullable
ResourceLocation getSkinId(ItemStack attachmentStack);
/**
* 设置配件的皮肤 ID
*/
void setSkinId(ItemStack attachmentStack, @Nullable ResourceLocation skinId);
/**
* 获取瞄具配件的缩放倍率的数字索引,仅瞄具配件可用
*/
int getZoomNumber(ItemStack attachmentStack);
/**
* 设置瞄具配件的缩放倍率的数字索引
*/
void setZoomNumber(ItemStack attachmentStack, int zoomNumber);
/**
* 配件类型
*/
@Nonnull
AttachmentType getType(ItemStack attachmentStack);
}

View File

@@ -0,0 +1,183 @@
package com.tacz.guns.api.item;
import com.tacz.guns.api.item.attachment.AttachmentType;
import com.tacz.guns.api.item.gun.AbstractGunItem;
import com.tacz.guns.api.item.gun.FireMode;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* 这里不包含枪械的逻辑,只包含枪械的各种 nbt 访问。<br>
* 你可以在 {@link AbstractGunItem} 看到枪械逻辑
*/
public interface IGun {
/**
* @return 如果物品类型为 IGun 则返回显式转换后的实例,否则返回 null。
*/
@Nullable
static IGun getIGunOrNull(@Nullable ItemStack stack) {
if (stack == null) {
return null;
}
if (stack.getItem() instanceof IGun iGun) {
return iGun;
}
return null;
}
/**
* 是否主手持枪
*/
static boolean mainhandHoldGun(LivingEntity livingEntity) {
return livingEntity.getMainHandItem().getItem() instanceof IGun;
}
/**
* 获取主手枪械的开火模式
*/
static FireMode getMainhandFireMode(LivingEntity livingEntity) {
ItemStack mainhandItem = livingEntity.getMainHandItem();
if (mainhandItem.getItem() instanceof IGun iGun) {
return iGun.getFireMode(mainhandItem);
}
return FireMode.UNKNOWN;
}
/**
* 获取瞄准进度
*
* @return 0-11 代表 100% 进度
*/
float getAimingZoom(ItemStack gunItem);
/**
* 获取枪械 ID
*/
@Nonnull
ResourceLocation getGunId(ItemStack gun);
/**
* 设置枪械 ID
*/
void setGunId(ItemStack gun, @Nullable ResourceLocation gunId);
/**
* 获取输入的经验值对应的等级。
*
* @param exp 经验值
* @return 对应的等级
*/
int getLevel(int exp);
/**
* 获取输入的等级需要至少多少的经验值。
*
* @param level 等级
* @return 至少需要的经验值
*/
int getExp(int level);
/**
* 返回允许的最大等级。
*
* @return 最大等级
*/
int getMaxLevel();
/**
* 获取枪械当前等级
*/
int getLevel(ItemStack gun);
/**
* 获取积累的全部经验值。
*
* @param gun 输入物品
* @return 全部经验值
*/
int getExp(ItemStack gun);
/**
* 获取到下个等级需要的经验值。
*
* @param gun 输入物品
* @return 到下个等级需要的经验值。如果等级已经到达最大,则返回 0
*/
int getExpToNextLevel(ItemStack gun);
/**
* 获取当前等级已经积累的经验值。
*
* @param gun 输入物品
* @return 当前等级已经积累的经验值
*/
int getExpCurrentLevel(ItemStack gun);
/**
* 获取开火模式
*
* @param gun 枪
* @return 开火模式
*/
FireMode getFireMode(ItemStack gun);
/**
* 设置开火模式
*/
void setFireMode(ItemStack gun, @Nullable FireMode fireMode);
/**
* 获取当前枪械弹药数
*/
int getCurrentAmmoCount(ItemStack gun);
/**
* 设置当前枪械弹药数
*/
void setCurrentAmmoCount(ItemStack gun, int ammoCount);
/**
* 减少一个当前枪械弹药数
*/
void reduceCurrentAmmoCount(ItemStack gun);
/**
* 获取当前枪械指定类型的配件
*/
@Nonnull
ItemStack getAttachment(ItemStack gun, AttachmentType type);
/**
* 安装配件
*/
void installAttachment(@Nonnull ItemStack gun, @Nonnull ItemStack attachment);
/**
* 卸载配件
*/
void unloadAttachment(@Nonnull ItemStack gun, AttachmentType type);
/**
* 该枪械是否允许装配该配件
*/
boolean allowAttachment(ItemStack gun, ItemStack attachmentItem);
/**
* 该枪械是否允许某类型配件
*/
boolean allowAttachmentType(ItemStack gun, AttachmentType type);
/**
* 枪管中是否有子弹,用于闭膛待击的枪械
*/
boolean hasBulletInBarrel(ItemStack gun);
/**
* 设置枪管中的子弹有无,用于闭膛待击的枪械
*/
void setBulletInBarrel(ItemStack gun, boolean bulletInBarrel);
}

View File

@@ -0,0 +1,40 @@
package com.tacz.guns.api.item.attachment;
import com.google.gson.annotations.SerializedName;
public enum AttachmentType {
/**
* 瞄具
*/
@SerializedName("scope")
SCOPE,
/**
* 枪口组件
*/
@SerializedName("muzzle")
MUZZLE,
/**
* 枪托
*/
@SerializedName("stock")
STOCK,
/**
* 握把
*/
@SerializedName("grip")
GRIP,
/**
* 激光指示器
*/
@SerializedName("laser")
LASER,
/**
* 扩容弹夹(匣)
*/
@SerializedName("extended_mag")
EXTENDED_MAG,
/**
* 用来表示物品不是配件的情况。
*/
NONE
}

View File

@@ -0,0 +1,37 @@
package com.tacz.guns.api.item.builder;
import com.tacz.guns.api.DefaultAssets;
import com.tacz.guns.api.item.IAmmo;
import com.tacz.guns.init.ModItems;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
public final class AmmoItemBuilder {
private int count = 1;
private ResourceLocation ammoId = DefaultAssets.DEFAULT_AMMO_ID;
private AmmoItemBuilder() {
}
public static AmmoItemBuilder create() {
return new AmmoItemBuilder();
}
public AmmoItemBuilder setCount(int count) {
this.count = Math.max(count, 1);
return this;
}
public AmmoItemBuilder setId(ResourceLocation id) {
this.ammoId = id;
return this;
}
public ItemStack build() {
ItemStack ammo = new ItemStack(ModItems.AMMO.get(), this.count);
if (ammo.getItem() instanceof IAmmo iAmmo) {
iAmmo.setAmmoId(ammo, this.ammoId);
}
return ammo;
}
}

View File

@@ -0,0 +1,44 @@
package com.tacz.guns.api.item.builder;
import com.tacz.guns.api.DefaultAssets;
import com.tacz.guns.api.item.IAttachment;
import com.tacz.guns.init.ModItems;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
public class AttachmentItemBuilder {
private int count = 1;
private ResourceLocation attachmentId = DefaultAssets.DEFAULT_ATTACHMENT_ID;
private ResourceLocation skinId = null;
private AttachmentItemBuilder() {
}
public static AttachmentItemBuilder create() {
return new AttachmentItemBuilder();
}
public AttachmentItemBuilder setCount(int count) {
this.count = Math.max(count, 1);
return this;
}
public AttachmentItemBuilder setId(ResourceLocation id) {
this.attachmentId = id;
return this;
}
public AttachmentItemBuilder setSkinId(ResourceLocation skinId) {
this.skinId = skinId;
return this;
}
public ItemStack build() {
ItemStack attachment = new ItemStack(ModItems.ATTACHMENT.get(), this.count);
if (attachment.getItem() instanceof IAttachment iAttachment) {
iAttachment.setAttachmentId(attachment, this.attachmentId);
iAttachment.setSkinId(attachment, this.skinId);
}
return attachment;
}
}

View File

@@ -0,0 +1,68 @@
package com.tacz.guns.api.item.builder;
import com.google.common.base.Preconditions;
import com.tacz.guns.api.TimelessAPI;
import com.tacz.guns.api.item.IGun;
import com.tacz.guns.api.item.gun.AbstractGunItem;
import com.tacz.guns.api.item.gun.FireMode;
import com.tacz.guns.api.item.gun.GunItemManager;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.registries.RegistryObject;
public final class GunItemBuilder {
private int count = 1;
private int ammoCount = 0;
private ResourceLocation gunId;
private FireMode fireMode = FireMode.UNKNOWN;
private boolean bulletInBarrel = false;
private GunItemBuilder() {
}
public static GunItemBuilder create() {
return new GunItemBuilder();
}
public GunItemBuilder setCount(int count) {
this.count = Math.max(count, 1);
return this;
}
public GunItemBuilder setAmmoCount(int count) {
this.ammoCount = Math.max(count, 0);
return this;
}
public GunItemBuilder setId(ResourceLocation id) {
this.gunId = id;
return this;
}
public GunItemBuilder setFireMode(FireMode fireMode) {
this.fireMode = fireMode;
return this;
}
public GunItemBuilder setAmmoInBarrel(boolean ammoInBarrel) {
this.bulletInBarrel = ammoInBarrel;
return this;
}
public ItemStack build() {
String itemType = TimelessAPI.getCommonGunIndex(gunId).map(index -> index.getPojo().getItemType()).orElse(null);
Preconditions.checkArgument(itemType != null, "Could not found gun id: " + gunId);
RegistryObject<? extends AbstractGunItem> gunItemRegistryObject = GunItemManager.getGunItemRegistryObject(itemType);
Preconditions.checkArgument(gunItemRegistryObject != null, "Could not found gun item type: " + itemType);
ItemStack gun = new ItemStack(gunItemRegistryObject.get(), this.count);
if (gun.getItem() instanceof IGun iGun) {
iGun.setGunId(gun, this.gunId);
iGun.setFireMode(gun, this.fireMode);
iGun.setCurrentAmmoCount(gun, this.ammoCount);
iGun.setBulletInBarrel(gun, this.bulletInBarrel);
}
return gun;
}
}

View File

@@ -0,0 +1,201 @@
package com.tacz.guns.api.item.gun;
import com.tacz.guns.api.TimelessAPI;
import com.tacz.guns.api.item.IAttachment;
import com.tacz.guns.api.item.IGun;
import com.tacz.guns.api.item.attachment.AttachmentType;
import com.tacz.guns.api.item.builder.GunItemBuilder;
import com.tacz.guns.client.renderer.item.GunItemRenderer;
import com.tacz.guns.client.resource.index.ClientGunIndex;
import com.tacz.guns.client.tab.CustomTab;
import com.tacz.guns.inventory.tooltip.GunTooltip;
import com.tacz.guns.resource.index.CommonGunIndex;
import com.tacz.guns.resource.pojo.data.gun.AttachmentPass;
import com.tacz.guns.resource.pojo.data.gun.GunData;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.BlockEntityWithoutLevelRenderer;
import net.minecraft.core.NonNullList;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.TranslatableComponent;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.inventory.tooltip.TooltipComponent;
import net.minecraft.world.item.CreativeModeTab;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.client.IItemRenderProperties;
import javax.annotation.Nonnull;
import java.util.Comparator;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
public abstract class AbstractGunItem extends Item implements IGun {
protected AbstractGunItem(Properties pProperties) {
super(pProperties);
}
private static Comparator<Map.Entry<ResourceLocation, ClientGunIndex>> idNameSort() {
return Comparator.comparingInt(m -> m.getValue().getSort());
}
/**
* 拉栓完成时调用
*/
public abstract void bolt(ItemStack gunItem);
/**
* 射击时触发
*/
public abstract void shoot(ItemStack gunItem, float pitch, float yaw, boolean tracer, LivingEntity shooter);
/**
* 切换开火模式时调用
*/
public abstract void fireSelect(ItemStack gunItem);
/**
* 换弹时触发枪械子弹更新时调用
*
* @param gunItem 枪械物品
* @param ammoCount 填充的子弹数量
* @param loadBarrel 是否需要往枪管里填子弹
*/
public abstract void reloadAmmo(ItemStack gunItem, int ammoCount, boolean loadBarrel);
/**
* 能否添加到此 CustomTab 中
*
* @param tab CustomTab
* @param stack 待添加的物品
*/
public abstract boolean canAddInTab(CustomTab tab, ItemStack stack);
/**
* 该方法具有通用的实现,放在此处
*/
@Override
public boolean allowAttachment(ItemStack gun, ItemStack attachmentItem) {
IAttachment iAttachment = IAttachment.getIAttachmentOrNull(attachmentItem);
IGun iGun = IGun.getIGunOrNull(gun);
if (iGun != null && iAttachment != null) {
AttachmentType type = iAttachment.getType(attachmentItem);
ResourceLocation attachmentId = iAttachment.getAttachmentId(attachmentItem);
return TimelessAPI.getCommonGunIndex(iGun.getGunId(gun)).map(gunIndex -> {
Map<AttachmentType, AttachmentPass> map = gunIndex.getGunData().getAllowAttachments();
if (map == null) {
return false;
}
AttachmentPass pass = map.get(type);
if (pass == null) {
return false;
}
return pass.isAllow(attachmentId);
}).orElse(false);
} else {
return false;
}
}
/**
* 该方法具有通用的实现,放在此处
*/
@Override
public boolean allowAttachmentType(ItemStack gun, AttachmentType type) {
IGun iGun = IGun.getIGunOrNull(gun);
if (iGun != null) {
return TimelessAPI.getCommonGunIndex(iGun.getGunId(gun)).map(gunIndex -> {
Map<AttachmentType, AttachmentPass> map = gunIndex.getGunData().getAllowAttachments();
if (map == null) {
return false;
}
return map.containsKey(type);
}).orElse(false);
} else {
return false;
}
}
/**
* 该方法具有通用的实现,放在此处
*/
@Override
@Nonnull
@OnlyIn(Dist.CLIENT)
public Component getName(@Nonnull ItemStack stack) {
ResourceLocation gunId = this.getGunId(stack);
Optional<ClientGunIndex> gunIndex = TimelessAPI.getClientGunIndex(gunId);
if (gunIndex.isPresent()) {
return new TranslatableComponent(gunIndex.get().getName());
}
return super.getName(stack);
}
/**
* 该方法具有通用的实现,放在此处
*/
@Override
@OnlyIn(Dist.CLIENT)
public void fillItemCategory(@Nonnull CreativeModeTab modeTab, @Nonnull NonNullList<ItemStack> stacks) {
if (modeTab instanceof CustomTab tab) {
String key = tab.getKey();
TimelessAPI.getAllClientGunIndex().stream().sorted(idNameSort()).forEach(entry -> {
ClientGunIndex index = entry.getValue();
if (key.equals(index.getType())) {
GunData gunData = index.getGunData();
ItemStack itemStack = GunItemBuilder.create()
.setId(entry.getKey())
.setFireMode(gunData.getFireModeSet().get(0))
.setAmmoCount(gunData.getAmmoAmount())
.setAmmoInBarrel(true)
.build();
if (canAddInTab(tab, itemStack)) {
stacks.add(itemStack);
}
}
});
}
}
/**
* 阻止玩家手臂挥动动画的播放
*/
@Override
public boolean onEntitySwing(ItemStack stack, LivingEntity entity) {
return true;
}
/**
* 该方法具有通用的实现,放在此处
*/
@Override
public void initializeClient(Consumer<IItemRenderProperties> consumer) {
consumer.accept(new IItemRenderProperties() {
@Override
public BlockEntityWithoutLevelRenderer getItemStackRenderer() {
Minecraft minecraft = Minecraft.getInstance();
return new GunItemRenderer(minecraft.getBlockEntityRenderDispatcher(), minecraft.getEntityModels());
}
});
}
/**
* 该方法具有通用的实现,放在此处
*/
@Override
@Nonnull
public Optional<TooltipComponent> getTooltipImage(ItemStack stack) {
if (stack.getItem() instanceof IGun iGun) {
Optional<CommonGunIndex> optional = TimelessAPI.getCommonGunIndex(this.getGunId(stack));
if (optional.isPresent()) {
CommonGunIndex gunIndex = optional.get();
ResourceLocation ammoId = gunIndex.getGunData().getAmmoId();
return Optional.of(new GunTooltip(stack, iGun, ammoId, gunIndex));
}
}
return Optional.empty();
}
}

View File

@@ -0,0 +1,26 @@
package com.tacz.guns.api.item.gun;
import com.google.gson.annotations.SerializedName;
public enum FireMode {
/**
* 全自动
*/
@SerializedName("auto")
AUTO,
/**
* 半自动
*/
@SerializedName("semi")
SEMI,
/**
* 多连发
*/
@SerializedName("burst")
BURST,
/**
* 未知的其他情况?
*/
@SerializedName("unknown")
UNKNOWN
}

View File

@@ -0,0 +1,26 @@
package com.tacz.guns.api.item.gun;
import com.google.common.collect.Maps;
import net.minecraftforge.registries.RegistryObject;
import java.util.Collection;
import java.util.Map;
public class GunItemManager {
private static final Map<String, RegistryObject<? extends AbstractGunItem>> GUN_ITEM_MAP = Maps.newHashMap();
/**
* 建议在 RegistryEvent.Register<Item> 事件时注册此枪械变种
*/
public static void registerGunItem(String name, RegistryObject<? extends AbstractGunItem> registryObject) {
GUN_ITEM_MAP.put(name, registryObject);
}
public static RegistryObject<? extends AbstractGunItem> getGunItemRegistryObject(String key) {
return GUN_ITEM_MAP.get(key);
}
public static Collection<RegistryObject<? extends AbstractGunItem>> getAllGunItems() {
return GUN_ITEM_MAP.values();
}
}

View File

@@ -0,0 +1,75 @@
package com.tacz.guns.api.item.nbt;
import com.tacz.guns.api.DefaultAssets;
import com.tacz.guns.api.TimelessAPI;
import com.tacz.guns.api.item.IAmmoBox;
import com.tacz.guns.api.item.IGun;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
public interface AmmoBoxItemDataAccessor extends IAmmoBox {
String AMMO_ID_TAG = "AmmoId";
String AMMO_COUNT_TAG = "AmmoCount";
String CREATIVE_TAG = "Creative";
@Override
default ResourceLocation getAmmoId(ItemStack ammoBox) {
CompoundTag tag = ammoBox.getOrCreateTag();
if (tag.contains(AMMO_ID_TAG, Tag.TAG_STRING)) {
return new ResourceLocation(tag.getString(AMMO_ID_TAG));
}
return DefaultAssets.EMPTY_AMMO_ID;
}
@Override
default void setAmmoId(ItemStack ammoBox, ResourceLocation ammoId) {
CompoundTag tag = ammoBox.getOrCreateTag();
tag.putString(AMMO_ID_TAG, ammoId.toString());
}
@Override
default int getAmmoCount(ItemStack ammoBox) {
CompoundTag tag = ammoBox.getOrCreateTag();
if (tag.contains(CREATIVE_TAG, Tag.TAG_BYTE)) {
return Integer.MAX_VALUE;
}
if (tag.contains(AMMO_COUNT_TAG, Tag.TAG_INT)) {
return tag.getInt(AMMO_COUNT_TAG);
}
return 0;
}
@Override
default void setAmmoCount(ItemStack ammoBox, int count) {
CompoundTag tag = ammoBox.getOrCreateTag();
if (tag.contains(CREATIVE_TAG, Tag.TAG_BYTE)) {
tag.putInt(AMMO_COUNT_TAG, Integer.MAX_VALUE);
return;
}
tag.putInt(AMMO_COUNT_TAG, count);
}
@Override
default boolean isAmmoBoxOfGun(ItemStack gun, ItemStack ammoBox) {
if (gun.getItem() instanceof IGun iGun && ammoBox.getItem() instanceof IAmmoBox iAmmoBox) {
ResourceLocation ammoId = iAmmoBox.getAmmoId(ammoBox);
if (ammoId.equals(DefaultAssets.EMPTY_AMMO_ID)) {
return false;
}
ResourceLocation gunId = iGun.getGunId(gun);
return TimelessAPI.getCommonGunIndex(gunId).map(gunIndex -> gunIndex.getGunData().getAmmoId().equals(ammoId)).orElse(false);
}
return false;
}
@Override
default boolean isCreative(ItemStack ammoBox) {
CompoundTag tag = ammoBox.getTag();
if (tag != null && tag.contains(CREATIVE_TAG, Tag.TAG_BYTE)) {
return tag.getBoolean(CREATIVE_TAG);
}
return false;
}
}

View File

@@ -0,0 +1,49 @@
package com.tacz.guns.api.item.nbt;
import com.tacz.guns.api.DefaultAssets;
import com.tacz.guns.api.TimelessAPI;
import com.tacz.guns.api.item.IAmmo;
import com.tacz.guns.api.item.IGun;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Objects;
public interface AmmoItemDataAccessor extends IAmmo {
String AMMO_ID_TAG = "AmmoId";
@Override
@Nonnull
default ResourceLocation getAmmoId(ItemStack ammo) {
CompoundTag nbt = ammo.getOrCreateTag();
if (nbt.contains(AMMO_ID_TAG, Tag.TAG_STRING)) {
ResourceLocation gunId = ResourceLocation.tryParse(nbt.getString(AMMO_ID_TAG));
return Objects.requireNonNullElse(gunId, DefaultAssets.EMPTY_AMMO_ID);
}
return DefaultAssets.EMPTY_AMMO_ID;
}
@Override
default void setAmmoId(ItemStack ammo, @Nullable ResourceLocation ammoId) {
CompoundTag nbt = ammo.getOrCreateTag();
if (ammoId != null) {
nbt.putString(AMMO_ID_TAG, ammoId.toString());
return;
}
nbt.putString(AMMO_ID_TAG, DefaultAssets.DEFAULT_AMMO_ID.toString());
}
@Override
default boolean isAmmoOfGun(ItemStack gun, ItemStack ammo) {
if (gun.getItem() instanceof IGun iGun && ammo.getItem() instanceof IAmmo iAmmo) {
ResourceLocation gunId = iGun.getGunId(gun);
ResourceLocation ammoId = iAmmo.getAmmoId(ammo);
return TimelessAPI.getCommonGunIndex(gunId).map(gunIndex -> gunIndex.getGunData().getAmmoId().equals(ammoId)).orElse(false);
}
return false;
}
}

View File

@@ -0,0 +1,72 @@
package com.tacz.guns.api.item.nbt;
import com.tacz.guns.api.DefaultAssets;
import com.tacz.guns.api.item.IAttachment;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Objects;
public interface AttachmentItemDataAccessor extends IAttachment {
String ATTACHMENT_ID_TAG = "AttachmentId";
String SKIN_ID_TAG = "Skin";
String ZOOM_NUMBER_TAG = "ZoomNumber";
@Override
@Nonnull
default ResourceLocation getAttachmentId(ItemStack attachmentStack) {
CompoundTag nbt = attachmentStack.getOrCreateTag();
if (nbt.contains(ATTACHMENT_ID_TAG, Tag.TAG_STRING)) {
ResourceLocation attachmentId = ResourceLocation.tryParse(nbt.getString(ATTACHMENT_ID_TAG));
return Objects.requireNonNullElse(attachmentId, DefaultAssets.EMPTY_ATTACHMENT_ID);
}
return DefaultAssets.EMPTY_ATTACHMENT_ID;
}
@Override
default void setAttachmentId(ItemStack attachmentStack, @Nullable ResourceLocation attachmentId) {
CompoundTag nbt = attachmentStack.getOrCreateTag();
if (attachmentId != null) {
nbt.putString(ATTACHMENT_ID_TAG, attachmentId.toString());
}
}
@Override
@Nullable
default ResourceLocation getSkinId(ItemStack attachmentStack) {
CompoundTag nbt = attachmentStack.getOrCreateTag();
if (nbt.contains(SKIN_ID_TAG, Tag.TAG_STRING)) {
return ResourceLocation.tryParse(nbt.getString(SKIN_ID_TAG));
}
return null;
}
@Override
default void setSkinId(ItemStack attachmentStack, @Nullable ResourceLocation skinId) {
CompoundTag nbt = attachmentStack.getOrCreateTag();
if (skinId != null) {
nbt.putString(SKIN_ID_TAG, skinId.toString());
} else {
nbt.remove(SKIN_ID_TAG);
}
}
@Override
default int getZoomNumber(ItemStack attachmentStack) {
CompoundTag nbt = attachmentStack.getOrCreateTag();
if (nbt.contains(ZOOM_NUMBER_TAG, Tag.TAG_INT)) {
return nbt.getInt(ZOOM_NUMBER_TAG);
}
return 0;
}
@Override
default void setZoomNumber(ItemStack attachmentStack, int zoomNumber) {
CompoundTag nbt = attachmentStack.getOrCreateTag();
nbt.putInt(ZOOM_NUMBER_TAG, zoomNumber);
}
}

View File

@@ -0,0 +1,200 @@
package com.tacz.guns.api.item.nbt;
import com.tacz.guns.api.DefaultAssets;
import com.tacz.guns.api.TimelessAPI;
import com.tacz.guns.api.item.IAttachment;
import com.tacz.guns.api.item.IGun;
import com.tacz.guns.api.item.attachment.AttachmentType;
import com.tacz.guns.api.item.gun.FireMode;
import com.tacz.guns.client.resource.index.ClientAttachmentIndex;
import com.tacz.guns.client.resource.index.ClientGunIndex;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Objects;
public interface GunItemDataAccessor extends IGun {
String GUN_ID_TAG = "GunId";
String GUN_FIRE_MODE_TAG = "GunFireMode";
String GUN_HAS_BULLET_IN_BARREL = "HasBulletInBarrel";
String GUN_CURRENT_AMMO_COUNT_TAG = "GunCurrentAmmoCount";
String GUN_ATTACHMENT_BASE = "Attachment";
String GUN_EXP_TAG = "GunLevelExp";
@Override
@Nonnull
default ResourceLocation getGunId(ItemStack gun) {
CompoundTag nbt = gun.getOrCreateTag();
if (nbt.contains(GUN_ID_TAG, Tag.TAG_STRING)) {
ResourceLocation gunId = ResourceLocation.tryParse(nbt.getString(GUN_ID_TAG));
return Objects.requireNonNullElse(gunId, DefaultAssets.EMPTY_GUN_ID);
}
return DefaultAssets.EMPTY_GUN_ID;
}
@Override
default void setGunId(ItemStack gun, @Nullable ResourceLocation gunId) {
CompoundTag nbt = gun.getOrCreateTag();
if (gunId != null) {
nbt.putString(GUN_ID_TAG, gunId.toString());
}
}
@Override
default int getLevel(ItemStack gun) {
CompoundTag nbt = gun.getOrCreateTag();
if (nbt.contains(GUN_EXP_TAG, Tag.TAG_INT)) {
return getLevel(nbt.getInt(GUN_EXP_TAG));
}
return 0;
}
@Override
default int getExp(ItemStack gun) {
CompoundTag nbt = gun.getOrCreateTag();
if (nbt.contains(GUN_EXP_TAG, Tag.TAG_INT)) {
return nbt.getInt(GUN_EXP_TAG);
}
return 0;
}
@Override
default int getExpToNextLevel(ItemStack gun) {
int exp = getExp(gun);
int level = getLevel(exp);
if (level >= getMaxLevel()) {
return 0;
}
int nextLevelExp = getExp(level + 1);
return nextLevelExp - exp;
}
@Override
default int getExpCurrentLevel(ItemStack gun) {
int exp = getExp(gun);
int level = getLevel(exp);
if (level <= 0) {
return exp;
} else {
return exp - getExp(level - 1);
}
}
@Override
default FireMode getFireMode(ItemStack gun) {
CompoundTag nbt = gun.getOrCreateTag();
if (nbt.contains(GUN_FIRE_MODE_TAG, Tag.TAG_STRING)) {
return FireMode.valueOf(nbt.getString(GUN_FIRE_MODE_TAG));
}
return FireMode.UNKNOWN;
}
@Override
default void setFireMode(ItemStack gun, @Nullable FireMode fireMode) {
CompoundTag nbt = gun.getOrCreateTag();
if (fireMode != null) {
nbt.putString(GUN_FIRE_MODE_TAG, fireMode.name());
return;
}
nbt.putString(GUN_FIRE_MODE_TAG, FireMode.UNKNOWN.name());
}
@Override
default int getCurrentAmmoCount(ItemStack gun) {
CompoundTag nbt = gun.getOrCreateTag();
if (nbt.contains(GUN_CURRENT_AMMO_COUNT_TAG, Tag.TAG_INT)) {
return nbt.getInt(GUN_CURRENT_AMMO_COUNT_TAG);
}
return 0;
}
@Override
default void setCurrentAmmoCount(ItemStack gun, int ammoCount) {
CompoundTag nbt = gun.getOrCreateTag();
nbt.putInt(GUN_CURRENT_AMMO_COUNT_TAG, Math.max(ammoCount, 0));
}
@Override
default void reduceCurrentAmmoCount(ItemStack gun) {
setCurrentAmmoCount(gun, getCurrentAmmoCount(gun) - 1);
}
@Override
@Nonnull
default ItemStack getAttachment(ItemStack gun, AttachmentType type) {
if (!allowAttachmentType(gun, type)) {
return ItemStack.EMPTY;
}
CompoundTag nbt = gun.getOrCreateTag();
String key = GUN_ATTACHMENT_BASE + type.name();
if (nbt.contains(key, Tag.TAG_COMPOUND)) {
return ItemStack.of(nbt.getCompound(key));
}
return ItemStack.EMPTY;
}
@Override
default void installAttachment(@Nonnull ItemStack gun, @Nonnull ItemStack attachment) {
if (!allowAttachment(gun, attachment)) {
return;
}
IAttachment iAttachment = IAttachment.getIAttachmentOrNull(attachment);
if (iAttachment == null) {
return;
}
CompoundTag nbt = gun.getOrCreateTag();
String key = GUN_ATTACHMENT_BASE + iAttachment.getType(attachment).name();
CompoundTag attachmentTag = new CompoundTag();
attachment.save(attachmentTag);
nbt.put(key, attachmentTag);
}
@Override
default void unloadAttachment(@Nonnull ItemStack gun, AttachmentType type) {
if (!allowAttachmentType(gun, type)) {
return;
}
CompoundTag nbt = gun.getOrCreateTag();
String key = GUN_ATTACHMENT_BASE + type.name();
CompoundTag attachmentTag = new CompoundTag();
ItemStack.EMPTY.save(attachmentTag);
nbt.put(key, attachmentTag);
}
@Override
default float getAimingZoom(ItemStack gunItem) {
float zoom = 1;
ItemStack scopeItem = this.getAttachment(gunItem, AttachmentType.SCOPE);
IAttachment iAttachment = IAttachment.getIAttachmentOrNull(scopeItem);
if (iAttachment != null) {
ResourceLocation scopeId = iAttachment.getAttachmentId(scopeItem);
int zoomNumber = iAttachment.getZoomNumber(scopeItem);
float[] zooms = TimelessAPI.getClientAttachmentIndex(scopeId).map(ClientAttachmentIndex::getZoom).orElse(null);
if (zooms != null) {
zoom = zooms[zoomNumber % zooms.length];
}
} else {
zoom = TimelessAPI.getClientGunIndex(this.getGunId(gunItem)).map(ClientGunIndex::getIronZoom).orElse(1f);
}
return zoom;
}
@Override
default boolean hasBulletInBarrel(ItemStack gun) {
CompoundTag nbt = gun.getOrCreateTag();
if (nbt.contains(GUN_HAS_BULLET_IN_BARREL, Tag.TAG_BYTE)) {
return nbt.getBoolean(GUN_HAS_BULLET_IN_BARREL);
}
return false;
}
@Override
default void setBulletInBarrel(ItemStack gun, boolean bulletInBarrel) {
CompoundTag nbt = gun.getOrCreateTag();
nbt.putBoolean(GUN_HAS_BULLET_IN_BARREL, bulletInBarrel);
}
}

View File

@@ -0,0 +1,130 @@
package com.tacz.guns.block;
import com.tacz.guns.block.entity.GunSmithTableBlockEntity;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.BedPart;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.DirectionProperty;
import net.minecraft.world.level.block.state.properties.EnumProperty;
import net.minecraft.world.level.material.Material;
import net.minecraft.world.level.material.PushReaction;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
import org.jetbrains.annotations.Nullable;
public class GunSmithTableBlock extends BaseEntityBlock {
public static final VoxelShape BLOCK_AABB = Block.box(0, 0, 0, 16, 15, 16);
public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING;
public static final EnumProperty<BedPart> PART = BlockStateProperties.BED_PART;
public GunSmithTableBlock() {
super(Properties.of(Material.WOOD).sound(SoundType.WOOD).strength(2.0F, 3.0F).noOcclusion());
this.registerDefaultState(this.stateDefinition.any().setValue(FACING, Direction.NORTH).setValue(PART, BedPart.FOOT));
}
private static Direction getNeighbourDirection(BedPart bedPart, Direction direction) {
return bedPart == BedPart.FOOT ? direction : direction.getOpposite();
}
@Override
public InteractionResult use(BlockState pState, Level level, BlockPos pos, Player player, InteractionHand pHand, BlockHitResult pHit) {
if (level.isClientSide) {
return InteractionResult.SUCCESS;
} else {
BlockEntity blockEntity = level.getBlockEntity(pos);
if (blockEntity instanceof GunSmithTableBlockEntity gunSmithTable) {
player.openMenu(gunSmithTable);
}
return InteractionResult.CONSUME;
}
}
@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
builder.add(FACING, PART);
}
@Nullable
@Override
public BlockEntity newBlockEntity(BlockPos pos, BlockState blockState) {
return new GunSmithTableBlockEntity(pos, blockState);
}
@Override
public BlockState getStateForPlacement(BlockPlaceContext context) {
Direction direction = context.getHorizontalDirection();
BlockPos clickedPos = context.getClickedPos();
BlockPos relative = clickedPos.relative(direction);
Level level = context.getLevel();
if (level.getBlockState(relative).canBeReplaced(context) && level.getWorldBorder().isWithinBounds(relative)) {
return this.defaultBlockState().setValue(FACING, direction);
}
return null;
}
@Override
public void playerWillDestroy(Level level, BlockPos pos, BlockState blockState, Player player) {
if (!level.isClientSide && player.isCreative()) {
BedPart bedPart = blockState.getValue(PART);
if (bedPart == BedPart.FOOT) {
BlockPos blockpos = pos.relative(getNeighbourDirection(bedPart, blockState.getValue(FACING)));
BlockState blockstate = level.getBlockState(blockpos);
if (blockstate.is(this) && blockstate.getValue(PART) == BedPart.HEAD) {
level.setBlock(blockpos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL | Block.UPDATE_SUPPRESS_DROPS);
level.levelEvent(player, LevelEvent.PARTICLES_DESTROY_BLOCK, blockpos, Block.getId(blockstate));
}
}
}
super.playerWillDestroy(level, pos, blockState, player);
}
@Override
public void setPlacedBy(Level worldIn, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) {
super.setPlacedBy(worldIn, pos, state, placer, stack);
if (!worldIn.isClientSide) {
BlockPos relative = pos.relative(state.getValue(FACING));
worldIn.setBlock(relative, state.setValue(PART, BedPart.HEAD), Block.UPDATE_ALL);
worldIn.blockUpdated(pos, Blocks.AIR);
state.updateNeighbourShapes(worldIn, pos, Block.UPDATE_ALL);
}
}
@Override
public BlockState updateShape(BlockState state, Direction direction, BlockState facingState, LevelAccessor level, BlockPos currentPos, BlockPos facingPos) {
if (direction == getNeighbourDirection(state.getValue(PART), state.getValue(FACING))) {
return facingState.is(this) && facingState.getValue(PART) != state.getValue(PART) ? state : Blocks.AIR.defaultBlockState();
} else {
return super.updateShape(state, direction, facingState, level, currentPos, facingPos);
}
}
@Override
public RenderShape getRenderShape(BlockState pState) {
return RenderShape.ENTITYBLOCK_ANIMATED;
}
@Override
public PushReaction getPistonPushReaction(BlockState state) {
return PushReaction.DESTROY;
}
@Override
public VoxelShape getShape(BlockState state, BlockGetter worldIn, BlockPos pos, CollisionContext context) {
return BLOCK_AABB;
}
}

View File

@@ -0,0 +1,235 @@
package com.tacz.guns.block;
import com.mojang.authlib.GameProfile;
import com.tacz.guns.block.entity.TargetBlockEntity;
import com.tacz.guns.init.ModBlocks;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.projectile.Projectile;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityTicker;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.*;
import net.minecraft.world.level.material.Material;
import net.minecraft.world.level.material.PushReaction;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape;
import org.jetbrains.annotations.Nullable;
import java.util.Random;
public class TargetBlock extends BaseEntityBlock {
public static final IntegerProperty OUTPUT_POWER = BlockStateProperties.POWER;
public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING;
public static final EnumProperty<DoubleBlockHalf> HALF = BlockStateProperties.DOUBLE_BLOCK_HALF;
public static final BooleanProperty STAND = BooleanProperty.create("stand");
public static final VoxelShape BOX_BOTTOM_STAND_X = Shapes.or(Block.box(6, 0, 6, 10, 16, 10), Block.box(6, 13, 2, 10, 16, 14));
public static final VoxelShape BOX_BOTTOM_STAND_Z = Shapes.or(Block.box(6, 0, 6, 10, 16, 10), Block.box(2, 13, 6, 14, 16, 10));
public static final VoxelShape BOX_BOTTOM_DOWN = Block.box(6, 0, 6, 10, 4, 10);
public static final VoxelShape BOX_UPPER_X = Block.box(6, 0, 2, 10, 16, 14);
public static final VoxelShape BOX_UPPER_Z = Block.box(2, 0, 6, 14, 16, 10);
public TargetBlock() {
super(Properties.of(Material.WOOD).sound(SoundType.WOOD).strength(2.0F, 3.0F).noOcclusion());
this.registerDefaultState(this.stateDefinition.any().setValue(FACING, Direction.NORTH).setValue(HALF, DoubleBlockHalf.LOWER).setValue(STAND, true).setValue(OUTPUT_POWER, 0));
}
public static int getRedstoneStrength(BlockHitResult hit, boolean isUpperBlock) {
// 击中下方,恒为 1
if (!isUpperBlock) {
return 1;
}
Vec3 hitLocation = hit.getLocation();
Direction direction = hit.getDirection();
// 标靶中心为 (0.5, 0.32, 0.5)
double x = Math.abs(Mth.frac(hitLocation.x) - 0.5);
double y = Math.abs(Mth.frac(hitLocation.y) - 0.32);
double z = Math.abs(Mth.frac(hitLocation.z) - 0.5);
Direction.Axis axis = direction.getAxis();
double distance;
if (axis == Direction.Axis.Y) {
distance = Math.max(x, z);
} else if (axis == Direction.Axis.Z) {
distance = Math.max(x, y);
} else {
distance = Math.max(y, z);
}
// 离开中心 0.25 单位就是最低分?
double percent = Mth.clamp((0.25 - distance) / 0.25, 0, 1);
return Math.max(1, Mth.ceil(15 * percent));
}
@Nullable
@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> blockEntityType) {
return state.getValue(HALF).equals(DoubleBlockHalf.LOWER) && level.isClientSide() ? createTickerHelper(blockEntityType, ModBlocks.TARGET_BE.get(), TargetBlockEntity::clientTick) : null;
}
@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
builder.add(FACING, HALF, STAND, OUTPUT_POWER);
}
@Nullable
@Override
public BlockEntity newBlockEntity(BlockPos pos, BlockState blockState) {
if (blockState.getValue(HALF).equals(DoubleBlockHalf.LOWER)) {
return new TargetBlockEntity(pos, blockState);
} else {
return null;
}
}
@Override
public VoxelShape getShape(BlockState state, BlockGetter worldIn, BlockPos pos, CollisionContext context) {
boolean stand = state.getValue(STAND);
boolean axis = state.getValue(FACING).getAxis().equals(Direction.Axis.X);
if (state.getValue(HALF).equals(DoubleBlockHalf.UPPER)) {
return stand ? (axis ? BOX_UPPER_X : BOX_UPPER_Z) : Shapes.empty();
}
return stand ? (axis ? BOX_BOTTOM_STAND_X : BOX_BOTTOM_STAND_Z) : BOX_BOTTOM_DOWN;
}
@Override
public void tick(BlockState state, ServerLevel level, BlockPos pos, Random random) {
// 计划刻的内容
if (!state.getValue(STAND)) {
level.setBlock(pos, state.setValue(STAND, true).setValue(OUTPUT_POWER, 0), Block.UPDATE_ALL);
}
}
@Override
public void onProjectileHit(Level world, BlockState state, BlockHitResult hit, Projectile projectile) {
if (hit.getDirection().getOpposite().equals(state.getValue(FACING))) {
if (state.getValue(HALF).equals(DoubleBlockHalf.LOWER)) {
world.getBlockEntity(hit.getBlockPos(), TargetBlockEntity.TYPE).ifPresent(e -> e.hit(world, state, hit, false));
} else if (state.getValue(HALF).equals(DoubleBlockHalf.UPPER)) {
world.getBlockEntity(hit.getBlockPos().below(), TargetBlockEntity.TYPE).ifPresent(e -> e.hit(world, state, hit, true));
}
}
}
@Override
public BlockState updateShape(BlockState state, Direction facing, BlockState facingState, LevelAccessor level, BlockPos currentPos, BlockPos facingPos) {
DoubleBlockHalf half = state.getValue(HALF);
boolean stand = state.getValue(STAND);
if (facing.getAxis() == Direction.Axis.Y) {
if (half.equals(DoubleBlockHalf.LOWER) && facing == Direction.UP || half.equals(DoubleBlockHalf.UPPER) && facing == Direction.DOWN) {
// 拆一半另外一半跟着没
if (!facingState.is(this)) {
return Blocks.AIR.defaultBlockState();
}
// 同步击倒状态
if (facingState.getValue(STAND) != stand) {
return state.setValue(STAND, facingState.getValue(STAND)).setValue(OUTPUT_POWER, facingState.getValue(OUTPUT_POWER));
}
}
}
// 底下方块没了也拆掉
if (half == DoubleBlockHalf.LOWER && facing == Direction.DOWN && !state.canSurvive(level, currentPos)) {
return Blocks.AIR.defaultBlockState();
} else {
return state;
}
}
@Override
public BlockState getStateForPlacement(BlockPlaceContext context) {
Direction direction = context.getHorizontalDirection();
BlockPos clickedPos = context.getClickedPos();
BlockPos above = clickedPos.above();
Level level = context.getLevel();
if (level.getBlockState(above).canBeReplaced(context) && level.getWorldBorder().isWithinBounds(above)) {
return this.defaultBlockState().setValue(FACING, direction);
}
return null;
}
@Override
public void setPlacedBy(Level world, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) {
super.setPlacedBy(world, pos, state, placer, stack);
if (!world.isClientSide) {
BlockPos above = pos.above();
world.setBlock(above, state.setValue(HALF, DoubleBlockHalf.UPPER), Block.UPDATE_ALL);
world.blockUpdated(pos, Blocks.AIR);
state.updateNeighbourShapes(world, pos, Block.UPDATE_ALL);
if (stack.hasCustomHoverName()) {
BlockEntity blockentity = world.getBlockEntity(pos);
if (blockentity instanceof TargetBlockEntity e) {
GameProfile gameprofile = new GameProfile(null, stack.getHoverName().getString());
e.setOwner(gameprofile);
e.setCustomName(stack.getHoverName());
e.refresh();
}
}
}
}
@Override
public ItemStack getCloneItemStack(BlockState state, HitResult target, BlockGetter level, BlockPos pos, Player player) {
BlockPos blockPos = state.getValue(HALF) == DoubleBlockHalf.LOWER ? pos : pos.below();
BlockEntity blockentity = level.getBlockEntity(blockPos);
if (blockentity instanceof TargetBlockEntity e) {
return new ItemStack(this).setHoverName(e.getCustomName());
}
return super.getCloneItemStack(state, target, level, pos, player);
}
@Override
public boolean canSurvive(BlockState state, LevelReader level, BlockPos pos) {
BlockPos blockpos = pos.below();
BlockState blockstate = level.getBlockState(blockpos);
if (state.getValue(HALF) == DoubleBlockHalf.LOWER) {
return true;
}
return blockstate.is(this);
}
@Override
public boolean isSignalSource(BlockState state) {
return true;
}
@Override
public int getSignal(BlockState blockState, BlockGetter blockAccess, BlockPos pos, Direction side) {
return blockState.getValue(OUTPUT_POWER);
}
@Override
public void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean isMoving) {
if (!level.isClientSide() && !state.is(oldState.getBlock())) {
if (state.getValue(OUTPUT_POWER) > 0 && !level.getBlockTicks().hasScheduledTick(pos, this)) {
level.setBlock(pos, state.setValue(OUTPUT_POWER, 0), Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS);
}
}
}
@Override
public RenderShape getRenderShape(BlockState state) {
return RenderShape.ENTITYBLOCK_ANIMATED;
}
@Override
public PushReaction getPistonPushReaction(BlockState state) {
return PushReaction.DESTROY;
}
}

View File

@@ -0,0 +1,52 @@
package com.tacz.guns.block.entity;
import com.tacz.guns.init.ModBlocks;
import com.tacz.guns.inventory.GunSmithTableMenu;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.TextComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientGamePacketListener;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.jetbrains.annotations.Nullable;
public class GunSmithTableBlockEntity extends BlockEntity implements MenuProvider {
public static final BlockEntityType<GunSmithTableBlockEntity> TYPE = BlockEntityType.Builder.of(GunSmithTableBlockEntity::new, ModBlocks.GUN_SMITH_TABLE.get()).build(null);
public GunSmithTableBlockEntity(BlockPos pos, BlockState blockState) {
super(TYPE, pos, blockState);
}
@Nullable
@Override
public Packet<ClientGamePacketListener> getUpdatePacket() {
return ClientboundBlockEntityDataPacket.create(this);
}
@Override
@OnlyIn(Dist.CLIENT)
public AABB getRenderBoundingBox() {
return new AABB(worldPosition.offset(-2, 0, -2), worldPosition.offset(2, 1, 2));
}
@Override
public Component getDisplayName() {
return new TextComponent("Gun Smith Table");
}
@Nullable
@Override
public AbstractContainerMenu createMenu(int id, Inventory inventory, Player player) {
return new GunSmithTableMenu(id, inventory);
}
}

View File

@@ -0,0 +1,145 @@
package com.tacz.guns.block.entity;
import com.mojang.authlib.GameProfile;
import com.tacz.guns.block.TargetBlock;
import com.tacz.guns.config.common.OtherConfig;
import com.tacz.guns.init.ModBlocks;
import com.tacz.guns.init.ModSounds;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.nbt.Tag;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.TextComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientGamePacketListener;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.Nameable;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.BlockHitResult;
import javax.annotation.Nullable;
import static com.tacz.guns.block.TargetBlock.OUTPUT_POWER;
import static com.tacz.guns.block.TargetBlock.STAND;
public class TargetBlockEntity extends BlockEntity implements Nameable {
public static final BlockEntityType<TargetBlockEntity> TYPE = BlockEntityType.Builder.of(TargetBlockEntity::new, ModBlocks.TARGET.get()).build(null);
/**
* 标靶复位时间,暂定为 5 秒
*/
private static final int RESET_TIME = 5 * 20;
private static final String OWNER_TAG = "Owner";
private static final String CUSTOM_NAME_TAG = "CustomName";
public float rot = 0;
public float oRot = 0;
private @Nullable GameProfile owner;
private @Nullable Component name;
public TargetBlockEntity(BlockPos pos, BlockState blockState) {
super(TYPE, pos, blockState);
}
public static void clientTick(Level level, BlockPos pos, BlockState state, TargetBlockEntity pBlockEntity) {
pBlockEntity.oRot = pBlockEntity.rot;
if (state.getValue(STAND)) {
pBlockEntity.rot = Math.max(pBlockEntity.rot - 18, 0);
} else {
pBlockEntity.rot = Math.min(pBlockEntity.rot + 45, 90);
}
}
@Nullable
public GameProfile getOwner() {
return owner;
}
public void setOwner(@Nullable GameProfile owner) {
this.owner = owner;
}
@Override
public void load(CompoundTag tag) {
super.load(tag);
if (tag.contains(OWNER_TAG, Tag.TAG_COMPOUND)) {
this.owner = NbtUtils.readGameProfile(tag.getCompound(OWNER_TAG));
}
if (tag.contains(CUSTOM_NAME_TAG, Tag.TAG_STRING)) {
this.name = Component.Serializer.fromJson(tag.getString(CUSTOM_NAME_TAG));
}
}
@Override
protected void saveAdditional(CompoundTag tag) {
super.saveAdditional(tag);
if (owner != null) {
tag.put(OWNER_TAG, NbtUtils.writeGameProfile(new CompoundTag(), owner));
}
if (this.name != null) {
tag.putString(CUSTOM_NAME_TAG, Component.Serializer.toJson(this.name));
}
}
@Override
public Component getName() {
return this.name != null ? this.name : TextComponent.EMPTY;
}
@Nullable
@Override
public Component getCustomName() {
return this.name;
}
public void setCustomName(Component name) {
this.name = name;
}
@Override
public Packet<ClientGamePacketListener> getUpdatePacket() {
return ClientboundBlockEntityDataPacket.create(this);
}
@Override
public CompoundTag getUpdateTag() {
return saveWithoutMetadata();
}
public void refresh() {
this.setChanged();
if (level != null) {
BlockState state = level.getBlockState(worldPosition);
level.sendBlockUpdated(worldPosition, state, state, Block.UPDATE_ALL);
}
}
@Override
public AABB getRenderBoundingBox() {
return new AABB(worldPosition.offset(-2, 0, -2), worldPosition.offset(2, 2, 2));
}
public void hit(Level level, BlockState state, BlockHitResult hit, boolean isUpperBlock) {
if (this.level != null && state.getValue(STAND)) {
BlockPos blockPos = hit.getBlockPos();
// 如果是击中上方,把状态移动到下方处理
if (isUpperBlock) {
blockPos = blockPos.below();
state = level.getBlockState(blockPos);
}
int redstoneStrength = TargetBlock.getRedstoneStrength(hit, isUpperBlock);
level.setBlock(blockPos, state.setValue(STAND, false).setValue(OUTPUT_POWER, redstoneStrength), Block.UPDATE_ALL);
level.scheduleTick(blockPos, state.getBlock(), RESET_TIME);
// 原版的声音传播距离由 volume 决定
// 当声音大于 1 时,距离为 = 16 * volume
float volume = OtherConfig.TARGET_SOUND_DISTANCE.get() / 16.0f;
volume = Math.max(volume, 0);
level.playSound(null, blockPos, ModSounds.TARGET_HIT.get(), SoundSource.BLOCKS, volume, this.level.random.nextFloat() * 0.1F + 0.9F);
}
}
}

View File

@@ -0,0 +1,9 @@
@ParametersAreNonnullByDefault
@MethodsReturnNonnullByDefault
@FieldsAreNonnullByDefault
package com.tacz.guns.block.entity;
import net.minecraft.FieldsAreNonnullByDefault;
import net.minecraft.MethodsReturnNonnullByDefault;
import javax.annotation.ParametersAreNonnullByDefault;

View File

@@ -0,0 +1,9 @@
@ParametersAreNonnullByDefault
@MethodsReturnNonnullByDefault
@FieldsAreNonnullByDefault
package com.tacz.guns.block;
import net.minecraft.FieldsAreNonnullByDefault;
import net.minecraft.MethodsReturnNonnullByDefault;
import javax.annotation.ParametersAreNonnullByDefault;

View File

@@ -0,0 +1,39 @@
package com.tacz.guns.client.animation;
import java.util.Arrays;
public class AnimationChannelContent {
public float[] keyframeTimeS;
/**
* 动画数值。该数组的第一维每个元素都与上面的 keyframeTime 顺序对应,
* 第二维可以是位移、旋转或缩放的值,其中旋转是四元数,数组长度可以为 8 或 4。(长度为 8 时,前四位存 Pre 数值,后四位存 Post 数值)
* 位移和缩放是三轴数值,数组长度可以为 6 或 3。
*/
public float[][] values;
/**
* 对于使用一般插值器的 Channel这个动画值没有意义。它专门用于 CustomInterpolator
*/
public LerpMode[] lerpModes;
public AnimationChannelContent() {
}
public AnimationChannelContent(AnimationChannelContent source) {
if (source.keyframeTimeS != null) {
this.keyframeTimeS = Arrays.copyOf(source.keyframeTimeS, source.keyframeTimeS.length);
}
if (source.values != null) {
// 深拷贝动画数值
this.values = Arrays.stream(source.values)
.map(values -> Arrays.copyOf(values, values.length))
.toArray(float[][]::new);
}
if (source.lerpModes != null) {
this.lerpModes = Arrays.copyOf(source.lerpModes, source.lerpModes.length);
}
}
public enum LerpMode {
LINEAR, SPHERICAL_LINEAR, CATMULLROM, SPHERICAL_CATMULLROM
}
}

View File

@@ -0,0 +1,155 @@
package com.tacz.guns.client.animation;
import com.google.common.collect.Maps;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.function.Supplier;
public class AnimationController {
protected final ArrayList<ObjectAnimationRunner> currentRunners = new ArrayList<>();
protected final ArrayList<Boolean> blending = new ArrayList<>();
private final AnimationListenerSupplier listenerSupplier;
private final ArrayList<Queue<AnimationPlan>> animationQueue = new ArrayList<>();
protected Map<String, ObjectAnimation> prototypes = Maps.newHashMap();
public AnimationController(List<ObjectAnimation> animationPrototypes, AnimationListenerSupplier model) {
for (ObjectAnimation prototype : animationPrototypes) {
if (prototype == null) {
continue;
}
prototypes.put(prototype.name, prototype);
}
this.listenerSupplier = model;
}
public void providePrototypeIfAbsent(String name, Supplier<ObjectAnimation> supplier) {
if (!prototypes.containsKey(name)) {
prototypes.put(name, supplier.get());
}
}
public boolean containPrototype(String name) {
return prototypes.containsKey(name);
}
@Nullable
public ObjectAnimationRunner getAnimation(int track) {
if (track >= currentRunners.size()) {
return null;
}
return currentRunners.get(track);
}
public void removeAnimation(int track) {
if (track < currentRunners.size()) {
currentRunners.set(track, null);
}
if (track < animationQueue.size()) {
animationQueue.set(track, null);
}
}
public void queueAnimation(int track, Queue<AnimationPlan> queue) {
// 确保数组长度正确
for (int i = animationQueue.size(); i <= track; i++) {
animationQueue.add(null);
}
animationQueue.set(track, queue);
if (queue != null) {
AnimationPlan plan = null;
while (plan == null && !queue.isEmpty()) {
plan = queue.poll();
}
if (plan != null) {
run(track, plan.animationName, plan.playType, plan.transitionTimeS);
}
}
}
public void runAnimation(int track, String animationName, ObjectAnimation.PlayType playType, float transitionTimeS) {
// 运行单个动画的时候视为执行一个只有一个动画的动画队列,因此需要清理旧的队列。
if (track < animationQueue.size()) {
animationQueue.set(track, null);
}
run(track, animationName, playType, transitionTimeS);
}
synchronized private void run(int track, String animationName, ObjectAnimation.PlayType playType, float transitionTimeS) {
ObjectAnimation prototype = prototypes.get(animationName);
if (prototype == null) {
return;
}
// 确保数组长度正确
for (int i = currentRunners.size(); i <= track; i++) {
currentRunners.add(null);
}
ObjectAnimation animation = new ObjectAnimation(prototype);
animation.applyAnimationListeners(listenerSupplier);
animation.playType = playType;
ObjectAnimationRunner runner = new ObjectAnimationRunner(animation);
runner.setProgressNs(0);
runner.run();
ObjectAnimationRunner oldRunner = currentRunners.get(track);
if (transitionTimeS > 0) {
if (oldRunner != null) {
oldRunner.transition(runner, (long) (transitionTimeS * 1e9));
} else {
currentRunners.set(track, runner);
}
} else {
currentRunners.set(track, runner);
}
}
public void setBlending(int track, boolean blend) {
// 确保数组长度正确
for (int i = blending.size(); i <= track; i++) {
blending.add(false);
}
blending.set(track, blend);
}
synchronized public void update() {
// 动画混合时track 级别越低,优先级越低(体现在旋转的叠加先后顺序上)
for (int i = currentRunners.size() - 1; i >= 0; i--) {
boolean blend = i < blending.size() ? blending.get(i) : false;
ObjectAnimationRunner runner = currentRunners.get(i);
if (runner == null) {
continue;
}
//更新当前动画runner
if (runner.isRunning() || runner.isHolding() || runner.isTransitioning()) {
runner.update(blend);
}
//更新过渡目标动画runner并且如果过渡已经完成将其塞进currentRunners
if (runner.getTransitionTo() != null) {
runner.getTransitionTo().update(blend);
if (!runner.isTransitioning()) {
currentRunners.set(i, runner.getTransitionTo());
runner = runner.getTransitionTo();
}
}
// 如果动画结束,检查队列是否有下一个动画,有则播放
if ((runner.isHolding() || runner.isStopped()) && !runner.isTransitioning()) {
if (i < animationQueue.size()) {
Queue<AnimationPlan> queue = animationQueue.get(i);
if (queue != null) {
AnimationPlan plan = null;
while (plan == null && !queue.isEmpty()) {
plan = queue.poll();
}
if (plan != null) {
run(i, plan.animationName, plan.playType, plan.transitionTimeS);
}
}
}
}
}
}
}

View File

@@ -0,0 +1,15 @@
package com.tacz.guns.client.animation;
public interface AnimationListener {
/**
* @param values When ChannelType is TRANSLATION, the length of values will be 3 and will store xyz offsets(NOT GLOBAL TRANSLATION!!! IT IS LOCAL OFFSET).
* When ChannelType is ROTATION, the length of values will be 4 and will store quaternions.(ALSO LOCAL ROTATION)
* When ChannelType is SCALE, the length of values will be 3 and will store xyz scale.(ALSO LOCAL SCALE)
* @param blend When blending, animation value should be accumulated, instead of being covered.
*/
void update(float[] values, boolean blend);
float[] recover();
ObjectAnimationChannel.ChannelType getType();
}

View File

@@ -0,0 +1,9 @@
package com.tacz.guns.client.animation;
import javax.annotation.Nullable;
public interface AnimationListenerSupplier {
@Nullable
AnimationListener supplyListeners(String nodeName, ObjectAnimationChannel.ChannelType type);
}

View File

@@ -0,0 +1,13 @@
package com.tacz.guns.client.animation;
public class AnimationPlan {
public String animationName;
public ObjectAnimation.PlayType playType;
public float transitionTimeS;
public AnimationPlan(String animationName, ObjectAnimation.PlayType playType, float transitionTimeS) {
this.animationName = animationName;
this.playType = playType;
this.transitionTimeS = transitionTimeS;
}
}

View File

@@ -0,0 +1,354 @@
package com.tacz.guns.client.animation;
import com.mojang.math.Vector3f;
import com.tacz.guns.GunMod;
import com.tacz.guns.client.animation.gltf.AccessorModel;
import com.tacz.guns.client.animation.gltf.AnimationModel;
import com.tacz.guns.client.animation.gltf.AnimationStructure;
import com.tacz.guns.client.animation.gltf.NodeModel;
import com.tacz.guns.client.animation.gltf.accessor.AccessorData;
import com.tacz.guns.client.animation.gltf.accessor.AccessorFloatData;
import com.tacz.guns.client.animation.interpolator.CustomInterpolator;
import com.tacz.guns.client.animation.interpolator.InterpolatorUtil;
import com.tacz.guns.client.model.BedrockAnimatedModel;
import com.tacz.guns.client.model.bedrock.BedrockModel;
import com.tacz.guns.client.model.bedrock.BedrockPart;
import com.tacz.guns.client.resource.pojo.animation.bedrock.AnimationBone;
import com.tacz.guns.client.resource.pojo.animation.bedrock.AnimationKeyframes;
import com.tacz.guns.client.resource.pojo.animation.bedrock.BedrockAnimation;
import com.tacz.guns.client.resource.pojo.animation.bedrock.BedrockAnimationFile;
import com.tacz.guns.client.resource.pojo.model.BonesItem;
import com.tacz.guns.util.math.MathUtil;
import it.unimi.dsi.fastutil.doubles.Double2ObjectMap;
import it.unimi.dsi.fastutil.doubles.Double2ObjectRBTreeMap;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class Animations {
public static AnimationController createControllerFromGltf(@Nonnull AnimationStructure structure, @Nonnull AnimationListenerSupplier supplier) {
return new AnimationController(createAnimationFromGltf(structure, (AnimationListenerSupplier[]) null), supplier);
}
public static @Nonnull List<ObjectAnimation> createAnimationFromGltf(@Nonnull AnimationStructure structure, @Nullable AnimationListenerSupplier... suppliers) {
List<ObjectAnimation> result = new ArrayList<>();
List<AnimationModel> animationModels = structure.getAnimationModels();
for (AnimationModel animationModel : animationModels) {
ObjectAnimation animation = new ObjectAnimation(animationModel.getName());
// 初始化动画轨道
List<AnimationModel.Channel> channelModels = animationModel.getChannels();
for (AnimationModel.Channel channelModel : channelModels) {
ObjectAnimationChannel channel = new ObjectAnimationChannel(ObjectAnimationChannel.ChannelType.valueOf(channelModel.path().toUpperCase()));
AnimationModel.Sampler sampler = channelModel.sampler();
// 初始化轨道的节点名称和插值器
AnimationModel.Interpolation interpolation = sampler.interpolation();
NodeModel nodeModel = channelModel.nodeModel();
// 四元数需要特殊的插值
if (channel.type.equals(ObjectAnimationChannel.ChannelType.ROTATION) && interpolation.equals(AnimationModel.Interpolation.LINEAR)) {
channel.interpolator = InterpolatorUtil.fromInterpolation(InterpolatorUtil.InterpolatorType.SLERP);
} else {
channel.interpolator = InterpolatorUtil.fromInterpolation(InterpolatorUtil.InterpolatorType.valueOf(interpolation.name()));
}
channel.node = nodeModel.getName();
// 初始化轨道的关键帧时间和关键帧数值
// 关键帧时间的访问器
AccessorModel input = sampler.input();
AccessorData inputData = input.getAccessorData();
if (!(inputData instanceof AccessorFloatData inputFloatData)) {
GunMod.LOGGER.warn("Input data is not an AccessorFloatData, but {}", inputData.getClass());
return result;
}
// 关键帧时间的访问器
AccessorModel output = sampler.output();
AccessorData outputData = output.getAccessorData();
if (!(outputData instanceof AccessorFloatData outputFloatData)) {
GunMod.LOGGER.warn("Output data is not an AccessorFloatData, but {}", inputData.getClass());
return result;
}
int numKeyElements = inputFloatData.getNumElements();
int numValuesElements = outputFloatData.getTotalNumComponents() / numKeyElements;
float[] keyframeTimeS = new float[numKeyElements];
float[][] values = new float[numKeyElements][numValuesElements];
for (int i = 0; i < numKeyElements; i++) {
keyframeTimeS[i] = inputFloatData.get(i);
for (int j = 0; j < numValuesElements; j++) {
values[i][j] = outputFloatData.get(i * numValuesElements + j);
}
}
channel.content.keyframeTimeS = keyframeTimeS;
channel.content.values = values;
// 加载完所有内容后编译插值器
channel.interpolator.compile(channel.content);
// 将轨道添加到动画
animation.addChannel(channel);
// 将动画监听器添加到动画中
if (suppliers != null) {
for (AnimationListenerSupplier supplier : suppliers) {
AnimationListener listener = supplier.supplyListeners(channel.node, channel.type);
if (listener != null) {
channel.addListener(listener);
}
}
}
}
// 将动画添加到结果列表
result.add(animation);
}
return result;
}
public static AnimationController createControllerFromBedrock(BedrockAnimationFile animationFile, BedrockAnimatedModel animatedModel) {
return new AnimationController(createAnimationFromBedrock(animationFile, animatedModel), animatedModel);
}
public static @Nonnull List<ObjectAnimation> createAnimationFromBedrock(BedrockAnimationFile animationFile, BedrockModel model) {
List<ObjectAnimation> result = new ArrayList<>();
for (Map.Entry<String, BedrockAnimation> animationEntry : animationFile.getAnimations().entrySet()) {
ObjectAnimation animation = new ObjectAnimation(animationEntry.getKey());
BedrockAnimation bedrockAnimation = animationEntry.getValue();
for (Map.Entry<String, AnimationBone> boneEntry : bedrockAnimation.getBones().entrySet()) {
AnimationBone bone = boneEntry.getValue();
AnimationKeyframes translationKeyframes = bone.getPosition();
AnimationKeyframes rotationKeyframes = bone.getRotation();
AnimationKeyframes scaleKeyframes = bone.getScale();
if (translationKeyframes != null) {
ObjectAnimationChannel translationChannel = new ObjectAnimationChannel(ObjectAnimationChannel.ChannelType.TRANSLATION);
translationChannel.node = boneEntry.getKey();
translationChannel.interpolator = new CustomInterpolator();
// 将位移数据转移进 AnimationChannel
writeBedrockTranslation(translationChannel, bone.getPosition(), model);
translationChannel.interpolator.compile(translationChannel.content);
animation.addChannel(translationChannel);
}
if (rotationKeyframes != null) {
ObjectAnimationChannel rotationChannel = new ObjectAnimationChannel(ObjectAnimationChannel.ChannelType.ROTATION);
rotationChannel.node = boneEntry.getKey();
rotationChannel.interpolator = new CustomInterpolator();
// 将旋转数据转移进 AnimationChannel
writeBedrockRotation(rotationChannel, bone.getRotation(), model);
rotationChannel.interpolator.compile(rotationChannel.content);
animation.addChannel(rotationChannel);
}
if (scaleKeyframes != null) {
ObjectAnimationChannel scaleChannel = new ObjectAnimationChannel(ObjectAnimationChannel.ChannelType.SCALE);
scaleChannel.node = boneEntry.getKey();
scaleChannel.interpolator = new CustomInterpolator();
// 将缩放数据转移进 AnimationChannel
writeBedrockScale(scaleChannel, bone.getScale());
scaleChannel.interpolator.compile(scaleChannel.content);
animation.addChannel(scaleChannel);
}
}
result.add(animation);
}
return result;
}
private static void writeBedrockTranslation(ObjectAnimationChannel animationChannel, AnimationKeyframes keyframes, BedrockModel model) {
// 基岩版动画中储存的动画数据为相对值,而 tac 的动画系统使用的是绝对值,所以需要叠加初始值。
// 此处就是在获取动画数据的初始值。
Vector3f base;
BedrockPart node = model.getNode(animationChannel.node);
if (node != null) {
if (node.getParent() != null) {
base = new Vector3f(node.x, -node.y, node.z);
} else {
BonesItem bone = model.getBone(animationChannel.node);
base = new Vector3f(-bone.getPivot().get(0), bone.getPivot().get(1), bone.getPivot().get(2));
}
} else {
base = new Vector3f(0, 0, 0);
}
Double2ObjectRBTreeMap<AnimationKeyframes.Keyframe> keyframesMap = keyframes.getKeyframes();
animationChannel.content.keyframeTimeS = new float[keyframesMap.size()];
animationChannel.content.values = new float[keyframesMap.size()][];
animationChannel.content.lerpModes = new AnimationChannelContent.LerpMode[keyframesMap.size()];
int index = 0;
for (Double2ObjectMap.Entry<AnimationKeyframes.Keyframe> entry : keyframesMap.double2ObjectEntrySet()) {
// 写入关键帧时间
animationChannel.content.keyframeTimeS[index] = (float) entry.getDoubleKey();
// 写入关键帧数值。
AnimationKeyframes.Keyframe keyframe = entry.getValue();
if (keyframe.pre() != null || keyframe.post() != null) {
if (keyframe.pre() != null && keyframe.post() != null) {
animationChannel.content.values[index] = new float[6];
Vector3f pre = keyframe.pre().copy();
Vector3f post = keyframe.post().copy();
pre.add(base);
post.add(base);
pre.mul(-1 / 16f, 1 / 16f, 1 / 16f);
post.mul(-1 / 16f, 1 / 16f, 1 / 16f);
readVector3fToArray(animationChannel.content.values[index], pre, 0);
readVector3fToArray(animationChannel.content.values[index], post, 3);
} else if (keyframe.pre() != null) {
animationChannel.content.values[index] = new float[3];
Vector3f pre = keyframe.pre().copy();
pre.add(base);
pre.mul(-1 / 16f, 1 / 16f, 1 / 16f);
readVector3fToArray(animationChannel.content.values[index], pre, 0);
} else {
animationChannel.content.values[index] = new float[3];
Vector3f post = keyframe.post().copy();
post.add(base);
post.mul(-1 / 16f, 1 / 16f, 1 / 16f);
readVector3fToArray(animationChannel.content.values[index], post, 0);
}
} else if (keyframe.data() != null) {
animationChannel.content.values[index] = new float[3];
Vector3f data = keyframe.data().copy();
data.add(base);
data.mul(-1 / 16f, 1 / 16f, 1 / 16f);
readVector3fToArray(animationChannel.content.values[index], data, 0);
}
// 写入关键帧插值类型
String lerpModeName = keyframe.lerpMode();
if (lerpModeName != null) {
try {
animationChannel.content.lerpModes[index] = AnimationChannelContent.LerpMode.valueOf(lerpModeName.toUpperCase());
} catch (IllegalArgumentException e) {
animationChannel.content.lerpModes[index] = AnimationChannelContent.LerpMode.LINEAR;
}
} else {
animationChannel.content.lerpModes[index] = AnimationChannelContent.LerpMode.LINEAR;
}
index++;
}
}
private static void writeBedrockRotation(ObjectAnimationChannel animationChannel, AnimationKeyframes keyframes, BedrockModel model) {
Vector3f base;
BedrockPart node = model.getNode(animationChannel.node);
if (node != null) {
// 基岩版模型上下颠倒x、y轴运动需要反向。
base = new Vector3f(-node.xRot, -node.yRot, node.zRot);
} else {
base = new Vector3f(0, 0, 0);
}
Double2ObjectRBTreeMap<AnimationKeyframes.Keyframe> keyframesMap = keyframes.getKeyframes();
animationChannel.content.keyframeTimeS = new float[keyframesMap.size()];
animationChannel.content.values = new float[keyframesMap.size()][];
animationChannel.content.lerpModes = new AnimationChannelContent.LerpMode[keyframesMap.size()];
int index = 0;
for (Double2ObjectMap.Entry<AnimationKeyframes.Keyframe> entry : keyframesMap.double2ObjectEntrySet()) {
// 写入关键帧时间
animationChannel.content.keyframeTimeS[index] = (float) entry.getDoubleKey();
// 写入关键帧数值。
AnimationKeyframes.Keyframe keyframe = entry.getValue();
if (keyframe.pre() != null || keyframe.post() != null) {
if (keyframe.pre() != null && keyframe.post() != null) {
animationChannel.content.values[index] = new float[8];
Vector3f pre = keyframe.pre().copy();
Vector3f post = keyframe.post().copy();
toAngle(pre);
toAngle(post);
pre.mul(-1, -1, 1);
post.mul(-1, -1, 1);
float[] q1 = MathUtil.toQuaternion(pre.x() + base.x(), pre.y() + base.y(), pre.z() + base.z());
float[] q2 = MathUtil.toQuaternion(post.x() + base.x(), post.y() + base.y(), post.z() + base.z());
System.arraycopy(q1, 0, animationChannel.content.values[index], 0, 4);
System.arraycopy(q2, 0, animationChannel.content.values[index], 4, 4);
} else if (keyframe.pre() != null) {
animationChannel.content.values[index] = new float[4];
Vector3f pre = keyframe.pre().copy();
toAngle(pre);
pre.mul(-1, -1, 1);
float[] q = MathUtil.toQuaternion(pre.x() + base.x(), pre.y() + base.y(), pre.z() + base.z());
System.arraycopy(q, 0, animationChannel.content.values[index], 0, 4);
} else {
animationChannel.content.values[index] = new float[4];
Vector3f post = keyframe.post().copy();
toAngle(post);
post.mul(-1, -1, 1);
float[] q = MathUtil.toQuaternion(post.x() + base.x(), post.y() + base.y(), post.z() + base.z());
System.arraycopy(q, 0, animationChannel.content.values[index], 0, 4);
}
} else if (keyframe.data() != null) {
animationChannel.content.values[index] = new float[4];
Vector3f data = keyframe.data().copy();
toAngle(data);
data.mul(-1, -1, 1);
float[] q = MathUtil.toQuaternion(data.x() + base.x(), data.y() + base.y(), data.z() + base.z());
System.arraycopy(q, 0, animationChannel.content.values[index], 0, 4);
}
String lerpModeName = keyframe.lerpMode();
if (lerpModeName != null) {
if (lerpModeName.equals(AnimationChannelContent.LerpMode.CATMULLROM.name().toLowerCase())) {
animationChannel.content.lerpModes[index] = AnimationChannelContent.LerpMode.SPHERICAL_CATMULLROM;
} else {
animationChannel.content.lerpModes[index] = AnimationChannelContent.LerpMode.SPHERICAL_LINEAR;
}
} else {
animationChannel.content.lerpModes[index] = AnimationChannelContent.LerpMode.SPHERICAL_LINEAR;
}
index++;
}
}
private static void writeBedrockScale(ObjectAnimationChannel animationChannel, AnimationKeyframes keyframes) {
Double2ObjectRBTreeMap<AnimationKeyframes.Keyframe> keyframesMap = keyframes.getKeyframes();
animationChannel.content.keyframeTimeS = new float[keyframesMap.size()];
animationChannel.content.values = new float[keyframesMap.size()][];
animationChannel.content.lerpModes = new AnimationChannelContent.LerpMode[keyframesMap.size()];
int index = 0;
for (Double2ObjectMap.Entry<AnimationKeyframes.Keyframe> entry : keyframesMap.double2ObjectEntrySet()) {
// 写入关键帧时间
animationChannel.content.keyframeTimeS[index] = (float) entry.getDoubleKey();
// 写入关键帧数值。
AnimationKeyframes.Keyframe keyframe = entry.getValue();
if (keyframe.pre() != null || keyframe.post() != null) {
if (keyframe.pre() != null && keyframe.post() != null) {
animationChannel.content.values[index] = new float[6];
Vector3f pre = keyframe.pre();
Vector3f post = keyframe.post();
readVector3fToArray(animationChannel.content.values[index], pre, 0);
readVector3fToArray(animationChannel.content.values[index], post, 3);
} else if (keyframe.pre() != null) {
animationChannel.content.values[index] = new float[3];
Vector3f pre = keyframe.pre();
readVector3fToArray(animationChannel.content.values[index], pre, 0);
} else {
animationChannel.content.values[index] = new float[3];
Vector3f post = keyframe.post();
readVector3fToArray(animationChannel.content.values[index], post, 0);
}
} else if (keyframe.data() != null) {
animationChannel.content.values[index] = new float[3];
Vector3f data = keyframe.data();
readVector3fToArray(animationChannel.content.values[index], data, 0);
}
// 写入关键帧插值类型
String lerpModeName = keyframe.lerpMode();
if (lerpModeName != null) {
try {
animationChannel.content.lerpModes[index] = AnimationChannelContent.LerpMode.valueOf(lerpModeName.toUpperCase());
} catch (IllegalArgumentException e) {
animationChannel.content.lerpModes[index] = AnimationChannelContent.LerpMode.LINEAR;
}
} else {
animationChannel.content.lerpModes[index] = AnimationChannelContent.LerpMode.LINEAR;
}
index++;
}
}
private static void toAngle(Vector3f vector3f) {
vector3f.set((float) Math.toRadians(vector3f.x()), (float) Math.toRadians(vector3f.y()), (float) Math.toRadians(vector3f.z()));
}
private static void readVector3fToArray(float[] array, Vector3f vector3f, int offset) {
array[offset] = vector3f.x();
array[offset + 1] = vector3f.y();
array[offset + 2] = vector3f.z();
}
}

View File

@@ -0,0 +1,114 @@
package com.tacz.guns.client.animation;
import javax.annotation.Nonnull;
import java.util.*;
/**
* 创建一个 {@link ObjectAnimationRunner} 实例以运行 {@link ObjectAnimation}
*/
public class ObjectAnimation {
/**
* 动画名称
*/
public final String name;
/**
* 此 map 的 key 是节点名称
*/
private final Map<String, List<ObjectAnimationChannel>> channels = new HashMap<>();
/**
* 播放类型
*/
public @Nonnull PlayType playType = PlayType.PLAY_ONCE_HOLD;
/**
* 当前播放进度时间,以纳秒为单位
*/
public long timeNs = 0;
/**
* 所有轨道的最大结束时间 {@link ObjectAnimationChannel#getEndTimeS()}
*/
private float maxEndTimeS = 0f;
protected ObjectAnimation(@Nonnull String name) {
this.name = Objects.requireNonNull(name);
}
/**
* 创建源对象动画的拷贝,
* 新对象动画的值与源动画的值相同,
* 但新对象动画不会包含任何动画监听器。
*/
public ObjectAnimation(ObjectAnimation source) {
this.name = source.name;
this.playType = source.playType;
this.maxEndTimeS = source.maxEndTimeS;
this.timeNs = source.timeNs;
for (Map.Entry<String, List<ObjectAnimationChannel>> entry : source.channels.entrySet()) {
List<ObjectAnimationChannel> newList = new ArrayList<>();
for (ObjectAnimationChannel channel : entry.getValue()) {
ObjectAnimationChannel newChannel = new ObjectAnimationChannel(channel.type, channel.content);
newChannel.node = channel.node;
newChannel.interpolator = channel.interpolator;
newList.add(newChannel);
}
this.channels.put(entry.getKey(), newList);
}
}
protected void addChannel(ObjectAnimationChannel channel) {
channels.compute(channel.node, (node, list) -> {
if (list == null) {
list = new ArrayList<>();
}
list.add(channel);
return list;
});
if (channel.getEndTimeS() > maxEndTimeS) {
maxEndTimeS = channel.getEndTimeS();
}
}
public Map<String, List<ObjectAnimationChannel>> getChannels() {
return channels;
}
public void applyAnimationListeners(AnimationListenerSupplier supplier) {
for (List<ObjectAnimationChannel> channelList : channels.values()) {
for (ObjectAnimationChannel channel : channelList) {
AnimationListener listener = supplier.supplyListeners(channel.node, channel.type);
if (listener != null) {
channel.addListener(listener);
}
}
}
}
/**
* 触发所有监听器,通知它们更新相关数值
*/
public void update(boolean blend) {
for (List<ObjectAnimationChannel> channels : channels.values()) {
for (ObjectAnimationChannel channel : channels) {
channel.update(timeNs / 1e9f, blend);
}
}
}
public float getMaxEndTimeS() {
return maxEndTimeS;
}
public enum PlayType {
/**
* 播放一次,停留在最后一帧
*/
PLAY_ONCE_HOLD,
/**
* 播放一次后停止
*/
PLAY_ONCE_STOP,
/**
* 循环播放
*/
LOOP
}
}

View File

@@ -0,0 +1,119 @@
package com.tacz.guns.client.animation;
import com.tacz.guns.client.animation.interpolator.Interpolator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class ObjectAnimationChannel {
public final ChannelType type;
private final List<AnimationListener> listeners = new ArrayList<>();
/**
* 节点名称
*/
public String node;
/**
* 这个轨道的内容,包括关键帧
*/
public AnimationChannelContent content;
public Interpolator interpolator;
/**
* 此变量用于动画过渡,
* 如果你不明白在做什么,请不要更改它
*/
boolean transitioning = false;
public ObjectAnimationChannel(ChannelType type) {
this.type = type;
this.content = new AnimationChannelContent();
}
public ObjectAnimationChannel(ChannelType type, AnimationChannelContent content) {
this.type = type;
this.content = content;
}
public void addListener(AnimationListener listener) {
if (listener.getType().equals(type)) {
listeners.add(listener);
} else {
throw new RuntimeException("trying to add wrong type of listener to channel.");
}
}
public void removeListener(AnimationListener listener) {
listeners.remove(listener);
}
public void clearListeners() {
listeners.clear();
}
public List<AnimationListener> getListeners() {
return listeners;
}
public float getEndTimeS() {
return content.keyframeTimeS[content.keyframeTimeS.length - 1];
}
/**
* 根据输入时间执行计算,并将结果通知所有 AnimationListener
*
* @param timeS 绝对时间(以秒为单位)
*/
public void update(float timeS, boolean blend) {
if (!transitioning) {
float[] result = getResult(timeS);
for (AnimationListener listener : listeners) {
listener.update(result, blend);
}
}
}
public float[] getResult(float timeS) {
int indexFrom = computeIndex(timeS);
int indexTo = Math.min(content.keyframeTimeS.length - 1, indexFrom + 1);
float alpha = computeAlpha(timeS, indexFrom);
int resultLength = type == ChannelType.ROTATION ? 4 : 3;
float[] result = new float[resultLength];
interpolator.interpolate(indexFrom, indexTo, alpha, result);
return result;
}
private int computeIndex(float timeS) {
int index = Arrays.binarySearch(content.keyframeTimeS, timeS);
if (index >= 0) {
return index;
}
return Math.max(0, -index - 2);
}
private float computeAlpha(float timeS, int indexFrom) {
if (timeS <= content.keyframeTimeS[0]) {
return 0.0f;
}
if (timeS >= content.keyframeTimeS[content.keyframeTimeS.length - 1]) {
return 1.0f;
}
float local = timeS - content.keyframeTimeS[indexFrom];
float delta = content.keyframeTimeS[indexFrom + 1] - content.keyframeTimeS[indexFrom];
return local / delta;
}
public enum ChannelType {
/**
* 位移
*/
TRANSLATION,
/**
* 旋转
*/
ROTATION,
/**
* 缩放
*/
SCALE
}
}

View File

@@ -0,0 +1,366 @@
package com.tacz.guns.client.animation;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;
public class ObjectAnimationRunner {
@Nonnull
private final ObjectAnimation animation;
protected long transitionTimeNs;
/**
* 用于动画过渡储存的是过渡起点动画的值与下方transitionFromChannels一一对应
*/
protected ArrayList<float[]> valueFrom;
/**
* 用于动画过渡储存的是过渡起点需要恢复原位的channel的值与下方recoverChannels一一对应
*/
protected ArrayList<float[]> valueRecover;
/**
* 用于动画过渡储存的是过渡起点动画的channel
*/
protected ArrayList<ObjectAnimationChannel> transitionFromChannels;
/**
* 用于动画过渡储存的是过渡终点动画的channel顺序与上面对应
*/
protected ArrayList<ObjectAnimationChannel> transitionToChannels;
/**
* 用于动画过渡储存的是过渡起点动画需要恢复到原位的channel
*/
protected ArrayList<ObjectAnimationChannel> recoverChannels;
private boolean running = false;
private long lastUpdateNs;
/**
* 当前动画播放进度
*/
private long progressNs;
private boolean isTransitioning = false;
@Nullable
private ObjectAnimationRunner transitionTo;
private long transitionProgressNs;
public ObjectAnimationRunner(@Nonnull ObjectAnimation animation) {
this.animation = Objects.requireNonNull(animation);
}
public @Nonnull ObjectAnimation getAnimation() {
return animation;
}
public @Nullable ObjectAnimationRunner getTransitionTo() {
return transitionTo;
}
public boolean isTransitioning() {
return isTransitioning;
}
public void run() {
if (!running) {
running = true;
lastUpdateNs = System.nanoTime();
}
}
public void pause() {
running = false;
}
public void hold() {
progressNs = (long) (animation.getMaxEndTimeS() * 1e9) + 1;
pause();
}
public void stop() {
progressNs = (long) (animation.getMaxEndTimeS() * 1e9) + 2;
pause();
}
public void reset() {
progressNs = 0;
}
public long getProgressNs() {
return progressNs;
}
public void setProgressNs(long progressNs) {
this.progressNs = progressNs;
}
public void transition(ObjectAnimationRunner transitionTo, long transitionTimeNS) {
if (this.transitionTo == null) {
this.valueFrom = new ArrayList<>();
this.valueRecover = new ArrayList<>();
this.transitionFromChannels = new ArrayList<>();
this.transitionToChannels = new ArrayList<>();
this.recoverChannels = new ArrayList<>();
this.transitionTo = transitionTo;
this.pause();
for (Map.Entry<String, List<ObjectAnimationChannel>> entry : animation.getChannels().entrySet()) {
List<ObjectAnimationChannel> toChannels = transitionTo.animation.getChannels().get(entry.getKey());
if (toChannels != null) {
// 如果过渡终点的动画中同一个node 包含相同类型的动画数据(位移、旋转、缩放),那么加入到 list 中用于更新。
for (ObjectAnimationChannel channel : entry.getValue()) {
Optional<ObjectAnimationChannel> toChannel =
toChannels.stream().filter(c -> c.type.equals(channel.type)).findAny();
if (toChannel.isPresent()) {
valueFrom.add(channel.getResult(progressNs / 1e9f));
transitionFromChannels.add(channel);
transitionToChannels.add(toChannel.get());
// 取消过渡目标的channel对模型的更新统一在起点channel进行更新。
toChannel.get().transitioning = true;
} else {
valueRecover.add(channel.getResult(progressNs / 1e9f));
recoverChannels.add(channel);
}
}
} else {
// 如果过渡终点的动画中 同一个 node 不包含动画数据,那么将过渡到原位。
for (ObjectAnimationChannel channel : entry.getValue()) {
valueRecover.add(channel.getResult(progressNs / 1e9f));
recoverChannels.add(channel);
}
}
}
} else if (isTransitioning) {
ArrayList<float[]> newValueFrom = new ArrayList<>();
ArrayList<float[]> newValueRecover = new ArrayList<>();
ArrayList<ObjectAnimationChannel> newTransitionFromChannels = new ArrayList<>();
ArrayList<ObjectAnimationChannel> newTransitionToChannels = new ArrayList<>();
ArrayList<ObjectAnimationChannel> newRecoverChannels = new ArrayList<>();
// 如果正在过渡,则需要把当前过渡计算出的插值保存,作为下次过渡的起点
for (int i = 0; i < transitionFromChannels.size(); i++) {
assert this.transitionTo != null;
ObjectAnimationChannel fromChannel = transitionFromChannels.get(i);
ObjectAnimationChannel toChannel = transitionToChannels.get(i);
float[] from = valueFrom.get(i);
float[] to = toChannel.getResult(this.transitionTo.progressNs / 1e9f);
float[] result = new float[from.length];
float progress = easeOutCubic((float) transitionProgressNs / transitionTimeNs);
if (fromChannel.type.equals(ObjectAnimationChannel.ChannelType.TRANSLATION)) {
lerp(from, to, progress, result);
} else if (fromChannel.type.equals(ObjectAnimationChannel.ChannelType.ROTATION)) {
slerp(from, to, progress, result);
} else if (fromChannel.type.equals(ObjectAnimationChannel.ChannelType.SCALE)) {
lerp(from, to, progress, result);
}
List<ObjectAnimationChannel> newToChannels = transitionTo.animation.getChannels().get(fromChannel.node);
if (newToChannels != null) {
Optional<ObjectAnimationChannel> newToChannel =
newToChannels.stream().filter(c -> c.type.equals(fromChannel.type)).findAny();
if (newToChannel.isPresent()) {
newValueFrom.add(result);
newTransitionFromChannels.add(fromChannel);
newTransitionToChannels.add(newToChannel.get());
// 取消过渡目标的channel对模型的更新统一在起点channel进行更新。
newToChannel.get().transitioning = true;
} else {
newValueRecover.add(result);
newRecoverChannels.add(fromChannel);
}
} else {
newValueRecover.add(result);
newRecoverChannels.add(fromChannel);
}
toChannel.transitioning = false;
}
this.valueFrom = newValueFrom;
this.valueRecover = newValueRecover;
this.transitionToChannels = newTransitionToChannels;
this.transitionFromChannels = newTransitionFromChannels;
this.recoverChannels = newRecoverChannels;
this.transitionTo = transitionTo;
}
this.transitionTimeNs = transitionTimeNS;
this.transitionProgressNs = 0;
this.isTransitioning = true;
}
public long getTransitionTimeNs() {
return transitionTimeNs;
}
public long getTransitionProgressNs() {
return transitionProgressNs;
}
public void setTransitionProgressNs(long progressNs) {
this.transitionProgressNs = progressNs;
}
public void stopTransition() {
this.isTransitioning = false;
for (ObjectAnimationChannel channel : transitionToChannels) {
channel.transitioning = false;
}
this.transitionTimeNs = 0;
this.transitionProgressNs = 0;
this.transitionFromChannels = null;
this.transitionToChannels = null;
this.recoverChannels = null;
this.valueFrom = null;
this.valueRecover = null;
}
public void update(boolean blend) {
long currentNs = System.nanoTime();
if (running) {
progressNs += currentNs - lastUpdateNs;
}
switch (animation.playType) {
case PLAY_ONCE_HOLD -> {
if (progressNs / 1e9 > animation.getMaxEndTimeS()) {
hold();
}
}
case PLAY_ONCE_STOP -> {
if (progressNs / 1e9 > animation.getMaxEndTimeS()) {
stop();
}
}
case LOOP -> {
if (progressNs / 1e9 > animation.getMaxEndTimeS()) {
if (animation.getMaxEndTimeS() == 0) {
progressNs = 0;
} else {
progressNs = progressNs % (long) (animation.getMaxEndTimeS() * 1e9);
}
}
}
}
animation.timeNs = progressNs;
if (isTransitioning) {
transitionProgressNs += currentNs - lastUpdateNs;
if (transitionProgressNs >= transitionTimeNs) {
stopTransition();
} else {
float transitionProgress = (float) transitionProgressNs / transitionTimeNs;
updateTransition(easeOutCubic(transitionProgress), blend);
}
} else {
animation.update(blend);
}
lastUpdateNs = currentNs;
}
public boolean isRunning() {
return running;
}
public boolean isHolding() {
return progressNs == (long) (getAnimation().getMaxEndTimeS() * 1e9) + 1;
}
public boolean isStopped() {
return progressNs == (long) (getAnimation().getMaxEndTimeS() * 1e9) + 2;
}
/**
* 动画过渡的时候计算出的插值将通过当前Runner中包含的ObjectAnimation中的channel对模型进行update
* 这意味着需要暂时取消transitionTo中对应channel的update功能(将变量available设置为false)
*/
private void updateTransition(float progress, boolean blend) {
assert transitionTo != null;
for (int i = 0; i < transitionToChannels.size(); i++) {
ObjectAnimationChannel fromChannel = transitionFromChannels.get(i);
ObjectAnimationChannel toChannel = transitionToChannels.get(i);
float[] from = valueFrom.get(i);
float[] to = toChannel.getResult(transitionTo.progressNs / 1e9f);
float[] result = new float[from.length];
if (fromChannel.type.equals(ObjectAnimationChannel.ChannelType.TRANSLATION)) {
lerp(from, to, progress, result);
} else if (fromChannel.type.equals(ObjectAnimationChannel.ChannelType.ROTATION)) {
slerp(from, to, progress, result);
} else if (fromChannel.type.equals(ObjectAnimationChannel.ChannelType.SCALE)) {
lerp(from, to, progress, result);
}
for (AnimationListener listener : fromChannel.getListeners()) {
listener.update(result, blend);
}
}
if (animation.playType != ObjectAnimation.PlayType.PLAY_ONCE_STOP) {
// 如果是 PLAY_ONCE_STOP动画结束后不应该 update 其本身的关键帧,因此不进行恢复过渡
for (int i = 0; i < recoverChannels.size(); i++) {
ObjectAnimationChannel channel = recoverChannels.get(i);
float[] from = valueRecover.get(i);
float[] result = new float[from.length];
if (channel.type.equals(ObjectAnimationChannel.ChannelType.TRANSLATION)) {
for (AnimationListener listener : channel.getListeners()) {
float[] to = listener.recover();
lerp(from, to, progress, result);
listener.update(result, blend);
}
} else if (channel.type.equals(ObjectAnimationChannel.ChannelType.ROTATION)) {
for (AnimationListener listener : channel.getListeners()) {
float[] to = listener.recover();
slerp(from, to, progress, result);
listener.update(result, blend);
}
} else if (channel.type.equals(ObjectAnimationChannel.ChannelType.SCALE)) {
for (AnimationListener listener : channel.getListeners()) {
float[] to = listener.recover();
lerp(from, to, progress, result);
listener.update(result, blend);
}
}
}
}
}
private float easeOutCubic(double x) {
return (float) (1 - Math.pow(1 - x, 4));
}
private void lerp(float[] from, float[] to, float alpha, float[] result) {
for (int i = 0; i < result.length; i++) {
result[i] = from[i] * (1 - alpha) + to[i] * alpha;
}
}
private void slerp(float[] from, float[] to, float alpha, float[] result) {
float ax = from[0];
float ay = from[1];
float az = from[2];
float aw = from[3];
float bx = to[0];
float by = to[1];
float bz = to[2];
float bw = to[3];
float dot = ax * bx + ay * by + az * bz + aw * bw;
if (dot < 0) {
bx = -bx;
by = -by;
bz = -bz;
bw = -bw;
dot = -dot;
}
float epsilon = 1e-6f;
float s0, s1;
if ((1.0 - dot) > epsilon) {
float omega = (float) Math.acos(dot);
float invSinOmega = 1.0f / (float) Math.sin(omega);
s0 = (float) Math.sin((1.0 - alpha) * omega) * invSinOmega;
s1 = (float) Math.sin(alpha * omega) * invSinOmega;
} else {
s0 = 1.0f - alpha;
s1 = alpha;
}
float rx = s0 * ax + s1 * bx;
float ry = s0 * ay + s1 * by;
float rz = s0 * az + s1 * bz;
float rw = s0 * aw + s1 * bw;
result[0] = rx;
result[1] = ry;
result[2] = rz;
result[3] = rw;
}
}

View File

@@ -0,0 +1,150 @@
package com.tacz.guns.client.animation.gltf;
import com.tacz.guns.client.animation.gltf.accessor.AccessorData;
import com.tacz.guns.client.animation.gltf.accessor.AccessorDatas;
import com.tacz.guns.client.animation.gltf.accessor.Accessors;
public class AccessorModel {
/**
* The component type, as a GL constant
*/
private final int componentType;
/**
* The {@link ElementType} of this accessor
*/
private final ElementType elementType;
/**
* The number of elements
*/
private final int count;
/**
* The offset in bytes, referring to the buffer view
*/
private int byteOffset;
/**
* The {@link BufferViewModel} for this model
*/
private BufferViewModel bufferViewModel;
/**
* The stride between the start of one element and the next
*/
private int byteStride;
/**
* The {@link AccessorData}
*/
private AccessorData accessorData;
/**
* The minimum components
*/
private Number[] max;
/**
* The maximum components
*/
private Number[] min;
/**
* Creates a new instance
*
* @param componentType The component type GL constant
* @param count The number of elements
* @param elementType The element type
*/
public AccessorModel(
int componentType,
int count,
ElementType elementType) {
this.componentType = componentType;
this.count = count;
this.elementType = elementType;
}
public BufferViewModel getBufferViewModel() {
return bufferViewModel;
}
/**
* Set the {@link BufferViewModel} for this model
*
* @param bufferViewModel The {@link BufferViewModel}
*/
public void setBufferViewModel(BufferViewModel bufferViewModel) {
this.bufferViewModel = bufferViewModel;
}
public int getComponentType() {
return componentType;
}
public Class<?> getComponentDataType() {
return Accessors.getDataTypeForAccessorComponentType(
getComponentType());
}
public int getComponentSizeInBytes() {
return Accessors.getNumBytesForAccessorComponentType(componentType);
}
public int getElementSizeInBytes() {
return elementType.getNumComponents() * getComponentSizeInBytes();
}
public int getByteOffset() {
return byteOffset;
}
/**
* Set the byte offset, referring to the {@link BufferViewModel}
*
* @param byteOffset The byte offset
*/
public void setByteOffset(int byteOffset) {
this.byteOffset = byteOffset;
}
public int getCount() {
return count;
}
public ElementType getElementType() {
return elementType;
}
public int getByteStride() {
return byteStride;
}
/**
* Set the byte stride, indicating the number of bytes between the start
* of one element and the start of the next element
*
* @param byteStride The byte stride
*/
public void setByteStride(int byteStride) {
this.byteStride = byteStride;
}
public AccessorData getAccessorData() {
if (accessorData == null) {
accessorData = AccessorDatas.create(this);
}
return accessorData;
}
public Number[] getMin() {
if (min == null) {
min = AccessorDatas.computeMin(getAccessorData());
}
return min.clone();
}
public Number[] getMax() {
if (max == null) {
max = AccessorDatas.computeMax(getAccessorData());
}
return max.clone();
}
}

View File

@@ -0,0 +1,108 @@
package com.tacz.guns.client.animation.gltf;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public class AnimationModel {
/**
* The {@link Channel} instances
* of this animation
*/
private final List<Channel> channels = new ArrayList<>();
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
/**
* Add the given {@link Channel}
*
* @param channel The {@link Channel}
*/
public void addChannel(Channel channel) {
Objects.requireNonNull(channel, "The channel may not be null");
this.channels.add(channel);
}
public List<Channel> getChannels() {
return Collections.unmodifiableList(channels);
}
public enum Interpolation {
/**
* Stepwise interpolation
*/
STEP,
/**
* Linear interpolation
*/
LINEAR,
/**
* Spline interpolation
*/
SPLINE
}
/**
* @param input The input data
* @param interpolation The interpolation method
* @param output The output data
*/
public record Sampler(AccessorModel input, Interpolation interpolation, AccessorModel output) {
/**
* Default constructor
*
* @param input The input
* @param interpolation The interpolation
* @param output The output
*/
public Sampler(
AccessorModel input,
Interpolation interpolation,
AccessorModel output) {
this.input = Objects.requireNonNull(
input, "The input may not be null");
this.interpolation = Objects.requireNonNull(
interpolation, "The interpolation may not be null");
this.output = Objects.requireNonNull(
output, "The output may not be null");
}
}
/**
* @param sampler The sampler
* @param nodeModel The node model
* @param path The path
*/
public record Channel(Sampler sampler, NodeModel nodeModel, String path) {
/**
* Default constructor
*
* @param sampler The sampler
* @param nodeModel The node model
* @param path The path
*/
public Channel(
Sampler sampler,
NodeModel nodeModel,
String path) {
this.sampler = Objects.requireNonNull(
sampler, "The sampler may not be null");
this.nodeModel = nodeModel;
this.path = Objects.requireNonNull(
path, "The path may not be null");
}
}
}

View File

@@ -0,0 +1,427 @@
package com.tacz.guns.client.animation.gltf;
import com.tacz.guns.client.animation.gltf.accessor.AccessorData;
import com.tacz.guns.client.animation.gltf.accessor.AccessorDatas;
import com.tacz.guns.client.animation.gltf.accessor.AccessorSparseUtils;
import com.tacz.guns.client.resource.pojo.animation.gltf.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
public class AnimationStructure {
private final List<AccessorModel> accessorModels = new ArrayList<>();
private final List<AnimationModel> animationModels = new ArrayList<>();
private final List<BufferModel> bufferModels = new ArrayList<>();
private final List<BufferViewModel> bufferViewModels = new ArrayList<>();
private final List<NodeModel> nodeModels = new ArrayList<>();
private final RawAnimationStructure gltf;
public AnimationStructure(RawAnimationStructure asset) {
gltf = asset;
createAccessorModels();
createAnimationModels();
createBufferModels();
createBufferViewModels();
createNodeModels();
initBufferModels();
initBufferViewModels();
initAccessorModels();
initAnimationModels();
initNodeModels();
}
private static float[] clone(float[] array) {
if (array == null) {
return null;
}
return array.clone();
}
private static BufferViewModel createBufferViewModel(
String uriString, ByteBuffer bufferData) {
BufferModel bufferModel = new BufferModel();
bufferModel.setUri(uriString);
bufferModel.setBufferData(bufferData);
BufferViewModel bufferViewModel =
new BufferViewModel(null);
bufferViewModel.setByteOffset(0);
bufferViewModel.setByteLength(bufferData.capacity());
bufferViewModel.setBufferModel(bufferModel);
return bufferViewModel;
}
private static BufferViewModel createBufferViewModel(
BufferView bufferView) {
int byteOffset = bufferView.getByteOffset() == null ? 0 : bufferView.getByteOffset();
int byteLength = bufferView.getByteLength();
Integer byteStride = bufferView.getByteStride();
Integer target = bufferView.getTarget();
BufferViewModel bufferViewModel =
new BufferViewModel(target);
bufferViewModel.setByteOffset(byteOffset);
bufferViewModel.setByteLength(byteLength);
bufferViewModel.setByteStride(byteStride);
return bufferViewModel;
}
private static boolean isDataUriString(String uriString) {
if (uriString == null) {
return false;
}
try {
URI uri = new URI(uriString);
return isDataUri(uri);
} catch (URISyntaxException e) {
return false;
}
}
private static boolean isDataUri(URI uri) {
return "data".equalsIgnoreCase(uri.getScheme());
}
public static byte[] readDataUri(String uriString) {
String encoding = "base64,";
int encodingIndex = uriString.indexOf(encoding);
if (encodingIndex < 0) {
throw new IllegalArgumentException(
"The given URI string is not a base64 encoded "
+ "data URI string: " + uriString);
}
int contentStartIndex = encodingIndex + encoding.length();
return Base64.getDecoder().decode(
uriString.substring(contentStartIndex));
}
public List<BufferModel> getBufferModels() {
return Collections.unmodifiableList(bufferModels);
}
public List<AccessorModel> getAccessorModels() {
return accessorModels;
}
public List<AnimationModel> getAnimationModels() {
return animationModels;
}
public List<BufferViewModel> getBufferViewModels() {
return bufferViewModels;
}
public List<NodeModel> getNodeModels() {
return nodeModels;
}
private void createBufferModels() {
List<Buffer> buffers = gltf.getBuffers() == null ? Collections.emptyList() : gltf.getBuffers();
for (int i = 0; i < buffers.size(); i++) {
Buffer buffer = buffers.get(i);
BufferModel bufferModel = new BufferModel();
bufferModel.setUri(buffer.getUri());
bufferModels.add(bufferModel);
}
}
private void initBufferModels() {
List<Buffer> buffers = gltf.getBuffers() == null ? Collections.emptyList() : gltf.getBuffers();
for (int i = 0; i < buffers.size(); i++) {
Buffer buffer = buffers.get(i);
BufferModel bufferModel = bufferModels.get(i);
{
String uri = buffer.getUri();
if (isDataUriString(uri)) {
byte[] data = readDataUri(uri);
ByteBuffer bufferData = Buffers.create(data);
bufferModel.setBufferData(bufferData);
}
}
}
}
private void createAccessorModels() {
List<Accessor> accessors = gltf.getAccessors() == null ? Collections.emptyList() : gltf.getAccessors();
for (Accessor accessor : accessors) {
Integer componentType = accessor.getComponentType();
Integer count = accessor.getCount();
ElementType elementType = ElementType.forString(accessor.getType());
AccessorModel accessorModel = new AccessorModel(
componentType, count, elementType);
accessorModels.add(accessorModel);
}
}
private void initAccessorModels() {
List<Accessor> accessors = gltf.getAccessors() == null ? Collections.emptyList() : gltf.getAccessors();
for (int i = 0; i < accessors.size(); i++) {
Accessor accessor = accessors.get(i);
AccessorModel accessorModel = accessorModels.get(i);
int byteOffset = accessor.getByteOffset() == null ? 0 : accessor.getByteOffset();
accessorModel.setByteOffset(byteOffset);
AccessorSparse accessorSparse = accessor.getSparse();
if (accessorSparse == null) {
initDenseAccessorModel(i, accessor, accessorModel);
} else {
initSparseAccessorModel(i, accessor, accessorModel);
}
}
}
private void createAnimationModels() {
List<Animation> animations = gltf.getAnimations() == null ? Collections.emptyList() : gltf.getAnimations();
for (int i = 0; i < animations.size(); i++) {
animationModels.add(new AnimationModel());
}
}
private void initAnimationModels() {
List<Animation> animations = gltf.getAnimations() == null ? Collections.emptyList() : gltf.getAnimations();
;
for (int i = 0; i < animations.size(); i++) {
Animation animation = animations.get(i);
AnimationModel animationModel = animationModels.get(i);
animationModel.setName(animation.getName());
List<AnimationChannel> channels =
animation.getChannels();
for (AnimationChannel animationChannel : channels) {
AnimationModel.Channel channel = createChannel(animation, animationChannel);
animationModel.addChannel(channel);
}
}
}
private void createBufferViewModels() {
List<BufferView> bufferViews = gltf.getBufferViews() == null ? Collections.emptyList() : gltf.getBufferViews();
;
for (BufferView bufferView : bufferViews) {
BufferViewModel bufferViewModel =
createBufferViewModel(bufferView);
bufferViewModels.add(bufferViewModel);
}
}
private void initBufferViewModels() {
List<BufferView> bufferViews = gltf.getBufferViews() == null ? Collections.emptyList() : gltf.getBufferViews();
;
for (int i = 0; i < bufferViews.size(); i++) {
BufferView bufferView = bufferViews.get(i);
BufferViewModel bufferViewModel = bufferViewModels.get(i);
int bufferIndex = bufferView.getBuffer();
BufferModel bufferModel = bufferModels.get(bufferIndex);
bufferViewModel.setBufferModel(bufferModel);
}
}
private void createNodeModels() {
List<Node> nodes = gltf.getNodes() == null ? Collections.emptyList() : gltf.getNodes();
for (int i = 0; i < nodes.size(); i++) {
nodeModels.add(new NodeModel());
}
}
private void initNodeModels() {
List<Node> nodes = gltf.getNodes() == null ? Collections.emptyList() : gltf.getNodes();
for (int i = 0; i < nodes.size(); i++) {
Node node = nodes.get(i);
NodeModel nodeModel = nodeModels.get(i);
nodeModel.setName(node.getName());
List<Integer> childIndices = node.getChildren() == null ? Collections.emptyList() : node.getChildren();
;
for (Integer childIndex : childIndices) {
NodeModel child = nodeModels.get(childIndex);
nodeModel.addChild(child);
}
float[] matrix = node.getMatrix();
float[] translation = node.getTranslation();
float[] rotation = node.getRotation();
float[] scale = node.getScale();
nodeModel.setMatrix(clone(matrix));
nodeModel.setTranslation(clone(translation));
nodeModel.setRotation(clone(rotation));
nodeModel.setScale(clone(scale));
}
}
private AnimationModel.Channel createChannel(
Animation animation, AnimationChannel animationChannel) {
List<AnimationSampler> samplers = animation.getSamplers();
int samplerIndex = animationChannel.getSampler();
AnimationSampler animationSampler = samplers.get(samplerIndex);
int inputAccessorIndex = animationSampler.getInput();
AccessorModel inputAccessorModel =
accessorModels.get(inputAccessorIndex);
int outputAccessorIndex = animationSampler.getOutput();
AccessorModel outputAccessorModel =
accessorModels.get(outputAccessorIndex);
String interpolationString =
animationSampler.getInterpolation();
AnimationModel.Interpolation interpolation =
interpolationString == null ? AnimationModel.Interpolation.LINEAR :
AnimationModel.Interpolation.valueOf(interpolationString);
AnimationModel.Sampler sampler = new AnimationModel.Sampler(
inputAccessorModel, interpolation, outputAccessorModel);
AnimationChannelTarget animationChannelTarget =
animationChannel.getTarget();
Integer nodeIndex = animationChannelTarget.getNode();
NodeModel nodeModel = null;
if (nodeIndex != null) {
nodeModel = nodeModels.get(nodeIndex);
}
String path = animationChannelTarget.getPath();
return new AnimationModel.Channel(sampler, nodeModel, path);
}
private void initDenseAccessorModel(int accessorIndex,
Accessor accessor, AccessorModel accessorModel) {
Integer bufferViewIndex = accessor.getBufferView();
if (bufferViewIndex != null) {
// When there is a BufferView referenced from the accessor, then
// the corresponding BufferViewModel may be assigned directly
BufferViewModel bufferViewModel =
bufferViewModels.get(bufferViewIndex);
accessorModel.setBufferViewModel(bufferViewModel);
} else {
// When there is no BufferView referenced from the accessor,
// then a NEW BufferViewModel (and Buffer) have to be created
int count = accessorModel.getCount();
int elementSizeInBytes = accessorModel.getElementSizeInBytes();
int byteLength = elementSizeInBytes * count;
ByteBuffer bufferData = Buffers.create(byteLength);
String uriString = "buffer_for_accessor" + accessorIndex + ".bin";
BufferViewModel bufferViewModel =
createBufferViewModel(uriString, bufferData);
accessorModel.setBufferViewModel(bufferViewModel);
}
BufferViewModel bufferViewModel = accessorModel.getBufferViewModel();
Integer byteStride = bufferViewModel.getByteStride();
if (byteStride == null) {
accessorModel.setByteStride(
accessorModel.getElementSizeInBytes());
} else {
accessorModel.setByteStride(byteStride);
}
}
private void initSparseAccessorModel(int accessorIndex,
Accessor accessor, AccessorModel accessorModel) {
// When the (sparse!) Accessor already refers to a BufferView,
// then this BufferView has to be replaced with a new one,
// to which the data substitution will be applied
int count = accessorModel.getCount();
int elementSizeInBytes = accessorModel.getElementSizeInBytes();
int byteLength = elementSizeInBytes * count;
ByteBuffer bufferData = Buffers.create(byteLength);
String uriString = "buffer_for_accessor" + accessorIndex + ".bin";
BufferViewModel denseBufferViewModel =
createBufferViewModel(uriString, bufferData);
accessorModel.setBufferViewModel(denseBufferViewModel);
accessorModel.setByteOffset(0);
Integer bufferViewIndex = accessor.getBufferView();
if (bufferViewIndex != null) {
// If the accessor refers to a BufferView, then the corresponding
// data serves as the basis for the initialization of the values,
// before the sparse substitution is applied
Consumer<ByteBuffer> sparseSubstitutionCallback = denseByteBuffer ->
{
BufferViewModel baseBufferViewModel =
bufferViewModels.get(bufferViewIndex);
ByteBuffer baseBufferViewData =
baseBufferViewModel.getBufferViewData();
AccessorData baseAccessorData = AccessorDatas.create(
accessorModel, baseBufferViewData);
AccessorData denseAccessorData =
AccessorDatas.create(accessorModel, bufferData);
substituteSparseAccessorData(accessor, accessorModel,
denseAccessorData, baseAccessorData);
};
denseBufferViewModel.setSparseSubstitutionCallback(
sparseSubstitutionCallback);
} else {
// When the sparse accessor does not yet refer to a BufferView,
// then a new one is created,
Consumer<ByteBuffer> sparseSubstitutionCallback = denseByteBuffer ->
{
AccessorData denseAccessorData =
AccessorDatas.create(accessorModel, bufferData);
substituteSparseAccessorData(accessor, accessorModel,
denseAccessorData, null);
};
denseBufferViewModel.setSparseSubstitutionCallback(
sparseSubstitutionCallback);
}
}
private void substituteSparseAccessorData(
Accessor accessor, AccessorModel accessorModel,
AccessorData denseAccessorData, AccessorData baseAccessorData) {
AccessorSparse accessorSparse = accessor.getSparse();
int count = accessorSparse.getCount();
AccessorSparseIndices accessorSparseIndices =
accessorSparse.getIndices();
AccessorData sparseIndicesAccessorData =
createSparseIndicesAccessorData(accessorSparseIndices, count);
AccessorSparseValues accessorSparseValues = accessorSparse.getValues();
ElementType elementType = accessorModel.getElementType();
AccessorData sparseValuesAccessorData =
createSparseValuesAccessorData(accessorSparseValues,
accessorModel.getComponentType(),
elementType.getNumComponents(), count);
AccessorSparseUtils.substituteAccessorData(
denseAccessorData,
baseAccessorData,
sparseIndicesAccessorData,
sparseValuesAccessorData);
}
private AccessorData createSparseIndicesAccessorData(
AccessorSparseIndices accessorSparseIndices, int count) {
Integer componentType = accessorSparseIndices.getComponentType();
Integer bufferViewIndex = accessorSparseIndices.getBufferView();
BufferViewModel bufferViewModel = bufferViewModels.get(bufferViewIndex);
ByteBuffer bufferViewData = bufferViewModel.getBufferViewData();
int byteOffset = accessorSparseIndices.getByteOffset() == null ? 0 : accessorSparseIndices.getByteOffset();
return AccessorDatas.create(
componentType, bufferViewData, byteOffset, count, 1, null);
}
private AccessorData createSparseValuesAccessorData(
AccessorSparseValues accessorSparseValues,
int componentType, int numComponentsPerElement, int count) {
Integer bufferViewIndex = accessorSparseValues.getBufferView();
BufferViewModel bufferViewModel = bufferViewModels.get(bufferViewIndex);
ByteBuffer bufferViewData = bufferViewModel.getBufferViewData();
int byteOffset = accessorSparseValues.getByteOffset() == null ? 0 : accessorSparseValues.getByteOffset();
return AccessorDatas.create(
componentType, bufferViewData, byteOffset, count,
numComponentsPerElement, null);
}
}

View File

@@ -0,0 +1,45 @@
package com.tacz.guns.client.animation.gltf;
import java.nio.ByteBuffer;
public class BufferModel {
/**
* The URI of the buffer data
*/
private String uri;
/**
* The actual data of the buffer
*/
private ByteBuffer bufferData;
public String getUri() {
return uri;
}
/**
* Set the URI for the buffer data
*
* @param uri The URI of the buffer data
*/
public void setUri(String uri) {
this.uri = uri;
}
public int getByteLength() {
return bufferData.capacity();
}
public ByteBuffer getBufferData() {
return Buffers.createSlice(bufferData);
}
/**
* Set the data of this buffer
*
* @param bufferData The buffer data
*/
public void setBufferData(ByteBuffer bufferData) {
this.bufferData = bufferData;
}
}

View File

@@ -0,0 +1,131 @@
package com.tacz.guns.client.animation.gltf;
import java.nio.ByteBuffer;
import java.util.function.Consumer;
public class BufferViewModel {
/**
* The optional target
*/
private final Integer target;
/**
* The {@link BufferModel} for this model
*/
private BufferModel bufferModel;
/**
* The byte offset
*/
private int byteOffset;
/**
* The byte length
*/
private int byteLength;
/**
* The byte stride
*/
private Integer byteStride;
/**
* An optional callback that will be used to perform the
* substitution of sparse accessor data in the
* {@link #getBufferViewData() buffer view data}
* when it is obtained for the first time.
*/
private Consumer<? super ByteBuffer> sparseSubstitutionCallback;
/**
* Whether the sparse substitution was already applied
*/
private boolean sparseSubstitutionApplied;
/**
* Creates a new instance
*
* @param target The optional target
*/
public BufferViewModel(Integer target) {
this.byteOffset = 0;
this.byteLength = 0;
this.target = target;
}
/**
* Set the callback that will perform the substitution of sparse accessor
* data in the {@link #getBufferViewData() buffer view data} when it is
* obtained for the first time.
*
* @param sparseSubstitutionCallback The callback
*/
public void setSparseSubstitutionCallback(
Consumer<? super ByteBuffer> sparseSubstitutionCallback) {
this.sparseSubstitutionCallback = sparseSubstitutionCallback;
}
public ByteBuffer getBufferViewData() {
ByteBuffer bufferData = bufferModel.getBufferData();
ByteBuffer bufferViewData =
Buffers.createSlice(bufferData, getByteOffset(), getByteLength());
if (sparseSubstitutionCallback != null && !sparseSubstitutionApplied) {
sparseSubstitutionCallback.accept(bufferViewData);
sparseSubstitutionApplied = true;
}
return bufferViewData;
}
public BufferModel getBufferModel() {
return bufferModel;
}
/**
* Set the {@link BufferModel} for this model
*
* @param bufferModel The {@link BufferModel}
*/
public void setBufferModel(BufferModel bufferModel) {
this.bufferModel = bufferModel;
}
public int getByteOffset() {
return byteOffset;
}
/**
* Set the byte offset of this view referring to its {@link BufferModel}
*
* @param byteOffset The byte offset
*/
public void setByteOffset(int byteOffset) {
this.byteOffset = byteOffset;
}
public int getByteLength() {
return byteLength;
}
/**
* Set the byte length of this buffer view
*
* @param byteLength The byte length
*/
public void setByteLength(int byteLength) {
this.byteLength = byteLength;
}
public Integer getByteStride() {
return byteStride;
}
/**
* Set the optional byte stride. This byte stride must be
* non-<code>null</code> if more than one accessor refers
* to this buffer view.
*
* @param byteStride The byte stride
*/
public void setByteStride(Integer byteStride) {
this.byteStride = byteStride;
}
public Integer getTarget() {
return target;
}
}

View File

@@ -0,0 +1,138 @@
/*
* www.javagl.de - JglTF
*
* Copyright 2015-2016 Marco Hutter - http://www.javagl.de
*
* 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.
*
* 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.
*/
package com.tacz.guns.client.animation.gltf;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* Utility methods related to buffers
*/
public class Buffers {
/**
* Private constructor to prevent instantiation
*/
private Buffers() {
// Private constructor to prevent instantiation
}
/**
* Create a slice of the given byte buffer, using its current position
* and limit. The returned slice will have the same byte order as the
* given buffer. If the given buffer is <code>null</code>, then
* <code>null</code> will be returned.
*
* @param byteBuffer The byte buffer
* @return The slice
*/
public static ByteBuffer createSlice(ByteBuffer byteBuffer) {
if (byteBuffer == null) {
return null;
}
return byteBuffer.slice().order(byteBuffer.order());
}
/**
* Create a slice of the given byte buffer, in the specified range.
* The returned buffer will have the same byte order as the given
* buffer. If the given buffer is <code>null</code>, then
* <code>null</code> will be returned.
*
* @param byteBuffer The byte buffer
* @param position The position where the slice should start
* @param length The length of the slice
* @return The slice
* @throws IllegalArgumentException If the range that is specified
* by the position and length are not valid for the given buffer
*/
public static ByteBuffer createSlice(
ByteBuffer byteBuffer, int position, int length) {
if (byteBuffer == null) {
return null;
}
int oldPosition = byteBuffer.position();
int oldLimit = byteBuffer.limit();
try {
int newLimit = position + length;
if (newLimit > byteBuffer.capacity()) {
throw new IllegalArgumentException(
"The new limit is " + newLimit + ", but the capacity is "
+ byteBuffer.capacity());
}
byteBuffer.limit(newLimit);
byteBuffer.position(position);
ByteBuffer slice = byteBuffer.slice();
slice.order(byteBuffer.order());
return slice;
} finally {
byteBuffer.limit(oldLimit);
byteBuffer.position(oldPosition);
}
}
/**
* Creates a new, direct byte buffer that contains the given data,
* with little-endian byte order
*
* @param data The data
* @return The byte buffer
*/
public static ByteBuffer create(byte data[]) {
return create(data, 0, data.length);
}
/**
* Creates a new, direct byte buffer that contains the specified range
* of the given data, with little-endian byte order
*
* @param data The data
* @param offset The offset in the data array
* @param length The length of the range
* @return The byte buffer
*/
public static ByteBuffer create(byte data[], int offset, int length) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(length);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
byteBuffer.put(data, offset, length);
byteBuffer.position(0);
return byteBuffer;
}
/**
* Create a new direct byte buffer with the given size, and little-endian
* byte order.
*
* @param size The size of the buffer
* @return The byte buffer
* @throws IllegalArgumentException If the given size is negative
*/
public static ByteBuffer create(int size) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(size);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
return byteBuffer;
}
}

View File

@@ -0,0 +1,95 @@
package com.tacz.guns.client.animation.gltf;
public enum ElementType {
/**
* The scalar type
*/
SCALAR(1),
/**
* The 2D vector type
*/
VEC2(2),
/**
* The 3D vector type
*/
VEC3(3),
/**
* The 4D vector type
*/
VEC4(4),
/**
* The 2x2 matrix type
*/
MAT2(4),
/**
* The 3x3 matrix type
*/
MAT3(9),
/**
* The 4x4 matrix type
*/
MAT4(16);
/**
* The number of components that one element consists of
*/
private final int numComponents;
/**
* Creates a new instance with the given number of components
*
* @param numComponents The number of components
*/
ElementType(int numComponents) {
this.numComponents = numComponents;
}
/**
* Returns whether the given string is a valid element type name, and may be
* passed to <code>ElementType.valueOf</code> without causing an exception.
*
* @param s The string
* @return Whether the given string is a valid element type
*/
public static boolean contains(String s) {
for (ElementType elementType : values()) {
if (elementType.name().equals(s)) {
return true;
}
}
return false;
}
/**
* Returns the element type for the given string. If the string is
* <code>null</code> or does not describe a valid element type,
* then <code>null</code> is returned
*
* @param string The string
* @return The element type
*/
public static ElementType forString(String string) {
if (string == null) {
return null;
}
if (!contains(string)) {
return null;
}
return ElementType.valueOf(string);
}
/**
* Returns the number of components that one element consists of
*
* @return The number of components
*/
public int getNumComponents() {
return numComponents;
}
}

View File

@@ -0,0 +1,687 @@
/*
* www.javagl.de - JglTF
*
* Copyright 2015-2016 Marco Hutter - http://www.javagl.de
*
* 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.
*
* 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.
*/
package com.tacz.guns.client.animation.gltf;
public class GltfConstants {
/**
* The GL_BGR constant (32992)
*/
public static final int GL_BGR = 32992;
/**
* The GL_RGB constant (6407)
*/
public static final int GL_RGB = 6407;
/**
* The GL_RGBA constant (6408)
*/
public static final int GL_RGBA = 6408;
/**
* The GL_BGRA constant (32993)
*/
public static final int GL_BGRA = 32993;
/**
* The GL_BYTE constant (5120)
*/
public static final int GL_BYTE = 5120;
/**
* The GL_UNSIGNED_BYTE constant (5121)
*/
public static final int GL_UNSIGNED_BYTE = 5121;
/**
* The GL_SHORT constant (5122)
*/
public static final int GL_SHORT = 5122;
/**
* The GL_UNSIGNED_SHORT constant (5123)
*/
public static final int GL_UNSIGNED_SHORT = 5123;
/**
* The GL_INT constant (5124)
*/
public static final int GL_INT = 5124;
/**
* The GL_UNSIGNED_INT constant (5125)
*/
public static final int GL_UNSIGNED_INT = 5125;
/**
* The GL_FLOAT constant (5126)
*/
public static final int GL_FLOAT = 5126;
/**
* The GL_FLOAT_VEC2 constant (35664)
*/
public static final int GL_FLOAT_VEC2 = 35664;
/**
* The GL_FLOAT_VEC3 constant (35665)
*/
public static final int GL_FLOAT_VEC3 = 35665;
/**
* The GL_FLOAT_VEC4 constant (35666)
*/
public static final int GL_FLOAT_VEC4 = 35666;
/**
* The GL_INT_VEC2 constant (35667)
*/
public static final int GL_INT_VEC2 = 35667;
/**
* The GL_INT_VEC3 constant (35668)
*/
public static final int GL_INT_VEC3 = 35668;
/**
* The GL_INT_VEC4 constant (35669)
*/
public static final int GL_INT_VEC4 = 35669;
/**
* The GL_BOOL constant (35670)
*/
public static final int GL_BOOL = 35670;
/**
* The GL_BOOL_VEC2 constant (35671)
*/
public static final int GL_BOOL_VEC2 = 35671;
/**
* The GL_BOOL_VEC3 constant (35672)
*/
public static final int GL_BOOL_VEC3 = 35672;
/**
* The GL_BOOL_VEC4 constant (35673)
*/
public static final int GL_BOOL_VEC4 = 35673;
/**
* The GL_FLOAT_MAT2 constant (35674)
*/
public static final int GL_FLOAT_MAT2 = 35674;
/**
* The GL_FLOAT_MAT3 constant (35675)
*/
public static final int GL_FLOAT_MAT3 = 35675;
/**
* The GL_FLOAT_MAT4 constant (35676)
*/
public static final int GL_FLOAT_MAT4 = 35676;
/**
* The GL_SAMPLER_2D constant (35678)
*/
public static final int GL_SAMPLER_2D = 35678;
/**
* The GL_SAMPLER_CUBE constant (35680)
*/
public static final int GL_SAMPLER_CUBE = 35680;
/**
* The GL_POINTS constant (0)
*/
public static final int GL_POINTS = 0;
/**
* The GL_LINES constant (1)
*/
public static final int GL_LINES = 1;
/**
* The GL_LINE_LOOP constant (2)
*/
public static final int GL_LINE_LOOP = 2;
/**
* The GL_LINE_STRIP constant (3)
*/
public static final int GL_LINE_STRIP = 3;
/**
* The GL_TRIANGLES constant (4)
*/
public static final int GL_TRIANGLES = 4;
/**
* The GL_TRIANGLE_STRIP constant (5)
*/
public static final int GL_TRIANGLE_STRIP = 5;
/**
* The GL_TRIANGLE_FAN constant (6)
*/
public static final int GL_TRIANGLE_FAN = 6;
/**
* The GL_VERTEX_SHADER constant (35633)
*/
public static final int GL_VERTEX_SHADER = 35633;
/**
* The GL_VERTEX_SHADER constant (35632)
*/
public static final int GL_FRAGMENT_SHADER = 35632;
/**
* The GL_TEXTURE_2D constant (3553)
*/
public static final int GL_TEXTURE_2D = 3553;
/**
* The GL_ARRAY_BUFFER constant (34962)
*/
public static final int GL_ARRAY_BUFFER = 34962;
/**
* The GL_ELEMENT_ARRAY_BUFFER constant (34963)
*/
public static final int GL_ELEMENT_ARRAY_BUFFER = 34963;
// glEnable for technique.states
/**
* The GL_BLEND constant (3042)
*/
public static final int GL_BLEND = 3042;
/**
* The GL_CULL_FACE constant (2884)
*/
public static final int GL_CULL_FACE = 2884;
/**
* The GL_DEPTH_TEST constant (2929)
*/
public static final int GL_DEPTH_TEST = 2929;
/**
* The GL_POLYGON_OFFSET_FILL constant (32823)
*/
public static final int GL_POLYGON_OFFSET_FILL = 32823;
/**
* The GL_SAMPLE_ALPHA_TO_COVERAGE constant (32926)
*/
public static final int GL_SAMPLE_ALPHA_TO_COVERAGE = 32926;
/**
* The GL_SCISSOR_TEST constant (3089)
*/
public static final int GL_SCISSOR_TEST = 3089;
// glBlendEquationSeparate
/**
* The GL_FUNC_ADD constant (32774)
*/
public static final int GL_FUNC_ADD = 32774;
/**
* The GL_FUNC_SUBTRACT constant (32778)
*/
public static final int GL_FUNC_SUBTRACT = 32778;
/**
* The GL_FUNC_REVERSE_SUBTRACT constant (32779)
*/
public static final int GL_FUNC_REVERSE_SUBTRACT = 32779;
// glBlendFuncSeparate
/**
* The GL_ZERO constant (0)
*/
public static final int GL_ZERO = 0;
/**
* The GL_ONE constant (1)
*/
public static final int GL_ONE = 1;
/**
* The GL_SRC_COLOR constant (768)
*/
public static final int GL_SRC_COLOR = 768;
/**
* The GL_ONE_MINUS_SRC_COLOR constant (769)
*/
public static final int GL_ONE_MINUS_SRC_COLOR = 769;
/**
* The GL_DST_COLOR constant (774)
*/
public static final int GL_DST_COLOR = 774;
/**
* The GL_ONE_MINUS_DST_COLOR constant (775)
*/
public static final int GL_ONE_MINUS_DST_COLOR = 775;
/**
* The GL_SRC_ALPHA constant (770)
*/
public static final int GL_SRC_ALPHA = 770;
/**
* The GL_ONE_MINUS_SRC_ALPHA constant (771)
*/
public static final int GL_ONE_MINUS_SRC_ALPHA = 771;
/**
* The GL_DST_ALPHA constant (772)
*/
public static final int GL_DST_ALPHA = 772;
/**
* The GL_ONE_MINUS_DST_ALPHA constant (773)
*/
public static final int GL_ONE_MINUS_DST_ALPHA = 773;
/**
* The GL_CONSTANT_COLOR constant (32769)
*/
public static final int GL_CONSTANT_COLOR = 32769;
/**
* The GL_ONE_MINUS_CONSTANT_COLOR constant (32770)
*/
public static final int GL_ONE_MINUS_CONSTANT_COLOR = 32770;
/**
* The GL_CONSTANT_ALPHA constant (32771)
*/
public static final int GL_CONSTANT_ALPHA = 32771;
/**
* The GL_ONE_MINUS_CONSTANT_ALPHA constant (32772)
*/
public static final int GL_ONE_MINUS_CONSTANT_ALPHA = 32772;
/**
* The GL_SRC_ALPHA_SATURATE constant (776)
*/
public static final int GL_SRC_ALPHA_SATURATE = 776;
// glCullFace
/**
* The GL_FRONT constant (1028)
*/
public static final int GL_FRONT = 1028;
/**
* The GL_BACK constant (1029)
*/
public static final int GL_BACK = 1029;
/**
* The GL_FRONT_AND_BACK constant (1032)
*/
public static final int GL_FRONT_AND_BACK = 1032;
// glDepthFunc
/**
* The GL_NEVER constant (512)
*/
public static final int GL_NEVER = 512;
/**
* The GL_LESS constant (513)
*/
public static final int GL_LESS = 513;
/**
* The GL_LEQUAL constant (515)
*/
public static final int GL_LEQUAL = 515;
/**
* The GL_EQUAL constant (514)
*/
public static final int GL_EQUAL = 514;
/**
* The GL_GREATER constant (516)
*/
public static final int GL_GREATER = 516;
/**
* The GL_NOTEQUAL constant (517)
*/
public static final int GL_NOTEQUAL = 517;
/**
* The GL_GEQUAL constant (518)
*/
public static final int GL_GEQUAL = 518;
/**
* The GL_ALWAYS constant (519)
*/
public static final int GL_ALWAYS = 519;
// glFrontFace
/**
* The GL_CW constant (2304)
*/
public static final int GL_CW = 2304;
/**
* The GL_CCW constant (2305)
*/
public static final int GL_CCW = 2305;
// glTexParameter
/**
* The GL_NEAREST constant (9728)
*/
public static final int GL_NEAREST = 9728;
/**
* The GL_LINEAR constant (9729)
*/
public static final int GL_LINEAR = 9729;
/**
* The GL_NEAREST_MIPMAP_NEAREST constant (9984)
*/
public static final int GL_NEAREST_MIPMAP_NEAREST = 9984;
/**
* The GL_LINEAR_MIPMAP_NEAREST constant (9985)
*/
public static final int GL_LINEAR_MIPMAP_NEAREST = 9985;
/**
* The GL_NEAREST_MIPMAP_LINEAR constant (9986)
*/
public static final int GL_NEAREST_MIPMAP_LINEAR = 9986;
/**
* The GL_LINEAR_MIPMAP_LINEAR constant (9987)
*/
public static final int GL_LINEAR_MIPMAP_LINEAR = 9987;
// glSamplerParameter
/**
* The GL_REPEAT constant (10497)
*/
public static final int GL_REPEAT = 10497;
/**
* The GL_MIRRORED_REPEAT constant (33648)
*/
public static final int GL_MIRRORED_REPEAT = 33648;
/**
* The GL_CLAMP_TO_EDGE constant (33071)
*/
public static final int GL_CLAMP_TO_EDGE = 33071;
/**
* The GL_CLAMP_TO_BORDER constant (33069)
*/
public static final int GL_CLAMP_TO_BORDER = 33069;
/**
* Private constructor to prevent instantiation
*/
private GltfConstants() {
// Private constructor to prevent instantiation
}
/**
* Returns the String representation of the given constant
*
* @param constant The constant
* @return The String for the constant
*/
public static String stringFor(int constant) {
switch (constant) {
case GL_BGR:
return "GL_BGR";
case GL_RGB:
return "GL_RGB";
case GL_RGBA:
return "GL_RGBA";
case GL_BGRA:
return "GL_BGRA";
case GL_BYTE:
return "GL_BYTE";
case GL_UNSIGNED_BYTE:
return "GL_UNSIGNED_BYTE";
case GL_SHORT:
return "GL_SHORT";
case GL_UNSIGNED_SHORT:
return "GL_UNSIGNED_SHORT";
case GL_INT:
return "GL_INT";
case GL_UNSIGNED_INT:
return "GL_UNSIGNED_INT";
case GL_FLOAT:
return "GL_FLOAT";
case GL_FLOAT_VEC2:
return "GL_FLOAT_VEC2";
case GL_FLOAT_VEC3:
return "GL_FLOAT_VEC3";
case GL_FLOAT_VEC4:
return "GL_FLOAT_VEC4";
case GL_INT_VEC2:
return "GL_INT_VEC2";
case GL_INT_VEC3:
return "GL_INT_VEC3";
case GL_INT_VEC4:
return "GL_INT_VEC4";
case GL_BOOL:
return "GL_BOOL";
case GL_BOOL_VEC2:
return "GL_BOOL_VEC2";
case GL_BOOL_VEC3:
return "GL_BOOL_VEC3";
case GL_BOOL_VEC4:
return "GL_BOOL_VEC4";
case GL_FLOAT_MAT2:
return "GL_FLOAT_MAT2";
case GL_FLOAT_MAT3:
return "GL_FLOAT_MAT3";
case GL_FLOAT_MAT4:
return "GL_FLOAT_MAT4";
case GL_SAMPLER_2D:
return "GL_SAMPLER_2D";
case GL_POINTS:
return "GL_ZERO or GL_POINTS";
case GL_LINES:
return "GL_ONE or GL_LINES";
case GL_LINE_LOOP:
return "GL_LINE_LOOP";
case GL_LINE_STRIP:
return "GL_LINE_STRIP";
case GL_TRIANGLES:
return "GL_TRIANGLES";
case GL_TRIANGLE_STRIP:
return "GL_TRIANGLE_STRIP";
case GL_VERTEX_SHADER:
return "GL_VERTEX_SHADER";
case GL_FRAGMENT_SHADER:
return "GL_FRAGMENT_SHADER";
case GL_TEXTURE_2D:
return "GL_TEXTURE_2D";
case GL_ARRAY_BUFFER:
return "GL_ARRAY_BUFFER";
case GL_ELEMENT_ARRAY_BUFFER:
return "GL_ELEMENT_ARRAY_BUFFER";
// glEnable for technique.states
case GL_BLEND:
return "GL_BLEND";
case GL_CULL_FACE:
return "GL_CULL_FACE";
case GL_DEPTH_TEST:
return "GL_DEPTH_TEST";
case GL_POLYGON_OFFSET_FILL:
return "GL_POLYGON_OFFSET_FILL";
case GL_SAMPLE_ALPHA_TO_COVERAGE:
return "GL_SAMPLE_ALPHA_TO_COVERAGE";
case GL_SCISSOR_TEST:
return "GL_SCISSOR_TEST";
// glBlendEquationSeparate
case GL_FUNC_ADD:
return "GL_FUNC_ADD";
case GL_FUNC_SUBTRACT:
return "GL_FUNC_SUBTRACT";
case GL_FUNC_REVERSE_SUBTRACT:
return "GL_FUNC_REVERSE_SUBTRACT";
// glBlendFuncSeparate
//case GL_ZERO: return "GL_ZERO"; // see GL_POINTS
//case GL_ONE: return "GL_ONE"; // see GL_LINES
case GL_SRC_COLOR:
return "GL_SRC_COLOR";
case GL_ONE_MINUS_SRC_COLOR:
return "GL_ONE_MINUS_SRC_COLOR";
case GL_DST_COLOR:
return "GL_DST_COLOR";
case GL_ONE_MINUS_DST_COLOR:
return "GL_ONE_MINUS_DST_COLOR";
case GL_SRC_ALPHA:
return "GL_SRC_ALPHA";
case GL_ONE_MINUS_SRC_ALPHA:
return "GL_ONE_MINUS_SRC_ALPHA";
case GL_DST_ALPHA:
return "GL_DST_ALPHA";
case GL_ONE_MINUS_DST_ALPHA:
return "GL_ONE_MINUS_DST_ALPHA";
case GL_CONSTANT_COLOR:
return "GL_CONSTANT_COLOR";
case GL_ONE_MINUS_CONSTANT_COLOR:
return "GL_ONE_MINUS_CONSTANT_COLOR";
case GL_CONSTANT_ALPHA:
return "GL_CONSTANT_ALPHA";
case GL_ONE_MINUS_CONSTANT_ALPHA:
return "GL_ONE_MINUS_CONSTANT_ALPHA";
case GL_SRC_ALPHA_SATURATE:
return "GL_SRC_ALPHA_SATURATE";
// glCullFace
case GL_FRONT:
return "GL_FRONT";
case GL_BACK:
return "GL_BACK";
case GL_FRONT_AND_BACK:
return "GL_FRONT_AND_BACK";
// glDepthFunc
case GL_NEVER:
return "GL_NEVER";
case GL_LESS:
return "GL_LESS";
case GL_LEQUAL:
return "GL_LEQUAL";
case GL_EQUAL:
return "GL_EQUAL";
case GL_GREATER:
return "GL_GREATER";
case GL_NOTEQUAL:
return "GL_NOTEQUAL";
case GL_GEQUAL:
return "GL_GEQUAL";
case GL_ALWAYS:
return "GL_ALWAYS";
// glFrontFace
case GL_CW:
return "GL_CW";
case GL_CCW:
return "GL_CCW";
// glTexParameter
case GL_NEAREST:
return "GL_NEAREST";
case GL_LINEAR:
return "GL_LINEAR";
case GL_NEAREST_MIPMAP_NEAREST:
return "GL_NEAREST_MIPMAP_NEAREST";
case GL_LINEAR_MIPMAP_NEAREST:
return "GL_LINEAR_MIPMAP_NEAREST";
case GL_NEAREST_MIPMAP_LINEAR:
return "GL_NEAREST_MIPMAP_LINEAR";
case GL_LINEAR_MIPMAP_LINEAR:
return "GL_LINEAR_MIPMAP_LINEAR";
// glSamplerParameter
case GL_REPEAT:
return "GL_REPEAT";
case GL_MIRRORED_REPEAT:
return "GL_MIRRORED_REPEAT";
case GL_CLAMP_TO_EDGE:
return "GL_CLAMP_TO_EDGE";
case GL_CLAMP_TO_BORDER:
return "GL_CLAMP_TO_BORDER";
default:
return "UNKNOWN_GL_CONSTANT[" + constant + "]";
}
}
}

View File

@@ -0,0 +1,81 @@
package com.tacz.guns.client.animation.gltf;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public class NodeModel {
private final List<NodeModel> children = new ArrayList<>();
private String name;
private float[] matrix;
private float[] translation;
private float[] rotation;
private float[] scale;
private NodeModel parent;
private static float[] check(float[] array, int expectedLength) {
if (array == null) {
return null;
}
if (array.length != expectedLength) {
throw new IllegalArgumentException("Expected " + expectedLength
+ " array elements, but found " + array.length);
}
return array;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public NodeModel getParent() {
return parent;
}
public List<NodeModel> getChildren() {
return Collections.unmodifiableList(children);
}
public void addChild(NodeModel child) {
Objects.requireNonNull(child, "The child may not be null");
children.add(child);
child.parent = this;
}
public float[] getMatrix() {
return matrix;
}
public void setMatrix(float[] matrix) {
this.matrix = check(matrix, 16);
}
public float[] getTranslation() {
return translation;
}
public void setTranslation(float[] translation) {
this.translation = check(translation, 3);
}
public float[] getRotation() {
return rotation;
}
public void setRotation(float[] rotation) {
this.rotation = check(rotation, 4);
}
public float[] getScale() {
return scale;
}
public void setScale(float[] scale) {
this.scale = check(scale, 3);
}
}

View File

@@ -0,0 +1,168 @@
/*
*
* Copyright 2015-2016 Marco Hutter - http://www.javagl.de
*
* 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.
*
* 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.
*/
package com.tacz.guns.client.animation.gltf.accessor;
import java.nio.ByteBuffer;
import java.util.Objects;
/**
* Package-private abstract base implementation of an {@link AccessorData}
*/
abstract class AbstractAccessorData implements AccessorData {
/**
* The component type
*/
private final Class<?> componentType;
/**
* The byte buffer of the buffer view that the accessor
* refers to
*/
private final ByteBuffer bufferViewByteBuffer;
/**
* The offset for the accessor inside the byte buffer of
* the buffer view
*/
private final int byteOffset;
/**
* The number of elements
*/
private final int numElements;
/**
* The number of components per element
*/
private final int numComponentsPerElement;
/**
* The number of bytes per component
*/
private final int numBytesPerComponent;
/**
* The stride, in number of bytes, between two consecutive elements
*/
private final int byteStridePerElement;
/**
* Default constructor
*
* @param componentType The component type
* @param bufferViewByteBuffer The byte buffer of the buffer view
* @param byteOffset The byte offset in the buffer view
* @param numElements The number of elements
* @param numComponentsPerElement The number of components per element
* @param numBytesPerComponent The number of bytes per component
* @param byteStride The byte stride between two elements. If this
* is <code>null</code> or <code>0</code>, then the stride will
* be the size of one element.
* @throws NullPointerException If the bufferViewByteBuffer is
* <code>null</code>
*/
AbstractAccessorData(Class<?> componentType,
ByteBuffer bufferViewByteBuffer, int byteOffset,
int numElements, int numComponentsPerElement,
int numBytesPerComponent, Integer byteStride) {
Objects.requireNonNull(bufferViewByteBuffer,
"The bufferViewByteBuffer is null");
this.componentType = componentType;
this.bufferViewByteBuffer = bufferViewByteBuffer;
this.byteOffset = byteOffset;
this.numElements = numElements;
this.numComponentsPerElement = numComponentsPerElement;
this.numBytesPerComponent = numBytesPerComponent;
if (byteStride == null || byteStride == 0) {
this.byteStridePerElement =
numComponentsPerElement * numBytesPerComponent;
} else {
this.byteStridePerElement = byteStride;
}
}
@Override
public final Class<?> getComponentType() {
return componentType;
}
@Override
public final int getNumElements() {
return numElements;
}
@Override
public final int getNumComponentsPerElement() {
return numComponentsPerElement;
}
@Override
public final int getTotalNumComponents() {
return numElements * numComponentsPerElement;
}
/**
* Returns the index of the byte in the byte buffer where the specified
* component starts
*
* @param elementIndex The element index
* @param componentIndex The component index
* @return The byte index
*/
protected final int getByteIndex(int elementIndex, int componentIndex) {
return byteOffset + elementIndex * byteStridePerElement + componentIndex * numBytesPerComponent;
}
/**
* Returns the underlying byte buffer
*
* @return The byte buffer
*/
protected final ByteBuffer getBufferViewByteBuffer() {
return bufferViewByteBuffer;
}
/**
* Returns the byte stride per element
*
* @return The byte stride
*/
protected final int getByteStridePerElement() {
return byteStridePerElement;
}
/**
* Returns the number of bytes per component
*
* @return The number of bytes per component
*/
protected final int getNumBytesPerComponent() {
return numBytesPerComponent;
}
}

View File

@@ -0,0 +1,310 @@
/*
* www.javagl.de - JglTF
*
* Copyright 2015-2016 Marco Hutter - http://www.javagl.de
*
* 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.
*
* 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.
*/
package com.tacz.guns.client.animation.gltf.accessor;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Locale;
/**
* A class for accessing the data that is described by an accessor.
* It allows accessing the byte buffer of the buffer view of the
* accessor, depending on the accessor parameters.<br>
* <br>
* This data consists of several elements (for example, 3D byte vectors),
* which consist of several components (for example, the 3 byte values).
*/
public final class AccessorByteData
extends AbstractAccessorData implements AccessorData {
/**
* Whether the data should be interpreted as unsigned values
*/
private final boolean unsigned;
/**
* Creates a new instance for accessing the data in the given
* byte buffer, according to the rules described by the given
* accessor parameters.
*
* @param componentType The component type
* @param bufferViewByteBuffer The byte buffer of the buffer view
* @param byteOffset The byte offset in the buffer view
* @param numElements The number of elements
* @param numComponentsPerElement The number of components per element
* @param byteStride The byte stride between two elements. If this
* is <code>null</code> or <code>0</code>, then the stride will
* be the size of one element.
* @throws NullPointerException If the bufferViewByteBuffer is
* <code>null</code>
* @throws IllegalArgumentException If the component type is not
* <code>GL_BYTE</code> or <code>GL_UNSIGEND_BYTE</code>
* @throws IllegalArgumentException If the given byte buffer does not
* have a sufficient capacity to provide the data for the accessor
*/
public AccessorByteData(int componentType,
ByteBuffer bufferViewByteBuffer, int byteOffset, int numElements,
int numComponentsPerElement, Integer byteStride) {
super(byte.class, bufferViewByteBuffer, byteOffset, numElements,
numComponentsPerElement, Byte.BYTES, byteStride);
AccessorDatas.validateByteType(componentType);
this.unsigned = AccessorDatas.isUnsignedType(componentType);
AccessorDatas.validateCapacity(byteOffset, getNumElements(),
getByteStridePerElement(), bufferViewByteBuffer.capacity());
}
/**
* Returns whether the data should be interpreted as unsigned
*
* @return Whether the data should be interpreted as unsigned
*/
public boolean isUnsigned() {
return unsigned;
}
/**
* Returns the value of the specified component of the specified element
*
* @param elementIndex The element index
* @param componentIndex The component index
* @return The value
* @throws IndexOutOfBoundsException If the given indices cause the
* underlying buffer to be accessed out of bounds
*/
public byte get(int elementIndex, int componentIndex) {
int byteIndex = getByteIndex(elementIndex, componentIndex);
return getBufferViewByteBuffer().get(byteIndex);
}
/**
* Returns the value of the specified component
*
* @param globalComponentIndex The global component index
* @return The value
* @throws IndexOutOfBoundsException If the given index causes the
* underlying buffer to be accessed out of bounds
*/
public byte get(int globalComponentIndex) {
int elementIndex =
globalComponentIndex / getNumComponentsPerElement();
int componentIndex =
globalComponentIndex % getNumComponentsPerElement();
return get(elementIndex, componentIndex);
}
/**
* Set the value of the specified component of the specified element
*
* @param elementIndex The element index
* @param componentIndex The component index
* @param value The value
* @throws IndexOutOfBoundsException If the given indices cause the
* underlying buffer to be accessed out of bounds
*/
public void set(int elementIndex, int componentIndex, byte value) {
int byteIndex = getByteIndex(elementIndex, componentIndex);
getBufferViewByteBuffer().put(byteIndex, value);
}
/**
* Set the value of the specified component
*
* @param globalComponentIndex The global component index
* @param value The value
* @throws IndexOutOfBoundsException If the given index causes the
* underlying buffer to be accessed out of bounds
*/
public void set(int globalComponentIndex, byte value) {
int elementIndex =
globalComponentIndex / getNumComponentsPerElement();
int componentIndex =
globalComponentIndex % getNumComponentsPerElement();
set(elementIndex, componentIndex, value);
}
/**
* Returns the value of the specified component of the specified element,
* taking into account whether the data {@link #isUnsigned()}: If the data
* is unsigned, the returned byte value will be converted into an
* unsigned integer value.
*
* @param elementIndex The element index
* @param componentIndex The component index
* @return The value
* @throws IndexOutOfBoundsException If the given indices cause the
* underlying buffer to be accessed out of bounds
*/
public int getInt(int elementIndex, int componentIndex) {
byte value = get(elementIndex, componentIndex);
return unsigned ? Byte.toUnsignedInt(value) : value;
}
/**
* Returns the value of the specified component, taking into account
* whether the data {@link #isUnsigned()}: If the data is unsigned,
* the returned byte value will be converted into an unsigned integer
* value.
*
* @param globalComponentIndex The global component index
* @return The value
* @throws IndexOutOfBoundsException If the given index causes the
* underlying buffer to be accessed out of bounds
*/
public int getInt(int globalComponentIndex) {
byte value = get(globalComponentIndex);
return unsigned ? Byte.toUnsignedInt(value) : value;
}
/**
* Returns an array containing the minimum component values of all elements
* of this accessor data. This will be an array whose length is the
* {@link #getNumComponentsPerElement() number of components per element}.
*
* @return The minimum values
*/
public byte[] computeMin() {
byte[] result = new byte[getNumComponentsPerElement()];
Arrays.fill(result, Byte.MAX_VALUE);
for (int e = 0; e < getNumElements(); e++) {
for (int c = 0; c < getNumComponentsPerElement(); c++) {
result[c] = (byte) Math.min(result[c], get(e, c));
}
}
return result;
}
/**
* Returns an array containing the maximum component values of all elements
* of this accessor data. This will be an array whose length is the
* {@link #getNumComponentsPerElement() number of components per element}.
*
* @return The minimum values
*/
public byte[] computeMax() {
byte[] result = new byte[getNumComponentsPerElement()];
Arrays.fill(result, Byte.MIN_VALUE);
for (int e = 0; e < getNumElements(); e++) {
for (int c = 0; c < getNumComponentsPerElement(); c++) {
result[c] = (byte) Math.max(result[c], get(e, c));
}
}
return result;
}
/**
* Returns an array containing the minimum component values of all elements
* of this accessor data. This will be an array whose length is the
* {@link #getNumComponentsPerElement() number of components per element}.
* These values are computed based on {@link #getInt(int, int)}.
*
* @return The minimum values
*/
public int[] computeMinInt() {
int[] result = new int[getNumComponentsPerElement()];
Arrays.fill(result, Integer.MAX_VALUE);
for (int e = 0; e < getNumElements(); e++) {
for (int c = 0; c < getNumComponentsPerElement(); c++) {
result[c] = Math.min(result[c], getInt(e, c));
}
}
return result;
}
/**
* Returns an array containing the maximum component values of all elements
* of this accessor data. This will be an array whose length is the
* {@link #getNumComponentsPerElement() number of components per element}.
* These values are computed based on {@link #getInt(int, int)}.
*
* @return The minimum values
*/
public int[] computeMaxInt() {
int result[] = new int[getNumComponentsPerElement()];
Arrays.fill(result, Integer.MIN_VALUE);
for (int e = 0; e < getNumElements(); e++) {
for (int c = 0; c < getNumComponentsPerElement(); c++) {
result[c] = Math.max(result[c], getInt(e, c));
}
}
return result;
}
@Override
public ByteBuffer createByteBuffer() {
int totalNumComponents = getTotalNumComponents();
int totalBytes = totalNumComponents * getNumBytesPerComponent();
ByteBuffer result = ByteBuffer.allocateDirect(totalBytes)
.order(ByteOrder.nativeOrder());
for (int i = 0; i < totalNumComponents; i++) {
byte component = get(i);
result.put(component);
}
result.position(0);
return result;
}
/**
* Creates a (potentially large!) string representation of the data
*
* @param locale The locale used for number formatting
* @param format The number format string
* @param elementsPerRow The number of elements per row. If this
* is not greater than 0, then all elements will be in a single row.
* @return The data string
*/
public String createString(
Locale locale, String format, int elementsPerRow) {
StringBuilder sb = new StringBuilder();
int nc = getNumComponentsPerElement();
sb.append("[");
for (int e = 0; e < getNumElements(); e++) {
if (e > 0) {
sb.append(", ");
if (elementsPerRow > 0 && (e % elementsPerRow) == 0) {
sb.append("\n ");
}
}
if (nc > 1) {
sb.append("(");
}
for (int c = 0; c < nc; c++) {
if (c > 0) {
sb.append(", ");
}
int component = getInt(e, c);
sb.append(String.format(locale, format, component));
}
if (nc > 1) {
sb.append(")");
}
}
sb.append("]");
return sb.toString();
}
}

View File

@@ -0,0 +1,89 @@
/*
*
* Copyright 2015-2016 Marco Hutter - http://www.javagl.de
*
* 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.
*
* 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.
*/
package com.tacz.guns.client.animation.gltf.accessor;
import java.nio.ByteBuffer;
/**
* Interface for classes that provide typed access to raw accessor data.
* The exact type of the data (and thus, the implementing class) is
* defined by the {@link #getComponentType() component type}:<br>
* <ul>
* <li>For <code>byte.class</code>, the implementation is an
* {@link AccessorByteData}</li>
* <li>For <code>short.class</code>, the implementation is an
* {@link AccessorShortData}</li>
* <li>For <code>int.class</code>, the implementation is an
* {@link AccessorIntData}</li>
* <li>For <code>float.class</code>, the implementation is an
* {@link AccessorFloatData}</li>
* </ul>
*/
public interface AccessorData {
/**
* Returns the type of the components that this class provides access to.
* This will usually be a primitive type, like <code>float.class</code>
* or <code>short.class</code>.
*
* @return The component type
*/
Class<?> getComponentType();
/**
* Returns the number of elements in this data (for example, the number
* of 3D vectors)
*
* @return The number of elements
*/
int getNumElements();
/**
* Returns the number of components per element (for example, 3 if the
* elements are 3D vectors)
*
* @return The number of components per element
*/
int getNumComponentsPerElement();
/**
* Returns the total number of components (that is, the number of elements
* multiplied with the number of components per element)
*
* @return The total number of components
*/
int getTotalNumComponents();
/**
* Creates a new, direct byte buffer (with native byte order) that
* contains the data for the accessor, in a compact form,
* without any offset, and without any additional stride (that is,
* all elements will be tightly packed).
*
* @return The byte buffer
*/
ByteBuffer createByteBuffer();
}

View File

@@ -0,0 +1,556 @@
/*
*
* Copyright 2015-2016 Marco Hutter - http://www.javagl.de
*
* 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.
*
* 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.
*/
package com.tacz.guns.client.animation.gltf.accessor;
import com.tacz.guns.client.animation.gltf.AccessorModel;
import com.tacz.guns.client.animation.gltf.BufferViewModel;
import com.tacz.guns.client.animation.gltf.GltfConstants;
import java.nio.ByteBuffer;
import java.util.Locale;
/**
* Methods to create instances of the accessor data utility classes
* that allow a <i>typed</i> access to the data that is contained in the
* buffer view that the accessor refers to.<br>
* <br>
* Unless otherwise noted, none of the arguments to these methods may
* be <code>null</code>.
*/
public class AccessorDatas {
/**
* Private constructor to prevent instantiation
*/
private AccessorDatas() {
// Private constructor to prevent instantiation
}
/**
* Create the {@link AccessorData} for the given {@link AccessorModel}
*
* @param accessorModel The {@link AccessorModel}
* @return The {@link AccessorData}
*/
public static AccessorData create(AccessorModel accessorModel) {
BufferViewModel bufferViewModel = accessorModel.getBufferViewModel();
ByteBuffer bufferViewData = bufferViewModel.getBufferViewData();
return create(accessorModel, bufferViewData);
}
/**
* Create the {@link AccessorData} for the given {@link AccessorModel}
* that refers to the data from the given buffer.
*
* @param accessorModel The {@link AccessorModel}
* @param byteBuffer The byte buffer containing the data
* @return The {@link AccessorData}
*/
public static AccessorData create(
AccessorModel accessorModel, ByteBuffer byteBuffer) {
if (accessorModel.getComponentDataType() == byte.class) {
return createByte(accessorModel, byteBuffer);
}
if (accessorModel.getComponentDataType() == short.class) {
return createShort(accessorModel, byteBuffer);
}
if (accessorModel.getComponentDataType() == int.class) {
return createInt(accessorModel, byteBuffer);
}
if (accessorModel.getComponentDataType() == float.class) {
return createFloat(accessorModel, byteBuffer);
}
return null;
}
/**
* Create an {@link AccessorData} depending on the given component type.
* This will return an {@link AccessorByteData}, {@link AccessorShortData},
* {@link AccessorIntData} or {@link AccessorFloatData}
*
* @param componentType The component type, as a GL constant (for example,
* <code>GL_UNSIGNED_SHORT</code> or <code>GL_FLOAT</code>)
* @param bufferViewData The buffer view data that the accessor refers to
* @param byteOffset The byte offset for the accessor
* @param count The count (number of elements) for the accessor
* @param numComponentsPerElement The number of components per element.
* For example, if the accessor type is <code>"VEC3"</code>, then this
* will be 3
* @param byteStride The optional byte stride for the accessor data
* @return The {@link AccessorData}
* @throws IllegalArgumentException If the given component type is
* not a valid GL constant
*/
public static AccessorData create(
int componentType, ByteBuffer bufferViewData, int byteOffset,
int count, int numComponentsPerElement, Integer byteStride) {
if (isByteType(componentType)) {
return new AccessorByteData(
componentType, bufferViewData, byteOffset, count,
numComponentsPerElement, byteStride);
}
if (isShortType(componentType)) {
return new AccessorShortData(
componentType, bufferViewData, byteOffset, count,
numComponentsPerElement, byteStride);
}
if (isIntType(componentType)) {
return new AccessorIntData(
componentType, bufferViewData, byteOffset, count,
numComponentsPerElement, byteStride);
}
if (isFloatType(componentType)) {
return new AccessorFloatData(
componentType, bufferViewData, byteOffset, count,
numComponentsPerElement, byteStride);
}
throw new IllegalArgumentException(
"Not a valid component type: " + componentType);
}
/**
* Returns whether the given constant is <code>GL_BYTE</code> or
* <code>GL_UNSIGNED_BYTE</code>.
*
* @param type The type constant
* @return Whether the type is a <code>byte</code> type
*/
public static boolean isByteType(int type) {
return
type == GltfConstants.GL_BYTE ||
type == GltfConstants.GL_UNSIGNED_BYTE;
}
/**
* Returns whether the given constant is <code>GL_SHORT</code> or
* <code>GL_UNSIGNED_SHORT</code>.
*
* @param type The type constant
* @return Whether the type is a <code>short</code> type
*/
public static boolean isShortType(int type) {
return
type == GltfConstants.GL_SHORT ||
type == GltfConstants.GL_UNSIGNED_SHORT;
}
/**
* Returns whether the given constant is <code>GL_INT</code> or
* <code>GL_UNSIGNED_INT</code>.
*
* @param type The type constant
* @return Whether the type is an <code>int</code> type
*/
public static boolean isIntType(int type) {
return
type == GltfConstants.GL_INT ||
type == GltfConstants.GL_UNSIGNED_INT;
}
/**
* Returns whether the given constant is <code>GL_FLOAT</code>.
*
* @param type The type constant
* @return Whether the type is a <code>float</code> type
*/
public static boolean isFloatType(int type) {
return type == GltfConstants.GL_FLOAT;
}
/**
* Returns whether the given constant is <code>GL_UNSIGNED_BYTE</code>,
* <code>GL_UNSIGNED_SHORT</code> or <code>GL_UNSIGNED_INT</code>.
*
* @param type The type constant
* @return Whether the type is an unsigned type
*/
static boolean isUnsignedType(int type) {
return
type == GltfConstants.GL_UNSIGNED_BYTE ||
type == GltfConstants.GL_UNSIGNED_SHORT ||
type == GltfConstants.GL_UNSIGNED_INT;
}
/**
* Make sure that the given type is <code>GL_BYTE</code> or
* <code>GL_UNSIGNED_BYTE</code>, and throw an
* <code>IllegalArgumentException</code> if this is not the case.
*
* @param type The type constant
* @throws IllegalArgumentException If the given type is not
* <code>GL_BYTE</code> or <code>GL_UNSIGNED_BYTE</code>
*/
static void validateByteType(int type) {
if (!isByteType(type)) {
throw new IllegalArgumentException(
"The type is not GL_BYTE or GL_UNSIGNED_BYTE, but " +
GltfConstants.stringFor(type));
}
}
/**
* Make sure that the given type is <code>GL_SHORT</code> or
* <code>GL_UNSIGNED_SHORT</code>, and throw an
* <code>IllegalArgumentException</code> if this is not the case.
*
* @param type The type constant
* @throws IllegalArgumentException If the given type is not
* <code>GL_SHORT</code> or <code>GL_UNSIGNED_BYTE</code>
*/
static void validateShortType(int type) {
if (!isShortType(type)) {
throw new IllegalArgumentException(
"The type is not GL_SHORT or GL_UNSIGNED_SHORT, but " +
GltfConstants.stringFor(type));
}
}
/**
* Make sure that the given type is <code>GL_INT</code> or
* <code>GL_UNSIGNED_INT</code>, and throw an
* <code>IllegalArgumentException</code> if this is not the case.
*
* @param type The type constant
* @throws IllegalArgumentException If the given type is not
* <code>GL_INT</code> or <code>GL_UNSIGNED_INT</code>
*/
static void validateIntType(int type) {
if (!isIntType(type)) {
throw new IllegalArgumentException(
"The type is not GL_INT or GL_UNSIGNED_INT, but " +
GltfConstants.stringFor(type));
}
}
/**
* Make sure that the given type is <code>GL_FLOAT</code>, and throw an
* <code>IllegalArgumentException</code> if this is not the case.
*
* @param type The type constant
* @throws IllegalArgumentException If the given type is not
* <code>GL_FLOAT</code>
*/
static void validateFloatType(int type) {
if (!isFloatType(type)) {
throw new IllegalArgumentException(
"The type is not GL_FLOAT, but " +
GltfConstants.stringFor(type));
}
}
/**
* Creates an {@link AccessorByteData} for the given {@link AccessorModel}
*
* @param accessorModel The {@link AccessorModel}
* @return The {@link AccessorByteData}
* @throws IllegalArgumentException If the
* {@link AccessorModel#getComponentType() component type} of the given
* accessor is not <code>GL_BYTE</code> or <code>GL_UNSIGNED_BYTE</code>
*/
static AccessorByteData createByte(AccessorModel accessorModel) {
BufferViewModel bufferViewModel = accessorModel.getBufferViewModel();
return createByte(accessorModel, bufferViewModel.getBufferViewData());
}
/**
* Creates an {@link AccessorByteData} for the given {@link AccessorModel}
*
* @param accessorModel The {@link AccessorModel}
* @param bufferViewByteBuffer The byte buffer of the
* {@link BufferViewModel} referenced by the {@link AccessorModel}
* @return The {@link AccessorByteData}
* @throws NullPointerException If any argument is <code>null</code>
* @throws IllegalArgumentException If the
* {@link AccessorModel#getComponentType() component type} of the given
* accessorModel is not <code>GL_BYTE</code> or
* <code>GL_UNSIGNED_BYTE</code>
*/
private static AccessorByteData createByte(
AccessorModel accessorModel, ByteBuffer bufferViewByteBuffer) {
return new AccessorByteData(accessorModel.getComponentType(),
bufferViewByteBuffer,
accessorModel.getByteOffset(),
accessorModel.getCount(),
accessorModel.getElementType().getNumComponents(),
accessorModel.getByteStride());
}
/**
* Creates an {@link AccessorShortData} for the given {@link AccessorModel}
*
* @param accessorModel The {@link AccessorModel}
* @return The {@link AccessorShortData}
* @throws IllegalArgumentException If the
* {@link AccessorModel#getComponentType() component type} of the given
* accessorModel is not <code>GL_SHORT</code> or
* <code>GL_UNSIGNED_SHORT</code>
*/
static AccessorShortData createShort(AccessorModel accessorModel) {
BufferViewModel bufferViewModel = accessorModel.getBufferViewModel();
return createShort(accessorModel, bufferViewModel.getBufferViewData());
}
/**
* Creates an {@link AccessorShortData} for the given {@link AccessorModel}
*
* @param accessorModel The {@link AccessorModel}
* @param bufferViewByteBuffer The byte buffer of the
* {@link BufferViewModel} referenced by the {@link AccessorModel}
* @return The {@link AccessorShortData}
* @throws NullPointerException If any argument is <code>null</code>
* @throws IllegalArgumentException If the
* {@link AccessorModel#getComponentType() component type} of the given
* accessorModel is not <code>GL_SHORT</code> or
* <code>GL_UNSIGNED_SHORT</code>
*/
private static AccessorShortData createShort(
AccessorModel accessorModel, ByteBuffer bufferViewByteBuffer) {
return new AccessorShortData(accessorModel.getComponentType(),
bufferViewByteBuffer,
accessorModel.getByteOffset(),
accessorModel.getCount(),
accessorModel.getElementType().getNumComponents(),
accessorModel.getByteStride());
}
/**
* Creates an {@link AccessorIntData} for the given {@link AccessorModel}
*
* @param accessorModel The {@link AccessorModel}
* @return The {@link AccessorIntData}
* @throws IllegalArgumentException If the
* {@link AccessorModel#getComponentType() component type} of the given
* accessorModel is not <code>GL_INT</code> or <code>GL_UNSIGNED_INT</code>
*/
static AccessorIntData createInt(AccessorModel accessorModel) {
BufferViewModel bufferViewModel = accessorModel.getBufferViewModel();
return createInt(accessorModel, bufferViewModel.getBufferViewData());
}
/**
* Creates an {@link AccessorIntData} for the given {@link AccessorModel}
*
* @param accessorModel The {@link AccessorModel}
* @param bufferViewByteBuffer The byte buffer of the
* {@link BufferViewModel} referenced by the {@link AccessorModel}
* @return The {@link AccessorIntData}
* @throws NullPointerException If any argument is <code>null</code>
* @throws IllegalArgumentException If the
* {@link AccessorModel#getComponentType() component type} of the given
* accessorModel is not <code>GL_INT</code> or <code>GL_UNSIGNED_INT</code>
*/
private static AccessorIntData createInt(
AccessorModel accessorModel, ByteBuffer bufferViewByteBuffer) {
return new AccessorIntData(accessorModel.getComponentType(),
bufferViewByteBuffer,
accessorModel.getByteOffset(),
accessorModel.getCount(),
accessorModel.getElementType().getNumComponents(),
accessorModel.getByteStride());
}
/**
* Creates an {@link AccessorFloatData} for the given {@link AccessorModel}
*
* @param accessorModel The {@link AccessorModel}
* @return The {@link AccessorFloatData}
* @throws IllegalArgumentException If the
* {@link AccessorModel#getComponentType() component type} of the given
* accessorModel is not <code>GL_FLOAT</code>
*/
public static AccessorFloatData createFloat(AccessorModel accessorModel) {
BufferViewModel bufferViewModel = accessorModel.getBufferViewModel();
return createFloat(accessorModel, bufferViewModel.getBufferViewData());
}
/**
* Creates an {@link AccessorFloatData} for the given {@link AccessorModel}
*
* @param accessorModel The {@link AccessorModel}
* @param bufferViewByteBuffer The byte buffer of the
* {@link BufferViewModel} referenced by the {@link AccessorModel}
* @return The {@link AccessorFloatData}
* @throws NullPointerException If any argument is <code>null</code>
* @throws IllegalArgumentException If the
*/
private static AccessorFloatData createFloat(
AccessorModel accessorModel, ByteBuffer bufferViewByteBuffer) {
return new AccessorFloatData(accessorModel.getComponentType(),
bufferViewByteBuffer,
accessorModel.getByteOffset(),
accessorModel.getCount(),
accessorModel.getElementType().getNumComponents(),
accessorModel.getByteStride());
}
/**
* Validate that the given {@link AccessorModel} parameters are valid for
* accessing a buffer with the given capacity
*
* @param byteOffset The byte offset
* @param numElements The number of elements
* @param byteStridePerElement The byte stride
* @param bufferCapacity The buffer capacity
* @throws IllegalArgumentException If the given byte buffer does not
* have a sufficient capacity
*/
static void validateCapacity(int byteOffset, int numElements,
int byteStridePerElement, int bufferCapacity) {
int expectedCapacity = numElements * byteStridePerElement;
if (expectedCapacity > bufferCapacity) {
throw new IllegalArgumentException(
"The accessorModel has an offset of " + byteOffset + " and " +
numElements + " elements with a byte stride of " +
byteStridePerElement + ", requiring " + expectedCapacity +
" bytes, but the buffer view has only " +
bufferCapacity + " bytes");
}
}
/**
* Compute the the minimum component values of the given
* {@link AccessorData}
*
* @param accessorData The {@link AccessorData}
* @return The minimum values
* @throws IllegalArgumentException If the given model has an unknown type
*/
public static Number[] computeMin(AccessorData accessorData) {
if (accessorData instanceof AccessorByteData) {
AccessorByteData accessorByteData =
(AccessorByteData) accessorData;
return NumberArrays.asNumbers(
accessorByteData.computeMinInt());
}
if (accessorData instanceof AccessorShortData) {
AccessorShortData accessorShortData =
(AccessorShortData) accessorData;
return NumberArrays.asNumbers(
accessorShortData.computeMinInt());
}
if (accessorData instanceof AccessorIntData) {
AccessorIntData accessorIntData =
(AccessorIntData) accessorData;
return NumberArrays.asNumbers(
accessorIntData.computeMinLong());
}
if (accessorData instanceof AccessorFloatData) {
AccessorFloatData accessorFloatData =
(AccessorFloatData) accessorData;
return NumberArrays.asNumbers(
accessorFloatData.computeMin());
}
throw new IllegalArgumentException(
"Invalid data type: " + accessorData);
}
/**
* Compute the the maximum component values of the given
* {@link AccessorData}
*
* @param accessorData The {@link AccessorData}
* @return The maximum values
* @throws IllegalArgumentException If the given model has an unknown type
*/
public static Number[] computeMax(AccessorData accessorData) {
if (accessorData instanceof AccessorByteData) {
AccessorByteData accessorByteData =
(AccessorByteData) accessorData;
return NumberArrays.asNumbers(
accessorByteData.computeMaxInt());
}
if (accessorData instanceof AccessorShortData) {
AccessorShortData accessorShortData =
(AccessorShortData) accessorData;
return NumberArrays.asNumbers(
accessorShortData.computeMaxInt());
}
if (accessorData instanceof AccessorIntData) {
AccessorIntData accessorIntData =
(AccessorIntData) accessorData;
return NumberArrays.asNumbers(
accessorIntData.computeMaxLong());
}
if (accessorData instanceof AccessorFloatData) {
AccessorFloatData accessorFloatData =
(AccessorFloatData) accessorData;
return NumberArrays.asNumbers(
accessorFloatData.computeMax());
}
throw new IllegalArgumentException(
"Invalid data type: " + accessorData);
}
/**
* Creates a (possibly large!) string representation of the given
* {@link AccessorData}, by calling
* {@link AccessorByteData#createString(Locale, String, int)},
* {@link AccessorShortData#createString(Locale, String, int)},
* {@link AccessorIntData#createString(Locale, String, int)} or
* {@link AccessorFloatData#createString(Locale, String, int)},
* depending on the type of the given data, with an unspecified
* format string.
*
* @param accessorData The {@link AccessorData}
* @param elementsPerRow The number of elements per row
* @return The string
*/
public static String createString(
AccessorData accessorData, int elementsPerRow) {
if (accessorData instanceof AccessorByteData) {
AccessorByteData accessorByteData =
(AccessorByteData) accessorData;
String accessorDataString =
accessorByteData.createString(
Locale.ENGLISH, "%4d", elementsPerRow);
return accessorDataString;
}
if (accessorData instanceof AccessorShortData) {
AccessorShortData accessorShortData =
(AccessorShortData) accessorData;
String accessorDataString =
accessorShortData.createString(
Locale.ENGLISH, "%6d", elementsPerRow);
return accessorDataString;
}
if (accessorData instanceof AccessorIntData) {
AccessorIntData accessorIntData =
(AccessorIntData) accessorData;
String accessorDataString =
accessorIntData.createString(
Locale.ENGLISH, "%11d", elementsPerRow);
return accessorDataString;
}
if (accessorData instanceof AccessorFloatData) {
AccessorFloatData accessorFloatData =
(AccessorFloatData) accessorData;
String accessorDataString =
accessorFloatData.createString(
Locale.ENGLISH, "%10.5f", elementsPerRow);
return accessorDataString;
}
return "Unknown accessor data type: " + accessorData;
}
}

View File

@@ -0,0 +1,226 @@
/*
* www.javagl.de - JglTF
*
* Copyright 2015-2016 Marco Hutter - http://www.javagl.de
*
* 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.
*
* 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.
*/
package com.tacz.guns.client.animation.gltf.accessor;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Locale;
/**
* A class for accessing the data that is described by an accessor.
* It allows accessing the byte buffer of the buffer view of the
* accessor, depending on the accessor parameters.<br>
* <br>
* This data consists of several elements (for example, 3D float vectors),
* which consist of several components (for example, the 3 float values).
*/
public final class AccessorFloatData
extends AbstractAccessorData
implements AccessorData {
/**
* Creates a new instance for accessing the data in the given
* byte buffer, according to the rules described by the given
* accessor parameters.
*
* @param componentType The component type
* @param bufferViewByteBuffer The byte buffer of the buffer view
* @param byteOffset The byte offset in the buffer view
* @param numElements The number of elements
* @param numComponentsPerElement The number of components per element
* @param byteStride The byte stride between two elements. If this
* is <code>null</code> or <code>0</code>, then the stride will
* be the size of one element.
* @throws NullPointerException If the bufferViewByteBuffer is
* <code>null</code>
* @throws IllegalArgumentException If the component type is not
* <code>GL_FLOAT</code>
* @throws IllegalArgumentException If the given byte buffer does not
* have a sufficient capacity to provide the data for the accessor
*/
public AccessorFloatData(int componentType,
ByteBuffer bufferViewByteBuffer, int byteOffset, int numElements,
int numComponentsPerElement, Integer byteStride) {
super(float.class, bufferViewByteBuffer, byteOffset, numElements,
numComponentsPerElement, Float.BYTES, byteStride);
AccessorDatas.validateFloatType(componentType);
AccessorDatas.validateCapacity(byteOffset, getNumElements(),
getByteStridePerElement(), bufferViewByteBuffer.capacity());
}
/**
* Returns the value of the specified component of the specified element
*
* @param elementIndex The element index
* @param componentIndex The component index
* @return The value
* @throws IndexOutOfBoundsException If the given indices cause the
* underlying buffer to be accessed out of bounds
*/
public float get(int elementIndex, int componentIndex) {
int byteIndex = getByteIndex(elementIndex, componentIndex);
return getBufferViewByteBuffer().getFloat(byteIndex);
}
/**
* Returns the value of the specified component
*
* @param globalComponentIndex The global component index
* @return The value
* @throws IndexOutOfBoundsException If the given index causes the
* underlying buffer to be accessed out of bounds
*/
public float get(int globalComponentIndex) {
int elementIndex =
globalComponentIndex / getNumComponentsPerElement();
int componentIndex =
globalComponentIndex % getNumComponentsPerElement();
return get(elementIndex, componentIndex);
}
/**
* Set the value of the specified component of the specified element
*
* @param elementIndex The element index
* @param componentIndex The component index
* @param value The value
* @throws IndexOutOfBoundsException If the given indices cause the
* underlying buffer to be accessed out of bounds
*/
public void set(int elementIndex, int componentIndex, float value) {
int byteIndex = getByteIndex(elementIndex, componentIndex);
getBufferViewByteBuffer().putFloat(byteIndex, value);
}
/**
* Set the value of the specified component
*
* @param globalComponentIndex The global component index
* @param value The value
* @throws IndexOutOfBoundsException If the given index causes the
* underlying buffer to be accessed out of bounds
*/
public void set(int globalComponentIndex, float value) {
int elementIndex =
globalComponentIndex / getNumComponentsPerElement();
int componentIndex =
globalComponentIndex % getNumComponentsPerElement();
set(elementIndex, componentIndex, value);
}
/**
* Returns an array containing the minimum component values of all elements
* of this accessor data. This will be an array whose length is the
* {@link #getNumComponentsPerElement() number of components per element}.
*
* @return The minimum values
*/
public float[] computeMin() {
float result[] = new float[getNumComponentsPerElement()];
Arrays.fill(result, Float.MAX_VALUE);
for (int e = 0; e < getNumElements(); e++) {
for (int c = 0; c < getNumComponentsPerElement(); c++) {
result[c] = Math.min(result[c], get(e, c));
}
}
return result;
}
/**
* Returns an array containing the maximum component values of all elements
* of this accessor data. This will be an array whose length is the
* {@link #getNumComponentsPerElement() number of components per element}.
*
* @return The minimum values
*/
public float[] computeMax() {
float result[] = new float[getNumComponentsPerElement()];
Arrays.fill(result, -Float.MAX_VALUE);
for (int e = 0; e < getNumElements(); e++) {
for (int c = 0; c < getNumComponentsPerElement(); c++) {
result[c] = Math.max(result[c], get(e, c));
}
}
return result;
}
@Override
public ByteBuffer createByteBuffer() {
int totalNumComponents = getTotalNumComponents();
int totalBytes = totalNumComponents * getNumBytesPerComponent();
ByteBuffer result = ByteBuffer.allocateDirect(totalBytes)
.order(ByteOrder.nativeOrder());
for (int i = 0; i < totalNumComponents; i++) {
float component = get(i);
result.putFloat(component);
}
result.position(0);
return result;
}
/**
* Creates a (potentially large!) string representation of the data
*
* @param locale The locale used for number formatting
* @param format The number format string
* @param elementsPerRow The number of elements per row. If this
* is not greater than 0, then all elements will be in a single row.
* @return The data string
*/
public String createString(
Locale locale, String format, int elementsPerRow) {
StringBuilder sb = new StringBuilder();
int nc = getNumComponentsPerElement();
sb.append("[");
for (int e = 0; e < getNumElements(); e++) {
if (e > 0) {
sb.append(", ");
if (elementsPerRow > 0 && (e % elementsPerRow) == 0) {
sb.append("\n ");
}
}
if (nc > 1) {
sb.append("(");
}
for (int c = 0; c < nc; c++) {
if (c > 0) {
sb.append(", ");
}
float component = get(e, c);
sb.append(String.format(locale, format, component));
}
if (nc > 1) {
sb.append(")");
}
}
sb.append("]");
return sb.toString();
}
}

View File

@@ -0,0 +1,311 @@
/*
* www.javagl.de - JglTF
*
* Copyright 2015-2016 Marco Hutter - http://www.javagl.de
*
* 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.
*
* 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.
*/
package com.tacz.guns.client.animation.gltf.accessor;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Locale;
/**
* A class for accessing the data that is described by an accessor.
* It allows accessing the byte buffer of the buffer view of the
* accessor, depending on the accessor parameters.<br>
* <br>
* This data consists of several elements (for example, 3D int vectors),
* which consist of several components (for example, the 3 int values).
*/
public final class AccessorIntData
extends AbstractAccessorData
implements AccessorData {
/**
* Whether the data should be interpreted as unsigned values
*/
private final boolean unsigned;
/**
* Creates a new instance for accessing the data in the given
* byte buffer, according to the rules described by the given
* accessor parameters.
*
* @param componentType The component type
* @param bufferViewByteBuffer The byte buffer of the buffer view
* @param byteOffset The byte offset in the buffer view
* @param numElements The number of elements
* @param numComponentsPerElement The number of components per element
* @param byteStride The byte stride between two elements. If this
* is <code>null</code> or <code>0</code>, then the stride will
* be the size of one element.
* @throws NullPointerException If the bufferViewByteBuffer is
* <code>null</code>
* @throws IllegalArgumentException If the component type is not
* <code>GL_INT</code> or <code>GL_UNSIGEND_INT</code>
* @throws IllegalArgumentException If the given byte buffer does not
* have a sufficient capacity to provide the data for the accessor
*/
public AccessorIntData(int componentType,
ByteBuffer bufferViewByteBuffer, int byteOffset, int numElements,
int numComponentsPerElement, Integer byteStride) {
super(int.class, bufferViewByteBuffer, byteOffset, numElements,
numComponentsPerElement, Integer.BYTES, byteStride);
AccessorDatas.validateIntType(componentType);
this.unsigned = AccessorDatas.isUnsignedType(componentType);
AccessorDatas.validateCapacity(byteOffset, getNumElements(),
getByteStridePerElement(), bufferViewByteBuffer.capacity());
}
/**
* Returns whether the data should be interpreted as unsigned
*
* @return Whether the data should be interpreted as unsigned
*/
public boolean isUnsigned() {
return unsigned;
}
/**
* Returns the value of the specified component of the specified element
*
* @param elementIndex The element index
* @param componentIndex The component index
* @return The value
* @throws IndexOutOfBoundsException If the given indices cause the
* underlying buffer to be accessed out of bounds
*/
public int get(int elementIndex, int componentIndex) {
int byteIndex = getByteIndex(elementIndex, componentIndex);
return getBufferViewByteBuffer().getInt(byteIndex);
}
/**
* Returns the value of the specified component
*
* @param globalComponentIndex The global component index
* @return The value
* @throws IndexOutOfBoundsException If the given index causes the
* underlying buffer to be accessed out of bounds
*/
public int get(int globalComponentIndex) {
int elementIndex =
globalComponentIndex / getNumComponentsPerElement();
int componentIndex =
globalComponentIndex % getNumComponentsPerElement();
return get(elementIndex, componentIndex);
}
/**
* Set the value of the specified component of the specified element
*
* @param elementIndex The element index
* @param componentIndex The component index
* @param value The value
* @throws IndexOutOfBoundsException If the given indices cause the
* underlying buffer to be accessed out of bounds
*/
public void set(int elementIndex, int componentIndex, int value) {
int byteIndex = getByteIndex(elementIndex, componentIndex);
getBufferViewByteBuffer().putInt(byteIndex, value);
}
/**
* Set the value of the specified component
*
* @param globalComponentIndex The global component index
* @param value The value
* @throws IndexOutOfBoundsException If the given index causes the
* underlying buffer to be accessed out of bounds
*/
public void set(int globalComponentIndex, int value) {
int elementIndex =
globalComponentIndex / getNumComponentsPerElement();
int componentIndex =
globalComponentIndex % getNumComponentsPerElement();
set(elementIndex, componentIndex, value);
}
/**
* Returns the value of the specified component of the specified element,
* taking into account whether the data {@link #isUnsigned()}: If the data
* is unsigned, the returned int value will be converted into an
* unsigned long value.
*
* @param elementIndex The element index
* @param componentIndex The component index
* @return The value
* @throws IndexOutOfBoundsException If the given indices cause the
* underlying buffer to be accessed out of bounds
*/
public long getLong(int elementIndex, int componentIndex) {
int value = get(elementIndex, componentIndex);
return unsigned ? Integer.toUnsignedLong(value) : value;
}
/**
* Returns the value of the specified component, taking into account
* whether the data {@link #isUnsigned()}: If the data is unsigned,
* the returned int value will be converted into an unsigned integer
* value.
*
* @param globalComponentIndex The global component index
* @return The value
* @throws IndexOutOfBoundsException If the given index causes the
* underlying buffer to be accessed out of bounds
*/
public long getLong(int globalComponentIndex) {
int value = get(globalComponentIndex);
return unsigned ? Integer.toUnsignedLong(value) : value;
}
/**
* Returns an array containing the minimum component values of all elements
* of this accessor data. This will be an array whose length is the
* {@link #getNumComponentsPerElement() number of components per element}.
*
* @return The minimum values
*/
public int[] computeMin() {
int result[] = new int[getNumComponentsPerElement()];
Arrays.fill(result, Integer.MAX_VALUE);
for (int e = 0; e < getNumElements(); e++) {
for (int c = 0; c < getNumComponentsPerElement(); c++) {
result[c] = Math.min(result[c], get(e, c));
}
}
return result;
}
/**
* Returns an array containing the maximum component values of all elements
* of this accessor data. This will be an array whose length is the
* {@link #getNumComponentsPerElement() number of components per element}.
*
* @return The minimum values
*/
public int[] computeMax() {
int result[] = new int[getNumComponentsPerElement()];
Arrays.fill(result, Integer.MIN_VALUE);
for (int e = 0; e < getNumElements(); e++) {
for (int c = 0; c < getNumComponentsPerElement(); c++) {
result[c] = Math.max(result[c], get(e, c));
}
}
return result;
}
/**
* Returns an array containing the minimum component values of all elements
* of this accessor data. This will be an array whose length is the
* {@link #getNumComponentsPerElement() number of components per element}.
* These values are computed based on {@link #getLong(int, int)}.
*
* @return The minimum values
*/
public long[] computeMinLong() {
long result[] = new long[getNumComponentsPerElement()];
Arrays.fill(result, Long.MAX_VALUE);
for (int e = 0; e < getNumElements(); e++) {
for (int c = 0; c < getNumComponentsPerElement(); c++) {
result[c] = Math.min(result[c], getLong(e, c));
}
}
return result;
}
/**
* Returns an array containing the maximum component values of all elements
* of this accessor data. This will be an array whose length is the
* {@link #getNumComponentsPerElement() number of components per element}.
* These values are computed based on {@link #getLong(int, int)}.
*
* @return The minimum values
*/
public long[] computeMaxLong() {
long result[] = new long[getNumComponentsPerElement()];
Arrays.fill(result, Long.MIN_VALUE);
for (int e = 0; e < getNumElements(); e++) {
for (int c = 0; c < getNumComponentsPerElement(); c++) {
result[c] = Math.max(result[c], getLong(e, c));
}
}
return result;
}
@Override
public ByteBuffer createByteBuffer() {
int totalNumComponents = getTotalNumComponents();
int totalBytes = totalNumComponents * getNumBytesPerComponent();
ByteBuffer result = ByteBuffer.allocateDirect(totalBytes)
.order(ByteOrder.nativeOrder());
for (int i = 0; i < totalNumComponents; i++) {
int component = get(i);
result.putInt(component);
}
result.position(0);
return result;
}
/**
* Creates a (potentially large!) string representation of the data
*
* @param locale The locale used for number formatting
* @param format The number format string
* @param elementsPerRow The number of elements per row. If this
* is not greater than 0, then all elements will be in a single row.
* @return The data string
*/
public String createString(
Locale locale, String format, int elementsPerRow) {
StringBuilder sb = new StringBuilder();
int nc = getNumComponentsPerElement();
sb.append("[");
for (int e = 0; e < getNumElements(); e++) {
if (e > 0) {
sb.append(", ");
if (elementsPerRow > 0 && (e % elementsPerRow) == 0) {
sb.append("\n ");
}
}
if (nc > 1) {
sb.append("(");
}
for (int c = 0; c < nc; c++) {
if (c > 0) {
sb.append(", ");
}
long component = getLong(e, c);
sb.append(String.format(locale, format, component));
}
if (nc > 1) {
sb.append(")");
}
}
sb.append("]");
return sb.toString();
}
}

View File

@@ -0,0 +1,312 @@
/*
* www.javagl.de - JglTF
*
* Copyright 2015-2016 Marco Hutter - http://www.javagl.de
*
* 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.
*
* 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.
*/
package com.tacz.guns.client.animation.gltf.accessor;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Locale;
/**
* A class for accessing the data that is described by an accessor.
* It allows accessing the byte buffer of the buffer view of the
* accessor, depending on the accessor parameters.<br>
* <br>
* This data consists of several elements (for example, 3D short vectors),
* which consist of several components (for example, the 3 short values).
*/
public final class AccessorShortData
extends AbstractAccessorData
implements AccessorData {
/**
* Whether the data should be interpreted as unsigned values
*/
private final boolean unsigned;
/**
* Creates a new instance for accessing the data in the given
* byte buffer, according to the rules described by the given
* accessor parameters.
*
* @param componentType The component type
* @param bufferViewByteBuffer The byte buffer of the buffer view
* @param byteOffset The byte offset in the buffer view
* @param numElements The number of elements
* @param numComponentsPerElement The number of components per element
* @param byteStride The byte stride between two elements. If this
* is <code>null</code> or <code>0</code>, then the stride will
* be the size of one element.
* @throws NullPointerException If the bufferViewByteBuffer is
* <code>null</code>
* @throws IllegalArgumentException If the component type is not
* <code>GL_SHORT</code> or <code>GL_UNSIGEND_SHORT</code>
* @throws IllegalArgumentException If the given byte buffer does not
* have a sufficient capacity to provide the data for the accessor
*/
public AccessorShortData(int componentType,
ByteBuffer bufferViewByteBuffer, int byteOffset, int numElements,
int numComponentsPerElement, Integer byteStride) {
super(short.class, bufferViewByteBuffer, byteOffset, numElements,
numComponentsPerElement, Short.BYTES, byteStride);
AccessorDatas.validateShortType(componentType);
this.unsigned = AccessorDatas.isUnsignedType(componentType);
AccessorDatas.validateCapacity(byteOffset, getNumElements(),
getByteStridePerElement(), bufferViewByteBuffer.capacity());
}
/**
* Returns whether the data should be interpreted as unsigned
*
* @return Whether the data should be interpreted as unsigned
*/
public boolean isUnsigned() {
return unsigned;
}
/**
* Returns the value of the specified component of the specified element
*
* @param elementIndex The element index
* @param componentIndex The component index
* @return The value
* @throws IndexOutOfBoundsException If the given indices cause the
* underlying buffer to be accessed out of bounds
*/
public short get(int elementIndex, int componentIndex) {
int byteIndex = getByteIndex(elementIndex, componentIndex);
return getBufferViewByteBuffer().getShort(byteIndex);
}
/**
* Returns the value of the specified component
*
* @param globalComponentIndex The global component index
* @return The value
* @throws IndexOutOfBoundsException If the given index causes the
* underlying buffer to be accessed out of bounds
*/
public short get(int globalComponentIndex) {
int elementIndex =
globalComponentIndex / getNumComponentsPerElement();
int componentIndex =
globalComponentIndex % getNumComponentsPerElement();
return get(elementIndex, componentIndex);
}
/**
* Set the value of the specified component of the specified element
*
* @param elementIndex The element index
* @param componentIndex The component index
* @param value The value
* @throws IndexOutOfBoundsException If the given indices cause the
* underlying buffer to be accessed out of bounds
*/
public void set(int elementIndex, int componentIndex, short value) {
int byteIndex = getByteIndex(elementIndex, componentIndex);
getBufferViewByteBuffer().putShort(byteIndex, value);
}
/**
* Set the value of the specified component
*
* @param globalComponentIndex The global component index
* @param value The value
* @throws IndexOutOfBoundsException If the given index causes the
* underlying buffer to be accessed out of bounds
*/
public void set(int globalComponentIndex, short value) {
int elementIndex =
globalComponentIndex / getNumComponentsPerElement();
int componentIndex =
globalComponentIndex % getNumComponentsPerElement();
set(elementIndex, componentIndex, value);
}
/**
* Returns the value of the specified component of the specified element,
* taking into account whether the data {@link #isUnsigned()}: If the data
* is unsigned, the returned short value will be converted into an
* unsigned integer value.
*
* @param elementIndex The element index
* @param componentIndex The component index
* @return The value
* @throws IndexOutOfBoundsException If the given indices cause the
* underlying buffer to be accessed out of bounds
*/
public int getInt(int elementIndex, int componentIndex) {
short value = get(elementIndex, componentIndex);
return unsigned ? Short.toUnsignedInt(value) : value;
}
/**
* Returns the value of the specified component, taking into account
* whether the data {@link #isUnsigned()}: If the data is unsigned,
* the returned short value will be converted into an unsigned integer
* value.
*
* @param globalComponentIndex The global component index
* @return The value
* @throws IndexOutOfBoundsException If the given index causes the
* underlying buffer to be accessed out of bounds
*/
public int getInt(int globalComponentIndex) {
short value = get(globalComponentIndex);
return unsigned ? Short.toUnsignedInt(value) : value;
}
/**
* Returns an array containing the minimum component values of all elements
* of this accessor data. This will be an array whose length is the
* {@link #getNumComponentsPerElement() number of components per element}.
*
* @return The minimum values
*/
public short[] computeMin() {
short result[] = new short[getNumComponentsPerElement()];
Arrays.fill(result, Short.MAX_VALUE);
for (int e = 0; e < getNumElements(); e++) {
for (int c = 0; c < getNumComponentsPerElement(); c++) {
result[c] = (short) Math.min(result[c], get(e, c));
}
}
return result;
}
/**
* Returns an array containing the maximum component values of all elements
* of this accessor data. This will be an array whose length is the
* {@link #getNumComponentsPerElement() number of components per element}.
*
* @return The minimum values
*/
public short[] computeMax() {
short result[] = new short[getNumComponentsPerElement()];
Arrays.fill(result, Short.MIN_VALUE);
for (int e = 0; e < getNumElements(); e++) {
for (int c = 0; c < getNumComponentsPerElement(); c++) {
result[c] = (short) Math.max(result[c], get(e, c));
}
}
return result;
}
/**
* Returns an array containing the minimum component values of all elements
* of this accessor data. This will be an array whose length is the
* {@link #getNumComponentsPerElement() number of components per element}.
* These values are computed based on {@link #getInt(int, int)}.
*
* @return The minimum values
*/
public int[] computeMinInt() {
int result[] = new int[getNumComponentsPerElement()];
Arrays.fill(result, Integer.MAX_VALUE);
for (int e = 0; e < getNumElements(); e++) {
for (int c = 0; c < getNumComponentsPerElement(); c++) {
result[c] = Math.min(result[c], getInt(e, c));
}
}
return result;
}
/**
* Returns an array containing the maximum component values of all elements
* of this accessor data. This will be an array whose length is the
* {@link #getNumComponentsPerElement() number of components per element}.
* These values are computed based on {@link #getInt(int, int)}.
*
* @return The minimum values
*/
public int[] computeMaxInt() {
int result[] = new int[getNumComponentsPerElement()];
Arrays.fill(result, Integer.MIN_VALUE);
for (int e = 0; e < getNumElements(); e++) {
for (int c = 0; c < getNumComponentsPerElement(); c++) {
result[c] = Math.max(result[c], getInt(e, c));
}
}
return result;
}
@Override
public ByteBuffer createByteBuffer() {
int totalNumComponents = getTotalNumComponents();
int totalBytes = totalNumComponents * getNumBytesPerComponent();
ByteBuffer result = ByteBuffer.allocateDirect(totalBytes)
.order(ByteOrder.nativeOrder());
for (int i = 0; i < totalNumComponents; i++) {
short component = get(i);
result.putShort(component);
}
result.position(0);
return result;
}
/**
* Creates a (potentially large!) string representation of the data
*
* @param locale The locale used for number formatting
* @param format The number format string
* @param elementsPerRow The number of elements per row. If this
* is not greater than 0, then all elements will be in a single row.
* @return The data string
*/
public String createString(
Locale locale, String format, int elementsPerRow) {
StringBuilder sb = new StringBuilder();
int nc = getNumComponentsPerElement();
sb.append("[");
for (int e = 0; e < getNumElements(); e++) {
if (e > 0) {
sb.append(", ");
if (elementsPerRow > 0 && (e % elementsPerRow) == 0) {
sb.append("\n ");
}
}
if (nc > 1) {
sb.append("(");
}
for (int c = 0; c < nc; c++) {
if (c > 0) {
sb.append(", ");
}
int component = getInt(e, c);
sb.append(String.format(locale, format, component));
}
if (nc > 1) {
sb.append(")");
}
}
sb.append("]");
return sb.toString();
}
}

View File

@@ -0,0 +1,314 @@
package com.tacz.guns.client.animation.gltf.accessor;
import java.util.logging.Logger;
public class AccessorSparseUtils {
/**
* The logger used in this class
*/
private static final Logger logger =
Logger.getLogger(AccessorSparseUtils.class.getName());
/**
* Private constructor to prevent instantiation
*/
private AccessorSparseUtils() {
// Private constructor to prevent instantiation
}
/**
* Extract indices from the given {@link AccessorData}. The given
* {@link AccessorData} must contain an integral type. That is,
* its {@link AccessorData#getComponentType() component type} must
* be <code>byte.class</code>, <code>short.class</code> or
* <code>int.class</code>.
*
* @param accessorData The {@link AccessorData}
* @return The indices
* @throws IllegalArgumentException If the given data does not contain
* an integral type
*/
private static int[] extractIndices(AccessorData accessorData) {
if (accessorData.getComponentType() == byte.class) {
AccessorByteData accessorByteData =
(AccessorByteData) accessorData;
int numElements = accessorByteData.getNumElements();
int indices[] = new int[numElements];
for (int i = 0; i < numElements; i++) {
indices[i] = accessorByteData.getInt(i, 0);
}
return indices;
}
if (accessorData.getComponentType() == short.class) {
AccessorShortData accessorShortData =
(AccessorShortData) accessorData;
int numElements = accessorShortData.getNumElements();
int indices[] = new int[numElements];
for (int i = 0; i < numElements; i++) {
indices[i] = accessorShortData.getInt(i, 0);
}
return indices;
}
if (accessorData.getComponentType() == int.class) {
AccessorIntData accessorIntData =
(AccessorIntData) accessorData;
int numElements = accessorIntData.getNumElements();
int indices[] = new int[numElements];
for (int i = 0; i < numElements; i++) {
indices[i] = accessorIntData.get(i, 0);
}
return indices;
}
throw new IllegalArgumentException(
"Invalid type for indices: " + accessorData.getComponentType());
}
/**
* Substitute the data in the given dense {@link AccessorData} with the
* (sparse) data that is provided by the given {@link AccessorData}
* objects. <br>
* <br>
* The <code>baseAccessorData</code> is the data that the dense data
* will be initialized with, <b>before</b> applying the substitution
* that is defined by the given sparse indices and values.<br>
* <br>
* The <code>sparseIndicesAccessorData</code> is an {@link AccessorData}
* that was created from the <code>accessor.sparse.indices</code>
* structure.<br>
* <br>
* The <code>sparseValuesAccessorData</code> is an {@link AccessorData}
* that was created from the <code>accessor.sparse.values</code>
* structure.<br>
* <br>
* This method does very few sanity checks. The caller is responsible
* for calling it only with arguments that are valid (in terms of
* indices and data types).
*
* @param denseAccessorData The dense {@link AccessorData} to be filled
* @param baseAccessorData The optional "base" {@link AccessorData}
* @param sparseIndicesAccessorData The sparse indices {@link AccessorData}
* @param sparseValuesAccessorData The sparse values {@link AccessorData}
* @throws IllegalArgumentException If the sparseIndicesAccessorData does
* not contain data with an integral type (byte, short, int).
*/
public static void substituteAccessorData(
AccessorData denseAccessorData,
AccessorData baseAccessorData,
AccessorData sparseIndicesAccessorData,
AccessorData sparseValuesAccessorData) {
Class<?> componentType = denseAccessorData.getComponentType();
if (componentType == byte.class) {
AccessorByteData sparseValuesAccessorByteData =
(AccessorByteData) sparseValuesAccessorData;
AccessorByteData baseAccessorByteData =
(AccessorByteData) baseAccessorData;
AccessorByteData denseAccessorByteData =
(AccessorByteData) denseAccessorData;
substituteByteAccessorData(
denseAccessorByteData,
baseAccessorByteData,
sparseIndicesAccessorData,
sparseValuesAccessorByteData);
} else if (componentType == short.class) {
AccessorShortData sparseValuesAccessorShortData =
(AccessorShortData) sparseValuesAccessorData;
AccessorShortData baseAccessorShortData =
(AccessorShortData) baseAccessorData;
AccessorShortData denseAccessorShortData =
(AccessorShortData) denseAccessorData;
substituteShortAccessorData(
denseAccessorShortData,
baseAccessorShortData,
sparseIndicesAccessorData,
sparseValuesAccessorShortData);
} else if (componentType == int.class) {
AccessorIntData sparseValuesAccessorIntData =
(AccessorIntData) sparseValuesAccessorData;
AccessorIntData baseAccessorIntData =
(AccessorIntData) baseAccessorData;
AccessorIntData denseAccessorIntData =
(AccessorIntData) denseAccessorData;
substituteIntAccessorData(
denseAccessorIntData,
baseAccessorIntData,
sparseIndicesAccessorData,
sparseValuesAccessorIntData);
} else if (componentType == float.class) {
AccessorFloatData sparseValuesAccessorFloatData =
(AccessorFloatData) sparseValuesAccessorData;
AccessorFloatData baseAccessorFloatData =
(AccessorFloatData) baseAccessorData;
AccessorFloatData denseAccessorFloatData =
(AccessorFloatData) denseAccessorData;
substituteFloatAccessorData(
denseAccessorFloatData,
baseAccessorFloatData,
sparseIndicesAccessorData,
sparseValuesAccessorFloatData);
} else {
logger.warning("Invalid component type for accessor: "
+ componentType);
}
}
/**
* See {@link #substituteAccessorData}
*
* @param denseAccessorData The dense {@link AccessorData} to be filled
* @param baseAccessorData The optional "base" {@link AccessorData}
* @param sparseIndicesAccessorData The sparse indices {@link AccessorData}
* @param sparseValuesAccessorData The sparse values {@link AccessorData}
* @throws IllegalArgumentException If the sparseIndicesAccessorData does
* not contain data with an integral type (byte, short, int).
*/
private static void substituteByteAccessorData(
AccessorByteData denseAccessorData,
AccessorByteData baseAccessorData,
AccessorData sparseIndicesAccessorData,
AccessorByteData sparseValuesAccessorData) {
int numElements = denseAccessorData.getNumElements();
int numComponentsPerElement =
denseAccessorData.getNumComponentsPerElement();
if (baseAccessorData != null) {
// Fill the dense AccessorData with the base data
for (int e = 0; e < numElements; e++) {
for (int c = 0; c < numComponentsPerElement; c++) {
byte value = baseAccessorData.get(e, c);
denseAccessorData.set(e, c, value);
}
}
}
// Apply the substitution based on the sparse indices and values
int indices[] = extractIndices(sparseIndicesAccessorData);
for (int i = 0; i < indices.length; i++) {
int targetElementIndex = indices[i];
for (int c = 0; c < numComponentsPerElement; c++) {
byte substitution = sparseValuesAccessorData.get(i, c);
denseAccessorData.set(targetElementIndex, c, substitution);
}
}
}
/**
* See {@link #substituteAccessorData}
*
* @param denseAccessorData The dense {@link AccessorData} to be filled
* @param baseAccessorData The optional "base" {@link AccessorData}
* @param sparseIndicesAccessorData The sparse indices {@link AccessorData}
* @param sparseValuesAccessorData The sparse values {@link AccessorData}
* @throws IllegalArgumentException If the sparseIndicesAccessorData does
* not contain data with an integral type (byte, short, int).
*/
private static void substituteShortAccessorData(
AccessorShortData denseAccessorData,
AccessorShortData baseAccessorData,
AccessorData sparseIndicesAccessorData,
AccessorShortData sparseValuesAccessorData) {
int numElements = denseAccessorData.getNumElements();
int numComponentsPerElement =
denseAccessorData.getNumComponentsPerElement();
if (baseAccessorData != null) {
// Fill the dense AccessorData with the base data
for (int e = 0; e < numElements; e++) {
for (int c = 0; c < numComponentsPerElement; c++) {
short value = baseAccessorData.get(e, c);
denseAccessorData.set(e, c, value);
}
}
}
// Apply the substitution based on the sparse indices and values
int indices[] = extractIndices(sparseIndicesAccessorData);
for (int i = 0; i < indices.length; i++) {
int targetElementIndex = indices[i];
for (int c = 0; c < numComponentsPerElement; c++) {
short substitution = sparseValuesAccessorData.get(i, c);
denseAccessorData.set(targetElementIndex, c, substitution);
}
}
}
/**
* See {@link #substituteAccessorData}
*
* @param denseAccessorData The dense {@link AccessorData} to be filled
* @param baseAccessorData The optional "base" {@link AccessorData}
* @param sparseIndicesAccessorData The sparse indices {@link AccessorData}
* @param sparseValuesAccessorData The sparse values {@link AccessorData}
* @throws IllegalArgumentException If the sparseIndicesAccessorData does
* not contain data with an integral type (byte, short, int).
*/
private static void substituteIntAccessorData(
AccessorIntData denseAccessorData,
AccessorIntData baseAccessorData,
AccessorData sparseIndicesAccessorData,
AccessorIntData sparseValuesAccessorData) {
int numElements = denseAccessorData.getNumElements();
int numComponentsPerElement =
denseAccessorData.getNumComponentsPerElement();
if (baseAccessorData != null) {
// Fill the dense AccessorData with the base data
for (int e = 0; e < numElements; e++) {
for (int c = 0; c < numComponentsPerElement; c++) {
int value = baseAccessorData.get(e, c);
denseAccessorData.set(e, c, value);
}
}
}
// Apply the substitution based on the sparse indices and values
int indices[] = extractIndices(sparseIndicesAccessorData);
for (int i = 0; i < indices.length; i++) {
int targetElementIndex = indices[i];
for (int c = 0; c < numComponentsPerElement; c++) {
int substitution = sparseValuesAccessorData.get(i, c);
denseAccessorData.set(targetElementIndex, c, substitution);
}
}
}
/**
* See {@link #substituteAccessorData}
*
* @param denseAccessorData The dense {@link AccessorData} to be filled
* @param baseAccessorData The optional "base" {@link AccessorData}
* @param sparseIndicesAccessorData The sparse indices {@link AccessorData}
* @param sparseValuesAccessorData The sparse values {@link AccessorData}
* @throws IllegalArgumentException If the sparseIndicesAccessorData does
* not contain data with an integral type (byte, short, int).
*/
private static void substituteFloatAccessorData(
AccessorFloatData denseAccessorData,
AccessorFloatData baseAccessorData,
AccessorData sparseIndicesAccessorData,
AccessorFloatData sparseValuesAccessorData) {
int numElements = denseAccessorData.getNumElements();
int numComponentsPerElement =
denseAccessorData.getNumComponentsPerElement();
if (baseAccessorData != null) {
// Fill the dense AccessorData with the base data
for (int e = 0; e < numElements; e++) {
for (int c = 0; c < numComponentsPerElement; c++) {
float value = baseAccessorData.get(e, c);
denseAccessorData.set(e, c, value);
}
}
}
// Apply the substitution based on the sparse indices and values
int indices[] = extractIndices(sparseIndicesAccessorData);
for (int i = 0; i < indices.length; i++) {
int targetElementIndex = indices[i];
for (int c = 0; c < numComponentsPerElement; c++) {
float substitution = sparseValuesAccessorData.get(i, c);
denseAccessorData.set(targetElementIndex, c, substitution);
}
}
}
}

View File

@@ -0,0 +1,137 @@
package com.tacz.guns.client.animation.gltf.accessor;
import com.tacz.guns.client.animation.gltf.GltfConstants;
public class Accessors {
/**
* Private constructor to prevent instantiation
*/
private Accessors() {
// Private constructor to prevent instantiation
}
/**
* Returns the number of components that one element has for the given
* accessor type. Valid parameters are
* <pre><code>
* "SCALAR" : 1
* "VEC2" : 2
* "VEC3" : 3
* "VEC4" : 4
* "MAT2" : 4
* "MAT3" : 9
* "MAT4" : 16
* </code></pre>
*
* @param accessorType The accessor type.
* @return The number of components
* @throws IllegalArgumentException If the given type is none of the
* valid parameters
*/
public static int getNumComponentsForAccessorType(String accessorType) {
switch (accessorType) {
case "SCALAR":
return 1;
case "VEC2":
return 2;
case "VEC3":
return 3;
case "VEC4":
return 4;
case "MAT2":
return 4;
case "MAT3":
return 9;
case "MAT4":
return 16;
default:
break;
}
throw new IllegalArgumentException(
"Invalid accessor type: " + accessorType);
}
/**
* Returns the number of bytes that one component with the given
* accessor component type consists of.
* Valid parameters are
* <pre><code>
* GL_BYTE : 1
* GL_UNSIGNED_BYTE : 1
* GL_SHORT : 2
* GL_UNSIGNED_SHORT : 2
* GL_INT : 4
* GL_UNSIGNED_INT : 4
* GL_FLOAT : 4
* </code></pre>
*
* @param componentType The component type
* @return The number of bytes
* @throws IllegalArgumentException If the given type is none of the
* valid parameters
*/
public static int getNumBytesForAccessorComponentType(int componentType) {
switch (componentType) {
case GltfConstants.GL_BYTE:
return 1;
case GltfConstants.GL_UNSIGNED_BYTE:
return 1;
case GltfConstants.GL_SHORT:
return 2;
case GltfConstants.GL_UNSIGNED_SHORT:
return 2;
case GltfConstants.GL_INT:
return 4;
case GltfConstants.GL_UNSIGNED_INT:
return 4;
case GltfConstants.GL_FLOAT:
return 4;
default:
break;
}
throw new IllegalArgumentException(
"Invalid accessor component type: " + componentType);
}
/**
* Returns the data type for the given accessor component type.
* Valid parameters and their return values are
* <pre><code>
* GL_BYTE : byte.class
* GL_UNSIGNED_BYTE : byte.class
* GL_SHORT : short.class
* GL_UNSIGNED_SHORT : short.class
* GL_INT : int.class
* GL_UNSIGNED_INT : int.class
* GL_FLOAT : float.class
* </code></pre>
*
* @param componentType The component type
* @return The data type
* @throws IllegalArgumentException If the given type is none of the
* valid parameters
*/
public static Class<?> getDataTypeForAccessorComponentType(
int componentType) {
switch (componentType) {
case GltfConstants.GL_BYTE:
return byte.class;
case GltfConstants.GL_UNSIGNED_BYTE:
return byte.class;
case GltfConstants.GL_SHORT:
return short.class;
case GltfConstants.GL_UNSIGNED_SHORT:
return short.class;
case GltfConstants.GL_INT:
return int.class;
case GltfConstants.GL_UNSIGNED_INT:
return int.class;
case GltfConstants.GL_FLOAT:
return float.class;
default:
break;
}
throw new IllegalArgumentException(
"Invalid accessor component type: " + componentType);
}
}

View File

@@ -0,0 +1,81 @@
/*
* www.javagl.de - JglTF
*
* Copyright 2015-2016 Marco Hutter - http://www.javagl.de
*
* 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.
*
* 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.
*/
package com.tacz.guns.client.animation.gltf.accessor;
/**
* Methods to convert primitive arrays to arrays of Number objects
*/
class NumberArrays {
/**
* Private constructor to prevent instantiation
*/
private NumberArrays() {
// Private constructor to prevent instantiation
}
/**
* Convert the given array into a Number array
*
* @param array The array
* @return The result
*/
static Number[] asNumbers(int array[]) {
Number result[] = new Number[array.length];
for (int i = 0; i < array.length; i++) {
result[i] = array[i];
}
return result;
}
/**
* Convert the given array into a Number array
*
* @param array The array
* @return The result
*/
static Number[] asNumbers(long array[]) {
Number result[] = new Number[array.length];
for (int i = 0; i < array.length; i++) {
result[i] = array[i];
}
return result;
}
/**
* Convert the given array into a Number array
*
* @param array The array
* @return The result
*/
static Number[] asNumbers(float array[]) {
Number result[] = new Number[array.length];
for (int i = 0; i < array.length; i++) {
result[i] = array[i];
}
return result;
}
}

View File

@@ -0,0 +1,88 @@
package com.tacz.guns.client.animation.internal;
public final class GunAnimationConstant {
/**
* 空仓挂机
*/
public static final String STATIC_BOLT_CAUGHT_ANIMATION = "static_bolt_caught";
/**
* 默认持枪动作
*/
public static final String STATIC_IDLE_ANIMATION = "static_idle";
/**
* 射击
*/
public static final String SHOOT_ANIMATION = "shoot";
/**
* 空仓换弹
*/
public static final String RELOAD_EMPTY_ANIMATION = "reload_empty";
/**
* 装了扩容弹匣后的空仓换弹
*/
public static final String RELOAD_EMPTY_EXTENDED_ANIMATION = "reload_empty_extended";
/**
* 拉栓
*/
public static final String BOLT_ANIMATION = "bolt";
/**
* 战术换弹
*/
public static final String RELOAD_TACTICAL_ANIMATION = "reload_tactical";
/**
* 装了扩容弹匣后的战术换弹
*/
public static final String RELOAD_TACTICAL_EXTENDED_ANIMATION = "reload_tactical_extended";
/**
* 切枪动画,切入
*/
public static final String DRAW_ANIMATION = "draw";
/**
* 切枪动画,切出
*/
public static final String PUT_AWAY_ANIMATION = "put_away";
/**
* 检视动画,非空仓
*/
public static final String INSPECT_ANIMATION = "inspect";
/**
* 检视动画,空仓
*/
public static final String INSPECT_EMPTY_ANIMATION = "inspect_empty";
/**
* 静止时的动画
*/
public static final String IDLE_ANIMATION = "idle";
/**
* 跑步动画,起始部分
*/
public static final String RUN_START_ANIMATION = "run_start";
/**
* 跑步动画
*/
public static final String RUN_LOOP_ANIMATION = "run";
/**
* 跑步持枪动画
*/
public static final String RUN_HOLD_ANIMATION = "run_hold";
/**
* 跑步动画,结束部分
*/
public static final String RUN_END_ANIMATION = "run_end";
/**
* 向前行走动画
*/
public static final String WALK_FORWARD_ANIMATION = "walk_forward";
/**
* 侧向行走动画
*/
public static final String WALK_SIDEWAY_ANIMATION = "walk_sideway";
/**
* 后退行走动画
*/
public static final String WALK_BACKWARD_ANIMATION = "walk_backward";
/**
* 行走时瞄准动画
*/
public static final String WALK_AIMING_ANIMATION = "walk_aiming";
}

View File

@@ -0,0 +1,413 @@
package com.tacz.guns.client.animation.internal;
import com.tacz.guns.client.animation.AnimationController;
import com.tacz.guns.client.animation.AnimationPlan;
import com.tacz.guns.client.animation.ObjectAnimation;
import com.tacz.guns.client.animation.ObjectAnimationRunner;
import it.unimi.dsi.fastutil.ints.IntLinkedOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import net.minecraft.client.player.Input;
import net.minecraft.world.entity.Entity;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import javax.annotation.Nonnull;
import java.util.ArrayDeque;
import static com.tacz.guns.client.animation.internal.GunAnimationConstant.*;
@OnlyIn(Dist.CLIENT)
public class GunAnimationStateMachine {
/**
* 下面两个变量需要放置在类的最顶端,
* IDEA 如果格式化修改了这两个变量位置,会导致游戏内动画异常
*/
protected static final IntSet BLENDING_TRACKS = new IntLinkedOpenHashSet();
protected static int TRACK_INDEX_TOP = 0;
/**
* 射击轨道 12 个,支持 0.5 秒的射击动画在 rpm 1200 以内播放
*/
public static final int[] SHOOTING_TRACKS = {
blendingTrack(), blendingTrack(), blendingTrack(), blendingTrack(),
blendingTrack(), blendingTrack(), blendingTrack(), blendingTrack(),
blendingTrack(), blendingTrack(), blendingTrack(), blendingTrack()
};
public static final int MOVEMENT_TRACK = blendingTrack();
public static final int MAIN_TRACK = staticTrack();
public static final int BOLT_CATCH_STATIC_TRACK = staticTrack();
public static final int HOLDING_POSE_STATIC_TRACK = staticTrack();
protected AnimationController controller;
protected boolean noAmmo = false;
protected boolean magExtended = false;
protected boolean onGround = true;
protected boolean pauseWalkAndRun = false;
protected boolean isAiming = false;
/**
* 记录开始冲刺时玩家的 walk distance以便让冲刺动画有统一的开头
*/
protected float baseDistanceWalked = 0.0f;
protected float keepDistanceWalked = 0.0f;
protected WalkDirection lastWalkDirection = WalkDirection.NONE;
protected boolean isWalkAiming = false;
public GunAnimationStateMachine(AnimationController controller) {
this.controller = controller;
for (int i = 0; i < TRACK_INDEX_TOP; i++) {
controller.setBlending(i, BLENDING_TRACKS.contains(i));
}
}
protected static int staticTrack() {
return TRACK_INDEX_TOP++;
}
protected static int blendingTrack() {
int track = TRACK_INDEX_TOP++;
BLENDING_TRACKS.add(track);
return track;
}
public void onGunShoot() {
// 开火动画应当打断检视动画
if (isPlayingInspectAnimation()) {
controller.removeAnimation(MAIN_TRACK);
}
for (int track : SHOOTING_TRACKS) {
if (tryRunShootAnimation(track)) {
return;
}
}
controller.runAnimation(SHOOTING_TRACKS[0], SHOOT_ANIMATION, ObjectAnimation.PlayType.PLAY_ONCE_STOP, 0f);
}
public void onGunReload() {
if (noAmmo) {
if (magExtended && controller.containPrototype(RELOAD_EMPTY_EXTENDED_ANIMATION)) {
controller.runAnimation(MAIN_TRACK, RELOAD_EMPTY_EXTENDED_ANIMATION, ObjectAnimation.PlayType.PLAY_ONCE_STOP, 0.2f);
} else {
controller.runAnimation(MAIN_TRACK, RELOAD_EMPTY_ANIMATION, ObjectAnimation.PlayType.PLAY_ONCE_STOP, 0.2f);
}
} else {
if (magExtended && controller.containPrototype(RELOAD_TACTICAL_EXTENDED_ANIMATION)) {
controller.runAnimation(MAIN_TRACK, RELOAD_TACTICAL_EXTENDED_ANIMATION, ObjectAnimation.PlayType.PLAY_ONCE_STOP, 0.2f);
} else {
controller.runAnimation(MAIN_TRACK, RELOAD_TACTICAL_ANIMATION, ObjectAnimation.PlayType.PLAY_ONCE_STOP, 0.2f);
}
}
}
public void onGunBolt() {
controller.runAnimation(MAIN_TRACK, BOLT_ANIMATION, ObjectAnimation.PlayType.PLAY_ONCE_STOP, 0.2f);
}
public void onGunDraw() {
controller.runAnimation(MOVEMENT_TRACK, IDLE_ANIMATION, ObjectAnimation.PlayType.LOOP, 0);
lastWalkDirection = WalkDirection.NONE;
controller.runAnimation(MAIN_TRACK, DRAW_ANIMATION, ObjectAnimation.PlayType.PLAY_ONCE_STOP, 0);
}
public void onShooterRun(float walkDist) {
if (isPlayingRunIntroOrLoop()) {
if (!onGround && !isPlayingRunHold()) {
controller.runAnimation(MOVEMENT_TRACK, RUN_HOLD_ANIMATION, ObjectAnimation.PlayType.LOOP, 0.6f);
isWalkAiming = false;
lastWalkDirection = WalkDirection.NONE;
}
return;
}
if (onGround) {
ArrayDeque<AnimationPlan> deque = new ArrayDeque<>();
if (!isPlayingRunHold()) {
deque.add(new AnimationPlan(RUN_START_ANIMATION, ObjectAnimation.PlayType.PLAY_ONCE_HOLD, 0.2f));
}
deque.add(new AnimationPlan(RUN_LOOP_ANIMATION, ObjectAnimation.PlayType.LOOP, 0.2f));
controller.queueAnimation(MOVEMENT_TRACK, deque);
isWalkAiming = false;
lastWalkDirection = WalkDirection.NONE;
baseDistanceWalked = walkDist;
}
}
public void onGunPutAway(float putAwayTimeS) {
controller.runAnimation(MAIN_TRACK, PUT_AWAY_ANIMATION, ObjectAnimation.PlayType.PLAY_ONCE_HOLD, putAwayTimeS * 0.75f);
// 改变 put away 动画的进度,如果刚刚切枪不久,则收枪应当更快。
ObjectAnimationRunner runner = controller.getAnimation(MAIN_TRACK);
if (runner != null) {
if (runner.isRunning() && PUT_AWAY_ANIMATION.equals(runner.getAnimation().name)) {
long progress = (long) (Math.max(runner.getAnimation().getMaxEndTimeS() - putAwayTimeS, 0) * 1e9);
runner.setProgressNs(progress);
return;
}
if (runner.getTransitionTo() != null && PUT_AWAY_ANIMATION.equals(runner.getTransitionTo().getAnimation().name)) {
long progress = (long) (Math.max(runner.getTransitionTo().getAnimation().getMaxEndTimeS() - putAwayTimeS, 0) * 1e9);
runner.getTransitionTo().setProgressNs(progress);
}
}
}
public void onShooterIdle() {
if (isPlayingIdleAnimation()) {
return;
}
ObjectAnimationRunner runner = controller.getAnimation(MOVEMENT_TRACK);
if (runner != null && (runner.isRunning() || runner.isTransitioning())) {
if (!isPlayingWalkAnimation() && !isPlayingRunIntroOrLoop() && !isPlayingRunHold()) {
return;
}
}
isWalkAiming = false;
lastWalkDirection = WalkDirection.NONE;
ArrayDeque<AnimationPlan> deque = new ArrayDeque<>();
if (isPlayingRunIntroOrLoop()) {
deque.add(new AnimationPlan(RUN_END_ANIMATION, ObjectAnimation.PlayType.PLAY_ONCE_HOLD, 0.3f));
}
deque.add(new AnimationPlan(IDLE_ANIMATION, ObjectAnimation.PlayType.LOOP, 0.4f));
controller.queueAnimation(MOVEMENT_TRACK, deque);
}
public void onGunInspect() {
if (noAmmo) {
controller.runAnimation(MAIN_TRACK, INSPECT_EMPTY_ANIMATION, ObjectAnimation.PlayType.PLAY_ONCE_STOP, 0.2f);
} else {
controller.runAnimation(MAIN_TRACK, INSPECT_ANIMATION, ObjectAnimation.PlayType.PLAY_ONCE_STOP, 0.2f);
}
}
public void onShooterWalk(Input input, float walkDist) {
if (!onGround && !isPlayingIdleAnimation()) {
controller.runAnimation(MOVEMENT_TRACK, IDLE_ANIMATION, ObjectAnimation.PlayType.LOOP, 0.6f);
isWalkAiming = false;
lastWalkDirection = WalkDirection.NONE;
return;
}
if (onGround) {
// 如果一边走路一边瞄准,则需要播放特定的动画 WALK_AIMING_ANIMATION。
if (isAiming) {
if (isWalkAiming) {
return;
}
isWalkAiming = true;
lastWalkDirection = WalkDirection.NONE;
ArrayDeque<AnimationPlan> deque = new ArrayDeque<>();
if (isPlayingRunIntroOrLoop() || isPlayingRunHold()) {
deque.add(new AnimationPlan(RUN_END_ANIMATION, ObjectAnimation.PlayType.PLAY_ONCE_HOLD, 0.3f));
}
deque.add(new AnimationPlan(WALK_AIMING_ANIMATION, ObjectAnimation.PlayType.LOOP, 0.3f));
controller.queueAnimation(MOVEMENT_TRACK, deque);
baseDistanceWalked = walkDist;
return;
}
WalkDirection direction = WalkDirection.fromInput(input);
// 同一个方向的动画播放只需要触发一次。
if (direction == lastWalkDirection) {
return;
}
isWalkAiming = false;
lastWalkDirection = direction;
ArrayDeque<AnimationPlan> deque = new ArrayDeque<>();
if (isPlayingRunIntroOrLoop() || isPlayingRunHold()) {
deque.add(new AnimationPlan(RUN_END_ANIMATION, ObjectAnimation.PlayType.PLAY_ONCE_HOLD, 0.3f));
}
switch (direction) {
case FORWARD ->
deque.add(new AnimationPlan(WALK_FORWARD_ANIMATION, ObjectAnimation.PlayType.LOOP, 0.4f));
case BACKWARD ->
deque.add(new AnimationPlan(WALK_BACKWARD_ANIMATION, ObjectAnimation.PlayType.LOOP, 0.4f));
case SIDE_WAY ->
deque.add(new AnimationPlan(WALK_SIDEWAY_ANIMATION, ObjectAnimation.PlayType.LOOP, 0.4f));
}
controller.queueAnimation(MOVEMENT_TRACK, deque);
baseDistanceWalked = walkDist;
}
}
public void onGunFireSelect() {
// TODO切换开火方式的动画
}
public void onGunCatchBolt() {
if (!isPlayingAnimation(BOLT_CATCH_STATIC_TRACK, STATIC_BOLT_CAUGHT_ANIMATION)) {
controller.runAnimation(BOLT_CATCH_STATIC_TRACK, STATIC_BOLT_CAUGHT_ANIMATION, ObjectAnimation.PlayType.LOOP, 0);
}
}
public GunAnimationStateMachine setNoAmmo(boolean noAmmo) {
this.noAmmo = noAmmo;
return this;
}
public GunAnimationStateMachine setMagExtended(boolean magExtended) {
this.magExtended = magExtended;
return this;
}
public GunAnimationStateMachine setOnGround(boolean onGround) {
this.onGround = onGround;
return this;
}
public GunAnimationStateMachine setPauseWalkAndRun(boolean pause) {
this.pauseWalkAndRun = pause;
return this;
}
public GunAnimationStateMachine setAiming(boolean isAiming) {
this.isAiming = isAiming;
return this;
}
public AnimationController getController() {
return controller;
}
/**
* @return 返回当前正在播放的动画是否需要隐藏准心。
*/
public boolean shouldHideCrossHair() {
if (isPlayingInspectAnimation()) {
return true;
}
return isPlayingRunHold() || isPlayingRunLoop();
}
public void update(float partialTicks, Entity entity) {
ObjectAnimationRunner runner = controller.getAnimation(MOVEMENT_TRACK);
if (runner != null) {
// 为了让冲刺和行走动画和原版的 viewBobbing 相适应,需要手动更新冲刺动画的进度
// 当前动画是run或者正在过渡向 run 动画的时候,就手动设置 run 动画的进度。
float deltaDistanceWalked = entity.walkDist - entity.walkDistO;
float distanceWalked;
if (pauseWalkAndRun) {
// 保持 distanceWalked 与 keepDistanceWalked 相同,即不随时间增长
distanceWalked = keepDistanceWalked;
baseDistanceWalked = entity.walkDist + deltaDistanceWalked * partialTicks - keepDistanceWalked;
} else {
// distanceWalked 与 keepDistanceWalked 一同随时间增长
distanceWalked = entity.walkDist + deltaDistanceWalked * partialTicks - baseDistanceWalked;
keepDistanceWalked = distanceWalked;
}
String animationName = runner.getAnimation().name;
if ((isNamedWalkAnimation(animationName) || RUN_LOOP_ANIMATION.equals(animationName)) && runner.isRunning()) {
runner.setProgressNs((long) (runner.getAnimation().getMaxEndTimeS() * (distanceWalked % 2f) / 2f * 1e9f));
}
if (runner.isTransitioning() && runner.getTransitionTo() != null) {
animationName = runner.getTransitionTo().getAnimation().name;
if (isNamedWalkAnimation(animationName) || RUN_LOOP_ANIMATION.equals(animationName)) {
runner.getTransitionTo().setProgressNs((long) (runner.getTransitionTo().getAnimation().getMaxEndTimeS() * (distanceWalked % 2f) / 2f * 1e9f));
}
}
}
controller.update();
}
public boolean isPlayingAnimation(int track, @Nonnull String... names) {
ObjectAnimationRunner runner = controller.getAnimation(track);
if (runner != null) {
String animationName = runner.getAnimation().name;
if (runner.isRunning()) {
for (String name : names) {
if (name.equals(animationName)) {
return true;
}
}
}
if (runner.isTransitioning() && runner.getTransitionTo() != null) {
animationName = runner.getTransitionTo().getAnimation().name;
for (String name : names) {
if (name.equals(animationName)) {
return true;
}
}
}
}
return false;
}
public boolean isPlayingRunAnimation() {
return isPlayingAnimation(MOVEMENT_TRACK, RUN_START_ANIMATION, RUN_LOOP_ANIMATION, RUN_HOLD_ANIMATION, RUN_END_ANIMATION);
}
public boolean isPlayingRunIntroOrLoop() {
return isPlayingAnimation(MOVEMENT_TRACK, RUN_LOOP_ANIMATION, RUN_START_ANIMATION);
}
public boolean isPlayingRunStart() {
return isPlayingAnimation(MOVEMENT_TRACK, RUN_START_ANIMATION);
}
public boolean isPlayingRunLoop() {
return isPlayingAnimation(MOVEMENT_TRACK, RUN_LOOP_ANIMATION);
}
public boolean isPlayingRunHold() {
return isPlayingAnimation(MOVEMENT_TRACK, RUN_HOLD_ANIMATION);
}
public boolean isPlayingRunEnd() {
return isPlayingAnimation(MOVEMENT_TRACK, RUN_END_ANIMATION);
}
public boolean isPlayingWalkAnimation() {
return isPlayingAnimation(MOVEMENT_TRACK, WALK_FORWARD_ANIMATION, WALK_BACKWARD_ANIMATION, WALK_SIDEWAY_ANIMATION, WALK_AIMING_ANIMATION);
}
public boolean isPlayingIdleAnimation() {
return isPlayingAnimation(MOVEMENT_TRACK, IDLE_ANIMATION);
}
public boolean isPlayingShootAnimation() {
for (int track : SHOOTING_TRACKS) {
ObjectAnimationRunner runner = controller.getAnimation(track);
if (runner != null) {
if (runner.isRunning()) {
return true;
}
if (runner.isTransitioning() && runner.getTransitionTo() != null) {
return true;
}
}
}
return false;
}
public boolean isPlayingInspectAnimation() {
return isPlayingAnimation(MAIN_TRACK, INSPECT_ANIMATION, INSPECT_EMPTY_ANIMATION);
}
public boolean isPlayingReloadAnimation() {
return isPlayingAnimation(MAIN_TRACK, RELOAD_EMPTY_ANIMATION, RELOAD_TACTICAL_ANIMATION);
}
public boolean isPlayingDrawAnimation() {
return isPlayingAnimation(MAIN_TRACK, DRAW_ANIMATION);
}
public void onGunReleaseBolt() {
controller.removeAnimation(BOLT_CATCH_STATIC_TRACK);
}
public void onIdleHoldingPose() {
if (!isPlayingAnimation(HOLDING_POSE_STATIC_TRACK, STATIC_IDLE_ANIMATION)) {
controller.runAnimation(HOLDING_POSE_STATIC_TRACK, STATIC_IDLE_ANIMATION, ObjectAnimation.PlayType.LOOP, 0);
}
}
private boolean isNamedWalkAnimation(String animationName) {
return WALK_SIDEWAY_ANIMATION.equals(animationName) || WALK_FORWARD_ANIMATION.equals(animationName) || WALK_BACKWARD_ANIMATION.equals(animationName)
|| WALK_AIMING_ANIMATION.equals(animationName);
}
private boolean tryRunShootAnimation(int track) {
ObjectAnimationRunner runner = controller.getAnimation(track);
if (runner != null && runner.isRunning() && SHOOT_ANIMATION.equals(runner.getAnimation().name)) {
return false;
}
if (runner != null && runner.getTransitionTo() != null && SHOOT_ANIMATION.equals(runner.getTransitionTo().getAnimation().name)) {
return false;
}
controller.runAnimation(track, SHOOT_ANIMATION, ObjectAnimation.PlayType.PLAY_ONCE_HOLD, 0);
return true;
}
}

View File

@@ -0,0 +1,23 @@
package com.tacz.guns.client.animation.internal;
import net.minecraft.client.player.Input;
public enum WalkDirection {
FORWARD,
SIDE_WAY,
BACKWARD,
NONE;
public static WalkDirection fromInput(Input input) {
if (input.up) {
return FORWARD;
}
if (input.down) {
return BACKWARD;
}
if (input.left || input.right) {
return SIDE_WAY;
}
return NONE;
}
}

View File

@@ -0,0 +1,160 @@
package com.tacz.guns.client.animation.interpolator;
import com.tacz.guns.client.animation.AnimationChannelContent;
import com.tacz.guns.client.animation.AnimationChannelContent.LerpMode;
import com.tacz.guns.util.math.MathUtil;
public class CustomInterpolator implements Interpolator {
private AnimationChannelContent content;
@Override
public void compile(AnimationChannelContent content) {
this.content = content;
}
@Override
public void interpolate(int indexFrom, int indexTo, float alpha, float[] result) {
LerpMode fromLerpMode = content.lerpModes[indexFrom];
LerpMode toLerpMode = content.lerpModes[indexTo];
if (fromLerpMode == LerpMode.SPHERICAL_LINEAR && toLerpMode == LerpMode.SPHERICAL_LINEAR) {
// 球面线性插值
this.doSphericalLinearLerp(indexFrom, indexTo, alpha, result);
}
if (fromLerpMode == LerpMode.SPHERICAL_CATMULLROM || toLerpMode == LerpMode.SPHERICAL_CATMULLROM) {
// 球面 Catmull-Rom 插值
this.doSphericalCatmullRomLerp(indexFrom, indexTo, alpha, result);
} else if (fromLerpMode == LerpMode.CATMULLROM || toLerpMode == LerpMode.CATMULLROM) {
// Catmull-Rom 插值
this.doCatmullromLerp(indexFrom, indexTo, alpha, result);
} else {
// 其他情况的插值计算
this.doOtherLerp(indexFrom, indexTo, alpha, result);
}
}
private void doOtherLerp(int indexFrom, int indexTo, float alpha, float[] result) {
// 如果动画值有 6 个,后三个为 Post 数值,用于插值起点
int offset = content.values[indexFrom].length == 6 ? 3 : 0;
for (int i = 0; i < 3; i++) {
if (indexFrom == indexTo) {
result[i] = content.values[indexFrom][i + offset];
} else {
result[i] = content.values[indexFrom][i + offset] * (1 - alpha) + content.values[indexTo][i] * alpha;
}
}
}
private void doCatmullromLerp(int indexFrom, int indexTo, float alpha, float[] result) {
if (content.values.length == 1) {
result[0] = content.values[0][0];
result[1] = content.values[0][1];
result[2] = content.values[0][2];
return;
}
float[] vx = new float[4];
float[] vy = new float[4];
float[] vz = new float[4];
int prev = indexFrom == 0 ? 0 : indexFrom - 1;
int next = indexTo == (content.values.length - 1) ? (content.values.length - 1) : indexTo + 1;
int prevOffset = content.values[prev].length == 6 ? 3 : 0;
vx[0] = content.values[prev][prevOffset];
vy[0] = content.values[prev][1 + prevOffset];
vz[0] = content.values[prev][2 + prevOffset];
vx[1] = content.values[indexFrom][0];
vy[1] = content.values[indexFrom][1];
vz[1] = content.values[indexFrom][2];
vx[2] = content.values[indexTo][0];
vy[2] = content.values[indexTo][1];
vz[2] = content.values[indexTo][2];
vx[3] = content.values[next][0];
vy[3] = content.values[next][1];
vz[3] = content.values[next][2];
// 这里用的是三次样条插值,主要是为了和 BlockBench 中的表现贴合。
// BlockBench 中调用的是 THREE.SplineCurve其实现是三次样条插值。如果用 Catmull-Rom 插值,区别会比较大。
result[0] = MathUtil.splineCurve(vx, 0.5f, alpha);
result[1] = MathUtil.splineCurve(vy, 0.5f, alpha);
result[2] = MathUtil.splineCurve(vz, 0.5f, alpha);
}
private void doSphericalLinearLerp(int indexFrom, int indexTo, float alpha, float[] result) {
if (content.values.length == 1) {
result[0] = content.values[0][0];
result[1] = content.values[0][1];
result[2] = content.values[0][2];
result[3] = content.values[0][3];
return;
}
// 如果旋转值有 8 个,后四个为 Post 数值,用于插值起点
int offset = content.values[indexFrom].length == 8 ? 4 : 0;
float ax = content.values[indexFrom][offset];
float ay = content.values[indexFrom][1 + offset];
float az = content.values[indexFrom][2 + offset];
float aw = content.values[indexFrom][3 + offset];
float bx = content.values[indexTo][0];
float by = content.values[indexTo][1];
float bz = content.values[indexTo][2];
float bw = content.values[indexTo][3];
float dot = ax * bx + ay * by + az * bz + aw * bw;
if (dot < 0) {
bx = -bx;
by = -by;
bz = -bz;
bw = -bw;
dot = -dot;
}
float epsilon = 1e-6f;
float s0, s1;
if ((1.0 - dot) > epsilon) {
float omega = (float) Math.acos(dot);
float invSinOmega = 1.0f / (float) Math.sin(omega);
s0 = (float) Math.sin((1.0 - alpha) * omega) * invSinOmega;
s1 = (float) Math.sin(alpha * omega) * invSinOmega;
} else {
s0 = 1.0f - alpha;
s1 = alpha;
}
float rx = s0 * ax + s1 * bx;
float ry = s0 * ay + s1 * by;
float rz = s0 * az + s1 * bz;
float rw = s0 * aw + s1 * bw;
result[0] = rx;
result[1] = ry;
result[2] = rz;
result[3] = rw;
}
private void doSphericalCatmullRomLerp(int indexFrom, int indexTo, float alpha, float[] result) {
if (content.values.length == 1) {
result[0] = content.values[0][0];
result[1] = content.values[0][1];
result[2] = content.values[0][2];
result[3] = content.values[0][3];
return;
}
int prev = indexFrom == 0 ? 0 : indexFrom - 1;
int next = indexTo == (content.values.length - 1) ? (content.values.length - 1) : indexTo + 1;
int prevOffset = content.values[prev].length == 8 ? 4 : 0;
float[] prevValue = content.values[prev];
float[] q0 = new float[]{prevValue[prevOffset], prevValue[1 + prevOffset], prevValue[2 + prevOffset], prevValue[3 + prevOffset]};
// 这里用的是三次样条插值,主要是为了和 BlockBench 中的表现贴合。
// BlockBench 中调用的是 THREE.SplineCurve其实现是三次样条插值。如果用 Catmull-Rom 插值,区别会比较大。
float[] r = MathUtil.quaternionSplineCurve(new float[][]{q0, content.values[indexFrom], content.values[indexTo], content.values[next]}, 0.5f, alpha);
result[0] = r[0];
result[1] = r[1];
result[2] = r[2];
result[3] = r[3];
}
@Override
public CustomInterpolator clone() {
try {
CustomInterpolator interpolator = (CustomInterpolator) super.clone();
interpolator.content = this.content;
return interpolator;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,11 @@
package com.tacz.guns.client.animation.interpolator;
import com.tacz.guns.client.animation.AnimationChannelContent;
public interface Interpolator extends Cloneable {
void compile(AnimationChannelContent content);
void interpolate(int indexFrom, int indexTo, float alpha, float[] result);
Interpolator clone();
}

View File

@@ -0,0 +1,27 @@
package com.tacz.guns.client.animation.interpolator;
public class InterpolatorUtil {
public static Interpolator fromInterpolation(InterpolatorType interpolation) {
switch (interpolation) {
case SPLINE -> {
return new Spline();
}
case STEP -> {
return new Step();
}
case SLERP -> {
return new SLerp();
}
default -> {
return new Linear();
}
}
}
public enum InterpolatorType {
LINEAR,
SLERP,
SPLINE,
STEP
}
}

View File

@@ -0,0 +1,37 @@
package com.tacz.guns.client.animation.interpolator;
import com.tacz.guns.client.animation.AnimationChannelContent;
public class Linear implements Interpolator {
private AnimationChannelContent content;
@Override
public void compile(AnimationChannelContent content) {
this.content = content;
}
@Override
public void interpolate(int indexFrom, int indexTo, float alpha, float[] result) {
// 如果动画值有 6 个,后三个为 Post 数值,用于插值起点
int offset = content.values[indexFrom].length == 6 ? 3 : 0;
for (int i = 0; i < result.length; i++) {
if (indexFrom == indexTo) {
result[i] = content.values[indexFrom][i + offset];
} else {
result[i] = content.values[indexFrom][i + offset] * (1 - alpha) + content.values[indexTo][i] * alpha;
}
}
}
@Override
public Linear clone() {
try {
Linear linear = (Linear) super.clone();
linear.content = this.content;
return linear;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,69 @@
package com.tacz.guns.client.animation.interpolator;
import com.tacz.guns.client.animation.AnimationChannelContent;
/**
* 用于四元数的线性插值。
*/
public class SLerp implements Interpolator {
private AnimationChannelContent content;
@Override
public void compile(AnimationChannelContent content) {
this.content = content;
}
@Override
public void interpolate(int indexFrom, int indexTo, float alpha, float[] result) {
// 如果旋转值有 8 个,后四个为 Post 数值,用于插值起点
int offset = content.values[indexFrom].length == 8 ? 4 : 0;
float ax = content.values[indexFrom][offset];
float ay = content.values[indexFrom][1 + offset];
float az = content.values[indexFrom][2 + offset];
float aw = content.values[indexFrom][3 + offset];
float bx = indexFrom == indexTo ? content.values[indexFrom][offset] : content.values[indexTo][0];
float by = indexFrom == indexTo ? content.values[indexFrom][1 + offset] : content.values[indexTo][1];
float bz = indexFrom == indexTo ? content.values[indexFrom][2 + offset] : content.values[indexTo][2];
float bw = indexFrom == indexTo ? content.values[indexFrom][3 + offset] : content.values[indexTo][3];
float dot = ax * bx + ay * by + az * bz + aw * bw;
if (dot < 0) {
bx = -bx;
by = -by;
bz = -bz;
bw = -bw;
dot = -dot;
}
float epsilon = 1e-6f;
float s0, s1;
if ((1.0 - dot) > epsilon) {
float omega = (float) Math.acos(dot);
float invSinOmega = 1.0f / (float) Math.sin(omega);
s0 = (float) Math.sin((1.0 - alpha) * omega) * invSinOmega;
s1 = (float) Math.sin(alpha * omega) * invSinOmega;
} else {
s0 = 1.0f - alpha;
s1 = alpha;
}
float rx = s0 * ax + s1 * bx;
float ry = s0 * ay + s1 * by;
float rz = s0 * az + s1 * bz;
float rw = s0 * aw + s1 * bw;
result[0] = rx;
result[1] = ry;
result[2] = rz;
result[3] = rw;
}
@Override
public SLerp clone() {
try {
SLerp sLerp = (SLerp) super.clone();
sLerp.content = this.content;
return sLerp;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,21 @@
package com.tacz.guns.client.animation.interpolator;
import com.tacz.guns.client.animation.AnimationChannelContent;
public class Spline implements Interpolator {
@Override
public void compile(AnimationChannelContent content) {
// TODO
}
@Override
public void interpolate(int indexFrom, int indexTo, float alpha, float[] result) {
// TODO
}
@Override
public Interpolator clone() {
// TODO
return null;
}
}

View File

@@ -0,0 +1,37 @@
package com.tacz.guns.client.animation.interpolator;
import com.tacz.guns.client.animation.AnimationChannelContent;
public class Step implements Interpolator {
private AnimationChannelContent content;
@Override
public void compile(AnimationChannelContent content) {
this.content = content;
}
@Override
public void interpolate(int indexFrom, int indexTo, float alpha, float[] result) {
// 如果动画值有 6 个,后三个为 Post 数值,用于插值起点
int offset = content.values[indexFrom].length == 6 ? 3 : 0;
for (int i = 0; i < result.length; i++) {
if (alpha < 1 || indexFrom == indexTo) {
result[i] = content.values[indexFrom][i + offset];
} else {
result[i] = content.values[indexTo][i];
}
}
}
@Override
public Step clone() {
try {
Step step = (Step) super.clone();
step.content = this.content;
return step;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,60 @@
package com.tacz.guns.client.download;
import com.tacz.guns.GunMod;
import com.tacz.guns.config.sync.SyncConfig;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
public final class ClientGunPackDownloadManager {
private static final Path DOWNLOAD_DIR_PATH = Paths.get("config", GunMod.MOD_ID, "server", "download");
private static final Pattern SHA1 = Pattern.compile("^[a-fA-F0-9]{40}$");
private static final ClientGunPackDownloader DOWNLOADER = new ClientGunPackDownloader(DOWNLOAD_DIR_PATH);
public static void init() {
createFolder();
}
public static void downloadClientGunPack() {
List<List<String>> download = SyncConfig.CLIENT_GUN_PACK_DOWNLOAD_URLS.get();
download.forEach(data -> {
String url = data.get(0);
String sha1 = data.get(1);
if (StringUtils.isBlank(url)) {
return;
}
if (!SHA1.matcher(sha1).matches()) {
return;
}
download(url, sha1);
});
}
public static void download(String url, String hash) {
// 统一使用小写
String lowerCaseHash = hash.toLowerCase(Locale.US);
DOWNLOADER.downloadAndLoadGunPack(url, lowerCaseHash).thenRun(() -> {
// 我成功下载资源并加载了
}).exceptionally(throwable -> {
// 失败了
return null;
});
}
private static void createFolder() {
File folder = DOWNLOAD_DIR_PATH.toFile();
if (!folder.isDirectory()) {
try {
Files.createDirectories(folder.toPath());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

View File

@@ -0,0 +1,175 @@
package com.tacz.guns.client.download;
import com.google.common.collect.Maps;
import com.tacz.guns.GunMod;
import com.tacz.guns.client.gui.ClientGunPackDownloadProgressScreen;
import com.tacz.guns.client.resource.ClientReloadManager;
import net.minecraft.SharedConstants;
import net.minecraft.Util;
import net.minecraft.WorldVersion;
import net.minecraft.client.Minecraft;
import net.minecraft.client.User;
import net.minecraft.client.gui.screens.ConfirmScreen;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.network.chat.CommonComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.TranslatableComponent;
import net.minecraft.util.HttpUtil;
import net.minecraftforge.fml.ModList;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import javax.annotation.Nullable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.locks.ReentrantLock;
public class ClientGunPackDownloader {
/**
* 最大允许文件大小 250 M
*/
private static final int MAX_FILE_SIZE = 250 * 1024 * 1024;
private final ReentrantLock downloadLock = new ReentrantLock();
private final Path serverGunPackPath;
private @Nullable CompletableFuture<?> currentDownload;
public ClientGunPackDownloader(Path serverGunPackPath) {
this.serverGunPackPath = serverGunPackPath;
}
private static Map<String, String> getDownloadHeaders() {
Map<String, String> map = Maps.newHashMap();
User user = Minecraft.getInstance().getUser();
WorldVersion currentVersion = SharedConstants.getCurrentVersion();
map.put("X-Minecraft-Username", user.getName());
map.put("X-Minecraft-UUID", user.getUuid());
map.put("X-Minecraft-Version", currentVersion.getName());
map.put("X-Minecraft-Version-ID", currentVersion.getId());
map.put("X-TACZ-Version", ModList.get().getModFileById(GunMod.MOD_ID).versionString());
map.put("User-Agent", "Minecraft Java/" + currentVersion.getName());
return map;
}
public CompletableFuture<?> downloadAndLoadGunPack(String url, String hash) {
// 加锁
this.downloadLock.lock();
// 最终返回的结果
CompletableFuture<?> resultFuture;
try {
// 检查并清除之前未完成,或者可能失败的下载线程
this.clearDownloadingGunPack();
// 将 hash 作为下载资源包的名称
File gunPack = serverGunPackPath.resolve(hash).toFile();
// 检查缓存的文件对不对,不对进行删除
this.removeMismatchFile(hash, gunPack);
// 下载线程
CompletableFuture<?> downloadFuture;
// 如果此资源包存在,那么直接加载即可
if (gunPack.exists()) {
downloadFuture = CompletableFuture.completedFuture("");
}
// 否则下载,并打开下载界面
else {
// 下载进度界面
ClientGunPackDownloadProgressScreen progressScreen = new ClientGunPackDownloadProgressScreen();
Minecraft minecraft = Minecraft.getInstance();
minecraft.executeBlocking(() -> minecraft.setScreen(progressScreen));
downloadFuture = HttpUtil.downloadTo(gunPack, url, getDownloadHeaders(), MAX_FILE_SIZE, progressScreen, minecraft.getProxy());
}
// 下载完成后的处理
this.currentDownload = downloadFuture.thenCompose(target -> {
// 文件 hash 不匹配,抛出错误
if (this.notMatchHash(hash, gunPack)) {
return Util.failedFuture(new RuntimeException("Hash check failure for file " + gunPack + ", see log"));
} else {
// 否则,加载枪械包客户端部分
return this.loadClientGunPack(gunPack);
}
}).whenComplete((target, throwable) -> this.afterFail(throwable, gunPack));
resultFuture = this.currentDownload;
} finally {
this.downloadLock.unlock();
}
return resultFuture;
}
private void afterFail(Throwable throwable, File gunPack) {
if (throwable == null) {
return;
}
GunMod.LOGGER.warn("Pack application failed: {}, deleting file {}", throwable.getMessage(), gunPack);
try {
Files.delete(gunPack.toPath());
} catch (IOException exception) {
GunMod.LOGGER.warn("Failed to delete file {}: {}", gunPack, exception.getMessage());
}
Minecraft.getInstance().execute(() -> this.displayFailScreen(Minecraft.getInstance()));
}
private void displayFailScreen(Minecraft mc) {
TranslatableComponent title = new TranslatableComponent("gui.tacz.client_gun_pack_downloader.fail.title");
TranslatableComponent subTitle = new TranslatableComponent("gui.tacz.client_gun_pack_downloader.fail.subtitle");
Component yesButton = CommonComponents.GUI_PROCEED;
TranslatableComponent noButton = new TranslatableComponent("menu.disconnect");
mc.setScreen(new ConfirmScreen(button -> {
if (button) {
mc.setScreen(null);
} else {
ClientPacketListener clientpacketlistener = mc.getConnection();
if (clientpacketlistener != null) {
clientpacketlistener.getConnection().disconnect(new TranslatableComponent("connect.aborted"));
}
}
}, title, subTitle, yesButton, noButton));
}
public void clearDownloadingGunPack() {
this.downloadLock.lock();
try {
if (this.currentDownload != null) {
this.currentDownload.cancel(true);
}
this.currentDownload = null;
} finally {
this.downloadLock.unlock();
}
}
public void removeMismatchFile(String expectedHash, File file) {
if (file.exists() && notMatchHash(expectedHash, file)) {
try {
FileUtils.delete(file);
} catch (IOException exception) {
GunMod.LOGGER.warn("Failed to delete file {}: {}", file, exception.getMessage());
}
}
}
private boolean notMatchHash(String expectedHash, File file) {
try (FileInputStream stream = new FileInputStream(file)) {
String fileHash = DigestUtils.sha1Hex(stream);
if (fileHash.toLowerCase(Locale.US).equals(expectedHash.toLowerCase(Locale.US))) {
GunMod.LOGGER.info("Found file {} matching requested fileHash {}", file, expectedHash);
return false;
}
GunMod.LOGGER.warn("File {} had wrong fileHash (expected {}, found {}).", file, expectedHash, fileHash);
} catch (IOException ioexception) {
GunMod.LOGGER.warn("File {} couldn't be hashed.", file, ioexception);
}
return true;
}
public CompletableFuture<?> loadClientGunPack(File file) {
ClientReloadManager.loadClientDownloadGunPack(file);
return CompletableFuture.completedFuture("");
}
}

View File

@@ -0,0 +1,205 @@
package com.tacz.guns.client.event;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Quaternion;
import com.tacz.guns.GunMod;
import com.tacz.guns.api.TimelessAPI;
import com.tacz.guns.api.client.event.BeforeRenderHandEvent;
import com.tacz.guns.api.client.event.FieldOfView;
import com.tacz.guns.api.client.gameplay.IClientPlayerGunOperator;
import com.tacz.guns.api.client.other.KeepingItemRenderer;
import com.tacz.guns.api.entity.IGunOperator;
import com.tacz.guns.api.event.common.GunFireEvent;
import com.tacz.guns.api.item.IGun;
import com.tacz.guns.client.model.BedrockGunModel;
import com.tacz.guns.client.resource.index.ClientGunIndex;
import com.tacz.guns.resource.pojo.data.attachment.RecoilModifier;
import com.tacz.guns.resource.pojo.data.gun.GunData;
import com.tacz.guns.util.AttachmentDataUtils;
import com.tacz.guns.util.math.MathUtil;
import com.tacz.guns.util.math.SecondOrderDynamics;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.EntityViewRenderEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction;
import java.util.Optional;
@Mod.EventBusSubscriber(value = Dist.CLIENT, modid = GunMod.MOD_ID)
public class CameraSetupEvent {
/**
* 用于平滑 FOV 变化
*/
private static final SecondOrderDynamics FOV_DYNAMICS = new SecondOrderDynamics(0.5f, 1.2f, 0.5f, 0);
private static PolynomialSplineFunction pitchSplineFunction;
private static PolynomialSplineFunction yawSplineFunction;
private static long shootTimeStamp = -1L;
private static double xRotO = 0;
private static double yRot0 = 0;
private static BedrockGunModel lastModel = null;
@SubscribeEvent
public static void applyLevelCameraAnimation(EntityViewRenderEvent.CameraSetup event) {
if (!Minecraft.getInstance().options.bobView) {
return;
}
LocalPlayer player = Minecraft.getInstance().player;
if (player == null) {
return;
}
ItemStack stack = ((KeepingItemRenderer) Minecraft.getInstance().getItemInHandRenderer()).getCurrentItem();
if (!(stack.getItem() instanceof IGun iGun)) {
return;
}
TimelessAPI.getClientGunIndex(iGun.getGunId(stack)).ifPresent(gunIndex -> {
BedrockGunModel gunModel = gunIndex.getGunModel();
if (lastModel != gunModel) {
// 切换枪械模型的时候清理一下摄像机动画数据,以避免上一次播放到一半的摄像机动画影响观感。
gunModel.cleanCameraAnimationTransform();
lastModel = gunModel;
}
IClientPlayerGunOperator clientPlayerGunOperator = IClientPlayerGunOperator.fromLocalPlayer(player);
float partialTicks = Minecraft.getInstance().getFrameTime();
float aimingProgress = clientPlayerGunOperator.getClientAimingProgress(partialTicks);
float zoom = iGun.getAimingZoom(stack);
float multiplier = 1 - aimingProgress + aimingProgress / (float) Math.sqrt(zoom);
Quaternion q = MathUtil.multiplyQuaternion(gunModel.getCameraAnimationObject().rotationQuaternion, multiplier);
double yaw = Math.asin(2 * (q.r() * q.j() - q.i() * q.k()));
double pitch = Math.atan2(2 * (q.r() * q.i() + q.j() * q.k()), 1 - 2 * (q.i() * q.i() + q.j() * q.j()));
double roll = Math.atan2(2 * (q.r() * q.k() + q.i() * q.j()), 1 - 2 * (q.j() * q.j() + q.k() * q.k()));
yaw = Math.toDegrees(yaw);
pitch = Math.toDegrees(pitch);
roll = Math.toDegrees(roll);
event.setYaw((float) yaw + event.getYaw());
event.setPitch((float) pitch + event.getPitch());
event.setRoll((float) roll + event.getRoll());
});
}
@SubscribeEvent
public static void applyItemInHandCameraAnimation(BeforeRenderHandEvent event) {
if (!Minecraft.getInstance().options.bobView) {
return;
}
LocalPlayer player = Minecraft.getInstance().player;
if (player == null) {
return;
}
ItemStack stack = ((KeepingItemRenderer) Minecraft.getInstance().getItemInHandRenderer()).getCurrentItem();
if (!(stack.getItem() instanceof IGun iGun)) {
return;
}
TimelessAPI.getClientGunIndex(iGun.getGunId(stack)).ifPresent(gunIndex -> {
BedrockGunModel gunModel = gunIndex.getGunModel();
PoseStack poseStack = event.getPoseStack();
IClientPlayerGunOperator clientPlayerGunOperator = IClientPlayerGunOperator.fromLocalPlayer(player);
float partialTicks = Minecraft.getInstance().getFrameTime();
float aimingProgress = clientPlayerGunOperator.getClientAimingProgress(partialTicks);
float zoom = iGun.getAimingZoom(stack);
float multiplier = 1 - aimingProgress + aimingProgress / (float) Math.sqrt(zoom);
Quaternion quaternion = MathUtil.multiplyQuaternion(gunModel.getCameraAnimationObject().rotationQuaternion, multiplier);
poseStack.mulPose(quaternion);
// 截至目前,摄像机动画数据已消费完毕。是否有更好的清理动画数据的方法?
gunModel.cleanCameraAnimationTransform();
});
}
@SubscribeEvent
public static void applyScopeMagnification(FieldOfView event) {
if (event.isItemWithHand()) {
return;
}
Entity entity = event.getCamera().getEntity();
if (entity instanceof LivingEntity livingEntity) {
ItemStack stack = ((KeepingItemRenderer) Minecraft.getInstance().getItemInHandRenderer()).getCurrentItem();
if (!(stack.getItem() instanceof IGun iGun)) {
float fov = FOV_DYNAMICS.update((float) event.getFOV());
event.setFOV(fov);
return;
}
float zoom = iGun.getAimingZoom(stack);
if (livingEntity instanceof LocalPlayer localPlayer) {
IClientPlayerGunOperator gunOperator = IClientPlayerGunOperator.fromLocalPlayer(localPlayer);
float aimingProgress = gunOperator.getClientAimingProgress((float) event.getPartialTicks());
float fov = FOV_DYNAMICS.update((float) MathUtil.magnificationToFov(1 + (zoom - 1) * aimingProgress, event.getFOV()));
event.setFOV(fov);
} else {
IGunOperator gunOperator = IGunOperator.fromLivingEntity(livingEntity);
float aimingProgress = gunOperator.getSynAimingProgress();
float fov = FOV_DYNAMICS.update((float) MathUtil.magnificationToFov(1 + (zoom - 1) * aimingProgress, event.getFOV()));
event.setFOV(fov);
}
}
}
@SubscribeEvent
public static void initialCameraRecoil(GunFireEvent event) {
if (event.getLogicalSide().isClient()) {
LocalPlayer player = Minecraft.getInstance().player;
if (player == null) {
return;
}
ItemStack mainhandItem = player.getMainHandItem();
if (!(mainhandItem.getItem() instanceof IGun iGun)) {
return;
}
ResourceLocation gunId = iGun.getGunId(mainhandItem);
Optional<ClientGunIndex> gunIndexOptional = TimelessAPI.getClientGunIndex(gunId);
if (gunIndexOptional.isEmpty()) {
return;
}
ClientGunIndex gunIndex = gunIndexOptional.get();
GunData gunData = gunIndex.getGunData();
// 获取所有配件对摄像机后坐力的修改
final float[] attachmentRecoilModifier = new float[]{0f, 0f};
AttachmentDataUtils.getAllAttachmentData(mainhandItem, gunData, attachmentData -> {
RecoilModifier recoilModifier = attachmentData.getRecoilModifier();
if (recoilModifier == null) {
return;
}
attachmentRecoilModifier[0] += recoilModifier.getPitch();
attachmentRecoilModifier[1] += recoilModifier.getYaw();
});
IClientPlayerGunOperator clientPlayerGunOperator = IClientPlayerGunOperator.fromLocalPlayer(player);
float partialTicks = Minecraft.getInstance().getFrameTime();
float aimingProgress = clientPlayerGunOperator.getClientAimingProgress(partialTicks);
float zoom = iGun.getAimingZoom(mainhandItem);
float aimingRecoilModifier = 1 - aimingProgress + aimingProgress / (float) Math.sqrt(zoom);
pitchSplineFunction = gunData.getRecoil().genPitchSplineFunction(modifierNumber(attachmentRecoilModifier[0]) * aimingRecoilModifier);
yawSplineFunction = gunData.getRecoil().genYawSplineFunction(modifierNumber(attachmentRecoilModifier[1]) * aimingRecoilModifier);
shootTimeStamp = System.currentTimeMillis();
xRotO = 0;
}
}
@SubscribeEvent
public static void applyCameraRecoil(EntityViewRenderEvent.CameraSetup event) {
LocalPlayer player = Minecraft.getInstance().player;
if (player == null) {
return;
}
long timeTotal = System.currentTimeMillis() - shootTimeStamp;
if (pitchSplineFunction != null && pitchSplineFunction.isValidPoint(timeTotal)) {
double value = pitchSplineFunction.value(timeTotal);
player.setXRot(player.getXRot() - (float) (value - xRotO));
xRotO = value;
}
if (yawSplineFunction != null && yawSplineFunction.isValidPoint(timeTotal)) {
double value = yawSplineFunction.value(timeTotal);
player.setYRot(player.getYRot() - (float) (value - yRot0));
yRot0 = value;
}
}
private static float modifierNumber(float modifier) {
return Math.max(0, 1 + modifier);
}
}

View File

@@ -0,0 +1,56 @@
package com.tacz.guns.client.event;
import com.tacz.guns.api.TimelessAPI;
import com.tacz.guns.api.event.common.LivingHurtByGunEvent;
import com.tacz.guns.api.event.common.LivingKillByGunEvent;
import com.tacz.guns.client.gui.overlay.KillAmountOverlay;
import com.tacz.guns.client.sound.SoundPlayManager;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.LivingEntity;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.LogicalSide;
import net.minecraftforge.fml.common.Mod;
@Mod.EventBusSubscriber(value = Dist.CLIENT)
public class ClientHitMark {
@SubscribeEvent
public static void onEntityHurt(LivingHurtByGunEvent event) {
LogicalSide logicalSide = event.getLogicalSide();
if (logicalSide != LogicalSide.CLIENT) {
return;
}
LivingEntity attacker = event.getAttacker();
LocalPlayer player = Minecraft.getInstance().player;
if (player != null && player.equals(attacker)) {
ResourceLocation gunId = event.getGunId();
RenderCrosshairEvent.markHitTimestamp();
if (event.isHeadShot()) {
RenderCrosshairEvent.markHeadShotTimestamp();
TimelessAPI.getClientGunIndex(gunId).ifPresent(index -> SoundPlayManager.playHeadHitSound(player, index));
} else {
TimelessAPI.getClientGunIndex(gunId).ifPresent(index -> SoundPlayManager.playFleshHitSound(player, index));
}
}
}
@SubscribeEvent
public static void onEntityKill(LivingKillByGunEvent event) {
LogicalSide logicalSide = event.getLogicalSide();
if (logicalSide != LogicalSide.CLIENT) {
return;
}
LivingEntity attacker = event.getAttacker();
LocalPlayer player = Minecraft.getInstance().player;
if (player != null && player.equals(attacker)) {
RenderCrosshairEvent.markKillTimestamp();
KillAmountOverlay.markTimestamp();
TimelessAPI.getClientGunIndex(event.getGunId()).ifPresent(index -> SoundPlayManager.playKillSound(player, index));
if (event.isHeadShot()) {
RenderCrosshairEvent.markHeadShotTimestamp();
}
}
}
}

View File

@@ -0,0 +1,43 @@
package com.tacz.guns.client.event;
import com.tacz.guns.GunMod;
import com.tacz.guns.api.item.IGun;
import com.tacz.guns.client.input.InteractKey;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.decoration.ItemFrame;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.EntityHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.InputEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
@Mod.EventBusSubscriber(value = Dist.CLIENT, modid = GunMod.MOD_ID)
public class ClientPreventGunClick {
@SubscribeEvent
public static void onClickInput(InputEvent.ClickInputEvent event) {
LocalPlayer player = Minecraft.getInstance().player;
if (player == null) {
return;
}
// 当交互键按下时,允许交互
if (InteractKey.INTERACT_KEY.isDown()) {
return;
}
// 只要主手有枪,那么禁止交互
ItemStack itemInHand = player.getItemInHand(InteractionHand.MAIN_HAND);
if (itemInHand.getItem() instanceof IGun) {
// 展示框可以交互
HitResult hitResult = Minecraft.getInstance().hitResult;
if (hitResult instanceof EntityHitResult entityHitResult && entityHitResult.getEntity() instanceof ItemFrame) {
return;
}
// 这个设置为 false 就能阻止客户端粒子的生成
event.setSwingHand(false);
event.setCanceled(true);
}
}
}

View File

@@ -0,0 +1,546 @@
package com.tacz.guns.client.event;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Matrix4f;
import com.mojang.math.Vector3f;
import com.tacz.guns.GunMod;
import com.tacz.guns.api.TimelessAPI;
import com.tacz.guns.api.client.event.RenderItemInHandBobEvent;
import com.tacz.guns.api.client.gameplay.IClientPlayerGunOperator;
import com.tacz.guns.api.client.other.KeepingItemRenderer;
import com.tacz.guns.api.event.common.GunFireEvent;
import com.tacz.guns.api.item.IAttachment;
import com.tacz.guns.api.item.IGun;
import com.tacz.guns.api.item.attachment.AttachmentType;
import com.tacz.guns.client.animation.internal.GunAnimationStateMachine;
import com.tacz.guns.client.gui.GunRefitScreen;
import com.tacz.guns.client.model.BedrockAttachmentModel;
import com.tacz.guns.client.model.BedrockGunModel;
import com.tacz.guns.client.model.bedrock.BedrockModel;
import com.tacz.guns.client.model.bedrock.BedrockPart;
import com.tacz.guns.client.model.functional.MuzzleFlashRender;
import com.tacz.guns.client.model.functional.ShellRender;
import com.tacz.guns.client.renderer.item.GunItemRenderer;
import com.tacz.guns.client.resource.InternalAssetLoader;
import com.tacz.guns.client.resource.index.ClientAttachmentIndex;
import com.tacz.guns.client.resource.index.ClientGunIndex;
import com.tacz.guns.config.client.RenderConfig;
import com.tacz.guns.entity.EntityKineticBullet;
import com.tacz.guns.util.math.Easing;
import com.tacz.guns.util.math.MathUtil;
import com.tacz.guns.util.math.PerlinNoise;
import com.tacz.guns.util.math.SecondOrderDynamics;
import net.minecraft.client.Camera;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.client.renderer.LightTexture;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.block.model.ItemTransforms;
import net.minecraft.client.renderer.block.model.ItemTransforms.TransformType;
import net.minecraft.client.renderer.texture.OverlayTexture;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.Mth;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.RenderHandEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static net.minecraft.client.renderer.block.model.ItemTransforms.TransformType.FIRST_PERSON_RIGHT_HAND;
/**
* 负责第一人称的枪械模型渲染。其他人称参见 {@link GunItemRenderer}
*/
@Mod.EventBusSubscriber(value = Dist.CLIENT, modid = GunMod.MOD_ID)
public class FirstPersonRenderGunEvent {
// 用于生成瞄准动作的运动曲线,使动作看起来更平滑
private static final SecondOrderDynamics AIMING_DYNAMICS = new SecondOrderDynamics(1.2f, 1.2f, 0.5f, 0);
// 用于打开改装界面时枪械运动的平滑
private static final SecondOrderDynamics REFIT_OPENING_DYNAMICS = new SecondOrderDynamics(1f, 1.2f, 0.5f, 0);
// 用于跳跃延滞动画的平滑
private static final SecondOrderDynamics JUMPING_DYNAMICS = new SecondOrderDynamics(0.28f, 1f, 0.65f, 0);
private static final float JUMPING_Y_SWAY = -2f;
private static final float JUMPING_SWAY_TIME = 0.3f;
private static final float LANDING_SWAY_TIME = 0.15f;
// 用于枪械后座的程序动画
private static final PerlinNoise SHOOT_X_SWAY_NOISE = new PerlinNoise(-0.2f, 0.2f, 400);
private static final PerlinNoise SHOOT_Y_ROTATION_NOISE = new PerlinNoise(-0.0136f, 0.0136f, 100);
private static final float SHOOT_Y_SWAY = -0.1f;
private static final float SHOOT_ANIMATION_TIME = 0.3f;
private static float jumpingSwayProgress = 0;
private static boolean lastOnGround = false;
private static long jumpingTimeStamp = -1;
private static long shootTimeStamp = -1;
@SubscribeEvent
public static void onRenderHand(RenderHandEvent event) {
if (event.getHand() == InteractionHand.OFF_HAND) {
return;
}
LocalPlayer player = Minecraft.getInstance().player;
if (player == null) {
return;
}
ItemStack stack = event.getItemStack();
if (!(stack.getItem() instanceof IGun iGun)) {
return;
}
// 获取 TransformType
TransformType transformType;
if (event.getHand() == InteractionHand.MAIN_HAND) {
transformType = FIRST_PERSON_RIGHT_HAND;
} else {
transformType = TransformType.FIRST_PERSON_LEFT_HAND;
}
ResourceLocation gunId = iGun.getGunId(stack);
TimelessAPI.getClientGunIndex(gunId).ifPresentOrElse(gunIndex -> {
BedrockGunModel gunModel = gunIndex.getGunModel();
GunAnimationStateMachine animationStateMachine = gunIndex.getAnimationStateMachine();
if (gunModel == null) {
return;
}
// 在渲染之前,先更新动画,让动画数据写入模型
if (animationStateMachine != null) {
animationStateMachine.update(event.getPartialTicks(), player);
}
PoseStack poseStack = event.getPoseStack();
poseStack.pushPose();
// 逆转原版施加在手上的延滞效果,改为写入模型动画数据中
float xRotOffset = Mth.lerp(event.getPartialTicks(), player.xBobO, player.xBob);
float yRotOffset = Mth.lerp(event.getPartialTicks(), player.yBobO, player.yBob);
float xRot = player.getViewXRot(event.getPartialTicks()) - xRotOffset;
float yRot = player.getViewYRot(event.getPartialTicks()) - yRotOffset;
poseStack.mulPose(Vector3f.XP.rotationDegrees(xRot * -0.1F));
poseStack.mulPose(Vector3f.YP.rotationDegrees(yRot * -0.1F));
BedrockPart rootNode = gunModel.getRootNode();
if (rootNode != null) {
xRot = (float) Math.tanh(xRot / 25) * 25;
yRot = (float) Math.tanh(yRot / 25) * 25;
rootNode.offsetX += yRot * 0.1F / 16F / 3F;
rootNode.offsetY += -xRot * 0.1F / 16F / 3F;
rootNode.additionalQuaternion.mul(Vector3f.XP.rotationDegrees(xRot * 0.05F));
rootNode.additionalQuaternion.mul(Vector3f.YP.rotationDegrees(yRot * 0.05F));
}
// 从渲染原点 (0, 24, 0) 移动到模型原点 (0, 0, 0)
poseStack.translate(0, 1.5f, 0);
// 基岩版模型是上下颠倒的,需要翻转过来。
poseStack.mulPose(Vector3f.ZP.rotationDegrees(180f));
// 应用持枪姿态变换,如第一人称摄像机定位
applyFirstPersonGunTransform(player, stack, gunIndex, poseStack, gunModel, event.getPartialTicks());
// 开启第一人称弹壳和火焰渲染
MuzzleFlashRender.isSelf = true;
ShellRender.isSelf = true;
{
// 如果正在打开改装界面,则取消手臂渲染
boolean renderHand = gunModel.getRenderHand();
if (GunRefitScreen.getOpeningProgress() != 0) {
gunModel.setRenderHand(false);
}
// 调用枪械模型渲染
RenderType renderType = RenderType.itemEntityTranslucentCull(gunIndex.getModelTexture());
gunModel.render(poseStack, stack, transformType, renderType, event.getPackedLight(), OverlayTexture.NO_OVERLAY);
// 调用曳光弹渲染
renderBulletTracer(player, poseStack, gunModel, event.getPartialTicks());
// 恢复手臂渲染
gunModel.setRenderHand(renderHand);
// 渲染完成后,将动画数据从模型中清除,不对其他视角下的模型渲染产生影响
poseStack.popPose();
gunModel.cleanAnimationTransform();
}
// 关闭第一人称弹壳和火焰渲染
MuzzleFlashRender.isSelf = false;
ShellRender.isSelf = false;
// 放这里,只有渲染了枪械,才取消后续(虽然一般来说也没有什么后续了)
event.setCanceled(true);
}, () -> renderBulletTracer(player, event.getPoseStack(), null, event.getPartialTicks()));
}
private static void renderBulletTracer(LocalPlayer player, PoseStack poseStack, BedrockGunModel gunModel, float partialTicks) {
if (!RenderConfig.FIRST_PERSON_BULLET_TRACER_ENABLE.get()) {
return;
}
Optional<BedrockModel> modelOptional = InternalAssetLoader.getBedrockModel(InternalAssetLoader.DEFAULT_BULLET_MODEL);
if (modelOptional.isEmpty()) {
return;
}
BedrockModel model = modelOptional.get();
Level level = player.getLevel();
AABB renderArea = player.getBoundingBox().inflate(256, 256, 256);
for (Entity entity : level.getEntities(player, renderArea, FirstPersonRenderGunEvent::bulletFromPlayer)) {
EntityKineticBullet entityBullet = (EntityKineticBullet) entity;
if (!entityBullet.isTracerAmmo()) {
continue;
}
Vec3 deltaMovement = entityBullet.getDeltaMovement().multiply(partialTicks, partialTicks, partialTicks);
Vec3 entityPosition = entityBullet.getPosition(0).add(deltaMovement);
Camera camera = Minecraft.getInstance().gameRenderer.getMainCamera();
Vec3 cameraPosition = camera.getPosition();
Vec3 originCameraPosition = entityBullet.getOriginCameraPosition();
if (originCameraPosition == null) {
if (gunModel == null) {
continue;
}
if (gunModel.getMuzzleFlashPosPath() != null) {
poseStack.pushPose();
for (BedrockPart bedrockPart : gunModel.getMuzzleFlashPosPath()) {
bedrockPart.translateAndRotateAndScale(poseStack);
}
Matrix4f pose = poseStack.last().pose();
originCameraPosition = new Vec3(cameraPosition.x, cameraPosition.y, cameraPosition.z);
entityBullet.setOriginCameraPosition(originCameraPosition);
entityBullet.setOriginRenderOffset(new Vec3(pose.m03, pose.m13, pose.m23));
poseStack.popPose();
} else {
continue;
}
}
Vec3 originRenderOffset = entityBullet.getOriginRenderOffset();
Vec3 alphaCameraTranslation = originCameraPosition.subtract(cameraPosition);
double distance = entityPosition.distanceTo(originCameraPosition);
Vec3 bulletDirection = entityPosition.subtract(originCameraPosition);
double yRot = MathUtil.getTwoVecAngle(new Vec3(0, 0, -1), new Vec3(bulletDirection.x, 0, bulletDirection.z));
double xRot = MathUtil.getTwoVecAngle(new Vec3(bulletDirection.x, 0, bulletDirection.z), bulletDirection);
if (yRot == -1) {
yRot = Math.toRadians(camera.getYRot() + 180f);
}
if (xRot == -1) {
xRot = Math.toRadians(camera.getXRot());
}
xRot *= bulletDirection.y > 0 ? -1 : 1;
yRot *= bulletDirection.x > 0 ? 1 : -1;
PoseStack poseStack1 = new PoseStack();
// 逆转摄像机的旋转,回到起始坐标
poseStack1.mulPose(Vector3f.XP.rotationDegrees(camera.getXRot()));
poseStack1.mulPose(Vector3f.YP.rotationDegrees(camera.getYRot() + 180f));
poseStack1.translate(alphaCameraTranslation.x, alphaCameraTranslation.y, alphaCameraTranslation.z);
// 恢复旋转角度,应用枪口定位偏移
poseStack1.mulPose(Vector3f.YN.rotation((float) yRot));
poseStack1.mulPose(Vector3f.XN.rotation((float) xRot));
poseStack1.translate(originRenderOffset.x, originRenderOffset.y, originRenderOffset.z - distance);
float trailLength = 0.5f * (float) entityBullet.getDeltaMovement().length();
poseStack1.translate(0, 0, -trailLength / 2);
poseStack1.scale(0.03f, 0.03f, trailLength);
ResourceLocation ammoId = entityBullet.getAmmoId();
TimelessAPI.getClientAmmoIndex(ammoId).ifPresent(index -> {
float[] tracerColor = index.getTracerColor();
RenderType type = RenderType.energySwirl(InternalAssetLoader.DEFAULT_BULLET_TEXTURE, 15, 15);
model.render(poseStack1, ItemTransforms.TransformType.NONE, type, LightTexture.pack(15, 15),
OverlayTexture.NO_OVERLAY, tracerColor[0], tracerColor[1], tracerColor[2], 1);
});
}
}
/**
* 当主手拿着枪械物品的时候,取消应用在它上面的 viewBobbing以便应用自定义的跑步/走路动画。
*/
@SubscribeEvent
public static void cancelItemInHandViewBobbing(RenderItemInHandBobEvent.BobView event) {
Minecraft mc = Minecraft.getInstance();
if (mc.player == null) {
return;
}
ItemStack itemStack = ((KeepingItemRenderer) Minecraft.getInstance().getItemInHandRenderer()).getCurrentItem();
if (IGun.getIGunOrNull(itemStack) != null) {
event.setCanceled(true);
}
}
@SubscribeEvent
public static void onGunFire(GunFireEvent event) {
if (event.getLogicalSide().isClient()) {
LocalPlayer player = Minecraft.getInstance().player;
if (player == null) {
return;
}
ItemStack mainhandItem = player.getMainHandItem();
IGun iGun = IGun.getIGunOrNull(mainhandItem);
if (iGun == null) {
return;
}
TimelessAPI.getClientGunIndex(iGun.getGunId(mainhandItem)).ifPresent(gunIndex -> {
// 记录开火时间戳,用于后坐力程序动画
shootTimeStamp = System.currentTimeMillis();
// 记录枪口火焰数据
MuzzleFlashRender.onShoot();
// 抛壳
if (gunIndex.getShellEjection() != null) {
ShellRender.addShell(gunIndex.getShellEjection().getRandomVelocity());
}
});
}
}
private static boolean bulletFromPlayer(Entity entity) {
if (entity instanceof EntityKineticBullet entityBullet) {
return entityBullet.getOwner() instanceof LocalPlayer;
}
return false;
}
private static void applyFirstPersonGunTransform(LocalPlayer player, ItemStack gunItemStack, ClientGunIndex gunIndex, PoseStack poseStack, BedrockGunModel model, float partialTicks) {
// 配合运动曲线,计算改装枪口的打开进度
float refitScreenOpeningProgress = REFIT_OPENING_DYNAMICS.update(GunRefitScreen.getOpeningProgress());
// 配合运动曲线,计算瞄准进度
float aimingProgress = AIMING_DYNAMICS.update(IClientPlayerGunOperator.fromLocalPlayer(player).getClientAimingProgress(partialTicks));
// 应用枪械动态,如后坐力、持枪跳跃等
applyGunMovements(model, aimingProgress, partialTicks);
// 应用各种摄像机定位组的变换(默认持枪、瞄准、改装界面等)
applyFirstPersonPositioningTransform(poseStack, model, gunItemStack, aimingProgress, refitScreenOpeningProgress);
// 应用动画约束变换
applyAnimationConstraintTransform(poseStack, model, aimingProgress * (1 - refitScreenOpeningProgress));
}
private static void applyGunMovements(BedrockGunModel model, float aimingProgress, float partialTicks) {
applyShootSwayAndRotation(model, aimingProgress);
applyJumpingSway(model, partialTicks);
}
/**
* 应用瞄具摄像机定位组、机瞄摄像机定位组和 Idle 摄像机定位组的变换。会在几个摄像机定位之间插值。
*/
private static void applyFirstPersonPositioningTransform(PoseStack poseStack, BedrockGunModel model, ItemStack stack, float aimingProgress, float refitScreenOpeningProgress) {
IGun iGun = IGun.getIGunOrNull(stack);
if (iGun == null) {
return;
}
Matrix4f transformMatrix = new Matrix4f();
transformMatrix.setIdentity();
// 应用瞄准定位
List<BedrockPart> idleNodePath = model.getIdleSightPath();
List<BedrockPart> aimingNodePath = null;
ItemStack scopeItem = iGun.getAttachment(stack, AttachmentType.SCOPE);
if (scopeItem.isEmpty()) {
// 未安装瞄具,使用机瞄定位组
aimingNodePath = model.getIronSightPath();
} else {
// 安装瞄具,组合瞄具定位组和瞄具视野定位组
List<BedrockPart> scopeNodePath = model.getScopePosPath();
if (scopeNodePath != null) {
aimingNodePath = new ArrayList<>(scopeNodePath);
IAttachment iAttachment = IAttachment.getIAttachmentOrNull(scopeItem);
if (iAttachment != null) {
ResourceLocation scopeId = iAttachment.getAttachmentId(scopeItem);
Optional<ClientAttachmentIndex> indexOptional = TimelessAPI.getClientAttachmentIndex(scopeId);
if (indexOptional.isPresent()) {
BedrockAttachmentModel attachmentModel = indexOptional.get().getAttachmentModel();
if (attachmentModel.getScopeViewPath() != null) {
aimingNodePath.addAll(attachmentModel.getScopeViewPath());
}
}
}
}
}
MathUtil.applyMatrixLerp(transformMatrix, getPositioningNodeInverse(idleNodePath), transformMatrix, (1 - refitScreenOpeningProgress));
MathUtil.applyMatrixLerp(transformMatrix, getPositioningNodeInverse(aimingNodePath), transformMatrix, (1 - refitScreenOpeningProgress) * aimingProgress);
// 应用改装界面开启时的定位
float refitTransformProgress = (float) Easing.easeOutCubic(GunRefitScreen.getTransformProgress());
AttachmentType oldType = GunRefitScreen.getOldTransformType();
AttachmentType currentType = GunRefitScreen.getCurrentTransformType();
List<BedrockPart> fromNode = model.getRefitAttachmentViewPath(oldType);
List<BedrockPart> toNode = model.getRefitAttachmentViewPath(currentType);
MathUtil.applyMatrixLerp(transformMatrix, getPositioningNodeInverse(fromNode), transformMatrix, refitScreenOpeningProgress);
MathUtil.applyMatrixLerp(transformMatrix, getPositioningNodeInverse(toNode), transformMatrix, refitScreenOpeningProgress * refitTransformProgress);
// 应用变换到 PoseStack
poseStack.translate(0, 1.5f, 0);
poseStack.mulPoseMatrix(transformMatrix);
poseStack.translate(0, -1.5f, 0);
}
/**
* 获取摄像机定位组的反相矩阵
*/
@Nonnull
private static Matrix4f getPositioningNodeInverse(List<BedrockPart> nodePath) {
Matrix4f matrix4f = new Matrix4f();
matrix4f.setIdentity();
if (nodePath != null) {
for (int i = nodePath.size() - 1; i >= 0; i--) {
BedrockPart part = nodePath.get(i);
// 计算反向的旋转
matrix4f.multiply(Vector3f.XN.rotation(part.xRot));
matrix4f.multiply(Vector3f.YN.rotation(part.yRot));
matrix4f.multiply(Vector3f.ZN.rotation(part.zRot));
// 计算反向的位移
if (part.getParent() != null) {
matrix4f.multiplyWithTranslation(-part.x / 16.0F, -part.y / 16.0F, -part.z / 16.0F);
} else {
matrix4f.multiplyWithTranslation(-part.x / 16.0F, (1.5F - part.y / 16.0F), -part.z / 16.0F);
}
}
}
return matrix4f;
}
private static void applyShootSwayAndRotation(BedrockGunModel model, float aimingProgress) {
BedrockPart rootNode = model.getRootNode();
if (rootNode != null) {
float progress = 1 - (System.currentTimeMillis() - shootTimeStamp) / (SHOOT_ANIMATION_TIME * 1000);
if (progress < 0) {
progress = 0;
}
progress = (float) Easing.easeOutCubic(progress);
rootNode.offsetX += SHOOT_X_SWAY_NOISE.getValue() / 16 * progress * (1 - aimingProgress);
// 基岩版模型 y 轴上下颠倒sway 值取相反数
rootNode.offsetY += -SHOOT_Y_SWAY / 16 * progress * (1 - aimingProgress);
rootNode.additionalQuaternion.mul(Vector3f.YP.rotation(SHOOT_Y_ROTATION_NOISE.getValue() * progress));
}
}
private static void applyJumpingSway(BedrockGunModel model, float partialTicks) {
if (jumpingTimeStamp == -1) {
jumpingTimeStamp = System.currentTimeMillis();
}
LocalPlayer player = Minecraft.getInstance().player;
if (player != null) {
double posY = Mth.lerp(partialTicks, Minecraft.getInstance().player.yOld, Minecraft.getInstance().player.getY());
float velocityY = (float) (posY - Minecraft.getInstance().player.yOld) / partialTicks;
if (player.isOnGround()) {
if (!lastOnGround) {
jumpingSwayProgress = velocityY / -0.1f;
if (jumpingSwayProgress > 1) {
jumpingSwayProgress = 1;
}
lastOnGround = true;
} else {
jumpingSwayProgress -= (System.currentTimeMillis() - jumpingTimeStamp) / (LANDING_SWAY_TIME * 1000);
if (jumpingSwayProgress < 0) {
jumpingSwayProgress = 0;
}
}
} else {
if (lastOnGround) {
// 0.42 是玩家自然起跳的速度
jumpingSwayProgress = velocityY / 0.42f;
if (jumpingSwayProgress > 1) {
jumpingSwayProgress = 1;
}
lastOnGround = false;
} else {
jumpingSwayProgress -= (System.currentTimeMillis() - jumpingTimeStamp) / (JUMPING_SWAY_TIME * 1000);
if (jumpingSwayProgress < 0) {
jumpingSwayProgress = 0;
}
}
}
}
jumpingTimeStamp = System.currentTimeMillis();
float ySway = JUMPING_DYNAMICS.update(JUMPING_Y_SWAY * jumpingSwayProgress);
BedrockPart rootNode = model.getRootNode();
if (rootNode != null) {
// 基岩版模型 y 轴上下颠倒sway 值取相反数
rootNode.offsetY += -ySway / 16;
}
}
/**
* 获取动画约束点的变换数据。
*
* @param originTranslation 用于输出约束点的原坐标
* @param animatedTranslation 用于输出约束点经过动画变换之后的坐标
* @param rotation 用于输出约束点的旋转
*/
private static void getAnimationConstraintTransform(List<BedrockPart> nodePath, @Nonnull Vector3f originTranslation, @Nonnull Vector3f animatedTranslation, @Nonnull Vector3f rotation) {
if (nodePath == null) {
return;
}
// 约束点动画变换矩阵
Matrix4f animeMatrix = new Matrix4f();
// 约束点初始变换矩阵
Matrix4f originMatrix = new Matrix4f();
animeMatrix.setIdentity();
originMatrix.setIdentity();
BedrockPart constrainNode = nodePath.get(nodePath.size() - 1);
for (BedrockPart part : nodePath) {
// 乘动画位移
if (part != constrainNode) {
animeMatrix.multiplyWithTranslation(part.offsetX, part.offsetY, part.offsetZ);
}
// 乘组位移
if (part.getParent() != null) {
animeMatrix.multiplyWithTranslation(part.x / 16.0F, part.y / 16.0F, part.z / 16.0F);
} else {
animeMatrix.multiplyWithTranslation(part.x / 16.0F, (part.y / 16.0F - 1.5F), part.z / 16.0F);
}
// 乘动画旋转
if (part != constrainNode) {
animeMatrix.multiply(part.additionalQuaternion);
}
// 乘组旋转
animeMatrix.multiply(Vector3f.ZP.rotation(part.zRot));
animeMatrix.multiply(Vector3f.YP.rotation(part.yRot));
animeMatrix.multiply(Vector3f.XP.rotation(part.xRot));
// 乘组位移
if (part.getParent() != null) {
originMatrix.multiplyWithTranslation(part.x / 16.0F, part.y / 16.0F, part.z / 16.0F);
} else {
originMatrix.multiplyWithTranslation(part.x / 16.0F, (part.y / 16.0F - 1.5F), part.z / 16.0F);
}
// 乘组旋转
originMatrix.multiply(Vector3f.ZP.rotation(part.zRot));
originMatrix.multiply(Vector3f.YP.rotation(part.yRot));
originMatrix.multiply(Vector3f.XP.rotation(part.xRot));
}
// 把变换数据写入输出
animatedTranslation.set(animeMatrix.m03, animeMatrix.m13, animeMatrix.m23);
originTranslation.set(originMatrix.m03, originMatrix.m13, originMatrix.m23);
Vector3f animatedRotation = MathUtil.getEulerAngles(animeMatrix);
Vector3f originRotation = MathUtil.getEulerAngles(originMatrix);
animatedRotation.sub(originRotation);
rotation.set(animatedRotation.x(), animatedRotation.y(), animatedRotation.z());
}
/**
* 应用动画约束变换。
*
* @param weight 控制约束变换的权重,用于插值。
*/
public static void applyAnimationConstraintTransform(PoseStack poseStack, BedrockGunModel gunModel, float weight) {
List<BedrockPart> nodePath = gunModel.getConstraintPath();
if (nodePath == null) {
return;
}
if (gunModel.getConstraintObject() == null) {
return;
}
// 获取动画约束点的变换信息
Vector3f originTranslation = new Vector3f();
Vector3f animatedTranslation = new Vector3f();
Vector3f rotation = new Vector3f();
Vector3f translationICA = gunModel.getConstraintObject().translationConstraint;
Vector3f rotationICA = gunModel.getConstraintObject().rotationConstraint;
getAnimationConstraintTransform(nodePath, originTranslation, animatedTranslation, rotation);
// 配合约束系数,计算约束位移需要的反向位移
Vector3f inverseTranslation = originTranslation.copy();
inverseTranslation.sub(animatedTranslation);
inverseTranslation.mul(1 - translationICA.x(), 1 - translationICA.y(), 1 - translationICA.z());
// 计算约束旋转需要的反向旋转。因需要插值,获取的是欧拉角
Vector3f inverseRotation = rotation.copy();
inverseRotation.mul(rotationICA.x() - 1, rotationICA.y() - 1, rotationICA.z() - 1);
// 约束旋转
poseStack.translate(animatedTranslation.x(), animatedTranslation.y() + 1.5f, animatedTranslation.z());
poseStack.mulPose(Vector3f.XP.rotation(inverseRotation.x() * weight));
poseStack.mulPose(Vector3f.YP.rotation(inverseRotation.y() * weight));
poseStack.mulPose(Vector3f.ZP.rotation(inverseRotation.z() * weight));
poseStack.translate(-animatedTranslation.x(), -animatedTranslation.y() - 1.5f, -animatedTranslation.z());
// 约束位移
poseStack.last().pose().translate(new Vector3f(-inverseTranslation.x() * weight, -inverseTranslation.y() * weight, inverseTranslation.z() * weight));
}
}

View File

@@ -0,0 +1,78 @@
package com.tacz.guns.client.event;
import com.tacz.guns.GunMod;
import com.tacz.guns.api.client.event.SwapItemWithOffHand;
import com.tacz.guns.api.client.gameplay.IClientPlayerGunOperator;
import com.tacz.guns.api.item.IGun;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.ClientPlayerNetworkEvent;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
@Mod.EventBusSubscriber(value = Dist.CLIENT, modid = GunMod.MOD_ID)
public class InventoryEvent {
// 用于切枪逻辑
private static int oldHotbarSelected = -1;
private static ItemStack oldHotbarSelectItem = ItemStack.EMPTY;
@SubscribeEvent
public static void onPlayerChangeSelect(TickEvent.ClientTickEvent event) {
LocalPlayer player = Minecraft.getInstance().player;
if (player == null) {
return;
}
Inventory inventory = player.getInventory();
// 玩家切换选中框的情况
if (oldHotbarSelected != inventory.selected) {
if (oldHotbarSelected == -1) {
IClientPlayerGunOperator.fromLocalPlayer(player).draw(ItemStack.EMPTY);
} else {
IClientPlayerGunOperator.fromLocalPlayer(player).draw(inventory.getItem(oldHotbarSelected));
}
oldHotbarSelected = inventory.selected;
oldHotbarSelectItem = inventory.getItem(inventory.selected).copy();
return;
}
// 玩家选中的物品改变的情况
ItemStack currentItem = inventory.getItem(inventory.selected);
if (!ItemStack.matches(oldHotbarSelectItem, currentItem)) {
if (!isSame(oldHotbarSelectItem, currentItem)) {
IClientPlayerGunOperator.fromLocalPlayer(player).draw(oldHotbarSelectItem);
}
oldHotbarSelectItem = currentItem.copy();
}
}
@SubscribeEvent
public static void onPlayerSwapMainHand(SwapItemWithOffHand event) {
LocalPlayer player = Minecraft.getInstance().player;
if (player == null) {
return;
}
IClientPlayerGunOperator.fromLocalPlayer(player).draw(player.getOffhandItem());
}
@SubscribeEvent
public static void onPlayerLoggedOut(ClientPlayerNetworkEvent.LoggedOutEvent event) {
// 离开游戏时重置客户端 draw 状态
oldHotbarSelected = -1;
oldHotbarSelectItem = ItemStack.EMPTY;
}
private static boolean isSame(ItemStack i, ItemStack j) {
IGun iGun1 = IGun.getIGunOrNull(i);
IGun iGun2 = IGun.getIGunOrNull(j);
if (iGun1 != null && iGun2 != null) {
return iGun1.getGunId(i).equals(iGun2.getGunId(j));
}
if (i.isEmpty() || j.isEmpty()) {
return i.isEmpty() && j.isEmpty();
}
return i.sameItem(j);
}
}

View File

@@ -0,0 +1,22 @@
package com.tacz.guns.client.event;
import com.mojang.blaze3d.audio.SoundBuffer;
import com.tacz.guns.client.sound.GunSoundInstance;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.sound.PlaySoundSourceEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
@Mod.EventBusSubscriber(value = Dist.CLIENT)
public class PlayGunSoundEvent {
@SubscribeEvent
public static void onPlaySoundSource(PlaySoundSourceEvent event) {
if (event.getSound() instanceof GunSoundInstance instance) {
SoundBuffer soundBuffer = instance.getSoundBuffer();
if (soundBuffer != null) {
event.getChannel().attachStaticBuffer(soundBuffer);
event.getChannel().play();
}
}
}
}

View File

@@ -0,0 +1,34 @@
package com.tacz.guns.client.event;
import com.tacz.guns.api.TimelessAPI;
import com.tacz.guns.api.event.common.LivingHurtByGunEvent;
import com.tacz.guns.client.renderer.other.GunHurtBobTweak;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.LivingEntity;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.LogicalSide;
import net.minecraftforge.fml.common.Mod;
@Mod.EventBusSubscriber(value = Dist.CLIENT)
public class PlayerHurtByGunEvent {
@SubscribeEvent
public static void onPlayerHurtByGun(LivingHurtByGunEvent event) {
LogicalSide logicalSide = event.getLogicalSide();
if (logicalSide != LogicalSide.CLIENT) {
return;
}
LivingEntity hurtEntity = event.getHurtEntity();
LocalPlayer player = Minecraft.getInstance().player;
// 当受伤的是自己的时候,触发受伤晃动的调整参数
if (player != null && player.equals(hurtEntity)) {
ResourceLocation gunId = event.getGunId();
TimelessAPI.getCommonGunIndex(gunId).ifPresent(index -> {
float tweakMultiplier = index.getGunData().getHurtBobTweakMultiplier();
GunHurtBobTweak.markTimestamp(tweakMultiplier);
});
}
}
}

Some files were not shown because too many files have changed in this diff Show More