增加喷射效果
build / build (push) Has been cancelled

This commit is contained in:
TermiNexus committed 2026-08-22 04:45:37 +08:00
commit 1eea4b9eaa
567 files changed
+81290

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# Linux start script should use lf
/gradlew text eol=lf
# These are Windows script files and should use crlf
*.bat text eol=crlf
+30
View File
@@ -0,0 +1,30 @@
# Automatically build the project and run any configured tests for every push
# and submitted pull request. This can help catch issues that only occur on
# certain platforms or Java versions, and provides a first line of defence
# against bad commits.
name: build
on: [pull_request, push]
jobs:
build:
runs-on: ubuntu-24.04
steps:
- name: checkout repository
uses: actions/checkout@v6
- name: validate gradle wrapper
uses: gradle/actions/wrapper-validation@v6
- name: setup jdk
uses: actions/setup-java@v5
with:
java-version: '25'
distribution: 'microsoft'
- name: make gradle wrapper executable
run: chmod +x ./gradlew
- name: build
run: ./gradlew build
- name: capture build artifacts
uses: actions/upload-artifact@v7
with:
name: Artifacts
path: build/libs/
+40
View File
@@ -0,0 +1,40 @@
# gradle
.gradle/
build/
out/
classes/
# eclipse
*.launch
# idea
.idea/
*.iml
*.ipr
*.iws
# vscode
.settings/
.vscode/
bin/
.classpath
.project
# macos
*.DS_Store
# fabric
run/
# java
hs_err_*.log
replay_*.log
*.hprof
*.jfr
+11
View File
@@ -0,0 +1,11 @@
{
"mcpServers": {
"blockbench": {
"command": "node",
"args": ["D:\\tools\\blockbench-mcp\\dist\\index.js"],
"env": {
"BLOCKBENCH_MCP_PORT": "8787"
}
}
}
}
+46
View File
@@ -0,0 +1,46 @@
# DeployPack.ps1 - Deploy built mod jar to client & server mods, plus the v1.4.1 resource pack to server
$ErrorActionPreference = 'Stop'
$Jar = 'D:\Users\TermiNexus\Desktop\NeedsOfNature-1.20.1-port-main\build\libs\needsofnature-1.4.4.jar'
$ClientMods = 'D:\Users\TermiNexus\Desktop\HMCL\OLD\.minecraft\versions\1.21.11-Fabric-Fuck\mods'
$ServerMods = 'D:\MCServer\MCN\mods'
# --- Resource pack (original feature) ---
$PackSrc = 'D:\Users\TermiNexus\Desktop\HMCL\OLD\.minecraft\versions\1.21.11-Fabric-Fuck\resourcepacks\needs_of_nature_default_packv1.4.1'
$PackDst = 'D:\MCServer\MCN\world\datapacks\needs_of_nature_default_packv1.4.1'
if (-not (Test-Path -LiteralPath $Jar)) { throw "Built jar not found: $Jar" }
# 1. Client mods: remove previous deploy, back up original, copy new jar
$Old = "$ClientMods\needsofnature-1.0.0.jar"
if (Test-Path -LiteralPath $Old) {
Remove-Item -LiteralPath $Old -Force
Write-Output "Removed old client deploy: $Old"
}
$OrigClient = "$ClientMods\needsofnature-1.4.4..jar"
if (Test-Path -LiteralPath $OrigClient) {
Move-Item -LiteralPath $OrigClient -Destination "$OrigClient.bak" -Force
Write-Output "Backed up client mod: $OrigClient -> .bak"
}
Copy-Item -LiteralPath $Jar -Destination "$ClientMods\needsofnature-1.4.4.jar" -Force
Write-Output "Client mod deployed: $ClientMods\needsofnature-1.4.4.jar"
# 2. Server mods: back up original, copy new jar (server may be running -> file locked)
$OrigServer = "$ServerMods\needsofnature-1.4.4..jar"
try {
if (Test-Path -LiteralPath $OrigServer) {
Move-Item -LiteralPath $OrigServer -Destination "$OrigServer.bak" -Force
Write-Output "Backed up server mod: $OrigServer -> .bak"
}
Copy-Item -LiteralPath $Jar -Destination "$ServerMods\needsofnature-1.4.4.jar" -Force
Write-Output "Server mod deployed: $ServerMods\needsofnature-1.4.4.jar"
} catch {
Write-Output "WARNING: Server mod deploy failed - file is locked. Stop the server first, then re-run this script."
throw $_
}
# 3. Resource pack to server datapacks (original feature)
if (-not (Test-Path -LiteralPath $PackSrc)) { throw "Resource pack not found: $PackSrc" }
if (Test-Path -LiteralPath $PackDst) { Remove-Item -LiteralPath $PackDst -Recurse -Force }
Copy-Item -LiteralPath $PackSrc -Destination $PackDst -Recurse -Force
Write-Output "Resource pack deployed: $PackSrc -> $PackDst"
+121
View File
@@ -0,0 +1,121 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.
+9
View File
@@ -0,0 +1,9 @@
# NeedsOfNature
## Setup
For setup instructions, please see the [Fabric Documentation page](https://docs.fabricmc.net/develop/getting-started/creating-a-project#setting-up) related to the IDE that you are using.
## License
This template is available under the CC0 license. Feel free to learn from it and incorporate it in your own projects.
+87
View File
@@ -0,0 +1,87 @@
plugins {
id 'net.fabricmc.fabric-loom-remap' version "${loom_version}"
id 'maven-publish'
}
repositories {
// Add repositories to retrieve artifacts from in here.
// You should only use this when depending on other mods because
// Loom adds the essential maven repositories to download Minecraft and libraries from automatically.
// See https://docs.gradle.org/current/userguide/declaring_repositories.html
// for more information about repositories.
}
dependencies {
// To change the versions see the gradle.properties file
minecraft "com.mojang:minecraft:${project.minecraft_version}"
mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2"
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
// Fabric API. This is technically optional, but you probably want it anyway.
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}"
// 原版 mod 源码已在本工程内(src),这里只挂外部依赖(本地 mods 文件夹)
def modsDir = "D:/Users/TermiNexus/Desktop/HMCL/OLD/.minecraft/versions/1.21.11-Fabric-Fuck/mods"
modImplementation files("${modsDir}/geckolib-fabric-1.21.11-5.4.5.jar")
modImplementation files("${modsDir}/trinkets-3.11.0-beta.2+1.21.11.jar")
modImplementation files("${modsDir}/cardinal-components-api-7.3.2.jar")
modImplementation files("${modsDir}/cardinal-components-base-7.3.2.jar")
modImplementation files("${modsDir}/placeholder-api-2.8.2+1.21.10.jar")
modImplementation files("${modsDir}/modmenu-17.0.1-beta.1.jar")
modImplementation files("${modsDir}/needsofnature-intiface-libs-generated.jar")
// Jade(snownee.jade.api)—— 编译需要
def jadeJar = "${modsDir}/Jade-1.21.11.jar"
if (!new File(jadeJar).exists()) {
throw new GradleException("缺少 Jade mod jar,请放到: ${jadeJar}")
}
modImplementation files(jadeJar)
}
processResources {
def version = project.version
inputs.property "version", version
filesMatching("fabric.mod.json") {
expand "version": version
}
}
tasks.withType(JavaCompile).configureEach {
it.options.release = 21
}
java {
// Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task
// if it is present.
// If you remove this line, sources will not be generated.
withSourcesJar()
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
jar {
def projectName = project.name
inputs.property "projectName", projectName
from("LICENSE") {
rename { "${it}_$projectName"}
}
}
// configure the maven publication
publishing {
publications {
create("mavenJava", MavenPublication) {
from components.java
}
}
// See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing.
repositories {
// Add repositories to publish to here.
// Notice: This block does NOT have the same function as the block in the top level.
// The repositories here will be used for publishing your artifact, not for
// retrieving dependencies.
}
}
+163
View File
@@ -0,0 +1,163 @@
"""
REPL 模式文件内容搜索工具
支持正则或纯文本搜索,默认搜索当前目录
"""
import os
import re
import sys
# 硬编码排除目录
EXCLUDE_DIRS = {
'.git', '.vs', 'node_modules', '__pycache__', '.idea',
'miniprogram_npm', '_backup_pre_redesign',
}
# 排除的文件扩展名
EXCLUDE_EXTS = {
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.bmp',
'.mp3', '.mp4', '.wav', '.avi',
'.zip', '.rar', '.7z', '.tar', '.gz',
'.exe', '.dll', '.so', '.dylib',
'.pyc', '.pyo', '.class', '.o', '.obj',
'.db', '.sqlite', '.vsidx',
}
# 默认搜索根目录
DEFAULT_ROOT = os.path.dirname(os.path.abspath(__file__))
def should_skip(dirpath):
"""判断目录是否应跳过"""
parts = dirpath.replace('\\', '/').split('/')
return any(p in EXCLUDE_DIRS for p in parts)
def search(pattern, root=DEFAULT_ROOT, use_regex=True, max_results=50):
"""搜索文件内容"""
if use_regex:
try:
compiled = re.compile(pattern, re.IGNORECASE)
except re.error as e:
print(f' 正则语法错误: {e}')
return
count = 0
for dirpath, dirnames, filenames in os.walk(root):
# 就地修改 dirnames 以跳过排除目录
dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS]
for fname in filenames:
ext = os.path.splitext(fname)[1].lower()
if ext in EXCLUDE_EXTS:
continue
fpath = os.path.join(dirpath, fname)
relpath = os.path.relpath(fpath, root)
try:
with open(fpath, 'r', encoding='utf-8', errors='ignore') as f:
for lineno, line in enumerate(f, 1):
line_stripped = line.rstrip('\n\r')
if use_regex:
match = compiled.search(line_stripped)
if match:
highlight = line_stripped[:match.start()] + \
f'>>> {match.group()} <<<' + \
line_stripped[match.end():]
print(f' {relpath}:{lineno} {highlight}')
count += 1
else:
if pattern.lower() in line_stripped.lower():
print(f' {relpath}:{lineno} {line_stripped}')
count += 1
if count >= max_results:
print(f'\n ... 已达上限 {max_results} 条,停止')
return
except (PermissionError, OSError):
continue
if count == 0:
print(' 未找到匹配内容')
else:
print(f'\n 共 {count} 条匹配')
def main():
print('=== 文件内容搜索工具 ===')
print(f'搜索根目录: {DEFAULT_ROOT}')
print(f'排除目录: {EXCLUDE_DIRS}')
print()
print('命令:')
print(' /regex <pattern> 正则搜索')
print(' /text <text> 纯文本搜索')
print(' /max <n> 设置结果上限(默认50)')
print(' /dir <path> 切换搜索目录')
print(' /help 显示帮助')
print(' /quit 退出')
print()
print('直接输入内容默认使用【纯文本】搜索,需要正则请加 /regex 前缀')
print()
root = DEFAULT_ROOT
use_regex = False # 修改1:默认纯文本
max_results = 50
while True:
try:
raw = input('find> ').strip()
except (EOFError, KeyboardInterrupt):
print('\n再见')
break
if not raw:
continue
if raw == '/quit':
print('再见')
break
if raw == '/help':
print(' /regex <pattern> 正则搜索')
print(' /text <text> 纯文本搜索')
print(' /max <n> 设置结果上限')
print(' /dir <path> 切换搜索目录')
print(' /help 显示帮助')
print(' /quit 退出')
print(' 直接输入内容默认纯文本搜索')
continue
if raw.startswith('/regex '):
pattern = raw[7:].strip()
if pattern:
search(pattern, root, use_regex=True, max_results=max_results)
continue
if raw.startswith('/text '):
pattern = raw[6:].strip()
if pattern:
search(pattern, root, use_regex=False, max_results=max_results)
continue
if raw.startswith('/max '):
try:
max_results = int(raw[5:].strip())
print(f' 结果上限设为 {max_results}')
except ValueError:
print(' 请输入数字')
continue
if raw.startswith('/dir '):
new_dir = raw[5:].strip()
if os.path.isdir(new_dir):
root = os.path.abspath(new_dir)
print(f' 搜索目录切换为: {root}')
else:
print(f' 目录不存在: {new_dir}')
continue
# 修改2:直接输入走纯文本搜索
search(raw, root, use_regex=False, max_results=max_results)
if __name__ == '__main__':
main()
+20
View File
@@ -0,0 +1,20 @@
# Done to increase the memory available to gradle.
org.gradle.jvmargs=-Xmx1G
org.gradle.parallel=true
# IntelliJ IDEA is not yet fully compatible with configuration cache, see: https://github.com/FabricMC/fabric-loom/issues/1349
org.gradle.configuration-cache=false
# Fabric Properties
# check these on https://fabricmc.net/develop
minecraft_version=1.21.11
yarn_mappings=1.21.11+build.6
loader_version=0.19.3
loom_version=1.17-SNAPSHOT
# Mod Properties
version=1.4.4
group=com.nonid
# Dependencies
fabric_api_version=0.141.6+1.21.11
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=file:///D:/%E8%BF%85%E9%9B%B7%E4%B8%8B%E8%BD%BD/gradle-9.5.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+248
View File
@@ -0,0 +1,248 @@
#!/bin/sh
#
# Copyright © 2015 the original 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.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# 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 ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
# 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
if ! command -v java >/dev/null 2>&1
then
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
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# 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"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
Vendored
+82
View File
@@ -0,0 +1,82 @@
@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
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
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% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
@rem Execute Gradle
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%
+13
View File
@@ -0,0 +1,13 @@
pluginManagement {
repositories {
maven {
name = 'Fabric'
url = 'https://maven.fabricmc.net/'
}
mavenCentral()
gradlePluginPortal()
}
}
// Should match your modid
rootProject.name = 'needsofnature'
+192
View File
@@ -0,0 +1,192 @@
package com.nonid;
import com.nonid.data.NonEntityProfiles;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Item.Settings;
import net.minecraft.registry.Registries;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.text.Text;
import net.minecraft.util.ActionResult;
import net.minecraft.util.Formatting;
import net.minecraft.util.Hand;
import net.minecraft.util.Identifier;
public class DebugStaffItem extends Item {
public DebugStaffItem(Settings settings) {
super(settings);
}
public ActionResult useOnEntity(ItemStack stack, PlayerEntity user, LivingEntity entity, Hand hand) {
if (entity instanceof EnergyHolder holder) {
if (user.getEntityWorld().isClient()) {
return ActionResult.SUCCESS;
} else {
ServerPlayerEntity serverPlayer = (ServerPlayerEntity)user;
if (user.isSneaking()) {
int current = holder.getEnergy();
int max = holder.getMaxEnergy();
int next = Math.min(max, current + 25);
holder.setEnergy(next);
return ActionResult.SUCCESS;
} else {
this.sendDebugReport(serverPlayer, entity, holder);
return ActionResult.SUCCESS;
}
}
} else {
return ActionResult.PASS;
}
}
private void sendDebugReport(ServerPlayerEntity player, LivingEntity entity, EnergyHolder holder) {
Identifier entityId = Registries.ENTITY_TYPE.getId(entity.getType());
player.sendMessage(Text.literal("[NoN 调试] " + entity.getDisplayName().getString() + " (" + entityId + ")").formatted(Formatting.GREEN), false);
player.sendMessage(Text.literal("UUID: " + entity.getUuid()).formatted(Formatting.GRAY), false);
player.sendMessage(Text.literal("性别: " + this.resolveGender(entity)).formatted(Formatting.GRAY), false);
player.sendMessage(
Text.literal("能量: " + holder.getEnergy() + "/" + holder.getMaxEnergy() + " | 已初始化: " + holder.isEnergyInitialized())
.formatted(Formatting.GRAY),
false
);
player.sendMessage(
Text.literal(
"能量增益: 倍率 "
+ this.format(holder.getEnergyGainMultiplier())
+ " | 漂移 "
+ this.format(holder.getEnergyGainDrift())
+ " | 实体 "
+ this.format(NonEntityProfiles.resolveEnergyGainMultiplier(entity))
)
.formatted(Formatting.GRAY),
false
);
if (entity.getEntityWorld() instanceof ServerWorld world) {
boolean activeOrPending = NeedsOfNature.isActorPendingOrActive(world, entity.getUuid());
player.sendMessage(Text.literal("动画: 活跃/待定 " + activeOrPending).formatted(Formatting.GRAY), false);
this.sendAuraReport(player, world, entity, holder);
this.sendCooldownReport(player, world, entity, holder);
}
if (entity instanceof LiquidHolder liquidHolder) {
Identifier liquidType = liquidHolder.getLiquidEntityTypeId();
String liquidInfo = liquidHolder.getLiquidStored()
+ "/"
+ liquidHolder.getLiquidCapacity()
+ "ml | composition "
+ liquidHolder.getLiquidComposition()
+ (liquidType == null ? "" : " | 实体 " + liquidType);
if (entity instanceof ServerPlayerEntity) {
liquidInfo = liquidInfo + " | 槽类型 " + NonGenderSystem.getLiquidTankType(entity);
}
player.sendMessage(Text.literal("液体: " + liquidInfo).formatted(Formatting.GRAY), false);
}
if (entity instanceof PregnancyHolder pregnancyHolder) {
Identifier pregnancyType = pregnancyHolder.getPregnantEntityTypeId();
String pregnancyInfo = pregnancyType == null
? "none"
: pregnancyType + " | 结束tick " + pregnancyHolder.getPregnancyEndTick() + " | 待孵化 " + pregnancyHolder.isPregnancyPendingHatch();
player.sendMessage(Text.literal("孕期: " + pregnancyInfo).formatted(Formatting.GRAY), false);
}
}
private void sendAuraReport(ServerPlayerEntity player, ServerWorld world, LivingEntity entity, EnergyHolder holder) {
if (entity instanceof MobEntity mob) {
NeedsOfNature.PlayerEnergyAuraDebug aura = NeedsOfNature.getPlayerEnergyAuraDebug(world, mob, holder);
if (aura == null) {
player.sendMessage(Text.literal("光环: 无 | 携带 " + this.format(holder.getEnergyAuraCarry())).formatted(Formatting.GRAY), false);
} else {
String sourceName = aura.player().getName().getString();
player.sendMessage(
Text.literal(
"光环: "
+ this.format(aura.totalMultiplier())
+ "x 来自 "
+ sourceName
+ " | 能量 "
+ aura.playerEnergy()
+ " | 破坏 "
+ aura.destroyedSkinStage()
+ " | 污渍 "
+ aura.messTotal()
+ " | 半径 "
+ this.format(aura.radius())
+ " | 携带 "
+ this.format(holder.getEnergyAuraCarry())
)
.formatted(Formatting.GRAY),
false
);
}
} else {
player.sendMessage(Text.literal("光环: 无").formatted(Formatting.GRAY), false);
}
}
private void sendCooldownReport(ServerPlayerEntity player, ServerWorld world, LivingEntity entity, EnergyHolder holder) {
List<String> cooldowns = new ArrayList<>();
String attackFailsafe = NeedsOfNature.getMobAttackFailsafeDebug(world, entity.getUuid());
if (attackFailsafe != null && !attackFailsafe.isBlank()) {
cooldowns.add(attackFailsafe);
}
cooldowns.addAll(holder.getActiveCooldownDebugLines(world));
if (cooldowns.isEmpty()) {
player.sendMessage(Text.literal("冷却: 无").formatted(Formatting.GRAY), false);
} else {
player.sendMessage(Text.literal("冷却:").formatted(Formatting.GRAY), false);
for (String line : cooldowns) {
player.sendMessage(Text.literal("- " + line).formatted(Formatting.DARK_GRAY), false);
}
}
}
private String resolveGender(LivingEntity entity) {
if (entity instanceof GenderHolder genderHolder) {
return this.formatGenderMask(genderHolder.getGenderMask());
} else {
boolean hasMale = entity.getCommandTags().contains("gender.male");
boolean hasFemale = entity.getCommandTags().contains("gender.female");
int mask = 0;
if (hasMale) {
mask |= 1;
}
if (hasFemale) {
mask |= 2;
}
return this.formatGenderMask(mask);
}
}
private String formatGenderMask(int mask) {
boolean male = (mask & 1) != 0;
boolean female = (mask & 2) != 0;
if (male && female) {
return "male & female";
} else if (male) {
return "male";
} else {
return female ? "female" : "none";
}
}
private String format(float value) {
return String.format(Locale.ROOT, "%.2f", value);
}
private String format(double value) {
return String.format(Locale.ROOT, "%.2f", value);
}
}
@@ -0,0 +1,11 @@
package com.nonid;
public interface DestroyedSkinHolder {
int getDestroyedSkinStage();
void setDestroyedSkinStage(int var1);
int getDestroyedSkinDamage();
void setDestroyedSkinDamage(int var1);
}
+62
View File
@@ -0,0 +1,62 @@
package com.nonid;
import java.util.List;
import java.util.UUID;
import net.minecraft.entity.LivingEntity;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
public interface EnergyHolder {
int getEnergy();
int getMaxEnergy();
void setEnergy(int var1);
default float getEnergyGainMultiplier() {
return 1.0F;
}
default void setEnergyGainMultiplier(float value) {
}
default float getEnergyGainDrift() {
return 1.0F;
}
default void setEnergyGainDrift(float value) {
}
default boolean isEnergyInitialized() {
return false;
}
default void markEnergyInitialized() {
}
default void ensureEnergyInitialized(ServerWorld world) {
}
default void onNonAnimationStarted() {
}
default void onNonAttackEscaped(LivingEntity player, int retryDelayTicks) {
}
default void requestAttackOnPlayer(ServerWorld world, ServerPlayerEntity player) {
}
default void applyPartnerCooldown(UUID partnerId, long nowTick, int cooldownTicks) {
}
default List<String> getActiveCooldownDebugLines(ServerWorld world) {
return List.of();
}
default double getEnergyAuraCarry() {
return 0.0;
}
default void setEnergyAuraCarry(double value) {
}
}
+16
View File
@@ -0,0 +1,16 @@
package com.nonid;
public interface GenderHolder {
int GENDER_NONE = 0;
int GENDER_MALE = 1;
int GENDER_FEMALE = 2;
int GENDER_BOTH = 3;
int getGenderMask();
void setGenderMask(int var1);
default boolean hasGender(int mask) {
return (this.getGenderMask() & mask) != 0;
}
}
@@ -0,0 +1,110 @@
package com.nonid;
import com.google.gson.Gson;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
public final class LiquidDonorHistory {
public static final int MAX_ENTRIES = 3;
private static final Gson GSON = new Gson();
private LiquidDonorHistory() {
}
public static List<LiquidDonorHistory.Entry> append(
Collection<LiquidDonorHistory.Entry> current, Identifier entityTypeId, @Nullable NeedsOfNature.PregnancyVariantData variantData
) {
ArrayList<LiquidDonorHistory.Entry> entries = new ArrayList<>(normalize(current));
entries.add(new LiquidDonorHistory.Entry(entityTypeId, variantData));
return keepNewest(entries);
}
public static List<LiquidDonorHistory.Entry> normalize(@Nullable Collection<LiquidDonorHistory.Entry> entries) {
if (entries != null && !entries.isEmpty()) {
ArrayList<LiquidDonorHistory.Entry> normalized = new ArrayList<>();
for (LiquidDonorHistory.Entry entry : entries) {
if (entry != null && entry.entityTypeId() != null) {
normalized.add(new LiquidDonorHistory.Entry(entry.entityTypeId(), entry.variantData()));
}
}
return keepNewest(normalized);
} else {
return List.of();
}
}
@Nullable
public static String encode(Collection<LiquidDonorHistory.Entry> entries) {
List<LiquidDonorHistory.Entry> normalized = normalize(entries);
if (normalized.isEmpty()) {
return null;
}
LiquidDonorHistory.StoredEntry[] stored = normalized.stream()
.map(entry -> new LiquidDonorHistory.StoredEntry(entry.entityTypeId().toString(), entry.variantData()))
.toArray(LiquidDonorHistory.StoredEntry[]::new);
return GSON.toJson(stored);
}
public static List<LiquidDonorHistory.Entry> decode(@Nullable String raw) {
if (raw != null && !raw.isBlank()) {
try {
LiquidDonorHistory.StoredEntry[] stored = (LiquidDonorHistory.StoredEntry[])GSON.fromJson(raw, LiquidDonorHistory.StoredEntry[].class);
if (stored != null && stored.length != 0) {
ArrayList<LiquidDonorHistory.Entry> decoded = new ArrayList<>();
for (LiquidDonorHistory.StoredEntry entry : stored) {
if (entry != null && entry.entityTypeId() != null) {
Identifier entityTypeId = Identifier.tryParse(entry.entityTypeId());
if (entityTypeId != null) {
decoded.add(new LiquidDonorHistory.Entry(entityTypeId, entry.variantData()));
}
}
}
return keepNewest(decoded);
} else {
return List.of();
}
} catch (RuntimeException ignored) {
return List.of();
}
} else {
return List.of();
}
}
private static List<LiquidDonorHistory.Entry> keepNewest(List<LiquidDonorHistory.Entry> entries) {
if (entries != null && !entries.isEmpty()) {
int fromIndex = Math.max(0, entries.size() - 3);
return List.copyOf(entries.subList(fromIndex, entries.size()));
} else {
return List.of();
}
}
@Nullable
private static NeedsOfNature.PregnancyVariantData normalizeVariantData(@Nullable NeedsOfNature.PregnancyVariantData variantData) {
return variantData != null && !variantData.isEmpty()
? new NeedsOfNature.PregnancyVariantData(variantData.components(), variantData.horseMarkingIndex())
: null;
}
public record Entry(Identifier entityTypeId, @Nullable NeedsOfNature.PregnancyVariantData variantData) {
public Entry {
if (entityTypeId == null) {
throw new IllegalArgumentException("entityTypeId cannot be null");
}
variantData = LiquidDonorHistory.normalizeVariantData(variantData);
}
}
private record StoredEntry(String entityTypeId, @Nullable NeedsOfNature.PregnancyVariantData variantData) {
}
}
+55
View File
@@ -0,0 +1,55 @@
package com.nonid;
import java.util.List;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
public interface LiquidHolder {
int getLiquidStored();
int getLiquidCapacity();
void setLiquidStored(int var1);
LiquidHolder.LiquidComposition getLiquidComposition();
@Nullable
Identifier getLiquidEntityTypeId();
@Nullable
NeedsOfNature.PregnancyVariantData getLiquidDonorVariantData();
void setLiquidCompositionMixed();
void setLiquidCompositionEntity(Identifier var1);
void setLiquidDonorVariantData(@Nullable NeedsOfNature.PregnancyVariantData var1);
List<LiquidDonorHistory.Entry> getRecentLiquidDonors();
void setRecentLiquidDonors(List<LiquidDonorHistory.Entry> var1);
default void recordRecentLiquidDonor(Identifier entityTypeId, @Nullable NeedsOfNature.PregnancyVariantData variantData) {
if (entityTypeId != null) {
this.setRecentLiquidDonors(LiquidDonorHistory.append(this.getRecentLiquidDonors(), entityTypeId, variantData));
}
}
default void clearRecentLiquidDonors() {
this.setRecentLiquidDonors(List.of());
}
void clearLiquidComposition();
default void addLiquidStored(int amount) {
if (amount != 0) {
this.setLiquidStored(this.getLiquidStored() + amount);
}
}
enum LiquidComposition {
EMPTY,
MIXED,
ENTITY;
}
}
+15
View File
@@ -0,0 +1,15 @@
package com.nonid;
public interface MessHolder {
int getVMess();
void setVMess(int var1);
int getAMess();
void setAMess(int var1);
int getMMess();
void setMMess(int var1);
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,79 @@
package com.nonid;
public record NonAccessoryEffects(
double liquidDecayMultiplier,
boolean equalizeLiquidDecayContext,
double playerEnergyGainMultiplier,
int liquidCapacityAdd,
double liquidGainMultiplier,
double filledEffectMultiplier,
double pregnancyChanceMultiplier,
double pregnancyDurationMultiplier,
double messGainMultiplier,
double destroyedSkinDamageMultiplier,
int attackEscapeHitsAdd,
double attackEscapeDamageMultiplier,
double playerEnergyAuraMultiplier,
double nearAnimationMobEnergyMultiplier
) {
public static final NonAccessoryEffects NEUTRAL = new NonAccessoryEffects(1.0, false, 1.0, 0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0, 1.0, 1.0, 1.0);
public NonAccessoryEffects combine(NonAccessoryEffects other) {
return other == null
? this
: new NonAccessoryEffects(
this.liquidDecayMultiplier * other.liquidDecayMultiplier,
this.equalizeLiquidDecayContext || other.equalizeLiquidDecayContext,
this.playerEnergyGainMultiplier * other.playerEnergyGainMultiplier,
this.liquidCapacityAdd + other.liquidCapacityAdd,
this.liquidGainMultiplier * other.liquidGainMultiplier,
this.filledEffectMultiplier * other.filledEffectMultiplier,
this.pregnancyChanceMultiplier * other.pregnancyChanceMultiplier,
this.pregnancyDurationMultiplier * other.pregnancyDurationMultiplier,
this.messGainMultiplier * other.messGainMultiplier,
this.destroyedSkinDamageMultiplier * other.destroyedSkinDamageMultiplier,
this.attackEscapeHitsAdd + other.attackEscapeHitsAdd,
this.attackEscapeDamageMultiplier * other.attackEscapeDamageMultiplier,
this.playerEnergyAuraMultiplier * other.playerEnergyAuraMultiplier,
this.nearAnimationMobEnergyMultiplier * other.nearAnimationMobEnergyMultiplier
);
}
public NonAccessoryEffects globalOnly() {
return new NonAccessoryEffects(
1.0,
false,
this.playerEnergyGainMultiplier,
0,
1.0,
1.0,
1.0,
1.0,
1.0,
this.destroyedSkinDamageMultiplier,
this.attackEscapeHitsAdd,
this.attackEscapeDamageMultiplier,
this.playerEnergyAuraMultiplier,
this.nearAnimationMobEnergyMultiplier
);
}
public NonAccessoryEffects tankScopedOnly() {
return new NonAccessoryEffects(
this.liquidDecayMultiplier,
this.equalizeLiquidDecayContext,
1.0,
this.liquidCapacityAdd,
this.liquidGainMultiplier,
this.filledEffectMultiplier,
this.pregnancyChanceMultiplier,
this.pregnancyDurationMultiplier,
this.messGainMultiplier,
1.0,
0,
1.0,
1.0,
1.0
);
}
}
@@ -0,0 +1,197 @@
package com.nonid;
import com.nonid.data.NonAccessoryDefinitions;
import com.nonid.data.NonLiquidRoles;
import com.nonid.internal.animation.api.AnimationService;
import com.nonid.internal.animation.data.AnimationGameplayMetadata;
import com.nonid.network.AccessoryShedStateS2CPayload;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.Identifier;
public final class NonAccessoryShedSystem {
private static final String V_SLOT = "legs/v";
private static final String A_SLOT = "legs/a";
private static final Map<UUID, NonAccessoryShedSystem.ShedState> SHED_STATES = new ConcurrentHashMap<>();
private NonAccessoryShedSystem() {
}
public static NonAccessoryShedSystem.ShedState resolveShedStateForAnimation(Identifier animationId) {
NonLiquidRoles.LiquidRoles roles = AnimationGameplayMetadata.liquidRoles(animationId);
if (roles != null && !roles.injectorRoles().isEmpty()) {
boolean shedV = false;
boolean shedA = false;
for (NonLiquidRoles.InjectorRole role : roles.injectorRoles().values()) {
if (role == NonLiquidRoles.InjectorRole.V) {
shedV = true;
} else if (role == NonLiquidRoles.InjectorRole.A) {
shedA = true;
}
}
return new NonAccessoryShedSystem.ShedState(shedV, shedA, false, false);
} else {
return NonAccessoryShedSystem.ShedState.NONE;
}
}
public static NonAccessoryShedSystem.ShedState forSlot(String slotId) {
if (slotId == null) {
return NonAccessoryShedSystem.ShedState.NONE;
} else {
String slot = slotId.toLowerCase(Locale.ROOT);
if (slot.equals("legs/v") || slot.startsWith("legs/v/")) {
return new NonAccessoryShedSystem.ShedState(true, false, true, false);
} else {
return !slot.equals("legs/a") && !slot.startsWith("legs/a/")
? NonAccessoryShedSystem.ShedState.NONE
: new NonAccessoryShedSystem.ShedState(false, true, false, true);
}
}
}
public static void addServerShedState(ServerWorld world, ServerPlayerEntity player, NonAccessoryShedSystem.ShedState state) {
if (world != null && player != null && state != null && !state.isEmpty()) {
UUID uuid = player.getUuid();
NonAccessoryShedSystem.ShedState merged = SHED_STATES.getOrDefault(uuid, NonAccessoryShedSystem.ShedState.NONE).merge(state);
SHED_STATES.put(uuid, merged);
syncToWorld(world, uuid, merged);
NonTrinketsIntegration.forceAccessorySync(player);
}
}
public static void clearServerShedState(ServerWorld world, UUID playerUuid) {
if (world != null && playerUuid != null) {
NonAccessoryShedSystem.ShedState removed = SHED_STATES.remove(playerUuid);
if (removed != null && !removed.isEmpty()) {
syncToWorld(world, playerUuid, NonAccessoryShedSystem.ShedState.NONE);
if (world.getEntity(playerUuid) instanceof ServerPlayerEntity player) {
NonTrinketsIntegration.forceAccessorySync(player);
}
}
}
}
public static void syncAllToViewer(ServerPlayerEntity viewer) {
if (viewer != null && !SHED_STATES.isEmpty()) {
for (Entry<UUID, NonAccessoryShedSystem.ShedState> entry : SHED_STATES.entrySet()) {
NonAccessoryShedSystem.ShedState state = entry.getValue();
if (state != null && !state.isEmpty()) {
ServerPlayNetworking.send(
viewer, new AccessoryShedStateS2CPayload(entry.getKey(), state.shedV(), state.shedA(), state.forcedV(), state.forcedA())
);
}
}
}
}
public static void setClientShedState(UUID playerUuid, boolean shedV, boolean shedA, boolean forcedV, boolean forcedA) {
if (playerUuid != null) {
NonAccessoryShedSystem.ShedState state = new NonAccessoryShedSystem.ShedState(shedV, shedA, forcedV, forcedA);
if (state.isEmpty()) {
SHED_STATES.remove(playerUuid);
} else {
SHED_STATES.put(playerUuid, state);
}
}
}
public static void clearAll() {
SHED_STATES.clear();
}
public static void tickServerWorld(ServerWorld world) {
if (world != null && !SHED_STATES.isEmpty()) {
for (UUID playerUuid : List.copyOf(SHED_STATES.keySet())) {
if (world.getEntity(playerUuid) instanceof ServerPlayerEntity player && !AnimationService.isActorPendingOrActive(world, playerUuid)) {
clearServerShedState(world, player.getUuid());
}
}
}
}
public static NonAccessoryShedSystem.ShedState getShedState(UUID playerUuid) {
return playerUuid == null ? NonAccessoryShedSystem.ShedState.NONE : SHED_STATES.getOrDefault(playerUuid, NonAccessoryShedSystem.ShedState.NONE);
}
public static boolean isSlotShed(UUID playerUuid, String slotId) {
return getShedState(playerUuid).isSlotShed(slotId);
}
public static boolean isSlotVisuallyShed(UUID playerUuid, String slotId, Identifier itemId) {
NonAccessoryShedSystem.ShedState state = getShedState(playerUuid);
if (!state.isSlotShed(slotId)) {
return false;
} else {
return state.isSlotForcedShed(slotId) ? true : !NonAccessoryDefinitions.getBehavior(itemId).ignoreInjectorSlotVisualShedding();
}
}
public static boolean isSlotEffectSuppressed(UUID playerUuid, String slotId, Identifier itemId) {
NonAccessoryShedSystem.ShedState state = getShedState(playerUuid);
if (!state.isSlotShed(slotId)) {
return false;
} else {
return state.isSlotForcedShed(slotId) ? true : !NonAccessoryDefinitions.getBehavior(itemId).ignoreInjectorSlotEffectSuppression();
}
}
private static void syncToWorld(ServerWorld world, UUID playerUuid, NonAccessoryShedSystem.ShedState state) {
AccessoryShedStateS2CPayload payload = new AccessoryShedStateS2CPayload(playerUuid, state.shedV(), state.shedA(), state.forcedV(), state.forcedA());
for (ServerPlayerEntity viewer : world.getPlayers()) {
ServerPlayNetworking.send(viewer, payload);
}
}
public record ShedState(boolean shedV, boolean shedA, boolean forcedV, boolean forcedA) {
public static final NonAccessoryShedSystem.ShedState NONE = new NonAccessoryShedSystem.ShedState(false, false, false, false);
public boolean isEmpty() {
return !this.shedV && !this.shedA;
}
public NonAccessoryShedSystem.ShedState merge(NonAccessoryShedSystem.ShedState other) {
return other != null && !other.isEmpty()
? new NonAccessoryShedSystem.ShedState(
this.shedV || other.shedV, this.shedA || other.shedA, this.forcedV || other.forcedV, this.forcedA || other.forcedA
)
: this;
}
public boolean isSlotShed(String slotId) {
if (slotId == null) {
return false;
} else {
String slot = slotId.toLowerCase(Locale.ROOT);
if (slot.equals("legs/v") || slot.startsWith("legs/v/")) {
return this.shedV;
} else {
return !slot.equals("legs/a") && !slot.startsWith("legs/a/") ? false : this.shedA;
}
}
}
public boolean isSlotForcedShed(String slotId) {
if (slotId == null) {
return false;
} else {
String slot = slotId.toLowerCase(Locale.ROOT);
if (slot.equals("legs/v") || slot.startsWith("legs/v/")) {
return this.forcedV;
} else {
return !slot.equals("legs/a") && !slot.startsWith("legs/a/") ? false : this.forcedA;
}
}
}
}
}
@@ -0,0 +1,208 @@
package com.nonid;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.advancement.AdvancementEntry;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.Identifier;
final class NonAdvancementHooks {
private static final int MOTHER_NATURE_PREGNANCIES = 25;
private static final int COLLECTORS_HABIT_BOTTLES = 25;
private static final int FIELD_RESEARCH_TYPES = 5;
private static final int TOSSED_AROUND_CHAIN = 5;
private static final int STABLE_DIET_CHAIN = 3;
private static final String ROOT = "root";
private static final String NATURAL_CURIOSITY = "natural_curiosity";
private static final String FILLED_TO_THE_BRIM = "filled_to_the_brim";
private static final String FAVOURITISM = "favouritism";
private static final String FIRST_PREGNANCY = "first_pregnancy";
private static final String MOTHER_NATURE = "mother_nature";
private static final String MESSED_UP = "messed_up";
private static final String GROUP_ACTIVITY = "group_activity";
private static final String TOSSED_AROUND = "tossed_around";
private static final String COLLECTORS_HABIT = "collectors_habit";
private static final String FIELD_RESEARCH = "field_research";
private static final String BAD_IDEA_BEACON = "bad_idea_beacon";
private static final String PRODUCTIVE_PREGNANCY = "productive_pregnancy";
private static final String EMERGENCY_SNACK = "emergency_snack";
private static final String STABLE_DIET = "stable_diet";
private static final String UDDERLY_UNFORTUNATE = "udderly_unfortunate";
private static final String ACQUIRE_HARDWARE = "acquire_hardware";
private static final Map<UUID, NonAdvancementHooks.FieldResearchState> FIELD_RESEARCH_STATES = new ConcurrentHashMap<>();
private static final Map<UUID, Integer> TOSSED_AROUND_STREAKS = new ConcurrentHashMap<>();
private static final Map<UUID, Integer> STABLE_DIET_STREAKS = new ConcurrentHashMap<>();
private NonAdvancementHooks() {
}
static void clearRuntimeState() {
FIELD_RESEARCH_STATES.clear();
TOSSED_AROUND_STREAKS.clear();
STABLE_DIET_STREAKS.clear();
}
static void clearPlayerState(UUID playerUuid) {
if (playerUuid != null) {
FIELD_RESEARCH_STATES.remove(playerUuid);
TOSSED_AROUND_STREAKS.remove(playerUuid);
STABLE_DIET_STREAKS.remove(playerUuid);
}
}
static void grant(ServerPlayerEntity player, String path) {
if (player != null && path != null && !path.isBlank()) {
if (player.getEntityWorld() instanceof ServerWorld world) {
MinecraftServer server = world.getServer();
if (server != null) {
if (!"root".equals(path)) {
grant(player, "root");
}
AdvancementEntry advancement = server.getAdvancementLoader().get(NeedsOfNature.id(path));
if (advancement != null) {
player.getAdvancementTracker().grantCriterion(advancement, "done");
}
}
}
}
}
static void grantEmergencySnack(ServerPlayerEntity player) {
grant(player, "emergency_snack");
recordStableDietSnack(player);
}
static void resetStableDietStreak(ServerPlayerEntity player) {
if (player != null) {
STABLE_DIET_STREAKS.remove(player.getUuid());
}
}
private static void recordStableDietSnack(ServerPlayerEntity player) {
if (player != null) {
int streak = STABLE_DIET_STREAKS.merge(player.getUuid(), 1, Integer::sum);
if (streak >= 3) {
grant(player, "stable_diet");
}
}
}
static void checkLiquidAdvancements(ServerPlayerEntity player, LiquidHolder holder) {
if (player != null && holder != null) {
int capacity = holder.getLiquidCapacity();
if (capacity > 0 && holder.getLiquidStored() >= capacity) {
grant(player, "filled_to_the_brim");
if (holder.getLiquidComposition() == LiquidHolder.LiquidComposition.ENTITY && holder.getLiquidEntityTypeId() != null) {
grant(player, "favouritism");
}
}
}
}
static void trackFieldResearch(ServerWorld world, ServerPlayerEntity player, Map<Identifier, Integer> perEntityAmounts) {
if (world != null && player != null && perEntityAmounts != null && !perEntityAmounts.isEmpty()) {
Set<Identifier> gainedTypes = new LinkedHashSet<>();
for (Entry<Identifier, Integer> entry : perEntityAmounts.entrySet()) {
if (entry.getKey() != null && entry.getValue() != null && entry.getValue() > 0) {
gainedTypes.add(entry.getKey());
}
}
if (!gainedTypes.isEmpty()) {
long day = world.getTimeOfDay() / 24000L;
UUID playerUuid = player.getUuid();
NonAdvancementHooks.FieldResearchState existing = FIELD_RESEARCH_STATES.get(playerUuid);
Set<Identifier> entityTypes = new LinkedHashSet<>();
if (existing != null && existing.day() == day) {
entityTypes.addAll(existing.entityTypes());
}
entityTypes.addAll(gainedTypes);
FIELD_RESEARCH_STATES.put(playerUuid, new NonAdvancementHooks.FieldResearchState(day, Set.copyOf(entityTypes)));
if (entityTypes.size() >= 5) {
grant(player, "field_research");
}
}
}
}
static void checkBadIdeaBeacon(ServerPlayerEntity player) {
if (player != null && player instanceof EnergyHolder energyHolder) {
if (energyHolder.getEnergy() >= 200) {
if (NeedsOfNature.getDestroyedSkinStageForPlayer(player) >= 4) {
grant(player, "bad_idea_beacon");
}
}
}
}
static void grantNaturalCuriosity(ServerPlayerEntity player) {
grant(player, "natural_curiosity");
}
static void grantGroupActivity(ServerPlayerEntity player) {
grant(player, "group_activity");
}
static void grantMessedUp(ServerPlayerEntity player) {
grant(player, "messed_up");
}
static void checkCollectorsHabit(ServerPlayerEntity player) {
if (player != null) {
if (NonStats.getBottlesFilled(player) >= 25) {
grant(player, "collectors_habit");
}
}
}
static void resetTossedAround(ServerPlayerEntity player) {
if (player != null) {
TOSSED_AROUND_STREAKS.remove(player.getUuid());
}
}
static void recordTossedAround(ServerPlayerEntity player) {
if (player != null) {
int streak = TOSSED_AROUND_STREAKS.merge(player.getUuid(), 1, Integer::sum);
if (streak >= 5) {
grant(player, "tossed_around");
}
}
}
static void grantFirstPregnancy(ServerPlayerEntity player) {
grant(player, "first_pregnancy");
}
static void checkMotherNature(ServerPlayerEntity player) {
if (player != null) {
if (NonStats.getPregnanciesCompleted(player) >= 25) {
grant(player, "mother_nature");
}
}
}
static void grantProductivePregnancy(ServerPlayerEntity player) {
grant(player, "productive_pregnancy");
}
static void grantUdderlyUnfortunate(ServerPlayerEntity player) {
grant(player, "udderly_unfortunate");
}
static void grantAcquireHardware(ServerPlayerEntity player) {
grant(player, "acquire_hardware");
}
private record FieldResearchState(long day, Set<Identifier> entityTypes) {
}
}
@@ -0,0 +1,181 @@
package com.nonid;
import com.nonid.internal.animation.api.AnimationService;
import java.util.List;
import java.util.Set;
import net.minecraft.entity.Entity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
public final class NonAnimationMatcher {
private NonAnimationMatcher() {
}
@Nullable
public static AnimationService.MatchedAnimation findAutomatic(
ServerWorld world,
List<? extends Entity> actors,
@Nullable Set<String> requiredTags,
@Nullable Entity positionAnchor,
boolean requireStartEligibility,
@Nullable Entity requiredActiveActor,
@Nullable Set<Identifier> excludedAnimationIds
) {
return findAutomatic(world, actors, requiredTags, positionAnchor, requireStartEligibility, requiredActiveActor, excludedAnimationIds, null, null, false);
}
@Nullable
public static AnimationService.MatchedAnimation findAutomatic(
ServerWorld world,
List<? extends Entity> actors,
@Nullable Set<String> requiredTags,
@Nullable Entity positionAnchor,
boolean requireStartEligibility,
@Nullable Entity requiredActiveActor,
@Nullable Set<Identifier> excludedAnimationIds,
@Nullable AnimationService.AnimationCandidateFilter additionalFilter,
@Nullable Identifier joinPredecessorAnimationId
) {
return findAutomatic(
world,
actors,
requiredTags,
positionAnchor,
requireStartEligibility,
requiredActiveActor,
excludedAnimationIds,
additionalFilter,
joinPredecessorAnimationId,
false
);
}
@Nullable
public static AnimationService.MatchedAnimation findAutomaticAttack(
ServerWorld world,
List<? extends Entity> actors,
@Nullable Set<String> requiredTags,
@Nullable Entity positionAnchor,
boolean requireStartEligibility,
@Nullable Entity requiredActiveActor,
@Nullable Set<Identifier> excludedAnimationIds
) {
return findAutomatic(world, actors, requiredTags, positionAnchor, requireStartEligibility, requiredActiveActor, excludedAnimationIds, null, null, true);
}
@Nullable
public static AnimationService.MatchedAnimation findAutomaticAttack(
ServerWorld world,
List<? extends Entity> actors,
@Nullable Set<String> requiredTags,
@Nullable Entity positionAnchor,
boolean requireStartEligibility,
@Nullable Entity requiredActiveActor,
@Nullable Set<Identifier> excludedAnimationIds,
@Nullable AnimationService.AnimationCandidateFilter additionalFilter,
@Nullable Identifier joinPredecessorAnimationId
) {
return findAutomatic(
world,
actors,
requiredTags,
positionAnchor,
requireStartEligibility,
requiredActiveActor,
excludedAnimationIds,
additionalFilter,
joinPredecessorAnimationId,
true
);
}
@Nullable
private static AnimationService.MatchedAnimation findAutomatic(
ServerWorld world,
List<? extends Entity> actors,
@Nullable Set<String> requiredTags,
@Nullable Entity positionAnchor,
boolean requireStartEligibility,
@Nullable Entity requiredActiveActor,
@Nullable Set<Identifier> excludedAnimationIds,
@Nullable AnimationService.AnimationCandidateFilter additionalFilter,
@Nullable Identifier joinPredecessorAnimationId,
boolean attackSelection
) {
AnimationService.AnimationCandidateFilter filter = (candidateWorld, definition, candidateActors) -> (!attackSelection || definition.attackEligible())
&& NonInjectorMatchPolicy.allowsAutomaticNonMatch(candidateWorld, definition, candidateActors)
&& (additionalFilter == null || additionalFilter.test(candidateWorld, definition, candidateActors));
return AnimationService.findAnimationForActors(
world,
actors,
new AnimationService.MatchOptions(
requiredTags == null ? Set.of() : requiredTags,
positionAnchor,
requireStartEligibility,
requiredActiveActor,
excludedAnimationIds == null ? Set.of() : excludedAnimationIds,
filter,
joinPredecessorAnimationId
)
);
}
public static List<AnimationService.EligibleAnimation> findEligibleAutomatic(
ServerWorld world,
List<? extends Entity> actors,
@Nullable Set<String> requiredTags,
@Nullable Entity positionAnchor,
boolean requireStartEligibility,
@Nullable Entity requiredActiveActor,
@Nullable Set<Identifier> excludedAnimationIds,
@Nullable Identifier joinPredecessorAnimationId
) {
return findEligibleAutomatic(
world, actors, requiredTags, positionAnchor, requireStartEligibility, requiredActiveActor, excludedAnimationIds, joinPredecessorAnimationId, false
);
}
public static List<AnimationService.EligibleAnimation> findEligibleAutomaticAttack(
ServerWorld world,
List<? extends Entity> actors,
@Nullable Set<String> requiredTags,
@Nullable Entity positionAnchor,
boolean requireStartEligibility,
@Nullable Entity requiredActiveActor,
@Nullable Set<Identifier> excludedAnimationIds,
@Nullable Identifier joinPredecessorAnimationId
) {
return findEligibleAutomatic(
world, actors, requiredTags, positionAnchor, requireStartEligibility, requiredActiveActor, excludedAnimationIds, joinPredecessorAnimationId, true
);
}
private static List<AnimationService.EligibleAnimation> findEligibleAutomatic(
ServerWorld world,
List<? extends Entity> actors,
@Nullable Set<String> requiredTags,
@Nullable Entity positionAnchor,
boolean requireStartEligibility,
@Nullable Entity requiredActiveActor,
@Nullable Set<Identifier> excludedAnimationIds,
@Nullable Identifier joinPredecessorAnimationId,
boolean attackSelection
) {
AnimationService.AnimationCandidateFilter filter = (candidateWorld, definition, candidateActors) -> (!attackSelection || definition.attackEligible())
&& NonInjectorMatchPolicy.allowsAutomaticNonMatch(candidateWorld, definition, candidateActors);
return AnimationService.findEligibleAnimationsForActors(
world,
actors,
new AnimationService.MatchOptions(
requiredTags == null ? Set.of() : requiredTags,
positionAnchor,
requireStartEligibility,
requiredActiveActor,
excludedAnimationIds == null ? Set.of() : excludedAnimationIds,
filter,
joinPredecessorAnimationId
)
);
}
}
@@ -0,0 +1,92 @@
package com.nonid;
import com.nonid.internal.animation.server.ServerAnimationController;
import java.util.Comparator;
import java.util.UUID;
import net.fabricmc.fabric.api.event.player.AttackEntityCallback;
import net.minecraft.entity.Entity;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.damage.DamageSource;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.projectile.ProjectileEntity;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.ActionResult;
import net.minecraft.util.Hand;
import org.jetbrains.annotations.Nullable;
public final class NonAnimationRescueSystem {
private NonAnimationRescueSystem() {
}
static void register() {
AttackEntityCallback.EVENT
.register(
(AttackEntityCallback)(player, world, hand, entity, hitResult) -> {
if (hand == Hand.MAIN_HAND
&& world instanceof ServerWorld serverWorld
&& player instanceof ServerPlayerEntity attacker
&& entity instanceof ServerPlayerEntity animatedPlayer) {
MobEntity mob = findRescueTarget(serverWorld, attacker, animatedPlayer);
if (mob == null) {
return ActionResult.PASS;
}
attacker.attack(mob);
return ActionResult.SUCCESS;
} else {
return ActionResult.PASS;
}
}
);
}
public static Entity redirectProjectileTarget(ServerWorld world, ProjectileEntity projectile, Entity originalTarget) {
if (projectile.getOwner() instanceof ServerPlayerEntity attacker && originalTarget instanceof ServerPlayerEntity animatedPlayer) {
MobEntity mob = findRescueTarget(world, attacker, animatedPlayer);
return (Entity)(mob == null ? originalTarget : mob);
} else {
return originalTarget;
}
}
@Nullable
public static MobEntity findDirectDamageRedirectTarget(ServerWorld world, LivingEntity originalTarget, DamageSource source) {
if (originalTarget instanceof ServerPlayerEntity animatedPlayer && source != null && source.isDirect()) {
ServerPlayerEntity attacker;
if (source.getAttacker() instanceof ServerPlayerEntity attributedAttacker) {
attacker = attributedAttacker;
} else {
if (!(source.getSource() instanceof ServerPlayerEntity directAttacker)) {
return null;
}
attacker = directAttacker;
}
return findRescueTarget(world, attacker, animatedPlayer);
} else {
return null;
}
}
@Nullable
private static MobEntity findRescueTarget(ServerWorld world, ServerPlayerEntity attacker, ServerPlayerEntity animatedPlayer) {
UUID instanceId = ServerAnimationController.findActiveInstanceId(world, animatedPlayer.getUuid());
if (instanceId == null) {
return null;
}
ServerAnimationController.ActiveInstanceSnapshot snapshot = ServerAnimationController.getActiveInstanceSnapshot(world, instanceId);
return snapshot != null && !snapshot.actorUuids().contains(attacker.getUuid())
? snapshot.actorUuids()
.stream()
.<Entity>map(world::getEntity)
.filter(MobEntity.class::isInstance)
.map(MobEntity.class::cast)
.filter(Entity::isAlive)
.min(Comparator.<MobEntity>comparingDouble(mob -> attacker.squaredDistanceTo(mob)).thenComparingInt(Entity::getId))
.orElse(null)
: null;
}
}
@@ -0,0 +1,111 @@
package com.nonid;
import com.nonid.internal.animation.server.AnimationSessionData;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.UUID;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
public record NonAnimationSessionData(
NonAnimationSessionData.Mode mode,
@Nullable UUID breedingRequestId,
@Nullable NonAnimationSessionData.Pregnancy pregnancy,
@Nullable NonAnimationSessionData.PlayerAttack playerAttack
) implements AnimationSessionData {
public NonAnimationSessionData {
mode = mode == null ? NonAnimationSessionData.Mode.UNSPECIFIED : mode;
}
public static NonAnimationSessionData voluntary() {
return new NonAnimationSessionData(NonAnimationSessionData.Mode.VOLUNTARY, null, null, null);
}
public static NonAnimationSessionData attack() {
return new NonAnimationSessionData(NonAnimationSessionData.Mode.ATTACK, null, null, null);
}
public static NonAnimationSessionData breeding(UUID requestId) {
return new NonAnimationSessionData(NonAnimationSessionData.Mode.UNSPECIFIED, requestId, null, null);
}
public static NonAnimationSessionData pregnancy(NonAnimationSessionData.Pregnancy pregnancy) {
return new NonAnimationSessionData(NonAnimationSessionData.Mode.PREGNANCY, null, pregnancy, null);
}
public static NonAnimationSessionData playerAttack(NonAnimationSessionData.PlayerAttack attack) {
return new NonAnimationSessionData(NonAnimationSessionData.Mode.ATTACK, null, null, attack);
}
public NonAnimationSessionData withPlayerAttack(@Nullable NonAnimationSessionData.PlayerAttack attack) {
return new NonAnimationSessionData(NonAnimationSessionData.Mode.ATTACK, this.breedingRequestId, this.pregnancy, attack);
}
public boolean isAttack() {
return this.mode == NonAnimationSessionData.Mode.ATTACK;
}
public boolean isVoluntary() {
return this.mode == NonAnimationSessionData.Mode.VOLUNTARY;
}
public boolean isPregnancy() {
return this.mode == NonAnimationSessionData.Mode.PREGNANCY;
}
public enum Mode {
UNSPECIFIED,
VOLUNTARY,
ATTACK,
PREGNANCY;
}
public record PlayerAttack(UUID victimUuid, UUID finishOwnerUuid, UUID initiatorUuid, Set<UUID> attackerUuids) {
public PlayerAttack {
if (victimUuid != null && finishOwnerUuid != null && initiatorUuid != null) {
LinkedHashSet<UUID> attackers = new LinkedHashSet<>();
if (attackerUuids != null) {
attackers.addAll(attackerUuids);
}
attackers.remove(victimUuid);
attackers.add(initiatorUuid);
attackerUuids = Set.copyOf(attackers);
} else {
throw new IllegalArgumentException("Player attack participants must not be null");
}
}
public NonAnimationSessionData.PlayerAttack withAdditionalAttacker(UUID attackerUuid) {
LinkedHashSet<UUID> attackers = new LinkedHashSet<>(this.attackerUuids);
if (attackerUuid != null && !attackerUuid.equals(this.victimUuid)) {
attackers.add(attackerUuid);
}
return new NonAnimationSessionData.PlayerAttack(this.victimUuid, this.finishOwnerUuid, attackerUuid, attackers);
}
public NonAnimationSessionData.PlayerAttack forDefeatedTail(UUID victim) {
return new NonAnimationSessionData.PlayerAttack(victim, this.finishOwnerUuid, this.initiatorUuid, this.attackerUuids);
}
}
public record Pregnancy(
Identifier sourceEntityTypeId,
Identifier birthEntityTypeId,
String birthMode,
int offspringCount,
@Nullable NeedsOfNature.PregnancyVariantData variantData
) {
public Pregnancy {
if (sourceEntityTypeId == null) {
throw new IllegalArgumentException("sourceEntityTypeId must not be null");
}
birthEntityTypeId = birthEntityTypeId == null ? sourceEntityTypeId : birthEntityTypeId;
birthMode = birthMode == null ? "direct" : birthMode;
offspringCount = Math.max(1, offspringCount);
variantData = NeedsOfNature.normalizePregnancyVariantData(variantData);
}
}
}
@@ -0,0 +1,358 @@
package com.nonid;
import com.nonid.api.NonAccessoryEffectsSnapshot;
import com.nonid.api.NonChangeSource;
import com.nonid.api.NonDestroyedSkinSnapshot;
import com.nonid.api.NonEvents;
import com.nonid.api.NonGender;
import com.nonid.api.NonLiquidComposition;
import com.nonid.api.NonLiquidTankSnapshot;
import com.nonid.api.NonMessSnapshot;
import com.nonid.api.NonMessType;
import com.nonid.api.NonPlayerStateSnapshot;
import com.nonid.api.NonPregnancySnapshot;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
public final class NonApiInternals {
private static final int DESTROYED_SKIN_MAX_DAMAGE = 10;
private NonApiInternals() {
}
public static NonPlayerStateSnapshot snapshotPlayerState(PlayerEntity player) {
return new NonPlayerStateSnapshot(
getGender(player),
snapshotLiquid(player),
snapshotMess(player),
snapshotDestroyedSkin(player),
snapshotPregnancy(player),
snapshotAccessoryEffects(player)
);
}
public static NonGender getGender(PlayerEntity player) {
return player instanceof GenderHolder holder ? NonGender.fromMask(holder.getGenderMask()) : NonGender.NONE;
}
public static boolean setGender(ServerPlayerEntity player, NonGender gender, NonChangeSource source) {
if (player == null || gender == null || gender == NonGender.NONE) {
return false;
}
if (!canChangeGender(player, source)) {
return false;
}
int before = player instanceof GenderHolder holder ? holder.getGenderMask() & 3 : 0;
int applied = NonGenderSystem.setPlayerGenderFromCommand(player, gender.mask(), source);
return (before & 3) != (applied & 3);
}
public static boolean canChangeGender(ServerPlayerEntity player, NonChangeSource source) {
if (player != null && source != null) {
return switch (source) {
case API, COMMAND, ITEM, JOIN_SELECTION, SYSTEM -> true;
case CLIENT_PREFERENCE -> NonGenderSystem.canClientChangeGenderFreely();
};
} else {
return false;
}
}
public static NonLiquidTankSnapshot snapshotLiquid(PlayerEntity player) {
return player instanceof LiquidHolder holder
? new NonLiquidTankSnapshot(
Math.max(0, holder.getLiquidStored()),
Math.max(0, holder.getLiquidCapacity()),
toApiLiquidComposition(holder.getLiquidComposition()),
holder.getLiquidEntityTypeId(),
NonLiquidSystem.resolveLiquidTintRgb(holder)
)
: new NonLiquidTankSnapshot(0, 0, NonLiquidComposition.EMPTY, null, 16777215);
}
public static int addLiquid(ServerPlayerEntity player, @Nullable Identifier sourceEntityType, int amountMl) {
if (!NeedsOfNature.getConfig().isLiquidTankEnabled()) {
return 0;
}
if (player != null && player instanceof LiquidHolder holder && amountMl > 0) {
int modifiedAmount = ((NonEvents.ModifyLiquidGain)NonEvents.MODIFY_LIQUID_GAIN.invoker()).modifyLiquidGain(player, sourceEntityType, amountMl);
if (modifiedAmount <= 0) {
return 0;
}
int added = addLiquidWithComposition(holder, sourceEntityType, modifiedAmount);
if (added > 0) {
NonPregnancyConceptionSystem.recordRecentDonor(holder, sourceEntityType, null);
NeedsOfNature.syncLiquidState(player);
}
return added;
} else {
return 0;
}
}
public static int drainLiquid(ServerPlayerEntity player, int amountMl) {
if (player != null && player instanceof LiquidHolder holder && amountMl > 0) {
int before = Math.max(0, holder.getLiquidStored());
int removed = Math.min(before, amountMl);
if (removed <= 0) {
return 0;
}
int next = before - removed;
holder.setLiquidStored(next);
if (next <= 0) {
holder.clearLiquidComposition();
}
NeedsOfNature.syncLiquidState(player);
return removed;
} else {
return 0;
}
}
public static NonMessSnapshot snapshotMess(PlayerEntity player) {
return player instanceof MessHolder holder
? new NonMessSnapshot(
NonMessSystem.clampMess(holder.getVMess()), NonMessSystem.clampMess(holder.getAMess()), NonMessSystem.clampMess(holder.getMMess())
)
: new NonMessSnapshot(0, 0, 0);
}
public static boolean addMess(ServerPlayerEntity player, NonMessType type, int amount) {
return changeMess(player, type, Math.max(0, amount));
}
public static boolean cleanMess(ServerPlayerEntity player, NonMessType type, int amount) {
return changeMess(player, type, -Math.max(0, amount));
}
public static boolean clearMess(ServerPlayerEntity player) {
if (player != null && player instanceof MessHolder holder) {
boolean changed = holder.getVMess() != 0 || holder.getAMess() != 0 || holder.getMMess() != 0;
NonMessSystem.clearMess(player);
return changed;
} else {
return false;
}
}
public static NonDestroyedSkinSnapshot snapshotDestroyedSkin(PlayerEntity player) {
return player instanceof DestroyedSkinHolder holder
? new NonDestroyedSkinSnapshot(
Math.max(0, Math.min(4, holder.getDestroyedSkinStage())), Math.max(0, Math.min(10, holder.getDestroyedSkinDamage())), 10
)
: new NonDestroyedSkinSnapshot(0, 0, 10);
}
public static boolean damageDestroyedSkin(ServerPlayerEntity player, int amount) {
return changeDestroyedSkinDamage(player, Math.max(0, amount));
}
public static boolean repairDestroyedSkin(ServerPlayerEntity player, int amount) {
return changeDestroyedSkinDamage(player, -Math.max(0, amount));
}
public static NonPregnancySnapshot snapshotPregnancy(PlayerEntity player) {
return player instanceof PregnancyHolder holder
? new NonPregnancySnapshot(holder.getPregnantEntityTypeId(), holder.getPregnancyEndTick(), holder.isPregnancyPendingHatch())
: new NonPregnancySnapshot(null, 0L, false);
}
public static boolean beginPregnancy(ServerPlayerEntity player, Identifier entityTypeId, long endTick) {
if (!NeedsOfNature.getConfig().isPregnancyEnabled()) {
return false;
} else if (player != null && entityTypeId != null) {
NeedsOfNature.beginPregnancy(player, entityTypeId, Math.max(1L, endTick));
return true;
} else {
return false;
}
}
public static boolean clearPregnancy(ServerPlayerEntity player) {
if (player == null) {
return false;
}
boolean hadPregnancy = player instanceof PregnancyHolder holder && holder.getPregnantEntityTypeId() != null;
NeedsOfNature.clearPregnancyState(player, true);
return hadPregnancy;
}
public static NonAccessoryEffectsSnapshot snapshotAccessoryEffects(PlayerEntity player) {
NonAccessoryEffects effects = player instanceof LivingEntity living ? NonTrinketsIntegration.getAccessoryEffects(living) : NonAccessoryEffects.NEUTRAL;
return new NonAccessoryEffectsSnapshot(
effects.liquidDecayMultiplier(),
effects.equalizeLiquidDecayContext(),
effects.playerEnergyGainMultiplier(),
effects.liquidCapacityAdd(),
effects.liquidGainMultiplier(),
effects.filledEffectMultiplier(),
effects.pregnancyChanceMultiplier(),
effects.pregnancyDurationMultiplier(),
effects.messGainMultiplier(),
effects.destroyedSkinDamageMultiplier(),
effects.attackEscapeHitsAdd(),
effects.attackEscapeDamageMultiplier(),
effects.playerEnergyAuraMultiplier(),
effects.nearAnimationMobEnergyMultiplier()
);
}
public static boolean isMessSystemEnabled() {
return NeedsOfNature.getConfig().isMessSystemEnabled();
}
public static boolean isDestroyedSkinSystemEnabled() {
return NeedsOfNature.getConfig().isDestroyedSkinSystemEnabled();
}
public static boolean isLiquidTankEnabled() {
return NeedsOfNature.getConfig().isLiquidTankEnabled();
}
public static boolean isPregnancyEnabled() {
return NeedsOfNature.getConfig().isPregnancyEnabled();
}
public static void fireLiquidChanged(ServerPlayerEntity player) {
if (player != null) {
((NonEvents.LiquidTankChanged)NonEvents.LIQUID_TANK_CHANGED.invoker()).onLiquidTankChanged(player, snapshotLiquid(player));
}
}
public static void fireMessChanged(ServerPlayerEntity player) {
if (player != null) {
((NonEvents.MessChanged)NonEvents.MESS_CHANGED.invoker()).onMessChanged(player, snapshotMess(player));
}
}
public static void fireDestroyedSkinChanged(ServerPlayerEntity player) {
if (player != null) {
((NonEvents.DestroyedSkinChanged)NonEvents.DESTROYED_SKIN_CHANGED.invoker()).onDestroyedSkinChanged(player, snapshotDestroyedSkin(player));
}
}
public static void firePregnancyChanged(ServerPlayerEntity player) {
if (player != null) {
((NonEvents.PregnancyChanged)NonEvents.PREGNANCY_CHANGED.invoker()).onPregnancyChanged(player, snapshotPregnancy(player));
}
}
public static void fireGenderChanged(ServerPlayerEntity player, int mask, NonChangeSource source) {
if (player != null) {
((NonEvents.PlayerGenderChanged)NonEvents.PLAYER_GENDER_CHANGED.invoker()).onPlayerGenderChanged(player, NonGender.fromMask(mask), source);
}
}
private static int addLiquidWithComposition(LiquidHolder holder, @Nullable Identifier sourceEntityType, int amount) {
int stored = Math.max(0, holder.getLiquidStored());
int capacity = Math.max(0, holder.getLiquidCapacity());
int room = Math.max(0, capacity - stored);
int added = Math.min(room, amount);
if (added <= 0) {
return 0;
}
if (stored <= 0) {
if (sourceEntityType != null) {
holder.setLiquidCompositionEntity(sourceEntityType);
} else {
holder.setLiquidCompositionMixed();
}
} else if (holder.getLiquidComposition() == LiquidHolder.LiquidComposition.ENTITY) {
Identifier existingType = holder.getLiquidEntityTypeId();
if (sourceEntityType == null || !sourceEntityType.equals(existingType)) {
holder.setLiquidCompositionMixed();
}
} else if (holder.getLiquidComposition() != LiquidHolder.LiquidComposition.MIXED) {
holder.setLiquidCompositionMixed();
}
holder.setLiquidStored(stored + added);
return added;
}
private static boolean changeMess(ServerPlayerEntity player, NonMessType type, int delta) {
if (!NeedsOfNature.getConfig().isMessSystemEnabled()) {
return false;
}
if (player != null && type != null && delta != 0 && player instanceof MessHolder holder) {
int before = switch (type) {
case V -> holder.getVMess();
case A -> holder.getAMess();
case M -> holder.getMMess();
};
int after = NonMessSystem.clampMess(before + delta);
if (after == before) {
return false;
}
switch (type) {
case V:
holder.setVMess(after);
break;
case A:
holder.setAMess(after);
break;
case M:
holder.setMMess(after);
}
NonMessSystem.broadcastMessState(player);
return true;
} else {
return false;
}
}
private static boolean changeDestroyedSkinDamage(ServerPlayerEntity player, int delta) {
if (!NeedsOfNature.getConfig().isDestroyedSkinSystemEnabled()) {
return false;
}
if (player != null && delta != 0 && player instanceof DestroyedSkinHolder holder) {
int before = Math.max(0, Math.min(10, holder.getDestroyedSkinDamage()));
int after = Math.max(0, Math.min(10, before + delta));
if (after == before) {
return false;
}
holder.setDestroyedSkinDamage(after);
NonDestroyedSkinSystem.setDestroyedSkinStage(player, destroyedSkinStageForDamage(after));
return true;
} else {
return false;
}
}
private static int destroyedSkinStageForDamage(int damage) {
if (damage >= 10) {
return 4;
} else if (damage >= 7) {
return 3;
} else if (damage >= 4) {
return 2;
} else {
return damage >= 1 ? 1 : 0;
}
}
private static NonLiquidComposition toApiLiquidComposition(LiquidHolder.LiquidComposition composition) {
return switch (composition) {
case ENTITY -> NonLiquidComposition.ENTITY;
case MIXED -> NonLiquidComposition.MIXED;
case EMPTY -> NonLiquidComposition.EMPTY;
};
}
}
@@ -0,0 +1,671 @@
package com.nonid;
import com.nonid.internal.animation.api.AnimationService;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.entity.Entity;
import net.minecraft.entity.LazyEntityReference;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.attribute.EntityAttributeInstance;
import net.minecraft.entity.attribute.EntityAttributes;
import net.minecraft.entity.damage.DamageSource;
import net.minecraft.entity.mob.Angerable;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.mob.Monster;
import net.minecraft.registry.Registries;
import net.minecraft.registry.RegistryKey;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.text.Text;
import net.minecraft.util.math.Box;
import net.minecraft.util.math.MathHelper;
import net.minecraft.world.World;
import org.jetbrains.annotations.Nullable;
final class NonAttackSystem {
private static final float ATTACK_ESCAPE_DAMAGE = 4.0F;
private static final float ATTACK_ESCAPE_MIN_DAMAGE = 1.0F;
private static final float ATTACK_ESCAPE_MIN_HEALTH = 1.0F;
private static final int ATTACK_OUTCOME_FAILSAFE_WINDOW_TICKS = 12000;
private static final int ATTACK_OUTCOME_FAILSAFE_COOLDOWN_TICKS = 12000;
private static final int ATTACK_BLOCK_DEBUG_COOLDOWN_TICKS = 400;
private static final double ATTACK_BLOCK_DEBUG_RADIUS = 24.0;
private static final double POST_ESCAPE_GATHER_RADIUS = 10.0;
private static final float ATTACK_ESCAPE_MIN_HEALTH_FACTOR = 0.3F;
private static final long DEFEATED_EASE_DURATION_TICKS = 300L;
private static final float DEFEATED_MAX_STEP_MULTIPLIER = 4.0F;
private static final float PVP_CRITICAL_HEALTH_THRESHOLD = 0.25F;
private static final float PVP_HEALTHY_DEFENDER_THRESHOLD = 0.75F;
private static final float PVP_DEFENDER_HEAT_NEUTRAL_THRESHOLD = 0.6F;
private static final int PVP_MASH_BASE_HITS = 12;
private static final Map<UUID, NonAttackSystem.AttackOutcomeFailsafeState> ATTACK_OUTCOME_FAILSAFE = new ConcurrentHashMap<>();
private static final Map<UUID, NonAttackSystem.AttackBlockDebugState> ATTACK_BLOCK_DEBUG = new ConcurrentHashMap<>();
private NonAttackSystem() {
}
static void clearRuntimeState() {
ATTACK_OUTCOME_FAILSAFE.clear();
ATTACK_BLOCK_DEBUG.clear();
}
static boolean canMobAttackPlayer(ServerPlayerEntity player) {
if (player == null) {
return false;
}
if (player.isSpectator()) {
return false;
}
if (!player.isCreative()) {
return true;
}
NonConfig config = NeedsOfNature.getConfig();
return config != null && config.canAttackCreativePlayers();
}
static void setAnimationStartProtection(ServerPlayerEntity player, int ticks) {
if (player != null) {
AnimationService.setAnimationStartProtection(player, ticks);
}
}
static boolean hasAnimationStartProtection(ServerWorld world, ServerPlayerEntity player) {
return world != null && player != null ? AnimationService.hasAnimationStartProtection(world, player) : false;
}
static boolean isMobAttackFailsafeActive(ServerWorld world, UUID mobUuid) {
if (world != null && mobUuid != null) {
NonAttackSystem.AttackOutcomeFailsafeState state = ATTACK_OUTCOME_FAILSAFE.get(mobUuid);
if (state == null) {
return false;
}
if (!state.worldKey.equals(world.getRegistryKey())) {
return false;
}
long nowTick = world.getTime();
pruneAttackOutcomeWindow(state, nowTick);
if (state.cooldownUntilTick <= nowTick) {
if (state.recentAttackOutcomeTicks.isEmpty()) {
ATTACK_OUTCOME_FAILSAFE.remove(mobUuid, state);
}
return false;
} else {
return true;
}
} else {
return false;
}
}
static int clearAttackOutcomeFailsafeEntries() {
int count = ATTACK_OUTCOME_FAILSAFE.size();
ATTACK_OUTCOME_FAILSAFE.clear();
return count;
}
static String getMobAttackFailsafeDebug(ServerWorld world, UUID mobUuid) {
if (world != null && mobUuid != null) {
NonAttackSystem.AttackOutcomeFailsafeState state = ATTACK_OUTCOME_FAILSAFE.get(mobUuid);
if (state != null && state.worldKey.equals(world.getRegistryKey())) {
long nowTick = world.getTime();
pruneAttackOutcomeWindow(state, nowTick);
long remaining = Math.max(0L, state.cooldownUntilTick - nowTick);
int recent = state.recentAttackOutcomeTicks.size();
if (remaining <= 0L && recent <= 0) {
ATTACK_OUTCOME_FAILSAFE.remove(mobUuid, state);
return null;
} else {
return remaining > 0L
? "attack failsafe: " + NeedsOfNature.formatTicksForDebug(remaining) + " remaining; peaks " + recent + "/" + attackOutcomeFailsafeThreshold()
: "attack failsafe: peaks " + recent + "/" + attackOutcomeFailsafeThreshold() + " in 10m window";
}
} else {
return null;
}
} else {
return null;
}
}
static NeedsOfNature.AttackBlockReason getMobAttackBlockReason(ServerWorld world, MobEntity mob, ServerPlayerEntity player) {
if (world != null && mob != null && player != null) {
if (mob.getEntityWorld() != world || player.getEntityWorld() != world) {
return null;
} else if (NeedsOfNature.isPregnancyFriendlyMob(mob)) {
return NeedsOfNature.AttackBlockReason.PREGNANCY_FRIENDLY;
} else if (!isAggressiveThreat(mob, player)) {
return null;
} else {
boolean mobBusy = NeedsOfNature.isMobGatheringOrActiveWithPlayer(world, mob, player);
boolean playerBusy = NeedsOfNature.hasActivePlayerAnimation(player);
if (mobBusy) {
return NeedsOfNature.AttackBlockReason.GATHERING_PAIR;
} else {
return playerBusy && mob instanceof EnergyHolder holder && holder.getEnergy() > 70
? NeedsOfNature.AttackBlockReason.ENERGY_ABOVE_70_PLAYER_BUSY
: null;
}
}
} else {
return null;
}
}
static void onMobAttackBlocked(ServerWorld world, MobEntity mob, ServerPlayerEntity player, NeedsOfNature.AttackBlockReason reason) {
if (world != null && mob != null && player != null && reason != null) {
if (reason == NeedsOfNature.AttackBlockReason.PREGNANCY_FRIENDLY) {
if (mob.getTarget() == player) {
mob.setTarget(null);
}
mob.getNavigation().stop();
if (mob instanceof Angerable angerable) {
LazyEntityReference<LivingEntity> angryAt = angerable.getAngryAt();
UUID angryAtUuid = angryAt == null ? null : angryAt.getUuid();
if (angryAtUuid == null || angryAtUuid.equals(player.getUuid())) {
angerable.stopAnger();
}
}
}
long now = world.getTime();
RegistryKey<World> worldKey = world.getRegistryKey();
UUID mobUuid = mob.getUuid();
NonAttackSystem.AttackBlockDebugState current = ATTACK_BLOCK_DEBUG.get(mobUuid);
if (current == null || !current.worldKey.equals(worldKey) || now >= current.nextTick) {
ATTACK_BLOCK_DEBUG.put(mobUuid, new NonAttackSystem.AttackBlockDebugState(worldKey, now + 400L));
Text mobName = Text.translatable(mob.getType().getTranslationKey());
Text playerName = player.getName();
Text message = switch (reason) {
case PREGNANCY_FRIENDLY -> Text.translatable("debug.needsofnature.attack_blocked_pregnancy_friendly", new Object[]{mobName, playerName});
case GATHERING_PAIR -> Text.translatable("debug.needsofnature.attack_blocked_gathering_pair", new Object[]{mobName, playerName});
case ENERGY_ABOVE_70_PLAYER_BUSY -> Text.translatable("debug.needsofnature.attack_blocked_energy_busy", new Object[]{mobName, playerName});
};
NeedsOfNature.sendDebugChatToNearby(world, mob, 24.0, NonDebugChatCategory.WARNING, message);
}
}
}
private static boolean isAggressiveThreat(MobEntity mob, ServerPlayerEntity player) {
if (mob == null || player == null) {
return false;
}
if (mob instanceof Monster) {
return true;
}
if (mob instanceof Angerable angerable) {
LazyEntityReference<LivingEntity> angryAt = angerable.getAngryAt();
UUID angryAtUuid = angryAt == null ? null : angryAt.getUuid();
if (angryAtUuid != null && angryAtUuid.equals(player.getUuid())) {
return true;
}
}
LivingEntity target = mob.getTarget();
return target != null && target.getUuid().equals(player.getUuid());
}
static boolean hasPlayerAndHostileActor(List<? extends Entity> actors) {
if (actors != null && !actors.isEmpty()) {
boolean hasPlayer = false;
boolean hasHostile = false;
for (Entity actor : actors) {
if (actor instanceof ServerPlayerEntity) {
hasPlayer = true;
} else if (isHostileOrAngryNeutral(actor)) {
hasHostile = true;
}
if (hasPlayer && hasHostile) {
return true;
}
}
return false;
} else {
return false;
}
}
private static boolean isHostileOrAngryNeutral(Entity entity) {
if (!(entity instanceof LivingEntity)) {
return false;
} else if (entity instanceof Monster) {
return true;
} else {
return entity instanceof Angerable angerable && entity instanceof MobEntity mob
? angerable.getAngryAt() != null || mob.getTarget() instanceof ServerPlayerEntity
: false;
}
}
static void recordAttackOutcomeForMobs(ServerWorld world, List<UUID> actorUuids, String reason) {
if (world != null && actorUuids != null && !actorUuids.isEmpty()) {
RegistryKey<World> worldKey = world.getRegistryKey();
long nowTick = world.getTime();
for (UUID actorUuid : actorUuids) {
if (world.getEntity(actorUuid) instanceof MobEntity mob && mob.isAlive() && !mob.isRemoved()) {
NonAttackSystem.AttackOutcomeFailsafeState state = ATTACK_OUTCOME_FAILSAFE.computeIfAbsent(
actorUuid, unused -> new NonAttackSystem.AttackOutcomeFailsafeState(worldKey)
);
if (!state.worldKey.equals(worldKey)) {
state.worldKey = worldKey;
state.recentAttackOutcomeTicks.clear();
state.cooldownUntilTick = 0L;
}
if (state.cooldownUntilTick <= nowTick) {
pruneAttackOutcomeWindow(state, nowTick);
state.recentAttackOutcomeTicks.addLast(nowTick);
if (state.recentAttackOutcomeTicks.size() >= attackOutcomeFailsafeThreshold()) {
state.recentAttackOutcomeTicks.clear();
state.cooldownUntilTick = nowTick + 12000L;
NeedsOfNature.logRuntime(
"[NoN] Attack peak failsafe armed for mob={} in world={} reason={} untilTick={}",
mob.getUuid(),
worldKey.getValue(),
reason,
state.cooldownUntilTick
);
}
}
}
}
}
}
static void startNearbyPostEscapeGathers(ServerWorld world, ServerPlayerEntity player, @Nullable List<UUID> escapedActorUuids) {
if (world != null && player != null && player.getEntityWorld() == world) {
if (canMobAttackPlayer(player)) {
if (player.isAlive() && !player.isRemoved()) {
if (!NonGatherSystem.isAutomaticMobPlayerStartBlockedByMount(player)) {
int maxMobs = NeedsOfNature.getConfig().getPostEscapeGatherMaxMobs();
if (maxMobs > 0) {
Box searchBox = player.getBoundingBox().expand(10.0);
List<MobEntity> candidates = world.getEntitiesByClass(
MobEntity.class, searchBox, mobx -> isPostEscapeGatherCandidate(world, player, mobx, escapedActorUuids)
);
if (!candidates.isEmpty()) {
candidates.sort(Comparator.comparingDouble(player::squaredDistanceTo));
int started = 0;
for (MobEntity mob : candidates) {
if (started >= maxMobs) {
break;
}
List<Entity> actors = new ArrayList<>(2);
actors.add(mob);
actors.add(player);
actors.sort(Comparator.comparingInt(Entity::getId));
NonGatherSystem.GatherCandidate gatherCandidate = NonGatherSystem.findGatherCandidate(world, actors, true, mob, player);
if (gatherCandidate != null) {
boolean gatherStarted = NonGatherSystem.startMobPlayerGather(
world, mob, player, gatherCandidate.animationId(), gatherCandidate.actorKeys(), gatherCandidate.stages()
);
if (gatherStarted) {
started++;
NeedsOfNature.logRuntime(
"Post-escape gather started: mobType={} player={} anim={} energy={}",
Registries.ENTITY_TYPE.getId(mob.getType()),
player.getName().getString(),
gatherCandidate.animationId(),
((EnergyHolder)mob).getEnergy()
);
}
}
}
if (started > 0) {
NeedsOfNature.sendDebugChat(
player, NonDebugChatCategory.INFO, Text.translatable("debug.needsofnature.post_escape_gather_started", new Object[]{started})
);
}
}
}
}
}
}
}
}
private static boolean isPostEscapeGatherCandidate(ServerWorld world, ServerPlayerEntity player, MobEntity mob, @Nullable List<UUID> escapedActorUuids) {
if (mob == null || mob.getEntityWorld() != world) {
return false;
}
if (!mob.isAlive() || mob.isRemoved()) {
return false;
}
if (!NonMobGatherEligibility.canInitiateAutoGather(mob)) {
return false;
}
if (mob instanceof EnergyHolder holder) {
if (holder.getEnergy() < 70) {
return false;
} else if (NonMobEnergySystem.isEnergyAttackStabilized(mob) && holder.getEnergy() >= 200) {
return false;
} else if (!NonMobEnergySystem.hasGenericAnimationSupport(mob.getType(), mob.isBaby())) {
return false;
} else if (NeedsOfNature.isActorPendingOrActive(world, mob.getUuid())) {
return false;
} else if (NeedsOfNature.isMobAttackFailsafeActive(world, mob.getUuid())) {
return false;
} else if (!mob.canSee(player)) {
return false;
} else {
return escapedActorUuids != null && escapedActorUuids.contains(mob.getUuid()) ? true : mob.squaredDistanceTo(player) <= 100.0;
}
} else {
return false;
}
}
private static int attackOutcomeFailsafeThreshold() {
NonConfig config = NeedsOfNature.getConfig();
return config == null ? 3 : config.getAttackOutcomeFailsafeThreshold();
}
private static void pruneAttackOutcomeWindow(NonAttackSystem.AttackOutcomeFailsafeState state, long nowTick) {
if (state != null) {
long oldestAllowedTick = nowTick - 12000L;
while (!state.recentAttackOutcomeTicks.isEmpty() && state.recentAttackOutcomeTicks.peekFirst() <= oldestAllowedTick) {
state.recentAttackOutcomeTicks.removeFirst();
}
}
}
static void pruneAttackOutcomeFailsafe(ServerWorld world, long nowTick) {
if (world != null && !ATTACK_OUTCOME_FAILSAFE.isEmpty()) {
RegistryKey<World> worldKey = world.getRegistryKey();
ATTACK_OUTCOME_FAILSAFE.entrySet().removeIf(entry -> {
NonAttackSystem.AttackOutcomeFailsafeState state = entry.getValue();
if (!state.worldKey.equals(worldKey)) {
return false;
}
pruneAttackOutcomeWindow(state, nowTick);
return state.cooldownUntilTick <= nowTick && state.recentAttackOutcomeTicks.isEmpty();
});
}
}
static void pruneAttackBlockDebug(ServerWorld world, long nowTick) {
if (world != null && !ATTACK_BLOCK_DEBUG.isEmpty()) {
RegistryKey<World> worldKey = world.getRegistryKey();
ATTACK_BLOCK_DEBUG.entrySet().removeIf(entry -> {
NonAttackSystem.AttackBlockDebugState state = entry.getValue();
return !state.worldKey.equals(worldKey) ? false : state.nextTick <= nowTick;
});
}
}
static void applyAttackEscapeDamage(ServerWorld world, ServerPlayerEntity player, @Nullable List<UUID> actorUuids) {
applyAttackEscapeDamage(world, player, actorUuids, null);
}
static void applyAttackEscapeDamage(ServerWorld world, ServerPlayerEntity player, @Nullable List<UUID> actorUuids, @Nullable UUID preferredAttackerUuid) {
if (world != null && player != null) {
if (player.isAlive()) {
float health = player.getHealth();
if (!(health <= 1.0F)) {
float maxRaw = health - 1.0F;
if (!(maxRaw <= 0.0F)) {
LivingEntity attacker = resolveAttackEscapeAttacker(world, player, actorUuids, preferredAttackerUuid);
float rawDamage = resolveAttackEscapeRawDamage(attacker);
rawDamage *= (float)NonTrinketsIntegration.getAccessoryEffects(player).attackEscapeDamageMultiplier();
rawDamage = Math.min(rawDamage, maxRaw);
if (!(rawDamage <= 0.0F)) {
if (attacker instanceof ServerPlayerEntity attackingPlayer) {
NonPlayerAttackSystem.applyPlayerAttributedEscapeDamage(world, player, attackingPlayer, rawDamage);
} else {
DamageSource source = resolveAttackEscapeDamageSource(player, attacker);
player.damage(world, source, rawDamage);
}
float after = player.getHealth();
float dealt = health - after;
if (dealt < 1.0F && after > 1.0F) {
float extra = Math.min(1.0F - dealt, after - 1.0F);
if (extra > 0.0F) {
player.setHealth(after - extra);
}
}
}
}
}
}
}
}
@Nullable
private static LivingEntity resolveAttackEscapeAttacker(
ServerWorld world, ServerPlayerEntity player, @Nullable List<UUID> actorUuids, @Nullable UUID preferredAttackerUuid
) {
if (world != null && player != null) {
if (preferredAttackerUuid != null && !preferredAttackerUuid.equals(player.getUuid())) {
Entity preferred = world.getEntity(preferredAttackerUuid);
if (preferred == null) {
preferred = world.getServer().getPlayerManager().getPlayer(preferredAttackerUuid);
}
if (preferred instanceof LivingEntity living && living.isAlive() && !living.isRemoved()) {
return living;
}
}
if (actorUuids != null && !actorUuids.isEmpty()) {
for (UUID actorUuid : actorUuids) {
if (actorUuid != null
&& !actorUuid.equals(player.getUuid())
&& world.getEntity(actorUuid) instanceof LivingEntity living
&& living.isAlive()
&& !living.isRemoved()) {
return living;
}
}
return null;
} else {
return null;
}
} else {
return null;
}
}
private static float resolveAttackEscapeRawDamage(@Nullable LivingEntity attacker) {
float baseDamage = 4.0F;
if (attacker != null) {
EntityAttributeInstance attackDamage = attacker.getAttributeInstance(EntityAttributes.ATTACK_DAMAGE);
if (attackDamage != null) {
double attributeDamage = attackDamage.getValue();
if (Double.isFinite(attributeDamage) && attributeDamage > 0.0) {
baseDamage = (float)attributeDamage;
}
}
}
if (!Float.isFinite(baseDamage)) {
baseDamage = 4.0F;
}
return Math.max(1.0F, baseDamage);
}
private static DamageSource resolveAttackEscapeDamageSource(ServerPlayerEntity player, @Nullable LivingEntity attacker) {
if (player == null) {
throw new IllegalArgumentException("player");
} else {
return attacker instanceof MobEntity mob ? player.getDamageSources().mobAttack(mob) : player.getDamageSources().generic();
}
}
static float resolveServerAttackEscapeStep(ServerWorld world, ServerPlayerEntity victim, boolean defeated, long contestStartTick) {
if (world != null && victim != null) {
float base = 1.0F / resolveAttackEscapeHits(victim);
float healthFactor = resolveAttackHealthFactor(victim);
float energyFactor = resolveAttackEnergyFactor(victim);
float randomScale = healthFactor >= 0.999F ? 1.0F : 0.5F + world.getRandom().nextFloat();
if (!defeated) {
return Math.max(0.0F, base * healthFactor * energyFactor * randomScale);
}
float ease = MathHelper.clamp((float)Math.max(0L, world.getTime() - contestStartTick) / 300.0F, 0.0F, 1.0F);
float adjustedHealth = MathHelper.lerp(ease, healthFactor, 1.0F);
float adjustedEnergy = MathHelper.lerp(ease, energyFactor, 1.0F);
float adjustedRandom = MathHelper.lerp(ease, randomScale, 1.0F);
float multiplier = MathHelper.lerp(ease, 1.0F, 4.0F);
return Math.max(0.0F, base * adjustedHealth * adjustedEnergy * adjustedRandom * multiplier);
} else {
return 0.0F;
}
}
static NonAttackSystem.PvpMashFactors resolveServerPlayerContestStep(
ServerWorld world, ServerPlayerEntity participant, boolean defender, boolean defeated, long contestStartTick
) {
if (world != null && participant != null) {
float base = 0.083333336F;
float healthMultiplier = resolvePvpCriticalHealthMultiplier(participant);
float heatMultiplier = resolvePvpHeatMultiplier(participant, defender);
float healthyDefenderMultiplier = defender ? resolvePvpHealthyDefenderMultiplier(participant) : 1.0F;
float defeatedEaseMultiplier = 1.0F;
if (defender && defeated) {
float ease = MathHelper.clamp((float)Math.max(0L, world.getTime() - contestStartTick) / 300.0F, 0.0F, 1.0F);
healthMultiplier = MathHelper.lerp(ease, healthMultiplier, 1.0F);
heatMultiplier = MathHelper.lerp(ease, heatMultiplier, 1.0F);
healthyDefenderMultiplier = MathHelper.lerp(ease, healthyDefenderMultiplier, 1.0F);
defeatedEaseMultiplier = MathHelper.lerp(ease, 1.0F, 4.0F);
}
float step = base * healthMultiplier * heatMultiplier * healthyDefenderMultiplier * defeatedEaseMultiplier;
return new NonAttackSystem.PvpMashFactors(Math.max(0.0F, step), healthMultiplier, heatMultiplier, healthyDefenderMultiplier, defeatedEaseMultiplier);
} else {
return NonAttackSystem.PvpMashFactors.none();
}
}
private static int resolveAttackEscapeHits(ServerPlayerEntity victim) {
int hits = NeedsOfNature.getConfig().getAttackEscapeHits() + NonTrinketsIntegration.getAccessoryEffects(victim).attackEscapeHitsAdd();
return Math.max(1, Math.min(50, hits));
}
private static float resolvePvpCriticalHealthMultiplier(ServerPlayerEntity player) {
float healthRatio = resolveHealthRatio(player);
if (healthRatio >= 0.25F) {
return 1.0F;
}
float criticalStrength = NeedsOfNature.getConfig().getPvpCriticalHealthMashStrengthPercent() / 100.0F;
float progress = MathHelper.clamp(healthRatio / 0.25F, 0.0F, 1.0F);
return MathHelper.lerp(progress, criticalStrength, 1.0F);
}
private static float resolvePvpHealthyDefenderMultiplier(ServerPlayerEntity player) {
float healthRatio = resolveHealthRatio(player);
if (healthRatio <= 0.75F) {
return 1.0F;
}
float progress = MathHelper.clamp((healthRatio - 0.75F) / 0.25F, 0.0F, 1.0F);
float fullHealthMultiplier = 1.0F + NeedsOfNature.getConfig().getPvpFullHealthEscapeBonusPercent() / 100.0F;
return MathHelper.lerp(progress, 1.0F, fullHealthMultiplier);
}
private static float resolvePvpHeatMultiplier(ServerPlayerEntity player, boolean defender) {
float energyRatio = resolveEnergyRatio(player);
NonConfig config = NeedsOfNature.getConfig();
if (!defender) {
float zeroHeatStrength = config.getPvpZeroHeatAttackStrengthPercent() / 100.0F;
return MathHelper.lerp(energyRatio, zeroHeatStrength, 1.0F);
}
if (energyRatio <= 0.6F) {
return 1.0F;
}
float progress = MathHelper.clamp((energyRatio - 0.6F) / 0.39999998F, 0.0F, 1.0F);
float maxHeatStrength = config.getPvpMaxHeatEscapeStrengthPercent() / 100.0F;
return MathHelper.lerp(progress, 1.0F, maxHeatStrength);
}
private static float resolveHealthRatio(ServerPlayerEntity player) {
float maxHealth = player.getMaxHealth();
return Float.isFinite(maxHealth) && !(maxHealth <= 0.0F) ? MathHelper.clamp(player.getHealth() / maxHealth, 0.0F, 1.0F) : 0.0F;
}
private static float resolveEnergyRatio(ServerPlayerEntity player) {
if (player instanceof EnergyHolder holder) {
int maxEnergy = Math.max(1, holder.getMaxEnergy());
return MathHelper.clamp((float)holder.getEnergy() / maxEnergy, 0.0F, 1.0F);
} else {
return 1.0F;
}
}
private static float resolveAttackHealthFactor(ServerPlayerEntity victim) {
float maxHealth = victim.getMaxHealth();
float minHealth = 0.5F;
if (maxHealth <= minHealth) {
return 0.3F;
}
float raw = MathHelper.clamp((victim.getHealth() - minHealth) / (maxHealth - minHealth), 0.0F, 1.0F);
float impact = NeedsOfNature.getConfig().getAttackEscapeDamageDifficultyPercent() / 100.0F;
return MathHelper.clamp(1.0F - (1.0F - raw) * impact, 0.3F, 1.0F);
}
private static float resolveAttackEnergyFactor(ServerPlayerEntity victim) {
if (victim instanceof EnergyHolder holder) {
int max = Math.max(1, holder.getMaxEnergy());
float energy = Math.max(0.0F, Math.min(max, holder.getEnergy()));
if (energy <= 100.0F) {
return 1.0F - 0.1F * energy / 100.0F;
}
float t = Math.min(1.0F, (energy - 100.0F) / 100.0F);
return Math.max(0.08F, 0.9F - 0.82F * t * t);
} else {
return 1.0F;
}
}
private record AttackBlockDebugState(RegistryKey<World> worldKey, long nextTick) {
}
private static final class AttackOutcomeFailsafeState {
RegistryKey<World> worldKey;
final ArrayDeque<Long> recentAttackOutcomeTicks = new ArrayDeque<>();
long cooldownUntilTick = 0L;
AttackOutcomeFailsafeState(RegistryKey<World> worldKey) {
this.worldKey = worldKey;
}
}
record PvpMashFactors(float step, float healthMultiplier, float heatMultiplier, float healthyDefenderMultiplier, float defeatedEaseMultiplier) {
private static NonAttackSystem.PvpMashFactors none() {
return new NonAttackSystem.PvpMashFactors(0.0F, 0.0F, 0.0F, 0.0F, 0.0F);
}
}
}
+209
View File
@@ -0,0 +1,209 @@
package com.nonid;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import net.minecraft.entity.Entity;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.Box;
import net.minecraft.util.math.MathHelper;
final class NonAuraSystem {
private static final int MESS_AURA_MAX_TOTAL = 15;
private static final double MESS_AURA_MAX_MULTIPLIER = 10.0;
private NonAuraSystem() {
}
static void tickPlayerEnergyAura(ServerWorld world, ServerPlayerEntity player) {
if (world != null && player != null && player.isAlive() && !player.isRemoved()) {
if (player instanceof EnergyHolder playerEnergyHolder) {
NonConfig config = NeedsOfNature.getConfig();
if (config != null) {
int pulseTicks = config.getPlayerEnergyAuraPulseTicks();
if (pulseTicks > 0 && world.getTime() % pulseTicks == 0L) {
int playerEnergy = playerEnergyHolder.getEnergy();
int destroyedStage = config.isDestroyedSkinSystemEnabled() ? NeedsOfNature.getDestroyedSkinStageForPlayer(player) : 0;
int messTotal = config.isMessSystemEnabled() ? NeedsOfNature.getTotalMessForPlayer(player) : 0;
if (config.isMessSystemEnabled()) {
NeedsOfNature.checkBadIdeaBeaconAdvancement(player);
}
double auraMultiplier = getCombinedPlayerAuraMultiplier(config, playerEnergy, destroyedStage, messTotal)
* NonTrinketsIntegration.getAccessoryEffects(player).playerEnergyAuraMultiplier();
if (Double.isFinite(auraMultiplier) && !(auraMultiplier <= 1.0)) {
double radius = getCombinedPlayerAuraRadius(config, playerEnergy, destroyedStage, messTotal);
if (Double.isFinite(radius) && !(radius <= 0.0)) {
double radiusSq = radius * radius;
Box searchBox = player.getBoundingBox().expand(radius);
List<MobEntity> mobs = world.getEntitiesByClass(
MobEntity.class,
searchBox,
mobx -> mobx.isAlive() && !mobx.isRemoved() && mobx.getEntityWorld() == world && mobx.squaredDistanceTo(player) <= radiusSq
);
if (!mobs.isEmpty()) {
for (MobEntity mob : mobs) {
if (mob instanceof EnergyHolder holder) {
holder.ensureEnergyInitialized(world);
int currentEnergy = holder.getEnergy();
int maxEnergy = holder.getMaxEnergy();
if (currentEnergy >= maxEnergy) {
holder.setEnergyAuraCarry(0.0);
} else if (hasPairAnimationWithPlayer(mob, player)) {
double baseGain = getExpectedMobEnergyGainPerPulse(world, mob, holder, pulseTicks);
if (Double.isFinite(baseGain) && !(baseGain <= 0.0)) {
double bonusGain = baseGain * (auraMultiplier - 1.0);
double totalGain = holder.getEnergyAuraCarry() + bonusGain;
int rawApplied = MathHelper.floor(totalGain);
if (rawApplied <= 0) {
holder.setEnergyAuraCarry(totalGain);
} else {
int appliedGain = Math.min(rawApplied, maxEnergy - currentEnergy);
if (appliedGain <= 0) {
holder.setEnergyAuraCarry(0.0);
} else {
holder.setEnergy(currentEnergy + appliedGain);
holder.setEnergyAuraCarry(appliedGain < rawApplied ? 0.0 : totalGain - appliedGain);
}
}
}
}
}
}
}
}
}
}
}
}
}
}
static NonAuraSystem.PlayerEnergyAuraDebug getPlayerEnergyAuraDebug(ServerWorld world, MobEntity mob, EnergyHolder holder) {
if (world != null && mob != null && holder != null) {
NonConfig config = NeedsOfNature.getConfig();
if (config == null) {
return null;
}
NonAuraSystem.PlayerEnergyAuraDebug best = null;
for (ServerPlayerEntity player : world.getPlayers()) {
if (player != null && player.isAlive() && !player.isRemoved() && player instanceof EnergyHolder playerEnergyHolder) {
int playerEnergy = playerEnergyHolder.getEnergy();
int destroyedStage = config.isDestroyedSkinSystemEnabled() ? NeedsOfNature.getDestroyedSkinStageForPlayer(player) : 0;
int messTotal = config.isMessSystemEnabled() ? NeedsOfNature.getTotalMessForPlayer(player) : 0;
double radius = getCombinedPlayerAuraRadius(config, playerEnergy, destroyedStage, messTotal);
if (Double.isFinite(radius) && !(radius <= 0.0) && !(mob.squaredDistanceTo(player) > radius * radius) && hasPairAnimationWithPlayer(mob, player)
)
{
double totalMultiplier = getCombinedPlayerAuraMultiplier(config, playerEnergy, destroyedStage, messTotal)
* NonTrinketsIntegration.getAccessoryEffects(player).playerEnergyAuraMultiplier();
if (Double.isFinite(totalMultiplier) && !(totalMultiplier <= 1.0) && (best == null || totalMultiplier > best.totalMultiplier())) {
best = new NonAuraSystem.PlayerEnergyAuraDebug(player, totalMultiplier, playerEnergy, destroyedStage, messTotal, radius);
}
}
}
}
return best;
} else {
return null;
}
}
private static double getCombinedPlayerAuraMultiplier(NonConfig config, int playerEnergy, int destroyedStage, int messTotal) {
if (config == null) {
return 1.0;
}
double energyBonus = Math.max(0.0, getPlayerEnergyAuraMultiplier(config, playerEnergy) - 1.0);
double destroyedBonus = Math.max(0.0, getDestroyedSkinAuraMultiplier(config, destroyedStage) - 1.0);
double messBonus = Math.max(0.0, getMessAuraMultiplier(messTotal) - 1.0);
return 1.0 + energyBonus + destroyedBonus + messBonus;
}
private static double getPlayerEnergyAuraMultiplier(NonConfig config, int playerEnergy) {
if (config == null || playerEnergy < 100) {
return 1.0;
} else if (playerEnergy >= 200) {
return config.getPlayerEnergyAuraMultHigh();
} else {
return playerEnergy >= 150 ? config.getPlayerEnergyAuraMultMid() : config.getPlayerEnergyAuraMultLow();
}
}
private static double getDestroyedSkinAuraMultiplier(NonConfig config, int stage) {
if (config == null) {
return 1.0;
}
return switch (stage) {
case 1 -> config.getDestroyedSkinAuraMultStage1();
case 2 -> config.getDestroyedSkinAuraMultStage2();
case 3 -> config.getDestroyedSkinAuraMultStage3();
case 4 -> config.getDestroyedSkinAuraMultStage4();
default -> 1.0;
};
}
private static double getMessAuraMultiplier(int messTotal) {
if (messTotal <= 0) {
return 1.0;
}
double progress = MathHelper.clamp(messTotal / 15.0, 0.0, 1.0);
return MathHelper.lerp(progress, 1.0, 10.0);
}
private static double getPlayerEnergyAuraRadius(NonConfig config, int playerEnergy) {
if (config != null && playerEnergy >= 100) {
double minRadius = config.getPlayerEnergyAuraRadiusMin();
double maxRadius = config.getPlayerEnergyAuraRadiusMax();
double progress = MathHelper.clamp((playerEnergy - 100) / 100.0, 0.0, 1.0);
return MathHelper.lerp(progress, minRadius, maxRadius);
} else {
return 0.0;
}
}
private static double getCombinedPlayerAuraRadius(NonConfig config, int playerEnergy, int destroyedStage, int messTotal) {
if (config == null) {
return 0.0;
} else if (playerEnergy >= 100) {
return getPlayerEnergyAuraRadius(config, playerEnergy);
} else {
return destroyedStage <= 0 && messTotal <= 0 ? 0.0 : config.getPlayerEnergyAuraRadiusMin();
}
}
private static boolean hasPairAnimationWithPlayer(MobEntity mob, ServerPlayerEntity player) {
if (mob != null && player != null) {
List<Entity> pair = new ArrayList<>(2);
pair.add(mob);
pair.add(player);
pair.sort(Comparator.comparingInt(Entity::getId));
return NonAnimationMatcher.findAutomatic(player.getEntityWorld(), pair, Set.of(), null, false, null, Set.of()) != null;
} else {
return false;
}
}
private static double getExpectedMobEnergyGainPerPulse(ServerWorld world, MobEntity mob, EnergyHolder holder, int pulseTicks) {
if (world != null && mob != null && holder != null && pulseTicks > 0) {
double nearAnimationMultiplier = NeedsOfNature.getNearbyVisibleAnimationGainMultiplier(world, mob);
double scale = NonMobEnergySystem.passiveGainScale(
mob, holder.getEnergyGainMultiplier(), holder.getEnergyGainDrift(), nearAnimationMultiplier > 1.0, nearAnimationMultiplier
);
return scale <= 0.0 ? 0.0 : pulseTicks * scale / 120.0;
} else {
return 0.0;
}
}
record PlayerEnergyAuraDebug(ServerPlayerEntity player, double totalMultiplier, int playerEnergy, int destroyedSkinStage, int messTotal, double radius) {
}
}
+714
View File
@@ -0,0 +1,714 @@
package com.nonid;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.nonid.network.SkinRippedInfoRequestS2CPayload;
import java.util.List;
import java.util.Locale;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.command.argument.EntityArgumentType;
import net.minecraft.entity.Entity;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.registry.Registries;
import net.minecraft.server.PlayerConfigEntry;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
final class NonCommands {
private NonCommands() {
}
static void register() {
CommandRegistrationCallback.EVENT
.register(
(CommandRegistrationCallback)(dispatcher, registryAccess, environment) -> dispatcher.register(
(LiteralArgumentBuilder)((LiteralArgumentBuilder)((LiteralArgumentBuilder)((LiteralArgumentBuilder)((LiteralArgumentBuilder)((LiteralArgumentBuilder)CommandManager.literal(
"needsofnature"
)
.requires(NonCommands::isOpCommandSource))
.then(
((LiteralArgumentBuilder)CommandManager.literal("getplayerinfo").executes(ctx -> {
ServerPlayerEntity self = ((ServerCommandSource)ctx.getSource()).getPlayer();
if (self == null) {
((ServerCommandSource)ctx.getSource())
.sendError(Text.literal("控制台用法: /needsofnature getplayerinfo <player>"));
return 0;
} else {
return sendPlayerInfo((ServerCommandSource)ctx.getSource(), self);
}
}))
.then(
CommandManager.argument("player", EntityArgumentType.player())
.executes(ctx -> sendPlayerInfo((ServerCommandSource)ctx.getSource(), EntityArgumentType.getPlayer(ctx, "player")))
)
))
.then(CommandManager.literal("resetmobcooldowns").executes(ctx -> resetMobCooldowns((ServerCommandSource)ctx.getSource()))))
.then(
((LiteralArgumentBuilder)CommandManager.literal("skin").then(messCommand()))
.then(
((LiteralArgumentBuilder)((LiteralArgumentBuilder)CommandManager.literal("ripped")
.then(CommandManager.literal("info").executes(ctx -> requestRippedSkinInfo((ServerCommandSource)ctx.getSource()))))
.then(
((LiteralArgumentBuilder)CommandManager.literal("reset").executes(ctx -> {
ServerPlayerEntity self = ((ServerCommandSource)ctx.getSource()).getPlayer();
if (self == null) {
((ServerCommandSource)ctx.getSource())
.sendError(Text.literal("控制台用法: /needsofnature skin ripped reset <player>"));
return 0;
} else {
return resetDestroyedSkinCommand((ServerCommandSource)ctx.getSource(), self);
}
}))
.then(
CommandManager.argument("player", EntityArgumentType.player())
.executes(
ctx -> resetDestroyedSkinCommand(
(ServerCommandSource)ctx.getSource(), EntityArgumentType.getPlayer(ctx, "player")
)
)
)
))
.then(
CommandManager.literal("set")
.then(
((RequiredArgumentBuilder)CommandManager.argument("stage", IntegerArgumentType.integer(0, 4)).executes(ctx -> {
ServerPlayerEntity self = ((ServerCommandSource)ctx.getSource()).getPlayer();
if (self == null) {
((ServerCommandSource)ctx.getSource())
.sendError(Text.literal("控制台用法: /needsofnature skin ripped set <stage> <player>"));
return 0;
} else {
return setDestroyedSkinStageCommand(
(ServerCommandSource)ctx.getSource(), self, IntegerArgumentType.getInteger(ctx, "stage")
);
}
}))
.then(
CommandManager.argument("player", EntityArgumentType.player())
.executes(
ctx -> setDestroyedSkinStageCommand(
(ServerCommandSource)ctx.getSource(),
EntityArgumentType.getPlayer(ctx, "player"),
IntegerArgumentType.getInteger(ctx, "stage")
)
)
)
)
)
)
))
.then(
((LiteralArgumentBuilder)((LiteralArgumentBuilder)((LiteralArgumentBuilder)CommandManager.literal("gender")
.requires(NonCommands::isOpCommandSource))
.then(
CommandManager.literal("get")
.then(
CommandManager.argument("player", EntityArgumentType.player())
.executes(
ctx -> getGenderCommand((ServerCommandSource)ctx.getSource(), EntityArgumentType.getPlayer(ctx, "player"))
)
)
))
.then(
CommandManager.literal("set")
.then(
CommandManager.argument("player", EntityArgumentType.player())
.then(
CommandManager.argument("gender", StringArgumentType.word())
.executes(
ctx -> setGenderCommand(
(ServerCommandSource)ctx.getSource(),
EntityArgumentType.getPlayer(ctx, "player"),
StringArgumentType.getString(ctx, "gender")
)
)
)
)
))
.then(
CommandManager.literal("reset")
.then(
CommandManager.argument("player", EntityArgumentType.player())
.executes(ctx -> resetGenderCommand((ServerCommandSource)ctx.getSource(), EntityArgumentType.getPlayer(ctx, "player")))
)
)
))
.then(
((LiteralArgumentBuilder)((LiteralArgumentBuilder)((LiteralArgumentBuilder)((LiteralArgumentBuilder)CommandManager.literal("pregnancy")
.then(
CommandManager.literal("cycle")
.then(
CommandManager.literal("set")
.then(
((RequiredArgumentBuilder)CommandManager.argument("day", IntegerArgumentType.integer(1, 30))
.executes(
ctx -> {
ServerPlayerEntity self = ((ServerCommandSource)ctx.getSource()).getPlayer();
if (self == null) {
((ServerCommandSource)ctx.getSource())
.sendError(
Text.literal("控制台用法: /needsofnature pregnancy cycle set <day> <player>")
);
return 0;
} else {
return setPregnancyCycleDayCommand(
(ServerCommandSource)ctx.getSource(), self, IntegerArgumentType.getInteger(ctx, "day")
);
}
}
))
.then(
CommandManager.argument("player", EntityArgumentType.player())
.executes(
ctx -> setPregnancyCycleDayCommand(
(ServerCommandSource)ctx.getSource(),
EntityArgumentType.getPlayer(ctx, "player"),
IntegerArgumentType.getInteger(ctx, "day")
)
)
)
)
)
))
.then(
CommandManager.literal("set")
.then(
CommandManager.argument("player", EntityArgumentType.player())
.then(
((RequiredArgumentBuilder)CommandManager.argument("entity", StringArgumentType.string())
.executes(
ctx -> setPregnancyCommand(
(ServerCommandSource)ctx.getSource(),
EntityArgumentType.getPlayer(ctx, "player"),
StringArgumentType.getString(ctx, "entity"),
NeedsOfNature.getConfig().getPregnancyDurationMinutes(),
0
)
))
.then(
((RequiredArgumentBuilder)CommandManager.argument("minutes", IntegerArgumentType.integer(1, 5256000))
.executes(
ctx -> setPregnancyCommand(
(ServerCommandSource)ctx.getSource(),
EntityArgumentType.getPlayer(ctx, "player"),
StringArgumentType.getString(ctx, "entity"),
IntegerArgumentType.getInteger(ctx, "minutes"),
0
)
))
.then(
CommandManager.argument("spawns", IntegerArgumentType.integer(1, 16))
.executes(
ctx -> setPregnancyCommand(
(ServerCommandSource)ctx.getSource(),
EntityArgumentType.getPlayer(ctx, "player"),
StringArgumentType.getString(ctx, "entity"),
IntegerArgumentType.getInteger(ctx, "minutes"),
IntegerArgumentType.getInteger(ctx, "spawns")
)
)
)
)
)
)
))
.then(
((LiteralArgumentBuilder)CommandManager.literal("clear").executes(ctx -> {
ServerPlayerEntity self = ((ServerCommandSource)ctx.getSource()).getPlayer();
if (self == null) {
((ServerCommandSource)ctx.getSource())
.sendError(Text.literal("控制台用法: /needsofnature pregnancy clear <player>"));
return 0;
} else {
return clearPregnancyCommand((ServerCommandSource)ctx.getSource(), self);
}
}))
.then(
CommandManager.argument("player", EntityArgumentType.player())
.executes(
ctx -> clearPregnancyCommand((ServerCommandSource)ctx.getSource(), EntityArgumentType.getPlayer(ctx, "player"))
)
)
))
.then(
((LiteralArgumentBuilder)CommandManager.literal("complete").executes(ctx -> {
ServerPlayerEntity self = ((ServerCommandSource)ctx.getSource()).getPlayer();
if (self == null) {
((ServerCommandSource)ctx.getSource())
.sendError(Text.literal("控制台用法: /needsofnature pregnancy complete <player>"));
return 0;
} else {
return completePregnancyCommand((ServerCommandSource)ctx.getSource(), self);
}
}))
.then(
CommandManager.argument("player", EntityArgumentType.player())
.executes(
ctx -> completePregnancyCommand((ServerCommandSource)ctx.getSource(), EntityArgumentType.getPlayer(ctx, "player"))
)
)
))
.then(
((LiteralArgumentBuilder)CommandManager.literal("get").executes(ctx -> {
ServerPlayerEntity self = ((ServerCommandSource)ctx.getSource()).getPlayer();
if (self == null) {
((ServerCommandSource)ctx.getSource()).sendError(Text.literal("控制台用法: /needsofnature pregnancy get <player>"));
return 0;
} else {
return sendPregnantEntity((ServerCommandSource)ctx.getSource(), self);
}
}))
.then(
CommandManager.argument("player", EntityArgumentType.player())
.executes(ctx -> sendPregnantEntity((ServerCommandSource)ctx.getSource(), EntityArgumentType.getPlayer(ctx, "player")))
)
)
)
)
);
}
private static LiteralArgumentBuilder<ServerCommandSource> messCommand() {
return (LiteralArgumentBuilder<ServerCommandSource>)((LiteralArgumentBuilder)((LiteralArgumentBuilder)CommandManager.literal("mess")
.requires(NonCommands::isOpCommandSource))
.then(
((LiteralArgumentBuilder)CommandManager.literal("clean").executes(ctx -> {
ServerPlayerEntity self = ((ServerCommandSource)ctx.getSource()).getPlayer();
if (self == null) {
((ServerCommandSource)ctx.getSource()).sendError(Text.literal("控制台用法: /needsofnature skin mess clean <player>"));
return 0;
} else {
return cleanMessCommand((ServerCommandSource)ctx.getSource(), self);
}
}))
.then(
CommandManager.argument("player", EntityArgumentType.player())
.executes(ctx -> cleanMessCommand((ServerCommandSource)ctx.getSource(), EntityArgumentType.getPlayer(ctx, "player")))
)
))
.then(
((LiteralArgumentBuilder)((LiteralArgumentBuilder)((LiteralArgumentBuilder)CommandManager.literal("set")
.then(
CommandManager.argument("v_value", IntegerArgumentType.integer(0, 10))
.then(
CommandManager.argument("a_value", IntegerArgumentType.integer(0, 10))
.then(
((RequiredArgumentBuilder)CommandManager.argument("m_value", IntegerArgumentType.integer(0, 10))
.executes(
ctx -> {
ServerPlayerEntity self = ((ServerCommandSource)ctx.getSource()).getPlayer();
if (self == null) {
((ServerCommandSource)ctx.getSource())
.sendError(Text.literal("控制台用法: /needsofnature skin mess set <v> <a> <m> <player>"));
return 0;
} else {
return setMessCommand(
(ServerCommandSource)ctx.getSource(),
self,
IntegerArgumentType.getInteger(ctx, "v_value"),
IntegerArgumentType.getInteger(ctx, "a_value"),
IntegerArgumentType.getInteger(ctx, "m_value")
);
}
}
))
.then(
CommandManager.argument("player", EntityArgumentType.player())
.executes(
ctx -> setMessCommand(
(ServerCommandSource)ctx.getSource(),
EntityArgumentType.getPlayer(ctx, "player"),
IntegerArgumentType.getInteger(ctx, "v_value"),
IntegerArgumentType.getInteger(ctx, "a_value"),
IntegerArgumentType.getInteger(ctx, "m_value")
)
)
)
)
)
))
.then(messSlotSetCommand("v")))
.then(messSlotSetCommand("a")))
.then(messSlotSetCommand("m"))
);
}
private static LiteralArgumentBuilder<ServerCommandSource> messSlotSetCommand(String slot) {
return (LiteralArgumentBuilder<ServerCommandSource>)CommandManager.literal(slot)
.then(
((RequiredArgumentBuilder)CommandManager.argument("value", IntegerArgumentType.integer(0, 10)).executes(ctx -> {
ServerPlayerEntity self = ((ServerCommandSource)ctx.getSource()).getPlayer();
if (self == null) {
((ServerCommandSource)ctx.getSource())
.sendError(Text.literal("控制台用法: /needsofnature skin mess set " + slot + " <value> <player>"));
return 0;
} else {
return setMessSlotCommand((ServerCommandSource)ctx.getSource(), self, slot, IntegerArgumentType.getInteger(ctx, "value"));
}
}))
.then(
CommandManager.argument("player", EntityArgumentType.player())
.executes(
ctx -> setMessSlotCommand(
(ServerCommandSource)ctx.getSource(),
EntityArgumentType.getPlayer(ctx, "player"),
slot,
IntegerArgumentType.getInteger(ctx, "value")
)
)
)
);
}
private static int sendPlayerInfo(ServerCommandSource source, ServerPlayerEntity target) {
int result = sendPlayerEnergy(source, target);
result += sendPlayerCooldowns(source, target);
result += sendPlayerContestInfo(source, target);
source.sendFeedback(() -> Text.literal(NonPregnancyConceptionSystem.describeCycle(target)), false);
return result + 1;
}
private static int sendPlayerContestInfo(ServerCommandSource source, ServerPlayerEntity target) {
List<String> lines = NonPlayerAttackSystem.getContestDebugLines(target);
for (String line : lines) {
source.sendFeedback(() -> Text.literal(line), false);
}
return lines.size();
}
private static int sendPlayerEnergy(ServerCommandSource source, ServerPlayerEntity target) {
if (target instanceof EnergyHolder holder) {
int current = holder.getEnergy();
int max = holder.getMaxEnergy();
source.sendFeedback(() -> Text.literal(target.getName().getString() + " energy: " + current + "/" + max), false);
return current;
} else {
source.sendError(Text.literal("目标没有能量数据。"));
return 0;
}
}
private static int sendPlayerCooldowns(ServerCommandSource source, ServerPlayerEntity target) {
if (!(target.getEntityWorld() instanceof ServerWorld world)) {
source.sendError(Text.literal("目标不在服务器世界中。"));
return 0;
} else {
List<String> cooldowns = NeedsOfNature.getActivePlayerCooldownDebugLines(world, target);
if (cooldowns.isEmpty()) {
source.sendFeedback(() -> Text.literal(target.getName().getString() + " cooldowns: none"), false);
return 0;
}
source.sendFeedback(() -> Text.literal(target.getName().getString() + " cooldowns:"), false);
for (String line : cooldowns) {
source.sendFeedback(() -> Text.literal("- " + line), false);
}
return cooldowns.size();
}
}
private static int requestRippedSkinInfo(ServerCommandSource source) {
ServerPlayerEntity player = source.getPlayer();
if (player == null) {
source.sendError(Text.literal("控制台用法: /needsofnature skin ripped info as a player"));
return 0;
} else {
ServerPlayNetworking.send(player, new SkinRippedInfoRequestS2CPayload());
source.sendFeedback(() -> Text.literal("已请求输出本地撕裂皮肤信息。"), false);
return 1;
}
}
private static boolean isOpCommandSource(ServerCommandSource source) {
if (source == null) {
return false;
}
ServerPlayerEntity player = source.getPlayer();
return player == null ? true : source.getServer().getPlayerManager().isOperator(new PlayerConfigEntry(player.getGameProfile()));
}
private static int resetMobCooldowns(ServerCommandSource source) {
int failsafeEntries = NeedsOfNature.clearAttackOutcomeFailsafeEntries();
int mobsReset = 0;
for (ServerWorld world : source.getServer().getWorlds()) {
for (Entity entity : world.iterateEntities()) {
if (entity instanceof MobEntity mob && mob instanceof EnergyHolder holder) {
holder.onNonAnimationStarted();
mobsReset++;
}
}
}
int total = failsafeEntries + mobsReset;
String feedback = "Reset mob cooldowns. Failsafe entries: " + failsafeEntries + ", mob partner cooldown sets: " + mobsReset + ".";
source.sendFeedback(() -> Text.literal(feedback), true);
return total;
}
private static int cleanMessCommand(ServerCommandSource source, ServerPlayerEntity target) {
NonMessSystem.clearMess(target);
source.sendFeedback(() -> Text.literal(target.getName().getString() + " mess cleared."), true);
return 1;
}
private static int setMessCommand(ServerCommandSource source, ServerPlayerEntity target, int v, int a, int m) {
if (!NeedsOfNature.isMessSystemEnabled()) {
source.sendError(Text.literal("污渍系统已禁用。"));
return 0;
} else if (target instanceof MessHolder holder) {
holder.setVMess(NonMessSystem.clampMess(v));
holder.setAMess(NonMessSystem.clampMess(a));
holder.setMMess(NonMessSystem.clampMess(m));
NonMessSystem.broadcastMessState(target);
maybeGrantMessedUp(target, holder);
sendMessSetFeedback(source, target, holder);
return 1;
} else {
source.sendError(Text.literal("目标没有污渍数据。"));
return 0;
}
}
private static int setMessSlotCommand(ServerCommandSource source, ServerPlayerEntity target, String slot, int value) {
if (!NeedsOfNature.isMessSystemEnabled()) {
source.sendError(Text.literal("污渍系统已禁用。"));
return 0;
}
if (target instanceof MessHolder holder) {
int clamped = NonMessSystem.clampMess(value);
switch (slot) {
case "v":
holder.setVMess(clamped);
break;
case "a":
holder.setAMess(clamped);
break;
case "m":
holder.setMMess(clamped);
break;
default:
source.sendError(Text.literal("未知污渍槽位 '" + slot + "'. Use v, a, or m."));
return 0;
}
NonMessSystem.broadcastMessState(target);
maybeGrantMessedUp(target, holder);
sendMessSetFeedback(source, target, holder);
return 1;
} else {
source.sendError(Text.literal("目标没有污渍数据。"));
return 0;
}
}
private static void sendMessSetFeedback(ServerCommandSource source, ServerPlayerEntity target, MessHolder holder) {
source.sendFeedback(
() -> Text.literal(
target.getName().getString()
+ " mess set to V="
+ NonMessSystem.clampMess(holder.getVMess())
+ ", A="
+ NonMessSystem.clampMess(holder.getAMess())
+ ", M="
+ NonMessSystem.clampMess(holder.getMMess())
+ "."
),
true
);
}
private static void maybeGrantMessedUp(ServerPlayerEntity target, MessHolder holder) {
if (NonMessSystem.clampMess(holder.getVMess()) >= 10
&& NonMessSystem.clampMess(holder.getAMess()) >= 10
&& NonMessSystem.clampMess(holder.getMMess()) >= 10) {
NonAdvancementHooks.grantMessedUp(target);
}
}
private static int getGenderCommand(ServerCommandSource source, ServerPlayerEntity target) {
source.sendFeedback(() -> Text.literal(target.getName().getString() + " gender: " + genderName(currentGenderMask(target))), false);
return 1;
}
private static int setGenderCommand(ServerCommandSource source, ServerPlayerEntity target, String genderRaw) {
int mask = parseGenderMask(genderRaw);
if (mask == 0) {
source.sendError(Text.literal("未知性别 '" + genderRaw + "'. Use male, female, or both."));
return 0;
} else {
int applied = NonGenderSystem.setPlayerGenderFromCommand(target, mask);
source.sendFeedback(() -> Text.literal(target.getName().getString() + " gender set to " + genderName(applied) + "."), true);
return 1;
}
}
private static int resetGenderCommand(ServerCommandSource source, ServerPlayerEntity target) {
NonGenderSystem.resetPlayerGenderSelection(target);
source.sendFeedback(() -> Text.literal(target.getName().getString() + " gender selection reset."), true);
return 1;
}
private static int currentGenderMask(ServerPlayerEntity target) {
return target instanceof GenderHolder holder ? holder.getGenderMask() & 3 : 0;
}
private static int parseGenderMask(String raw) {
if (raw == null) {
return 0;
}
return switch (raw.trim().toLowerCase(Locale.ROOT)) {
case "m", "male" -> 1;
case "f", "female" -> 2;
case "b", "both", "male+female", "female+male" -> 3;
default -> 0;
};
}
private static String genderName(int mask) {
return switch (mask & 3) {
case 1 -> "male";
case 2 -> "female";
case 3 -> "male+female";
default -> "none";
};
}
private static int setDestroyedSkinStageCommand(ServerCommandSource source, ServerPlayerEntity target, int stage) {
if (!NeedsOfNature.isDestroyedSkinSystemEnabled()) {
source.sendError(Text.literal("撕裂皮肤系统已禁用。"));
return 0;
}
int clamped = Math.max(0, Math.min(4, stage));
if (target instanceof DestroyedSkinHolder holder) {
holder.setDestroyedSkinDamage(NonDestroyedSkinSystem.destroyedSkinDamageForStage(clamped));
}
NonDestroyedSkinSystem.setDestroyedSkinStage(target, clamped);
source.sendFeedback(
() -> Text.literal(
target.getName().getString()
+ " ripped skin stage set to "
+ clamped
+ " (damage "
+ NonDestroyedSkinSystem.destroyedSkinDamageForStage(clamped)
+ "/10)."
),
true
);
return clamped;
}
private static int resetDestroyedSkinCommand(ServerCommandSource source, ServerPlayerEntity target) {
if (!NeedsOfNature.isDestroyedSkinSystemEnabled()) {
source.sendError(Text.literal("撕裂皮肤系统已禁用。"));
return 0;
} else {
NonDestroyedSkinSystem.resetDestroyedSkinDamage(target);
source.sendFeedback(() -> Text.literal(target.getName().getString() + " ripped skin damage reset."), true);
return 1;
}
}
private static int sendPregnantEntity(ServerCommandSource source, ServerPlayerEntity target) {
Identifier pregnantEntity = NeedsOfNature.getPregnantEntityTypeId(target);
if (pregnantEntity == null) {
source.sendFeedback(() -> Text.literal(target.getName().getString() + " is not pregnant."), false);
return 0;
} else {
source.sendFeedback(() -> Text.literal(target.getName().getString() + " pregnant entity: " + pregnantEntity), false);
return 1;
}
}
private static int setPregnancyCycleDayCommand(ServerCommandSource source, ServerPlayerEntity target, int day) {
if (!NonPregnancyConceptionSystem.setCycleDay(target, day)) {
source.sendError(Text.literal("目标没有孕期周期数据。"));
return 0;
} else {
source.sendFeedback(() -> Text.literal(target.getName().getString() + " pregnancy cycle set to day " + day + "/30."), true);
return 1;
}
}
private static int setPregnancyCommand(ServerCommandSource source, ServerPlayerEntity target, String entityIdRaw, int minutes, int offspringCountOverride) {
if (NonGenderSystem.isOnlyMale(target)) {
source.sendError(Text.literal("无法为纯男性玩家分娩。"));
return 0;
}
Identifier entityId = Identifier.tryParse(entityIdRaw);
if (entityId != null && Registries.ENTITY_TYPE.containsId(entityId)) {
if (target.getEntityWorld() instanceof ServerWorld world) {
long var13 = NeedsOfNature.getGlobalTick(world);
long durationTicks = Math.max(1L, Math.min(5256000L, minutes)) * 1200L;
NeedsOfNature.beginPregnancy(target, entityId, var13 + durationTicks, null, offspringCountOverride);
int resolvedDisplaySpawnCount = ((PregnancyHolder)target).getPregnancyOffspringCount();
if (resolvedDisplaySpawnCount <= 0) {
resolvedDisplaySpawnCount = offspringCountOverride > 0 ? Math.max(1, Math.min(16, offspringCountOverride)) : 1;
}
int displaySpawnCount = resolvedDisplaySpawnCount;
source.sendFeedback(
() -> Text.literal(
"Set pregnancy for "
+ target.getName().getString()
+ " to "
+ entityId
+ " ("
+ minutes
+ " minutes, "
+ displaySpawnCount
+ " spawn"
+ (displaySpawnCount == 1 ? "" : "s")
+ ")."
),
true
);
return 1;
} else {
source.sendError(Text.literal("目标不在服务器世界中。"));
return 0;
}
} else {
source.sendError(Text.literal("未知实体 ID: " + entityIdRaw));
return 0;
}
}
private static int clearPregnancyCommand(ServerCommandSource source, ServerPlayerEntity target) {
NeedsOfNature.clearPregnancyState(target, true);
source.sendFeedback(() -> Text.literal("已清除 " + target.getName().getString() + "."), true);
return 1;
}
private static int completePregnancyCommand(ServerCommandSource source, ServerPlayerEntity target) {
Identifier pregnantEntity = NeedsOfNature.getPregnantEntityTypeId(target);
if (pregnantEntity == null) {
source.sendError(Text.literal(target.getName().getString() + " is not pregnant."));
return 0;
} else if (target.getEntityWorld() instanceof ServerWorld world) {
NeedsOfNature.markPregnancyPending(target, NeedsOfNature.getGlobalTick(world));
source.sendFeedback(() -> Text.literal("已标记 " + target.getName().getString() + "."), true);
return 1;
} else {
source.sendError(Text.literal("目标不在服务器世界中。"));
return 0;
}
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,73 @@
package com.nonid;
import com.nonid.data.NonAccessoryBehavior;
import com.nonid.data.NonAccessoryDefinitions;
import dev.emi.trinkets.api.SlotReference;
import dev.emi.trinkets.api.Trinket;
import dev.emi.trinkets.api.TrinketComponent;
import dev.emi.trinkets.api.TrinketsApi;
import java.util.Objects;
import net.minecraft.entity.LivingEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.registry.Registries;
import net.minecraft.util.Pair;
public final class NonDataAccessoryTrinket implements Trinket {
public static final NonDataAccessoryTrinket INSTANCE = new NonDataAccessoryTrinket();
private NonDataAccessoryTrinket() {
}
public boolean canEquip(ItemStack stack, SlotReference slot, LivingEntity entity) {
if (stack != null && !stack.isEmpty() && slot != null && entity != null) {
NonAccessoryBehavior behavior = NonAccessoryDefinitions.getBehavior(Registries.ITEM.getId(stack.getItem()));
String targetSlot = slot.getId();
return !NonTrinketsIntegration.isSlotAvailableFor(entity, targetSlot)
? false
: TrinketsApi.getTrinketComponent(entity).map(component -> canEquipWithCurrent(component, stack, slot, behavior, targetSlot)).orElse(true);
} else {
return false;
}
}
public void onEquip(ItemStack stack, SlotReference slot, LivingEntity entity) {
NonTrinketsCompat.onAccessoryEquipped(entity, slot);
}
public void onUnequip(ItemStack stack, SlotReference slot, LivingEntity entity) {
NonTrinketsCompat.onAccessoryUnequipped(entity, slot);
}
private static boolean canEquipWithCurrent(
TrinketComponent component, ItemStack stack, SlotReference target, NonAccessoryBehavior behavior, String targetSlot
) {
String exclusiveGroup = behavior.exclusiveGroup();
for (Pair<SlotReference, ItemStack> pair : component.getAllEquipped()) {
SlotReference otherSlot = (SlotReference)pair.getLeft();
ItemStack otherStack = (ItemStack)pair.getRight();
if (otherSlot != null && otherStack != null && !otherStack.isEmpty() && !isSameSlot(otherSlot, target)) {
NonAccessoryBehavior otherBehavior = NonAccessoryDefinitions.getBehavior(Registries.ITEM.getId(otherStack.getItem()));
if (otherBehavior.occupiesSlot(targetSlot)) {
return false;
}
for (String occupied : behavior.occupiesSlots()) {
if (NonAccessoryBehavior.isSlot(otherSlot.getId(), occupied)) {
return false;
}
}
if (exclusiveGroup != null && !exclusiveGroup.isBlank() && exclusiveGroup.equals(otherBehavior.exclusiveGroup())) {
return false;
}
}
}
return true;
}
private static boolean isSameSlot(SlotReference first, SlotReference second) {
return first != null && second != null && first.index() == second.index() && Objects.equals(first.getId(), second.getId());
}
}
@@ -0,0 +1,9 @@
package com.nonid;
public enum NonDebugChatCategory {
INFO,
WARNING,
SETUP,
ERROR,
ALWAYS;
}
@@ -0,0 +1,45 @@
package com.nonid;
public enum NonDebugChatMode {
ALL("all"),
SETUP_WARNINGS_ERRORS("setup_warnings_errors"),
SETUP_ERRORS("setup_errors"),
ERRORS_ONLY("errors_only");
private final String id;
NonDebugChatMode(String id) {
this.id = id;
}
public String id() {
return this.id;
}
public boolean allows(NonDebugChatCategory category) {
if (category == NonDebugChatCategory.ALWAYS) {
return true;
}
return switch (this) {
case ALL -> true;
case SETUP_WARNINGS_ERRORS -> category == NonDebugChatCategory.SETUP
|| category == NonDebugChatCategory.WARNING
|| category == NonDebugChatCategory.ERROR;
case SETUP_ERRORS -> category == NonDebugChatCategory.SETUP || category == NonDebugChatCategory.ERROR;
case ERRORS_ONLY -> category == NonDebugChatCategory.ERROR;
};
}
public static NonDebugChatMode fromId(String id, NonDebugChatMode fallback) {
if (id != null) {
for (NonDebugChatMode mode : values()) {
if (mode.id.equalsIgnoreCase(id)) {
return mode;
}
}
}
return fallback;
}
}
@@ -0,0 +1,142 @@
package com.nonid;
import com.nonid.data.NonPeakStages;
import com.nonid.internal.animation.api.AnimationService;
import com.nonid.internal.animation.data.AnimationDefinitions;
import com.nonid.internal.animation.util.WeightedPicker;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.function.Function;
import java.util.function.ToDoubleFunction;
import net.minecraft.entity.Entity;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
final class NonDefeatedAnimationSelector {
private static final String DEFEATED_TAG_PREFIX = "defeat.";
private NonDefeatedAnimationSelector() {
}
@Nullable
static AnimationService.MatchedAnimation find(ServerWorld world, ServerPlayerEntity player, @Nullable Identifier sourceAnimationId) {
if (world != null && player != null && player.getEntityWorld() == world) {
List<Entity> actors = List.of(player);
List<AnimationDefinitions.Definition> candidates = new ArrayList<>();
for (AnimationDefinitions.Definition definition : AnimationDefinitions.findActorMatchingCandidates(actors)) {
if (isDefeatedDefinition(definition) && NonInjectorMatchPolicy.allowsAutomaticNonMatch(world, definition, actors)) {
candidates.add(definition);
}
}
AnimationDefinitions.Definition picked = pickPreferredCandidate(
candidates,
AnimationDefinitions.Definition::contentTags,
AnimationDefinitions.Definition::weight,
sourceContentTags(sourceAnimationId),
world.getRandom().nextDouble()
);
if (picked == null) {
return null;
}
List<String> actorKeys = AnimationDefinitions.resolveActorKeys(picked, actors, world.getRandom());
return actorKeys.size() != actors.size()
? null
: new AnimationService.MatchedAnimation(picked.id(), List.copyOf(actorKeys), picked.stages() == null ? List.of() : List.copyOf(picked.stages()));
} else {
return null;
}
}
@Nullable
static <T> T pickPreferredCandidate(
List<T> candidates,
Function<T, ? extends Collection<String>> contentTags,
ToDoubleFunction<T> weight,
Collection<String> sourceContentTags,
double randomValue
) {
if (candidates != null && !candidates.isEmpty() && contentTags != null && weight != null) {
Set<String> sourceTags = normalizeTags(sourceContentTags);
List<T> preferred = new ArrayList<>();
for (T candidate : candidates) {
if (candidate != null) {
Set<String> candidateTags = normalizeTags((Collection<String>)contentTags.apply(candidate));
if (sharesContentTag(sourceTags, candidateTags)) {
preferred.add(candidate);
}
}
}
List<T> pool = preferred.isEmpty() ? candidates : preferred;
return WeightedPicker.pick(pool, weight, randomValue);
} else {
return null;
}
}
private static boolean isDefeatedDefinition(AnimationDefinitions.Definition definition) {
if (definition != null && definition.animationTags() != null) {
for (String tag : definition.animationTags()) {
if (tag != null && tag.startsWith("defeat.")) {
return true;
}
}
return false;
} else {
return false;
}
}
private static List<String> sourceContentTags(@Nullable Identifier sourceAnimationId) {
if (sourceAnimationId == null) {
return List.of();
}
AnimationDefinitions.Definition source = AnimationDefinitions.getDefinition(NonPeakStages.baseAnimationId(sourceAnimationId));
return source != null && source.contentTags() != null ? source.contentTags() : List.of();
}
private static boolean sharesContentTag(Set<String> first, Set<String> second) {
if (!first.isEmpty() && !second.isEmpty()) {
for (String tag : first) {
if (second.contains(tag)) {
return true;
}
}
return false;
} else {
return first.isEmpty() && second.isEmpty();
}
}
private static Set<String> normalizeTags(Collection<String> tags) {
if (tags != null && !tags.isEmpty()) {
LinkedHashSet<String> normalized = new LinkedHashSet<>();
for (String raw : tags) {
if (raw != null) {
String tag = raw.trim().toLowerCase(Locale.ROOT);
if (!tag.isEmpty()) {
normalized.add(tag);
}
}
}
return normalized.isEmpty() ? Set.of() : Set.copyOf(normalized);
} else {
return Set.of();
}
}
}
@@ -0,0 +1,697 @@
package com.nonid;
import com.nonid.network.DestroyedSkinMaskSyncS2CPayload;
import com.nonid.network.DestroyedSkinMaskUploadC2SPayload;
import com.nonid.network.DestroyedSkinParticlesS2CPayload;
import com.nonid.network.DestroyedSkinStateS2CPayload;
import com.nonid.network.DestroyedSkinSyncS2CPayload;
import com.nonid.network.DestroyedSkinUploadC2SPayload;
import com.nonid.network.ManualDamageDestroyedSkinC2SPayload;
import com.nonid.network.RepairDestroyedSkinC2SPayload;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.registry.Registries;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.sound.SoundCategory;
import net.minecraft.sound.SoundEvent;
import net.minecraft.sound.SoundEvents;
final class NonDestroyedSkinSystem {
private static final int DESTROYED_SKIN_MAX_DAMAGE = 10;
private static final int DESTROYED_SKIN_STAGE_1_DAMAGE = 1;
private static final int DESTROYED_SKIN_STAGE_2_DAMAGE = 4;
private static final int DESTROYED_SKIN_STAGE_3_DAMAGE = 7;
private static final int DESTROYED_SKIN_STAGE_4_DAMAGE = 10;
private static final int DESTROYED_SKIN_REPAIR_COOLDOWN_TICKS = 7;
private static final int UPLOAD_COOLDOWN_TICKS = 20;
private static final Map<UUID, byte[]> DESTROYED_SKINS_BY_PLAYER = new ConcurrentHashMap<>();
private static final Map<UUID, byte[][]> DESTROYED_SKIN_MASKS_BY_PLAYER = new ConcurrentHashMap<>();
private static final Map<UUID, Integer> DESTROYED_SKIN_STAGE_BY_PLAYER = new ConcurrentHashMap<>();
private static final Map<UUID, Long> DESTROYED_SKIN_REPAIR_COOLDOWNS = new ConcurrentHashMap<>();
private static final Map<NonDestroyedSkinSystem.UploadKey, Long> NEXT_UPLOAD_TICK = new ConcurrentHashMap<>();
private NonDestroyedSkinSystem() {
}
static void syncDestroyedSkins(ServerPlayerEntity player) {
if (player != null) {
MinecraftServer server = player.getEntityWorld().getServer();
if (server != null) {
boolean enabled = NeedsOfNature.isDestroyedSkinSystemEnabled();
for (ServerPlayerEntity listedPlayer : server.getPlayerManager().getPlayerList()) {
UUID uuid = listedPlayer.getUuid();
if (!enabled) {
sendDestroyedSkinStage(player, uuid, 0);
} else {
byte[] skin = DESTROYED_SKINS_BY_PLAYER.get(uuid);
if (skin != null && skin.length > 0) {
sendDestroyedSkin(player, uuid, getDestroyedSkinStage(uuid), skin);
} else {
sendDestroyedSkinStage(player, uuid, getDestroyedSkinStage(uuid));
}
byte[][] masks = DESTROYED_SKIN_MASKS_BY_PLAYER.get(uuid);
if (masks != null) {
for (int stage = 1; stage < Math.min(masks.length, 4); stage++) {
byte[] bytes = masks[stage];
if (bytes != null && bytes.length > 0) {
sendDestroyedSkinMask(player, uuid, stage, bytes);
}
}
}
}
}
}
}
}
static void registerDestroyedSkinNetworking() {
ServerPlayNetworking.registerGlobalReceiver(
DestroyedSkinUploadC2SPayload.ID,
(payload, context) -> context.server()
.execute(
() -> {
ServerPlayerEntity player = context.player();
if (player != null) {
if (NeedsOfNature.isDestroyedSkinSystemEnabled()) {
byte[] bytes = payload.pngBytes();
UUID playerUuid = player.getUuid();
if (acceptUpload(player, new NonDestroyedSkinSystem.UploadKey(playerUuid, 0))) {
if (!isValidDestroyedSkinPayload(bytes)) {
NeedsOfNature.LOGGER
.warn(
"[NoN] Ignoring invalid destroyed skin upload from {} ({} bytes).",
player.getName().getString(),
bytes == null ? 0 : bytes.length
);
} else {
byte[] previous = DESTROYED_SKINS_BY_PLAYER.get(playerUuid);
if (!Arrays.equals(previous, bytes)) {
DESTROYED_SKINS_BY_PLAYER.put(playerUuid, (byte[])bytes.clone());
NeedsOfNature.logRuntime(
"[NoN] Received destroyed skin upload from {} ({} bytes, stage {}).",
player.getName().getString(),
bytes.length,
getDestroyedSkinStage(playerUuid)
);
broadcastDestroyedSkin(context.server(), playerUuid, getDestroyedSkinStage(playerUuid), bytes);
}
}
}
}
}
}
)
);
ServerPlayNetworking.registerGlobalReceiver(
DestroyedSkinMaskUploadC2SPayload.ID,
(payload, context) -> context.server()
.execute(
() -> {
ServerPlayerEntity player = context.player();
if (player != null) {
if (NeedsOfNature.isDestroyedSkinSystemEnabled()) {
int stage = payload.stage();
byte[] bytes = payload.pngBytes();
if (stage > 0 && stage < 4 && bytes != null) {
UUID playerUuid = player.getUuid();
if (acceptUpload(player, new NonDestroyedSkinSystem.UploadKey(playerUuid, stage))) {
byte[][] existingMasks = DESTROYED_SKIN_MASKS_BY_PLAYER.get(playerUuid);
byte[] previous = existingMasks == null ? null : existingMasks[stage];
if (!Arrays.equals(previous, bytes.length == 0 ? null : bytes)) {
if (bytes.length == 0) {
byte[][] masks = existingMasks;
if (masks != null && stage < masks.length) {
masks[stage] = null;
if (!hasAnyMask(masks)) {
DESTROYED_SKIN_MASKS_BY_PLAYER.remove(playerUuid);
}
}
broadcastDestroyedSkinMask(context.server(), playerUuid, stage, bytes);
} else if (!isValidDestroyedSkinMaskPayload(bytes)) {
NeedsOfNature.LOGGER
.warn(
"[NoN] Ignoring invalid destroyed skin mask upload from {} (stage {}, {} bytes).",
new Object[]{player.getName().getString(), stage, bytes.length}
);
} else {
byte[][] masks = DESTROYED_SKIN_MASKS_BY_PLAYER.computeIfAbsent(playerUuid, ignored -> new byte[4][]);
masks[stage] = (byte[])bytes.clone();
NeedsOfNature.logRuntime(
"[NoN] Received destroyed skin mask upload from {} (stage {}, {} bytes).",
player.getName().getString(),
stage,
bytes.length
);
broadcastDestroyedSkinMask(context.server(), playerUuid, stage, bytes);
}
}
}
} else {
NeedsOfNature.LOGGER
.warn(
"[NoN] Ignoring invalid destroyed skin mask upload from {} (stage {}, {} bytes).",
new Object[]{player.getName().getString(), stage, bytes == null ? 0 : bytes.length}
);
}
}
}
}
)
);
}
static void registerDestroyedSkinRepairNetworking() {
ServerPlayNetworking.registerGlobalReceiver(
RepairDestroyedSkinC2SPayload.ID, (payload, context) -> context.server().execute(() -> repairDestroyedSkin(context.player()))
);
ServerPlayNetworking.registerGlobalReceiver(
ManualDamageDestroyedSkinC2SPayload.ID, (payload, context) -> context.server().execute(() -> manuallyDamageDestroyedSkin(context.player()))
);
}
private static void repairDestroyedSkin(ServerPlayerEntity player) {
if (NeedsOfNature.isDestroyedSkinSystemEnabled()) {
if (player != null && player.isAlive() && player instanceof DestroyedSkinHolder holder) {
if (player.getEntityWorld() instanceof ServerWorld world) {
if (NonSkinRepairBlocks.isNearRepairBlock(world, player.getBlockPos(), NeedsOfNature.getConfig())) {
UUID uuid = player.getUuid();
long now = world.getTime();
long blockedUntil = DESTROYED_SKIN_REPAIR_COOLDOWNS.getOrDefault(uuid, 0L);
if (now >= blockedUntil) {
int beforeDamage = clampDestroyedSkinDamage(holder.getDestroyedSkinDamage());
if (beforeDamage <= 0) {
DESTROYED_SKIN_REPAIR_COOLDOWNS.remove(uuid);
} else {
int afterDamage = clampDestroyedSkinDamage(beforeDamage - 1);
holder.setDestroyedSkinDamage(afterDamage);
setDestroyedSkinStage(player, destroyedSkinStageForDamage(afterDamage));
playDestroyedSkinRepairSound(world, player);
if (afterDamage > 0) {
DESTROYED_SKIN_REPAIR_COOLDOWNS.put(uuid, now + 7L);
} else {
DESTROYED_SKIN_REPAIR_COOLDOWNS.remove(uuid);
}
}
}
}
}
}
}
}
private static void manuallyDamageDestroyedSkin(ServerPlayerEntity player) {
if (NeedsOfNature.isDestroyedSkinSystemEnabled()) {
if (player != null && player.isAlive()) {
if (player.getEntityWorld() instanceof ServerWorld world) {
if (hasShearsForManualDestroyedSkinDamage(player)) {
int beforeDamage = player instanceof DestroyedSkinHolder holder ? clampDestroyedSkinDamage(holder.getDestroyedSkinDamage()) : 10;
applyDestroyedSkinAttackDamage(world, player);
int afterDamage = player instanceof DestroyedSkinHolder holder ? clampDestroyedSkinDamage(holder.getDestroyedSkinDamage()) : beforeDamage;
if (afterDamage > beforeDamage) {
playDestroyedSkinShearSound(world, player);
}
}
}
}
}
}
private static boolean hasShearsForManualDestroyedSkinDamage(ServerPlayerEntity player) {
if (player == null) {
return false;
}
ItemStack cursorStack = player.currentScreenHandler != null ? player.currentScreenHandler.getCursorStack() : ItemStack.EMPTY;
return cursorStack.isOf(Items.SHEARS) || player.getMainHandStack().isOf(Items.SHEARS) || player.getOffHandStack().isOf(Items.SHEARS);
}
private static boolean isValidDestroyedSkinPayload(byte[] bytes) {
if (bytes != null && bytes.length > 0 && bytes.length <= 1048576) {
int[] dimensions = readPngDimensions(bytes);
if (dimensions == null) {
return false;
}
int width = dimensions[0];
int height = dimensions[1];
return width == height && width >= 64 && width <= 512 && width % 64 == 0 && hasValidPngChunkStructure(bytes);
} else {
return false;
}
}
private static boolean isValidDestroyedSkinMaskPayload(byte[] bytes) {
if (bytes != null && bytes.length > 0 && bytes.length <= 65536) {
int[] dimensions = readPngDimensions(bytes);
return dimensions != null && dimensions[0] == 64 && dimensions[1] == 64 && hasValidPngChunkStructure(bytes);
} else {
return false;
}
}
private static boolean acceptUpload(ServerPlayerEntity player, NonDestroyedSkinSystem.UploadKey key) {
long now = player.getEntityWorld().getTime();
long next = NEXT_UPLOAD_TICK.getOrDefault(key, Long.MIN_VALUE);
if (now < next) {
return false;
}
NEXT_UPLOAD_TICK.put(key, now + 20L);
return true;
}
private static boolean hasValidPngChunkStructure(byte[] bytes) {
int offset = 8;
boolean sawIdat = false;
boolean first = true;
while (offset + 12 <= bytes.length) {
long length = Integer.toUnsignedLong(readBigEndianInt(bytes, offset));
if (length > bytes.length - offset - 12L) {
return false;
}
int typeOffset = offset + 4;
boolean ihdr = chunkTypeEquals(bytes, typeOffset, 'I', 'H', 'D', 'R');
boolean idat = chunkTypeEquals(bytes, typeOffset, 'I', 'D', 'A', 'T');
boolean iend = chunkTypeEquals(bytes, typeOffset, 'I', 'E', 'N', 'D');
if (first && (!ihdr || length != 13L)) {
return false;
}
first = false;
sawIdat |= idat;
offset += Math.toIntExact(length + 12L);
if (iend) {
return length == 0L && sawIdat && offset == bytes.length;
}
}
return false;
}
private static boolean chunkTypeEquals(byte[] bytes, int offset, char a, char b, char c, char d) {
return offset >= 0
&& offset + 4 <= bytes.length
&& bytes[offset] == (byte)a
&& bytes[offset + 1] == (byte)b
&& bytes[offset + 2] == (byte)c
&& bytes[offset + 3] == (byte)d;
}
static void clearPlayerRuntimeState(MinecraftServer server, UUID playerUuid) {
if (playerUuid != null) {
broadcastDestroyedSkin(server, playerUuid, 0, new byte[0]);
for (int stage = 1; stage < 4; stage++) {
broadcastDestroyedSkinMask(server, playerUuid, stage, new byte[0]);
}
broadcastDestroyedSkinStage(server, playerUuid, 0);
DESTROYED_SKINS_BY_PLAYER.remove(playerUuid);
DESTROYED_SKIN_MASKS_BY_PLAYER.remove(playerUuid);
DESTROYED_SKIN_STAGE_BY_PLAYER.remove(playerUuid);
DESTROYED_SKIN_REPAIR_COOLDOWNS.remove(playerUuid);
NEXT_UPLOAD_TICK.keySet().removeIf(key -> playerUuid.equals(key.playerUuid()));
}
}
static void clearRuntimeState() {
DESTROYED_SKINS_BY_PLAYER.clear();
DESTROYED_SKIN_MASKS_BY_PLAYER.clear();
DESTROYED_SKIN_STAGE_BY_PLAYER.clear();
DESTROYED_SKIN_REPAIR_COOLDOWNS.clear();
NEXT_UPLOAD_TICK.clear();
}
private static boolean hasAnyMask(byte[][] masks) {
if (masks == null) {
return false;
}
for (int stage = 1; stage < masks.length; stage++) {
if (masks[stage] != null && masks[stage].length > 0) {
return true;
}
}
return false;
}
private static int[] readPngDimensions(byte[] bytes) {
if (bytes == null || bytes.length < 24) {
return null;
} else if ((bytes[0] & 255) == 137
&& bytes[1] == 80
&& bytes[2] == 78
&& bytes[3] == 71
&& bytes[4] == 13
&& bytes[5] == 10
&& bytes[6] == 26
&& bytes[7] == 10
&& bytes[8] == 0
&& bytes[9] == 0
&& bytes[10] == 0
&& bytes[11] == 13
&& bytes[12] == 73
&& bytes[13] == 72
&& bytes[14] == 68
&& bytes[15] == 82) {
int width = readBigEndianInt(bytes, 16);
int height = readBigEndianInt(bytes, 20);
return width > 0 && height > 0 ? new int[]{width, height} : null;
} else {
return null;
}
}
private static int readBigEndianInt(byte[] bytes, int offset) {
return (bytes[offset] & 0xFF) << 24 | (bytes[offset + 1] & 0xFF) << 16 | (bytes[offset + 2] & 0xFF) << 8 | bytes[offset + 3] & 0xFF;
}
private static int getDestroyedSkinStage(UUID playerUuid) {
return playerUuid == null ? 0 : Math.max(0, Math.min(4, DESTROYED_SKIN_STAGE_BY_PLAYER.getOrDefault(playerUuid, 0)));
}
static int getDestroyedSkinStageForPlayer(ServerPlayerEntity player) {
if (player == null) {
return 0;
} else if (!NeedsOfNature.isDestroyedSkinSystemEnabled()) {
return 0;
} else {
return player instanceof DestroyedSkinHolder holder
? Math.max(0, Math.min(4, holder.getDestroyedSkinStage()))
: getDestroyedSkinStage(player.getUuid());
}
}
static void broadcastDestroyedSkinStage(ServerPlayerEntity player) {
if (player != null) {
MinecraftServer server = player.getEntityWorld().getServer();
if (server != null) {
broadcastDestroyedSkinStage(server, player.getUuid(), getDestroyedSkinStageForPlayer(player));
}
}
}
static void syncDestroyedSkinStageToOtherPlayers(ServerPlayerEntity player) {
if (player != null) {
MinecraftServer server = player.getEntityWorld().getServer();
if (server != null) {
int stage = getDestroyedSkinStageForPlayer(player);
for (ServerPlayerEntity target : server.getPlayerManager().getPlayerList()) {
if (!target.getUuid().equals(player.getUuid())) {
sendDestroyedSkinStage(target, player.getUuid(), stage);
}
}
}
}
}
static void cacheDestroyedSkinStage(ServerPlayerEntity player) {
if (player != null) {
if (!NeedsOfNature.isDestroyedSkinSystemEnabled()) {
DESTROYED_SKIN_STAGE_BY_PLAYER.remove(player.getUuid());
} else if (player instanceof DestroyedSkinHolder holder) {
int damage = clampDestroyedSkinDamage(holder.getDestroyedSkinDamage());
int stage = damage > 0 ? destroyedSkinStageForDamage(damage) : Math.max(0, Math.min(4, holder.getDestroyedSkinStage()));
if (stage == 0) {
DESTROYED_SKIN_STAGE_BY_PLAYER.remove(player.getUuid());
} else {
DESTROYED_SKIN_STAGE_BY_PLAYER.put(player.getUuid(), stage);
}
}
}
}
static void setDestroyedSkinStage(ServerPlayerEntity player, int stage) {
if (player != null) {
UUID uuid = player.getUuid();
if (!NeedsOfNature.isDestroyedSkinSystemEnabled()) {
clearDestroyedSkinStoredState(player);
if (player.getEntityWorld() instanceof ServerWorld world) {
broadcastDestroyedSkinStage(world.getServer(), uuid, 0);
}
} else {
int clamped = Math.max(0, Math.min(4, stage));
if (player instanceof DestroyedSkinHolder holder) {
holder.setDestroyedSkinStage(clamped);
}
if (clamped == 0) {
DESTROYED_SKIN_STAGE_BY_PLAYER.remove(uuid);
} else {
DESTROYED_SKIN_STAGE_BY_PLAYER.put(uuid, clamped);
}
if (player.getEntityWorld() instanceof ServerWorld world) {
broadcastDestroyedSkinStage(world.getServer(), uuid, clamped);
}
NonApiInternals.fireDestroyedSkinChanged(player);
NeedsOfNature.checkBadIdeaBeaconAdvancement(player);
}
}
}
static void applyDestroyedSkinAttackDamage(ServerWorld world, ServerPlayerEntity player) {
if (NeedsOfNature.isDestroyedSkinSystemEnabled()) {
if (world != null && player != null && player instanceof DestroyedSkinHolder holder) {
int damageGain = rollDestroyedSkinDamage(world, player);
applyDestroyedSkinDamage(world, player, holder, damageGain);
}
}
}
static void applyDestroyedSkinVoluntaryJoinInitialDamage(ServerWorld world, ServerPlayerEntity player) {
if (NeedsOfNature.isDestroyedSkinSystemEnabled()) {
if (world != null && player != null && player instanceof DestroyedSkinHolder holder) {
int beforeDamage = clampDestroyedSkinDamage(holder.getDestroyedSkinDamage());
if (beforeDamage == 0) {
applyDestroyedSkinDamage(world, player, holder, 1);
}
}
}
}
private static void applyDestroyedSkinDamage(ServerWorld world, ServerPlayerEntity player, DestroyedSkinHolder holder, int damageGain) {
if (world != null && player != null && holder != null && damageGain > 0) {
int beforeDamage = clampDestroyedSkinDamage(holder.getDestroyedSkinDamage());
int afterDamage = clampDestroyedSkinDamage(beforeDamage + damageGain);
if (afterDamage != beforeDamage) {
holder.setDestroyedSkinDamage(afterDamage);
int beforeStage = destroyedSkinStageForDamage(beforeDamage);
int afterStage = destroyedSkinStageForDamage(afterDamage);
setDestroyedSkinStage(player, afterStage);
playDestroyedSkinRipSound(world, player);
broadcastDestroyedSkinRipParticles(world, player);
if (afterStage != beforeStage) {
NeedsOfNature.logRuntime("[NoN] {} destroyed skin advanced to stage {} ({}/{}).", player.getName().getString(), afterStage, afterDamage, 10);
}
}
}
}
private static int rollDestroyedSkinDamage(ServerWorld world, ServerPlayerEntity player) {
double multiplier = NonTrinketsIntegration.getAccessoryEffects(player).destroyedSkinDamageMultiplier();
if (Double.isFinite(multiplier) && !(multiplier <= 0.0)) {
int whole = (int)Math.floor(multiplier);
double fractional = multiplier - whole;
if (fractional > 0.0 && world.getRandom().nextDouble() < fractional) {
whole++;
}
return Math.max(0, whole);
} else {
return 0;
}
}
static void resetDestroyedSkinDamage(ServerPlayerEntity player) {
if (player != null) {
if (player instanceof DestroyedSkinHolder holder) {
holder.setDestroyedSkinDamage(0);
}
setDestroyedSkinStage(player, 0);
}
}
static void clearDestroyedSkinStoredState(ServerPlayerEntity player) {
if (player != null) {
UUID uuid = player.getUuid();
if (player instanceof DestroyedSkinHolder holder) {
holder.setDestroyedSkinDamage(0);
holder.setDestroyedSkinStage(0);
}
DESTROYED_SKIN_STAGE_BY_PLAYER.remove(uuid);
DESTROYED_SKIN_REPAIR_COOLDOWNS.remove(uuid);
}
}
private static int clampDestroyedSkinDamage(int damage) {
return Math.max(0, Math.min(10, damage));
}
private static int destroyedSkinStageForDamage(int damage) {
int clamped = clampDestroyedSkinDamage(damage);
if (clamped >= 10) {
return 4;
} else if (clamped >= 7) {
return 3;
} else if (clamped >= 4) {
return 2;
} else {
return clamped >= 1 ? 1 : 0;
}
}
static int destroyedSkinDamageForStage(int stage) {
return switch (Math.max(0, Math.min(4, stage))) {
case 1 -> 1;
case 2 -> 4;
case 3 -> 7;
case 4 -> 10;
default -> 0;
};
}
private static void playDestroyedSkinRipSound(ServerWorld world, ServerPlayerEntity player) {
if (world != null && player != null) {
int index = 1 + world.getRandom().nextInt(3);
String soundPath = "rip" + String.format(Locale.ROOT, "%02d", index);
float pitch = 0.94F + world.getRandom().nextFloat() * 0.12F;
world.playSound(
null,
player.getX(),
player.getY() + player.getStandingEyeHeight() * 0.5,
player.getZ(),
(SoundEvent)Registries.SOUND_EVENT.get(NeedsOfNature.id(soundPath)),
SoundCategory.PLAYERS,
0.75F,
pitch
);
}
}
private static void playDestroyedSkinShearSound(ServerWorld world, ServerPlayerEntity player) {
if (world != null && player != null) {
world.playSound(
null,
player.getX(),
player.getY() + player.getStandingEyeHeight() * 0.5,
player.getZ(),
SoundEvents.ENTITY_SHEEP_SHEAR,
SoundCategory.PLAYERS,
0.8F,
0.95F + world.getRandom().nextFloat() * 0.1F
);
}
}
private static void playDestroyedSkinRepairSound(ServerWorld world, ServerPlayerEntity player) {
if (world != null && player != null) {
world.playSound(
null,
player.getX(),
player.getY() + player.getStandingEyeHeight() * 0.5,
player.getZ(),
SoundEvents.BLOCK_WOOL_PLACE,
SoundCategory.PLAYERS,
1.35F,
1.15F + world.getRandom().nextFloat() * 0.15F
);
}
}
private static void broadcastDestroyedSkinRipParticles(ServerWorld world, ServerPlayerEntity player) {
if (world != null && player != null) {
int count = 3 + world.getRandom().nextInt(4);
long seed = world.getRandom().nextLong();
double x = player.getX();
double y = player.getY() + player.getHeight() * 0.55;
double z = player.getZ();
DestroyedSkinParticlesS2CPayload payload = new DestroyedSkinParticlesS2CPayload(player.getUuid(), x, y, z, count, seed);
for (ServerPlayerEntity target : world.getPlayers()) {
if (!(target.squaredDistanceTo(player) > 9216.0) && ServerPlayNetworking.canSend(target, DestroyedSkinParticlesS2CPayload.ID)) {
ServerPlayNetworking.send(target, payload);
}
}
}
}
private static void broadcastDestroyedSkin(MinecraftServer server, UUID playerUuid, int stage, byte[] bytes) {
if (server != null && playerUuid != null && bytes != null) {
for (ServerPlayerEntity target : server.getPlayerManager().getPlayerList()) {
sendDestroyedSkin(target, playerUuid, stage, bytes);
}
}
}
private static void broadcastDestroyedSkinMask(MinecraftServer server, UUID playerUuid, int stage, byte[] bytes) {
if (server != null && playerUuid != null && bytes != null) {
for (ServerPlayerEntity target : server.getPlayerManager().getPlayerList()) {
sendDestroyedSkinMask(target, playerUuid, stage, bytes);
}
}
}
private static void broadcastDestroyedSkinStage(MinecraftServer server, UUID playerUuid, int stage) {
if (server != null && playerUuid != null) {
for (ServerPlayerEntity target : server.getPlayerManager().getPlayerList()) {
sendDestroyedSkinStage(target, playerUuid, stage);
}
}
}
private static void sendDestroyedSkin(ServerPlayerEntity target, UUID playerUuid, int stage, byte[] bytes) {
if (target != null && playerUuid != null && bytes != null) {
if (ServerPlayNetworking.canSend(target, DestroyedSkinSyncS2CPayload.ID)) {
if (!NeedsOfNature.isDestroyedSkinSystemEnabled()) {
stage = 0;
}
ServerPlayNetworking.send(target, new DestroyedSkinSyncS2CPayload(playerUuid, stage, bytes));
}
}
}
private static void sendDestroyedSkinMask(ServerPlayerEntity target, UUID playerUuid, int stage, byte[] bytes) {
if (target != null && playerUuid != null && bytes != null) {
if (stage > 0 && stage < 4) {
if (ServerPlayNetworking.canSend(target, DestroyedSkinMaskSyncS2CPayload.ID)) {
ServerPlayNetworking.send(target, new DestroyedSkinMaskSyncS2CPayload(playerUuid, stage, bytes));
}
}
}
}
private static void sendDestroyedSkinStage(ServerPlayerEntity target, UUID playerUuid, int stage) {
if (target != null && playerUuid != null) {
if (ServerPlayNetworking.canSend(target, DestroyedSkinStateS2CPayload.ID)) {
if (!NeedsOfNature.isDestroyedSkinSystemEnabled()) {
stage = 0;
}
ServerPlayNetworking.send(target, new DestroyedSkinStateS2CPayload(playerUuid, stage));
}
}
}
private record UploadKey(UUID playerUuid, int maskStage) {
}
}
+166
View File
@@ -0,0 +1,166 @@
package com.nonid;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents.ServerStopped;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents.Disconnect;
import net.minecraft.entity.Entity;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import org.jetbrains.annotations.Nullable;
public final class NonDiagnostics {
private static final boolean RUNTIME_LOGS = Boolean.getBoolean("needsofnature.runtimeLogs");
private static final int MAX_PENDING_SETUP_CHAT_MESSAGES = 100;
private static final Map<UUID, NonDebugChatMode> DEBUG_CHAT_PREFERENCES = new ConcurrentHashMap<>();
private static final List<NonDiagnostics.PendingSetupChatMessage> PENDING_SETUP_CHAT = new ArrayList<>();
private static boolean initialized;
private NonDiagnostics() {
}
public static synchronized void initialize() {
if (!initialized) {
initialized = true;
ServerTickEvents.END_SERVER_TICK.register(NonDiagnostics::flushPendingSetupChat);
ServerPlayConnectionEvents.DISCONNECT.register((Disconnect)(handler, server) -> DEBUG_CHAT_PREFERENCES.remove(handler.player.getUuid()));
ServerLifecycleEvents.SERVER_STOPPED.register((ServerStopped)server -> {
DEBUG_CHAT_PREFERENCES.clear();
synchronized (PENDING_SETUP_CHAT) {
PENDING_SETUP_CHAT.clear();
}
});
}
}
public static void setDebugChatPreference(UUID playerUuid, @Nullable NonDebugChatMode mode) {
if (playerUuid != null) {
DEBUG_CHAT_PREFERENCES.put(playerUuid, mode == null ? NonDebugChatMode.SETUP_ERRORS : mode);
}
}
public static boolean shouldSendDebugChat(@Nullable ServerPlayerEntity player, @Nullable NonDebugChatCategory category) {
if (player == null) {
return false;
}
NonDebugChatMode mode = DEBUG_CHAT_PREFERENCES.getOrDefault(player.getUuid(), NonDebugChatMode.SETUP_ERRORS);
return mode.allows(category == null ? NonDebugChatCategory.INFO : category);
}
public static boolean shouldSendDebugChat(@Nullable ServerPlayerEntity player, @Nullable NonDebugChatCategory category, boolean force) {
return force || shouldSendDebugChat(player, category);
}
public static void sendDebugChat(ServerPlayerEntity player, NonDebugChatCategory category, Text message, boolean force) {
if (message != null && shouldSendDebugChat(player, category, force)) {
player.sendMessage(message.copy().formatted(chatColor(category)), false);
}
}
public static void logSetupWarning(String template, Object... args) {
NeedsOfNature.LOGGER.warn(template, args);
queueSetupChat(formatLogTemplate(template, args));
}
public static void logSetupError(String template, Object... args) {
NeedsOfNature.LOGGER.error(template, args);
queueSetupChat(formatLogTemplate(template, args));
}
public static void logRuntime(String template, Object... args) {
if (RUNTIME_LOGS) {
NeedsOfNature.LOGGER.info(template, args);
} else {
NeedsOfNature.LOGGER.debug(template, args);
}
}
public static void queueSetupChat(String message) {
if (message != null && !message.isBlank()) {
synchronized (PENDING_SETUP_CHAT) {
if (PENDING_SETUP_CHAT.size() >= 100) {
PENDING_SETUP_CHAT.removeFirst();
}
PENDING_SETUP_CHAT.add(new NonDiagnostics.PendingSetupChatMessage(Text.literal(message), new HashSet<>()));
}
}
}
public static String formatLogTemplate(String template, Object... args) {
if (template == null) {
return "";
}
if (args != null && args.length != 0) {
StringBuilder out = new StringBuilder(template.length() + args.length * 8);
int argIndex = 0;
for (int i = 0; i < template.length(); i++) {
char c = template.charAt(i);
if (c == '{' && i + 1 < template.length() && template.charAt(i + 1) == '}' && argIndex < args.length) {
Object arg = args[argIndex++];
if (!(arg instanceof Throwable)) {
out.append(String.valueOf(arg));
}
i++;
} else {
out.append(c);
}
}
return out.toString();
} else {
return template;
}
}
public static Formatting chatColor(@Nullable NonDebugChatCategory category) {
if (category == null) {
return Formatting.WHITE;
}
return switch (category) {
case ALWAYS -> Formatting.DARK_GRAY;
case SETUP -> Formatting.YELLOW;
case WARNING -> Formatting.LIGHT_PURPLE;
case ERROR -> Formatting.RED;
case INFO -> Formatting.WHITE;
};
}
private static void flushPendingSetupChat(MinecraftServer server) {
if (server != null) {
List<ServerPlayerEntity> players = server.getPlayerManager().getPlayerList();
if (!players.isEmpty()) {
synchronized (PENDING_SETUP_CHAT) {
PENDING_SETUP_CHAT.removeIf(message -> {
for (ServerPlayerEntity player : players) {
if (player != null && !message.sentTo().contains(player.getUuid())) {
sendDebugChat(player, NonDebugChatCategory.SETUP, message.message(), false);
message.sentTo().add(player.getUuid());
}
}
return message.sentTo().containsAll(players.stream().map(Entity::getUuid).toList());
});
}
}
}
}
private record PendingSetupChatMessage(Text message, Set<UUID> sentTo) {
}
}
@@ -0,0 +1,7 @@
package com.nonid;
public enum NonGatherCancelReason {
NO_LOS,
TOO_FAR,
TIMEOUT;
}
@@ -0,0 +1,772 @@
package com.nonid;
import com.nonid.api.animation.NonAnimationDamageBehavior;
import com.nonid.internal.animation.api.AnimationService;
import com.nonid.internal.animation.data.AnimationDefinitions;
import com.nonid.internal.animation.network.AnimationStageInfo;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.entity.Entity;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.registry.Registries;
import net.minecraft.registry.RegistryKey;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.Box;
import net.minecraft.world.World;
import org.jetbrains.annotations.Nullable;
public final class NonGatherSystem {
private static final double MOVE_SPEED = 1.15;
private static final int REPATH_EVERY_TICKS = 5;
private static final int TIMEOUT_TICKS = 3600;
private static final int NO_LOS_TIMEOUT_TICKS = 100;
private static final int TOO_FAR_TIMEOUT_TICKS = 200;
private static final double TIMEOUT_MAX_DIST_SQ = 100.0;
private static final double GATHER_EXTRA_DIST = 0.25;
private static final double GATHER_TARGET_OVERSHOOT = 1.0;
private static final double START_MAX_VERTICAL_DELTA = 2.5;
private static final float LOOK_YAW_SPEED = 30.0F;
private static final float LOOK_PITCH_SPEED = 30.0F;
private static final Map<UUID, NonGatherSystem.Session> SESSIONS_BY_INITIATOR = new ConcurrentHashMap<>();
private NonGatherSystem() {
}
static void clearRuntimeState() {
SESSIONS_BY_INITIATOR.clear();
}
@Nullable
public static NonGatherSystem.GatherCandidate findGatherCandidate(
ServerWorld world, List<Entity> actors, boolean isAttack, Entity attacker, @Nullable Entity gatherAnchor
) {
if (world != null && actors != null && !actors.isEmpty() && attacker != null) {
List<Entity> sortedActors = new ArrayList<>(actors);
sortedActors.sort(Comparator.comparingInt(Entity::getId));
AnimationService.MatchedAnimation matched = isAttack
? NonAnimationMatcher.findAutomaticAttack(world, sortedActors, Set.of(), gatherAnchor, true, attacker, Set.of())
: NonAnimationMatcher.findAutomatic(world, sortedActors, Set.of(), gatherAnchor, true, null, Set.of());
return toGatherCandidate(matched);
} else {
return null;
}
}
@Nullable
public static NonGatherSystem.GatherCandidate findUnprotectedGatherCandidate(
ServerWorld world, List<Entity> actors, boolean isAttack, Entity attacker, @Nullable Entity gatherAnchor
) {
NonGatherSystem.GatherCandidate candidate = findGatherCandidate(world, actors, isAttack, attacker, gatherAnchor);
if (candidate == null) {
return null;
}
AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(candidate.animationId());
if (definition == null) {
return null;
}
List<Entity> sortedActors = new ArrayList<>(actors);
sortedActors.sort(Comparator.comparingInt(Entity::getId));
return !NeedsOfNature.isProtectedByAccessory(world, definition, sortedActors)
? candidate
: findUnprotectedFallbackCandidate(world, sortedActors, isAttack, attacker, gatherAnchor, true, candidate.animationId(), null);
}
@Nullable
static NonGatherSystem.GatherCandidate findUnprotectedFallbackCandidate(
ServerWorld world,
List<Entity> actors,
boolean isAttack,
Entity attacker,
@Nullable Entity gatherAnchor,
boolean requireStartEligibility,
Identifier blockedAnimationId,
@Nullable Identifier joinPredecessorAnimationId
) {
if (blockedAnimationId == null) {
return null;
}
List<Entity> sortedActors = new ArrayList<>(actors);
sortedActors.sort(Comparator.comparingInt(Entity::getId));
Set<Identifier> excluded = new HashSet<>();
excluded.add(blockedAnimationId);
while (true) {
AnimationService.MatchedAnimation matched = isAttack
? NonAnimationMatcher.findAutomaticAttack(
world, sortedActors, Set.of(), gatherAnchor, requireStartEligibility, attacker, excluded, null, joinPredecessorAnimationId
)
: NonAnimationMatcher.findAutomatic(
world, sortedActors, Set.of(), gatherAnchor, requireStartEligibility, null, excluded, null, joinPredecessorAnimationId
);
NonGatherSystem.GatherCandidate candidate = toGatherCandidate(matched);
if (candidate == null) {
return null;
}
AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(candidate.animationId());
if (definition == null) {
return null;
}
if (!NeedsOfNature.isProtectedByAccessory(world, definition, sortedActors)) {
return candidate;
}
excluded.add(candidate.animationId());
}
}
@Nullable
private static NonGatherSystem.GatherCandidate toGatherCandidate(@Nullable AnimationService.MatchedAnimation matched) {
return matched == null ? null : new NonGatherSystem.GatherCandidate(matched.animationId(), matched.actorKeys(), matched.stages());
}
public static boolean startMobPlayerGather(
ServerWorld world, MobEntity mob, ServerPlayerEntity player, Identifier animationId, List<String> actorKeys, List<AnimationStageInfo> stages
) {
if (world != null && mob != null && player != null && animationId != null) {
if (mob.getEntityWorld() != world || player.getEntityWorld() != world) {
return false;
} else if (!NonMobGatherEligibility.canInitiateAutoGather(mob)) {
return false;
} else if (isAutomaticMobPlayerStartBlockedByMount(player)) {
reportAutomaticMobPlayerMountBlock(world, mob, player, animationId, "gather_start");
return false;
} else {
NonGatherSystem.Session session = new NonGatherSystem.Session(
NonGatherSystem.SessionType.MOB_PLAYER,
mob.getUuid(),
player.getUuid(),
animationId,
List.copyOf(actorKeys),
stages == null ? List.of() : List.copyOf(stages),
true,
null,
world.getRegistryKey(),
world.getTime(),
0,
0,
0
);
SESSIONS_BY_INITIATOR.put(mob.getUuid(), session);
NeedsOfNature.trackNoNGatherPair(world, List.of(mob, player));
return true;
}
} else {
return false;
}
}
public static boolean isAutomaticMobPlayerStartBlockedByMount(ServerPlayerEntity player) {
if (player != null && player.hasVehicle()) {
Entity vehicle = player.getVehicle();
if (vehicle != null && !vehicle.isRemoved()) {
NonConfig config = NeedsOfNature.getConfig();
if (config == null) {
return false;
} else {
return vehicle instanceof LivingEntity ? config.blockAnimationsWhileRidingLivingEntities() : config.blockAnimationsWhileRidingVehicles();
}
} else {
return false;
}
} else {
return false;
}
}
public static void reportAutomaticMobPlayerMountBlock(
ServerWorld world, MobEntity mob, ServerPlayerEntity player, @Nullable Identifier animationId, String phase
) {
if (world != null && mob != null && player != null) {
Entity vehicle = player.getVehicle();
Identifier mobType = Registries.ENTITY_TYPE.getId(mob.getType());
Identifier vehicleType = vehicle == null ? null : Registries.ENTITY_TYPE.getId(vehicle.getType());
NeedsOfNature.logRuntime(
"NoN automatic mob-player start blocked by mounted-player protection: phase={} mobType={} player={} vehicle={} anim={}",
phase,
mobType,
player.getName().getString(),
vehicleType,
animationId
);
Text vehicleName = (Text)(vehicle == null ? Text.literal("载具") : vehicle.getDisplayName());
NeedsOfNature.sendDebugChat(
player, NonDebugChatCategory.WARNING, Text.translatable("debug.needsofnature.mounted_animation_blocked", new Object[]{vehicleName})
);
}
}
public static void startMobMobGather(
ServerWorld world,
MobEntity initiator,
MobEntity partner,
Identifier animationId,
List<String> actorKeys,
List<AnimationStageInfo> stages,
boolean attackMode
) {
if (world != null && initiator != null && partner != null && animationId != null) {
if (initiator.getEntityWorld() == world && partner.getEntityWorld() == world) {
if (NonMobGatherEligibility.canInitiateAutoGather(initiator) && NonMobGatherEligibility.canBeAutoGatherTarget(partner)) {
NonGatherSystem.Session session = new NonGatherSystem.Session(
NonGatherSystem.SessionType.MOB_MOB,
initiator.getUuid(),
partner.getUuid(),
animationId,
List.copyOf(actorKeys),
stages == null ? List.of() : List.copyOf(stages),
attackMode,
null,
world.getRegistryKey(),
world.getTime(),
0,
0,
0
);
SESSIONS_BY_INITIATOR.put(initiator.getUuid(), session);
NeedsOfNature.trackNoNGatherPair(world, List.of(initiator, partner));
}
}
}
}
public static void startJoinGather(ServerWorld world, MobEntity joiningMob, ServerPlayerEntity requester, UUID activeInstanceId, Identifier animationId) {
if (world != null && joiningMob != null && requester != null && activeInstanceId != null) {
if (joiningMob.getEntityWorld() == world && requester.getEntityWorld() == world) {
if (NonMobGatherEligibility.canInitiateAutoGather(joiningMob)) {
NonGatherSystem.Session session = new NonGatherSystem.Session(
NonGatherSystem.SessionType.MOB_JOIN_ACTIVE_ANIMATION,
joiningMob.getUuid(),
requester.getUuid(),
animationId,
List.of(),
List.of(),
false,
activeInstanceId,
world.getRegistryKey(),
world.getTime(),
0,
0,
0
);
SESSIONS_BY_INITIATOR.put(joiningMob.getUuid(), session);
NeedsOfNature.trackNoNGatherPair(world, List.of(joiningMob, requester));
}
}
}
}
public static void tickServerWorld(ServerWorld world) {
if (world != null && !SESSIONS_BY_INITIATOR.isEmpty()) {
for (NonGatherSystem.Session session : List.copyOf(SESSIONS_BY_INITIATOR.values())) {
if (session != null && world.getRegistryKey().equals(session.worldKey())) {
if (!(world.getEntity(session.initiatorUuid()) instanceof MobEntity mob && mob.getEntityWorld() == world)) {
remove(session, world, false, null, "initiator_missing");
} else if (session.type() == NonGatherSystem.SessionType.MOB_PLAYER) {
tickMobPlayer(world, session, mob);
} else if (session.type() == NonGatherSystem.SessionType.MOB_MOB) {
tickMobMob(world, session, mob);
} else {
tickJoin(world, session, mob);
}
}
}
}
}
public static boolean isActorGathering(UUID actorUuid) {
if (actorUuid != null && !SESSIONS_BY_INITIATOR.isEmpty()) {
for (NonGatherSystem.Session session : SESSIONS_BY_INITIATOR.values()) {
if (session != null && (actorUuid.equals(session.initiatorUuid()) || actorUuid.equals(session.targetUuid()))) {
return true;
}
}
return false;
} else {
return false;
}
}
public static boolean isMobGatheringWithPlayer(ServerWorld world, UUID mobUuid, UUID playerUuid) {
if (world != null && mobUuid != null && playerUuid != null) {
NonGatherSystem.Session session = SESSIONS_BY_INITIATOR.get(mobUuid);
if (session == null || !world.getRegistryKey().equals(session.worldKey())) {
return false;
} else {
return !playerUuid.equals(session.targetUuid())
? false
: session.type() == NonGatherSystem.SessionType.MOB_PLAYER || session.type() == NonGatherSystem.SessionType.MOB_JOIN_ACTIVE_ANIMATION;
}
} else {
return false;
}
}
public static void cancelForActors(ServerWorld world, Collection<UUID> actorUuids) {
if (world != null && actorUuids != null && !actorUuids.isEmpty() && !SESSIONS_BY_INITIATOR.isEmpty()) {
Set<UUID> actors = new HashSet<>(actorUuids);
for (NonGatherSystem.Session session : List.copyOf(SESSIONS_BY_INITIATOR.values())) {
if (session != null
&& world.getRegistryKey().equals(session.worldKey())
&& (actors.contains(session.initiatorUuid()) || actors.contains(session.targetUuid()))) {
remove(session, world, false, null, "player_request_accepted");
}
}
}
}
private static void tickMobPlayer(ServerWorld world, NonGatherSystem.Session session, MobEntity mob) {
ServerPlayerEntity player = world.getServer().getPlayerManager().getPlayer(session.targetUuid());
if (player == null || player.getEntityWorld() != world || !player.isAlive() || player.isRemoved() || mob.isRemoved()) {
remove(session, world, false, null, "invalid_actor");
} else if (!NonMobGatherEligibility.canInitiateAutoGather(mob)) {
remove(session, world, false, null, "mob_not_auto_gatherable");
} else if (!NeedsOfNature.canMobAttackPlayer(player) || NeedsOfNature.isMobAttackFailsafeActive(world, mob.getUuid())) {
remove(session, world, false, null, "player_not_attackable_or_failsafe");
} else if (isAutomaticMobPlayerStartBlockedByMount(player)) {
reportAutomaticMobPlayerMountBlock(world, mob, player, session.animationId(), "close_range_start");
remove(session, world, false, null, "player_mounted_protected");
} else if (AnimationService.isActorPendingOrActive(world, mob.getUuid()) || AnimationService.isActorPendingOrActive(world, player.getUuid())) {
remove(session, world, false, null, "afw_actor_pending_or_active");
} else if (timedOut(world, session)) {
remove(session, world, true, NonGatherCancelReason.TIMEOUT, "timeout");
} else {
int noLosTicks = mob.canSee(player) ? 0 : session.noLosTicks() + 1;
int tooFarTicks = mob.squaredDistanceTo(player) > 100.0 ? session.tooFarTicks() + 1 : 0;
if (!cancelledForDistance(session, world, noLosTicks, tooFarTicks)) {
mob.getLookControl().lookAt(player, 30.0F, 30.0F);
if (NonAttackSystem.hasAnimationStartProtection(world, player)) {
int repathCooldown = Math.max(0, session.repathCooldown() - 1);
if (repathCooldown <= 0) {
startMovingIntoTarget(mob, player);
repathCooldown = 5;
}
update(session, repathCooldown, noLosTicks, tooFarTicks);
} else if (closeEnoughToStart(mob, player)) {
List<Entity> actors = sortedActors(mob, player);
NonGatherSystem.StartCandidate startCandidate = resolveProtectedStartCandidate(world, session, actors, true, mob, player);
if (startCandidate == null) {
NeedsOfNature.clearNoNGatherPair(world, List.of(mob.getUuid(), player.getUuid()));
SESSIONS_BY_INITIATOR.remove(session.initiatorUuid(), session);
} else {
UUID instanceId = NeedsOfNature.startAnimationNowWithContext(
world,
startCandidate.animationId(),
actors,
startCandidate.actorKeys(),
startCandidate.stages(),
NonAnimationDamageBehavior.STOP_ON_DAMAGE,
false,
player,
NeedsOfNature.buildModeContext(true)
);
NeedsOfNature.clearNoNGatherPair(world, List.of(mob.getUuid(), player.getUuid()));
SESSIONS_BY_INITIATOR.remove(session.initiatorUuid(), session);
if (instanceId == null) {
NeedsOfNature.logRuntime(
"NoN gather start failed at close range: mobType={} player={} anim={}",
Registries.ENTITY_TYPE.getId(mob.getType()),
player.getName().getString(),
startCandidate.animationId()
);
}
}
} else {
int repathCooldown = Math.max(0, session.repathCooldown() - 1);
if (repathCooldown <= 0) {
startMovingIntoTarget(mob, player);
repathCooldown = 5;
}
update(session, repathCooldown, noLosTicks, tooFarTicks);
}
}
}
}
private static void tickMobMob(ServerWorld world, NonGatherSystem.Session session, MobEntity initiator) {
if (!(
world.getEntity(session.targetUuid()) instanceof MobEntity partner
&& partner.getEntityWorld() == world
&& partner.isAlive()
&& !partner.isRemoved()
&& !initiator.isRemoved()
)) {
remove(session, world, false, null, "invalid_actor");
} else if (!NonMobGatherEligibility.canInitiateAutoGather(initiator) || !NonMobGatherEligibility.canBeAutoGatherTarget(partner)) {
remove(session, world, false, null, "mob_not_auto_gatherable");
} else if (AnimationService.isActorPendingOrActive(world, initiator.getUuid()) || AnimationService.isActorPendingOrActive(world, partner.getUuid())) {
remove(session, world, false, null, "afw_actor_pending_or_active");
} else if (timedOut(world, session)) {
remove(session, world, true, NonGatherCancelReason.TIMEOUT, "timeout");
} else {
boolean hasLos = initiator.canSee(partner) && partner.canSee(initiator);
int noLosTicks = hasLos ? 0 : session.noLosTicks() + 1;
int tooFarTicks = initiator.squaredDistanceTo(partner) > 100.0 ? session.tooFarTicks() + 1 : 0;
if (!cancelledForDistance(session, world, noLosTicks, tooFarTicks)) {
initiator.getLookControl().lookAt(partner, 30.0F, 30.0F);
partner.getLookControl().lookAt(initiator, 30.0F, 30.0F);
if (closeEnoughToStart(initiator, partner)) {
List<Entity> actors = sortedActors(initiator, partner);
NonGatherSystem.StartCandidate startCandidate = resolveProtectedStartCandidate(
world, session, actors, session.attackMode(), initiator, session.attackMode() ? partner : null
);
if (startCandidate == null) {
NeedsOfNature.clearNoNGatherPair(world, List.of(initiator.getUuid(), partner.getUuid()));
SESSIONS_BY_INITIATOR.remove(session.initiatorUuid(), session);
} else {
UUID instanceId = NeedsOfNature.startAnimationNowWithContext(
world,
startCandidate.animationId(),
actors,
startCandidate.actorKeys(),
startCandidate.stages(),
NonAnimationDamageBehavior.STOP_ON_DAMAGE,
false,
session.attackMode() ? partner : null,
NeedsOfNature.buildModeContext(session.attackMode())
);
NeedsOfNature.clearNoNGatherPair(world, List.of(initiator.getUuid(), partner.getUuid()));
SESSIONS_BY_INITIATOR.remove(session.initiatorUuid(), session);
if (instanceId == null) {
NeedsOfNature.logRuntime(
"NoN mob gather start failed at close range: first={} second={} anim={}",
Registries.ENTITY_TYPE.getId(initiator.getType()),
Registries.ENTITY_TYPE.getId(partner.getType()),
startCandidate.animationId()
);
}
}
} else {
int repathCooldown = Math.max(0, session.repathCooldown() - 1);
if (repathCooldown <= 0) {
startMovingIntoTarget(initiator, partner);
startMovingIntoTarget(partner, initiator);
repathCooldown = 5;
}
update(session, repathCooldown, noLosTicks, tooFarTicks);
}
}
}
}
private static void tickJoin(ServerWorld world, NonGatherSystem.Session session, MobEntity joiningMob) {
ServerPlayerEntity requester = world.getServer().getPlayerManager().getPlayer(session.targetUuid());
if (requester == null
|| requester.getEntityWorld() != world
|| !requester.isAlive()
|| requester.isRemoved()
|| joiningMob.isRemoved()
|| session.activeInstanceUuid() == null) {
remove(session, world, false, null, "invalid_join_actor");
} else if (!NonMobGatherEligibility.canInitiateAutoGather(joiningMob)) {
remove(session, world, false, null, "mob_not_auto_gatherable");
} else if (AnimationService.isActorPendingOrActive(world, joiningMob.getUuid())) {
remove(session, world, false, null, "afw_actor_pending_or_active");
} else {
AnimationStageInfo stage = AnimationService.getCurrentStage(world, session.activeInstanceUuid());
if (stage == null) {
remove(session, world, false, null, "join_stage_unavailable");
} else if (timedOut(world, session)) {
remove(session, world, true, NonGatherCancelReason.TIMEOUT, "timeout");
} else {
int noLosTicks = joiningMob.canSee(requester) ? 0 : session.noLosTicks() + 1;
int tooFarTicks = joiningMob.squaredDistanceTo(requester) > 100.0 ? session.tooFarTicks() + 1 : 0;
if (!cancelledForDistance(session, world, noLosTicks, tooFarTicks)) {
joiningMob.getLookControl().lookAt(requester, 30.0F, 30.0F);
if (closeEnoughToJoin(joiningMob, requester)) {
Identifier joinAnimationId = resolveProtectedJoinAnimationId(world, session, joiningMob, requester);
if (joinAnimationId == null) {
NeedsOfNature.clearNoNGatherPair(world, List.of(joiningMob.getUuid(), requester.getUuid()));
SESSIONS_BY_INITIATOR.remove(session.initiatorUuid(), session);
} else {
boolean voluntaryManualPeakJoin = NeedsOfNature.isVoluntaryManualPeakInstance(world, session.activeInstanceUuid());
boolean accepted = NeedsOfNature.joinAnimationNow(world, session.activeInstanceUuid(), List.of(joiningMob), requester, joinAnimationId);
NeedsOfNature.clearNoNGatherPair(world, List.of(joiningMob.getUuid(), requester.getUuid()));
SESSIONS_BY_INITIATOR.remove(session.initiatorUuid(), session);
if (!accepted) {
NeedsOfNature.logRuntime(
"NoN join gather failed at close range: mobType={} player={} instance={} anim={}",
Registries.ENTITY_TYPE.getId(joiningMob.getType()),
requester.getName().getString(),
session.activeInstanceUuid(),
session.animationId()
);
} else if (voluntaryManualPeakJoin) {
NonDestroyedSkinSystem.applyDestroyedSkinVoluntaryJoinInitialDamage(world, requester);
}
}
} else {
int repathCooldown = Math.max(0, session.repathCooldown() - 1);
if (repathCooldown <= 0) {
startMovingIntoTarget(joiningMob, requester);
repathCooldown = 5;
}
update(session, repathCooldown, noLosTicks, tooFarTicks);
}
}
}
}
}
private static void startMovingIntoTarget(MobEntity mob, Entity target) {
if (mob != null && target != null) {
double dx = target.getX() - mob.getX();
double dz = target.getZ() - mob.getZ();
double length = Math.sqrt(dx * dx + dz * dz);
double targetX = target.getX();
double targetZ = target.getZ();
if (length > 1.0E-4) {
targetX += dx / length * 1.0;
targetZ += dz / length * 1.0;
}
mob.getNavigation().startMovingTo(targetX, target.getY(), targetZ, 1.15);
}
}
@Nullable
private static Identifier resolveProtectedJoinAnimationId(
ServerWorld world, NonGatherSystem.Session session, MobEntity joiningMob, ServerPlayerEntity requester
) {
List<Entity> combinedActors = resolveJoinActors(world, session.activeInstanceUuid(), joiningMob);
if (combinedActors.isEmpty()) {
return session.animationId();
}
Identifier predecessorAnimationId = AnimationService.getActiveAnimationId(world, session.activeInstanceUuid());
if (predecessorAnimationId == null) {
return null;
}
AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(session.animationId());
if (definition == null) {
return session.animationId();
}
if (!NonInjectorMatchPolicy.allowsAutomaticNonMatch(world, definition, combinedActors)) {
boolean isAttack = NeedsOfNature.isInstanceAttack(world, session.activeInstanceUuid());
NonGatherSystem.GatherCandidate fallback = findUnprotectedFallbackCandidate(
world, combinedActors, isAttack, joiningMob, requester, true, session.animationId(), predecessorAnimationId
);
return fallback == null ? null : fallback.animationId();
}
if (!NeedsOfNature.isProtectedByAccessory(world, definition, combinedActors)) {
return session.animationId();
}
if (!NeedsOfNature.consumeProtectorForAnimation(world, definition, combinedActors)) {
return session.animationId();
}
boolean isAttack = NeedsOfNature.isInstanceAttack(world, session.activeInstanceUuid());
NonGatherSystem.GatherCandidate fallback = findUnprotectedFallbackCandidate(
world, combinedActors, isAttack, joiningMob, requester, true, session.animationId(), predecessorAnimationId
);
return fallback == null ? null : fallback.animationId();
}
private static List<Entity> resolveJoinActors(ServerWorld world, @Nullable UUID activeInstanceUuid, MobEntity joiningMob) {
if (world != null && activeInstanceUuid != null && joiningMob != null) {
List<Entity> actors = new ArrayList<>();
for (UUID actorUuid : NeedsOfNature.getInstanceActors(world, activeInstanceUuid)) {
Entity actor = world.getEntity(actorUuid);
if (actor == null) {
actor = world.getServer().getPlayerManager().getPlayer(actorUuid);
}
if (actor != null && actor.getEntityWorld() == world && !actor.isRemoved()) {
actors.add(actor);
}
}
if (joiningMob.getEntityWorld() == world && !joiningMob.isRemoved()) {
actors.add(joiningMob);
}
actors.sort(Comparator.comparingInt(Entity::getId));
return actors;
} else {
return List.of();
}
}
private static boolean timedOut(ServerWorld world, NonGatherSystem.Session session) {
return world.getTime() - session.startTick() > 3600L;
}
@Nullable
private static NonGatherSystem.StartCandidate resolveProtectedStartCandidate(
ServerWorld world, NonGatherSystem.Session session, List<Entity> actors, boolean isAttack, Entity attacker, @Nullable Entity gatherAnchor
) {
AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(session.animationId());
if (definition == null) {
return new NonGatherSystem.StartCandidate(session.animationId(), session.actorKeys(), session.stages());
}
if (!NonInjectorMatchPolicy.allowsAutomaticNonMatch(world, definition, actors)) {
NonGatherSystem.GatherCandidate fallback = findUnprotectedFallbackCandidate(
world, actors, isAttack, attacker, gatherAnchor, true, session.animationId(), null
);
return fallback == null ? null : new NonGatherSystem.StartCandidate(fallback.animationId(), fallback.actorKeys(), fallback.stages());
}
if (!NeedsOfNature.isProtectedByAccessory(world, definition, actors)) {
return new NonGatherSystem.StartCandidate(session.animationId(), session.actorKeys(), session.stages());
}
if (!NeedsOfNature.consumeProtectorForAnimation(world, definition, actors)) {
return new NonGatherSystem.StartCandidate(session.animationId(), session.actorKeys(), session.stages());
}
NonGatherSystem.GatherCandidate fallback = findUnprotectedFallbackCandidate(
world, actors, isAttack, attacker, gatherAnchor, true, session.animationId(), null
);
return fallback == null ? null : new NonGatherSystem.StartCandidate(fallback.animationId(), fallback.actorKeys(), fallback.stages());
}
private static boolean cancelledForDistance(NonGatherSystem.Session session, ServerWorld world, int noLosTicks, int tooFarTicks) {
if (noLosTicks > 100) {
remove(session, world, true, NonGatherCancelReason.NO_LOS, "no_los");
return true;
} else if (tooFarTicks > 200) {
remove(session, world, true, NonGatherCancelReason.TOO_FAR, "too_far");
return true;
} else {
return false;
}
}
private static void update(NonGatherSystem.Session session, int repathCooldown, int noLosTicks, int tooFarTicks) {
SESSIONS_BY_INITIATOR.put(
session.initiatorUuid(),
new NonGatherSystem.Session(
session.type(),
session.initiatorUuid(),
session.targetUuid(),
session.animationId(),
session.actorKeys(),
session.stages(),
session.attackMode(),
session.activeInstanceUuid(),
session.worldKey(),
session.startTick(),
repathCooldown,
noLosTicks,
tooFarTicks
)
);
}
private static void remove(NonGatherSystem.Session session, ServerWorld world, boolean applyCooldown, @Nullable NonGatherCancelReason reason) {
remove(session, world, applyCooldown, reason, "unspecified");
}
private static void remove(NonGatherSystem.Session session, ServerWorld world, boolean applyCooldown, @Nullable NonGatherCancelReason reason, String detail) {
if (session != null && world != null) {
Entity initiator = world.getEntity(session.initiatorUuid());
Entity target = world.getEntity(session.targetUuid());
if (initiator instanceof MobEntity mob) {
mob.getNavigation().stop();
}
if (target instanceof MobEntity mob) {
mob.getNavigation().stop();
}
List<UUID> actorUuids = List.of(session.initiatorUuid(), session.targetUuid());
if (applyCooldown && reason != null) {
NeedsOfNature.applyNoNGatherCancellation(world, session.animationId(), actorUuids, reason);
} else {
NeedsOfNature.clearNoNGatherPair(world, actorUuids);
}
SESSIONS_BY_INITIATOR.remove(session.initiatorUuid(), session);
}
}
private static boolean closeEnoughToStart(Entity a, Entity b) {
if (a != null && b != null) {
return Math.abs(a.getY() - b.getY()) > 2.5 ? false : horizontalBoxDistanceSq(a.getBoundingBox(), b.getBoundingBox()) <= 0.0625;
} else {
return false;
}
}
private static boolean closeEnoughToJoin(Entity a, Entity b) {
return closeEnoughToStart(a, b);
}
private static double horizontalBoxDistanceSq(Box a, Box b) {
double dx = axisGap(a.minX, a.maxX, b.minX, b.maxX);
double dz = axisGap(a.minZ, a.maxZ, b.minZ, b.maxZ);
return dx * dx + dz * dz;
}
private static double axisGap(double aMin, double aMax, double bMin, double bMax) {
if (aMax < bMin) {
return bMin - aMax;
} else {
return bMax < aMin ? aMin - bMax : 0.0;
}
}
private static List<Entity> sortedActors(Entity first, Entity second) {
List<Entity> actors = new ArrayList<>(2);
actors.add(first);
actors.add(second);
actors.sort(Comparator.comparingInt(Entity::getId));
return actors;
}
public record GatherCandidate(Identifier animationId, List<String> actorKeys, List<AnimationStageInfo> stages) {
}
private record Session(
NonGatherSystem.SessionType type,
UUID initiatorUuid,
UUID targetUuid,
Identifier animationId,
List<String> actorKeys,
List<AnimationStageInfo> stages,
boolean attackMode,
@Nullable UUID activeInstanceUuid,
RegistryKey<World> worldKey,
long startTick,
int repathCooldown,
int noLosTicks,
int tooFarTicks
) {
}
private enum SessionType {
MOB_PLAYER,
MOB_MOB,
MOB_JOIN_ACTIVE_ANIMATION;
}
private record StartCandidate(Identifier animationId, List<String> actorKeys, List<AnimationStageInfo> stages) {
}
}
@@ -0,0 +1,408 @@
package com.nonid;
import com.nonid.api.NonChangeSource;
import com.nonid.data.NonEntityProfiles;
import com.nonid.network.GenderSelectionPromptS2CPayload;
import com.nonid.network.SetPlayerGenderC2SPayload;
import java.util.Map;
import java.util.Set;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerEntityEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerEntityEvents.Load;
import net.fabricmc.fabric.api.event.player.UseEntityCallback;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.passive.CowEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.text.Text;
import net.minecraft.util.ActionResult;
final class NonGenderSystem {
private static final String GENDER_TAG_MALE = "gender.male";
private static final String GENDER_TAG_FEMALE = "gender.female";
private static final String GENDER_TAG_SELECTED = "gender.selected";
private static final int DEFAULT_PLAYER_GENDER_MASK = 2;
private NonGenderSystem() {
}
static void registerMobGenderTags() {
ServerEntityEvents.ENTITY_LOAD.register((Load)(entity, world) -> {
if (entity instanceof MobEntity) {
if (entity.getType() != NeedsOfNature.HORSE_LIQUID_COLLECTOR_ENTITY_TYPE) {
if (entity.getType() != NeedsOfNature.PREGNANCY_EGG_ENTITY_TYPE) {
Set<String> tags = entity.getCommandTags();
boolean hasMale = tags.contains("gender.male");
boolean hasFemale = tags.contains("gender.female");
if (!hasMale && !hasFemale) {
NonConfig.GenderSpawnChances chances = resolveGenderSpawnChances(entity);
int roll = world.getRandom().nextInt(100);
if (roll < chances.male()) {
entity.addCommandTag("gender.male");
hasMale = true;
} else if (roll < chances.male() + chances.female()) {
entity.addCommandTag("gender.female");
hasFemale = true;
} else {
entity.addCommandTag("gender.male");
entity.addCommandTag("gender.female");
hasMale = true;
hasFemale = true;
}
}
if (entity instanceof GenderHolder holder) {
int mask = 0;
if (hasMale) {
mask |= 1;
}
if (hasFemale) {
mask |= 2;
}
holder.setGenderMask(mask);
}
if (entity instanceof EnergyHolder holder && world instanceof ServerWorld serverWorld) {
holder.ensureEnergyInitialized(serverWorld);
}
}
}
}
});
}
static void registerCowMilkingGenderGate() {
UseEntityCallback.EVENT.register((UseEntityCallback)(player, world, hand, entity, hitResult) -> {
NonConfig config = NeedsOfNature.getConfig();
if (config == null || !config.femaleCowOnlyMilking()) {
return ActionResult.PASS;
}
if (entity instanceof CowEntity cow) {
if (cow.getType() != EntityType.COW) {
return ActionResult.PASS;
} else {
ItemStack stack = player.getStackInHand(hand);
if (!stack.isEmpty() && stack.isOf(Items.BUCKET)) {
return (ActionResult)(canCowBeMilked(cow) ? ActionResult.PASS : ActionResult.FAIL);
} else {
return ActionResult.PASS;
}
}
} else {
return ActionResult.PASS;
}
});
}
static void registerPlayerGenderRequests() {
ServerPlayNetworking.registerGlobalReceiver(SetPlayerGenderC2SPayload.ID, (payload, context) -> context.server().execute(() -> {
ServerPlayerEntity player = context.player();
if (player != null) {
handleClientGenderRequest(player, payload.mask(), payload.initialSelection());
}
}));
}
static void ensurePlayerGenderInitialized(ServerPlayerEntity player) {
if (player != null) {
int mask = 0;
if (player instanceof GenderHolder holder) {
mask = holder.getGenderMask() & 3;
}
if (mask == 0) {
mask = resolveGenderMaskFromTags(player);
}
if (mask != 0) {
markPlayerGenderSelected(player);
}
if (mask == 0) {
NonConfig config = NeedsOfNature.getConfig();
if (config.requirePlayerGenderSelectionOnJoin()) {
sendGenderSelectionPrompt(player);
} else {
int var3 = 2;
applyPlayerGenderMask(player, var3, true, NonChangeSource.SYSTEM);
}
} else {
applyGenderMask(player, mask);
if (NeedsOfNature.getConfig().requirePlayerGenderSelectionOnJoin() && !hasPlayerGenderSelected(player)) {
sendGenderSelectionPrompt(player);
}
}
}
}
static int setPlayerGenderFromCommand(ServerPlayerEntity player, int mask) {
return setPlayerGenderFromCommand(player, mask, NonChangeSource.COMMAND);
}
static int setPlayerGenderFromCommand(ServerPlayerEntity player, int mask, NonChangeSource source) {
int normalized = normalizeGenderMask(mask);
applyPlayerGenderMask(player, normalized, true, source == null ? NonChangeSource.COMMAND : source);
return normalized;
}
static void resetPlayerGenderSelection(ServerPlayerEntity player) {
if (player != null) {
clearPlayerGenderSelectionState(player);
sendGenderSelectionPrompt(player);
}
}
static boolean canClientChangeGenderFreely() {
return NeedsOfNature.getConfig().allowPlayerGenderChangeAnytime();
}
static boolean isStartingGenderAllowed(int mask) {
int normalized = normalizeGenderMask(mask);
int allowed = NeedsOfNature.getConfig().getAllowedStartingGenderMask();
return (allowed & startingGenderChoiceBit(normalized)) != 0;
}
private static void handleClientGenderRequest(ServerPlayerEntity player, int requestedMask, boolean initialSelection) {
NonConfig config = NeedsOfNature.getConfig();
int normalized = normalizeGenderMask(requestedMask);
boolean selected = hasPlayerGenderSelected(player);
if (initialSelection) {
if (selected && !config.allowPlayerGenderChangeAnytime()) {
sendCurrentGenderFeedback(player, "Gender has already been selected on this server.");
} else if (!isStartingGenderAllowed(normalized)) {
sendGenderSelectionPrompt(player);
} else {
applyPlayerGenderMask(player, normalized, true, NonChangeSource.JOIN_SELECTION);
}
} else {
if (!config.allowPlayerGenderChangeAnytime()) {
if (!selected && config.requirePlayerGenderSelectionOnJoin()) {
sendGenderSelectionPrompt(player);
return;
}
if (selected) {
sendCurrentGenderFeedback(player, "Gender changes are disabled by this server.");
return;
}
if (!isStartingGenderAllowed(normalized)) {
sendGenderSelectionPrompt(player);
return;
}
}
applyPlayerGenderMask(player, normalized, true, NonChangeSource.CLIENT_PREFERENCE);
}
}
private static void applyPlayerGenderMask(ServerPlayerEntity player, int mask, boolean markSelected) {
applyPlayerGenderMask(player, mask, markSelected, NonChangeSource.SYSTEM);
}
private static void applyPlayerGenderMask(ServerPlayerEntity player, int mask, boolean markSelected, NonChangeSource source) {
applyGenderMask(player, mask, source);
if (markSelected) {
markPlayerGenderSelected(player);
}
}
private static void markPlayerGenderSelected(ServerPlayerEntity player) {
if (player != null) {
player.addCommandTag("gender.selected");
}
}
private static void clearPlayerGenderSelectionState(ServerPlayerEntity player) {
if (player != null) {
player.removeCommandTag("gender.selected");
player.removeCommandTag("gender.male");
player.removeCommandTag("gender.female");
if (player instanceof GenderHolder holder) {
holder.setGenderMask(0);
}
}
}
private static boolean hasPlayerGenderSelected(ServerPlayerEntity player) {
return player != null && player.getCommandTags().contains("gender.selected");
}
private static void sendGenderSelectionPrompt(ServerPlayerEntity player) {
if (player != null) {
NonConfig config = NeedsOfNature.getConfig();
ServerPlayNetworking.send(
player,
new GenderSelectionPromptS2CPayload(
config.getAllowedStartingGenderMask(), currentOrDefaultPlayerMask(player), !config.allowPlayerGenderChangeAnytime()
)
);
}
}
private static void sendCurrentGenderFeedback(ServerPlayerEntity player, String message) {
if (player != null) {
player.sendMessage(Text.literal("[NoN] " + message), false);
}
}
private static int currentOrDefaultPlayerMask(ServerPlayerEntity player) {
if (player instanceof GenderHolder holder) {
int mask = holder.getGenderMask() & 3;
if (mask != 0) {
return mask;
}
}
return firstAllowedStartingMask(NeedsOfNature.getConfig().getAllowedStartingGenderMask());
}
private static int firstAllowedStartingMask(int allowedMask) {
int allowed = allowedMask & 3;
if ((allowedMask & 2) != 0) {
return 2;
} else if ((allowedMask & 1) != 0) {
return 1;
} else {
return (allowedMask & 4) != 0 ? 3 : 3;
}
}
private static int startingGenderChoiceBit(int genderMask) {
return switch (genderMask & 3) {
case 1 -> 1;
case 2 -> 2;
case 3 -> 4;
default -> 2;
};
}
private static int normalizeGenderMask(int mask) {
int normalized = mask & 3;
return normalized == 0 ? 2 : normalized;
}
static NonConfig.GenderSpawnChances resolveGenderSpawnChances(Entity entity) {
return NonEntityProfiles.resolve(entity).genderSpawn();
}
static Map<String, NonConfig.GenderSpawnChances> resolveEffectiveGenderSpawnMap() {
return NonEntityProfiles.resolveEffectiveGenderSpawnMap();
}
static boolean hasMaleTag(Entity entity) {
return entity != null && entity.getCommandTags().contains("gender.male");
}
static boolean hasFemaleTag(Entity entity) {
return entity != null && entity.getCommandTags().contains("gender.female");
}
static boolean isOnlyMale(Entity entity) {
return entity instanceof GenderHolder holder ? (holder.getGenderMask() & 3) == 1 : false;
}
static boolean hasMaleGender(Entity entity) {
if (entity instanceof GenderHolder holder) {
int mask = holder.getGenderMask() & 3;
if (mask != 0) {
return (mask & 1) != 0;
}
}
return hasMaleTag(entity);
}
static boolean hasFemaleGender(Entity entity) {
if (entity instanceof GenderHolder holder) {
int mask = holder.getGenderMask() & 3;
if (mask != 0) {
return (mask & 2) != 0;
}
}
return hasFemaleTag(entity);
}
static NonLiquidTankType getLiquidTankType(Entity entity) {
return isOnlyMale(entity) ? NonLiquidTankType.A : NonLiquidTankType.V;
}
static void applyGenderMask(Entity entity, int mask) {
applyGenderMask(entity, mask, NonChangeSource.SYSTEM);
}
static boolean isOnlyFemale(Entity entity) {
return entity == null ? false : hasFemaleGender(entity) && !hasMaleGender(entity);
}
static void applyGenderMask(Entity entity, int mask, NonChangeSource source) {
if (entity != null) {
int beforeMask = entity instanceof GenderHolder existingHolder ? existingHolder.getGenderMask() & 3 : 0;
int normalizedMask = mask & 3;
if (normalizedMask == 0) {
normalizedMask = 2;
}
entity.removeCommandTag("gender.male");
entity.removeCommandTag("gender.female");
if ((normalizedMask & 1) != 0) {
entity.addCommandTag("gender.male");
}
if ((normalizedMask & 2) != 0) {
entity.addCommandTag("gender.female");
}
if (entity instanceof GenderHolder holder) {
holder.setGenderMask(normalizedMask);
}
if (normalizedMask == 1 && entity instanceof ServerPlayerEntity player) {
if (NonMessSystem.normalizeMaleOnlyMess(player, false)) {
NonMessSystem.broadcastMessState(player);
}
NeedsOfNature.clearPregnancyState(player, true);
}
if (entity instanceof ServerPlayerEntity player && (beforeMask & 3) != (normalizedMask & 3)) {
NonApiInternals.fireGenderChanged(player, normalizedMask, source == null ? NonChangeSource.SYSTEM : source);
NonPregnancyConceptionSystem.syncState(player);
}
}
}
private static boolean canCowBeMilked(CowEntity cow) {
if (cow == null) {
return true;
} else {
return cow instanceof GenderHolder holder ? holder.hasGender(2) : cow.getCommandTags().contains("gender.female");
}
}
private static int resolveGenderMaskFromTags(Entity entity) {
if (entity == null) {
return 0;
}
Set<String> tags = entity.getCommandTags();
int mask = 0;
if (tags.contains("gender.male")) {
mask |= 1;
}
if (tags.contains("gender.female")) {
mask |= 2;
}
return mask;
}
}
@@ -0,0 +1,47 @@
package com.nonid;
import com.nonid.data.NonLiquidRoles;
import com.nonid.internal.animation.data.AnimationDefinitions;
import com.nonid.internal.animation.data.AnimationGameplayMetadata;
import java.util.List;
import net.minecraft.entity.Entity;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
public final class NonInjectorMatchPolicy {
private NonInjectorMatchPolicy() {
}
public static boolean allowsAutomaticNonMatch(ServerWorld world, AnimationDefinitions.Definition definition, List<Entity> actors) {
if (world == null || definition == null || actors == null || actors.isEmpty()) {
return false;
}
if (!hasMaleOnlyPlayer(actors)) {
return true;
}
NonLiquidRoles.LiquidRoles roles = AnimationGameplayMetadata.liquidRoles(definition.id());
if (roles != null && !roles.injectorRoles().isEmpty()) {
boolean hasV = roles.injectorRoles().containsValue(NonLiquidRoles.InjectorRole.V);
boolean hasA = roles.injectorRoles().containsValue(NonLiquidRoles.InjectorRole.A);
if (!hasV) {
return true;
} else {
return hasA ? false : NeedsOfNature.getConfig().convertMaleOnlyVInjectionsToA();
}
} else {
return true;
}
}
private static boolean hasMaleOnlyPlayer(List<Entity> actors) {
for (Entity actor : actors) {
if (actor instanceof ServerPlayerEntity && NonGenderSystem.isOnlyMale(actor)) {
return true;
}
}
return false;
}
}
+208
View File
@@ -0,0 +1,208 @@
package com.nonid;
import com.nonid.item.EnergyStabilizerItem;
import com.nonid.potion.NonPotions;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.component.type.FoodComponent;
import net.minecraft.component.type.PotionContentsComponent;
import net.minecraft.component.type.TooltipDisplayComponent;
import net.minecraft.component.type.FoodComponent.Builder;
import net.minecraft.entity.EntityType;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.item.ItemGroup.Entries;
import net.minecraft.potion.Potion;
import net.minecraft.registry.Registries;
import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
import net.minecraft.util.Hand;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
final class NonItemSystem {
private static final FoodComponent LIQUID_BOTTLE_FOOD = new Builder().nutrition(1).saturationModifier(0.0F).alwaysEdible().build();
private static final FoodComponent FERTILE_NECTAR_FOOD = new Builder().nutrition(7).saturationModifier(0.0F).alwaysEdible().build();
private static final TooltipDisplayComponent LIQUID_BOTTLE_TOOLTIP = TooltipDisplayComponent.DEFAULT.with(DataComponentTypes.POTION_CONTENTS, true);
private NonItemSystem() {
}
static void giveItemOrDrop(ServerPlayerEntity player, ItemStack stack) {
if (player != null && stack != null && !stack.isEmpty()) {
if (!player.getInventory().insertStack(stack)) {
player.dropItem(stack, false);
}
}
}
static boolean consumeOneGlassBottle(ServerPlayerEntity player, Hand hand) {
if (player != null && hand != null) {
ItemStack stack = player.getStackInHand(hand);
if (!stack.isEmpty() && stack.isOf(Items.GLASS_BOTTLE)) {
stack.decrement(1);
return true;
} else {
return false;
}
} else {
return false;
}
}
static void refundGlassBottle(ServerPlayerEntity player) {
giveItemOrDrop(player, new ItemStack(Items.GLASS_BOTTLE));
}
static ItemStack createBottleStackForTank(LiquidHolder holder) {
Identifier entityTypeId = null;
if (holder != null && holder.getLiquidComposition() == LiquidHolder.LiquidComposition.ENTITY) {
entityTypeId = holder.getLiquidEntityTypeId();
}
return createLiquidBottleStack(entityTypeId);
}
static ItemStack createLiquidBottleStack(@Nullable Identifier entityTypeId) {
int tint = NonLiquidSystem.resolveBottleTintRgb(entityTypeId);
PotionContentsComponent contents = new PotionContentsComponent(
Optional.of(Registries.POTION.getEntry(NonPotions.LIQUID)), Optional.of(tint), List.of(), Optional.empty()
);
ItemStack stack = new ItemStack(Items.POTION);
stack.set(DataComponentTypes.POTION_CONTENTS, contents);
stack.set(DataComponentTypes.MAX_STACK_SIZE, 16);
stack.set(DataComponentTypes.FOOD, LIQUID_BOTTLE_FOOD);
NonPotions.applyHoneyBottleDrinkSound(stack);
stack.set(DataComponentTypes.TOOLTIP_DISPLAY, LIQUID_BOTTLE_TOOLTIP);
NonPotions.setLiquidBottleEntityTypeId(stack, entityTypeId);
if (entityTypeId == null) {
stack.set(DataComponentTypes.CUSTOM_NAME, Text.translatable("item.needsofnature.mixed_liquid_bottle").copy().styled(style -> style.withItalic(false)));
} else {
stack.set(
DataComponentTypes.CUSTOM_NAME,
Text.translatable("item.needsofnature.entity_liquid_bottle", new Object[]{resolveEntityNameText(entityTypeId)})
.copy()
.styled(style -> style.withItalic(false))
);
}
return stack;
}
static void addEntityLiquidAndStabilizersToItemGroup(Entries entries, @Nullable NonConfig config) {
for (String rawEntityId : NonLiquidSystem.resolveEffectiveLiquidGainMap().keySet()) {
Identifier entityId = Identifier.tryParse(rawEntityId);
if (entityId != null) {
entries.add(createLiquidBottleStack(entityId));
entries.add(createEnergyStabilizerStack(entityId));
}
}
}
static void addNonPotionVariantsToItemGroup(Entries entries) {
List<RegistryEntry<Potion>> nonPotions = new ArrayList<>();
for (Potion potion : Registries.POTION) {
Identifier potionId = Registries.POTION.getId(potion);
if (potionId != null && "needsofnature".equals(potionId.getNamespace())) {
nonPotions.add(Registries.POTION.getEntry(potion));
}
}
entries.add(createLiquidBottleStack(null));
for (RegistryEntry<Potion> potionEntry : nonPotions) {
if (potionEntry.value() != NonPotions.LIQUID) {
entries.add(createPotionVariantStack(Items.POTION, potionEntry));
}
}
for (RegistryEntry<Potion> potionEntry : nonPotions) {
if (potionEntry.value() != NonPotions.LIQUID) {
entries.add(createPotionVariantStack(Items.SPLASH_POTION, potionEntry));
}
}
for (RegistryEntry<Potion> potionEntry : nonPotions) {
if (potionEntry.value() != NonPotions.LIQUID) {
entries.add(createPotionVariantStack(Items.LINGERING_POTION, potionEntry));
}
}
for (RegistryEntry<Potion> potionEntry : nonPotions) {
if (potionEntry.value() != NonPotions.LIQUID) {
entries.add(createPotionVariantStack(Items.TIPPED_ARROW, potionEntry));
}
}
}
static ItemStack createPotionVariantStack(Item item, RegistryEntry<Potion> potionEntry) {
ItemStack stack = new ItemStack(item);
stack.set(DataComponentTypes.POTION_CONTENTS, new PotionContentsComponent(potionEntry));
if (item == Items.POTION && potionEntry.value() == NonPotions.FERTILE_NECTAR) {
stack.set(DataComponentTypes.FOOD, FERTILE_NECTAR_FOOD);
stack.set(DataComponentTypes.MAX_STACK_SIZE, 16);
NonPotions.applyHoneyBottleDrinkSound(stack);
}
return stack;
}
static boolean isNonPotionVariantStack(ItemStack stack) {
if (stack != null && !stack.isEmpty()) {
Item item = stack.getItem();
boolean potionLike = item == Items.POTION || item == Items.SPLASH_POTION || item == Items.LINGERING_POTION || item == Items.TIPPED_ARROW;
if (!potionLike) {
return false;
}
PotionContentsComponent contents = (PotionContentsComponent)stack.getOrDefault(DataComponentTypes.POTION_CONTENTS, PotionContentsComponent.DEFAULT);
Optional<RegistryEntry<Potion>> potionOpt = contents.potion();
if (potionOpt.isEmpty()) {
return false;
}
Identifier potionId = Registries.POTION.getId((Potion)potionOpt.get().value());
return potionId != null && "needsofnature".equals(potionId.getNamespace());
} else {
return false;
}
}
static ItemStack createEnergyStabilizerStack(@Nullable Identifier entityTypeId) {
return EnergyStabilizerItem.createStackForEntity(entityTypeId);
}
private static Text resolveEntityNameText(Identifier entityTypeId) {
return (Text)(entityTypeId == null
? Text.literal("未知")
: Registries.ENTITY_TYPE
.getOptionalValue(entityTypeId)
.<Text>map(EntityType::getName)
.orElseGet(() -> Text.literal(formatFallbackEntityName(entityTypeId))));
}
private static String formatFallbackEntityName(Identifier entityTypeId) {
String[] parts = entityTypeId.getPath().replace('/', '_').split("_");
StringBuilder out = new StringBuilder();
for (String part : parts) {
if (part != null && !part.isEmpty()) {
if (!out.isEmpty()) {
out.append(' ');
}
out.append(Character.toUpperCase(part.charAt(0)));
if (part.length() > 1) {
out.append(part.substring(1));
}
}
}
return out.isEmpty() ? entityTypeId.toString() : out.toString();
}
}
@@ -0,0 +1,301 @@
package com.nonid;
import com.nonid.client.NonLiquidColors;
import com.nonid.data.NonLiquidGainOverrides;
import com.nonid.effect.NonStatusEffects;
import com.nonid.network.LiquidStateS2CPayload;
import com.nonid.particle.NonParticles;
import com.nonid.potion.NonPotions;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.component.type.PotionContentsComponent;
import net.minecraft.entity.effect.StatusEffect;
import net.minecraft.entity.effect.StatusEffectInstance;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.item.ItemStack;
import net.minecraft.registry.Registries;
import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.Vec3d;
import org.jetbrains.annotations.Nullable;
final class NonLiquidSystem {
private static final double LIQUID_SNEAK_DECAY_MULTIPLIER = 16.0;
private static final double LIQUID_WATER_DECAY_MULTIPLIER = 2.0;
private static final double LIQUID_DECAY_MIN_MULT = 0.1;
private static final double LIQUID_DECAY_MAX_MULT = 1.5;
private static final double LIQUID_SNEAK_PARTICLE_BACK_OFFSET = 0.28;
private static final Map<UUID, Double> LIQUID_DECAY_REMAINDER = new ConcurrentHashMap<>();
private NonLiquidSystem() {
}
static void clearRuntimeState() {
LIQUID_DECAY_REMAINDER.clear();
}
static void syncLiquidState(ServerPlayerEntity player) {
if (player != null) {
if (player instanceof LiquidHolder holder) {
enforceLiquidCapacity(holder);
int stored = holder.getLiquidStored();
int capacity = holder.getLiquidCapacity();
updateFilledEffect(player, holder);
int tint = resolveLiquidTintRgb(holder);
ServerPlayNetworking.send(player, new LiquidStateS2CPayload(stored, capacity, tint));
NonApiInternals.fireLiquidChanged(player);
NonMessSystem.broadcastMessState(player);
NonAdvancementHooks.checkLiquidAdvancements(player, holder);
}
}
}
private static void enforceLiquidCapacity(LiquidHolder holder) {
if (holder != null) {
int capacity = holder.getLiquidCapacity();
int stored = holder.getLiquidStored();
if (capacity <= 0) {
if (stored > 0) {
holder.setLiquidStored(0);
}
} else {
if (stored > capacity) {
holder.setLiquidStored(capacity);
}
}
}
}
static void syncLiquidStateToAll(@Nullable MinecraftServer server) {
if (server != null) {
for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) {
syncLiquidState(player);
}
}
}
static void refreshLiquidBottleTintsToAll(@Nullable MinecraftServer server) {
if (server != null) {
for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) {
refreshLiquidBottleTints(player);
}
}
}
private static void refreshLiquidBottleTints(ServerPlayerEntity player) {
if (player != null) {
PlayerInventory inventory = player.getInventory();
for (int i = 0; i < inventory.size(); i++) {
refreshLiquidBottleTint(inventory.getStack(i));
}
refreshLiquidBottleTint(player.currentScreenHandler.getCursorStack());
}
}
private static void refreshLiquidBottleTint(ItemStack stack) {
if (NonPotions.isLiquidBottle(stack)) {
Identifier entityTypeId = NonPotions.getLiquidBottleEntityTypeId(stack);
int tint = resolveBottleTintRgb(entityTypeId);
PotionContentsComponent current = (PotionContentsComponent)stack.getOrDefault(DataComponentTypes.POTION_CONTENTS, PotionContentsComponent.DEFAULT);
PotionContentsComponent updated = new PotionContentsComponent(current.potion(), Optional.of(tint), List.of(), Optional.empty());
stack.set(DataComponentTypes.POTION_CONTENTS, updated);
}
}
static int resolveLiquidTintRgb(LiquidHolder holder) {
if (holder != null && holder.getLiquidStored() > 0) {
return holder.getLiquidComposition() == LiquidHolder.LiquidComposition.ENTITY
? resolveEntityLiquidColorRgb(holder.getLiquidEntityTypeId())
: resolveMixedLiquidColorRgb();
} else {
return resolveMixedLiquidColorRgb();
}
}
static int resolveBottleTintRgb(@Nullable Identifier entityTypeId) {
return entityTypeId == null ? resolveMixedLiquidColorRgb() : resolveEntityLiquidColorRgb(entityTypeId);
}
private static int resolveEntityLiquidColorRgb(@Nullable Identifier entityTypeId) {
if (entityTypeId == null) {
return 16776427;
}
String key = entityTypeId.toString();
String configuredHex = NeedsOfNature.getConfig().getLiquidColorByEntity().get(key);
Integer configuredRgb = NonLiquidColors.parseHexColor(configuredHex);
if (configuredRgb != null) {
return configuredRgb;
}
Integer packOverride = NonLiquidGainOverrides.getColorRgb(entityTypeId);
return packOverride != null ? packOverride : 16776427;
}
private static int resolveMixedLiquidColorRgb() {
String configuredHex = NeedsOfNature.getConfig().getLiquidColorByEntity().get("needsofnature:mixed");
Integer configuredRgb = NonLiquidColors.parseHexColor(configuredHex);
if (configuredRgb != null) {
return configuredRgb;
}
Integer packOverride = NonLiquidGainOverrides.getMixedColorRgb();
return packOverride != null ? packOverride : 15920063;
}
static Map<String, Integer> resolveEffectiveLiquidGainMap() {
Map<String, Integer> merged = new LinkedHashMap<>(NonLiquidGainOverrides.getGainByEntity());
merged.putAll(NeedsOfNature.getConfig().getLiquidGainByEntity());
return Map.copyOf(merged);
}
static Map<String, String> resolveEffectiveLiquidColorMap() {
Map<String, String> merged = new LinkedHashMap<>(NonLiquidGainOverrides.getColorByEntity());
String packMixedHex = NonLiquidGainOverrides.getMixedColorHex();
if (packMixedHex != null) {
merged.put("needsofnature:mixed", packMixedHex);
}
merged.putAll(NeedsOfNature.getConfig().getLiquidColorByEntity());
return Map.copyOf(merged);
}
static void tickLiquidDecay(ServerWorld world) {
if (world != null) {
if (!world.getPlayers().isEmpty()) {
NonConfig config = NeedsOfNature.getConfig();
boolean liquidTankEnabled = config.isLiquidTankEnabled();
double decayPerSecond = config.getLiquidDecayPerSecond();
for (ServerPlayerEntity player : world.getPlayers()) {
if (player instanceof LiquidHolder holder) {
if (!liquidTankEnabled) {
LIQUID_DECAY_REMAINDER.remove(player.getUuid());
if (holder.getLiquidStored() > 0) {
holder.setLiquidStored(0);
syncLiquidState(player);
}
updateFilledEffect(player, holder);
} else {
updateFilledEffect(player, holder);
if (!(decayPerSecond <= 0.0) && NeedsOfNature.getActivePlayerInstance(player) == null) {
int stored = holder.getLiquidStored();
if (stored <= 0) {
LIQUID_DECAY_REMAINDER.remove(player.getUuid());
updateFilledEffect(player, holder);
} else {
NonAccessoryEffects accessoryEffects = NonTrinketsIntegration.getActiveTankAccessoryEffects(player);
double decayPerTick = decayPerSecond / 20.0;
boolean equalizedDecay = accessoryEffects.equalizeLiquidDecayContext();
if (!equalizedDecay && player.isSneaking()) {
decayPerTick *= 16.0;
}
if (!equalizedDecay && (player.isTouchingWater() || player.isSubmergedInWater())) {
decayPerTick *= 2.0;
}
int capacity = holder.getLiquidCapacity();
double fillRatio = 0.0;
if (capacity > 0) {
fillRatio = Math.min(1.0, Math.max(0.0, (double)stored / capacity));
}
double curve = 1.0 - Math.log1p((1.0 - fillRatio) * 9.0) / Math.log1p(9.0);
double decayMultiplier = 0.1 + 1.4 * curve;
decayPerTick *= decayMultiplier;
decayPerTick *= accessoryEffects.liquidDecayMultiplier();
double remainder = LIQUID_DECAY_REMAINDER.getOrDefault(player.getUuid(), 0.0);
double accum = remainder + decayPerTick;
int decay = (int)Math.floor(accum);
remainder = accum - decay;
LIQUID_DECAY_REMAINDER.put(player.getUuid(), remainder);
if (decay > 0) {
int next = Math.max(0, stored - decay);
if (next != stored) {
holder.setLiquidStored(next);
syncLiquidState(player);
}
spawnLiquidDecayParticles(world, player);
}
}
}
}
}
}
}
}
}
static void updateFilledEffect(ServerPlayerEntity player, LiquidHolder holder) {
if (player != null && holder != null) {
int amplifier = getAmplifier(holder);
RegistryEntry<StatusEffect> entry = Registries.STATUS_EFFECT.getEntry(NonStatusEffects.FILLED);
StatusEffectInstance current = player.getStatusEffect(entry);
if (amplifier < 0) {
player.removeStatusEffect(entry);
} else {
if (current != null) {
if (current.getAmplifier() == amplifier) {
return;
}
player.removeStatusEffect(entry);
}
player.addStatusEffect(new StatusEffectInstance(entry, -1, amplifier, false, false, true));
}
}
}
private static int getAmplifier(LiquidHolder holder) {
int capacity = holder.getLiquidCapacity();
int stored = holder.getLiquidStored();
int amplifier = -1;
if (capacity > 0) {
float ratio = Math.max(0.0F, Math.min(1.0F, (float)stored / capacity));
if (ratio >= 0.8F) {
amplifier = 2;
} else if (ratio >= 0.5F) {
amplifier = 1;
} else if (ratio >= 0.3F) {
amplifier = 0;
}
}
return amplifier;
}
private static void spawnLiquidDecayParticles(ServerWorld world, ServerPlayerEntity player) {
if (world != null && player != null) {
int tintRgb = player instanceof LiquidHolder holder ? resolveLiquidTintRgb(holder) : resolveBottleTintRgb(null);
Vec3d spawnPos = resolveLiquidDecayParticlePos(player);
world.spawnParticles(NonParticles.liquidFalling(tintRgb), spawnPos.x, spawnPos.y, spawnPos.z, 1, 0.0, 0.0, 0.0, 0.0);
}
}
private static Vec3d resolveLiquidDecayParticlePos(ServerPlayerEntity player) {
double x = player.getX();
double z = player.getZ();
if (player.isSneaking()) {
double yawRad = Math.toRadians(player.bodyYaw);
x += Math.sin(yawRad) * 0.28;
z -= Math.cos(yawRad) * 0.28;
}
return new Vec3d(x, player.getBodyY(0.4), z);
}
}
@@ -0,0 +1,22 @@
package com.nonid;
import com.nonid.data.NonLiquidRoles;
public enum NonLiquidTankType {
V("v"),
A("a");
private final String slotName;
NonLiquidTankType(String slotName) {
this.slotName = slotName;
}
public String trinketsSlotId() {
return "legs/" + this.slotName;
}
public boolean accepts(NonLiquidRoles.InjectorRole role) {
return this == V && role == NonLiquidRoles.InjectorRole.V || this == A && role == NonLiquidRoles.InjectorRole.A;
}
}
+264
View File
@@ -0,0 +1,264 @@
package com.nonid;
import com.nonid.data.NonLiquidRoles;
import com.nonid.network.MessStateS2CPayload;
import com.nonid.particle.NonParticles;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.entity.Entity;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.Identifier;
final class NonMessSystem {
private static final Map<UUID, Long> MESS_CLEAN_NEXT_TICKS = new ConcurrentHashMap<>();
private NonMessSystem() {
}
static void clearRuntimeState() {
MESS_CLEAN_NEXT_TICKS.clear();
}
static void incrementMessOnPeak(
ServerWorld world, List<UUID> actorUuids, List<String> actorKeys, Identifier animationId, Set<NonLiquidRoles.InjectorRole> suppliedInjectorRoles
) {
if (NeedsOfNature.isMessSystemEnabled()) {
if (world != null && actorUuids != null && !actorUuids.isEmpty()) {
if (suppliedInjectorRoles != null && !suppliedInjectorRoles.isEmpty()) {
boolean hasV = suppliedInjectorRoles.contains(NonLiquidRoles.InjectorRole.V);
boolean hasA = suppliedInjectorRoles.contains(NonLiquidRoles.InjectorRole.A);
boolean hasM = suppliedInjectorRoles.contains(NonLiquidRoles.InjectorRole.M);
if (hasV || hasA || hasM) {
for (ServerPlayerEntity player : NeedsOfNature.collectPlayerReceiverActors(world, actorUuids, actorKeys, animationId)) {
if (player instanceof MessHolder holder) {
normalizeMaleOnlyMess(player, false);
int messGain = rollMessGain(world, player);
if (messGain > 0) {
if (hasV) {
if (NonGenderSystem.isOnlyMale(player)) {
if (NeedsOfNature.getConfig().convertMaleOnlyVInjectionsToA()) {
holder.setAMess(clampMess(holder.getAMess() + messGain));
}
} else {
holder.setVMess(clampMess(holder.getVMess() + messGain));
}
}
if (hasA) {
holder.setAMess(clampMess(holder.getAMess() + messGain));
}
if (hasM) {
holder.setMMess(clampMess(holder.getMMess() + messGain));
}
broadcastMessState(player);
if (holder.getVMess() >= 10 && holder.getAMess() >= 10 && holder.getMMess() >= 10) {
NonAdvancementHooks.grantMessedUp(player);
}
}
}
}
}
}
}
}
}
private static int rollMessGain(ServerWorld world, ServerPlayerEntity player) {
double multiplier = NonTrinketsIntegration.getActiveTankAccessoryEffects(player).messGainMultiplier();
if (Double.isFinite(multiplier) && !(multiplier <= 0.0)) {
int whole = (int)Math.floor(multiplier);
double fractional = multiplier - whole;
if (fractional > 0.0 && world.getRandom().nextDouble() < fractional) {
whole++;
}
return Math.max(0, whole);
} else {
return 0;
}
}
static void clearMess(ServerPlayerEntity player) {
if (player != null && player instanceof MessHolder holder) {
holder.setVMess(0);
holder.setAMess(0);
holder.setMMess(0);
MESS_CLEAN_NEXT_TICKS.remove(player.getUuid());
broadcastMessState(player);
}
}
static void clearMessStoredState(ServerPlayerEntity player) {
if (player != null && player instanceof MessHolder holder) {
holder.setVMess(0);
holder.setAMess(0);
holder.setMMess(0);
MESS_CLEAN_NEXT_TICKS.remove(player.getUuid());
}
}
static int clampMess(int value) {
return Math.max(0, Math.min(10, value));
}
static void tickMessCleaning(ServerWorld world, long now) {
if (NeedsOfNature.isMessSystemEnabled()) {
if (world != null) {
for (ServerPlayerEntity player : world.getPlayers()) {
if (player instanceof MessHolder holder) {
normalizeMaleOnlyMess(player, false);
UUID playerUuid = player.getUuid();
if (!hasAnyMess(holder)) {
MESS_CLEAN_NEXT_TICKS.remove(playerUuid);
} else {
boolean inWater = player.isTouchingWater() || player.isSubmergedInWater();
boolean inRain = !inWater && isExposedToRain(world, player);
if (!inWater && !inRain) {
MESS_CLEAN_NEXT_TICKS.remove(playerUuid);
} else {
Long scheduledTick = MESS_CLEAN_NEXT_TICKS.get(playerUuid);
if (scheduledTick == null) {
MESS_CLEAN_NEXT_TICKS.put(playerUuid, now + 20L);
} else {
long nextTick = scheduledTick;
if (now >= nextTick) {
boolean changed = decrementMess(holder);
if (changed) {
broadcastMessState(player);
if (inWater) {
spawnMessCleaningWaterParticles(world, player);
}
}
if (hasAnyMess(holder)) {
MESS_CLEAN_NEXT_TICKS.put(playerUuid, now + 20L);
} else {
MESS_CLEAN_NEXT_TICKS.remove(playerUuid);
}
}
}
}
}
}
}
}
}
}
static boolean normalizeMaleOnlyMess(Entity entity, boolean convertVToA) {
if (NonGenderSystem.isOnlyMale(entity) && entity instanceof MessHolder holder) {
int v = clampMess(holder.getVMess());
if (v <= 0) {
return false;
}
holder.setVMess(0);
if (convertVToA) {
holder.setAMess(clampMess(holder.getAMess() + v));
}
return true;
} else {
return false;
}
}
static void broadcastMessState(ServerPlayerEntity player) {
if (player != null) {
NonApiInternals.fireMessChanged(player);
MinecraftServer server = player.getEntityWorld().getServer();
if (server != null) {
for (ServerPlayerEntity target : server.getPlayerManager().getPlayerList()) {
sendMessState(target, player);
}
}
}
}
static void syncMessStates(ServerPlayerEntity target) {
if (target != null) {
MinecraftServer server = target.getEntityWorld().getServer();
if (server != null) {
for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) {
sendMessState(target, player);
}
}
}
}
private static void sendMessState(ServerPlayerEntity target, ServerPlayerEntity player) {
if (target != null && player != null) {
if (ServerPlayNetworking.canSend(target, MessStateS2CPayload.ID)) {
int v = 0;
int a = 0;
int m = 0;
if (NeedsOfNature.isMessSystemEnabled() && player instanceof MessHolder holder) {
normalizeMaleOnlyMess(player, false);
v = clampMess(holder.getVMess());
a = clampMess(holder.getAMess());
m = clampMess(holder.getMMess());
}
int tint = 16777215;
int stored = 0;
int capacity = 0;
if (player instanceof LiquidHolder liquidHolder) {
tint = NonLiquidSystem.resolveLiquidTintRgb(liquidHolder);
stored = Math.max(0, liquidHolder.getLiquidStored());
capacity = Math.max(0, liquidHolder.getLiquidCapacity());
}
ServerPlayNetworking.send(target, new MessStateS2CPayload(player.getUuid(), v, a, m, tint, stored, capacity));
}
}
}
private static boolean hasAnyMess(MessHolder holder) {
return holder != null && (holder.getVMess() > 0 || holder.getAMess() > 0 || holder.getMMess() > 0);
}
private static boolean decrementMess(MessHolder holder) {
if (holder == null) {
return false;
}
int beforeV = clampMess(holder.getVMess());
int beforeA = clampMess(holder.getAMess());
int beforeM = clampMess(holder.getMMess());
if (beforeV <= 0 && beforeA <= 0 && beforeM <= 0) {
return false;
}
holder.setVMess(Math.max(0, beforeV - 1));
holder.setAMess(Math.max(0, beforeA - 1));
holder.setMMess(Math.max(0, beforeM - 1));
return true;
}
private static boolean isExposedToRain(ServerWorld world, ServerPlayerEntity player) {
return world != null && player != null && world.isRaining() ? world.hasRain(player.getBlockPos()) : false;
}
private static void spawnMessCleaningWaterParticles(ServerWorld world, ServerPlayerEntity player) {
if (world != null && player != null) {
int tintRgb = player instanceof LiquidHolder holder ? NonLiquidSystem.resolveLiquidTintRgb(holder) : NonLiquidSystem.resolveBottleTintRgb(null);
int count = 2 + world.getRandom().nextInt(3);
double y = player.getBodyY(0.45);
for (int i = 0; i < count; i++) {
double ox = (world.getRandom().nextDouble() - 0.5) * player.getWidth();
double oy = (world.getRandom().nextDouble() - 0.5) * 0.45;
double oz = (world.getRandom().nextDouble() - 0.5) * player.getWidth();
world.spawnParticles(NonParticles.liquidWater(tintRgb), player.getX() + ox, y + oy, player.getZ() + oz, 1, 0.0, -0.01, 0.0, 0.0);
}
}
}
}
@@ -0,0 +1,227 @@
package com.nonid;
import com.nonid.data.NonEntityProfiles;
import com.nonid.internal.animation.data.AnimationDefinitions;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.Map.Entry;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.passive.HorseEntity;
import net.minecraft.registry.Registries;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
public final class NonMobEnergySystem {
public static final int DEFAULT_TICKS_PER_ENERGY = 120;
public static final int PARTNER_SCAN_INTERVAL_TICKS = 40;
public static final int POST_ATTEMPT_COOLDOWN_TICKS = 100;
public static final int ROLL_FAIL_COOLDOWN_TICKS = 1200;
public static final int ENERGY_JOIN_MIN = 70;
public static final int ENERGY_MOB_MATCH_MIN = 120;
public static final int ENERGY_ATTACK = 200;
public static final double PARTNER_SCAN_RADIUS = 20.0;
public static final double PLAYER_COOLDOWN_BYPASS_DIST_SQ = 64.0;
public static final double PLAYER_ANIMATION_LINGER_RADIUS = 3.0;
public static final double PLAYER_ANIMATION_LINGER_RADIUS_SQ = 9.0;
public static final double PLAYER_ANIMATION_LINGER_MIN_DISTANCE = 1.5;
public static final double PLAYER_ANIMATION_LINGER_MIN_DISTANCE_SQ = 2.25;
public static final double PLAYER_ANIMATION_LINGER_MOVE_SPEED = 1.1;
private static final int GENDER_MIXED = 0;
private static final int GENDER_MALE = 1;
private static final int GENDER_FEMALE = 2;
private NonMobEnergySystem() {
}
public static double passiveGainScale(MobEntity mob, float multiplier, float drift, boolean nearAnimation, double nearAnimationMultiplier) {
double rate = NeedsOfNature.getConfig().getEnergyGainRate();
double profileMultiplier = NonEntityProfiles.resolveEnergyGainMultiplier(mob);
double scale = multiplier * drift * rate * profileMultiplier;
if (nearAnimation) {
scale *= Math.max(1.0, nearAnimationMultiplier);
}
return Double.isFinite(scale) && scale > 0.0 ? scale : 0.0;
}
public static int ticksPerGain(MobEntity mob, float multiplier, float drift, boolean nearAnimation, double nearAnimationMultiplier) {
double scale = passiveGainScale(mob, multiplier, drift, nearAnimation, nearAnimationMultiplier);
return scale <= 0.0 ? 120 : Math.max(1, (int)Math.round(120.0 / scale));
}
public static float randomGainMultiplier(ServerWorld world) {
float a = world.getRandom().nextFloat();
float b = world.getRandom().nextFloat();
float tri = (a + b) * 0.5F;
return 0.4F + tri * 0.6F;
}
public static float randomGainDrift(ServerWorld world) {
return 0.9F + world.getRandom().nextFloat() * 0.2F;
}
public static boolean hasGenericAnimationSupport(EntityType<?> type, boolean baby) {
Identifier entityTypeId = Registries.ENTITY_TYPE.getId(type);
return AnimationDefinitions.hasEnabledGenericAnimationForEntityType(entityTypeId, baby);
}
public static boolean isEnergyAttackStabilized(Entity entity) {
return entity != null && entity.getCommandTags().contains("non.energy_stabilized");
}
public static boolean bypassesEntityEntityRoll(Entity self, Entity candidate) {
return self != null && candidate != null
? self instanceof HorseEntity && candidate.getType() == NeedsOfNature.HORSE_LIQUID_COLLECTOR_ENTITY_TYPE
|| self.getType() == NeedsOfNature.HORSE_LIQUID_COLLECTOR_ENTITY_TYPE && candidate instanceof HorseEntity
: false;
}
public static boolean passesSameGenderRoll(ServerWorld world, Entity debugSource, List<? extends Entity> actors) {
if (world == null) {
return true;
}
int group = resolveGenderGroup(actors);
int percent;
String label;
if (group == 1) {
percent = NeedsOfNature.getConfig().getMaleMaleChancePercent();
label = "male+male";
} else if (group == 2) {
percent = NeedsOfNature.getConfig().getFemaleFemaleChancePercent();
label = "female+female";
} else {
percent = 100;
label = "mixed";
}
return rollPercent(world, debugSource, percent, label);
}
public static int resolveMultiActorJoinChancePercent(Entity joiningEntity) {
Integer profileChance = NonEntityProfiles.resolve(joiningEntity).multiActorJoinChancePercent();
return profileChance == null ? NeedsOfNature.getConfig().getMultiActorJoinChancePercent() : Math.max(0, Math.min(100, profileChance));
}
public static boolean passesMultiActorJoinRoll(ServerWorld world, Entity debugSource, int totalActors, int chancePercent) {
return world != null && totalActors > 2 ? rollPercent(world, debugSource, chancePercent, "join 3+ actors (total=" + totalActors + ")") : true;
}
public static boolean passesEntityEntityRoll(ServerWorld world, Entity debugSource) {
if (world == null) {
return true;
}
int percent = NeedsOfNature.getConfig().getEntityEntityChancePercent();
return rollPercent(world, debugSource, percent, "entity+entity");
}
public static void appendCooldownMapDebug(
List<String> lines,
ServerWorld world,
long nowTick,
String label,
Map<UUID, Long> cooldowns,
Map<UUID, Long> bypassableCooldowns,
boolean includeBypassState
) {
if (lines != null && world != null && cooldowns != null && !cooldowns.isEmpty()) {
int shown = 0;
for (Entry<UUID, Long> entry : cooldowns.entrySet()) {
UUID partnerId = entry.getKey();
long remaining = entry.getValue() - nowTick;
if (partnerId != null && remaining > 0L) {
String suffix = "";
if (includeBypassState && bypassableCooldowns != null && bypassableCooldowns.containsKey(partnerId)) {
suffix = " (bypass: close+LOS)";
}
lines.add(label + ": " + debugEntityName(world, partnerId) + " " + NeedsOfNature.formatTicksForDebug(remaining) + suffix);
if (++shown >= 5) {
int extra = cooldowns.size() - shown;
if (extra > 0) {
lines.add(label + ": +" + extra + " more");
}
break;
}
}
}
}
}
public static String debugEntityName(ServerWorld world, UUID uuid) {
if (uuid == null) {
return "unknown";
}
Entity entity = world.getEntity(uuid);
if (entity != null) {
return entity.getDisplayName().getString();
}
String raw = uuid.toString();
return raw.length() <= 8 ? raw : raw.substring(0, 8);
}
private static int resolveGenderGroup(List<? extends Entity> actors) {
int group = 0;
if (actors != null && !actors.isEmpty()) {
for (Entity actor : actors) {
if (actor == null) {
return 0;
}
boolean hasMale = actor.getCommandTags().contains("gender.male");
boolean hasFemale = actor.getCommandTags().contains("gender.female");
int actorGroup;
if (hasMale && !hasFemale) {
actorGroup = 1;
} else {
if (!hasFemale || hasMale) {
return 0;
}
actorGroup = 2;
}
if (group == 0) {
group = actorGroup;
} else if (group != actorGroup) {
return 0;
}
}
return group;
} else {
return 0;
}
}
private static boolean rollPercent(ServerWorld world, Entity debugSource, int percent, String label) {
if (percent >= 100) {
return true;
}
if (percent <= 0) {
return false;
}
boolean success = world.getRandom().nextInt(100) < percent;
sendRollDebug(world, debugSource, label, percent, success);
return success;
}
private static void sendRollDebug(ServerWorld world, Entity debugSource, String label, int percent, boolean success) {
if (world != null && debugSource != null) {
String result = success ? "success" : "fail";
NeedsOfNature.sendDebugChatToNearby(
world, debugSource, 20.0, NonDebugChatCategory.INFO, Text.translatable("debug.needsofnature.roll_result", new Object[]{label, percent, result})
);
}
}
}
@@ -0,0 +1,36 @@
package com.nonid;
import com.nonid.entity.HorseLiquidCollectorEntity;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.passive.CamelEntity;
import net.minecraft.entity.passive.FoxEntity;
import net.minecraft.entity.passive.TameableEntity;
public final class NonMobGatherEligibility {
private NonMobGatherEligibility() {
}
public static boolean canInitiateAutoGather(MobEntity mob) {
if (mob == null || !mob.isAlive() || mob.isRemoved()) {
return false;
}
if (!mob.isAiDisabled() && !mob.isSleeping()) {
if (mob instanceof TameableEntity tameable && (tameable.isSitting() || tameable.isInSittingPose())) {
return false;
} else {
return mob instanceof FoxEntity fox && (fox.isSitting() || fox.isSleeping()) ? false : !(mob instanceof CamelEntity camel && camel.isSitting());
}
} else {
return false;
}
}
public static boolean canBeAutoGatherTarget(MobEntity mob) {
if (mob == null || !mob.isAlive() || mob.isRemoved()) {
return false;
} else {
return mob instanceof HorseLiquidCollectorEntity ? true : canInitiateAutoGather(mob);
}
}
}
@@ -0,0 +1,717 @@
package com.nonid;
import com.nonid.api.animation.NonAnimationDamageBehavior;
import com.nonid.internal.animation.api.AnimationService;
import com.nonid.internal.animation.data.AnimationDefinitions;
import com.nonid.internal.animation.server.AnimationStartContext;
import com.nonid.internal.animation.server.ServerAnimationController;
import com.nonid.network.AttackStateS2CPayload;
import com.nonid.network.PlayerAttackInputC2SPayload;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents;
import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents.AfterDamage;
import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents.AllowDamage;
import net.fabricmc.fabric.api.event.player.AttackEntityCallback;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents.Disconnect;
import net.minecraft.entity.Entity;
import net.minecraft.entity.damage.DamageSource;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.Items;
import net.minecraft.registry.RegistryKey;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.ActionResult;
import net.minecraft.util.Hand;
import net.minecraft.util.Identifier;
import net.minecraft.world.World;
import org.jetbrains.annotations.Nullable;
final class NonPlayerAttackSystem {
private static final int INPUT_LEFT = 1;
private static final int INPUT_RIGHT = 2;
private static final long INPUT_MIN_INTERVAL_TICKS = 1L;
private static final float PVP_MASH_RANDOM_MIN = 0.8F;
private static final float PVP_MASH_RANDOM_MAX = 1.2F;
private static final Map<UUID, NonPlayerAttackSystem.Contest> CONTESTS = new ConcurrentHashMap<>();
private static final Map<UUID, NonPlayerAttackSystem.DefeatedOwnership> DEFEATED_OWNERSHIP = new ConcurrentHashMap<>();
private static final Map<UUID, NonPlayerAttackSystem.FinishAttempt> FINISH_ATTEMPTS = new ConcurrentHashMap<>();
private static final ThreadLocal<NonPlayerAttackSystem.DamageBypass> VANILLA_PVP_BYPASS = new ThreadLocal<>();
private NonPlayerAttackSystem() {
}
static void register() {
AttackEntityCallback.EVENT.register((AttackEntityCallback)(player, world, hand, entity, hitResult) -> {
if (!(world instanceof ServerWorld serverWorld && player instanceof ServerPlayerEntity attacker && entity instanceof ServerPlayerEntity victim)) {
return ActionResult.PASS;
} else if (hand == Hand.MAIN_HAND && attacker.getMainHandStack().isEmpty()) {
return (ActionResult)(tryStartPlayerAttack(serverWorld, attacker, victim) ? ActionResult.SUCCESS : ActionResult.PASS);
} else {
return ActionResult.PASS;
}
});
ServerPlayNetworking.registerGlobalReceiver(
PlayerAttackInputC2SPayload.ID, (payload, context) -> context.server().execute(() -> handleInput(context.player(), payload))
);
ServerLivingEntityEvents.ALLOW_DAMAGE
.register(
(AllowDamage)(entity, source, amount) -> {
if (entity instanceof ServerPlayerEntity victim && victim.getEntityWorld() instanceof ServerWorld world) {
NonPlayerAttackSystem.DefeatedOwnership ownership = validOwnership(world, victim.getUuid());
if (ownership == null) {
return true;
} else {
ServerPlayerEntity attributed = resolveAttributedPlayer(source);
if (attributed != null && ownership.ownerUuid().equals(attributed.getUuid())) {
FINISH_ATTEMPTS.put(
victim.getUuid(),
new NonPlayerAttackSystem.FinishAttempt(
world.getRegistryKey(), attributed.getUuid(), ownership.instanceId(), countReadyTotems(victim), world.getTime()
)
);
return true;
} else {
return false;
}
}
} else {
return true;
}
}
);
ServerLivingEntityEvents.AFTER_DAMAGE.register((AfterDamage)(entity, source, baseDamageTaken, damageTaken, blocked) -> {
if (entity instanceof ServerPlayerEntity victim && victim.getEntityWorld() instanceof ServerWorld world) {
NonPlayerAttackSystem.FinishAttempt attempt = FINISH_ATTEMPTS.remove(victim.getUuid());
if (attempt != null && attempt.worldKey().equals(world.getRegistryKey()) && !blocked) {
ServerPlayerEntity attributed = resolveAttributedPlayer(source);
if (attributed != null && attempt.ownerUuid().equals(attributed.getUuid())) {
if (victim.isAlive() && countReadyTotems(victim) < attempt.totemsBefore()) {
AnimationService.stopAnimation(world, attempt.instanceId());
}
}
}
}
});
ServerPlayConnectionEvents.DISCONNECT.register((Disconnect)(handler, server) -> server.execute(() -> clearForPlayer(handler.player)));
}
private static boolean tryStartPlayerAttack(ServerWorld world, ServerPlayerEntity attacker, ServerPlayerEntity victim) {
if (world != null && attacker != null && victim != null) {
if (attacker == victim || attacker.getEntityWorld() != world || victim.getEntityWorld() != world) {
return false;
} else if (!attacker.isAlive() || attacker.isRemoved() || attacker.isSpectator()) {
return false;
} else if (!victim.isAlive() || victim.isRemoved() || victim.isSpectator()) {
return false;
} else if (isOwnerFinishingDamage(world, victim, attacker)) {
return false;
} else if (victim.isCreative() && !NeedsOfNature.getConfig().canAttackCreativePlayers()) {
return false;
} else if (!(attacker instanceof EnergyHolder holder && holder.getEnergy() >= 200)) {
return false;
} else {
if (!NonPlayerInteractionSystem.areAttacksAllowed(victim, attacker)) {
return false;
}
if (AnimationService.isActorPendingOrActive(world, attacker.getUuid()) || AnimationService.isActorPendingOrActive(world, victim.getUuid())) {
return false;
}
if (!NonAttackSystem.hasAnimationStartProtection(world, attacker) && !NonAttackSystem.hasAnimationStartProtection(world, victim)) {
NonPlayerAttackSystem.AttackSelection selection = selectAttack(world, attacker, victim, true, true);
if (selection.match() == null) {
return selection.blockedByProtector();
}
AnimationService.MatchedAnimation match = selection.match();
List<Entity> actors = sortedActors(attacker, victim);
NonGatherSystem.cancelForActors(world, List.of(attacker.getUuid(), victim.getUuid()));
UUID instanceId = NeedsOfNature.startAnimationNowWithContext(
world,
match.animationId(),
actors,
match.actorKeys(),
match.stages(),
NonAnimationDamageBehavior.STOP_ON_DAMAGE,
false,
victim,
buildPlayerAttackContext(victim.getUuid(), attacker.getUuid(), Set.of(attacker.getUuid())),
attacker,
false,
true
);
if (instanceId != null) {
NeedsOfNature.logRuntime(
"[NoN] Player attack started: attacker={} victim={} animation={} instance={}",
attacker.getName().getString(),
victim.getName().getString(),
match.animationId(),
instanceId
);
}
return true;
} else {
return false;
}
}
} else {
return false;
}
}
@Nullable
static AnimationService.MatchedAnimation findViableAttack(ServerWorld world, ServerPlayerEntity attacker, ServerPlayerEntity victim) {
NonPlayerAttackSystem.AttackSelection selection = selectAttack(world, attacker, victim, false, false);
return selection.match();
}
private static NonPlayerAttackSystem.AttackSelection selectAttack(
ServerWorld world, ServerPlayerEntity attacker, ServerPlayerEntity victim, boolean requireStartEligibility, boolean consumeProtector
) {
List<Entity> actors = sortedActors(attacker, victim);
Set<Identifier> excluded = new LinkedHashSet<>();
boolean blockedByProtector = false;
boolean protectorConsumed = false;
while (true) {
AnimationService.MatchedAnimation match = NonAnimationMatcher.findAutomaticAttack(
world,
actors,
Set.of(),
victim,
requireStartEligibility,
attacker,
excluded,
(candidateWorld, definitionx, candidateActors) -> NonPlayerInteractionSystem.isPlayerInteractionDefinition(definitionx),
null
);
if (match == null) {
return new NonPlayerAttackSystem.AttackSelection(null, blockedByProtector);
}
AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(match.animationId());
if (definition == null) {
excluded.add(match.animationId());
} else {
if (!NeedsOfNature.isProtectedByAccessory(world, definition, actors)) {
return new NonPlayerAttackSystem.AttackSelection(match, blockedByProtector);
}
blockedByProtector = true;
if (consumeProtector && !protectorConsumed) {
protectorConsumed = NeedsOfNature.consumeProtectorForAnimation(world, definition, actors);
}
excluded.add(match.animationId());
}
}
}
static AnimationStartContext buildPlayerAttackContext(UUID victimUuid, UUID ownerUuid, Set<UUID> attackerUuids) {
return AnimationStartContext.empty()
.withSessionData(NonAnimationSessionData.playerAttack(new NonAnimationSessionData.PlayerAttack(victimUuid, ownerUuid, ownerUuid, attackerUuids)));
}
static AnimationStartContext prepareDefeatedJoinContext(
ServerWorld world, List<UUID> existingActorUuids, ServerPlayerEntity requester, AnimationStartContext sourceContext
) {
AnimationStartContext context = sourceContext == null ? AnimationStartContext.empty() : sourceContext;
NonAnimationSessionData.PlayerAttack existing = playerAttack(context);
UUID victimUuid = existing == null ? firstPlayerUuid(world, existingActorUuids) : existing.victimUuid();
if (victimUuid == null) {
return context;
}
UUID finishOwnerUuid = existing == null ? requester.getUuid() : existing.finishOwnerUuid();
LinkedHashSet<UUID> attackers = new LinkedHashSet<>();
if (existing != null) {
attackers.addAll(existing.attackerUuids());
}
attackers.add(requester.getUuid());
return context.withSessionData(
NonAnimationSessionData.playerAttack(new NonAnimationSessionData.PlayerAttack(victimUuid, finishOwnerUuid, requester.getUuid(), attackers))
);
}
static AnimationStartContext defeatedTailContext(AnimationStartContext sourceContext, UUID victimUuid) {
AnimationStartContext context = sourceContext == null ? AnimationStartContext.empty() : sourceContext;
NonAnimationSessionData.PlayerAttack attack = playerAttack(context);
return attack == null
? context.withSessionData(NonAnimationSessionData.attack())
: context.withSessionData(NonAnimationSessionData.playerAttack(attack.forDefeatedTail(victimUuid)));
}
static boolean isPlayerAttackContext(@Nullable AnimationStartContext context) {
return playerAttack(context) != null;
}
@Nullable
static ServerPlayerEntity resolveAttackVictim(ServerWorld world, UUID instanceId, List<UUID> actorUuids) {
ServerAnimationController.ActiveInstanceSnapshot snapshot = ServerAnimationController.getActiveInstanceSnapshot(world, instanceId);
NonAnimationSessionData.PlayerAttack attack = snapshot == null ? null : playerAttack(snapshot.startContext());
if (attack != null && actorUuids != null && actorUuids.contains(attack.victimUuid())) {
ServerPlayerEntity victim = world.getServer().getPlayerManager().getPlayer(attack.victimUuid());
if (victim != null && victim.getEntityWorld() == world) {
return victim;
}
}
if (actorUuids == null) {
return null;
}
for (UUID actorUuid : actorUuids) {
ServerPlayerEntity player = world.getServer().getPlayerManager().getPlayer(actorUuid);
if (player != null && player.getEntityWorld() == world) {
return player;
}
}
return null;
}
static void onAnimationStarted(
ServerWorld world, UUID instanceId, List<UUID> actorUuids, @Nullable NonAnimationSessionData.PlayerAttack attack, long startTick, boolean defeated
) {
if (attack != null && actorUuids != null && actorUuids.contains(attack.victimUuid())) {
Set<UUID> actorSet = Set.copyOf(actorUuids);
LinkedHashSet<UUID> activeAttackers = new LinkedHashSet<>();
for (UUID attackerUuid : attack.attackerUuids()) {
if (actorSet.contains(attackerUuid)) {
ServerPlayerEntity attacker = world.getServer().getPlayerManager().getPlayer(attackerUuid);
if (attacker != null && attacker.getEntityWorld() == world) {
activeAttackers.add(attackerUuid);
}
}
}
if (defeated) {
DEFEATED_OWNERSHIP.put(
attack.victimUuid(),
new NonPlayerAttackSystem.DefeatedOwnership(world.getRegistryKey(), attack.victimUuid(), attack.finishOwnerUuid(), instanceId)
);
}
if (!activeAttackers.isEmpty()) {
NonPlayerAttackSystem.Contest contest = new NonPlayerAttackSystem.Contest(
world.getRegistryKey(), instanceId, attack.victimUuid(), attack.finishOwnerUuid(), attack.initiatorUuid(), activeAttackers, startTick, defeated
);
CONTESTS.put(instanceId, contest);
}
}
}
static void onAnimationStopped(ServerWorld world, UUID instanceId) {
NonPlayerAttackSystem.Contest contest = CONTESTS.remove(instanceId);
if (contest != null) {
FINISH_ATTEMPTS.remove(contest.victimUuid);
}
DEFEATED_OWNERSHIP.entrySet().removeIf(entry -> {
NonPlayerAttackSystem.DefeatedOwnership ownership = entry.getValue();
return ownership.instanceId().equals(instanceId) && ownership.worldKey().equals(world.getRegistryKey());
});
}
static boolean syncEscapeControl(ServerWorld world, UUID instanceId, boolean escapable, boolean defeated) {
NonPlayerAttackSystem.Contest contest = CONTESTS.get(instanceId);
if (contest != null && contest.worldKey.equals(world.getRegistryKey())) {
contest.escapable = escapable;
contest.defeated = defeated;
broadcastState(world, contest);
return true;
} else {
return false;
}
}
static boolean isContest(UUID instanceId) {
return instanceId != null && CONTESTS.containsKey(instanceId);
}
static List<String> getContestDebugLines(ServerPlayerEntity target) {
if (target != null && target.getEntityWorld() instanceof ServerWorld world) {
NonPlayerAttackSystem.Contest contest = findContestForPlayer(world, target.getUuid());
if (contest == null) {
return List.of();
} else {
ServerPlayerEntity victim = world.getServer().getPlayerManager().getPlayer(contest.victimUuid);
if (victim != null && victim.getEntityWorld() == world) {
List<String> lines = new ArrayList<>();
String role = contest.victimUuid.equals(target.getUuid()) ? "defender" : "attacker";
lines.add(
String.format(
Locale.ROOT, "PvP contest %s | role %s | progress %.1f%%", contest.instanceId.toString().substring(0, 8), role, contest.progress * 100.0F
)
);
lines.add(formatContestParticipant(world, contest, victim, true));
contest.activeAttackerUuids
.stream()
.sorted(Comparator.comparing(UUID::toString))
.map(uuid -> world.getServer().getPlayerManager().getPlayer(uuid))
.filter(player -> player != null && player.getEntityWorld() == world)
.map(player -> formatContestParticipant(world, contest, player, false))
.forEach(lines::add);
return List.copyOf(lines);
} else {
return List.of();
}
}
} else {
return List.of();
}
}
@Nullable
private static NonPlayerAttackSystem.Contest findContestForPlayer(ServerWorld world, UUID playerUuid) {
for (NonPlayerAttackSystem.Contest contest : CONTESTS.values()) {
if (contest.worldKey.equals(world.getRegistryKey()) && (contest.victimUuid.equals(playerUuid) || contest.activeAttackerUuids.contains(playerUuid))) {
return contest;
}
}
return null;
}
private static String formatContestParticipant(ServerWorld world, NonPlayerAttackSystem.Contest contest, ServerPlayerEntity participant, boolean defender) {
NonAttackSystem.PvpMashFactors factors = NonAttackSystem.resolveServerPlayerContestStep(world, participant, defender, contest.defeated, contest.startTick);
int energy = 0;
int maxEnergy = 0;
if (participant instanceof EnergyHolder holder) {
energy = holder.getEnergy();
maxEnergy = holder.getMaxEnergy();
}
return String.format(
Locale.ROOT,
"%s %s: %s%.2f-%.2f%%/input | HP %.1f/%.1f | energy %d/%d | health %.2fx, heat %.2fx, healthy %.2fx, ease %.2fx",
defender ? "Defender" : "Attacker",
participant.getName().getString(),
defender ? "+" : "-",
factors.step() * 0.8F * 100.0F,
factors.step() * 1.2F * 100.0F,
participant.getHealth(),
participant.getMaxHealth(),
energy,
maxEnergy,
factors.healthMultiplier(),
factors.heatMultiplier(),
factors.healthyDefenderMultiplier(),
factors.defeatedEaseMultiplier()
);
}
static void tick(ServerWorld world) {
long now = world.getTime();
for (NonPlayerAttackSystem.Contest contest : List.copyOf(CONTESTS.values())) {
if (contest.worldKey.equals(world.getRegistryKey())) {
if (AnimationService.getCurrentStage(world, contest.instanceId) == null) {
CONTESTS.remove(contest.instanceId, contest);
} else {
ServerPlayerEntity victim = world.getServer().getPlayerManager().getPlayer(contest.victimUuid);
if (victim == null || victim.getEntityWorld() != world || !victim.isAlive() || victim.isRemoved()) {
CONTESTS.remove(contest.instanceId, contest);
}
}
}
}
DEFEATED_OWNERSHIP.entrySet().removeIf(entry -> {
NonPlayerAttackSystem.DefeatedOwnership ownership = entry.getValue();
return ownership.worldKey().equals(world.getRegistryKey()) && AnimationService.getCurrentStage(world, ownership.instanceId()) == null;
});
FINISH_ATTEMPTS.entrySet().removeIf(entry -> {
NonPlayerAttackSystem.FinishAttempt attempt = entry.getValue();
return attempt.worldKey().equals(world.getRegistryKey()) && now - attempt.tick() > 1L;
});
}
private static void handleInput(ServerPlayerEntity player, PlayerAttackInputC2SPayload payload) {
if (player != null && payload != null && player.getEntityWorld() instanceof ServerWorld world) {
NonPlayerAttackSystem.Contest contest = CONTESTS.get(payload.instanceId());
if (contest != null && !contest.resolving && contest.escapable) {
if (contest.worldKey.equals(world.getRegistryKey())) {
boolean defender = contest.victimUuid.equals(player.getUuid());
boolean attacker = contest.activeAttackerUuids.contains(player.getUuid());
if (defender || attacker) {
int key = payload.keyId();
if (key == 1 || key == 2) {
long now = world.getTime();
long lastTick = contest.lastInputTicks.getOrDefault(player.getUuid(), Long.MIN_VALUE);
if (lastTick == Long.MIN_VALUE || now - lastTick >= 1L) {
int lastKey = contest.lastKeys.getOrDefault(player.getUuid(), 0);
if (lastKey != key) {
contest.lastInputTicks.put(player.getUuid(), now);
contest.lastKeys.put(player.getUuid(), key);
ServerPlayerEntity victim = world.getServer().getPlayerManager().getPlayer(contest.victimUuid);
if (victim != null && victim.getEntityWorld() == world && victim.isAlive()) {
NonAttackSystem.PvpMashFactors factors = NonAttackSystem.resolveServerPlayerContestStep(
world, player, defender, contest.defeated, contest.startTick
);
float randomMultiplier = 0.8F + world.getRandom().nextFloat() * 0.40000004F;
float step = factors.step() * randomMultiplier;
contest.progress = clampProgress(contest.progress + (defender ? step : -step));
broadcastState(world, contest);
if (contest.progress >= 1.0F) {
contest.resolving = true;
NeedsOfNature.completeAttackEscape(world, victim, contest.instanceId, contest.initiatorUuid);
}
}
}
}
}
}
}
}
}
}
private static void broadcastState(ServerWorld world, NonPlayerAttackSystem.Contest contest) {
AttackStateS2CPayload.EscapeProfile profile = contest.defeated
? AttackStateS2CPayload.EscapeProfile.DEFEATED
: AttackStateS2CPayload.EscapeProfile.NORMAL;
ServerPlayerEntity victim = world.getServer().getPlayerManager().getPlayer(contest.victimUuid);
if (victim != null && victim.getEntityWorld() == world) {
ServerPlayNetworking.send(
victim, new AttackStateS2CPayload(contest.instanceId, true, contest.escapable, profile, AttackStateS2CPayload.PvpRole.DEFENDER, contest.progress)
);
}
for (UUID attackerUuid : contest.activeAttackerUuids) {
ServerPlayerEntity attacker = world.getServer().getPlayerManager().getPlayer(attackerUuid);
if (attacker != null && attacker.getEntityWorld() == world) {
ServerPlayNetworking.send(
attacker,
new AttackStateS2CPayload(contest.instanceId, true, contest.escapable, profile, AttackStateS2CPayload.PvpRole.ATTACKER, contest.progress)
);
}
}
}
static boolean isOwnerFinishingDamage(ServerWorld world, ServerPlayerEntity victim, @Nullable DamageSource source) {
ServerPlayerEntity attacker = resolveAttributedPlayer(source);
return attacker != null && isOwnerFinishingDamage(world, victim, attacker);
}
static boolean shouldBypassVanillaPvpCheck(ServerPlayerEntity victim, PlayerEntity attacker) {
if (victim != null && attacker != null && victim.getEntityWorld() instanceof ServerWorld world) {
NonPlayerAttackSystem.DamageBypass bypass = VANILLA_PVP_BYPASS.get();
if (bypass != null && bypass.victimUuid().equals(victim.getUuid()) && bypass.attackerUuid().equals(attacker.getUuid())) {
return true;
}
NonPlayerAttackSystem.DefeatedOwnership ownership = validOwnership(world, victim.getUuid());
return ownership != null && ownership.ownerUuid().equals(attacker.getUuid());
} else {
return false;
}
}
static void applyPlayerAttributedEscapeDamage(ServerWorld world, ServerPlayerEntity victim, ServerPlayerEntity attacker, float amount) {
NonPlayerAttackSystem.DamageBypass previous = VANILLA_PVP_BYPASS.get();
VANILLA_PVP_BYPASS.set(new NonPlayerAttackSystem.DamageBypass(victim.getUuid(), attacker.getUuid()));
try {
victim.damage(world, victim.getDamageSources().playerAttack(attacker), amount);
} finally {
if (previous == null) {
VANILLA_PVP_BYPASS.remove();
} else {
VANILLA_PVP_BYPASS.set(previous);
}
}
}
private static boolean isOwnerFinishingDamage(ServerWorld world, ServerPlayerEntity victim, ServerPlayerEntity attacker) {
NonPlayerAttackSystem.DefeatedOwnership ownership = validOwnership(world, victim.getUuid());
return ownership != null && ownership.ownerUuid().equals(attacker.getUuid());
}
static void onOwnerFinishingDeath(ServerWorld world, ServerPlayerEntity victim) {
if (world != null && victim != null) {
NonPlayerAttackSystem.DefeatedOwnership ownership = DEFEATED_OWNERSHIP.get(victim.getUuid());
if (ownership != null && ownership.worldKey().equals(world.getRegistryKey())) {
FINISH_ATTEMPTS.remove(victim.getUuid());
}
}
}
@Nullable
static ServerPlayerEntity resolveAttributedPlayer(@Nullable DamageSource source) {
if (source == null) {
return null;
} else if (source.getAttacker() instanceof ServerPlayerEntity player) {
return player;
} else {
return source.getSource() instanceof ServerPlayerEntity player ? player : null;
}
}
static NonAnimationDamageBehavior defeatedDamageBehavior(AnimationStartContext context) {
return isPlayerAttackContext(context) ? NonAnimationDamageBehavior.IGNORE_DAMAGE : NonAnimationDamageBehavior.STOP_ON_DAMAGE;
}
static void clearForRespawn(ServerPlayerEntity player) {
clearForPlayer(player);
}
private static void clearForPlayer(ServerPlayerEntity player) {
if (player != null && player.getEntityWorld() instanceof ServerWorld world) {
UUID var6 = player.getUuid();
LinkedHashSet<UUID> stopInstances = new LinkedHashSet<>();
for (NonPlayerAttackSystem.Contest contest : List.copyOf(CONTESTS.values())) {
if (contest.worldKey.equals(world.getRegistryKey())
&& (
contest.victimUuid.equals(var6)
|| contest.finishOwnerUuid.equals(var6)
|| contest.initiatorUuid.equals(var6)
|| contest.activeAttackerUuids.contains(var6)
)) {
stopInstances.add(contest.instanceId);
}
}
for (NonPlayerAttackSystem.DefeatedOwnership ownership : List.copyOf(DEFEATED_OWNERSHIP.values())) {
if (ownership.worldKey().equals(world.getRegistryKey()) && (ownership.victimUuid().equals(var6) || ownership.ownerUuid().equals(var6))) {
stopInstances.add(ownership.instanceId());
}
}
for (UUID instanceId : stopInstances) {
AnimationService.stopAnimation(world, instanceId);
}
CONTESTS.entrySet().removeIf(entry -> stopInstances.contains(entry.getKey()));
DEFEATED_OWNERSHIP.entrySet().removeIf(entry -> entry.getKey().equals(var6) || entry.getValue().ownerUuid().equals(var6));
FINISH_ATTEMPTS.remove(var6);
}
}
static void clearRuntimeState() {
CONTESTS.clear();
DEFEATED_OWNERSHIP.clear();
FINISH_ATTEMPTS.clear();
}
@Nullable
private static NonPlayerAttackSystem.DefeatedOwnership validOwnership(ServerWorld world, UUID victimUuid) {
NonPlayerAttackSystem.DefeatedOwnership ownership = DEFEATED_OWNERSHIP.get(victimUuid);
if (ownership == null || !ownership.worldKey().equals(world.getRegistryKey())) {
return null;
} else if (AnimationService.getCurrentStage(world, ownership.instanceId()) == null) {
DEFEATED_OWNERSHIP.remove(victimUuid, ownership);
return null;
} else {
return ownership;
}
}
@Nullable
static NonAnimationSessionData.PlayerAttack playerAttack(@Nullable AnimationStartContext context) {
return context != null && context.sessionData() instanceof NonAnimationSessionData data ? data.playerAttack() : null;
}
@Nullable
private static UUID firstPlayerUuid(ServerWorld world, List<UUID> actorUuids) {
if (world != null && actorUuids != null) {
for (UUID actorUuid : actorUuids) {
ServerPlayerEntity player = world.getServer().getPlayerManager().getPlayer(actorUuid);
if (player != null && player.getEntityWorld() == world) {
return player.getUuid();
}
}
return null;
} else {
return null;
}
}
private static List<Entity> sortedActors(Entity first, Entity second) {
ArrayList<Entity> actors = new ArrayList<>(List.of(first, second));
actors.sort(Comparator.comparingInt(Entity::getId));
return List.copyOf(actors);
}
private static int countReadyTotems(ServerPlayerEntity player) {
int count = 0;
if (player.getMainHandStack().isOf(Items.TOTEM_OF_UNDYING)) {
count += player.getMainHandStack().getCount();
}
if (player.getOffHandStack().isOf(Items.TOTEM_OF_UNDYING)) {
count += player.getOffHandStack().getCount();
}
return count;
}
private static float clampProgress(float progress) {
return !Float.isFinite(progress) ? 0.0F : Math.max(0.0F, Math.min(1.0F, progress));
}
private record AttackSelection(@Nullable AnimationService.MatchedAnimation match, boolean blockedByProtector) {
}
private static final class Contest {
final RegistryKey<World> worldKey;
final UUID instanceId;
final UUID victimUuid;
final UUID finishOwnerUuid;
final UUID initiatorUuid;
final Set<UUID> activeAttackerUuids;
final long startTick;
final Map<UUID, Integer> lastKeys = new HashMap<>();
final Map<UUID, Long> lastInputTicks = new HashMap<>();
float progress;
boolean escapable;
boolean defeated;
boolean resolving;
Contest(
RegistryKey<World> worldKey,
UUID instanceId,
UUID victimUuid,
UUID finishOwnerUuid,
UUID initiatorUuid,
Set<UUID> activeAttackerUuids,
long startTick,
boolean defeated
) {
this.worldKey = worldKey;
this.instanceId = instanceId;
this.victimUuid = victimUuid;
this.finishOwnerUuid = finishOwnerUuid;
this.initiatorUuid = initiatorUuid;
this.activeAttackerUuids = Set.copyOf(activeAttackerUuids);
this.startTick = startTick;
this.defeated = defeated;
}
}
private record DamageBypass(UUID victimUuid, UUID attackerUuid) {
}
private record DefeatedOwnership(RegistryKey<World> worldKey, UUID victimUuid, UUID ownerUuid, UUID instanceId) {
}
private record FinishAttempt(RegistryKey<World> worldKey, UUID ownerUuid, UUID instanceId, int totemsBefore, long tick) {
}
}
@@ -0,0 +1,7 @@
package com.nonid;
import net.minecraft.server.network.ServerPlayerEntity;
public interface NonPlayerAvoidanceState {
boolean shouldSuppressPlayerAvoidance(ServerPlayerEntity var1);
}
@@ -0,0 +1,383 @@
package com.nonid;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.Map.Entry;
import net.minecraft.datafixer.DataFixTypes;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.world.PersistentState;
import net.minecraft.world.PersistentStateType;
import org.jetbrains.annotations.Nullable;
public final class NonPlayerInteractionProfiles extends PersistentState {
private static final Codec<NonPlayerInteractionProfiles> CODEC = RecordCodecBuilder.create(
instance -> instance.group(
Codec.unboundedMap(Codec.STRING, NonPlayerInteractionProfiles.PlayerProfiles.CODEC)
.optionalFieldOf("profiles", Map.of())
.forGetter(state -> state.profilesByOwner),
Codec.unboundedMap(Codec.STRING, Codec.STRING).optionalFieldOf("known_players", Map.of()).forGetter(state -> state.knownPlayerNames)
)
.apply(instance, NonPlayerInteractionProfiles::new)
);
private static final PersistentStateType<NonPlayerInteractionProfiles> TYPE = new PersistentStateType(
"needsofnature_player_interactions", NonPlayerInteractionProfiles::new, CODEC, DataFixTypes.SAVED_DATA_COMMAND_STORAGE
);
private final Map<String, NonPlayerInteractionProfiles.PlayerProfiles> profilesByOwner;
private final Map<String, String> knownPlayerNames;
private NonPlayerInteractionProfiles() {
this(Map.of(), Map.of());
}
private NonPlayerInteractionProfiles(Map<String, NonPlayerInteractionProfiles.PlayerProfiles> profilesByOwner, Map<String, String> knownPlayerNames) {
this.profilesByOwner = new LinkedHashMap<>(profilesByOwner == null ? Map.of() : profilesByOwner);
this.knownPlayerNames = new LinkedHashMap<>(knownPlayerNames == null ? Map.of() : knownPlayerNames);
}
public static NonPlayerInteractionProfiles get(ServerWorld world) {
if (world != null && world.getServer() != null && world.getServer().getOverworld() != null) {
return (NonPlayerInteractionProfiles)world.getServer().getOverworld().getPersistentStateManager().getOrCreate(TYPE);
} else {
throw new IllegalArgumentException("A live server world is required");
}
}
public synchronized void recordKnownPlayer(ServerPlayerEntity player) {
if (player != null) {
this.recordKnownPlayer(player.getUuid(), player.getGameProfile().name());
}
}
public synchronized void recordKnownPlayer(UUID uuid, String name) {
if (uuid != null) {
String safeName = sanitizeName(name);
String uuidKey = uuid.toString();
boolean changed = !safeName.equals(this.knownPlayerNames.put(uuidKey, safeName));
Map<String, NonPlayerInteractionProfiles.PlayerProfiles> updatedOwners = new LinkedHashMap<>();
for (Entry<String, NonPlayerInteractionProfiles.PlayerProfiles> ownerEntry : this.profilesByOwner.entrySet()) {
NonPlayerInteractionProfiles.PlayerProfiles profiles = ownerEntry.getValue();
NonPlayerInteractionProfiles.ProfileOverride existing = profiles.overrides().get(uuidKey);
if (existing != null && !safeName.equals(existing.name())) {
Map<String, NonPlayerInteractionProfiles.ProfileOverride> overrides = new LinkedHashMap<>(profiles.overrides());
overrides.put(uuidKey, new NonPlayerInteractionProfiles.ProfileOverride(safeName, existing.mode()));
updatedOwners.put(ownerEntry.getKey(), new NonPlayerInteractionProfiles.PlayerProfiles(profiles.defaultMode(), overrides));
}
}
if (!updatedOwners.isEmpty()) {
this.profilesByOwner.putAll(updatedOwners);
changed = true;
}
if (changed) {
this.markDirty();
}
}
}
@Nullable
public synchronized NonPlayerInteractionProfiles.KnownPlayer findKnownPlayerByName(String name) {
String normalized = sanitizeName(name);
if (normalized.isEmpty()) {
return null;
}
for (Entry<String, String> entry : this.knownPlayerNames.entrySet()) {
if (entry.getValue().equalsIgnoreCase(normalized)) {
try {
return new NonPlayerInteractionProfiles.KnownPlayer(UUID.fromString(entry.getKey()), entry.getValue());
} catch (IllegalArgumentException ignored) {
return null;
}
}
}
return null;
}
@Nullable
public synchronized NonPlayerInteractionProfiles.KnownPlayer findKnownPlayer(UUID uuid) {
if (uuid == null) {
return null;
}
String name = this.knownPlayerNames.get(uuid.toString());
return name == null ? null : new NonPlayerInteractionProfiles.KnownPlayer(uuid, name);
}
public synchronized NonPlayerInteractionProfiles.Mode resolveMode(UUID ownerUuid, UUID otherUuid, int allowedMask) {
NonPlayerInteractionProfiles.PlayerProfiles profiles = this.getOrDefault(ownerUuid);
NonPlayerInteractionProfiles.ProfileOverride override = otherUuid == null ? null : profiles.overrides().get(otherUuid.toString());
return NonPlayerInteractionProfiles.Mode.normalizeAllowed(override == null ? profiles.defaultMode() : override.mode(), allowedMask);
}
public synchronized NonPlayerInteractionProfiles.Snapshot snapshot(UUID ownerUuid, int allowedMask) {
this.normalizeOwner(ownerUuid, allowedMask);
NonPlayerInteractionProfiles.PlayerProfiles profiles = this.getOrDefault(ownerUuid);
NonPlayerInteractionProfiles.Mode normalizedDefault = NonPlayerInteractionProfiles.Mode.normalizeAllowed(profiles.defaultMode(), allowedMask);
List<NonPlayerInteractionProfiles.OverrideView> overrides = new ArrayList<>();
for (Entry<String, NonPlayerInteractionProfiles.ProfileOverride> entry : profiles.overrides().entrySet()) {
try {
UUID uuid = UUID.fromString(entry.getKey());
NonPlayerInteractionProfiles.ProfileOverride value = entry.getValue();
overrides.add(new NonPlayerInteractionProfiles.OverrideView(uuid, value.name(), value.mode()));
} catch (IllegalArgumentException var11) {
}
}
overrides.sort(Comparator.comparing(NonPlayerInteractionProfiles.OverrideView::name, String.CASE_INSENSITIVE_ORDER));
List<NonPlayerInteractionProfiles.KnownPlayer> known = new ArrayList<>();
for (Entry<String, String> entry : this.knownPlayerNames.entrySet()) {
try {
UUID uuid = UUID.fromString(entry.getKey());
if (!uuid.equals(ownerUuid)) {
known.add(new NonPlayerInteractionProfiles.KnownPlayer(uuid, entry.getValue()));
}
} catch (IllegalArgumentException var10) {
}
}
known.sort(Comparator.comparing(NonPlayerInteractionProfiles.KnownPlayer::name, String.CASE_INSENSITIVE_ORDER));
return new NonPlayerInteractionProfiles.Snapshot(normalizedDefault, List.copyOf(overrides), List.copyOf(known));
}
public synchronized void setDefaultMode(UUID ownerUuid, NonPlayerInteractionProfiles.Mode mode, int allowedMask) {
if (ownerUuid != null) {
NonPlayerInteractionProfiles.PlayerProfiles existing = this.getOrDefault(ownerUuid);
this.profilesByOwner
.put(
ownerUuid.toString(),
new NonPlayerInteractionProfiles.PlayerProfiles(NonPlayerInteractionProfiles.Mode.normalizeAllowed(mode, allowedMask), existing.overrides())
);
this.markDirty();
}
}
public synchronized void setOverride(UUID ownerUuid, UUID otherUuid, String name, NonPlayerInteractionProfiles.Mode mode, int allowedMask) {
if (ownerUuid != null && otherUuid != null && !ownerUuid.equals(otherUuid)) {
NonPlayerInteractionProfiles.PlayerProfiles existing = this.getOrDefault(ownerUuid);
Map<String, NonPlayerInteractionProfiles.ProfileOverride> overrides = new LinkedHashMap<>(existing.overrides());
String safeName = sanitizeName(name);
if (safeName.isEmpty()) {
safeName = this.knownPlayerNames.getOrDefault(otherUuid.toString(), otherUuid.toString());
}
overrides.put(
otherUuid.toString(),
new NonPlayerInteractionProfiles.ProfileOverride(safeName, NonPlayerInteractionProfiles.Mode.normalizeAllowed(mode, allowedMask))
);
this.profilesByOwner.put(ownerUuid.toString(), new NonPlayerInteractionProfiles.PlayerProfiles(existing.defaultMode(), overrides));
this.knownPlayerNames.put(otherUuid.toString(), safeName);
this.markDirty();
}
}
public synchronized void removeOverride(UUID ownerUuid, UUID otherUuid) {
if (ownerUuid != null && otherUuid != null) {
NonPlayerInteractionProfiles.PlayerProfiles existing = this.getOrDefault(ownerUuid);
if (existing.overrides().containsKey(otherUuid.toString())) {
Map<String, NonPlayerInteractionProfiles.ProfileOverride> overrides = new LinkedHashMap<>(existing.overrides());
overrides.remove(otherUuid.toString());
this.profilesByOwner.put(ownerUuid.toString(), new NonPlayerInteractionProfiles.PlayerProfiles(existing.defaultMode(), overrides));
this.markDirty();
}
}
}
public synchronized void normalizeAll(int allowedMask) {
boolean changed = false;
for (String ownerKey : List.copyOf(this.profilesByOwner.keySet())) {
changed |= this.normalizeOwnerKey(ownerKey, allowedMask);
}
if (changed) {
this.markDirty();
}
}
private void normalizeOwner(UUID ownerUuid, int allowedMask) {
if (ownerUuid != null && this.normalizeOwnerKey(ownerUuid.toString(), allowedMask)) {
this.markDirty();
}
}
private boolean normalizeOwnerKey(String ownerKey, int allowedMask) {
NonPlayerInteractionProfiles.PlayerProfiles existing = this.profilesByOwner.get(ownerKey);
if (existing == null) {
return false;
}
NonPlayerInteractionProfiles.Mode normalizedDefault = NonPlayerInteractionProfiles.Mode.normalizeAllowed(existing.defaultMode(), allowedMask);
Map<String, NonPlayerInteractionProfiles.ProfileOverride> overrides = new LinkedHashMap<>();
boolean changed = normalizedDefault != existing.defaultMode();
for (Entry<String, NonPlayerInteractionProfiles.ProfileOverride> entry : existing.overrides().entrySet()) {
NonPlayerInteractionProfiles.ProfileOverride value = entry.getValue();
NonPlayerInteractionProfiles.Mode normalized = NonPlayerInteractionProfiles.Mode.normalizeAllowed(value.mode(), allowedMask);
overrides.put(entry.getKey(), new NonPlayerInteractionProfiles.ProfileOverride(value.name(), normalized));
changed |= normalized != value.mode();
}
if (changed) {
this.profilesByOwner.put(ownerKey, new NonPlayerInteractionProfiles.PlayerProfiles(normalizedDefault, overrides));
}
return changed;
}
private NonPlayerInteractionProfiles.PlayerProfiles getOrDefault(UUID ownerUuid) {
return ownerUuid == null
? new NonPlayerInteractionProfiles.PlayerProfiles(NonPlayerInteractionProfiles.Mode.REQUESTS_AND_ATTACKS, Map.of())
: this.profilesByOwner
.getOrDefault(
ownerUuid.toString(), new NonPlayerInteractionProfiles.PlayerProfiles(NonPlayerInteractionProfiles.Mode.REQUESTS_AND_ATTACKS, Map.of())
);
}
public static int normalizeAllowedMask(int mask) {
int normalized = mask & 15;
return normalized == 0 ? 15 : normalized;
}
private static String sanitizeName(@Nullable String name) {
if (name == null) {
return "";
}
String trimmed = name.trim();
return trimmed.length() <= 64 ? trimmed : trimmed.substring(0, 64);
}
public record KnownPlayer(UUID uuid, String name) {
}
public enum Mode {
DENY_ALL("deny_all", NonPlayerInteractionProfiles.RequestPolicy.DENY, false),
REQUESTS_ONLY("requests_only", NonPlayerInteractionProfiles.RequestPolicy.ASK, false),
REQUESTS_AND_ATTACKS("requests_and_attacks", NonPlayerInteractionProfiles.RequestPolicy.ASK, true),
AUTO_ACCEPT("auto_accept", NonPlayerInteractionProfiles.RequestPolicy.AUTO_ACCEPT, true);
public static final Codec<NonPlayerInteractionProfiles.Mode> CODEC = Codec.STRING
.xmap(value -> fromId(value, REQUESTS_AND_ATTACKS), NonPlayerInteractionProfiles.Mode::id);
private final String id;
private final NonPlayerInteractionProfiles.RequestPolicy requestPolicy;
private final boolean attacksAllowed;
Mode(String id, NonPlayerInteractionProfiles.RequestPolicy requestPolicy, boolean attacksAllowed) {
this.id = id;
this.requestPolicy = requestPolicy;
this.attacksAllowed = attacksAllowed;
}
public String id() {
return this.id;
}
public NonPlayerInteractionProfiles.RequestPolicy requestPolicy() {
return this.requestPolicy;
}
public boolean attacksAllowed() {
return this.attacksAllowed;
}
public int bit() {
return 1 << this.ordinal();
}
public static NonPlayerInteractionProfiles.Mode fromId(@Nullable String value, NonPlayerInteractionProfiles.Mode fallback) {
if (value != null) {
String normalized = value.trim().toLowerCase(Locale.ROOT);
for (NonPlayerInteractionProfiles.Mode mode : values()) {
if (mode.id.equals(normalized)) {
return mode;
}
}
}
return fallback == null ? REQUESTS_AND_ATTACKS : fallback;
}
public static NonPlayerInteractionProfiles.Mode leastPermissiveAllowed(int allowedMask) {
int normalized = NonPlayerInteractionProfiles.normalizeAllowedMask(allowedMask);
for (NonPlayerInteractionProfiles.Mode mode : values()) {
if ((normalized & mode.bit()) != 0) {
return mode;
}
}
return REQUESTS_AND_ATTACKS;
}
public static NonPlayerInteractionProfiles.Mode normalizeAllowed(@Nullable NonPlayerInteractionProfiles.Mode mode, int allowedMask) {
NonPlayerInteractionProfiles.Mode resolved = mode == null ? REQUESTS_AND_ATTACKS : mode;
return (NonPlayerInteractionProfiles.normalizeAllowedMask(allowedMask) & resolved.bit()) != 0 ? resolved : leastPermissiveAllowed(allowedMask);
}
}
public record OverrideView(UUID uuid, String name, NonPlayerInteractionProfiles.Mode mode) {
}
public record PlayerProfiles(NonPlayerInteractionProfiles.Mode defaultMode, Map<String, NonPlayerInteractionProfiles.ProfileOverride> overrides) {
private static final Codec<NonPlayerInteractionProfiles.PlayerProfiles> CODEC = RecordCodecBuilder.create(
instance -> instance.group(
NonPlayerInteractionProfiles.Mode.CODEC
.optionalFieldOf("default_mode", NonPlayerInteractionProfiles.Mode.REQUESTS_AND_ATTACKS)
.forGetter(NonPlayerInteractionProfiles.PlayerProfiles::defaultMode),
Codec.unboundedMap(Codec.STRING, NonPlayerInteractionProfiles.ProfileOverride.CODEC)
.optionalFieldOf("overrides", Map.of())
.forGetter(NonPlayerInteractionProfiles.PlayerProfiles::overrides)
)
.apply(instance, NonPlayerInteractionProfiles.PlayerProfiles::new)
);
public PlayerProfiles {
defaultMode = defaultMode == null ? NonPlayerInteractionProfiles.Mode.REQUESTS_AND_ATTACKS : defaultMode;
overrides = overrides == null ? Map.of() : Map.copyOf(overrides);
}
}
public record ProfileOverride(String name, NonPlayerInteractionProfiles.Mode mode) {
private static final Codec<NonPlayerInteractionProfiles.ProfileOverride> CODEC = RecordCodecBuilder.create(
instance -> instance.group(
Codec.STRING.optionalFieldOf("name", "").forGetter(NonPlayerInteractionProfiles.ProfileOverride::name),
NonPlayerInteractionProfiles.Mode.CODEC
.optionalFieldOf("mode", NonPlayerInteractionProfiles.Mode.REQUESTS_AND_ATTACKS)
.forGetter(NonPlayerInteractionProfiles.ProfileOverride::mode)
)
.apply(instance, NonPlayerInteractionProfiles.ProfileOverride::new)
);
public ProfileOverride {
name = NonPlayerInteractionProfiles.sanitizeName(name);
mode = mode == null ? NonPlayerInteractionProfiles.Mode.REQUESTS_AND_ATTACKS : mode;
}
}
public enum RequestPolicy {
DENY,
ASK,
AUTO_ACCEPT;
}
public record Snapshot(
NonPlayerInteractionProfiles.Mode defaultMode,
List<NonPlayerInteractionProfiles.OverrideView> overrides,
List<NonPlayerInteractionProfiles.KnownPlayer> knownPlayers
) {
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,226 @@
package com.nonid;
import com.nonid.data.NonEntityProfiles;
import com.nonid.network.PregnancyCycleStateS2CPayload;
import java.util.List;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.Identifier;
final class NonPregnancyConceptionSystem {
private static final Identifier PLAYER_ENTITY_TYPE_ID = Identifier.of("minecraft", "player");
private static final long TICKS_PER_DAY = 24000L;
private static final long CYCLE_TICKS = 720000L;
private static final long CHECK_INTERVAL_TICKS = 20L;
private static final double CHECKS_PER_MINUTE = 60.0;
private static final int SAFE_PHASE_FIRST_END_DAY = 5;
private static final int SAFE_PHASE_SECOND_START_DAY = 26;
private NonPregnancyConceptionSystem() {
}
static void tick(ServerWorld world) {
if (world != null && !world.getPlayers().isEmpty() && world.getTime() % 20L == 0L) {
for (ServerPlayerEntity player : world.getPlayers()) {
if (isCycleEnabledFor(player)) {
updateCycle(player, world, true);
tryConception(world, player);
}
}
}
}
static void syncState(ServerPlayerEntity player) {
if (player != null && player.getEntityWorld() instanceof ServerWorld world) {
if (!isCycleEnabledFor(player)) {
ServerPlayNetworking.send(player, new PregnancyCycleStateS2CPayload(false, 0, PregnancyCyclePhase.NORMAL));
} else {
updateCycle(player, world, false);
int day = getCycleDay(player);
ServerPlayNetworking.send(player, new PregnancyCycleStateS2CPayload(true, day, resolvePhase(day)));
}
}
}
static String describeCycle(ServerPlayerEntity player) {
if (player != null && isCycleEnabledFor(player)) {
if (player.getEntityWorld() instanceof ServerWorld world) {
updateCycle(player, world, false);
}
int day = getCycleDay(player);
return "pregnancy cycle: day " + day + "/30 (" + resolvePhase(day).displayName() + ")";
} else {
return "pregnancy cycle: disabled";
}
}
static void copyState(ServerPlayerEntity oldPlayer, ServerPlayerEntity newPlayer) {
if (oldPlayer instanceof PregnancyCycleHolder oldHolder && newPlayer instanceof PregnancyCycleHolder newHolder) {
if (!oldHolder.isPregnancyCycleInitialized()) {
newHolder.clearPregnancyCycleState();
} else {
newHolder.setPregnancyCycleState(
oldHolder.getPregnancyCycleProgressTicks(), oldHolder.getPregnancyCycleLastGameTick(), oldHolder.getPregnancyCycleLastTimeOfDay()
);
}
}
}
static void recordRecentDonor(LiquidHolder holder, Identifier entityTypeId, NeedsOfNature.PregnancyVariantData variantData) {
if (holder != null && holder.getLiquidStored() > 0 && entityTypeId != null && !PLAYER_ENTITY_TYPE_ID.equals(entityTypeId)) {
holder.recordRecentLiquidDonor(entityTypeId, variantData);
}
}
static boolean setCycleDay(ServerPlayerEntity player, int day) {
if (player instanceof PregnancyCycleHolder holder && player.getEntityWorld() instanceof ServerWorld world) {
ServerWorld clockWorld = world.getServer() != null && world.getServer().getOverworld() != null ? world.getServer().getOverworld() : world;
int clampedDay = Math.max(1, Math.min(30, day));
long progressTicks = (clampedDay - 1) * 24000L;
holder.setPregnancyCycleState(progressTicks, Math.max(0L, clockWorld.getTime()), Math.max(0L, clockWorld.getTimeOfDay()));
syncState(player);
return true;
} else {
return false;
}
}
private static void tryConception(ServerWorld world, ServerPlayerEntity player) {
NonConfig config = NeedsOfNature.getConfig();
if (config != null && config.isPregnancyEnabled() && config.isLiquidBasedPregnancyEnabled()) {
if (config.isLiquidTankEnabled() && player.isAlive() && !player.isRemoved() && !player.isSpectator()) {
if (NeedsOfNature.getPregnantEntityTypeId(player) == null && !NonTrinketsIntegration.blocksPregnancy(player)) {
if (player instanceof LiquidHolder holder) {
if (holder.getLiquidStored() > 0 && holder.getLiquidCapacity() > 0) {
Identifier sourceType;
NeedsOfNature.PregnancyVariantData sourceVariantData;
if (holder.getLiquidComposition() == LiquidHolder.LiquidComposition.ENTITY) {
sourceType = holder.getLiquidEntityTypeId();
sourceVariantData = holder.getLiquidDonorVariantData();
} else {
if (holder.getLiquidComposition() != LiquidHolder.LiquidComposition.MIXED) {
return;
}
List<LiquidDonorHistory.Entry> donors = holder.getRecentLiquidDonors();
if (donors.isEmpty()) {
return;
}
LiquidDonorHistory.Entry selected = donors.get(world.getRandom().nextInt(donors.size()));
sourceType = selected.entityTypeId();
sourceVariantData = selected.variantData();
}
if (sourceType != null && !PLAYER_ENTITY_TYPE_ID.equals(sourceType)) {
int profileChance = NonEntityProfiles.resolve(sourceType).pregnancyChancePercent();
if (profileChance > 0) {
NonAccessoryEffects accessoryEffects = NonTrinketsIntegration.getActiveTankAccessoryEffects(player);
double fillRatio = Math.min(1.0, (double)holder.getLiquidStored() / holder.getLiquidCapacity());
int cycleDay = getCycleDay(player);
PregnancyCyclePhase phase = resolvePhase(cycleDay);
double cycleMultiplier = switch (phase) {
case SAFE -> config.getPregnancySafeDayChancePercent() / 100.0;
case NORMAL -> 1.0;
case RISKY -> config.getPregnancyRiskyChancePercent() / 100.0;
case OVULATION -> config.getPregnancyOvulationChancePercent() / 100.0;
};
double accessoryMultiplier = Math.max(0.0, accessoryEffects.pregnancyChanceMultiplier());
double naturalMinuteChance = profileChance / 100.0 * fillRatio * cycleMultiplier * accessoryMultiplier;
int fertilityFloor = NeedsOfNature.getFertilityIncreaserChanceFloor(player);
double potionMinuteChance = 0.0;
if (fertilityFloor > 0) {
potionMinuteChance = fertilityFloor / 100.0 * fillRatio * cycleMultiplier * accessoryMultiplier;
if (phase == PregnancyCyclePhase.RISKY || phase == PregnancyCyclePhase.OVULATION) {
potionMinuteChance = Math.min(potionMinuteChance, fertilityFloor / 100.0 * fillRatio);
}
}
double minuteChance = Math.max(naturalMinuteChance, potionMinuteChance);
minuteChance = Math.max(0.0, Math.min(1.0, minuteChance));
if (!(minuteChance <= 0.0)) {
double checkChance = minuteChance >= 1.0 ? 1.0 : 1.0 - Math.pow(1.0 - minuteChance, 0.016666666666666666);
if (!(world.getRandom().nextDouble() >= checkChance)) {
long durationTicks = Math.max(
1L, Math.round(config.getPregnancyDurationTicks() * Math.max(0.01, accessoryEffects.pregnancyDurationMultiplier()))
);
NeedsOfNature.beginPregnancy(player, sourceType, NeedsOfNature.getGlobalTick(world) + durationTicks, sourceVariantData);
if (sourceType.equals(NeedsOfNature.getPregnantEntityTypeId(player))) {
NonStats.incrementPregnancyStarted(player);
NonAdvancementHooks.grantFirstPregnancy(player);
}
}
}
}
}
}
}
}
}
}
}
private static boolean isCycleEnabledFor(ServerPlayerEntity player) {
NonConfig config = NeedsOfNature.getConfig();
return player != null && config != null && config.isPregnancyEnabled() && config.isLiquidBasedPregnancyEnabled() && !NonGenderSystem.isOnlyMale(player);
}
private static void updateCycle(ServerPlayerEntity player, ServerWorld world, boolean syncOnDayChange) {
if (player instanceof PregnancyCycleHolder holder) {
ServerWorld clockWorld = world.getServer() != null && world.getServer().getOverworld() != null ? world.getServer().getOverworld() : world;
long nowGameTick = Math.max(0L, clockWorld.getTime());
long nowTimeOfDay = Math.max(0L, clockWorld.getTimeOfDay());
if (!holder.isPregnancyCycleInitialized()) {
long initialProgress = Math.floorMod(world.getRandom().nextLong(), 720000L);
holder.setPregnancyCycleState(initialProgress, nowGameTick, nowTimeOfDay);
if (syncOnDayChange) {
syncState(player);
}
} else {
int oldDay = getCycleDay(holder.getPregnancyCycleProgressTicks());
long gameDelta = nonNegativeDelta(nowGameTick, holder.getPregnancyCycleLastGameTick());
long daytimeDelta = nonNegativeDelta(nowTimeOfDay, holder.getPregnancyCycleLastTimeOfDay());
long elapsed = Math.max(gameDelta, daytimeDelta);
long progress = Math.floorMod(holder.getPregnancyCycleProgressTicks() + Math.floorMod(elapsed, 720000L), 720000L);
holder.setPregnancyCycleState(progress, nowGameTick, nowTimeOfDay);
if (syncOnDayChange && oldDay != getCycleDay(progress)) {
syncState(player);
}
}
}
}
private static long nonNegativeDelta(long current, long previous) {
return current >= previous ? current - previous : 0L;
}
private static int getCycleDay(ServerPlayerEntity player) {
return player instanceof PregnancyCycleHolder holder && holder.isPregnancyCycleInitialized() ? getCycleDay(holder.getPregnancyCycleProgressTicks()) : 1;
}
private static int getCycleDay(long progressTicks) {
long normalized = Math.floorMod(progressTicks, 720000L);
return (int)(normalized / 24000L) + 1;
}
private static PregnancyCyclePhase resolvePhase(int day) {
NonConfig config = NeedsOfNature.getConfig();
if (config != null) {
int riskyStartDay = config.getPregnancyRiskyStartDay();
int ovulationEndDay = config.getPregnancyOvulationEndDay();
int ovulationStartDay = Math.max(riskyStartDay, ovulationEndDay - 1);
if (day >= ovulationStartDay && day <= ovulationEndDay) {
return PregnancyCyclePhase.OVULATION;
}
if (day >= riskyStartDay && day < ovulationStartDay) {
return PregnancyCyclePhase.RISKY;
}
}
return day > 5 && day < 26 ? PregnancyCyclePhase.NORMAL : PregnancyCyclePhase.SAFE;
}
}
@@ -0,0 +1,33 @@
package com.nonid;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
public final class NonSkinRepairBlocks {
private NonSkinRepairBlocks() {
}
public static boolean isNearRepairBlock(World world, BlockPos center, NonConfig config) {
if (world != null && center != null) {
for (BlockPos pos : BlockPos.iterateOutwards(center, 1, 1, 1)) {
if (isRepairBlock(world.getBlockState(pos), config)) {
return true;
}
}
return false;
} else {
return false;
}
}
public static boolean isRepairBlock(BlockState state, NonConfig config) {
if (state == null) {
return false;
} else {
return state.isOf(Blocks.LOOM) ? true : config != null && config.isCraftingTableSkinRepairAllowed() && state.isOf(Blocks.CRAFTING_TABLE);
}
}
}
+247
View File
@@ -0,0 +1,247 @@
package com.nonid;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.stat.StatFormatter;
import net.minecraft.stat.Stats;
import net.minecraft.util.Identifier;
public final class NonStats {
private static final String STAT_PATH_PEAKED_TOTAL = "peaked_animations_total";
private static final String STAT_PATH_LIQUID_GAINED_TOTAL = "liquid_gained_ml_total";
private static final String STAT_PATH_MULTI_ACTOR_TOTAL = "multi_actor_animations_total";
private static final String STAT_PATH_WATER_TOTAL = "water_animations_total";
private static final String STAT_PATH_ANIMATION_START_TOTAL = "animations_started_total";
private static final String STAT_PATH_ATTACK_ANIMATION_START_TOTAL = "attack_animations_started_total";
private static final String STAT_PATH_ATTACK_ESCAPE_SUCCESS_TOTAL = "attack_escapes_success_total";
private static final String STAT_PATH_M_INJECTOR_PEAKS_ML_TOTAL = "m_injector_peaks_ml_total";
private static final String STAT_PATH_MANUAL_PEAK_TOTAL = "manual_peaks_total";
private static final String STAT_PATH_FILL_BOTTLE_TOTAL = "bottles_filled_total";
private static final String STAT_PATH_PREGNANCY_STARTED_TOTAL = "pregnancies_started_total";
private static final String STAT_PATH_PREGNANCY_COMPLETED_TOTAL = "pregnancies_completed_total";
private static final String STAT_PATH_OFFSPRING_SPAWNED_TOTAL = "offspring_spawned_total";
private static final Identifier STAT_PEAKED_TOTAL = NeedsOfNature.id("peaked_animations_total");
private static final Identifier STAT_LIQUID_GAINED_TOTAL = NeedsOfNature.id("liquid_gained_ml_total");
private static final Identifier STAT_MULTI_ACTOR_TOTAL = NeedsOfNature.id("multi_actor_animations_total");
private static final Identifier STAT_WATER_TOTAL = NeedsOfNature.id("water_animations_total");
private static final Identifier STAT_ANIMATION_START_TOTAL = NeedsOfNature.id("animations_started_total");
private static final Identifier STAT_ATTACK_ANIMATION_START_TOTAL = NeedsOfNature.id("attack_animations_started_total");
private static final Identifier STAT_ATTACK_ESCAPE_SUCCESS_TOTAL = NeedsOfNature.id("attack_escapes_success_total");
private static final Identifier STAT_M_INJECTOR_PEAKS_ML_TOTAL = NeedsOfNature.id("m_injector_peaks_ml_total");
private static final Identifier STAT_MANUAL_PEAK_TOTAL = NeedsOfNature.id("manual_peaks_total");
private static final Identifier STAT_FILL_BOTTLE_TOTAL = NeedsOfNature.id("bottles_filled_total");
private static final Identifier STAT_PREGNANCY_STARTED_TOTAL = NeedsOfNature.id("pregnancies_started_total");
private static final Identifier STAT_PREGNANCY_COMPLETED_TOTAL = NeedsOfNature.id("pregnancies_completed_total");
private static final Identifier STAT_OFFSPRING_SPAWNED_TOTAL = NeedsOfNature.id("offspring_spawned_total");
private static final Map<Identifier, Identifier> PEAKED_BY_ENTITY = new LinkedHashMap<>();
private static final Map<Identifier, Identifier> LIQUID_GAINED_BY_ENTITY = new LinkedHashMap<>();
private static final Map<Identifier, Identifier> OFFSPRING_SPAWNED_BY_ENTITY = new LinkedHashMap<>();
private NonStats() {
}
public static void registerBaseStats() {
registerStat(STAT_PEAKED_TOTAL, StatFormatter.DEFAULT);
registerStat(STAT_LIQUID_GAINED_TOTAL, StatFormatter.DEFAULT);
registerStat(STAT_MULTI_ACTOR_TOTAL, StatFormatter.DEFAULT);
registerStat(STAT_WATER_TOTAL, StatFormatter.DEFAULT);
registerStat(STAT_ANIMATION_START_TOTAL, StatFormatter.DEFAULT);
registerStat(STAT_ATTACK_ANIMATION_START_TOTAL, StatFormatter.DEFAULT);
registerStat(STAT_ATTACK_ESCAPE_SUCCESS_TOTAL, StatFormatter.DEFAULT);
registerStat(STAT_M_INJECTOR_PEAKS_ML_TOTAL, StatFormatter.DEFAULT);
registerStat(STAT_MANUAL_PEAK_TOTAL, StatFormatter.DEFAULT);
registerStat(STAT_FILL_BOTTLE_TOTAL, StatFormatter.DEFAULT);
registerStat(STAT_PREGNANCY_STARTED_TOTAL, StatFormatter.DEFAULT);
registerStat(STAT_PREGNANCY_COMPLETED_TOTAL, StatFormatter.DEFAULT);
registerStat(STAT_OFFSPRING_SPAWNED_TOTAL, StatFormatter.DEFAULT);
}
public static void registerKnownEntityStats() {
for (Identifier entityId : Registries.ENTITY_TYPE.getIds()) {
ensureEntityStats(entityId);
}
}
public static void incrementAnimationStarted(ServerPlayerEntity player) {
increase(player, STAT_ANIMATION_START_TOTAL, 1);
}
public static void incrementMultiActorAnimation(ServerPlayerEntity player) {
increase(player, STAT_MULTI_ACTOR_TOTAL, 1);
}
public static void incrementWaterAnimation(ServerPlayerEntity player) {
increase(player, STAT_WATER_TOTAL, 1);
}
public static void incrementAttackAnimationStarted(ServerPlayerEntity player) {
increase(player, STAT_ATTACK_ANIMATION_START_TOTAL, 1);
}
public static void incrementAttackEscapeSuccess(ServerPlayerEntity player) {
increase(player, STAT_ATTACK_ESCAPE_SUCCESS_TOTAL, 1);
}
public static void addMInjectorPeaksMl(ServerPlayerEntity player, int amount) {
increase(player, STAT_M_INJECTOR_PEAKS_ML_TOTAL, amount);
}
public static void incrementManualPeak(ServerPlayerEntity player) {
increase(player, STAT_MANUAL_PEAK_TOTAL, 1);
}
public static void incrementFillBottle(ServerPlayerEntity player) {
increase(player, STAT_FILL_BOTTLE_TOTAL, 1);
}
public static int getBottlesFilled(ServerPlayerEntity player) {
return get(player, STAT_FILL_BOTTLE_TOTAL);
}
public static void incrementPregnancyStarted(ServerPlayerEntity player) {
increase(player, STAT_PREGNANCY_STARTED_TOTAL, 1);
}
public static void incrementPregnancyCompleted(ServerPlayerEntity player) {
increase(player, STAT_PREGNANCY_COMPLETED_TOTAL, 1);
}
public static int getPregnanciesCompleted(ServerPlayerEntity player) {
return get(player, STAT_PREGNANCY_COMPLETED_TOTAL);
}
public static void addOffspringSpawned(ServerPlayerEntity player, Identifier entityTypeId, int amount) {
if (player != null && amount > 0) {
increase(player, STAT_OFFSPRING_SPAWNED_TOTAL, amount);
if (entityTypeId != null) {
ensureEntityStats(entityTypeId);
Identifier stat = OFFSPRING_SPAWNED_BY_ENTITY.get(entityTypeId);
if (stat != null) {
increase(player, stat, amount);
}
}
}
}
public static void incrementPeakedAnimations(ServerPlayerEntity player, Set<Identifier> entityTypes) {
increase(player, STAT_PEAKED_TOTAL, 1);
if (entityTypes != null && !entityTypes.isEmpty()) {
for (Identifier entityTypeId : entityTypes) {
if (entityTypeId != null) {
ensureEntityStats(entityTypeId);
Identifier stat = PEAKED_BY_ENTITY.get(entityTypeId);
if (stat != null) {
increase(player, stat, 1);
}
}
}
}
}
public static void addLiquidGained(ServerPlayerEntity player, int amount, Map<Identifier, Integer> perEntityAmounts) {
if (amount > 0) {
increase(player, STAT_LIQUID_GAINED_TOTAL, amount);
if (perEntityAmounts != null && !perEntityAmounts.isEmpty()) {
for (Entry<Identifier, Integer> entry : perEntityAmounts.entrySet()) {
Identifier entityId = entry.getKey();
int entityAmount = entry.getValue() == null ? 0 : entry.getValue();
if (entityId != null && entityAmount > 0) {
ensureEntityStats(entityId);
Identifier stat = LIQUID_GAINED_BY_ENTITY.get(entityId);
if (stat != null) {
increase(player, stat, entityAmount);
}
}
}
}
}
}
private static void increase(ServerPlayerEntity player, Identifier statId, int amount) {
if (player != null && statId != null && amount > 0) {
if (registerStat(statId, StatFormatter.DEFAULT)) {
player.increaseStat(statId, amount);
}
}
}
private static int get(ServerPlayerEntity player, Identifier statId) {
if (player == null || statId == null) {
return 0;
} else {
return !registerStat(statId, StatFormatter.DEFAULT) ? 0 : player.getStatHandler().getStat(Stats.CUSTOM.getOrCreateStat(statId));
}
}
private static void ensureEntityStats(Identifier entityId) {
synchronized (NonStats.class) {
if (!PEAKED_BY_ENTITY.containsKey(entityId)) {
Identifier peakedStat = NeedsOfNature.id("peaked_animations_entity_" + sanitizeEntityId(entityId));
if (registerStat(peakedStat, StatFormatter.DEFAULT)) {
PEAKED_BY_ENTITY.put(entityId, peakedStat);
}
}
if (!LIQUID_GAINED_BY_ENTITY.containsKey(entityId)) {
Identifier liquidStat = NeedsOfNature.id("liquid_gained_ml_entity_" + sanitizeEntityId(entityId));
if (registerStat(liquidStat, StatFormatter.DEFAULT)) {
LIQUID_GAINED_BY_ENTITY.put(entityId, liquidStat);
}
}
if (!OFFSPRING_SPAWNED_BY_ENTITY.containsKey(entityId)) {
Identifier offspringStat = NeedsOfNature.id("offspring_spawned_entity_" + sanitizeEntityId(entityId));
if (registerStat(offspringStat, StatFormatter.DEFAULT)) {
OFFSPRING_SPAWNED_BY_ENTITY.put(entityId, offspringStat);
}
}
}
}
private static boolean registerStat(Identifier statId, StatFormatter formatter) {
if (statId == null) {
return false;
}
if (!Registries.CUSTOM_STAT.containsId(statId)) {
try {
Registry.register(Registries.CUSTOM_STAT, statId, statId);
} catch (RuntimeException ex) {
return false;
}
}
Stats.CUSTOM.getOrCreateStat(statId, formatter);
return true;
}
private static String sanitizeEntityId(Identifier entityId) {
return sanitizePathPart(entityId.getNamespace()) + "_" + sanitizePathPart(entityId.getPath());
}
private static String sanitizePathPart(String value) {
if (value != null && !value.isBlank()) {
StringBuilder out = new StringBuilder(value.length());
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
if (c >= 'a' && c <= 'z') {
out.append(c);
} else if (c >= '0' && c <= '9') {
out.append(c);
} else {
out.append('_');
}
}
return out.isEmpty() ? "unknown" : out.toString();
} else {
return "unknown";
}
}
}
@@ -0,0 +1,466 @@
package com.nonid;
import com.nonid.data.NonAccessoryBehavior;
import com.nonid.data.NonAccessoryDefinitions;
import com.nonid.data.NonAccessoryItemRegistry;
import com.nonid.data.NonAccessoryStackData;
import com.nonid.data.NonLiquidRoles;
import dev.emi.trinkets.api.SlotReference;
import dev.emi.trinkets.api.TrinketComponent;
import dev.emi.trinkets.api.TrinketsApi;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import net.fabricmc.fabric.api.util.TriState;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.entity.LivingEntity;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.particle.ParticleTypes;
import net.minecraft.registry.Registries;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.sound.SoundCategory;
import net.minecraft.sound.SoundEvents;
import net.minecraft.util.Identifier;
import net.minecraft.util.Pair;
final class NonTrinketsCompat {
private static final Map<UUID, NonAccessoryEffects> LAST_SYNCED_EFFECTS = new ConcurrentHashMap<>();
private static final Map<UUID, Set<String>> PENDING_UNEQUIPPED_SLOTS = new ConcurrentHashMap<>();
private NonTrinketsCompat() {
}
static void registerPredicates() {
TrinketsApi.registerTrinketPredicate(
NeedsOfNature.id("female"),
(stack, slot, entity) -> NonAccessoryBehavior.isSlot(slot.getId(), "legs/v") && !NonGenderSystem.hasFemaleGender(entity)
? TriState.FALSE
: TriState.DEFAULT
);
TrinketsApi.registerTrinketPredicate(
NeedsOfNature.id("male"),
(stack, slot, entity) -> NonAccessoryBehavior.isSlot(slot.getId(), "legs/d") && !NonGenderSystem.hasMaleGender(entity)
? TriState.FALSE
: TriState.DEFAULT
);
}
static void registerTrinketItem(Item item) {
if (item != null) {
TrinketsApi.registerTrinket(item, NonDataAccessoryTrinket.INSTANCE);
}
}
static NonAccessoryEffects getGlobalAccessoryEffects(LivingEntity entity) {
return entity == null
? NonAccessoryEffects.NEUTRAL
: TrinketsApi.getTrinketComponent(entity)
.map(TrinketComponent::getAllEquipped)
.orElse(List.of())
.stream()
.filter(pair -> !isPendingUnequippedSlot(entity, (SlotReference)pair.getLeft()))
.filter(pair -> {
Identifier itemId = Registries.ITEM.getId(((ItemStack)pair.getRight()).getItem());
return !NonAccessoryShedSystem.isSlotEffectSuppressed(entity.getUuid(), ((SlotReference)pair.getLeft()).getId(), itemId);
})
.map(pair -> Registries.ITEM.getId(((ItemStack)pair.getRight()).getItem()))
.map(NonAccessoryDefinitions::getEffects)
.map(NonAccessoryEffects::globalOnly)
.reduce(NonAccessoryEffects.NEUTRAL, NonAccessoryEffects::combine);
}
static NonAccessoryEffects getActiveTankAccessoryEffects(LivingEntity entity) {
if (entity == null) {
return NonAccessoryEffects.NEUTRAL;
}
String activeSlotId = NonTrinketsIntegration.getActiveTankSlotId(entity);
return TrinketsApi.getTrinketComponent(entity)
.map(TrinketComponent::getAllEquipped)
.orElse(List.of())
.stream()
.filter(pair -> !isPendingUnequippedSlot(entity, (SlotReference)pair.getLeft()))
.filter(pair -> NonAccessoryBehavior.isSlot(((SlotReference)pair.getLeft()).getId(), activeSlotId))
.filter(pair -> {
Identifier itemId = Registries.ITEM.getId(((ItemStack)pair.getRight()).getItem());
return !NonAccessoryShedSystem.isSlotEffectSuppressed(entity.getUuid(), ((SlotReference)pair.getLeft()).getId(), itemId);
})
.map(pair -> Registries.ITEM.getId(((ItemStack)pair.getRight()).getItem()))
.map(NonAccessoryDefinitions::getEffects)
.map(NonAccessoryEffects::tankScopedOnly)
.reduce(NonAccessoryEffects.NEUTRAL, NonAccessoryEffects::combine);
}
static boolean blocksPregnancy(ServerPlayerEntity player) {
if (player == null) {
return false;
}
String activeSlotId = NonTrinketsIntegration.getActiveTankSlotId(player);
return TrinketsApi.getTrinketComponent(player)
.map(TrinketComponent::getAllEquipped)
.orElse(List.of())
.stream()
.filter(pair -> !isPendingUnequippedSlot(player, (SlotReference)pair.getLeft()))
.anyMatch(
pair -> {
ItemStack stack = (ItemStack)pair.getRight();
if (stack != null && !stack.isEmpty()) {
String slotId = ((SlotReference)pair.getLeft()).getId();
if (!NonAccessoryBehavior.isSlot(slotId, activeSlotId)) {
return false;
}
Identifier itemId = Registries.ITEM.getId(stack.getItem());
return NonAccessoryShedSystem.isSlotEffectSuppressed(player.getUuid(), slotId, itemId)
? false
: NonAccessoryDefinitions.getBehavior(itemId).blocksPregnancy();
} else {
return false;
}
}
);
}
static boolean hasProtectorFor(ServerPlayerEntity player, NonLiquidRoles.InjectorRole role) {
return player != null && role != null && role != NonLiquidRoles.InjectorRole.NONE
? TrinketsApi.getTrinketComponent(player)
.map(TrinketComponent::getAllEquipped)
.orElse(List.of())
.stream()
.filter(pair -> !isPendingUnequippedSlot(player, (SlotReference)pair.getLeft()))
.anyMatch(pair -> {
ItemStack stack = (ItemStack)pair.getRight();
return stack != null && !stack.isEmpty() ? NonAccessoryDefinitions.getBehavior(Registries.ITEM.getId(stack.getItem())).blocks(role) : false;
})
: false;
}
static boolean consumeProtectorFor(ServerWorld world, ServerPlayerEntity player, NonLiquidRoles.InjectorRole role) {
if (world != null && player != null && role != null && role != NonLiquidRoles.InjectorRole.NONE) {
TrinketComponent component = (TrinketComponent)TrinketsApi.getTrinketComponent(player).orElse(null);
if (component == null) {
return false;
}
for (Pair<SlotReference, ItemStack> pair : component.getAllEquipped()) {
if (!isPendingUnequippedSlot(player, (SlotReference)pair.getLeft())) {
ItemStack stack = (ItemStack)pair.getRight();
if (stack != null && !stack.isEmpty()) {
NonAccessoryBehavior behavior = NonAccessoryDefinitions.getBehavior(Registries.ITEM.getId(stack.getItem()));
if (behavior.blocks(role)) {
if (Math.max(stack.getMaxDamage(), behavior.maxDurability()) <= 0) {
return false;
}
int cost = Math.max(1, behavior.protectionDurabilityCost());
applyDurabilityCost(world, player, (SlotReference)pair.getLeft(), stack, cost);
world.playSound(
null,
player.getX(),
player.getY(),
player.getZ(),
SoundEvents.ITEM_SHIELD_BLOCK,
SoundCategory.PLAYERS,
0.75F,
0.9F + world.getRandom().nextFloat() * 0.2F
);
world.spawnParticles(ParticleTypes.CRIT, player.getX(), player.getBodyY(0.55), player.getZ(), 8, 0.25, 0.35, 0.25, 0.08);
forceAccessorySync(player);
return true;
}
}
}
}
return false;
} else {
return false;
}
}
static void applyInjectionDurabilityCost(ServerWorld world, ServerPlayerEntity player, NonLiquidRoles.InjectorRole role) {
if (world != null && player != null && role != null) {
if (role == NonLiquidRoles.InjectorRole.V || role == NonLiquidRoles.InjectorRole.A) {
TrinketComponent component = (TrinketComponent)TrinketsApi.getTrinketComponent(player).orElse(null);
if (component != null) {
for (Pair<SlotReference, ItemStack> pair : component.getAllEquipped()) {
if (!isPendingUnequippedSlot(player, (SlotReference)pair.getLeft())) {
ItemStack stack = (ItemStack)pair.getRight();
if (stack != null && !stack.isEmpty()) {
String slotId = ((SlotReference)pair.getLeft()).getId();
if ((role != NonLiquidRoles.InjectorRole.V || NonAccessoryBehavior.isSlot(slotId, "legs/v"))
&& (role != NonLiquidRoles.InjectorRole.A || NonAccessoryBehavior.isSlot(slotId, "legs/a"))) {
NonAccessoryBehavior behavior = NonAccessoryDefinitions.getBehavior(Registries.ITEM.getId(stack.getItem()));
int cost = role == NonLiquidRoles.InjectorRole.V ? behavior.vInjectionDurabilityCost() : behavior.aInjectionDurabilityCost();
if (cost > 0) {
applyDurabilityCost(world, player, (SlotReference)pair.getLeft(), stack, cost);
}
}
}
}
}
forceAccessorySync(player);
}
}
}
}
private static boolean applyDurabilityCost(ServerWorld world, ServerPlayerEntity player, SlotReference slot, ItemStack stack, int cost) {
if (world != null && player != null && slot != null && stack != null && !stack.isEmpty() && cost > 0) {
NonAccessoryBehavior behavior = NonAccessoryDefinitions.getBehavior(Registries.ITEM.getId(stack.getItem()));
int maxDamage = Math.max(stack.getMaxDamage(), behavior.maxDurability());
if (maxDamage <= 0) {
return false;
}
if (stack.getMaxDamage() <= 0) {
stack.set(DataComponentTypes.MAX_DAMAGE, maxDamage);
}
if (stack.get(DataComponentTypes.DAMAGE) == null) {
stack.set(DataComponentTypes.DAMAGE, 0);
}
stack.damage(cost, world, player, brokenItem -> TrinketsApi.onTrinketBroken(stack, slot, player));
slot.inventory().markUpdate();
return true;
} else {
return false;
}
}
static void onAccessoryEquipped(LivingEntity entity, SlotReference slot) {
clearPendingUnequippedSlot(entity, slot);
if (entity instanceof ServerPlayerEntity player) {
forceAccessorySync(player);
}
}
static void onAccessoryUnequipped(LivingEntity entity, SlotReference slot) {
markPendingUnequippedSlot(entity, slot);
if (entity instanceof ServerPlayerEntity player) {
forceAccessorySync(player);
}
}
static void syncAccessoryStateIfChanged(ServerPlayerEntity player) {
if (player != null) {
NonAccessoryEffects effects = getGlobalAccessoryEffects(player);
effects = effects.combine(getActiveTankAccessoryEffects(player));
NonAccessoryEffects previous = LAST_SYNCED_EFFECTS.put(player.getUuid(), effects);
if (!effects.equals(previous)) {
NeedsOfNature.syncPlayerRuntimeSettings(player);
NeedsOfNature.syncLiquidState(player);
}
}
}
static void forceAccessorySync(ServerPlayerEntity player) {
if (player != null) {
LAST_SYNCED_EFFECTS.remove(player.getUuid());
syncAccessoryStateIfChanged(player);
}
}
static void clearAccessoryState(UUID playerUuid) {
if (playerUuid != null) {
LAST_SYNCED_EFFECTS.remove(playerUuid);
PENDING_UNEQUIPPED_SLOTS.remove(playerUuid);
}
}
static void clearRuntimeState() {
LAST_SYNCED_EFFECTS.clear();
PENDING_UNEQUIPPED_SLOTS.clear();
}
static List<NonTrinketsIntegration.ResolvedVisualLayer> resolveEquippedSkinOverlays(LivingEntity entity) {
return entity == null
? List.of()
: TrinketsApi.getTrinketComponent(entity)
.map(
component -> {
List<NonTrinketsIntegration.ResolvedVisualLayer> overlays = new ArrayList<>();
for (Pair<SlotReference, ItemStack> pair : component.getAllEquipped()) {
if (pair.getRight() != null && !((ItemStack)pair.getRight()).isEmpty()) {
Identifier itemId = Registries.ITEM.getId(((ItemStack)pair.getRight()).getItem());
if (!NonAccessoryShedSystem.isSlotVisuallyShed(entity.getUuid(), ((SlotReference)pair.getLeft()).getId(), itemId)) {
for (NonAccessoryItemRegistry.VisualLayer layer : NonAccessoryItemRegistry.getSkinOverlays(itemId)
.forSlot(((SlotReference)pair.getLeft()).getId())) {
NonTrinketsIntegration.ResolvedVisualLayer resolved = NonTrinketsIntegration.resolveVisualLayer((ItemStack)pair.getRight(), layer);
if (resolved != null) {
overlays.add(resolved);
}
}
}
}
}
return overlays.isEmpty() ? List.<NonTrinketsIntegration.ResolvedVisualLayer>of() : List.copyOf(overlays);
}
)
.orElse(List.<NonTrinketsIntegration.ResolvedVisualLayer>of());
}
static Set<String> resolveVirtualActorTags(LivingEntity entity) {
ItemStack stack = getMaleRoleAccessoryStack(entity);
if (stack.isEmpty()) {
return Set.of();
}
NonAccessoryItemRegistry.AccessoryItemDefinition definition = NonAccessoryItemRegistry.getDefinition(Registries.ITEM.getId(stack.getItem()));
return definition == null ? Set.of() : definition.actorVisualBehavior().virtualActorTags();
}
static ItemStack getMaleRoleAccessoryStack(LivingEntity entity) {
if (entity != null && NonGenderSystem.isOnlyFemale(entity)) {
TrinketComponent component = (TrinketComponent)TrinketsApi.getTrinketComponent(entity).orElse(null);
if (component == null) {
return ItemStack.EMPTY;
}
for (Pair<SlotReference, ItemStack> pair : component.getAllEquipped()) {
ItemStack stack = (ItemStack)pair.getRight();
if (stack != null && !stack.isEmpty() && !isPendingUnequippedSlot(entity, (SlotReference)pair.getLeft())) {
String slotId = ((SlotReference)pair.getLeft()).getId();
if (NonAccessoryBehavior.isSlot(slotId, "legs/v")) {
Identifier itemId = Registries.ITEM.getId(stack.getItem());
if (!NonAccessoryShedSystem.isSlotVisuallyShed(entity.getUuid(), slotId, itemId)
&& !NonAccessoryShedSystem.isSlotEffectSuppressed(entity.getUuid(), slotId, itemId)) {
NonAccessoryItemRegistry.AccessoryItemDefinition definition = NonAccessoryItemRegistry.getDefinition(itemId);
if (definition != null && definition.actorVisualBehavior().substitutesMaleRole()) {
return stack;
}
}
}
}
}
return ItemStack.EMPTY;
} else {
return ItemStack.EMPTY;
}
}
static NonTrinketsIntegration.StoredInjectorLiquid peekStoredInjectorLiquid(ServerPlayerEntity player) {
ItemStack stack = getMaleRoleAccessoryStack(player);
if (stack.isEmpty()) {
return null;
} else {
NonAccessoryItemRegistry.AccessoryItemDefinition definition = NonAccessoryItemRegistry.getDefinition(Registries.ITEM.getId(stack.getItem()));
if (definition != null && definition.actorVisualBehavior().storedLiquidCapacityMl() > 0) {
NonAccessoryStackData.StoredLiquid liquid = NonAccessoryStackData.getStoredLiquid(stack);
return liquid == null
? null
: new NonTrinketsIntegration.StoredInjectorLiquid(
liquid.entityTypeId(), Math.min(liquid.amountMl(), definition.actorVisualBehavior().storedLiquidCapacityMl())
);
} else {
return null;
}
}
}
static NonTrinketsIntegration.StoredInjectorLiquid consumeStoredInjectorLiquid(ServerPlayerEntity player) {
NonTrinketsIntegration.StoredInjectorLiquid liquid = peekStoredInjectorLiquid(player);
if (liquid == null) {
return null;
}
ItemStack stack = getMaleRoleAccessoryStack(player);
if (stack.isEmpty()) {
return null;
}
NonAccessoryStackData.setStoredLiquid(stack, null, 0);
TrinketsApi.getTrinketComponent(player).ifPresent(component -> {
for (Pair<SlotReference, ItemStack> pair : component.getAllEquipped()) {
if (pair.getRight() == stack) {
((SlotReference)pair.getLeft()).inventory().markUpdate();
break;
}
}
});
forceAccessorySync(player);
return liquid;
}
static List<ItemStack> getVisuallyShedAccessoryStacks(LivingEntity entity) {
return entity == null ? List.of() : TrinketsApi.getTrinketComponent(entity).map(component -> {
List<ItemStack> stacks = new ArrayList<>();
for (Pair<SlotReference, ItemStack> pair : component.getAllEquipped()) {
if (pair.getRight() != null && !((ItemStack)pair.getRight()).isEmpty()) {
Identifier itemId = Registries.ITEM.getId(((ItemStack)pair.getRight()).getItem());
if (NonAccessoryShedSystem.isSlotVisuallyShed(entity.getUuid(), ((SlotReference)pair.getLeft()).getId(), itemId)) {
stacks.add((ItemStack)pair.getRight());
}
}
}
return stacks.isEmpty() ? List.<ItemStack>of() : List.copyOf(stacks);
}).orElse(List.<ItemStack>of());
}
static NonTrinketsIntegration.TankScopeStatus resolveTankScopeStatus(LivingEntity entity, ItemStack stack) {
return entity != null && stack != null && !stack.isEmpty()
? TrinketsApi.getTrinketComponent(entity)
.map(
component -> {
for (Pair<SlotReference, ItemStack> pair : component.getAllEquipped()) {
if (pair.getRight() == stack) {
return NonTrinketsIntegration.isActiveTankSlot(entity, ((SlotReference)pair.getLeft()).getId())
? NonTrinketsIntegration.TankScopeStatus.ACTIVE_EQUIPPED
: NonTrinketsIntegration.TankScopeStatus.INACTIVE_EQUIPPED;
}
}
return NonTrinketsIntegration.TankScopeStatus.NOT_EQUIPPED;
}
)
.orElse(NonTrinketsIntegration.TankScopeStatus.NOT_EQUIPPED)
: NonTrinketsIntegration.TankScopeStatus.NOT_EQUIPPED;
}
private static void markPendingUnequippedSlot(LivingEntity entity, SlotReference slot) {
String slotId = slotId(slot);
if (entity != null && slotId != null) {
PENDING_UNEQUIPPED_SLOTS.computeIfAbsent(entity.getUuid(), ignored -> ConcurrentHashMap.newKeySet()).add(slotId);
}
}
private static void clearPendingUnequippedSlot(LivingEntity entity, SlotReference slot) {
String slotId = slotId(slot);
if (entity != null && slotId != null) {
Set<String> slots = PENDING_UNEQUIPPED_SLOTS.get(entity.getUuid());
if (slots != null) {
slots.remove(slotId);
if (slots.isEmpty()) {
PENDING_UNEQUIPPED_SLOTS.remove(entity.getUuid());
}
}
}
}
private static boolean isPendingUnequippedSlot(LivingEntity entity, SlotReference slot) {
String slotId = slotId(slot);
if (entity != null && slotId != null) {
Set<String> slots = PENDING_UNEQUIPPED_SLOTS.get(entity.getUuid());
return slots != null && slots.contains(slotId);
} else {
return false;
}
}
private static String slotId(SlotReference slot) {
return slot == null ? null : slot.getId();
}
}
@@ -0,0 +1,187 @@
package com.nonid;
import com.nonid.data.NonAccessoryBehavior;
import com.nonid.data.NonAccessoryItemRegistry;
import com.nonid.data.NonAccessoryStackData;
import com.nonid.data.NonLiquidRoles;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.entity.LivingEntity;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.registry.Registries;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
public final class NonTrinketsIntegration {
private static final String TRINKETS_MOD_ID = "trinkets";
private static final boolean TRINKETS_LOADED = FabricLoader.getInstance().isModLoaded("trinkets");
private NonTrinketsIntegration() {
}
public static boolean isTrinketsLoaded() {
return TRINKETS_LOADED;
}
public static void registerPredicates() {
if (TRINKETS_LOADED) {
NonTrinketsCompat.registerPredicates();
}
}
public static void registerTrinketItem(Item item) {
if (TRINKETS_LOADED) {
NonTrinketsCompat.registerTrinketItem(item);
}
}
public static boolean isSlotAvailableFor(LivingEntity entity, String slotId) {
if (entity == null || slotId == null) {
return false;
} else if (NonAccessoryBehavior.isSlot(slotId, "legs/v")) {
return NonGenderSystem.hasFemaleGender(entity);
} else {
return NonAccessoryBehavior.isSlot(slotId, "legs/d") ? NonGenderSystem.hasMaleGender(entity) : true;
}
}
public static NonAccessoryEffects getAccessoryEffects(LivingEntity entity) {
return getGlobalAccessoryEffects(entity);
}
public static NonAccessoryEffects getGlobalAccessoryEffects(LivingEntity entity) {
return TRINKETS_LOADED && entity != null ? NonTrinketsCompat.getGlobalAccessoryEffects(entity) : NonAccessoryEffects.NEUTRAL;
}
public static NonAccessoryEffects getActiveTankAccessoryEffects(LivingEntity entity) {
return TRINKETS_LOADED && entity != null ? NonTrinketsCompat.getActiveTankAccessoryEffects(entity) : NonAccessoryEffects.NEUTRAL;
}
public static String getActiveTankSlotId(LivingEntity entity) {
return NonGenderSystem.getLiquidTankType(entity).trinketsSlotId();
}
public static boolean isActiveTankSlot(LivingEntity entity, String slotId) {
return entity != null && slotId != null ? NonAccessoryBehavior.isSlot(slotId, getActiveTankSlotId(entity)) : false;
}
public static boolean blocksPregnancy(ServerPlayerEntity player) {
return TRINKETS_LOADED && player != null ? NonTrinketsCompat.blocksPregnancy(player) : false;
}
public static boolean hasProtectorFor(ServerPlayerEntity player, NonLiquidRoles.InjectorRole role) {
return TRINKETS_LOADED && player != null && role != null && role != NonLiquidRoles.InjectorRole.NONE
? NonTrinketsCompat.hasProtectorFor(player, role)
: false;
}
public static boolean consumeProtectorFor(ServerWorld world, ServerPlayerEntity player, NonLiquidRoles.InjectorRole role) {
return TRINKETS_LOADED && world != null && player != null && role != null && role != NonLiquidRoles.InjectorRole.NONE
? NonTrinketsCompat.consumeProtectorFor(world, player, role)
: false;
}
public static void applyInjectionDurabilityCost(ServerWorld world, ServerPlayerEntity player, NonLiquidRoles.InjectorRole role) {
if (TRINKETS_LOADED && world != null && player != null && role != null) {
NonTrinketsCompat.applyInjectionDurabilityCost(world, player, role);
}
}
public static void syncAccessoryStateIfChanged(ServerPlayerEntity player) {
if (TRINKETS_LOADED && player != null) {
NonTrinketsCompat.syncAccessoryStateIfChanged(player);
}
}
public static void forceAccessorySync(ServerPlayerEntity player) {
if (TRINKETS_LOADED && player != null) {
NonTrinketsCompat.forceAccessorySync(player);
}
}
public static void clearAccessoryState(UUID playerUuid) {
if (TRINKETS_LOADED && playerUuid != null) {
NonTrinketsCompat.clearAccessoryState(playerUuid);
}
}
static void clearRuntimeState() {
if (TRINKETS_LOADED) {
NonTrinketsCompat.clearRuntimeState();
}
}
public static List<NonTrinketsIntegration.ResolvedVisualLayer> resolveEquippedSkinOverlays(LivingEntity entity) {
return TRINKETS_LOADED && entity != null ? NonTrinketsCompat.resolveEquippedSkinOverlays(entity) : List.of();
}
public static Set<String> resolveVirtualActorTags(LivingEntity entity) {
return TRINKETS_LOADED && entity != null ? NonTrinketsCompat.resolveVirtualActorTags(entity) : Set.of();
}
public static ItemStack getMaleRoleAccessoryStack(LivingEntity entity) {
return TRINKETS_LOADED && entity != null ? NonTrinketsCompat.getMaleRoleAccessoryStack(entity) : ItemStack.EMPTY;
}
public static boolean hasMaleRoleAccessory(LivingEntity entity) {
return !getMaleRoleAccessoryStack(entity).isEmpty();
}
@Nullable
public static NonTrinketsIntegration.StoredInjectorLiquid peekStoredInjectorLiquid(ServerPlayerEntity player) {
return TRINKETS_LOADED && player != null ? NonTrinketsCompat.peekStoredInjectorLiquid(player) : null;
}
@Nullable
public static NonTrinketsIntegration.StoredInjectorLiquid consumeStoredInjectorLiquid(ServerPlayerEntity player) {
return TRINKETS_LOADED && player != null ? NonTrinketsCompat.consumeStoredInjectorLiquid(player) : null;
}
@Nullable
public static NonTrinketsIntegration.ResolvedVisualLayer resolveVisualLayer(ItemStack stack, NonAccessoryItemRegistry.VisualLayer layer) {
if (stack != null && !stack.isEmpty() && layer != null && layer.texture() != null) {
Identifier itemId = Registries.ITEM.getId(stack.getItem());
NonAccessoryItemRegistry.AccessoryItemDefinition definition = NonAccessoryItemRegistry.getDefinition(itemId);
int fallbackDye = definition == null ? 16777215 : definition.defaultDyeColor();
int tintRgb = switch (layer.tintMode()) {
case NONE -> 16777215;
case DYE -> NonAccessoryStackData.resolveDyeRgb(stack, fallbackDye);
case STORED_LIQUID -> {
NonAccessoryStackData.StoredLiquid liquid = NonAccessoryStackData.getStoredLiquid(stack);
yield liquid == null ? -1 : NeedsOfNature.resolveBottleLiquidTintRgb(liquid.entityTypeId());
}
};
return tintRgb < 0 ? null : new NonTrinketsIntegration.ResolvedVisualLayer(layer.texture(), tintRgb & 16777215);
} else {
return null;
}
}
public static List<ItemStack> getVisuallyShedAccessoryStacks(LivingEntity entity) {
return TRINKETS_LOADED && entity != null ? NonTrinketsCompat.getVisuallyShedAccessoryStacks(entity) : List.of();
}
public static NonTrinketsIntegration.TankScopeStatus resolveTankScopeStatus(LivingEntity entity, ItemStack stack) {
return TRINKETS_LOADED && entity != null && stack != null && !stack.isEmpty()
? NonTrinketsCompat.resolveTankScopeStatus(entity, stack)
: NonTrinketsIntegration.TankScopeStatus.NOT_EQUIPPED;
}
public record ResolvedVisualLayer(Identifier texture, int tintRgb) {
}
public record StoredInjectorLiquid(Identifier entityTypeId, int amountMl) {
}
public enum TankScopeStatus {
NOT_EQUIPPED,
ACTIVE_EQUIPPED,
INACTIVE_EQUIPPED;
}
}
@@ -0,0 +1,15 @@
package com.nonid;
public interface PregnancyCycleHolder {
boolean isPregnancyCycleInitialized();
long getPregnancyCycleProgressTicks();
long getPregnancyCycleLastGameTick();
long getPregnancyCycleLastTimeOfDay();
void setPregnancyCycleState(long var1, long var3, long var5);
void clearPregnancyCycleState();
}
@@ -0,0 +1,22 @@
package com.nonid;
public enum PregnancyCyclePhase {
SAFE,
NORMAL,
RISKY,
OVULATION;
public static PregnancyCyclePhase fromNetworkId(int id) {
PregnancyCyclePhase[] values = values();
return id >= 0 && id < values.length ? values[id] : NORMAL;
}
public String displayName() {
return switch (this) {
case SAFE -> "safe";
case NORMAL -> "normal";
case RISKY -> "risky";
case OVULATION -> "ovulation";
};
}
}
@@ -0,0 +1,32 @@
package com.nonid;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
public interface PregnancyHolder {
@Nullable
Identifier getPregnantEntityTypeId();
long getPregnancyEndTick();
boolean isPregnancyPendingHatch();
@Nullable
NeedsOfNature.PregnancyVariantData getPregnancyVariantData();
int getPregnancyOffspringCount();
void setPregnancyOffspringCount(int var1);
void setPregnancyState(@Nullable Identifier var1, long var2, boolean var4, @Nullable NeedsOfNature.PregnancyVariantData var5);
default void setPregnancyState(@Nullable Identifier entityTypeId, long endTick, boolean pendingHatch) {
this.setPregnancyState(entityTypeId, endTick, pendingHatch, null);
}
void clearPregnancyState();
default boolean hasPregnancyState() {
return this.getPregnantEntityTypeId() != null;
}
}
@@ -0,0 +1,19 @@
package com.nonid.api;
public record NonAccessoryEffectsSnapshot(
double liquidDecayMultiplier,
boolean equalizeLiquidDecayContext,
double playerEnergyGainMultiplier,
int liquidCapacityAdd,
double liquidGainMultiplier,
double filledEffectMultiplier,
double pregnancyChanceMultiplier,
double pregnancyDurationMultiplier,
double messGainMultiplier,
double destroyedSkinDamageMultiplier,
int attackEscapeHitsAdd,
double attackEscapeDamageMultiplier,
double playerEnergyAuraMultiplier,
double nearAnimationMobEnergyMultiplier
) {
}
@@ -0,0 +1,10 @@
package com.nonid.api;
public enum NonChangeSource {
API,
COMMAND,
ITEM,
CLIENT_PREFERENCE,
JOIN_SELECTION,
SYSTEM;
}
@@ -0,0 +1,4 @@
package com.nonid.api;
public record NonDestroyedSkinSnapshot(int stage, int damage, int maxDamage) {
}
@@ -0,0 +1,7 @@
package com.nonid.api;
public record NonEnergySnapshot(int energy, int maxEnergy, float passiveGainMultiplier, float passiveGainDrift, boolean initialized) {
public float fraction() {
return this.maxEnergy <= 0 ? 0.0F : Math.clamp((float)this.energy / this.maxEnergy, 0.0F, 1.0F);
}
}
@@ -0,0 +1,45 @@
package com.nonid.api;
import com.nonid.EnergyHolder;
import net.minecraft.entity.LivingEntity;
import org.jetbrains.annotations.Nullable;
public final class NonEntityApi {
private NonEntityApi() {
}
public static boolean hasEnergy(LivingEntity entity) {
return entity instanceof EnergyHolder;
}
@Nullable
public static NonEnergySnapshot getEnergy(LivingEntity entity) {
return entity instanceof EnergyHolder holder
? new NonEnergySnapshot(
holder.getEnergy(), holder.getMaxEnergy(), holder.getEnergyGainMultiplier(), holder.getEnergyGainDrift(), holder.isEnergyInitialized()
)
: null;
}
public static boolean setEnergy(LivingEntity entity, int energy) {
if (entity instanceof EnergyHolder holder && !entity.getEntityWorld().isClient()) {
int before = holder.getEnergy();
holder.setEnergy(energy);
return holder.getEnergy() != before;
} else {
return false;
}
}
public static int addEnergy(LivingEntity entity, int amount) {
if (entity instanceof EnergyHolder holder && !entity.getEntityWorld().isClient() && amount != 0) {
int before = holder.getEnergy();
long requested = (long)before + amount;
int safeRequested = requested > 2147483647L ? Integer.MAX_VALUE : (requested < -2147483648L ? Integer.MIN_VALUE : (int)requested);
holder.setEnergy(safeRequested);
return holder.getEnergy() - before;
} else {
return 0;
}
}
}
+105
View File
@@ -0,0 +1,105 @@
package com.nonid.api;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
public final class NonEvents {
public static final Event<NonEvents.PlayerGenderChanged> PLAYER_GENDER_CHANGED = EventFactory.createArrayBacked(
NonEvents.PlayerGenderChanged.class, callbacks -> (player, gender, source) -> {
for (NonEvents.PlayerGenderChanged callback : callbacks) {
callback.onPlayerGenderChanged(player, gender, source);
}
}
);
public static final Event<NonEvents.LiquidTankChanged> LIQUID_TANK_CHANGED = EventFactory.createArrayBacked(
NonEvents.LiquidTankChanged.class, callbacks -> (player, snapshot) -> {
for (NonEvents.LiquidTankChanged callback : callbacks) {
callback.onLiquidTankChanged(player, snapshot);
}
}
);
public static final Event<NonEvents.MessChanged> MESS_CHANGED = EventFactory.createArrayBacked(
NonEvents.MessChanged.class, callbacks -> (player, snapshot) -> {
for (NonEvents.MessChanged callback : callbacks) {
callback.onMessChanged(player, snapshot);
}
}
);
public static final Event<NonEvents.DestroyedSkinChanged> DESTROYED_SKIN_CHANGED = EventFactory.createArrayBacked(
NonEvents.DestroyedSkinChanged.class, callbacks -> (player, snapshot) -> {
for (NonEvents.DestroyedSkinChanged callback : callbacks) {
callback.onDestroyedSkinChanged(player, snapshot);
}
}
);
public static final Event<NonEvents.PregnancyChanged> PREGNANCY_CHANGED = EventFactory.createArrayBacked(
NonEvents.PregnancyChanged.class, callbacks -> (player, snapshot) -> {
for (NonEvents.PregnancyChanged callback : callbacks) {
callback.onPregnancyChanged(player, snapshot);
}
}
);
public static final Event<NonEvents.ModifyLiquidGain> MODIFY_LIQUID_GAIN = EventFactory.createArrayBacked(
NonEvents.ModifyLiquidGain.class, callbacks -> (player, sourceEntityType, amountMl) -> {
int result = Math.max(0, amountMl);
for (NonEvents.ModifyLiquidGain callback : callbacks) {
result = Math.max(0, callback.modifyLiquidGain(player, sourceEntityType, result));
}
return result;
}
);
public static final Event<NonEvents.AllowPregnancy> ALLOW_PREGNANCY = EventFactory.createArrayBacked(
NonEvents.AllowPregnancy.class, callbacks -> (player, entityTypeId) -> {
for (NonEvents.AllowPregnancy callback : callbacks) {
if (!callback.allowPregnancy(player, entityTypeId)) {
return false;
}
}
return true;
}
);
private NonEvents() {
}
@FunctionalInterface
public interface AllowPregnancy {
boolean allowPregnancy(ServerPlayerEntity var1, Identifier var2);
}
@FunctionalInterface
public interface DestroyedSkinChanged {
void onDestroyedSkinChanged(ServerPlayerEntity var1, NonDestroyedSkinSnapshot var2);
}
@FunctionalInterface
public interface LiquidTankChanged {
void onLiquidTankChanged(ServerPlayerEntity var1, NonLiquidTankSnapshot var2);
}
@FunctionalInterface
public interface MessChanged {
void onMessChanged(ServerPlayerEntity var1, NonMessSnapshot var2);
}
@FunctionalInterface
public interface ModifyLiquidGain {
int modifyLiquidGain(ServerPlayerEntity var1, @Nullable Identifier var2, int var3);
}
@FunctionalInterface
public interface PlayerGenderChanged {
void onPlayerGenderChanged(ServerPlayerEntity var1, NonGender var2, NonChangeSource var3);
}
@FunctionalInterface
public interface PregnancyChanged {
void onPregnancyChanged(ServerPlayerEntity var1, NonPregnancySnapshot var2);
}
}
@@ -0,0 +1,27 @@
package com.nonid.api;
public enum NonGender {
NONE(0),
MALE(1),
FEMALE(2),
BOTH(3);
private final int mask;
NonGender(int mask) {
this.mask = mask;
}
public int mask() {
return this.mask;
}
public static NonGender fromMask(int mask) {
return switch (mask & 3) {
case 1 -> MALE;
case 2 -> FEMALE;
case 3 -> BOTH;
default -> NONE;
};
}
}
@@ -0,0 +1,7 @@
package com.nonid.api;
public enum NonLiquidComposition {
EMPTY,
MIXED,
ENTITY;
}
@@ -0,0 +1,10 @@
package com.nonid.api;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
public record NonLiquidTankSnapshot(int storedMl, int capacityMl, NonLiquidComposition composition, @Nullable Identifier entityTypeId, int tintRgb) {
public float fillRatio() {
return this.capacityMl <= 0 ? 0.0F : Math.max(0.0F, Math.min(1.0F, (float)this.storedMl / this.capacityMl));
}
}
@@ -0,0 +1,7 @@
package com.nonid.api;
public record NonMessSnapshot(int v, int a, int m) {
public int total() {
return Math.max(0, this.v) + Math.max(0, this.a) + Math.max(0, this.m);
}
}
@@ -0,0 +1,7 @@
package com.nonid.api;
public enum NonMessType {
V,
A,
M;
}
@@ -0,0 +1,100 @@
package com.nonid.api;
import com.nonid.NonApiInternals;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
public final class NonPlayerApi {
private NonPlayerApi() {
}
public static NonPlayerStateSnapshot getState(PlayerEntity player) {
return NonApiInternals.snapshotPlayerState(player);
}
public static NonGender getGender(PlayerEntity player) {
return NonApiInternals.getGender(player);
}
public static boolean setGender(ServerPlayerEntity player, NonGender gender, NonChangeSource source) {
return NonApiInternals.setGender(player, gender, source);
}
public static boolean canChangeGender(ServerPlayerEntity player, NonChangeSource source) {
return NonApiInternals.canChangeGender(player, source);
}
public static NonLiquidTankSnapshot getLiquidTank(PlayerEntity player) {
return NonApiInternals.snapshotLiquid(player);
}
public static int addLiquid(ServerPlayerEntity player, @Nullable Identifier sourceEntityType, int amountMl) {
return NonApiInternals.addLiquid(player, sourceEntityType, amountMl);
}
public static int drainLiquid(ServerPlayerEntity player, int amountMl) {
return NonApiInternals.drainLiquid(player, amountMl);
}
public static NonMessSnapshot getMess(PlayerEntity player) {
return NonApiInternals.snapshotMess(player);
}
public static boolean addMess(ServerPlayerEntity player, NonMessType type, int amount) {
return NonApiInternals.addMess(player, type, amount);
}
public static boolean cleanMess(ServerPlayerEntity player, NonMessType type, int amount) {
return NonApiInternals.cleanMess(player, type, amount);
}
public static boolean clearMess(ServerPlayerEntity player) {
return NonApiInternals.clearMess(player);
}
public static NonDestroyedSkinSnapshot getDestroyedSkin(PlayerEntity player) {
return NonApiInternals.snapshotDestroyedSkin(player);
}
public static boolean damageDestroyedSkin(ServerPlayerEntity player, int amount) {
return NonApiInternals.damageDestroyedSkin(player, amount);
}
public static boolean repairDestroyedSkin(ServerPlayerEntity player, int amount) {
return NonApiInternals.repairDestroyedSkin(player, amount);
}
public static NonPregnancySnapshot getPregnancy(PlayerEntity player) {
return NonApiInternals.snapshotPregnancy(player);
}
public static boolean beginPregnancy(ServerPlayerEntity player, Identifier entityTypeId, long endTick) {
return NonApiInternals.beginPregnancy(player, entityTypeId, endTick);
}
public static boolean clearPregnancy(ServerPlayerEntity player) {
return NonApiInternals.clearPregnancy(player);
}
public static NonAccessoryEffectsSnapshot getAccessoryEffects(PlayerEntity player) {
return NonApiInternals.snapshotAccessoryEffects(player);
}
public static boolean isMessSystemEnabled() {
return NonApiInternals.isMessSystemEnabled();
}
public static boolean isDestroyedSkinSystemEnabled() {
return NonApiInternals.isDestroyedSkinSystemEnabled();
}
public static boolean isLiquidTankEnabled() {
return NonApiInternals.isLiquidTankEnabled();
}
public static boolean isPregnancyEnabled() {
return NonApiInternals.isPregnancyEnabled();
}
}
@@ -0,0 +1,155 @@
package com.nonid.api;
import com.nonid.NeedsOfNature;
import com.nonid.NonPlayerInteractionProfiles;
import com.nonid.NonPlayerInteractionSystem;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import org.jetbrains.annotations.Nullable;
public final class NonPlayerInteractionApi {
private NonPlayerInteractionApi() {
}
@Nullable
public static NonPlayerInteractionApi.Profile getProfile(ServerPlayerEntity owner) {
ServerWorld world = serverWorld(owner);
if (world == null) {
return null;
}
NonPlayerInteractionProfiles.Snapshot snapshot = NonPlayerInteractionProfiles.get(world).snapshot(owner.getUuid(), allowedMask());
return new NonPlayerInteractionApi.Profile(
toApi(snapshot.defaultMode()),
snapshot.overrides().stream().map(entry -> new NonPlayerInteractionApi.Override(entry.uuid(), entry.name(), toApi(entry.mode()))).toList(),
snapshot.knownPlayers().stream().map(entry -> new NonPlayerInteractionApi.KnownPlayer(entry.uuid(), entry.name())).toList()
);
}
@Nullable
public static NonPlayerInteractionApi.Mode getResolvedMode(ServerPlayerEntity owner, UUID otherPlayer) {
ServerWorld world = serverWorld(owner);
return world != null && otherPlayer != null
? toApi(NonPlayerInteractionProfiles.get(world).resolveMode(owner.getUuid(), otherPlayer, allowedMask()))
: null;
}
public static boolean canRequest(ServerPlayerEntity requester, ServerPlayerEntity target) {
NonPlayerInteractionApi.Mode mode = target != null && requester != null ? getResolvedMode(target, requester.getUuid()) : null;
return mode != null && mode.acceptsRequests();
}
public static boolean canAttack(ServerPlayerEntity attacker, ServerPlayerEntity target) {
NonPlayerInteractionApi.Mode mode = target != null && attacker != null ? getResolvedMode(target, attacker.getUuid()) : null;
return mode != null && mode.acceptsAttacks();
}
public static boolean setDefaultMode(ServerPlayerEntity owner, NonPlayerInteractionApi.Mode mode) {
ServerWorld world = serverWorld(owner);
if (world != null && mode != null) {
NonPlayerInteractionProfiles.get(world).setDefaultMode(owner.getUuid(), toInternal(mode), allowedMask());
sync(owner);
return true;
} else {
return false;
}
}
public static boolean setOverride(ServerPlayerEntity owner, ServerPlayerEntity otherPlayer, NonPlayerInteractionApi.Mode mode) {
return otherPlayer == null ? false : setOverride(owner, otherPlayer.getUuid(), otherPlayer.getGameProfile().name(), mode);
}
public static boolean setOverride(ServerPlayerEntity owner, UUID otherPlayer, String otherPlayerName, NonPlayerInteractionApi.Mode mode) {
ServerWorld world = serverWorld(owner);
if (world != null && otherPlayer != null && mode != null && !otherPlayer.equals(owner.getUuid())) {
NonPlayerInteractionProfiles.get(world).setOverride(owner.getUuid(), otherPlayer, otherPlayerName, toInternal(mode), allowedMask());
sync(owner);
return true;
} else {
return false;
}
}
public static boolean removeOverride(ServerPlayerEntity owner, UUID otherPlayer) {
ServerWorld world = serverWorld(owner);
if (world != null && otherPlayer != null) {
NonPlayerInteractionProfiles.get(world).removeOverride(owner.getUuid(), otherPlayer);
sync(owner);
return true;
} else {
return false;
}
}
@Nullable
private static ServerWorld serverWorld(@Nullable ServerPlayerEntity player) {
return player != null && player.getEntityWorld() instanceof ServerWorld world ? world : null;
}
private static int allowedMask() {
return NeedsOfNature.getConfig().getAllowedPlayerInteractionProfilesMask();
}
private static void sync(ServerPlayerEntity owner) {
NonPlayerInteractionSystem.sendProfileSnapshot(owner, "");
}
private static NonPlayerInteractionApi.Mode toApi(NonPlayerInteractionProfiles.Mode mode) {
return switch (mode) {
case DENY_ALL -> NonPlayerInteractionApi.Mode.DENY_ALL;
case REQUESTS_ONLY -> NonPlayerInteractionApi.Mode.REQUESTS_ONLY;
case REQUESTS_AND_ATTACKS -> NonPlayerInteractionApi.Mode.REQUESTS_AND_ATTACKS;
case AUTO_ACCEPT -> NonPlayerInteractionApi.Mode.AUTO_ACCEPT;
};
}
private static NonPlayerInteractionProfiles.Mode toInternal(NonPlayerInteractionApi.Mode mode) {
return switch (mode) {
case DENY_ALL -> NonPlayerInteractionProfiles.Mode.DENY_ALL;
case REQUESTS_ONLY -> NonPlayerInteractionProfiles.Mode.REQUESTS_ONLY;
case REQUESTS_AND_ATTACKS -> NonPlayerInteractionProfiles.Mode.REQUESTS_AND_ATTACKS;
case AUTO_ACCEPT -> NonPlayerInteractionProfiles.Mode.AUTO_ACCEPT;
};
}
public record KnownPlayer(UUID playerUuid, String playerName) {
}
public enum Mode {
DENY_ALL,
REQUESTS_ONLY,
REQUESTS_AND_ATTACKS,
AUTO_ACCEPT;
public String id() {
return this.name().toLowerCase(Locale.ROOT);
}
public boolean acceptsRequests() {
return this != DENY_ALL;
}
public boolean automaticallyAcceptsRequests() {
return this == AUTO_ACCEPT;
}
public boolean acceptsAttacks() {
return this == REQUESTS_AND_ATTACKS || this == AUTO_ACCEPT;
}
}
public record Override(UUID playerUuid, String playerName, NonPlayerInteractionApi.Mode mode) {
}
public record Profile(
NonPlayerInteractionApi.Mode defaultMode, List<NonPlayerInteractionApi.Override> overrides, List<NonPlayerInteractionApi.KnownPlayer> knownPlayers
) {
public Profile {
overrides = List.copyOf(overrides);
knownPlayers = List.copyOf(knownPlayers);
}
}
}
@@ -0,0 +1,11 @@
package com.nonid.api;
public record NonPlayerStateSnapshot(
NonGender gender,
NonLiquidTankSnapshot liquidTank,
NonMessSnapshot mess,
NonDestroyedSkinSnapshot destroyedSkin,
NonPregnancySnapshot pregnancy,
NonAccessoryEffectsSnapshot accessoryEffects
) {
}
@@ -0,0 +1,10 @@
package com.nonid.api;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
public record NonPregnancySnapshot(@Nullable Identifier entityTypeId, long endTick, boolean pendingHatch) {
public boolean active() {
return this.entityTypeId != null;
}
}
@@ -0,0 +1,55 @@
package com.nonid.api.animation;
import java.util.LinkedHashSet;
import java.util.Set;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.minecraft.entity.Entity;
import org.jetbrains.annotations.NotNull;
public final class NonActorEvents {
public static final Event<NonActorEvents.@NotNull Provider> PROVIDE = EventFactory.createArrayBacked(NonActorEvents.Provider.class, callbacks -> entity -> {
if (callbacks.length != 0 && entity != null) {
LinkedHashSet<String> tags = new LinkedHashSet<>();
for (NonActorEvents.Provider callback : callbacks) {
Set<String> provided = callback.provide(entity);
if (provided != null && !provided.isEmpty()) {
for (String tag : provided) {
if (tag != null && !tag.isBlank()) {
tags.add(tag);
}
}
}
}
return tags.isEmpty() ? Set.of() : Set.copyOf(tags);
} else {
return Set.of();
}
});
private NonActorEvents() {
}
public static Set<String> resolve(Entity entity) {
if (entity == null) {
return Set.of();
}
Set<String> commandTags = entity.getCommandTags();
Set<String> provided = ((NonActorEvents.Provider)PROVIDE.invoker()).provide(entity);
if (provided.isEmpty()) {
return commandTags;
}
LinkedHashSet<String> tags = new LinkedHashSet<>(commandTags);
tags.addAll(provided);
return Set.copyOf(tags);
}
@FunctionalInterface
public interface Provider {
Set<String> provide(Entity var1);
}
}
@@ -0,0 +1,31 @@
package com.nonid.api.animation;
import java.util.Set;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
public record NonAnimationActor(
Set<Identifier> entityTypes,
@Nullable String entityVariant,
Set<String> requiredTags,
Set<String> requiredAnyTags,
String label,
boolean baby,
NonAnimationActor.Activity activity,
@Nullable Identifier leftPropItemId,
@Nullable Identifier rightPropItemId,
@Nullable Identifier floorPropItemId
) {
public NonAnimationActor {
entityTypes = entityTypes == null ? Set.of() : Set.copyOf(entityTypes);
requiredTags = requiredTags == null ? Set.of() : Set.copyOf(requiredTags);
requiredAnyTags = requiredAnyTags == null ? Set.of() : Set.copyOf(requiredAnyTags);
label = label == null ? "" : label;
activity = activity == null ? NonAnimationActor.Activity.ACTIVE : activity;
}
public enum Activity {
ACTIVE,
PASSIVE;
}
}
@@ -0,0 +1,358 @@
package com.nonid.api.animation;
import com.nonid.internal.animation.api.AnimationService;
import com.nonid.internal.animation.api.NonAnimationViews;
import com.nonid.internal.animation.data.AnimationDefinitions;
import com.nonid.internal.animation.server.ServerAnimationController;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.LivingEntity;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
public final class NonAnimationApi {
private NonAnimationApi() {
}
public static List<NonAnimationDefinition> getLoadedDefinitions() {
return AnimationDefinitions.getLoadedDefinitionsSnapshot().stream().map(NonAnimationApi::definitionView).toList();
}
@Nullable
public static NonAnimationDefinition getDefinition(Identifier animationId) {
return definitionView(AnimationDefinitions.getDefinition(animationId));
}
public static boolean isEnabled(Identifier animationId) {
return AnimationDefinitions.isAnimationEnabled(animationId);
}
public static boolean hasAnimationForEntityType(@Nullable EntityType<?> type, boolean baby) {
return AnimationService.hasEnabledGenericAnimationForEntityType(type, baby);
}
public static List<NonAnimationCandidate> findCandidates(ServerWorld world, List<? extends Entity> actors, @Nullable NonAnimationApi.MatchOptions options) {
if (world != null && actors != null && !actors.isEmpty()) {
NonAnimationApi.MatchOptions safe = options == null ? NonAnimationApi.MatchOptions.defaults() : options;
AnimationService.MatchOptions internalOptions = internalOptions(safe);
List<NonAnimationCandidate> result = new ArrayList<>();
for (AnimationService.EligibleAnimation candidate : AnimationService.findEligibleAnimationsForActors(world, actors, internalOptions)) {
AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(candidate.animationId());
if (definition != null) {
boolean eligible = AnimationService.isAnimationStartEligible(world, definition, actors, candidate.actorKeys(), safe.positionAnchor());
result.add(new NonAnimationCandidate(definitionView(definition), candidate.actorKeys(), eligible));
}
}
result.sort(Comparator.comparing(candidatex -> candidatex.definition().id().toString()));
return List.copyOf(result);
} else {
return List.of();
}
}
@Nullable
public static NonAnimationCandidate findMatch(ServerWorld world, List<? extends Entity> actors, @Nullable NonAnimationApi.MatchOptions options) {
if (world != null && actors != null && !actors.isEmpty()) {
NonAnimationApi.MatchOptions safe = options == null ? NonAnimationApi.MatchOptions.defaults() : options;
AnimationService.MatchedAnimation match = AnimationService.findAnimationForActors(world, actors, internalOptions(safe));
if (match == null) {
return null;
}
AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(match.animationId());
if (definition == null) {
return null;
}
boolean eligible = AnimationService.isAnimationStartEligible(world, definition, actors, match.actorKeys(), safe.positionAnchor());
return new NonAnimationCandidate(definitionView(definition), match.actorKeys(), eligible);
} else {
return null;
}
}
public static boolean canStart(
ServerWorld world, Identifier animationId, List<? extends Entity> actors, @Nullable List<String> actorKeys, @Nullable Entity positionAnchor
) {
AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(animationId);
if (definition == null) {
return false;
} else {
return actorKeys == null
? AnimationService.isAnimationStartEligible(world, definition, actors, positionAnchor)
: AnimationService.isAnimationStartEligible(world, definition, actors, actorKeys, positionAnchor);
}
}
@Nullable
public static UUID start(
ServerWorld world,
Identifier animationId,
List<? extends Entity> actors,
@Nullable List<String> actorKeys,
@Nullable NonAnimationApi.StartOptions options
) {
AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(animationId);
if (definition == null) {
return null;
}
NonAnimationApi.StartOptions safe = options == null ? NonAnimationApi.StartOptions.defaults() : options;
return AnimationService.startAnimationNow(
world,
animationId,
actors,
actorKeys,
definition.stages(),
new AnimationService.StartOptions(
safe.damageBehavior(),
safe.ignoreAttackers(),
safe.positionAnchor(),
safe.requester(),
safe.metadata(),
safe.forceDebugChat(),
safe.allowMultiplePlayers()
)
);
}
@Nullable
public static UUID startMatched(
ServerWorld world,
List<? extends Entity> actors,
@Nullable NonAnimationApi.MatchOptions matchOptions,
@Nullable NonAnimationApi.StartOptions startOptions
) {
NonAnimationCandidate candidate = findMatch(world, actors, matchOptions);
return candidate != null && candidate.startEligible() ? start(world, candidate.definition().id(), actors, candidate.actorKeys(), startOptions) : null;
}
@Nullable
public static NonAnimationSession getSession(ServerWorld world, UUID instanceId) {
if (world != null && instanceId != null) {
ServerAnimationController.ActiveInstanceSnapshot snapshot = ServerAnimationController.getActiveInstanceSnapshot(world, instanceId);
return snapshot == null ? null : NonAnimationViews.session(snapshot);
} else {
return null;
}
}
@Nullable
public static NonAnimationStage getCurrentStage(ServerWorld world, UUID instanceId) {
return NonAnimationViews.stage(AnimationService.getCurrentStage(world, instanceId));
}
public static boolean advanceStage(ServerWorld world, UUID instanceId) {
return AnimationService.advanceStage(world, instanceId);
}
public static boolean advanceToStage(ServerWorld world, UUID instanceId, int stageNumber) {
return AnimationService.advanceToStage(world, instanceId, stageNumber);
}
public static void multiplySpeed(ServerWorld world, UUID instanceId, double multiplier) {
AnimationService.adjustSpeedSilently(world, instanceId, multiplier);
}
public static boolean stop(ServerWorld world, UUID instanceId) {
return AnimationService.stopAnimation(world, instanceId);
}
public static void stopAll(ServerWorld world) {
AnimationService.stopAll(world);
}
public static boolean enqueue(
ServerWorld world, Identifier animationId, List<? extends Entity> actors, int insertIndex, @Nullable NonAnimationApi.StartOptions options
) {
NonAnimationApi.StartOptions safe = options == null ? NonAnimationApi.StartOptions.defaults() : options;
return AnimationService.enqueueAnimation(
world, animationId, actors, insertIndex, safe.damageBehavior(), safe.ignoreAttackers(), safe.metadata(), safe.requester()
);
}
public static void clearQueue(ServerWorld world, ServerPlayerEntity player) {
AnimationService.clearQueuedAnimations(world, player);
}
public static boolean isActorPendingOrActive(ServerWorld world, UUID actorUuid) {
return AnimationService.isActorPendingOrActive(world, actorUuid);
}
public static boolean shouldIgnoreAttackers(ServerWorld world, LivingEntity actor) {
return AnimationService.shouldIgnoreAttackers(world, actor);
}
public static void setStartProtection(ServerPlayerEntity player, int ticks) {
AnimationService.setAnimationStartProtection(player, ticks);
}
public static void clearStartProtection(ServerPlayerEntity player) {
AnimationService.clearAnimationStartProtection(player);
}
public static boolean hasStartProtection(ServerWorld world, ServerPlayerEntity player) {
return AnimationService.hasAnimationStartProtection(world, player);
}
private static AnimationService.MatchOptions internalOptions(NonAnimationApi.MatchOptions options) {
AnimationService.AnimationCandidateFilter filter = options.candidateFilter() == null
? null
: (world, definition, actors) -> options.candidateFilter().test(world, definitionView(definition), actors);
return new AnimationService.MatchOptions(
options.requiredAnimationTags(),
options.positionAnchor(),
options.requireStartEligibility(),
options.requiredActiveActor(),
options.excludedAnimationIds(),
filter,
options.joinPredecessorAnimationId()
);
}
@Nullable
private static NonAnimationDefinition definitionView(@Nullable AnimationDefinitions.Definition definition) {
return definition == null
? null
: new NonAnimationDefinition(
definition.id(),
definition.displayName(),
definition.actors().stream().map(NonAnimationApi::actorView).toList(),
definition.requiredUnionTags(),
definition.animationTags(),
definition.contentTags(),
NonAnimationViews.stages(definition.stages()),
definition.weight(),
definition.attackEligible(),
blockView(definition.blockRequirements()),
definition.waterRequirement().name().toLowerCase(Locale.ROOT),
definition.positionAnchorActor(),
packView(definition.packInfo()),
definition.joinAfter(),
AnimationDefinitions.isDefinitionEnabled(definition)
);
}
private static NonAnimationActor actorView(AnimationDefinitions.ActorConstraint actor) {
return new NonAnimationActor(
actor.entityTypes(),
actor.entityVariant(),
actor.requiredTags(),
actor.requiredTagsAny(),
actor.label(),
actor.ageRequirement() == AnimationDefinitions.AgeRequirement.BABY,
actor.activity() == AnimationDefinitions.ActorActivity.PASSIVE ? NonAnimationActor.Activity.PASSIVE : NonAnimationActor.Activity.ACTIVE,
actor.propLeftItemId(),
actor.propRightItemId(),
actor.propFloorItemId()
);
}
@Nullable
private static NonAnimationPack packView(@Nullable AnimationDefinitions.AnimationPackInfo pack) {
return pack == null
? null
: new NonAnimationPack(
pack.id(),
pack.resourcePackId(),
pack.name(),
pack.author(),
pack.version(),
pack.description(),
pack.localModelRoot(),
pack.modelImports()
.stream()
.map(modelImport -> new NonAnimationPack.ModelImport(modelImport.packId(), modelImport.modelRoot(), modelImport.entityTypes()))
.toList()
);
}
@Nullable
private static NonAnimationBlockRequirement blockView(@Nullable AnimationDefinitions.BlockRequirements requirement) {
if (requirement == null) {
return null;
}
AnimationDefinitions.BlockPredicate blocks = requirement.blocks();
return new NonAnimationBlockRequirement(
requirement.type(),
requirement.height() == null ? null : new NonAnimationBlockRequirement.WallHeight(requirement.height().min(), requirement.height().max()),
requirement.width() == null ? null : new NonAnimationBlockRequirement.WallWidth(requirement.width().min(), requirement.width().max()),
requirement.facing() == null ? null : requirement.facing().id(),
requirement.clearance() == null
? null
: new NonAnimationBlockRequirement.Dimensions(requirement.clearance().width(), requirement.clearance().height(), requirement.clearance().depth()),
requirement.support() == null ? null : requirement.support().id(),
requirement.placement() == null ? null : requirement.placement().id(),
requirement.surfaceRadius(),
requirement.surfaceFootprint() == null
? null
: new NonAnimationBlockRequirement.Footprint(
requirement.surfaceFootprint().width(),
requirement.surfaceFootprint().depth(),
requirement.surfaceFootprint().height(),
requirement.surfaceFootprint().margin()
),
blocks == null ? Set.of() : blocks.blockIds(),
blocks == null ? Set.of() : blocks.tagIds()
);
}
@FunctionalInterface
public interface CandidateFilter {
boolean test(ServerWorld var1, NonAnimationDefinition var2, List<Entity> var3);
}
public record MatchOptions(
Set<String> requiredAnimationTags,
@Nullable Entity positionAnchor,
boolean requireStartEligibility,
@Nullable Entity requiredActiveActor,
Set<Identifier> excludedAnimationIds,
@Nullable NonAnimationApi.CandidateFilter candidateFilter,
@Nullable Identifier joinPredecessorAnimationId
) {
public MatchOptions {
requiredAnimationTags = requiredAnimationTags == null ? Set.of() : Set.copyOf(requiredAnimationTags);
excludedAnimationIds = excludedAnimationIds == null ? Set.of() : Set.copyOf(excludedAnimationIds);
}
public static NonAnimationApi.MatchOptions defaults() {
return new NonAnimationApi.MatchOptions(Set.of(), null, false, null, Set.of(), null, null);
}
public static NonAnimationApi.MatchOptions startEligible(@Nullable Entity positionAnchor) {
return new NonAnimationApi.MatchOptions(Set.of(), positionAnchor, true, null, Set.of(), null, null);
}
}
public record StartOptions(
NonAnimationDamageBehavior damageBehavior,
boolean ignoreAttackers,
@Nullable Entity positionAnchor,
@Nullable ServerPlayerEntity requester,
Map<String, String> metadata,
boolean forceDebugChat,
boolean allowMultiplePlayers
) {
public StartOptions {
damageBehavior = damageBehavior == null ? NonAnimationDamageBehavior.IGNORE_DAMAGE : damageBehavior;
metadata = metadata == null ? Map.of() : Map.copyOf(metadata);
}
public static NonAnimationApi.StartOptions defaults() {
return new NonAnimationApi.StartOptions(NonAnimationDamageBehavior.IGNORE_DAMAGE, false, null, null, Map.of(), false, false);
}
}
}
@@ -0,0 +1,36 @@
package com.nonid.api.animation;
import java.util.Set;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
public record NonAnimationBlockRequirement(
String type,
@Nullable NonAnimationBlockRequirement.WallHeight wallHeight,
@Nullable NonAnimationBlockRequirement.WallWidth wallWidth,
@Nullable String facing,
@Nullable NonAnimationBlockRequirement.Dimensions clearance,
@Nullable String support,
@Nullable String placement,
int surfaceRadius,
@Nullable NonAnimationBlockRequirement.Footprint surfaceFootprint,
Set<Identifier> blockIds,
Set<Identifier> blockTagIds
) {
public NonAnimationBlockRequirement {
blockIds = blockIds == null ? Set.of() : Set.copyOf(blockIds);
blockTagIds = blockTagIds == null ? Set.of() : Set.copyOf(blockTagIds);
}
public record Dimensions(int width, int height, int depth) {
}
public record Footprint(int width, int depth, int height, int margin) {
}
public record WallHeight(int minimum, @Nullable Integer maximum) {
}
public record WallWidth(int minimum, @Nullable Integer maximum) {
}
}
@@ -0,0 +1,9 @@
package com.nonid.api.animation;
import java.util.List;
public record NonAnimationCandidate(NonAnimationDefinition definition, List<String> actorKeys, boolean startEligible) {
public NonAnimationCandidate {
actorKeys = actorKeys == null ? List.of() : List.copyOf(actorKeys);
}
}
@@ -0,0 +1,28 @@
package com.nonid.api.animation;
import java.util.Locale;
public enum NonAnimationDamageBehavior {
STOP_ON_DAMAGE,
IGNORE_DAMAGE,
BLOCK_DAMAGE;
public String id() {
return this.name().toLowerCase(Locale.ROOT);
}
public static NonAnimationDamageBehavior fromId(String id, NonAnimationDamageBehavior fallback) {
if (id != null && !id.isBlank()) {
String norm = id.trim().toLowerCase(Locale.ROOT);
return switch (norm) {
case "stop_on_damage" -> STOP_ON_DAMAGE;
case "ignore_damage" -> IGNORE_DAMAGE;
case "block_damage" -> BLOCK_DAMAGE;
default -> fallback;
};
} else {
return fallback;
}
}
}
@@ -0,0 +1,34 @@
package com.nonid.api.animation;
import java.util.List;
import java.util.Set;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
public record NonAnimationDefinition(
Identifier id,
@Nullable String displayName,
List<NonAnimationActor> actors,
Set<String> requiredUnionTags,
Set<String> animationTags,
List<String> contentTags,
List<NonAnimationStage> stages,
double weight,
boolean attackEligible,
@Nullable NonAnimationBlockRequirement blockRequirement,
String waterRequirement,
@Nullable String positionAnchorActor,
@Nullable NonAnimationPack pack,
Set<Identifier> joinAfter,
boolean enabled
) {
public NonAnimationDefinition {
actors = actors == null ? List.of() : List.copyOf(actors);
requiredUnionTags = requiredUnionTags == null ? Set.of() : Set.copyOf(requiredUnionTags);
animationTags = animationTags == null ? Set.of() : Set.copyOf(animationTags);
contentTags = contentTags == null ? List.of() : List.copyOf(contentTags);
stages = stages == null ? List.of() : List.copyOf(stages);
waterRequirement = waterRequirement == null ? "none" : waterRequirement;
joinAfter = joinAfter == null ? Set.of() : Set.copyOf(joinAfter);
}
}
@@ -0,0 +1,117 @@
package com.nonid.api.animation;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public final class NonAnimationEvents {
public static final Event<NonAnimationEvents.@NotNull BeforeStart> BEFORE_START = EventFactory.createArrayBacked(
NonAnimationEvents.BeforeStart.class, callbacks -> context -> {
for (NonAnimationEvents.BeforeStart callback : callbacks) {
if (!callback.allow(context)) {
return false;
}
}
return true;
}
);
public static final Event<NonAnimationEvents.@NotNull Started> STARTED = EventFactory.createArrayBacked(
NonAnimationEvents.Started.class, callbacks -> context -> {
for (NonAnimationEvents.Started callback : callbacks) {
callback.onStarted(context);
}
}
);
public static final Event<NonAnimationEvents.@NotNull Stopped> STOPPED = EventFactory.createArrayBacked(
NonAnimationEvents.Stopped.class, callbacks -> context -> {
for (NonAnimationEvents.Stopped callback : callbacks) {
callback.onStopped(context);
}
}
);
public static final Event<NonAnimationEvents.@NotNull StageChanged> STAGE_CHANGED = EventFactory.createArrayBacked(
NonAnimationEvents.StageChanged.class, callbacks -> context -> {
for (NonAnimationEvents.StageChanged callback : callbacks) {
callback.onStageChanged(context);
}
}
);
private NonAnimationEvents() {
}
@FunctionalInterface
public interface BeforeStart {
boolean allow(NonAnimationEvents.BeforeStartContext var1);
}
public record BeforeStartContext(
ServerWorld world,
Identifier animationId,
List<UUID> actorUuids,
List<String> actorKeys,
List<NonAnimationStage> stages,
@Nullable ServerPlayerEntity requester,
NonAnimationDamageBehavior damageBehavior,
boolean ignoreAttackers,
Map<String, String> metadata
) {
public BeforeStartContext {
actorUuids = List.copyOf(actorUuids);
actorKeys = List.copyOf(actorKeys);
stages = List.copyOf(stages);
metadata = Map.copyOf(metadata);
}
}
@FunctionalInterface
public interface StageChanged {
void onStageChanged(NonAnimationEvents.StageChangedContext var1);
}
public record StageChangedContext(
ServerWorld world,
UUID instanceId,
Identifier animationId,
List<UUID> actorUuids,
List<String> actorKeys,
long advanceTick,
@Nullable NonAnimationStage currentStage
) {
public StageChangedContext {
actorUuids = List.copyOf(actorUuids);
actorKeys = List.copyOf(actorKeys);
}
}
@FunctionalInterface
public interface Started {
void onStarted(NonAnimationEvents.StartedContext var1);
}
public record StartedContext(ServerWorld world, NonAnimationSession session, @Nullable ServerPlayerEntity requester) {
}
@FunctionalInterface
public interface Stopped {
void onStopped(NonAnimationEvents.StoppedContext var1);
}
public record StoppedContext(
ServerWorld world, UUID instanceId, Identifier animationId, List<UUID> actorUuids, List<String> actorKeys, List<NonAnimationStage> stages
) {
public StoppedContext {
actorUuids = List.copyOf(actorUuids);
actorKeys = List.copyOf(actorKeys);
stages = List.copyOf(stages);
}
}
}
@@ -0,0 +1,33 @@
package com.nonid.api.animation;
import java.util.List;
import java.util.Set;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
public record NonAnimationPack(
String id,
String resourcePackId,
String name,
String author,
@Nullable String version,
@Nullable String description,
@Nullable Identifier localModelRoot,
List<NonAnimationPack.ModelImport> modelImports
) {
public NonAnimationPack {
modelImports = modelImports == null ? List.of() : List.copyOf(modelImports);
}
public NonAnimationPack(
String id, String resourcePackId, String name, String author, @Nullable String version, @Nullable String description, @Nullable Identifier localModelRoot
) {
this(id, resourcePackId, name, author, version, description, localModelRoot, List.of());
}
public record ModelImport(String packId, Identifier modelRoot, Set<Identifier> entityTypes) {
public ModelImport {
entityTypes = entityTypes == null ? Set.of() : Set.copyOf(entityTypes);
}
}
}
@@ -0,0 +1,22 @@
package com.nonid.api.animation;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
public record NonAnimationSession(
UUID instanceId,
Identifier animationId,
List<UUID> actorUuids,
@Nullable NonAnimationStage currentStage,
NonAnimationDamageBehavior damageBehavior,
boolean ignoreAttackers,
Map<String, String> metadata
) {
public NonAnimationSession {
actorUuids = actorUuids == null ? List.of() : List.copyOf(actorUuids);
metadata = metadata == null ? Map.of() : Map.copyOf(metadata);
}
}
@@ -0,0 +1,24 @@
package com.nonid.api.animation;
import java.util.List;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
public record NonAnimationStage(
Identifier animationId,
boolean looping,
long lengthTicks,
boolean joinsAllowed,
double speed,
Identifier playbackAnimationId,
List<NonAnimationStage.ActorProp> actorProps,
double cycleMidpointOffsetSeconds,
double cycleTicks
) {
public NonAnimationStage {
actorProps = actorProps == null ? List.of() : List.copyOf(actorProps);
}
public record ActorProp(String actorKey, @Nullable Identifier leftItemId, @Nullable Identifier rightItemId, @Nullable Identifier floorItemId) {
}
}
@@ -0,0 +1,59 @@
package com.nonid.api.client;
import java.util.UUID;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.minecraft.entity.Entity;
import net.minecraft.particle.ParticleEffect;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.Vec3d;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public final class NonAnimationParticleEvents {
public static final Event<NonAnimationParticleEvents.@NotNull ParticleResolver> RESOLVE = EventFactory.createArrayBacked(
NonAnimationParticleEvents.ParticleResolver.class, callbacks -> context -> {
for (NonAnimationParticleEvents.ParticleResolver callback : callbacks) {
NonAnimationParticleEvents.ParticleOverride override = callback.resolve(context);
if (override != null) {
return override;
}
}
return null;
}
);
private NonAnimationParticleEvents() {
}
public record ParticleContext(
@Nullable Entity actor,
UUID actorUuid,
@Nullable Identifier animationId,
@Nullable Identifier animationResource,
@Nullable String animationKey,
Identifier particleId,
String locator,
String script,
Vec3d position,
@Nullable UUID instanceId
) {
}
public record ParticleOverride(@Nullable ParticleEffect particle, boolean cancel) {
public static NonAnimationParticleEvents.ParticleOverride spawn(ParticleEffect particle) {
return new NonAnimationParticleEvents.ParticleOverride(particle, false);
}
public static NonAnimationParticleEvents.ParticleOverride skip() {
return new NonAnimationParticleEvents.ParticleOverride(null, true);
}
}
@FunctionalInterface
public interface ParticleResolver {
@Nullable
NonAnimationParticleEvents.ParticleOverride resolve(NonAnimationParticleEvents.ParticleContext var1);
}
}
@@ -0,0 +1,106 @@
package com.nonid.api.client;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.minecraft.entity.LivingEntity;
import net.minecraft.item.ItemDisplayContext;
import net.minecraft.item.ItemStack;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public final class NonAnimationRenderEvents {
public static final Event<NonAnimationRenderEvents.@NotNull ModelResolver> RESOLVE = EventFactory.createArrayBacked(
NonAnimationRenderEvents.ModelResolver.class, callbacks -> (entity, entityTypeId, currentModel, currentTexture) -> {
for (NonAnimationRenderEvents.ModelResolver callback : callbacks) {
NonAnimationRenderEvents.ModelOverride resolved = callback.resolve(entity, entityTypeId, currentModel, currentTexture);
if (resolved != null) {
return resolved;
}
}
return null;
}
);
public static final Event<NonAnimationRenderEvents.@NotNull RenderResolver> RESOLVE_RENDER = EventFactory.createArrayBacked(
NonAnimationRenderEvents.RenderResolver.class, callbacks -> (entity, entityTypeId, currentModel, currentTexture) -> {
for (NonAnimationRenderEvents.RenderResolver callback : callbacks) {
NonAnimationRenderEvents.RenderOverride resolved = callback.resolve(entity, entityTypeId, currentModel, currentTexture);
if (resolved != null) {
return resolved;
}
}
return null;
}
);
private NonAnimationRenderEvents() {
}
public record BoneItemProp(ItemStack stack, ItemDisplayContext displayContext) {
public BoneItemProp(ItemStack stack) {
this(stack, ItemDisplayContext.THIRD_PERSON_RIGHT_HAND);
}
public BoneItemProp {
stack = stack == null ? ItemStack.EMPTY : stack.copy();
displayContext = displayContext == null ? ItemDisplayContext.THIRD_PERSON_RIGHT_HAND : displayContext;
}
}
public record ModelOverride(@Nullable Identifier model, @Nullable Identifier texture) {
}
@FunctionalInterface
public interface ModelResolver {
@Nullable
NonAnimationRenderEvents.ModelOverride resolve(LivingEntity var1, @Nullable Identifier var2, Identifier var3, Identifier var4);
}
public record RenderOverride(
@Nullable Identifier model,
@Nullable Identifier texture,
@Nullable List<Identifier> layerTextures,
@Nullable Map<String, Identifier> boneTextures,
@Nullable Map<String, NonAnimationRenderEvents.BoneItemProp> boneItems,
@Nullable Map<String, Boolean> boneVisibility,
@Nullable Map<String, Set<Integer>> hiddenBoneCubeIndices
) {
public RenderOverride(@Nullable Identifier model, @Nullable Identifier texture) {
this(model, texture, null, null, null, null, null);
}
public RenderOverride(@Nullable List<Identifier> layerTextures, @Nullable Map<String, Identifier> boneTextures) {
this(null, null, layerTextures, boneTextures, null, null, null);
}
public RenderOverride(
@Nullable List<Identifier> layerTextures,
@Nullable Map<String, Identifier> boneTextures,
@Nullable Map<String, NonAnimationRenderEvents.BoneItemProp> boneItems
) {
this(null, null, layerTextures, boneTextures, boneItems, null, null);
}
public RenderOverride(
@Nullable Identifier model,
@Nullable Identifier texture,
@Nullable List<Identifier> layerTextures,
@Nullable Map<String, Identifier> boneTextures,
@Nullable Map<String, NonAnimationRenderEvents.BoneItemProp> boneItems,
@Nullable Map<String, Boolean> boneVisibility
) {
this(model, texture, layerTextures, boneTextures, boneItems, boneVisibility, null);
}
}
@FunctionalInterface
public interface RenderResolver {
@Nullable
NonAnimationRenderEvents.RenderOverride resolve(LivingEntity var1, @Nullable Identifier var2, Identifier var3, Identifier var4);
}
}
@@ -0,0 +1,67 @@
package com.nonid.api.client;
import java.util.UUID;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.minecraft.entity.LivingEntity;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public final class NonAnimationSoundEvents {
public static final Event<NonAnimationSoundEvents.@NotNull SoundResolver> RESOLVE = EventFactory.createArrayBacked(
NonAnimationSoundEvents.SoundResolver.class, callbacks -> context -> {
for (NonAnimationSoundEvents.SoundResolver callback : callbacks) {
NonAnimationSoundEvents.SoundOverride override = callback.resolve(context);
if (override != null) {
return override;
}
}
return null;
}
);
private NonAnimationSoundEvents() {
}
public record SoundContext(
LivingEntity anchor,
Identifier animationId,
Identifier animationResource,
String animationKey,
String effect,
double timeSeconds,
float volume,
float pitch,
UUID instanceId,
int stageIndex,
boolean loop,
double cueTimeTicks,
double cycleTicks,
double animationSpeed,
double nextSameEffectCueDelayTicks,
double nextSameEffectCueDelayMs
) {
}
public record SoundOverride(@Nullable Identifier soundId, float volume, float pitch, boolean cancel) {
public static NonAnimationSoundEvents.SoundOverride play(Identifier soundId, float volume, float pitch) {
return new NonAnimationSoundEvents.SoundOverride(soundId, volume, pitch, false);
}
public static NonAnimationSoundEvents.SoundOverride play(Identifier soundId) {
return new NonAnimationSoundEvents.SoundOverride(soundId, 1.0F, 1.0F, false);
}
public static NonAnimationSoundEvents.SoundOverride skip() {
return new NonAnimationSoundEvents.SoundOverride(null, 1.0F, 1.0F, true);
}
}
@FunctionalInterface
public interface SoundResolver {
@Nullable
NonAnimationSoundEvents.SoundOverride resolve(NonAnimationSoundEvents.SoundContext var1);
}
}
@@ -0,0 +1,33 @@
package com.nonid.client;
import com.nonid.entity.HorseLiquidCollectorEntity;
import com.nonid.internal.animation.AnimationEngine;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.NotNull;
import software.bernie.geckolib.constant.dataticket.DataTicket;
import software.bernie.geckolib.model.GeoModel;
import software.bernie.geckolib.renderer.base.GeoRenderState;
public class HorseLiquidCollectorGeoModel extends GeoModel<HorseLiquidCollectorEntity> {
public static final DataTicket<@NotNull Boolean> COLLECTOR_FULL_TICKET = DataTicket.create("non_horse_liquid_collector_full", Boolean.class);
public static final Identifier MODEL_ID = Identifier.of("needsofnature", "entity/horse_liquid_collector");
public static final Identifier EMPTY_TEXTURE_ID = Identifier.of("needsofnature", "textures/entity/horse_liquid_collector/horse_liquid_collector_empty.png");
public static final Identifier FULL_TEXTURE_ID = Identifier.of("needsofnature", "textures/entity/horse_liquid_collector/horse_liquid_collector_full.png");
private static final Identifier PLACEHOLDER_ANIMATION_ID = AnimationEngine.packId("afw/placeholder");
@NotNull
public Identifier getModelResource(@NotNull GeoRenderState renderState) {
return MODEL_ID;
}
@NotNull
public Identifier getTextureResource(GeoRenderState renderState) {
boolean full = (Boolean)renderState.getOrDefaultGeckolibData(COLLECTOR_FULL_TICKET, Boolean.FALSE);
return full ? FULL_TEXTURE_ID : EMPTY_TEXTURE_ID;
}
@NotNull
public Identifier getAnimationResource(HorseLiquidCollectorEntity animatable) {
return PLACEHOLDER_ANIMATION_ID;
}
}
@@ -0,0 +1,27 @@
package com.nonid.client;
import com.nonid.internal.animation.AnimationEngine;
import com.nonid.item.HorseLiquidCollectorItem;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.NotNull;
import software.bernie.geckolib.model.GeoModel;
import software.bernie.geckolib.renderer.base.GeoRenderState;
public class HorseLiquidCollectorItemGeoModel extends GeoModel<HorseLiquidCollectorItem> {
private static final Identifier PLACEHOLDER_ANIMATION_ID = AnimationEngine.packId("afw/placeholder");
@NotNull
public Identifier getModelResource(@NotNull GeoRenderState renderState) {
return HorseLiquidCollectorGeoModel.MODEL_ID;
}
@NotNull
public Identifier getTextureResource(@NotNull GeoRenderState renderState) {
return HorseLiquidCollectorGeoModel.EMPTY_TEXTURE_ID;
}
@NotNull
public Identifier getAnimationResource(HorseLiquidCollectorItem animatable) {
return PLACEHOLDER_ANIMATION_ID;
}
}
@@ -0,0 +1,10 @@
package com.nonid.client;
import com.nonid.item.HorseLiquidCollectorItem;
import software.bernie.geckolib.renderer.GeoItemRenderer;
public class HorseLiquidCollectorItemRenderer extends GeoItemRenderer<HorseLiquidCollectorItem> {
public HorseLiquidCollectorItemRenderer() {
super(new HorseLiquidCollectorItemGeoModel());
}
}
@@ -0,0 +1,28 @@
package com.nonid.client;
import com.nonid.entity.HorseLiquidCollectorEntity;
import net.minecraft.client.render.OverlayTexture;
import net.minecraft.client.render.entity.EntityRendererFactory.Context;
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
import org.jetbrains.annotations.NotNull;
import software.bernie.geckolib.renderer.GeoEntityRenderer;
import software.bernie.geckolib.renderer.base.GeoRenderState;
public class HorseLiquidCollectorRenderer<R extends LivingEntityRenderState & GeoRenderState> extends GeoEntityRenderer<HorseLiquidCollectorEntity, R> {
public HorseLiquidCollectorRenderer(Context context) {
super(context, new HorseLiquidCollectorGeoModel());
}
public void addRenderData(HorseLiquidCollectorEntity animatable, Void relatedObject, R renderState, float partialTick) {
super.addRenderData(animatable, relatedObject, renderState, partialTick);
renderState.addGeckolibData(HorseLiquidCollectorGeoModel.COLLECTOR_FULL_TICKET, animatable.isCollectorFull());
}
public int getPackedOverlay(HorseLiquidCollectorEntity animatable, Void relatedObject, float u, float partialTick) {
return OverlayTexture.DEFAULT_UV;
}
protected float getDeathMaxRotation(@NotNull GeoRenderState renderState) {
return 0.0F;
}
}
@@ -0,0 +1,14 @@
package com.nonid.client;
import com.nonid.NonTrinketsIntegration;
import java.util.List;
import net.minecraft.entity.LivingEntity;
public final class NonAccessorySkinOverlays {
private NonAccessorySkinOverlays() {
}
public static List<NonTrinketsIntegration.ResolvedVisualLayer> resolveEquippedOverlays(LivingEntity entity) {
return NonTrinketsIntegration.resolveEquippedSkinOverlays(entity);
}
}
@@ -0,0 +1,270 @@
package com.nonid.client;
import com.nonid.NonAccessoryEffects;
import com.nonid.NonTrinketsIntegration;
import com.nonid.data.NonAccessoryBehavior;
import com.nonid.data.NonAccessoryDefinitions;
import com.nonid.data.NonAccessoryItemRegistry;
import com.nonid.data.NonAccessoryStackData;
import com.nonid.data.NonLiquidRoles;
import java.util.List;
import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.registry.Registries;
import net.minecraft.text.MutableText;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import net.minecraft.util.Identifier;
public final class NonAccessoryTooltips {
private static final Text TANK_SCOPED_ICON = Text.literal("\ud83e\uddea ");
private NonAccessoryTooltips() {
}
public static void register() {
ItemTooltipCallback.EVENT.register((ItemTooltipCallback)(stack, context, type, lines) -> appendTooltip(stack, lines));
}
private static void appendTooltip(ItemStack stack, List<Text> lines) {
if (stack != null && !stack.isEmpty()) {
Identifier itemId = Registries.ITEM.getId(stack.getItem());
if (NonAccessoryDefinitions.hasDefinition(itemId)) {
NonAccessoryEffects effects = NonAccessoryDefinitions.getEffects(itemId);
NonTrinketsIntegration.TankScopeStatus tankScope = resolveTankScopeStatus(stack);
int effectStartIndex = lines.size();
boolean warnedInactiveTankSlot = false;
warnedInactiveTankSlot |= addLiquidDecayTooltip(lines, itemId, effects.liquidDecayMultiplier(), tankScope);
if (effects.equalizeLiquidDecayContext()) {
lines.add(
tankScopedText(
Text.translatable("item.needsofnature.accessory.tooltip.equalize_liquid_decay_context"),
itemId,
"equalize_liquid_decay_context",
true,
tankScope
)
);
warnedInactiveTankSlot |= tankScope == NonTrinketsIntegration.TankScopeStatus.INACTIVE_EQUIPPED;
}
addMultiplierTooltip(lines, itemId, "player_energy_gain", "player_energy_gain_multiplier", effects.playerEnergyGainMultiplier(), false);
warnedInactiveTankSlot |= addAdditiveTooltip(lines, itemId, "liquid_capacity", "liquid_capacity_add", effects.liquidCapacityAdd(), true, tankScope);
warnedInactiveTankSlot |= addMultiplierTooltip(
lines, itemId, "liquid_gain", "liquid_gain_multiplier", effects.liquidGainMultiplier(), true, tankScope
);
warnedInactiveTankSlot |= addMultiplierTooltip(
lines, itemId, "filled_effect", "filled_effect_multiplier", effects.filledEffectMultiplier(), false, tankScope
);
warnedInactiveTankSlot |= addMultiplierTooltip(
lines, itemId, "pregnancy_chance", "pregnancy_chance_multiplier", effects.pregnancyChanceMultiplier(), false, tankScope
);
warnedInactiveTankSlot |= addMultiplierTooltip(
lines, itemId, "pregnancy_duration", "pregnancy_duration_multiplier", effects.pregnancyDurationMultiplier(), false, tankScope
);
warnedInactiveTankSlot |= addMultiplierTooltip(lines, itemId, "mess_gain", "mess_gain_multiplier", effects.messGainMultiplier(), false, tankScope);
addMultiplierTooltip(lines, itemId, "destroyed_skin_damage", "destroyed_skin_damage_multiplier", effects.destroyedSkinDamageMultiplier(), false);
addAdditiveTooltip(lines, itemId, "attack_escape_hits", "attack_escape_hits_add", effects.attackEscapeHitsAdd(), false);
addMultiplierTooltip(lines, itemId, "attack_escape_damage", "attack_escape_damage_multiplier", effects.attackEscapeDamageMultiplier(), false);
addMultiplierTooltip(lines, itemId, "player_energy_aura", "player_energy_aura_multiplier", effects.playerEnergyAuraMultiplier(), false);
addMultiplierTooltip(
lines, itemId, "near_animation_mob_energy", "near_animation_mob_energy_multiplier", effects.nearAnimationMobEnergyMultiplier(), false
);
addBehaviorTooltips(lines, itemId, stack);
if (warnedInactiveTankSlot) {
lines.add(effectStartIndex, Text.translatable("item.needsofnature.accessory.tooltip.inactive_tank_slot").formatted(Formatting.GRAY));
}
}
}
}
private static void addBehaviorTooltips(List<Text> lines, Identifier itemId, ItemStack stack) {
NonAccessoryBehavior behavior = NonAccessoryDefinitions.getBehavior(itemId);
if (!behavior.blocksInjectorTypes().isEmpty()) {
boolean blocksV = behavior.blocksInjectorTypes().contains(NonLiquidRoles.InjectorRole.V);
boolean blocksA = behavior.blocksInjectorTypes().contains(NonLiquidRoles.InjectorRole.A);
if (blocksV && blocksA) {
lines.add(Text.translatable("item.needsofnature.accessory.tooltip.blocks_injector_av").formatted(Formatting.GREEN));
} else if (blocksV) {
lines.add(Text.translatable("item.needsofnature.accessory.tooltip.blocks_injector_v").formatted(Formatting.GREEN));
} else if (blocksA) {
lines.add(Text.translatable("item.needsofnature.accessory.tooltip.blocks_injector_a").formatted(Formatting.GREEN));
}
}
if (behavior.occupiesSlots().size() > 1) {
lines.add(Text.translatable("item.needsofnature.accessory.tooltip.occupies_av").formatted(Formatting.GRAY));
}
if (behavior.ignoreInjectorSlotEffectSuppression()) {
lines.add(Text.translatable("item.needsofnature.accessory.tooltip.ignore_injector_slot_effect_suppression").formatted(Formatting.GRAY));
}
if (behavior.blocksPregnancy()) {
lines.add(Text.translatable("item.needsofnature.accessory.tooltip.blocks_pregnancy").formatted(Formatting.GREEN));
}
NonAccessoryItemRegistry.AccessoryItemDefinition definition = NonAccessoryItemRegistry.getDefinition(itemId);
if (definition != null && definition.actorVisualBehavior().substitutesMaleRole()) {
lines.add(Text.translatable("item.needsofnature.accessory.tooltip.male_roles").formatted(Formatting.GREEN));
int capacity = definition.actorVisualBehavior().storedLiquidCapacityMl();
if (capacity <= 0) {
lines.add(Text.translatable("item.needsofnature.accessory.tooltip.no_injected_liquid").formatted(Formatting.GRAY));
} else {
NonAccessoryStackData.StoredLiquid stored = NonAccessoryStackData.getStoredLiquid(stack);
if (stored == null) {
lines.add(Text.translatable("item.needsofnature.accessory.tooltip.stored_liquid.empty", new Object[]{capacity}).formatted(Formatting.GRAY));
} else {
lines.add(
Text.translatable(
"item.needsofnature.accessory.tooltip.stored_liquid.filled", new Object[]{stored.amountMl(), stored.entityTypeId().toString()}
)
.formatted(Formatting.AQUA)
);
}
}
}
}
private static boolean addLiquidDecayTooltip(List<Text> lines, Identifier itemId, double value, NonTrinketsIntegration.TankScopeStatus tankScope) {
if (isNeutral(value)) {
return false;
}
boolean beneficial = value < 1.0;
String key = beneficial ? "item.needsofnature.accessory.tooltip.liquid_decay_reduced" : "item.needsofnature.accessory.tooltip.liquid_decay_increased";
lines.add(tankScopedText(Text.translatable(key, new Object[]{signedPercentDelta(value)}), itemId, "liquid_decay_multiplier", beneficial, tankScope));
return tankScope == NonTrinketsIntegration.TankScopeStatus.INACTIVE_EQUIPPED;
}
private static void addMultiplierTooltip(List<Text> lines, Identifier itemId, String keyPart, String effectKey, double value, boolean higherIsBeneficial) {
if (!isNeutral(value)) {
boolean increased = value > 1.0;
boolean beneficial = increased == higherIsBeneficial;
String direction = increased ? "increased" : "reduced";
lines.add(
styledText(
Text.translatable("item.needsofnature.accessory.tooltip." + keyPart + "." + direction, new Object[]{signedPercentDelta(value)}),
itemId,
effectKey,
beneficial
)
);
}
}
private static boolean addMultiplierTooltip(
List<Text> lines,
Identifier itemId,
String keyPart,
String effectKey,
double value,
boolean higherIsBeneficial,
NonTrinketsIntegration.TankScopeStatus tankScope
) {
if (isNeutral(value)) {
return false;
}
boolean increased = value > 1.0;
boolean beneficial = increased == higherIsBeneficial;
String direction = increased ? "increased" : "reduced";
lines.add(
tankScopedText(
Text.translatable("item.needsofnature.accessory.tooltip." + keyPart + "." + direction, new Object[]{signedPercentDelta(value)}),
itemId,
effectKey,
beneficial,
tankScope
)
);
return tankScope == NonTrinketsIntegration.TankScopeStatus.INACTIVE_EQUIPPED;
}
private static void addAdditiveTooltip(List<Text> lines, Identifier itemId, String keyPart, String effectKey, int value, boolean higherIsBeneficial) {
if (value != 0) {
boolean increased = value > 0;
boolean beneficial = increased == higherIsBeneficial;
String direction = increased ? "increased" : "reduced";
lines.add(
styledText(
Text.translatable("item.needsofnature.accessory.tooltip." + keyPart + "." + direction, new Object[]{signedInt(value)}),
itemId,
effectKey,
beneficial
)
);
}
}
private static boolean addAdditiveTooltip(
List<Text> lines,
Identifier itemId,
String keyPart,
String effectKey,
int value,
boolean higherIsBeneficial,
NonTrinketsIntegration.TankScopeStatus tankScope
) {
if (value == 0) {
return false;
}
boolean increased = value > 0;
boolean beneficial = increased == higherIsBeneficial;
String direction = increased ? "increased" : "reduced";
lines.add(
tankScopedText(
Text.translatable("item.needsofnature.accessory.tooltip." + keyPart + "." + direction, new Object[]{signedInt(value)}),
itemId,
effectKey,
beneficial,
tankScope
)
);
return tankScope == NonTrinketsIntegration.TankScopeStatus.INACTIVE_EQUIPPED;
}
private static Text tankScopedText(Text body, Identifier itemId, String effectKey, boolean beneficial, NonTrinketsIntegration.TankScopeStatus tankScope) {
MutableText text = Text.literal("").append(TANK_SCOPED_ICON).append(body);
return (Text)(tankScope == NonTrinketsIntegration.TankScopeStatus.INACTIVE_EQUIPPED
? text.formatted(Formatting.GRAY)
: applyColor(text, NonAccessoryDefinitions.getTooltipColor(itemId, effectKey), beneficial));
}
private static Text styledText(Text body, Identifier itemId, String effectKey, boolean beneficial) {
return applyColor(body.copy(), NonAccessoryDefinitions.getTooltipColor(itemId, effectKey), beneficial);
}
private static Text applyColor(MutableText text, NonAccessoryDefinitions.TooltipColor color, boolean beneficial) {
if (color == null) {
return text.formatted(beneficial ? Formatting.GREEN : Formatting.RED);
} else if (color.formatting() != null) {
return text.formatted(color.formatting());
} else {
return color.rgb() != null ? text.setStyle(text.getStyle().withColor(color.rgb())) : text.formatted(beneficial ? Formatting.GREEN : Formatting.RED);
}
}
private static boolean isNeutral(double value) {
return Math.abs(value - 1.0) < 1.0E-6;
}
private static String signedPercentDelta(double value) {
int percent = (int)Math.round(Math.abs(value - 1.0) * 100.0);
String sign = value >= 1.0 ? "+" : "-";
return sign + percent + "%";
}
private static String signedInt(int value) {
return (value > 0 ? "+" : "") + value;
}
private static NonTrinketsIntegration.TankScopeStatus resolveTankScopeStatus(ItemStack stack) {
MinecraftClient client = MinecraftClient.getInstance();
ClientPlayerEntity player = client == null ? null : client.player;
return NonTrinketsIntegration.resolveTankScopeStatus(player, stack);
}
}
@@ -0,0 +1,94 @@
package com.nonid.client;
import com.mojang.blaze3d.textures.GpuTextureView;
import com.nonid.internal.animation.AnimationEngine;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gl.RenderPipelines;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.util.Identifier;
public final class NonAnimationPreviewRenderer {
private static final Map<String, Optional<NonAnimationPreviewRenderer.PreviewTexture>> TEXTURE_CACHE = new HashMap<>();
private NonAnimationPreviewRenderer() {
}
public static void draw(DrawContext context, TextRenderer textRenderer, String animationId, int x, int y, int size) {
Optional<NonAnimationPreviewRenderer.PreviewTexture> preview = previewTexture(animationId);
if (preview.isPresent()) {
NonAnimationPreviewRenderer.PreviewTexture texture = preview.get();
int textureWidth = texture.width();
int textureHeight = texture.height();
int drawWidth = size;
int drawHeight = size;
if (textureWidth > textureHeight) {
drawHeight = Math.max(1, Math.round(size * ((float)textureHeight / textureWidth)));
} else if (textureHeight > textureWidth) {
drawWidth = Math.max(1, Math.round(size * ((float)textureWidth / textureHeight)));
}
int drawX = x + (size - drawWidth) / 2;
int drawY = y + (size - drawHeight) / 2;
context.fill(x, y, x + size, y + size, -13948117);
context.drawTexture(
RenderPipelines.GUI_TEXTURED,
texture.id(),
drawX,
drawY,
0.0F,
0.0F,
drawWidth,
drawHeight,
textureWidth,
textureHeight,
textureWidth,
textureHeight
);
} else {
context.fill(x, y, x + size, y + size, -13948117);
context.drawCenteredTextWithShadow(textRenderer, "?", x + size / 2, y + size / 2 - 4, -5592406);
}
}
public static void clearCache() {
TEXTURE_CACHE.clear();
}
private static Optional<NonAnimationPreviewRenderer.PreviewTexture> previewTexture(String animationId) {
return TEXTURE_CACHE.computeIfAbsent(animationId, NonAnimationPreviewRenderer::resolvePreviewTexture);
}
private static Optional<NonAnimationPreviewRenderer.PreviewTexture> resolvePreviewTexture(String animationId) {
Identifier id = Identifier.tryParse(animationId);
if (id == null) {
return Optional.empty();
}
String path = "textures/afw/previews/" + id.getPath() + ".png";
Identifier namespaced = Identifier.of(id.getNamespace(), path);
MinecraftClient client = MinecraftClient.getInstance();
Identifier preview = null;
if (client.getResourceManager().getResource(namespaced).isPresent()) {
preview = namespaced;
} else {
Identifier fallback = AnimationEngine.packId(path);
if (client.getResourceManager().getResource(fallback).isPresent()) {
preview = fallback;
}
}
if (preview == null) {
return Optional.empty();
}
GpuTextureView textureView = client.getTextureManager().getTexture(preview).getGlTextureView();
return Optional.of(new NonAnimationPreviewRenderer.PreviewTexture(preview, Math.max(1, textureView.getWidth(0)), Math.max(1, textureView.getHeight(0))));
}
private record PreviewTexture(Identifier id, int width, int height) {
}
}
@@ -0,0 +1,307 @@
package com.nonid.client;
import com.nonid.NeedsOfNature;
import com.nonid.NonAccessoryShedSystem;
import com.nonid.NonTrinketsIntegration;
import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import net.fabricmc.fabric.api.client.rendering.v1.world.WorldRenderContext;
import net.fabricmc.fabric.api.client.rendering.v1.world.WorldRenderEvents;
import net.minecraft.block.BlockState;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.AbstractClientPlayerEntity;
import net.minecraft.client.render.entity.ItemEntityRenderer;
import net.minecraft.client.render.entity.state.ItemStackEntityRenderState;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.entity.EquipmentSlot;
import net.minecraft.entity.ItemEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.RotationAxis;
import net.minecraft.util.math.Vec3d;
import net.minecraft.util.math.Direction.Axis;
import net.minecraft.util.shape.VoxelShape;
public final class NonArmorShedClient {
private static final EquipmentSlot[] ARMOR_SLOTS = new EquipmentSlot[]{EquipmentSlot.HEAD, EquipmentSlot.CHEST, EquipmentSlot.LEGS, EquipmentSlot.FEET};
private static final int FLYING_TICKS = 24;
private static final int GROUND_TICKS = 40;
private static final int GROUND_FADE_TICKS = 30;
private static final double GRAVITY = 0.035;
private static final double AIR_DRAG = 0.96;
private static final double GROUND_DRAG = 0.78;
private static final float ITEM_SCALE = 1.0F;
private static final long FRESH_START_MAX_AGE_TICKS = 5L;
private static final long QUEUED_HANDOFF_SKIP_TICKS = 12L;
private static final Identifier PLAYER_DEFEATED = Identifier.of("needsofnature", "player_defeated-belly");
private static final Identifier PLAYER_BIRTH = Identifier.of("needsofnature", "player_birth");
private static final List<NonArmorShedClient.ArmorProp> PROPS = new ArrayList<>();
private static final Map<UUID, Long> LAST_SPAWNED_START_TICK = new HashMap<>();
private static final Map<UUID, Long> LAST_ACTIVE_TICK = new HashMap<>();
private NonArmorShedClient() {
}
public static void register() {
WorldRenderEvents.AFTER_ENTITIES.register(NonArmorShedClient::render);
}
public static void clientTick(MinecraftClient client) {
if (!NeedsOfNature.getConfig().isArmorShedEffectEnabled()) {
clear();
} else {
tickProps(client);
if (client != null && client.world != null) {
long now = client.world.getTime();
for (AbstractClientPlayerEntity player : client.world.getPlayers()) {
if (player != null && !player.isRemoved()) {
UUID uuid = player.getUuid();
if (!ClientAnimationRuntime.isActorActive(uuid)) {
LAST_SPAWNED_START_TICK.remove(uuid);
} else {
Long previousActiveTick = LAST_ACTIVE_TICK.get(uuid);
Long startTick = ClientAnimationRuntime.findStartTickForActor(uuid);
if (startTick == null) {
LAST_ACTIVE_TICK.put(uuid, now);
} else {
Long lastSpawned = LAST_SPAWNED_START_TICK.get(uuid);
if (lastSpawned != null && lastSpawned.equals(startTick)) {
LAST_ACTIVE_TICK.put(uuid, now);
} else {
if (shouldSpawnForStart(uuid, now, startTick, previousActiveTick)) {
spawnForPlayer(player, startTick);
}
LAST_SPAWNED_START_TICK.put(uuid, startTick);
LAST_ACTIVE_TICK.put(uuid, now);
}
}
}
}
}
} else {
clear();
}
}
}
public static void clear() {
PROPS.clear();
LAST_SPAWNED_START_TICK.clear();
LAST_ACTIVE_TICK.clear();
}
private static boolean shouldSpawnForStart(UUID playerUuid, long now, long startTick, Long previousActiveTick) {
if (now - startTick > 5L) {
return false;
}
if (previousActiveTick != null && now - previousActiveTick <= 12L) {
return false;
}
Identifier activeAnimation = ClientAnimationRuntime.findLatestActiveAnimationIdContaining(playerUuid);
return !isQueuedFollowUpAnimation(activeAnimation);
}
private static boolean isQueuedFollowUpAnimation(Identifier animationId) {
return animationId == null ? false : PLAYER_DEFEATED.equals(animationId) || PLAYER_BIRTH.equals(animationId);
}
private static void spawnForPlayer(AbstractClientPlayerEntity player, long startTick) {
Vec3d origin = new Vec3d(player.getX(), player.getY() + player.getHeight() * 0.72, player.getZ());
long seedBase = player.getUuid().getMostSignificantBits() ^ player.getUuid().getLeastSignificantBits() ^ startTick;
Random random = new Random(seedBase);
float yawRad = player.getYaw() * (float) (Math.PI / 180.0);
Vec3d forward = new Vec3d(-MathHelper.sin(yawRad), 0.0, MathHelper.cos(yawRad));
Vec3d right = new Vec3d(forward.z, 0.0, -forward.x);
for (int i = 0; i < ARMOR_SLOTS.length; i++) {
EquipmentSlot slot = ARMOR_SLOTS[i];
ItemStack stack = player.getEquippedStack(slot);
if (stack != null && !stack.isEmpty()) {
double side = (i - 1.5) * 0.18;
Vec3d spawnPos = origin.add(right.multiply(side)).add(0.0, -0.08 * i, 0.0);
addProp(stack, spawnPos, forward, right, random, seedBase, i, ARMOR_SLOTS.length);
}
}
spawnShedAccessories(player, origin, forward, right, random, seedBase);
}
private static void spawnShedAccessories(AbstractClientPlayerEntity player, Vec3d origin, Vec3d forward, Vec3d right, Random random, long seedBase) {
NonAccessoryShedSystem.ShedState shedState = NonAccessoryShedSystem.getShedState(player.getUuid());
if (!shedState.isEmpty()) {
List<ItemStack> stacks = NonTrinketsIntegration.getVisuallyShedAccessoryStacks(player);
for (int index = 0; index < stacks.size(); index++) {
ItemStack stack = stacks.get(index);
if (stack != null && !stack.isEmpty()) {
double side = (index - 0.5) * 0.24;
Vec3d spawnPos = origin.add(right.multiply(side)).add(0.0, -0.34, 0.0);
addProp(stack, spawnPos, forward, right, random, seedBase, 20 + index, 2);
}
}
}
}
private static void addProp(ItemStack stack, Vec3d spawnPos, Vec3d forward, Vec3d right, Random random, long seedBase, int index, int count) {
ItemStack visualStack = stack.copy();
visualStack.setCount(1);
double angle = (Math.PI * 2) * index / Math.max(1, count) + (random.nextDouble() - 0.5) * 0.55;
Vec3d outward = right.multiply(Math.cos(angle)).add(forward.multiply(Math.sin(angle))).normalize();
double speed = 0.105 + random.nextDouble() * 0.035;
Vec3d velocity = outward.multiply(speed).add(0.0, 0.15 + random.nextDouble() * 0.13, 0.0);
PROPS.add(
new NonArmorShedClient.ArmorProp(
visualStack,
spawnPos,
velocity,
random.nextFloat() * 360.0F,
random.nextFloat() * 360.0F,
7.0F + random.nextFloat() * 7.0F,
10.0F + random.nextFloat() * 12.0F,
(int)(seedBase + index * 31L)
)
);
}
private static void tickProps(MinecraftClient client) {
if (!PROPS.isEmpty()) {
if (client != null && client.world != null) {
Iterator<NonArmorShedClient.ArmorProp> iterator = PROPS.iterator();
while (iterator.hasNext()) {
NonArmorShedClient.ArmorProp prop = iterator.next();
prop.age++;
prop.previousPosition = prop.position;
if (prop.landed) {
prop.groundAge++;
prop.velocity = prop.velocity.multiply(0.78, 0.0, 0.78);
if (prop.groundAge > 40) {
iterator.remove();
}
} else {
prop.velocity = prop.velocity.add(0.0, -0.035, 0.0).multiply(0.96, 0.98, 0.96);
Vec3d next = prop.position.add(prop.velocity);
NonArmorShedClient.GroundHit hit = findGround(client, next);
if (hit.hit() && prop.velocity.y <= 0.0 && prop.age > 3) {
prop.position = new Vec3d(next.x, hit.y(), next.z);
prop.velocity = Vec3d.ZERO;
prop.landed = true;
} else {
prop.position = next;
if (prop.age > 64) {
iterator.remove();
}
}
}
}
} else {
clear();
}
}
}
private static NonArmorShedClient.GroundHit findGround(MinecraftClient client, Vec3d pos) {
if (client != null && client.world != null) {
BlockPos blockPos = BlockPos.ofFloored(pos.x, pos.y - 0.04, pos.z);
BlockState state = client.world.getBlockState(blockPos);
VoxelShape shape = state.getCollisionShape(client.world, blockPos);
if (shape.isEmpty()) {
return NonArmorShedClient.GroundHit.MISS;
}
double top = blockPos.getY() + shape.getMax(Axis.Y);
return pos.y < top + 0.12 ? new NonArmorShedClient.GroundHit(true, top + 0.03) : NonArmorShedClient.GroundHit.MISS;
} else {
return NonArmorShedClient.GroundHit.MISS;
}
}
private static void render(WorldRenderContext context) {
if (NeedsOfNature.getConfig().isArmorShedEffectEnabled()) {
if (!PROPS.isEmpty()) {
MinecraftClient client = MinecraftClient.getInstance();
if (client != null && client.world != null) {
Vec3d cameraPos = context.worldState().cameraRenderState.pos;
if (cameraPos != null) {
float tickProgress = client.getRenderTickCounter().getTickProgress(false);
MatrixStack matrices = context.matrices();
for (NonArmorShedClient.ArmorProp prop : PROPS) {
Vec3d renderPos = prop.previousPosition.lerp(prop.position, tickProgress);
matrices.push();
matrices.translate(renderPos.x - cameraPos.x, renderPos.y - cameraPos.y, renderPos.z - cameraPos.z);
float smoothAge = prop.age + tickProgress;
float yaw = prop.landed ? prop.groundYaw : prop.baseYaw + smoothAge * prop.yawSpeed;
float pitch = prop.landed ? 90.0F : prop.basePitch + smoothAge * prop.pitchSpeed;
matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(yaw));
matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(pitch));
float fadeScale = prop.landed ? prop.groundFadeScale(tickProgress) : 1.0F;
matrices.scale(1.0F * fadeScale, 1.0F * fadeScale, 1.0F * fadeScale);
ItemStackEntityRenderState state = new ItemStackEntityRenderState();
ItemEntity fake = new ItemEntity(client.world, prop.position.x, prop.position.y, prop.position.z, prop.stack);
state.update(fake, prop.stack, client.getItemModelManager());
ItemEntityRenderer.renderStack(matrices, context.commandQueue(), 15728880, state, net.minecraft.util.math.random.Random.create(prop.seed));
matrices.pop();
}
}
}
}
}
}
private static final class ArmorProp {
final ItemStack stack;
Vec3d position;
Vec3d previousPosition;
Vec3d velocity;
final float baseYaw;
final float basePitch;
final float yawSpeed;
final float pitchSpeed;
final float groundYaw;
final int seed;
int age;
int groundAge;
boolean landed;
ArmorProp(ItemStack stack, Vec3d position, Vec3d velocity, float baseYaw, float basePitch, float yawSpeed, float pitchSpeed, int seed) {
this.stack = stack;
this.position = position;
this.previousPosition = position;
this.velocity = velocity;
this.baseYaw = baseYaw;
this.basePitch = basePitch;
this.yawSpeed = yawSpeed;
this.pitchSpeed = pitchSpeed;
this.groundYaw = baseYaw;
this.seed = seed;
}
float groundFadeScale(float tickProgress) {
int fadeStart = Math.max(0, 10);
float smoothGroundAge = this.groundAge + tickProgress;
if (smoothGroundAge <= fadeStart) {
return 1.0F;
}
float remaining = (40.0F - smoothGroundAge) / 30.0F;
return MathHelper.clamp(remaining, 0.0F, 1.0F);
}
}
private record GroundHit(boolean hit, double y) {
private static final NonArmorShedClient.GroundHit MISS = new NonArmorShedClient.GroundHit(false, 0.0);
}
}
Loaded 100 of 567 files, more files were not shown because too many files have changed in this diff. Show more