commit
1eea4b9eaa
567 files changed
+81290
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"blockbench": {
|
||||
"command": "node",
|
||||
"args": ["D:\\tools\\blockbench-mcp\\dist\\index.js"],
|
||||
"env": {
|
||||
"BLOCKBENCH_MCP_PORT": "8787"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
|
||||
Vendored
BIN
Binary file not shown.
+9
@@ -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
|
||||
@@ -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
@@ -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%
|
||||
@@ -0,0 +1,13 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
maven {
|
||||
name = 'Fabric'
|
||||
url = 'https://maven.fabricmc.net/'
|
||||
}
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
// Should match your modid
|
||||
rootProject.name = 'needsofnature'
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in new issue
Block a user