mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 05:50:39 +00:00
Compare commits
270
Commits
No files matched your search
@@ -1,87 +1,78 @@
|
|||||||
name: Iris Bug Report
|
name: Bug report
|
||||||
description: File a bug report for Iris
|
description: Something in Iris isn't working.
|
||||||
labels: [ bug ]
|
labels: [bug]
|
||||||
|
type: Bug
|
||||||
|
assignees: [SwiftSwamp]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thank you for taking the time to fill this out!
|
Make sure you're on the [latest release](https://github.com/VolmitSoftware/Iris/releases/latest) and that it hasn't [already been reported](https://github.com/VolmitSoftware/Iris/issues?q=is%3Aissue).
|
||||||
If this does not work for you, feel free to use the [blank](https://github.com/VolmitSoftware/Iris/issues/new) format.
|
|
||||||
|
The Iris plugin supports Minecraft 26.1.2 through 26.3 on Paper, Purpur, Leaf, Canvas, Folia, and Spigot. The Fabric, Forge, and NeoForge builds support 26.3. Both need Java 25.
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: how
|
id: issue
|
||||||
attributes:
|
attributes:
|
||||||
label: Problem
|
label: What issue are you having?
|
||||||
description: Please give a text description of how you reached the problem
|
description: Include any error message you saw.
|
||||||
value: |
|
|
||||||
1. Install Iris...
|
|
||||||
2. Do this...
|
|
||||||
3. Do that...
|
|
||||||
4. Observe the error...
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: what
|
id: steps
|
||||||
attributes:
|
attributes:
|
||||||
label: Solution
|
label: Steps to reproduce
|
||||||
description: Explain where you think the problem comes from (optional)
|
placeholder: |
|
||||||
placeholder: The code to place a is missing b and c...
|
1. Start a server with Iris installed
|
||||||
|
2. Change ... in the config
|
||||||
|
3. Run /iris ...
|
||||||
|
4. ... happens instead of ...
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: diagnostics
|
||||||
|
attributes:
|
||||||
|
label: Debug dump or latest log file
|
||||||
|
description: Run `/iris debug dump upload=true` and paste the link. If Iris won't start, upload `logs/latest.log` to [mclo.gs](https://mclo.gs) and paste that link instead.
|
||||||
|
placeholder: https://mclo.gs/...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: plugin-version
|
||||||
|
attributes:
|
||||||
|
label: "Iris version"
|
||||||
|
description: Run `/iris debug version` (`/iris version` on Fabric, Forge, and NeoForge).
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: platform
|
id: platform
|
||||||
attributes:
|
attributes:
|
||||||
label: Platform
|
label: Server software
|
||||||
description: Which server platform is Iris running on? Pick the loader or server software, not the launcher.
|
|
||||||
options:
|
options:
|
||||||
- Paper
|
- "Paper"
|
||||||
- Purpur
|
- "Purpur"
|
||||||
- Leaf
|
- "Leaf"
|
||||||
- Canvas
|
- "Canvas"
|
||||||
- Folia
|
- "Folia"
|
||||||
- Spigot / CraftBukkit
|
- "Spigot"
|
||||||
- Fabric
|
- "Fabric"
|
||||||
- Forge
|
- "Forge"
|
||||||
- NeoForge
|
- "NeoForge"
|
||||||
- Other (describe under Problem)
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
id: mcversion
|
|
||||||
attributes:
|
|
||||||
label: Minecraft Version
|
|
||||||
description: What version of Minecraft is the server on? Iris 4.x supports 26.1.2 to 26.3 on Bukkit-family servers and 26.3 on Fabric, Forge and NeoForge; older versions are not supported.
|
|
||||||
options:
|
|
||||||
- '26.3'
|
|
||||||
- '26.2'
|
|
||||||
- '26.1.2'
|
|
||||||
- Other (unsupported)
|
- Other (unsupported)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: dropdown
|
||||||
id: loaderversion
|
id: minecraft
|
||||||
attributes:
|
attributes:
|
||||||
label: Platform / Loader Version
|
label: Minecraft version
|
||||||
description: Exact server or loader build (see console). For example "Paper 26.3-133", "Fabric Loader 0.19.5", "NeoForge 26.3.0.33-beta".
|
options:
|
||||||
placeholder: DO NOT SAY "LATEST"
|
- "26.3"
|
||||||
|
- "26.2"
|
||||||
|
- "26.1.2"
|
||||||
|
- Other (unsupported)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: textarea
|
||||||
id: irisversion
|
id: extra
|
||||||
attributes:
|
attributes:
|
||||||
label: Iris Version
|
label: Anything else?
|
||||||
description: What version of Iris are you running? (see console)
|
description: Other plugins involved, screenshots, or videos.
|
||||||
placeholder: DO NOT SAY "LATEST"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: logs
|
|
||||||
attributes:
|
|
||||||
label: Log
|
|
||||||
description: Paste a full log. Always use [mclogs](https://mclo.gs) Or [Pastebin](https://pastebin.com/). Must not be a crash report. Must be a full log. Must not be a screenshot of a log.
|
|
||||||
placeholder: https://mslog.gs/...
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: markdown
|
|
||||||
id: thanks
|
|
||||||
attributes:
|
|
||||||
value: "Thank you for filling out the form! We will be with you soon. Please do not ask support to review your report."
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Help and questions
|
||||||
|
url: https://volmitsoftware.com/discord
|
||||||
|
about: Ask setup and configuration questions on the Volmit Discord.
|
||||||
|
- name: "Iris documentation"
|
||||||
|
url: https://docs.volmitsoftware.com/iris
|
||||||
|
about: Installation, commands, permissions, and configuration.
|
||||||
@@ -1,46 +1,28 @@
|
|||||||
name: Iris Feature Request
|
name: Suggestion
|
||||||
description: File a feature request for Iris. If you want to report a bug this is not the place.
|
description: Suggest a new feature or a change to Iris.
|
||||||
labels: [ feature ]
|
labels: [enhancement]
|
||||||
|
type: Feature
|
||||||
|
assignees: [SwiftSwamp]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thank you for taking the time to fill this out!
|
Check the [existing suggestions](https://github.com/VolmitSoftware/Iris/issues?q=is%3Aissue+label%3Aenhancement) first and add relevant information to the existing issue.
|
||||||
If this does not work for you, feel free to use the [blank](https://github.com/VolmitSoftware/Iris/issues/new) format.
|
- type: textarea
|
||||||
- type: dropdown
|
id: problem
|
||||||
id: arc
|
|
||||||
attributes:
|
attributes:
|
||||||
label: Adding, Removing, or Changing
|
label: What are your suggestions for new features or changes?
|
||||||
description: What are you doing
|
|
||||||
options:
|
|
||||||
- Adding
|
|
||||||
- Removing
|
|
||||||
- Changing
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: atype
|
|
||||||
attributes:
|
|
||||||
label: Type of Modification
|
|
||||||
description: What is it for?
|
|
||||||
value: I want to ...
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: desc
|
id: proposal
|
||||||
attributes:
|
attributes:
|
||||||
label: What are you trying to modify
|
label: How should it work?
|
||||||
description: Give as detailed of a description as you can for the modification that you want done (include pictures if applicable)
|
description: Provide as much detail as possible on what you would like changed or added.
|
||||||
value: The way I would implement this is ...
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: alternative
|
id: extra
|
||||||
attributes:
|
attributes:
|
||||||
label: Alternatives
|
label: Anything else?
|
||||||
description: What alternatives have you considered?
|
description: Any additional information such as screenshots, examples, etc.
|
||||||
value: If this could not be implemented I would ...
|
|
||||||
- type: markdown
|
|
||||||
id: thanks
|
|
||||||
attributes:
|
|
||||||
value: "Thank you for filling out the form! We will be with you soon. Please do not ask support to review your report."
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
name: Translation
|
||||||
|
description: A message in Iris is wrong, missing, or unclear.
|
||||||
|
labels: [bug]
|
||||||
|
type: Bug
|
||||||
|
assignees: [SwiftSwamp]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
The language files are in [`core/src/main/resources/languages/`](https://github.com/VolmitSoftware/Iris/tree/HEAD/core/src/main/resources/languages).
|
||||||
|
|
||||||
|
You can create a pull request to modify the files directly.
|
||||||
|
- type: dropdown
|
||||||
|
id: language
|
||||||
|
attributes:
|
||||||
|
label: Language
|
||||||
|
options:
|
||||||
|
- English (en_US)
|
||||||
|
- German (de_DE)
|
||||||
|
- Spanish (es_ES)
|
||||||
|
- Finnish (fi_FI)
|
||||||
|
- French (fr_FR)
|
||||||
|
- Hebrew (he_IL)
|
||||||
|
- Italian (it_IT)
|
||||||
|
- Japanese (ja-JP)
|
||||||
|
- Korean (ko_KR)
|
||||||
|
- Lithuanian (lt_LT)
|
||||||
|
- Dutch (nl_NL)
|
||||||
|
- Polish (pl_PL)
|
||||||
|
- Portuguese (pt_PT)
|
||||||
|
- Russian (ru_RU)
|
||||||
|
- Turkish (tr_TR)
|
||||||
|
- Vietnamese (vi_VI)
|
||||||
|
- Chinese, Simplified (zh_CN)
|
||||||
|
- Chinese, Traditional (zh_TW)
|
||||||
|
- Another language (name it below)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: current
|
||||||
|
attributes:
|
||||||
|
label: Current text
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: suggested
|
||||||
|
attributes:
|
||||||
|
label: Suggested text
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: where
|
||||||
|
attributes:
|
||||||
|
label: Where does it appear?
|
||||||
|
description: The command used or message key where it is appearing.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "Iris",
|
||||||
|
"platform": "modded",
|
||||||
|
"versionProperty": "irisVersion",
|
||||||
|
"betaTag": "modded-beta",
|
||||||
|
"targets": "gradle/mod-targets.json",
|
||||||
|
"volmlibRef": "master"
|
||||||
|
}
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import tomllib
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
|
||||||
|
def read_config(root: Path) -> dict:
|
||||||
|
return json.loads((root / '.github/release.json').read_text())
|
||||||
|
|
||||||
|
|
||||||
|
def targets(root: Path, config: dict) -> list[dict[str, str]]:
|
||||||
|
if config['platform'] == 'bukkit':
|
||||||
|
return [{'id': 'bukkit', 'loader': 'bukkit', 'minecraft': ''}]
|
||||||
|
manifest: Path = root / config['targets']
|
||||||
|
if manifest.is_file():
|
||||||
|
versions: dict = json.loads(manifest.read_text())
|
||||||
|
entries: list[tuple[str, list[str]]] = [(version, ['fabric', 'forge', 'neoforge']) for version in versions]
|
||||||
|
else:
|
||||||
|
entries = []
|
||||||
|
for path in sorted(manifest.glob('*.json')):
|
||||||
|
target: dict = json.loads(path.read_text())
|
||||||
|
if path.stem != target['minecraft']:
|
||||||
|
raise ValueError(f'Minecraft manifest does not match {path.name}')
|
||||||
|
entries.append((target['minecraft'], list(target['loaders'])))
|
||||||
|
matrix: list[dict[str, str]] = []
|
||||||
|
for version, loaders in entries:
|
||||||
|
if not re.fullmatch(r'[0-9]+(?:\.[0-9]+)+', version):
|
||||||
|
raise ValueError(f'Invalid Minecraft version: {version}')
|
||||||
|
if set(loaders) != {'fabric', 'forge', 'neoforge'}:
|
||||||
|
raise ValueError(f'Incomplete loader matrix for {version}')
|
||||||
|
for loader in sorted(loaders):
|
||||||
|
matrix.append({'id': f'{loader}-{version}', 'loader': loader, 'minecraft': version})
|
||||||
|
if not matrix:
|
||||||
|
raise ValueError('Release matrix is empty')
|
||||||
|
return matrix
|
||||||
|
|
||||||
|
|
||||||
|
def plan(root: Path, config: dict, event: dict, environment: dict) -> dict:
|
||||||
|
branch: str = event.get('repository', {}).get('default_branch', '')
|
||||||
|
if (environment.get('GITHUB_EVENT_NAME') != 'push' or event.get('deleted')
|
||||||
|
or not branch or event.get('ref') != f'refs/heads/{branch}'):
|
||||||
|
return {'channel': 'none'}
|
||||||
|
commit: dict = event.get('head_commit') or {}
|
||||||
|
message: str = commit.get('message', '')
|
||||||
|
beta: bool = 'B+' in message
|
||||||
|
stable: bool = 'V+' in message
|
||||||
|
if beta and stable:
|
||||||
|
raise ValueError('Use exactly one release marker: B+ or V+')
|
||||||
|
if not beta and not stable:
|
||||||
|
return {'channel': 'none'}
|
||||||
|
sha: str = environment['GITHUB_SHA']
|
||||||
|
if event.get('after') != sha or commit.get('id') != sha or not re.fullmatch(r'[a-f0-9]{40}', sha):
|
||||||
|
raise ValueError('Release commit does not match the pushed HEAD')
|
||||||
|
key: str = config['versionProperty']
|
||||||
|
properties: str = (root / 'gradle.properties').read_text()
|
||||||
|
values: list[str] = re.findall(rf'^{re.escape(key)}\s*=\s*(\S+)\s*$', properties, re.MULTILINE)
|
||||||
|
if len(values) != 1 or not re.fullmatch(r'(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)', values[0]):
|
||||||
|
raise ValueError(f'{key} must declare one stable major.minor.patch version')
|
||||||
|
run: int = int(environment['GITHUB_RUN_NUMBER'])
|
||||||
|
attempt: int = int(environment['GITHUB_RUN_ATTEMPT'])
|
||||||
|
if run < 1 or attempt < 1:
|
||||||
|
raise ValueError('Invalid workflow run identity')
|
||||||
|
version: str = f'{values[0]}-beta.{run}.{attempt}' if beta else values[0]
|
||||||
|
return {
|
||||||
|
'channel': 'beta' if beta else 'stable',
|
||||||
|
'version': version,
|
||||||
|
'version_property': key,
|
||||||
|
'tag': config['betaTag'] if beta else f'v{version}',
|
||||||
|
'sha': sha,
|
||||||
|
'run': run,
|
||||||
|
'attempt': attempt,
|
||||||
|
'matrix': {'include': targets(root, config)},
|
||||||
|
'volmlib_ref': config['volmlibRef'],
|
||||||
|
'wormholes_ref': config.get('wormholesRef', ''),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def collect_artifacts(directory: Path, release: dict) -> dict[str, bytes]:
|
||||||
|
expected: dict[str, dict] = {f"release-{entry['id']}": entry for entry in release['matrix']['include']}
|
||||||
|
if {path.name for path in directory.iterdir()} != set(expected):
|
||||||
|
raise ValueError('Downloaded artifacts do not match the complete release matrix')
|
||||||
|
assets: dict[str, bytes] = {}
|
||||||
|
descriptors: dict[str, str] = {'bukkit': 'plugin.yml', 'fabric': 'fabric.mod.json',
|
||||||
|
'forge': 'META-INF/mods.toml', 'neoforge': 'META-INF/neoforge.mods.toml'}
|
||||||
|
for name, target in expected.items():
|
||||||
|
folder: Path = directory / name
|
||||||
|
paths: list[Path] = list(folder.iterdir()) if folder.is_dir() and not folder.is_symlink() else []
|
||||||
|
if len(paths) != 1 or paths[0].suffix != '.jar' or paths[0].is_symlink() or not paths[0].is_file():
|
||||||
|
raise ValueError(f'{name} must contain exactly one runtime jar')
|
||||||
|
path: Path = paths[0]
|
||||||
|
loader: str = target['loader']
|
||||||
|
suffix: str = re.escape(release['version']) + r'-bukkit\.jar'
|
||||||
|
if loader != 'bukkit':
|
||||||
|
display: str = {'fabric': 'Fabric', 'forge': 'Forge', 'neoforge': 'NeoForge'}[loader]
|
||||||
|
suffix = (r'v' + re.escape(release['version']) + r' \[' + display + r'\] '
|
||||||
|
+ re.escape(target['minecraft']) + r'\+[^/\s]+\.jar')
|
||||||
|
if not re.fullmatch(r'[A-Za-z][A-Za-z0-9]*' + ('-' if loader == 'bukkit' else ' ') + suffix, path.name):
|
||||||
|
raise ValueError(f'Artifact does not match the release target: {path.name}')
|
||||||
|
with zipfile.ZipFile(path) as archive:
|
||||||
|
entries: set[str] = set(archive.namelist())
|
||||||
|
descriptor: str = descriptors[target['loader']]
|
||||||
|
if descriptor not in entries:
|
||||||
|
raise ValueError(f'{path.name} is missing {descriptor}')
|
||||||
|
excluded: set[str] = set(descriptors.values()) - {descriptor}
|
||||||
|
if 'velocity-plugin.json' in entries or entries.intersection(excluded):
|
||||||
|
raise ValueError(f'{path.name} contains an excluded platform')
|
||||||
|
contents: str = archive.read(descriptor).decode()
|
||||||
|
if loader == 'bukkit':
|
||||||
|
match: re.Match | None = re.search(r'^version:\s*[\'\"]?([^\s\'\"]+)', contents, re.MULTILINE)
|
||||||
|
version: str | None = match[1] if match else None
|
||||||
|
elif loader == 'fabric':
|
||||||
|
metadata: dict = json.loads(contents)
|
||||||
|
version = metadata.get('version')
|
||||||
|
if metadata.get('depends', {}).get('minecraft') not in (target['minecraft'], '~' + target['minecraft']):
|
||||||
|
raise ValueError(f'{path.name} declares a different Minecraft target')
|
||||||
|
else:
|
||||||
|
metadata = tomllib.loads(contents)
|
||||||
|
mods: list[dict] = metadata.get('mods', [])
|
||||||
|
version = mods[0].get('version') if len(mods) == 1 else None
|
||||||
|
dependencies: list[dict] = metadata.get('dependencies', {}).get(mods[0].get('modId'), []) if len(mods) == 1 else []
|
||||||
|
minecraft: list[str] = [entry.get('versionRange') for entry in dependencies if entry.get('modId') == 'minecraft']
|
||||||
|
if minecraft != ['[' + target['minecraft'] + ']']:
|
||||||
|
raise ValueError(f'{path.name} declares a different Minecraft target')
|
||||||
|
if version != release['version']:
|
||||||
|
raise ValueError(f'{path.name} declares a different product version')
|
||||||
|
asset_name: str = re.sub(r'[^A-Za-z0-9._-]+', '-', path.name)
|
||||||
|
if asset_name in assets:
|
||||||
|
raise ValueError(f'Duplicate release asset: {asset_name}')
|
||||||
|
assets[asset_name] = path.read_bytes()
|
||||||
|
checksums: str = ''.join(f'{hashlib.sha256(data).hexdigest()} {name}\n' for name, data in sorted(assets.items()))
|
||||||
|
assets[f"SHA256SUMS-{release['version']}.txt"] = checksums.encode()
|
||||||
|
return assets
|
||||||
|
|
||||||
|
|
||||||
|
class GitHub:
|
||||||
|
def __init__(self, repository: str, token: str):
|
||||||
|
if not re.fullmatch(r'[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+', repository):
|
||||||
|
raise ValueError('Invalid GitHub repository')
|
||||||
|
self.repository = repository
|
||||||
|
self.token = token
|
||||||
|
|
||||||
|
def request(self, method: str, path: str, payload: dict | bytes | None = None, missing: bool = False) -> dict | list | None:
|
||||||
|
url: str = path if path.startswith('https://uploads.github.com/') else f'https://api.github.com/repos/{self.repository}/{path}'
|
||||||
|
binary: bool = isinstance(payload, bytes)
|
||||||
|
data: bytes | None = payload if binary else json.dumps(payload).encode() if payload is not None else None
|
||||||
|
request: urllib.request.Request = urllib.request.Request(url, data=data, method=method, headers={
|
||||||
|
'Authorization': f'Bearer {self.token}',
|
||||||
|
'Accept': 'application/vnd.github+json',
|
||||||
|
'X-GitHub-Api-Version': '2026-03-10',
|
||||||
|
'Content-Type': 'application/octet-stream' if binary else 'application/json',
|
||||||
|
'User-Agent': 'Volmit-release',
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request, timeout=120) as response:
|
||||||
|
body: bytes = response.read()
|
||||||
|
return json.loads(body) if body else None
|
||||||
|
except urllib.error.HTTPError as error:
|
||||||
|
if missing and error.code == 404:
|
||||||
|
return None
|
||||||
|
raise RuntimeError(f'GitHub {method} {urllib.parse.urlsplit(url).path}: HTTP {error.code}') from error
|
||||||
|
|
||||||
|
def tag_commit(self, tag: str) -> str | None:
|
||||||
|
reference: dict | None = self.request('GET', f'git/ref/tags/{tag}', missing=True)
|
||||||
|
if reference is None:
|
||||||
|
return None
|
||||||
|
target: dict = reference['object']
|
||||||
|
for _ in range(8):
|
||||||
|
if target['type'] == 'commit':
|
||||||
|
return target['sha']
|
||||||
|
if target['type'] != 'tag':
|
||||||
|
break
|
||||||
|
target = self.request('GET', f"git/tags/{target['sha']}")['object']
|
||||||
|
raise ValueError(f'{tag} does not resolve to a commit')
|
||||||
|
|
||||||
|
def assets(self, release_id: int) -> list[dict]:
|
||||||
|
result: list[dict] = []
|
||||||
|
page: int = 1
|
||||||
|
while True:
|
||||||
|
batch: list[dict] = self.request('GET', f'releases/{release_id}/assets?per_page=100&page={page}')
|
||||||
|
result.extend(batch)
|
||||||
|
if len(batch) < 100:
|
||||||
|
return result
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_dependency(repository: str, reference: str, token: str) -> str:
|
||||||
|
commit: dict = GitHub(repository, token).request('GET', f'commits/{urllib.parse.quote(reference, safe="")}')
|
||||||
|
sha: str = commit['sha']
|
||||||
|
if not re.fullmatch(r'[a-f0-9]{40}', sha):
|
||||||
|
raise ValueError(f'{repository} did not resolve to a commit')
|
||||||
|
return sha
|
||||||
|
|
||||||
|
|
||||||
|
def release_metadata(existing: dict) -> dict:
|
||||||
|
match: re.Match | None = re.search(r'<!-- release: (.+) -->', existing.get('body') or '')
|
||||||
|
if match is None:
|
||||||
|
raise ValueError('Existing release has no publication identity; refusing to overwrite it')
|
||||||
|
return json.loads(match[1])
|
||||||
|
|
||||||
|
|
||||||
|
def verify_assets(existing: list[dict], assets: dict[str, bytes]) -> bool:
|
||||||
|
return {entry['name']: entry.get('digest') for entry in existing} == {
|
||||||
|
name: 'sha256:' + hashlib.sha256(data).hexdigest() for name, data in assets.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def publish(github: GitHub, config: dict, release: dict, assets: dict[str, bytes]) -> str:
|
||||||
|
tag: str = release['tag']
|
||||||
|
beta: bool = release['channel'] == 'beta'
|
||||||
|
existing: dict | None = github.request('GET', f'releases/tags/{tag}', missing=True)
|
||||||
|
commit: str | None = github.tag_commit(tag)
|
||||||
|
if not beta and commit is not None and commit != release['sha']:
|
||||||
|
raise ValueError(f'{tag} already points to another commit')
|
||||||
|
if existing is not None:
|
||||||
|
metadata: dict = release_metadata(existing)
|
||||||
|
if bool(existing['prerelease']) != beta:
|
||||||
|
raise ValueError('Existing release belongs to a different release channel')
|
||||||
|
if beta and (metadata['run'], metadata['attempt']) > (release['run'], release['attempt']):
|
||||||
|
return 'Skipped older beta run'
|
||||||
|
if not beta and metadata['sha'] != release['sha']:
|
||||||
|
raise ValueError('Stable release belongs to another commit')
|
||||||
|
if not existing['draft'] and commit == release['sha'] and metadata['version'] == release['version']:
|
||||||
|
if verify_assets(github.assets(existing['id']), assets):
|
||||||
|
return 'Release already published with identical artifacts'
|
||||||
|
if not beta:
|
||||||
|
raise ValueError('Published release artifacts differ; refusing to overwrite them')
|
||||||
|
if existing.get('immutable'):
|
||||||
|
raise ValueError('Rolling beta requires mutable releases in repository settings')
|
||||||
|
if not beta and not existing['draft']:
|
||||||
|
raise ValueError('Stable release is already published')
|
||||||
|
elif beta and commit is not None:
|
||||||
|
raise ValueError('Beta tag exists without a release; refusing to move it')
|
||||||
|
identity: dict = {key: release[key] for key in ('version', 'sha', 'run', 'attempt', 'volmlib_ref', 'wormholes_ref') if key in release}
|
||||||
|
platform: str = 'Modded' if config['platform'] == 'modded' else 'CraftBukkit'
|
||||||
|
body: str = (f"{platform} jars for {config['name']} {release['version']}.\n\n"
|
||||||
|
f"Commit: `{release['sha']}`\n\n"
|
||||||
|
f"<!-- release: {json.dumps(identity, sort_keys=True)} -->")
|
||||||
|
attributes: dict = {
|
||||||
|
'tag_name': tag, 'target_commitish': release['sha'],
|
||||||
|
'name': f"{config['name']} {platform} {'Beta' if beta else release['version']}",
|
||||||
|
'body': body, 'prerelease': beta, 'make_latest': 'false',
|
||||||
|
}
|
||||||
|
if existing is None:
|
||||||
|
existing = github.request('POST', 'releases', {**attributes, 'draft': True})
|
||||||
|
elif beta:
|
||||||
|
existing = github.request('PATCH', f"releases/{existing['id']}", {**attributes, 'draft': True})
|
||||||
|
uploaded: list[dict] = github.assets(existing['id'])
|
||||||
|
by_name: dict[str, dict] = {entry['name']: entry for entry in uploaded}
|
||||||
|
for name, data in assets.items():
|
||||||
|
digest: str = 'sha256:' + hashlib.sha256(data).hexdigest()
|
||||||
|
if name in by_name:
|
||||||
|
if by_name[name].get('state') == 'starter':
|
||||||
|
github.request('DELETE', f"releases/assets/{by_name[name]['id']}")
|
||||||
|
elif by_name[name].get('digest') != digest:
|
||||||
|
raise ValueError(f'Existing asset differs: {name}')
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
upload_url: str = existing['upload_url'].split('{')[0] + '?' + urllib.parse.urlencode({'name': name})
|
||||||
|
receipt: dict = github.request('POST', upload_url, data)
|
||||||
|
if receipt.get('digest') != digest or receipt['name'] != name:
|
||||||
|
raise ValueError(f'Upload verification failed: {name}')
|
||||||
|
for entry in uploaded:
|
||||||
|
if entry['name'] not in assets:
|
||||||
|
github.request('DELETE', f"releases/assets/{entry['id']}")
|
||||||
|
commit = github.tag_commit(tag)
|
||||||
|
if commit is None:
|
||||||
|
github.request('POST', 'git/refs', {'ref': f'refs/tags/{tag}', 'sha': release['sha']})
|
||||||
|
elif commit != release['sha']:
|
||||||
|
if not beta:
|
||||||
|
raise ValueError('Stable tag changed during publication')
|
||||||
|
github.request('PATCH', f'git/refs/tags/{tag}', {'sha': release['sha'], 'force': True})
|
||||||
|
if not beta and config['platform'] == 'bukkit':
|
||||||
|
latest: dict | None = github.request('GET', 'releases/latest', missing=True)
|
||||||
|
match: re.Match | None = re.match(r'^v?(\d+)\.(\d+)\.(\d+)(?:$|-)', latest['tag_name']) if latest else None
|
||||||
|
if latest is None or (match and tuple(map(int, release['version'].split('.'))) > tuple(map(int, match.groups()))):
|
||||||
|
attributes['make_latest'] = 'true'
|
||||||
|
github.request('PATCH', f"releases/{existing['id']}", {**attributes, 'draft': False})
|
||||||
|
return f"Published {tag} at {release['sha']}"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser: argparse.ArgumentParser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('command', choices=['plan', 'publish'])
|
||||||
|
parser.add_argument('--artifacts', type=Path)
|
||||||
|
arguments: argparse.Namespace = parser.parse_args()
|
||||||
|
root: Path = Path(__file__).resolve().parent.parent
|
||||||
|
config: dict = read_config(root)
|
||||||
|
event: dict = json.loads(Path(os.environ['GITHUB_EVENT_PATH']).read_text())
|
||||||
|
environment: dict = dict(os.environ)
|
||||||
|
if arguments.command == 'publish':
|
||||||
|
environment['GITHUB_RUN_ATTEMPT'] = environment['RELEASE_ATTEMPT']
|
||||||
|
release: dict = plan(root, config, event, environment)
|
||||||
|
if arguments.command == 'plan':
|
||||||
|
if release['channel'] != 'none':
|
||||||
|
release['volmlib_ref'] = resolve_dependency('VolmitSoftware/VolmLib', config['volmlibRef'], environment['GH_TOKEN'])
|
||||||
|
if release['wormholes_ref']:
|
||||||
|
release['wormholes_ref'] = resolve_dependency('VolmitSoftware/Wormholes', config['wormholesRef'], environment['GH_TOKEN'])
|
||||||
|
with Path(os.environ['GITHUB_OUTPUT']).open('a') as output:
|
||||||
|
for key, value in release.items():
|
||||||
|
output.write(f'{key}={json.dumps(value, separators=(",", ":")) if isinstance(value, dict) else value}\n')
|
||||||
|
print(f"Release channel: {release['channel']}")
|
||||||
|
elif release['channel'] != 'none':
|
||||||
|
if arguments.artifacts is None:
|
||||||
|
raise ValueError('Publication requires the complete artifact directory')
|
||||||
|
for key in ('volmlib_ref', 'wormholes_ref'):
|
||||||
|
if release[key]:
|
||||||
|
resolved: str = environment[f'RELEASE_{key.upper()}']
|
||||||
|
if not re.fullmatch(r'[a-f0-9]{40}', resolved):
|
||||||
|
raise ValueError(f'{key} is not a resolved dependency commit')
|
||||||
|
release[key] = resolved
|
||||||
|
github: GitHub = GitHub(os.environ['GITHUB_REPOSITORY'], os.environ['GH_TOKEN'])
|
||||||
|
print(publish(github, config, release, collect_artifacts(arguments.artifacts, release)))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except (ValueError, RuntimeError) as error:
|
||||||
|
print(f'::error::{error}', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
@@ -0,0 +1,718 @@
|
|||||||
|
import copy
|
||||||
|
import hashlib
|
||||||
|
import importlib.util
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
import urllib.parse
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
|
||||||
|
SPEC = importlib.util.spec_from_file_location('release', Path(__file__).with_name('release.py'))
|
||||||
|
release = importlib.util.module_from_spec(SPEC)
|
||||||
|
SPEC.loader.exec_module(release)
|
||||||
|
|
||||||
|
SHA = 'a' * 40
|
||||||
|
OTHER_SHA = 'b' * 40
|
||||||
|
|
||||||
|
|
||||||
|
def digest(data: bytes) -> str:
|
||||||
|
return 'sha256:' + hashlib.sha256(data).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
class FakeGitHub:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.tags: dict[str, str] = {}
|
||||||
|
self.releases: dict[int, dict] = {}
|
||||||
|
self.uploads: dict[int, dict[str, dict]] = {}
|
||||||
|
self.writes: list[tuple[str, str]] = []
|
||||||
|
self.latest: dict | None = None
|
||||||
|
self.fail_upload: str | None = None
|
||||||
|
self.fail_delete: int | None = None
|
||||||
|
self.invalid_receipt: bool = False
|
||||||
|
self.next_asset: int = 1
|
||||||
|
|
||||||
|
def seed(self, publication: dict, assets: dict[str, bytes], draft: bool = False,
|
||||||
|
immutable: bool = False) -> dict:
|
||||||
|
identity: dict = {key: publication[key] for key in ('sha', 'version', 'run', 'attempt')}
|
||||||
|
record: dict = {
|
||||||
|
'id': len(self.releases) + 1,
|
||||||
|
'tag_name': publication['tag'],
|
||||||
|
'body': '<!-- release: ' + json.dumps(identity) + ' -->',
|
||||||
|
'draft': draft,
|
||||||
|
'prerelease': publication['channel'] == 'beta',
|
||||||
|
'immutable': immutable,
|
||||||
|
}
|
||||||
|
record['upload_url'] = f"https://uploads.github.com/repos/example/product/releases/{record['id']}/assets{{?name,label}}"
|
||||||
|
self.releases[record['id']] = record
|
||||||
|
self.uploads[record['id']] = {}
|
||||||
|
for name, data in assets.items():
|
||||||
|
self.add_asset(record['id'], name, data)
|
||||||
|
self.tags[publication['tag']] = publication['sha']
|
||||||
|
return record
|
||||||
|
|
||||||
|
def add_asset(self, identifier: int, name: str, data: bytes) -> dict:
|
||||||
|
receipt: dict = {'id': self.next_asset, 'name': name, 'digest': digest(data)}
|
||||||
|
self.next_asset += 1
|
||||||
|
self.uploads[identifier][name] = receipt
|
||||||
|
return copy.deepcopy(receipt)
|
||||||
|
|
||||||
|
def tag_commit(self, tag: str) -> str | None:
|
||||||
|
return self.tags.get(tag)
|
||||||
|
|
||||||
|
def assets(self, identifier: int) -> list[dict]:
|
||||||
|
return copy.deepcopy(list(self.uploads[identifier].values()))
|
||||||
|
|
||||||
|
def request(self, method: str, path: str, payload: dict | bytes | None = None,
|
||||||
|
missing: bool = False) -> dict | list | None:
|
||||||
|
if method != 'GET':
|
||||||
|
self.writes.append((method, path))
|
||||||
|
if method == 'GET' and path.startswith('releases/tags/'):
|
||||||
|
tag: str = path.removeprefix('releases/tags/')
|
||||||
|
return copy.deepcopy(next((record for record in self.releases.values()
|
||||||
|
if record['tag_name'] == tag), None))
|
||||||
|
if method == 'GET' and path == 'releases/latest':
|
||||||
|
return copy.deepcopy(self.latest)
|
||||||
|
if method == 'POST' and path == 'releases':
|
||||||
|
identifier: int = len(self.releases) + 1
|
||||||
|
record: dict = copy.deepcopy(payload)
|
||||||
|
record.update({'id': identifier, 'immutable': False,
|
||||||
|
'upload_url': f'https://uploads.github.com/repos/example/product/releases/{identifier}/assets{{?name,label}}'})
|
||||||
|
self.releases[identifier] = record
|
||||||
|
self.uploads[identifier] = {}
|
||||||
|
return copy.deepcopy(record)
|
||||||
|
if method == 'POST' and path.startswith('https://uploads.github.com/'):
|
||||||
|
address: urllib.parse.SplitResult = urllib.parse.urlsplit(path)
|
||||||
|
name: str = urllib.parse.parse_qs(address.query)['name'][0]
|
||||||
|
if name == self.fail_upload:
|
||||||
|
raise RuntimeError('Upload unavailable')
|
||||||
|
identifier: int = int(address.path.split('/')[-2])
|
||||||
|
receipt: dict = self.add_asset(identifier, name, payload)
|
||||||
|
if self.invalid_receipt:
|
||||||
|
receipt['digest'] = digest(b'corrupted')
|
||||||
|
return receipt
|
||||||
|
if method == 'POST' and path == 'git/refs':
|
||||||
|
tag: str = payload['ref'].removeprefix('refs/tags/')
|
||||||
|
if tag in self.tags:
|
||||||
|
raise RuntimeError('Tag already exists')
|
||||||
|
self.tags[tag] = payload['sha']
|
||||||
|
return {'object': {'type': 'commit', 'sha': payload['sha']}}
|
||||||
|
if method == 'PATCH' and path.startswith('git/refs/tags/'):
|
||||||
|
self.tags[path.removeprefix('git/refs/tags/')] = payload['sha']
|
||||||
|
return None
|
||||||
|
if method == 'DELETE' and path.startswith('releases/assets/'):
|
||||||
|
identifier: int = int(path.rsplit('/', 1)[1])
|
||||||
|
if identifier == self.fail_delete:
|
||||||
|
raise RuntimeError('Delete unavailable')
|
||||||
|
for uploaded in self.uploads.values():
|
||||||
|
for name, entry in list(uploaded.items()):
|
||||||
|
if entry['id'] == identifier:
|
||||||
|
del uploaded[name]
|
||||||
|
return None
|
||||||
|
raise AssertionError(f'Unknown asset: {identifier}')
|
||||||
|
if method == 'PATCH' and path.startswith('releases/'):
|
||||||
|
identifier: int = int(path.rsplit('/', 1)[1])
|
||||||
|
self.releases[identifier].update(copy.deepcopy(payload))
|
||||||
|
if payload.get('make_latest') == 'true':
|
||||||
|
self.latest = copy.deepcopy(self.releases[identifier])
|
||||||
|
return copy.deepcopy(self.releases[identifier])
|
||||||
|
raise AssertionError(f'Unexpected API request: {method} {path}')
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseTestCase(unittest.TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
temporary: tempfile.TemporaryDirectory = tempfile.TemporaryDirectory()
|
||||||
|
self.addCleanup(temporary.cleanup)
|
||||||
|
self.root: Path = Path(temporary.name)
|
||||||
|
self.config: dict = {'name': 'Example', 'platform': 'bukkit', 'versionProperty': 'exampleVersion',
|
||||||
|
'betaTag': 'bukkit-beta', 'volmlibRef': 'c' * 40}
|
||||||
|
(self.root / 'gradle.properties').write_text('exampleVersion=1.2.3\n')
|
||||||
|
self.event: dict = {'repository': {'default_branch': 'main'}, 'ref': 'refs/heads/main',
|
||||||
|
'after': SHA, 'head_commit': {'id': SHA, 'message': 'V+ Publish'}}
|
||||||
|
self.environment: dict = {'GITHUB_EVENT_NAME': 'push', 'GITHUB_SHA': SHA,
|
||||||
|
'GITHUB_RUN_NUMBER': '17', 'GITHUB_RUN_ATTEMPT': '2'}
|
||||||
|
network = patch.object(release.urllib.request, 'urlopen', side_effect=AssertionError('Network is forbidden'))
|
||||||
|
network.start()
|
||||||
|
self.addCleanup(network.stop)
|
||||||
|
|
||||||
|
def publication(self, channel: str = 'stable', **changes) -> dict:
|
||||||
|
result: dict = {
|
||||||
|
'channel': channel, 'version': '1.2.3-beta.17.2' if channel == 'beta' else '1.2.3',
|
||||||
|
'tag': 'bukkit-beta' if channel == 'beta' else 'v1.2.3', 'sha': SHA, 'run': 17, 'attempt': 2,
|
||||||
|
'matrix': {'include': [{'id': 'bukkit', 'loader': 'bukkit', 'minecraft': ''}]},
|
||||||
|
}
|
||||||
|
result.update(changes)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def matrix(self, versions: tuple[str, ...] = ('1.20.1', '1.21.1')) -> list[dict]:
|
||||||
|
return [{'id': f'{loader}-{version}', 'loader': loader, 'minecraft': version}
|
||||||
|
for version in versions for loader in ('fabric', 'forge', 'neoforge')]
|
||||||
|
|
||||||
|
def artifact_tree(self, publication: dict, extra: tuple[str, ...] = ()) -> Path:
|
||||||
|
directory: Path = self.root / 'artifacts'
|
||||||
|
directory.mkdir(exist_ok=True)
|
||||||
|
descriptors: dict[str, str] = {'bukkit': 'plugin.yml', 'fabric': 'fabric.mod.json',
|
||||||
|
'forge': 'META-INF/mods.toml', 'neoforge': 'META-INF/neoforge.mods.toml'}
|
||||||
|
for target in publication['matrix']['include']:
|
||||||
|
folder: Path = directory / f"release-{target['id']}"
|
||||||
|
folder.mkdir()
|
||||||
|
loader: str = target['loader']
|
||||||
|
if loader == 'bukkit':
|
||||||
|
filename: str = f"Example-{publication['version']}-bukkit.jar"
|
||||||
|
metadata: str = f"name: Example\nversion: {publication['version']}\nmain: example.Plugin\n"
|
||||||
|
else:
|
||||||
|
display: str = {'fabric': 'Fabric', 'forge': 'Forge', 'neoforge': 'NeoForge'}[loader]
|
||||||
|
filename = f"Example v{publication['version']} [{display}] {target['minecraft']}+1.0.0.jar"
|
||||||
|
if loader == 'fabric':
|
||||||
|
metadata = json.dumps({'schemaVersion': 1, 'id': 'example', 'version': publication['version'],
|
||||||
|
'depends': {'minecraft': '~' + target['minecraft']}})
|
||||||
|
else:
|
||||||
|
metadata = (f"modLoader = 'javafml'\nloaderVersion = '[1,)'\nlicense = 'MIT'\n"
|
||||||
|
f"[[mods]]\nmodId = 'example'\nversion = '{publication['version']}'\n"
|
||||||
|
f"[[dependencies.example]]\nmodId = 'minecraft'\n"
|
||||||
|
f"versionRange = '[{target['minecraft']}]'\n")
|
||||||
|
path: Path = folder / filename
|
||||||
|
with zipfile.ZipFile(path, 'w') as archive:
|
||||||
|
archive.writestr(descriptors[loader], metadata)
|
||||||
|
for name in extra:
|
||||||
|
archive.writestr(name, '')
|
||||||
|
return directory
|
||||||
|
|
||||||
|
|
||||||
|
class PlanTests(ReleaseTestCase):
|
||||||
|
def test_markers_are_case_sensitive(self) -> None:
|
||||||
|
for message, channel in [('B+ Beta', 'beta'), ('V+ Release', 'stable'),
|
||||||
|
('b+ beta', 'none'), ('v+ release', 'none'), ('Fix a bug', 'none')]:
|
||||||
|
with self.subTest(message=message):
|
||||||
|
self.event['head_commit']['message'] = message
|
||||||
|
result: dict = release.plan(self.root, self.config, self.event, self.environment)
|
||||||
|
self.assertEqual(channel, result['channel'])
|
||||||
|
if channel == 'beta':
|
||||||
|
self.assertEqual('1.2.3-beta.17.2', result['version'])
|
||||||
|
self.assertEqual('bukkit-beta', result['tag'])
|
||||||
|
elif channel == 'stable':
|
||||||
|
self.assertEqual('1.2.3', result['version'])
|
||||||
|
self.assertEqual('v1.2.3', result['tag'])
|
||||||
|
|
||||||
|
def test_both_markers_fail(self) -> None:
|
||||||
|
self.event['head_commit']['message'] = 'B+ first\nV+ second'
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.plan(self.root, self.config, self.event, self.environment)
|
||||||
|
|
||||||
|
def test_only_pushed_default_branch_head_can_release(self) -> None:
|
||||||
|
cases: list[tuple[dict, dict]] = [
|
||||||
|
({'ref': 'refs/heads/feature'}, {}), ({'ref': 'refs/tags/V+1.2.3'}, {}),
|
||||||
|
({'deleted': True}, {}), ({'repository': {}}, {}),
|
||||||
|
({'head_commit': None}, {}), ({}, {'GITHUB_EVENT_NAME': 'pull_request'}),
|
||||||
|
]
|
||||||
|
for event_changes, environment_changes in cases:
|
||||||
|
with self.subTest(event=event_changes, environment=environment_changes):
|
||||||
|
self.assertEqual({'channel': 'none'}, release.plan(
|
||||||
|
self.root, self.config, {**self.event, **event_changes},
|
||||||
|
{**self.environment, **environment_changes}))
|
||||||
|
|
||||||
|
def test_head_identity_must_match_event_and_environment(self) -> None:
|
||||||
|
for field in ('after', 'commit', 'environment'):
|
||||||
|
with self.subTest(field=field):
|
||||||
|
event: dict = copy.deepcopy(self.event)
|
||||||
|
environment: dict = self.environment.copy()
|
||||||
|
if field == 'commit':
|
||||||
|
event['head_commit']['id'] = OTHER_SHA
|
||||||
|
elif field == 'environment':
|
||||||
|
environment['GITHUB_SHA'] = 'not-a-sha'
|
||||||
|
else:
|
||||||
|
event['after'] = OTHER_SHA
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.plan(self.root, self.config, event, environment)
|
||||||
|
|
||||||
|
def test_old_commits_do_not_trigger_release(self) -> None:
|
||||||
|
self.event['commits'] = [{'id': OTHER_SHA, 'message': 'V+ Release'}]
|
||||||
|
self.event['head_commit']['message'] = 'Fix a bug'
|
||||||
|
self.assertEqual({'channel': 'none'}, release.plan(self.root, self.config, self.event, self.environment))
|
||||||
|
|
||||||
|
def test_invalid_or_duplicate_versions_fail(self) -> None:
|
||||||
|
for value in ('', 'exampleVersion=01.2.3\n', 'exampleVersion=1.2\n',
|
||||||
|
'exampleVersion=1.2.3-beta.1\n', 'exampleVersion=1.2.3\nexampleVersion=2.3.4\n'):
|
||||||
|
with self.subTest(value=value):
|
||||||
|
(self.root / 'gradle.properties').write_text(value)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.plan(self.root, self.config, self.event, self.environment)
|
||||||
|
|
||||||
|
def test_invalid_run_identity_fails(self) -> None:
|
||||||
|
for key in ('GITHUB_RUN_NUMBER', 'GITHUB_RUN_ATTEMPT'):
|
||||||
|
with self.subTest(key=key), self.assertRaises(ValueError):
|
||||||
|
release.plan(self.root, self.config, self.event, {**self.environment, key: '0'})
|
||||||
|
|
||||||
|
def test_minecraft_file_matrix_contains_all_loaders(self) -> None:
|
||||||
|
(self.root / 'targets.json').write_text(json.dumps({'1.20.1': {}, '1.21.1': {}}))
|
||||||
|
config: dict = {**self.config, 'platform': 'modded', 'targets': 'targets.json'}
|
||||||
|
self.assertEqual(self.matrix(), release.targets(self.root, config))
|
||||||
|
|
||||||
|
def test_minecraft_directory_matrix_matches_files(self) -> None:
|
||||||
|
folder: Path = self.root / 'targets'
|
||||||
|
folder.mkdir()
|
||||||
|
(folder / '1.20.1.json').write_text(json.dumps({'minecraft': '1.20.1',
|
||||||
|
'loaders': ['fabric', 'forge', 'neoforge']}))
|
||||||
|
config: dict = {**self.config, 'platform': 'modded', 'targets': 'targets'}
|
||||||
|
self.assertEqual(self.matrix(('1.20.1',)), release.targets(self.root, config))
|
||||||
|
for minecraft, loaders in [('1.20.2', ['fabric', 'forge', 'neoforge']), ('1.20.1', ['fabric'])]:
|
||||||
|
with self.subTest(minecraft=minecraft, loaders=loaders):
|
||||||
|
(folder / '1.20.1.json').write_text(json.dumps({'minecraft': minecraft, 'loaders': loaders}))
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.targets(self.root, config)
|
||||||
|
|
||||||
|
def test_empty_or_invalid_minecraft_matrix_fails(self) -> None:
|
||||||
|
for content in ({}, {'invalid': {}}):
|
||||||
|
with self.subTest(content=content):
|
||||||
|
(self.root / 'targets.json').write_text(json.dumps(content))
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.targets(self.root, {**self.config, 'platform': 'modded', 'targets': 'targets.json'})
|
||||||
|
|
||||||
|
|
||||||
|
class ArtifactTests(ReleaseTestCase):
|
||||||
|
def test_complete_modded_matrix_and_checksums(self) -> None:
|
||||||
|
publication: dict = self.publication(matrix={'include': self.matrix()})
|
||||||
|
assets: dict[str, bytes] = release.collect_artifacts(self.artifact_tree(publication), publication)
|
||||||
|
self.assertEqual(7, len(assets))
|
||||||
|
checksums: bytes = assets.pop('SHA256SUMS-1.2.3.txt')
|
||||||
|
self.assertEqual(''.join(f'{hashlib.sha256(data).hexdigest()} {name}\n'
|
||||||
|
for name, data in sorted(assets.items())).encode(), checksums)
|
||||||
|
|
||||||
|
def test_missing_or_extra_matrix_target_fails(self) -> None:
|
||||||
|
publication: dict = self.publication()
|
||||||
|
directory: Path = self.artifact_tree(publication)
|
||||||
|
(directory / 'release-extra').mkdir()
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.collect_artifacts(directory, publication)
|
||||||
|
(directory / 'release-extra').rmdir()
|
||||||
|
(directory / 'release-bukkit').rename(directory / 'release-wrong')
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.collect_artifacts(directory, publication)
|
||||||
|
|
||||||
|
def test_intermediate_artifact_beside_runtime_fails(self) -> None:
|
||||||
|
publication: dict = self.publication()
|
||||||
|
directory: Path = self.artifact_tree(publication)
|
||||||
|
(directory / 'release-bukkit' / 'sources.jar').write_bytes(b'sources')
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.collect_artifacts(directory, publication)
|
||||||
|
|
||||||
|
def test_symbolic_link_artifacts_fail(self) -> None:
|
||||||
|
publication: dict = self.publication()
|
||||||
|
directory: Path = self.artifact_tree(publication)
|
||||||
|
path: Path = next((directory / 'release-bukkit').iterdir())
|
||||||
|
target: Path = self.root / path.name
|
||||||
|
path.rename(target)
|
||||||
|
path.symlink_to(target)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.collect_artifacts(directory, publication)
|
||||||
|
|
||||||
|
def test_bukkit_is_forbidden_in_modded_release(self) -> None:
|
||||||
|
publication: dict = self.publication(matrix={'include': self.matrix(('1.20.1',))})
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.collect_artifacts(self.artifact_tree(publication, ('plugin.yml',)), publication)
|
||||||
|
|
||||||
|
def test_velocity_is_forbidden_in_bukkit_release(self) -> None:
|
||||||
|
publication: dict = self.publication()
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.collect_artifacts(self.artifact_tree(publication, ('velocity-plugin.json',)), publication)
|
||||||
|
|
||||||
|
def test_modded_descriptor_is_forbidden_in_bukkit_release(self) -> None:
|
||||||
|
publication: dict = self.publication()
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.collect_artifacts(self.artifact_tree(publication, ('fabric.mod.json',)), publication)
|
||||||
|
|
||||||
|
def test_required_platform_descriptor_is_checked(self) -> None:
|
||||||
|
publication: dict = self.publication()
|
||||||
|
directory: Path = self.artifact_tree(publication)
|
||||||
|
path: Path = next((directory / 'release-bukkit').iterdir())
|
||||||
|
with zipfile.ZipFile(path, 'w') as archive:
|
||||||
|
archive.writestr('fabric.mod.json', '{}')
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.collect_artifacts(directory, publication)
|
||||||
|
|
||||||
|
def test_wrong_version_and_embedded_version_suffix_fail(self) -> None:
|
||||||
|
publication: dict = self.publication()
|
||||||
|
directory: Path = self.artifact_tree(publication)
|
||||||
|
path: Path = next((directory / 'release-bukkit').iterdir())
|
||||||
|
for version in ('9.8.7', '11.2.3'):
|
||||||
|
with self.subTest(version=version):
|
||||||
|
changed: Path = path.with_name(f'Example-{version}-bukkit.jar')
|
||||||
|
path.rename(changed)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.collect_artifacts(directory, publication)
|
||||||
|
changed.rename(path)
|
||||||
|
|
||||||
|
def test_descriptor_version_must_match_release(self) -> None:
|
||||||
|
publication: dict = self.publication()
|
||||||
|
directory: Path = self.artifact_tree(publication)
|
||||||
|
path: Path = next((directory / 'release-bukkit').iterdir())
|
||||||
|
with zipfile.ZipFile(path, 'w') as archive:
|
||||||
|
archive.writestr('plugin.yml', 'name: Example\nversion: 1.2.4\nmain: example.Plugin\n')
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.collect_artifacts(directory, publication)
|
||||||
|
|
||||||
|
def test_modded_descriptor_versions_must_match_release(self) -> None:
|
||||||
|
publication: dict = self.publication(matrix={'include': self.matrix(('1.20.1',))})
|
||||||
|
directory: Path = self.artifact_tree(publication)
|
||||||
|
for folder in directory.iterdir():
|
||||||
|
with self.subTest(target=folder.name):
|
||||||
|
path: Path = next(folder.iterdir())
|
||||||
|
original: bytes = path.read_bytes()
|
||||||
|
with zipfile.ZipFile(path) as archive:
|
||||||
|
descriptor: str = archive.namelist()[0]
|
||||||
|
contents: str = archive.read(descriptor).decode().replace('1.2.3', '1.2.4')
|
||||||
|
with zipfile.ZipFile(path, 'w') as archive:
|
||||||
|
archive.writestr(descriptor, contents)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.collect_artifacts(directory, publication)
|
||||||
|
path.write_bytes(original)
|
||||||
|
|
||||||
|
def test_modded_filename_must_match_minecraft_target(self) -> None:
|
||||||
|
publication: dict = self.publication(matrix={'include': self.matrix(('1.20.1',))})
|
||||||
|
directory: Path = self.artifact_tree(publication)
|
||||||
|
path: Path = next((directory / 'release-fabric-1.20.1').iterdir())
|
||||||
|
path.rename(path.with_name(path.name.replace('1.20.1', '1.21.1')))
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.collect_artifacts(directory, publication)
|
||||||
|
|
||||||
|
def test_modded_metadata_must_match_minecraft_target(self) -> None:
|
||||||
|
publication: dict = self.publication(matrix={'include': self.matrix(('1.20.1',))})
|
||||||
|
directory: Path = self.artifact_tree(publication)
|
||||||
|
for folder in directory.iterdir():
|
||||||
|
with self.subTest(target=folder.name):
|
||||||
|
path: Path = next(folder.iterdir())
|
||||||
|
original: bytes = path.read_bytes()
|
||||||
|
with zipfile.ZipFile(path) as archive:
|
||||||
|
descriptor: str = archive.namelist()[0]
|
||||||
|
contents: str = archive.read(descriptor).decode().replace('1.20.1', '1.21.1')
|
||||||
|
with zipfile.ZipFile(path, 'w') as archive:
|
||||||
|
archive.writestr(descriptor, contents)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.collect_artifacts(directory, publication)
|
||||||
|
path.write_bytes(original)
|
||||||
|
|
||||||
|
|
||||||
|
class PublicationTests(ReleaseTestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
super().setUp()
|
||||||
|
self.github: FakeGitHub = FakeGitHub()
|
||||||
|
self.assets: dict[str, bytes] = {'Example-1.2.3-bukkit.jar': b'jar', 'SHA256SUMS-1.2.3.txt': b'checksums'}
|
||||||
|
|
||||||
|
def test_new_stable_release_is_published_at_head(self) -> None:
|
||||||
|
release.publish(self.github, self.config, self.publication(), self.assets)
|
||||||
|
record: dict = self.github.releases[1]
|
||||||
|
self.assertEqual(SHA, self.github.tags['v1.2.3'])
|
||||||
|
self.assertFalse(record['draft'])
|
||||||
|
self.assertFalse(record['prerelease'])
|
||||||
|
self.assertEqual('true', record['make_latest'])
|
||||||
|
self.assertEqual(set(self.assets), set(self.github.uploads[1]))
|
||||||
|
|
||||||
|
def test_stable_existing_tag_collision_has_no_writes(self) -> None:
|
||||||
|
self.github.tags['v1.2.3'] = OTHER_SHA
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.publish(self.github, self.config, self.publication(), self.assets)
|
||||||
|
self.assertEqual([], self.github.writes)
|
||||||
|
self.assertEqual(OTHER_SHA, self.github.tags['v1.2.3'])
|
||||||
|
|
||||||
|
def test_stable_same_content_rerun_has_no_writes(self) -> None:
|
||||||
|
self.github.seed(self.publication(), self.assets, immutable=True)
|
||||||
|
result: str = release.publish(self.github, self.config, self.publication(), self.assets)
|
||||||
|
self.assertIn('already published', result)
|
||||||
|
self.assertEqual([], self.github.writes)
|
||||||
|
|
||||||
|
def test_published_stable_artifacts_cannot_be_replaced(self) -> None:
|
||||||
|
self.github.seed(self.publication(), self.assets, immutable=True)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.publish(self.github, self.config, self.publication(),
|
||||||
|
{**self.assets, 'Example-1.2.3-bukkit.jar': b'different'})
|
||||||
|
self.assertEqual([], self.github.writes)
|
||||||
|
|
||||||
|
def test_stable_release_identity_collision_has_no_writes(self) -> None:
|
||||||
|
self.github.seed(self.publication(sha=OTHER_SHA), self.assets)
|
||||||
|
self.github.tags['v1.2.3'] = SHA
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.publish(self.github, self.config, self.publication(), self.assets)
|
||||||
|
self.assertEqual([], self.github.writes)
|
||||||
|
|
||||||
|
def test_stable_release_cannot_reuse_published_prerelease(self) -> None:
|
||||||
|
record: dict = self.github.seed(self.publication(), self.assets)
|
||||||
|
record['prerelease'] = True
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.publish(self.github, self.config, self.publication(), self.assets)
|
||||||
|
self.assertEqual([], self.github.writes)
|
||||||
|
|
||||||
|
def test_partially_uploaded_stable_draft_resumes(self) -> None:
|
||||||
|
record: dict = self.github.seed(self.publication(), {'Example-1.2.3-bukkit.jar': b'jar'}, draft=True)
|
||||||
|
original_id: int = self.github.uploads[record['id']]['Example-1.2.3-bukkit.jar']['id']
|
||||||
|
release.publish(self.github, self.config, self.publication(), self.assets)
|
||||||
|
self.assertFalse(self.github.releases[record['id']]['draft'])
|
||||||
|
self.assertEqual(original_id, self.github.uploads[record['id']]['Example-1.2.3-bukkit.jar']['id'])
|
||||||
|
self.assertEqual(set(self.assets), set(self.github.uploads[record['id']]))
|
||||||
|
self.assertEqual(1, len(self.github.releases))
|
||||||
|
|
||||||
|
def test_conflicting_draft_asset_fails_without_replacement(self) -> None:
|
||||||
|
self.github.seed(self.publication(), {'Example-1.2.3-bukkit.jar': b'other'}, draft=True)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.publish(self.github, self.config, self.publication(), self.assets)
|
||||||
|
self.assertEqual([], self.github.writes)
|
||||||
|
self.assertTrue(self.github.releases[1]['draft'])
|
||||||
|
|
||||||
|
def test_newer_beta_run_or_attempt_is_preserved(self) -> None:
|
||||||
|
for run, attempt in ((18, 1), (17, 3)):
|
||||||
|
with self.subTest(run=run, attempt=attempt):
|
||||||
|
github: FakeGitHub = FakeGitHub()
|
||||||
|
github.seed(self.publication('beta', run=run, attempt=attempt, sha=OTHER_SHA), self.assets)
|
||||||
|
result: str = release.publish(github, self.config, self.publication('beta'), self.assets)
|
||||||
|
self.assertIn('Skipped', result)
|
||||||
|
self.assertEqual([], github.writes)
|
||||||
|
self.assertEqual(OTHER_SHA, github.tags['bukkit-beta'])
|
||||||
|
|
||||||
|
def test_successful_beta_replaces_assets_and_remains_prerelease(self) -> None:
|
||||||
|
previous: dict = self.publication('beta', run=16, attempt=1, sha=OTHER_SHA, version='1.2.3-beta.16.1')
|
||||||
|
record: dict = self.github.seed(previous, {'old.jar': b'old'})
|
||||||
|
release.publish(self.github, self.config, self.publication('beta'), self.assets)
|
||||||
|
current: dict = self.github.releases[record['id']]
|
||||||
|
self.assertEqual(SHA, self.github.tags['bukkit-beta'])
|
||||||
|
self.assertTrue(current['prerelease'])
|
||||||
|
self.assertFalse(current['draft'])
|
||||||
|
self.assertEqual('false', current['make_latest'])
|
||||||
|
self.assertEqual(set(self.assets), set(self.github.uploads[record['id']]))
|
||||||
|
|
||||||
|
def test_failed_beta_upload_preserves_tag_and_assets_in_recoverable_draft(self) -> None:
|
||||||
|
previous: dict = self.publication('beta', run=16, attempt=1, sha=OTHER_SHA, version='1.2.3-beta.16.1')
|
||||||
|
record: dict = self.github.seed(previous, {'old.jar': b'old'})
|
||||||
|
self.github.fail_upload = 'SHA256SUMS-1.2.3.txt'
|
||||||
|
with self.assertRaises(RuntimeError):
|
||||||
|
release.publish(self.github, self.config, self.publication('beta'), self.assets)
|
||||||
|
self.assertEqual(OTHER_SHA, self.github.tags['bukkit-beta'])
|
||||||
|
self.assertTrue(self.github.releases[record['id']]['draft'])
|
||||||
|
self.assertEqual(17, release.release_metadata(self.github.releases[record['id']])['run'])
|
||||||
|
self.assertIn('old.jar', self.github.uploads[record['id']])
|
||||||
|
self.assertFalse(any(method == 'DELETE' or path.startswith('git/refs')
|
||||||
|
for method, path in self.github.writes))
|
||||||
|
|
||||||
|
def test_failed_beta_cleanup_resumes_without_reuploading_verified_assets(self) -> None:
|
||||||
|
previous: dict = self.publication('beta', run=16, attempt=1, sha=OTHER_SHA, version='1.2.3-beta.16.1')
|
||||||
|
record: dict = self.github.seed(previous, {'old.jar': b'old'})
|
||||||
|
self.github.fail_delete = self.github.uploads[record['id']]['old.jar']['id']
|
||||||
|
with self.assertRaises(RuntimeError):
|
||||||
|
release.publish(self.github, self.config, self.publication('beta'), self.assets)
|
||||||
|
self.assertTrue(self.github.releases[record['id']]['draft'])
|
||||||
|
self.assertEqual(OTHER_SHA, self.github.tags['bukkit-beta'])
|
||||||
|
uploaded_ids: dict[str, int] = {name: self.github.uploads[record['id']][name]['id'] for name in self.assets}
|
||||||
|
self.github.fail_delete = None
|
||||||
|
release.publish(self.github, self.config, self.publication('beta'), self.assets)
|
||||||
|
self.assertFalse(self.github.releases[record['id']]['draft'])
|
||||||
|
self.assertEqual(SHA, self.github.tags['bukkit-beta'])
|
||||||
|
self.assertEqual(uploaded_ids, {name: entry['id'] for name, entry in self.github.uploads[record['id']].items()})
|
||||||
|
|
||||||
|
def test_incomplete_newer_beta_draft_blocks_older_run(self) -> None:
|
||||||
|
self.github.seed(self.publication('beta', run=18, sha=OTHER_SHA), {}, draft=True)
|
||||||
|
result: str = release.publish(self.github, self.config, self.publication('beta'), self.assets)
|
||||||
|
self.assertIn('Skipped', result)
|
||||||
|
self.assertEqual([], self.github.writes)
|
||||||
|
self.assertTrue(self.github.releases[1]['draft'])
|
||||||
|
|
||||||
|
def test_starter_upload_in_draft_is_replaced_before_publication(self) -> None:
|
||||||
|
self.github.seed(self.publication(), {'Example-1.2.3-bukkit.jar': b''}, draft=True)
|
||||||
|
starter: dict = self.github.uploads[1]['Example-1.2.3-bukkit.jar']
|
||||||
|
identifier: int = starter['id']
|
||||||
|
starter.update({'state': 'starter', 'digest': None})
|
||||||
|
release.publish(self.github, self.config, self.publication(), self.assets)
|
||||||
|
self.assertFalse(self.github.releases[1]['draft'])
|
||||||
|
self.assertNotEqual(identifier, self.github.uploads[1]['Example-1.2.3-bukkit.jar']['id'])
|
||||||
|
self.assertEqual(digest(b'jar'), self.github.uploads[1]['Example-1.2.3-bukkit.jar']['digest'])
|
||||||
|
|
||||||
|
def test_invalid_upload_digest_does_not_create_tag_or_publish(self) -> None:
|
||||||
|
self.github.invalid_receipt = True
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.publish(self.github, self.config, self.publication(), self.assets)
|
||||||
|
self.assertEqual({}, self.github.tags)
|
||||||
|
self.assertTrue(self.github.releases[1]['draft'])
|
||||||
|
|
||||||
|
def test_beta_will_not_replace_stable_or_immutable_release(self) -> None:
|
||||||
|
for immutable, prerelease in ((True, True), (False, False)):
|
||||||
|
with self.subTest(immutable=immutable, prerelease=prerelease):
|
||||||
|
github: FakeGitHub = FakeGitHub()
|
||||||
|
record: dict = github.seed(self.publication('beta', run=16, sha=OTHER_SHA), self.assets,
|
||||||
|
immutable=immutable)
|
||||||
|
record['prerelease'] = prerelease
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.publish(github, self.config, self.publication('beta'), self.assets)
|
||||||
|
self.assertEqual([], github.writes)
|
||||||
|
|
||||||
|
def test_unowned_existing_release_is_preserved(self) -> None:
|
||||||
|
record: dict = self.github.seed(self.publication(), self.assets)
|
||||||
|
record['body'] = 'Published manually'
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.publish(self.github, self.config, self.publication(), self.assets)
|
||||||
|
self.assertEqual([], self.github.writes)
|
||||||
|
|
||||||
|
def test_beta_tag_without_release_is_preserved(self) -> None:
|
||||||
|
self.github.tags['bukkit-beta'] = OTHER_SHA
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.publish(self.github, self.config, self.publication('beta'), self.assets)
|
||||||
|
self.assertEqual([], self.github.writes)
|
||||||
|
|
||||||
|
def test_modded_stable_release_never_changes_latest(self) -> None:
|
||||||
|
self.github.latest = {'tag_name': 'v1.0.0'}
|
||||||
|
release.publish(self.github, {**self.config, 'platform': 'modded'}, self.publication(), self.assets)
|
||||||
|
self.assertFalse(self.github.releases[1]['prerelease'])
|
||||||
|
self.assertEqual('false', self.github.releases[1]['make_latest'])
|
||||||
|
self.assertEqual('v1.0.0', self.github.latest['tag_name'])
|
||||||
|
|
||||||
|
def test_bukkit_backport_does_not_replace_newer_latest(self) -> None:
|
||||||
|
for latest in ('v1.3.0', 'v2.0.0', 'v1.2.3'):
|
||||||
|
with self.subTest(latest=latest):
|
||||||
|
github: FakeGitHub = FakeGitHub()
|
||||||
|
github.latest = {'tag_name': latest}
|
||||||
|
release.publish(github, self.config, self.publication(), self.assets)
|
||||||
|
self.assertEqual('false', github.releases[1]['make_latest'])
|
||||||
|
self.assertEqual(latest, github.latest['tag_name'])
|
||||||
|
|
||||||
|
def test_newer_bukkit_release_becomes_latest(self) -> None:
|
||||||
|
self.github.latest = {'tag_name': 'v1.2.2'}
|
||||||
|
release.publish(self.github, self.config, self.publication(), self.assets)
|
||||||
|
self.assertEqual('true', self.github.releases[1]['make_latest'])
|
||||||
|
self.assertEqual('v1.2.3', self.github.latest['tag_name'])
|
||||||
|
|
||||||
|
|
||||||
|
class GitHubReadTests(ReleaseTestCase):
|
||||||
|
def test_annotated_tag_resolves_to_commit(self) -> None:
|
||||||
|
github: release.GitHub = release.GitHub('example/product', 'test-token')
|
||||||
|
responses: dict[str, dict] = {
|
||||||
|
'git/ref/tags/v1.2.3': {'object': {'type': 'tag', 'sha': OTHER_SHA}},
|
||||||
|
f'git/tags/{OTHER_SHA}': {'object': {'type': 'commit', 'sha': SHA}},
|
||||||
|
}
|
||||||
|
with patch.object(github, 'request', side_effect=lambda method, path, **options: responses[path]):
|
||||||
|
self.assertEqual(SHA, github.tag_commit('v1.2.3'))
|
||||||
|
|
||||||
|
def test_noncommit_tag_cannot_be_published(self) -> None:
|
||||||
|
github: release.GitHub = release.GitHub('example/product', 'test-token')
|
||||||
|
with patch.object(github, 'request', return_value={'object': {'type': 'tree', 'sha': SHA}}):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
github.tag_commit('v1.2.3')
|
||||||
|
|
||||||
|
def test_assets_include_all_pages(self) -> None:
|
||||||
|
github: release.GitHub = release.GitHub('example/product', 'test-token')
|
||||||
|
first: list[dict] = [{'id': number, 'name': f'{number}.jar'} for number in range(100)]
|
||||||
|
last: list[dict] = [{'id': 100, 'name': 'last.jar'}]
|
||||||
|
with patch.object(github, 'request', side_effect=[first, last]):
|
||||||
|
self.assertEqual(first + last, github.assets(1))
|
||||||
|
|
||||||
|
def test_invalid_repository_is_rejected(self) -> None:
|
||||||
|
for repository in ('example', '../example/product', 'example/product/extra'):
|
||||||
|
with self.subTest(repository=repository), self.assertRaises(ValueError):
|
||||||
|
release.GitHub(repository, 'test-token')
|
||||||
|
|
||||||
|
|
||||||
|
class DependencyResolutionTests(ReleaseTestCase):
|
||||||
|
def test_branch_resolves_to_full_commit_sha(self) -> None:
|
||||||
|
with patch.object(release.GitHub, 'request', return_value={'sha': SHA}) as request:
|
||||||
|
self.assertEqual(SHA, release.resolve_dependency('example/product', 'master', 'test-token'))
|
||||||
|
request.assert_called_once_with('GET', 'commits/master')
|
||||||
|
|
||||||
|
def test_full_commit_reference_is_verified(self) -> None:
|
||||||
|
with patch.object(release.GitHub, 'request', return_value={'sha': SHA}) as request:
|
||||||
|
self.assertEqual(SHA, release.resolve_dependency('example/product', SHA, 'test-token'))
|
||||||
|
request.assert_called_once_with('GET', f'commits/{SHA}')
|
||||||
|
|
||||||
|
def test_incomplete_or_invalid_resolved_sha_fails(self) -> None:
|
||||||
|
for sha in ('', 'a' * 39, 'a' * 41, 'A' * 40, 'g' * 40, 'master'):
|
||||||
|
with self.subTest(sha=sha), patch.object(release.GitHub, 'request', return_value={'sha': sha}):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.resolve_dependency('example/product', 'master', 'test-token')
|
||||||
|
|
||||||
|
def test_reference_is_encoded_as_single_url_path_component(self) -> None:
|
||||||
|
with patch.object(release.GitHub, 'request', return_value={'sha': SHA}) as request:
|
||||||
|
self.assertEqual(SHA, release.resolve_dependency('example/product', 'feature/next+release #?', 'test-token'))
|
||||||
|
request.assert_called_once_with('GET', 'commits/feature%2Fnext%2Brelease%20%23%3F')
|
||||||
|
|
||||||
|
|
||||||
|
class MainTests(ReleaseTestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
super().setUp()
|
||||||
|
self.config.update({'volmlibRef': 'master', 'wormholesRef': 'release/api'})
|
||||||
|
(self.root / '.github').mkdir()
|
||||||
|
(self.root / '.github/release.json').write_text(json.dumps(self.config))
|
||||||
|
self.event['head_commit']['message'] = 'B+ Publish beta'
|
||||||
|
(self.root / 'event.json').write_text(json.dumps(self.event))
|
||||||
|
self.environment.update({
|
||||||
|
'GITHUB_EVENT_PATH': str(self.root / 'event.json'),
|
||||||
|
'GITHUB_OUTPUT': str(self.root / 'output.txt'),
|
||||||
|
'GITHUB_REPOSITORY': 'example/product',
|
||||||
|
'GH_TOKEN': 'test-token',
|
||||||
|
})
|
||||||
|
script = patch.object(release, '__file__', str(self.root / '.github/release.py'))
|
||||||
|
script.start()
|
||||||
|
self.addCleanup(script.stop)
|
||||||
|
output = patch('sys.stdout', new=io.StringIO())
|
||||||
|
output.start()
|
||||||
|
self.addCleanup(output.stop)
|
||||||
|
|
||||||
|
def test_plan_freezes_each_dependency_branch_once(self) -> None:
|
||||||
|
with patch.dict(release.os.environ, self.environment, clear=True), \
|
||||||
|
patch.object(release.sys, 'argv', ['release.py', 'plan']), \
|
||||||
|
patch.object(release.GitHub, 'request', side_effect=[{'sha': SHA}, {'sha': OTHER_SHA}]) as request:
|
||||||
|
release.main()
|
||||||
|
outputs: dict[str, str] = dict(line.split('=', 1) for line in (self.root / 'output.txt').read_text().splitlines())
|
||||||
|
self.assertEqual(SHA, outputs['volmlib_ref'])
|
||||||
|
self.assertEqual(OTHER_SHA, outputs['wormholes_ref'])
|
||||||
|
self.assertEqual('1.2.3-beta.17.2', outputs['version'])
|
||||||
|
self.assertEqual('2', outputs['attempt'])
|
||||||
|
self.assertEqual(2, request.call_count)
|
||||||
|
self.assertEqual(['commits/master', 'commits/release%2Fapi'], [call.args[1] for call in request.call_args_list])
|
||||||
|
|
||||||
|
def test_unmarked_push_never_resolves_dependencies(self) -> None:
|
||||||
|
self.event['head_commit']['message'] = 'Fix a bug'
|
||||||
|
(self.root / 'event.json').write_text(json.dumps(self.event))
|
||||||
|
with patch.dict(release.os.environ, self.environment, clear=True), \
|
||||||
|
patch.object(release.sys, 'argv', ['release.py', 'plan']), \
|
||||||
|
patch.object(release, 'resolve_dependency', side_effect=AssertionError('Unexpected dependency lookup')):
|
||||||
|
release.main()
|
||||||
|
self.assertEqual('channel=none\n', (self.root / 'output.txt').read_text())
|
||||||
|
|
||||||
|
def test_publish_rerun_uses_original_planned_attempt_and_dependency_commits(self) -> None:
|
||||||
|
publication: dict = self.publication('beta')
|
||||||
|
artifacts: Path = self.artifact_tree(publication)
|
||||||
|
github: FakeGitHub = FakeGitHub()
|
||||||
|
self.environment.update({'GITHUB_RUN_ATTEMPT': '3', 'RELEASE_ATTEMPT': '2',
|
||||||
|
'RELEASE_VOLMLIB_REF': SHA, 'RELEASE_WORMHOLES_REF': OTHER_SHA})
|
||||||
|
with patch.dict(release.os.environ, self.environment, clear=True), \
|
||||||
|
patch.object(release.sys, 'argv', ['release.py', 'publish', '--artifacts', str(artifacts)]), \
|
||||||
|
patch.object(release, 'GitHub', return_value=github), \
|
||||||
|
patch.object(release, 'resolve_dependency', side_effect=AssertionError('Dependencies must stay frozen')):
|
||||||
|
release.main()
|
||||||
|
metadata: dict = release.release_metadata(github.releases[1])
|
||||||
|
self.assertEqual('1.2.3-beta.17.2', metadata['version'])
|
||||||
|
self.assertEqual(2, metadata['attempt'])
|
||||||
|
self.assertEqual(SHA, metadata['volmlib_ref'])
|
||||||
|
self.assertEqual(OTHER_SHA, metadata['wormholes_ref'])
|
||||||
|
self.assertFalse(github.releases[1]['draft'])
|
||||||
|
self.assertEqual({'Example-1.2.3-beta.17.2-bukkit.jar', 'SHA256SUMS-1.2.3-beta.17.2.txt'},
|
||||||
|
set(github.uploads[1]))
|
||||||
|
writes: list[tuple[str, str]] = github.writes.copy()
|
||||||
|
release.os.environ['GITHUB_RUN_ATTEMPT'] = '4'
|
||||||
|
release.main()
|
||||||
|
self.assertEqual(writes, github.writes)
|
||||||
|
|
||||||
|
def test_publish_rejects_unresolved_dependency_reference_before_writing(self) -> None:
|
||||||
|
artifacts: Path = self.artifact_tree(self.publication('beta'))
|
||||||
|
github: FakeGitHub = FakeGitHub()
|
||||||
|
self.environment.update({'RELEASE_ATTEMPT': '2', 'RELEASE_VOLMLIB_REF': SHA,
|
||||||
|
'RELEASE_WORMHOLES_REF': OTHER_SHA})
|
||||||
|
for key in ('RELEASE_VOLMLIB_REF', 'RELEASE_WORMHOLES_REF'):
|
||||||
|
with self.subTest(key=key), \
|
||||||
|
patch.dict(release.os.environ, {**self.environment, key: 'master'}, clear=True), \
|
||||||
|
patch.object(release.sys, 'argv', ['release.py', 'publish', '--artifacts', str(artifacts)]), \
|
||||||
|
patch.object(release, 'GitHub', return_value=github):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
release.main()
|
||||||
|
self.assertEqual([], github.writes)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['**']
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '.github/**'
|
||||||
|
- '**/*.gradle'
|
||||||
|
- '**/gradle.properties'
|
||||||
|
- 'gradle/**/*.json'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
PYTHONDONTWRITEBYTECODE: '1'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Test release handling
|
||||||
|
run: python3 -m unittest discover -s .github -p 'test_release.py' -v
|
||||||
|
|
||||||
|
plan:
|
||||||
|
needs: validate
|
||||||
|
if: github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
channel: ${{ steps.release.outputs.channel }}
|
||||||
|
version: ${{ steps.release.outputs.version }}
|
||||||
|
attempt: ${{ steps.release.outputs.attempt }}
|
||||||
|
version_property: ${{ steps.release.outputs.version_property }}
|
||||||
|
matrix: ${{ steps.release.outputs.matrix }}
|
||||||
|
volmlib_ref: ${{ steps.release.outputs.volmlib_ref }}
|
||||||
|
wormholes_ref: ${{ steps.release.outputs.wormholes_ref }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Select release channel and targets
|
||||||
|
id: release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: python3 .github/release.py plan
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: plan
|
||||||
|
if: needs.plan.outputs.channel != 'none'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 120
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
max-parallel: 3
|
||||||
|
matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}
|
||||||
|
env:
|
||||||
|
ORG_GRADLE_PROJECT_localVolmLibDirectory: ${{ github.workspace }}/VolmLib
|
||||||
|
RELEASE_VERSION: ${{ needs.plan.outputs.version }}
|
||||||
|
VERSION_PROPERTY: ${{ needs.plan.outputs.version_property }}
|
||||||
|
RELEASE_LOADER: ${{ matrix.loader }}
|
||||||
|
MINECRAFT_VERSION: ${{ matrix.minecraft }}
|
||||||
|
steps:
|
||||||
|
- name: Check out release sources
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||||
|
with:
|
||||||
|
path: project
|
||||||
|
submodules: recursive
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Check out VolmLib
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||||
|
with:
|
||||||
|
repository: VolmitSoftware/VolmLib
|
||||||
|
ref: ${{ needs.plan.outputs.volmlib_ref }}
|
||||||
|
path: VolmLib
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Check out Wormholes API sources
|
||||||
|
if: needs.plan.outputs.wormholes_ref != ''
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||||
|
with:
|
||||||
|
repository: VolmitSoftware/Wormholes
|
||||||
|
ref: ${{ needs.plan.outputs.wormholes_ref }}
|
||||||
|
path: WormholesPlugin
|
||||||
|
submodules: recursive
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Set up Java 21 toolchain
|
||||||
|
uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: '21'
|
||||||
|
- name: Set up Java 25
|
||||||
|
uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: '25'
|
||||||
|
- uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6
|
||||||
|
- name: Build Wormholes API
|
||||||
|
if: needs.plan.outputs.wormholes_ref != ''
|
||||||
|
working-directory: WormholesPlugin
|
||||||
|
run: ./gradlew apiJar --no-daemon --max-workers=2 '-Dorg.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8'
|
||||||
|
- name: Locate Wormholes API
|
||||||
|
if: needs.plan.outputs.wormholes_ref != ''
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
api_jars=(WormholesPlugin/build/libs/*-api.jar)
|
||||||
|
test "${#api_jars[@]}" -eq 1
|
||||||
|
test -f "${api_jars[0]}"
|
||||||
|
echo "ORG_GRADLE_PROJECT_wormholesApiJar=$PWD/${api_jars[0]}" >> "$GITHUB_ENV"
|
||||||
|
- name: Build and verify release jar
|
||||||
|
working-directory: project
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
arguments=(prepareRelease --no-daemon --max-workers=2
|
||||||
|
'-Dorg.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8'
|
||||||
|
"-P$VERSION_PROPERTY=$RELEASE_VERSION")
|
||||||
|
if [[ "$RELEASE_LOADER" != bukkit ]]; then
|
||||||
|
arguments+=("-PreleaseLoader=$RELEASE_LOADER" "-PminecraftVersion=$MINECRAFT_VERSION"
|
||||||
|
-PmoddedBuildParallelism=1 -PmoddedTestForks=1)
|
||||||
|
fi
|
||||||
|
./gradlew "${arguments[@]}"
|
||||||
|
- name: Upload verified jar
|
||||||
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9
|
||||||
|
with:
|
||||||
|
name: release-${{ matrix.id }}
|
||||||
|
path: project/build/release/*.jar
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 7
|
||||||
|
compression-level: 0
|
||||||
|
|
||||||
|
publish:
|
||||||
|
needs: [plan, build]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
concurrency:
|
||||||
|
group: release-publication
|
||||||
|
cancel-in-progress: false
|
||||||
|
queue: max
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Download complete release matrix
|
||||||
|
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333
|
||||||
|
with:
|
||||||
|
pattern: release-*
|
||||||
|
path: build/release-artifacts
|
||||||
|
- name: Publish verified release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
RELEASE_ATTEMPT: ${{ needs.plan.outputs.attempt }}
|
||||||
|
RELEASE_VOLMLIB_REF: ${{ needs.plan.outputs.volmlib_ref }}
|
||||||
|
RELEASE_WORMHOLES_REF: ${{ needs.plan.outputs.wormholes_ref }}
|
||||||
|
run: python3 .github/release.py publish --artifacts build/release-artifacts
|
||||||
@@ -76,3 +76,8 @@ BUILDS/
|
|||||||
.compatwt/
|
.compatwt/
|
||||||
|
|
||||||
.worktrees/
|
.worktrees/
|
||||||
|
|
||||||
|
/VolmLib/
|
||||||
|
/docs/
|
||||||
|
/.perf/
|
||||||
|
/.codegraph/
|
||||||
+452
-296
@@ -1,160 +1,215 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for software and other kinds of works.
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed to take away your freedom to share and change the
|
The licenses for most software and other practical works are designed
|
||||||
works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all
|
to take away your freedom to share and change the works. By contrast,
|
||||||
versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
the GNU General Public License for most of our software; it applies also to any other work released this way by its
|
share and change all versions of a program--to make sure it remains free
|
||||||
authors. You can apply it to your programs, too.
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make
|
When we speak of free software, we are referring to freedom, not
|
||||||
sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and
|
have the freedom to distribute copies of free software (and charge for
|
||||||
that you know you can do these things.
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights.
|
To protect your rights, we need to prevent others from denying you
|
||||||
Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it:
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
responsibilities to respect the freedom of others.
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients
|
For example, if you distribute copies of such a program, whether
|
||||||
the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
show them these terms so they know their rights.
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
(1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
and/or modify it.
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software.
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems
|
that there is no warranty for this free software. For both users' and
|
||||||
will not be attributed erroneously to authors of previous versions.
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run modified versions of the software inside them, although
|
Some devices are designed to deny users access to install or run
|
||||||
the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the
|
modified versions of the software inside them, although the manufacturer
|
||||||
software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those
|
protecting users' freedom to change the software. The systematic
|
||||||
products. If such problems arise substantially in other domains, we stand ready to extend this provision to those
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
domains in future versions of the GPL, as needed to protect the freedom of users.
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents. States should not allow patents to restrict
|
Finally, every program is threatened constantly by software patents.
|
||||||
development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger
|
States should not allow patents to restrict development and use of
|
||||||
that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
patents cannot be used to render the program non-free.
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and modification follow.
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
0. Definitions.
|
0. Definitions.
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
Licensees" and
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
"recipients" may be individuals or organizations.
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission,
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
work "based on" the earlier work.
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based on the Program.
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily
|
To "propagate" a work means to do anything with it that, without
|
||||||
liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy.
|
permission, would make you directly or secondarily liable for
|
||||||
Propagation includes copying, distribution (with or without modification), making available to the public, and in some
|
infringement under applicable copyright law, except executing it on a
|
||||||
countries other activities as well.
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction
|
To "convey" a work means any kind of propagation that enables other
|
||||||
with a user through a computer network, with no transfer of a copy, is not conveying.
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright
|
to the extent that it includes a convenient and prominently visible
|
||||||
notice, and (2)
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
tells the user that there is no warranty for the work (except to the extent that warranties are provided), that
|
tells the user that there is no warranty for the work (except to the
|
||||||
licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a
|
extent that warranties are provided), that licensees may convey the
|
||||||
list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
1. Source Code.
|
1. Source Code.
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means
|
The "source code" for a work means the preferred form of the work
|
||||||
any non-source form of a work.
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body,
|
A "Standard Interface" means an interface that either is an official
|
||||||
or, in the case of interfaces specified for a particular programming language, one that is widely used among developers
|
standard defined by a recognized standards body, or, in the case of
|
||||||
working in that language.
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in
|
The "System Libraries" of an executable work include anything, other
|
||||||
the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is
|
packaging a Major Component, but which is not part of that Major
|
||||||
available to the public in source code form. A
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
"Major Component", in this context, means a major essential component
|
"Major Component", in this context, means a major essential component
|
||||||
(kernel, window system, and so on) of the specific operating system
|
(kernel, window system, and so on) of the specific operating system
|
||||||
(if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
to run it.
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (
|
The "Corresponding Source" for a work in object code form means all
|
||||||
for an executable work) run the object code and to modify the work, including scripts to control those activities.
|
the source code needed to generate, install, and (for an executable
|
||||||
However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs
|
work) run the object code and to modify the work, including scripts to
|
||||||
which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding
|
control those activities. However, it does not include the work's
|
||||||
Source includes interface definition files associated with source files for the work, and the source code for shared
|
System Libraries, or general-purpose tools or generally available free
|
||||||
libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data
|
programs which are used unmodified in performing those activities but
|
||||||
communication or control flow between those subprograms and other parts of the work.
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users can regenerate automatically from other parts of the
|
The Corresponding Source need not include anything that users
|
||||||
Corresponding Source.
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that same work.
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
2. Basic Permissions.
|
2. Basic Permissions.
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided
|
All rights granted under this License are granted for the term of
|
||||||
the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program.
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
The output from running a covered work is covered by this License only if the output, given its content, constitutes a
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not convey, without conditions so long as your license
|
You may make, run and propagate covered works that you do not
|
||||||
otherwise remains in force. You may convey covered works to others for the sole purpose of having them make
|
convey, without conditions so long as your license otherwise remains
|
||||||
modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with
|
in force. You may convey covered works to others for the sole purpose
|
||||||
the terms of this License in conveying all material for which you do not control copyright. Those thus making or running
|
of having them make modifications exclusively for you, or provide you
|
||||||
the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that
|
with facilities for running those works, provided that you comply with
|
||||||
prohibit them from making any copies of your copyrighted material outside their relationship with you.
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not
|
Conveying under any other circumstances is permitted solely under
|
||||||
allowed; section 10 makes it unnecessary.
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling
|
No covered work shall be deemed part of an effective technological
|
||||||
obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or
|
measure under any applicable law fulfilling obligations under article
|
||||||
restricting circumvention of such measures.
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you
|
circumvention of technological measures to the extent such circumvention
|
||||||
disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users,
|
is effected by exercising rights under this License with respect to
|
||||||
your or third parties' legal rights to forbid circumvention of technological measures.
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you
|
You may convey verbatim copies of the Program's source code as you
|
||||||
conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating
|
receive it, in any medium, provided that you conspicuously and
|
||||||
that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
of the absence of any warranty; and give all recipients a copy of this License along with the Program.
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for
|
You may charge any price or no price for each copy that you convey,
|
||||||
a fee.
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source
|
You may convey a work based on the Program, or the modifications to
|
||||||
code under the terms of section 4, provided that you also meet all of these conditions:
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified
|
a) The work must carry prominent notices stating that you modified
|
||||||
it, and giving a relevant date.
|
it, and giving a relevant date.
|
||||||
@@ -177,17 +232,22 @@ code under the terms of section 4, provided that you also meet all of these cond
|
|||||||
interfaces that do not display Appropriate Legal Notices, your
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
work need not make them do so.
|
work need not make them do so.
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent works, which are not by their nature extensions of
|
A compilation of a covered work with other separate and independent
|
||||||
the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or
|
works, which are not by their nature extensions of the covered work,
|
||||||
distribution medium, is called an
|
and which are not combined with it such as to form a larger program,
|
||||||
"aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
this License to apply to the other parts of the aggregate.
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the
|
You may convey a covered work in object code form under the terms
|
||||||
machine-readable Corresponding Source under the terms of this License, in one of these ways:
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
(including a physical distribution medium), accompanied by the
|
(including a physical distribution medium), accompanied by the
|
||||||
@@ -230,55 +290,77 @@ machine-readable Corresponding Source under the terms of this License, in one of
|
|||||||
Source of the work are being offered to the general public at no
|
Source of the work are being offered to the general public at no
|
||||||
charge under subsection 6d.
|
charge under subsection 6d.
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library,
|
A separable portion of the object code, whose source code is excluded
|
||||||
need not be included in conveying the object code work.
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In
|
tangible personal property which is normally used for personal, family,
|
||||||
determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
particular product received by a particular user, "normally used" refers to a typical or common use of that class of
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
product, regardless of the status of the particular user or of the way in which the particular user actually uses, or
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
expects or is expected to use, the product. A product is a consumer product regardless of whether the product has
|
product received by a particular user, "normally used" refers to a
|
||||||
substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of
|
typical or common use of that class of product, regardless of the status
|
||||||
the product.
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information
|
"Installation Information" for a User Product means any methods,
|
||||||
required to install and execute modified versions of a covered work in that User Product from a modified version of its
|
procedures, authorization keys, or other information required to install
|
||||||
Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code
|
and execute modified versions of a covered work in that User Product from
|
||||||
is in no case prevented or interfered with solely because modification has been made.
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the
|
If you convey an object code work under this section in, or with, or
|
||||||
conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding
|
part of a transaction in which the right of possession and use of the
|
||||||
Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
apply if neither you nor any third party retains the ability to install modified object code on the User Product (for
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
example, the work has been installed in ROM).
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a requirement to continue to provide support
|
The requirement to provide Installation Information does not include a
|
||||||
service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product
|
requirement to continue to provide support service, warranty, or updates
|
||||||
in which it has been modified or installed. Access to a network may be denied when the modification itself materially
|
for a work that has been modified or installed by the recipient, or for
|
||||||
and adversely affects the operation of the network or violates the rules and protocols for communication across the
|
the User Product in which it has been modified or installed. Access to a
|
||||||
network.
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
that is publicly documented (and with an implementation available to the public in source code form), and must require
|
in accord with this section must be in a format that is publicly
|
||||||
no special password or key for unpacking, reading or copying.
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
7. Additional Terms.
|
7. Additional Terms.
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were
|
License by making exceptions from one or more of its conditions.
|
||||||
included in this License, to the extent that they are valid under applicable law. If additional permissions apply only
|
Additional permissions that are applicable to the entire Program shall
|
||||||
to part of the Program, that part may be used separately under those permissions, but the entire Program remains
|
be treated as though they were included in this License, to the extent
|
||||||
governed by this License without regard to the additional permissions.
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or
|
When you convey a copy of a covered work, you may at your option
|
||||||
from any part of it. (Additional permissions may be written to require their own removal in certain cases when you
|
remove any additional permissions from that copy, or from any part of
|
||||||
modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have
|
it. (Additional permissions may be written to require their own
|
||||||
or can give appropriate copyright permission.
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by
|
Notwithstanding any other provision of this License, for material you
|
||||||
the copyright holders of that material) supplement the terms of this License with terms:
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
terms of sections 15 and 16 of this License; or
|
terms of sections 15 and 16 of this License; or
|
||||||
@@ -303,182 +385,251 @@ the copyright holders of that material) supplement the terms of this License wit
|
|||||||
any liability that these contractual assumptions directly impose on
|
any liability that these contractual assumptions directly impose on
|
||||||
those licensors and authors.
|
those licensors and authors.
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the
|
All other non-permissive additional terms are considered "further
|
||||||
Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
a term that is a further restriction, you may remove that term. If a license document contains a further restriction but
|
received it, or any part of it, contains a notice stating that it is
|
||||||
permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of
|
governed by this License along with a term that is a further
|
||||||
that license document, provided that the further restriction does not survive such relicensing or conveying.
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a
|
If you add terms to a covered work in accord with this section, you
|
||||||
statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
exceptions; the above requirements apply either way.
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
8. Termination.
|
8. Termination.
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to
|
You may not propagate or modify a covered work except as expressly
|
||||||
propagate or modify it is void, and will automatically terminate your rights under this License (including any patent
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
licenses granted under the third paragraph of section 11).
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your license from a particular copyright holder is
|
However, if you cease all violation of this License, then your
|
||||||
reinstated (a)
|
license from a particular copyright holder is reinstated (a)
|
||||||
provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b)
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
after the cessation.
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you
|
Moreover, your license from a particular copyright holder is
|
||||||
of the violation by some reasonable means, this is the first time you have received notice of violation of this
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the
|
violation by some reasonable means, this is the first time you have
|
||||||
notice.
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the licenses of parties who have received copies or
|
Termination of your rights under this section does not terminate the
|
||||||
rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not
|
licenses of parties who have received copies or rights from you under
|
||||||
qualify to receive new licenses for the same material under section 10.
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a
|
You are not required to accept this License in order to receive or
|
||||||
covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run,
|
Each time you convey a covered work, the recipient automatically
|
||||||
modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third
|
receives a license from the original licensors, to run, modify and
|
||||||
parties with this License.
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or
|
An "entity transaction" is a transaction transferring control of an
|
||||||
subdividing an organization, or merging organizations. If propagation of a covered work results from an entity
|
organization, or substantially all assets of one, or subdividing an
|
||||||
transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work
|
organization, or merging organizations. If propagation of a covered
|
||||||
the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the
|
work results from an entity transaction, each party to that
|
||||||
Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with
|
transaction who receives a copy of the work also receives whatever
|
||||||
reasonable efforts.
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For
|
You may not impose any further restrictions on the exercise of the
|
||||||
example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License,
|
rights granted or affirmed under this License. For example, you may
|
||||||
and you may not initiate litigation
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using,
|
rights granted under this License, and you may not initiate litigation
|
||||||
selling, offering for sale, or importing the Program or any portion of it.
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
11. Patents.
|
11. Patents.
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
Program is based. The work thus licensed is called the contributor's "contributor version".
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already
|
A contributor's "essential patent claims" are all patent claims
|
||||||
acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or
|
owned or controlled by the contributor, whether already acquired or
|
||||||
selling its contributor version, but do not include claims that would be infringed only as a consequence of further
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent
|
by this License, of making, using, or selling its contributor version,
|
||||||
sublicenses in a manner consistent with the requirements of this License.
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its
|
patent license under the contributor's essential patent claims, to
|
||||||
contributor version.
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not
|
In the following three paragraphs, a "patent license" is any express
|
||||||
to enforce a patent
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
(such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a
|
(such as an express permission to practice a patent or covenant not to
|
||||||
patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
available for anyone to copy, free of charge and under the terms of this License, through a publicly available network
|
and the Corresponding Source of the work is not available for anyone
|
||||||
server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available,
|
to copy, free of charge and under the terms of this License, through a
|
||||||
or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a
|
publicly available network server or other readily accessible means,
|
||||||
manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
that country that you have reason to believe are valid.
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring
|
If, pursuant to or in connection with a single transaction or
|
||||||
conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is
|
covered work, and grant a patent license to some of the parties
|
||||||
automatically extended to all recipients of the covered work and works based on it.
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of,
|
A patent license is "discriminatory" if it does not include within
|
||||||
or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
may not convey a covered work if you are a party to an arrangement with a third party that is in the business of
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
distributing software, under which you make payment to the third party based on the extent of your activity of conveying
|
specifically granted under this License. You may not convey a covered
|
||||||
the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a
|
work if you are a party to an arrangement with a third party that is
|
||||||
discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from
|
in the business of distributing software, under which you make payment
|
||||||
those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered
|
to the third party based on the extent of your activity of conveying
|
||||||
work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
infringement that may otherwise be available to you under applicable patent law.
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
would be to refrain entirely from conveying the Program.
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work
|
Notwithstanding any other provision of this License, you have
|
||||||
licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the
|
permission to link or combine any covered work with a work licensed
|
||||||
resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply
|
combined work, and to convey the resulting work. The terms of this
|
||||||
to the combination as such.
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new
|
the GNU General Public License from time to time. Such new versions will
|
||||||
problems or concerns.
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the
|
Each version is given a distinguishing version number. If the
|
||||||
GNU General Public License "or any later version" applies to it, you have the option of following the terms and
|
Program specifies that a certain numbered version of the GNU General
|
||||||
conditions either of that numbered version or of any later version published by the Free Software Foundation. If the
|
Public License "or any later version" applies to it, you have the
|
||||||
Program does not specify a version number of the GNU General Public License, you may choose any version ever published
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
by the Free Software Foundation.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used,
|
If the Program specifies that a proxy can decide which future
|
||||||
that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
Program.
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
Later license versions may give you additional or different permissions. However, no additional obligations are imposed
|
Later license versions may give you additional or different
|
||||||
on any author or copyright holder as a result of your choosing to follow a later version.
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
16. Limitation of Liability.
|
16. Limitation of Liability.
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL,
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
DAMAGES.
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil
|
above cannot be given local legal effect according to their terms,
|
||||||
liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program
|
reviewing courts shall apply local law that most closely approximates
|
||||||
in return for a fee.
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve
|
If you develop a new program, and you want it to be of the greatest
|
||||||
this is to make it free software which everyone can redistribute and change under these terms.
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to
|
To do so, attach the following notices to the program. It is safest
|
||||||
most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer
|
to attach them to the start of each source file to most effectively
|
||||||
to where the full notice is found.
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
Copyright (C) <year> <name of author>
|
Copyright (C) <year> <name of author>
|
||||||
@@ -498,21 +649,26 @@ to where the full notice is found.
|
|||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
<program> Copyright (C) <year> <name of author>
|
<program> Copyright (C) <year> <name of author>
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
This is free software, and you are welcome to redistribute it
|
This is free software, and you are welcome to redistribute it
|
||||||
under certain conditions; type `show c' for details.
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
course, your program's commands might be different; for a GUI interface, you would use an "about box".
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
<https://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is
|
The GNU General Public License does not permit incorporating your program
|
||||||
a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
@@ -25,7 +25,7 @@ Canonical English is defined in the typed Java catalogs under `core/src/main/jav
|
|||||||
| Spigot / CraftBukkit | plugin jar | 26.1.2 - 26.3 | Managed `iris:*` creation and generation; exact vanilla-slot `/iris replace` is unavailable |
|
| Spigot / CraftBukkit | plugin jar | 26.1.2 - 26.3 | Managed `iris:*` creation and generation; exact vanilla-slot `/iris replace` is unavailable |
|
||||||
| Fabric | mod jar | 26.3 | Server worldgen + client HUD; requires Fabric Loader 0.19.5+ |
|
| Fabric | mod jar | 26.3 | Server worldgen + client HUD; requires Fabric Loader 0.19.5+ |
|
||||||
| Forge | mod jar | 26.3 | Server worldgen + client HUD; current target is Forge 26.3-66.0.8 |
|
| Forge | mod jar | 26.3 | Server worldgen + client HUD; current target is Forge 26.3-66.0.8 |
|
||||||
| NeoForge | mod jar | 26.3 | Server worldgen + client HUD; current target is NeoForge 26.3.0.33-beta |
|
| NeoForge | mod jar | 26.3 | Server worldgen + client HUD; requires NeoForge 26.3.0.65-beta+; current target is NeoForge 26.3.0.83 |
|
||||||
|
|
||||||
Java 25 is required on every platform.
|
Java 25 is required on every platform.
|
||||||
|
|
||||||
@@ -248,28 +248,26 @@ which is what a board reader means.
|
|||||||
|
|
||||||
Requirements: JDK 25 (set `JAVA_HOME` to it). The Gradle wrapper handles everything else.
|
Requirements: JDK 25 (set `JAVA_HOME` to it). The Gradle wrapper handles everything else.
|
||||||
|
|
||||||
```
|
```sh
|
||||||
./gradlew buildAllToOut
|
./gradlew buildAll
|
||||||
```
|
```
|
||||||
|
|
||||||
builds every platform artifact into the workspace-level `../PluginOuts/` directory:
|
This builds and verifies the Bukkit plugin, API, SPI and the Fabric, Forge and NeoForge jars
|
||||||
|
for every Minecraft version declared in `gradle/mod-targets.json`. Artifacts stay in the
|
||||||
|
producing module's `build/` directory. The packed Bukkit plugin is in `build/libs/`, the
|
||||||
|
API is in `build/api/`, the SPI is in `spi/build/libs/`, and mod jars are in
|
||||||
|
`adapters/<loader>/build/<minecraft>/libs/`.
|
||||||
|
|
||||||
```
|
Use `./gradlew buildBukkit` for the plugin, or `verify<Loader><mc>` for one mod jar,
|
||||||
Iris v<version> [CraftBukkit] <mc>.jar
|
for example `verifyFabric26_2` or `verifyNeoForge1_21_1`. `buildFabric`, `buildForge`
|
||||||
Iris v<version> [Fabric] <mc>+<loader>.jar
|
and `buildNeoforge` select the `minecraftVersion` Gradle property. `./gradlew checkAll`
|
||||||
Iris v<version> [Forge] <mc>+<loader>.jar
|
runs every check plus the complete mod matrix. Forge 1.21.1 compiles its tests because
|
||||||
Iris v<version> [NeoForge] <mc>+<loader>.jar
|
Forge 52 cannot bootstrap Minecraft outside FML.
|
||||||
```
|
|
||||||
|
|
||||||
Per-platform tasks: `./gradlew buildBukkit`, `buildFabric`, `buildForge`, `buildNeoforge`. The
|
Multiplexor reads `gradle/multiplexor.gradle` to discover targets and build each in a
|
||||||
SPI jar (the pure-JVM adapter/platform contract, not the stable plugin API) is built to `spi/build/libs/` by
|
separate source sandbox. Use its project build and deployment commands to export jars
|
||||||
`./gradlew :spi:jar`.
|
or install them into managed test instances. Gradle builds do not copy artifacts to
|
||||||
|
external destinations.
|
||||||
`./gradlew buildAll` also runs `buildAllToOut` and copies the jars into a consumer dropin tree
|
|
||||||
for a local test server. The consumer directory defaults to `../../[Minecraft Server]/consumers/`
|
|
||||||
when it exists, otherwise `build/consumers/` inside the repo. `-Plocation=/path/to/consumers`
|
|
||||||
changes only the consumer directory. Both tasks copy all four verified platform jars into
|
|
||||||
`../PluginOuts/`.
|
|
||||||
|
|
||||||
VolmLib is built from source when a `VolmLib` checkout sits beside the repo or in a parent directory
|
VolmLib is built from source when a `VolmLib` checkout sits beside the repo or in a parent directory
|
||||||
(`VOLMLIB_DIR` or `-PlocalVolmLibDirectory=<path>` pick another one). Without one, or with
|
(`VOLMLIB_DIR` or `-PlocalVolmLibDirectory=<path>` pick another one). Without one, or with
|
||||||
|
|||||||
+67
-2
@@ -245,9 +245,15 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
|||||||
for (int y = groundY + 1; y <= groundY + 32; y++) {
|
for (int y = groundY + 1; y <= groundY + 32; y++) {
|
||||||
assertTrue("Roof at " + x + "," + y, state(blocks, x, y, 0).isAir());
|
assertTrue("Roof at " + x + "," + y, state(blocks, x, y, 0).isAir());
|
||||||
}
|
}
|
||||||
for (int y = 12; y < groundY; y++) {
|
int supportY = x < 2 ? groundY - 33 : 12;
|
||||||
|
for (int y = supportY; y < groundY; y++) {
|
||||||
assertTrue("Missing foundation at " + x + "," + y, state(blocks, x, y, 0).isSolid());
|
assertTrue("Missing foundation at " + x + "," + y, state(blocks, x, y, 0).isSolid());
|
||||||
}
|
}
|
||||||
|
if (x < 2) {
|
||||||
|
for (int y = 13; y < supportY; y++) {
|
||||||
|
assertTrue("Cave beneath support at " + x + "," + y, state(blocks, x, y, 0).isAir());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,6 +274,57 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
|||||||
assertTrue(state(blocks, 0, groundY + 1, 0).isAir());
|
assertTrue(state(blocks, 0, groundY + 1, 0).isAir());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void flattenPreservesCavesBelowIntactGroundAtTheSkirtEdge() {
|
||||||
|
StructureStart start = desertStart();
|
||||||
|
int groundY = start.getBoundingBox().minY() - 1;
|
||||||
|
int x = start.getBoundingBox().maxX() + 23;
|
||||||
|
int z = start.getBoundingBox().minZ();
|
||||||
|
BoundingBox area = new BoundingBox(x, 0, z, x, 127, z);
|
||||||
|
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||||
|
for (int y = groundY - 35; y <= groundY; y++) {
|
||||||
|
BlockState block = y >= groundY - 34 && y <= groundY - 13
|
||||||
|
? Blocks.CAVE_AIR.defaultBlockState() : Blocks.STONE.defaultBlockState();
|
||||||
|
put(blocks, x, y, z, block);
|
||||||
|
}
|
||||||
|
put(blocks, x, groundY - 1, z, Blocks.DIRT.defaultBlockState());
|
||||||
|
put(blocks, x, groundY, z, Blocks.GRASS_BLOCK.defaultBlockState());
|
||||||
|
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, StructureTerrainMode.FLATTEN);
|
||||||
|
((IrisStructureTerrain) target.terrain()).setHorizontalPadding(24).setFlattenRange(64);
|
||||||
|
|
||||||
|
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area, List.of(target), (px, pz) -> groundY);
|
||||||
|
|
||||||
|
for (int y = groundY - 34; y <= groundY - 13; y++) {
|
||||||
|
assertEquals("Cave at " + y, Blocks.CAVE_AIR.defaultBlockState(), state(blocks, x, y, z));
|
||||||
|
}
|
||||||
|
assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, x, groundY - 1, z));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void flattenBridgesTheFoundationGapWithoutFillingACaveBelowItsSupport() {
|
||||||
|
StructureStart start = desertStart();
|
||||||
|
int groundY = start.getBoundingBox().minY() - 1;
|
||||||
|
BoundingBox area = new BoundingBox(0, 0, 0, 0, 127, 0);
|
||||||
|
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||||
|
put(blocks, 0, groundY, 0, Blocks.GRASS_BLOCK.defaultBlockState());
|
||||||
|
for (int y = groundY - 11; y <= groundY - 9; y++) {
|
||||||
|
put(blocks, 0, y, 0, Blocks.STONE.defaultBlockState());
|
||||||
|
}
|
||||||
|
for (int y = groundY - 20; y <= groundY - 12; y++) {
|
||||||
|
put(blocks, 0, y, 0, Blocks.CAVE_AIR.defaultBlockState());
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area,
|
||||||
|
List.of(surfaceTarget(start, StructureTerrainMode.FLATTEN)), (x, z) -> groundY);
|
||||||
|
|
||||||
|
for (int y = groundY - 8; y < groundY; y++) {
|
||||||
|
assertEquals("Foundation at " + y, Blocks.STONE.defaultBlockState(), state(blocks, 0, y, 0));
|
||||||
|
}
|
||||||
|
for (int y = groundY - 20; y <= groundY - 12; y++) {
|
||||||
|
assertEquals("Cave at " + y, Blocks.CAVE_AIR.defaultBlockState(), state(blocks, 0, y, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void flattenLeavesBuriedAndSubmergedInstancesUntouched() {
|
public void flattenLeavesBuriedAndSubmergedInstancesUntouched() {
|
||||||
StructureStart start = desertStart();
|
StructureStart start = desertStart();
|
||||||
@@ -619,10 +676,18 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
|||||||
for (int z = 0; z <= 8; z++) {
|
for (int z = 0; z <= 8; z++) {
|
||||||
int groundY = plan.flattenedHeights().get(NativeStructureTemplateOccupancy.columnKey(x, z)) - 1;
|
int groundY = plan.flattenedHeights().get(NativeStructureTemplateOccupancy.columnKey(x, z)) - 1;
|
||||||
assertTrue("Village grade at " + x + "," + z + ": " + groundY, groundY >= 64 && groundY <= 70);
|
assertTrue("Village grade at " + x + "," + z + ": " + groundY, groundY >= 64 && groundY <= 70);
|
||||||
for (int y = 20; y <= groundY; y++) {
|
boolean originalFoundation = (x == 0 || x == 8) && (z == 0 || z == 8);
|
||||||
|
int supportY = originalFoundation ? 63 : 20;
|
||||||
|
for (int y = supportY; y <= groundY; y++) {
|
||||||
assertTrue("Disconnected village ground at " + x + "," + y + "," + z,
|
assertTrue("Disconnected village ground at " + x + "," + y + "," + z,
|
||||||
state(blocks, x, y, z).isSolid());
|
state(blocks, x, y, z).isSolid());
|
||||||
}
|
}
|
||||||
|
if (originalFoundation) {
|
||||||
|
for (int y = 21; y < supportY; y++) {
|
||||||
|
assertTrue("Cave beneath foundation at " + x + "," + y + "," + z,
|
||||||
|
state(blocks, x, y, z).isAir());
|
||||||
|
}
|
||||||
|
}
|
||||||
for (int y = groundY + 1; y <= 96; y++) {
|
for (int y = groundY + 1; y <= 96; y++) {
|
||||||
assertTrue("Roof remains above village at " + x + "," + y + "," + z,
|
assertTrue("Roof remains above village at " + x + "," + y + "," + z,
|
||||||
state(blocks, x, y, z).isAir());
|
state(blocks, x, y, z).isAir());
|
||||||
|
|||||||
+10
-6
@@ -3,6 +3,7 @@ package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
|||||||
import art.arcane.volmlib.nativelib.v26_2_R1.terrain.NativeStructureWorldgenAccess;
|
import art.arcane.volmlib.nativelib.v26_2_R1.terrain.NativeStructureWorldgenAccess;
|
||||||
|
|
||||||
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
|
||||||
import art.arcane.volmlib.util.structure.StructureTerrainMode;
|
import art.arcane.volmlib.util.structure.StructureTerrainMode;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructurePostProcessor;
|
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructurePostProcessor;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureTerrainIntegrator;
|
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureTerrainIntegrator;
|
||||||
@@ -230,8 +231,9 @@ public class NativeStructureWorldgenAccessTest {
|
|||||||
recording.world(), generationCenter,
|
recording.world(), generationCenter,
|
||||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||||
true,
|
true, true,
|
||||||
position -> false);
|
position -> false,
|
||||||
|
NativeBlockChangeSet.disabled());
|
||||||
int x = generationCenter.getMiddleBlockX();
|
int x = generationCenter.getMiddleBlockX();
|
||||||
int z = generationCenter.getMiddleBlockZ();
|
int z = generationCenter.getMiddleBlockZ();
|
||||||
|
|
||||||
@@ -248,8 +250,9 @@ public class NativeStructureWorldgenAccessTest {
|
|||||||
recording.world(), generationCenter(),
|
recording.world(), generationCenter(),
|
||||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||||
false,
|
false, true,
|
||||||
position::equals);
|
position::equals,
|
||||||
|
NativeBlockChangeSet.disabled());
|
||||||
|
|
||||||
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
|
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
|
||||||
assertFalse(access.ensureCanWrite(position));
|
assertFalse(access.ensureCanWrite(position));
|
||||||
@@ -319,8 +322,9 @@ public class NativeStructureWorldgenAccessTest {
|
|||||||
recording.world(), generationCenter(),
|
recording.world(), generationCenter(),
|
||||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||||
false,
|
false, true,
|
||||||
position -> false);
|
position -> false,
|
||||||
|
NativeBlockChangeSet.disabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ChunkPos generationCenter() {
|
private static ChunkPos generationCenter() {
|
||||||
|
|||||||
+19
-8
@@ -9,6 +9,7 @@ import art.arcane.iris.structure.nativegen.NativeStructureVolumeIndex;
|
|||||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.structure.StructureTerrainSettings;
|
||||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import net.minecraft.SharedConstants;
|
import net.minecraft.SharedConstants;
|
||||||
@@ -69,7 +70,8 @@ public class NativeStructureVolumeIndexTest {
|
|||||||
StructureStart start = swampHut(0, 0, 4, 6);
|
StructureStart start = swampHut(0, 0, 4, 6);
|
||||||
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||||
|
|
||||||
KList<NativeStructureVolume> volumes = NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, start);
|
KList<NativeStructureVolume> volumes = NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, start,
|
||||||
|
StructureTerrainSettings.SOURCE, (x, z) -> 63);
|
||||||
|
|
||||||
assertEquals(1, volumes.size());
|
assertEquals(1, volumes.size());
|
||||||
NativeStructureVolume volume = volumes.getFirst();
|
NativeStructureVolume volume = volumes.getFirst();
|
||||||
@@ -84,8 +86,10 @@ public class NativeStructureVolumeIndexTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void invalidStartsContributeNoVolumes() {
|
public void invalidStartsContributeNoVolumes() {
|
||||||
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, StructureStart.INVALID_START));
|
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, StructureStart.INVALID_START,
|
||||||
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, null));
|
StructureTerrainSettings.SOURCE, (x, z) -> 63));
|
||||||
|
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, null,
|
||||||
|
StructureTerrainSettings.SOURCE, (x, z) -> 63));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -323,7 +327,7 @@ public class NativeStructureVolumeIndexTest {
|
|||||||
BlockingResolver coordination = new BlockingResolver();
|
BlockingResolver coordination = new BlockingResolver();
|
||||||
NativeStructureVolumeIndex.OriginResolver volumes = (engine, chunkX, chunkZ) ->
|
NativeStructureVolumeIndex.OriginResolver volumes = (engine, chunkX, chunkZ) ->
|
||||||
new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
|
new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
|
||||||
-256, 0, -256, 256, 255, 256));
|
-256, 0, -256, 256, 255, 256, null));
|
||||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting((engine, chunkX, chunkZ) -> {
|
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting((engine, chunkX, chunkZ) -> {
|
||||||
coordination.volumesAt(engine, chunkX, chunkZ);
|
coordination.volumesAt(engine, chunkX, chunkZ);
|
||||||
return volumes.volumesAt(engine, chunkX, chunkZ);
|
return volumes.volumesAt(engine, chunkX, chunkZ);
|
||||||
@@ -369,7 +373,7 @@ public class NativeStructureVolumeIndexTest {
|
|||||||
terrain.join();
|
terrain.join();
|
||||||
}
|
}
|
||||||
return new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
|
return new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
|
||||||
-256, 0, -256, 256, 100, 256));
|
-256, 0, -256, 256, 100, 256, null));
|
||||||
});
|
});
|
||||||
ExecutorService owner = Executors.newSingleThreadExecutor();
|
ExecutorService owner = Executors.newSingleThreadExecutor();
|
||||||
ForkJoinPool workers = new ForkJoinPool(2);
|
ForkJoinPool workers = new ForkJoinPool(2);
|
||||||
@@ -493,7 +497,13 @@ public class NativeStructureVolumeIndexTest {
|
|||||||
private static Map<?, ?> cache(NativeStructureVolumeIndex index, String name) throws Exception {
|
private static Map<?, ?> cache(NativeStructureVolumeIndex index, String name) throws Exception {
|
||||||
Field field = NativeStructureVolumeIndex.class.getDeclaredField(name);
|
Field field = NativeStructureVolumeIndex.class.getDeclaredField(name);
|
||||||
field.setAccessible(true);
|
field.setAccessible(true);
|
||||||
return (Map<?, ?>) field.get(index);
|
Object value = field.get(index);
|
||||||
|
if (value instanceof Map<?, ?> map) {
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
Field entries = value.getClass().getDeclaredField("entries");
|
||||||
|
entries.setAccessible(true);
|
||||||
|
return (Map<?, ?>) entries.get(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static StructureStart swampHut(int chunkX, int chunkZ, int x, int z) {
|
private static StructureStart swampHut(int chunkX, int chunkZ, int x, int z) {
|
||||||
@@ -544,7 +554,8 @@ public class NativeStructureVolumeIndexTest {
|
|||||||
if (chunkX != originChunkX || chunkZ != originChunkZ) {
|
if (chunkX != originChunkX || chunkZ != originChunkZ) {
|
||||||
return NativeStructureVolume.NONE;
|
return NativeStructureVolume.NONE;
|
||||||
}
|
}
|
||||||
return NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, swampHut(chunkX, chunkZ, 4, 6));
|
return NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, swampHut(chunkX, chunkZ, 4, 6),
|
||||||
|
StructureTerrainSettings.SOURCE, (x, z) -> 63);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,7 +607,7 @@ public class NativeStructureVolumeIndexTest {
|
|||||||
return new KList<>(new NativeStructureVolume(
|
return new KList<>(new NativeStructureVolume(
|
||||||
chunkX + ":" + chunkZ,
|
chunkX + ":" + chunkZ,
|
||||||
0, 0, 0,
|
0, 0, 0,
|
||||||
15, 255, 15));
|
15, 255, 15, null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private record ScopedEngine(IrisEngine engine, AtomicInteger scopes) {
|
private record ScopedEngine(IrisEngine engine, AtomicInteger scopes) {
|
||||||
|
|||||||
+6
@@ -14,6 +14,7 @@ import net.minecraft.SharedConstants;
|
|||||||
import net.minecraft.server.Bootstrap;
|
import net.minecraft.server.Bootstrap;
|
||||||
import net.minecraft.world.level.LevelHeightAccessor;
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
import net.minecraft.world.level.levelgen.Heightmap;
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import org.junit.AfterClass;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -45,6 +46,11 @@ public final class IrisChunkGeneratorHeightCacheTest {
|
|||||||
NativeBlockTags.bindHeightmapFixtures();
|
NativeBlockTags.bindHeightmapFixtures();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void restoreBlockTags() {
|
||||||
|
NativeBlockTags.restoreTags();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void repeatedNativeHeightUsesLiveHeightQueriesAcrossRuntimeChanges() throws Exception {
|
public void repeatedNativeHeightUsesLiveHeightQueriesAcrossRuntimeChanges() throws Exception {
|
||||||
Fixture fixture = fixture();
|
Fixture fixture = fixture();
|
||||||
|
|||||||
+67
-2
@@ -245,9 +245,15 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
|||||||
for (int y = groundY + 1; y <= groundY + 32; y++) {
|
for (int y = groundY + 1; y <= groundY + 32; y++) {
|
||||||
assertTrue("Roof at " + x + "," + y, state(blocks, x, y, 0).isAir());
|
assertTrue("Roof at " + x + "," + y, state(blocks, x, y, 0).isAir());
|
||||||
}
|
}
|
||||||
for (int y = 12; y < groundY; y++) {
|
int supportY = x < 2 ? groundY - 33 : 12;
|
||||||
|
for (int y = supportY; y < groundY; y++) {
|
||||||
assertTrue("Missing foundation at " + x + "," + y, state(blocks, x, y, 0).isSolid());
|
assertTrue("Missing foundation at " + x + "," + y, state(blocks, x, y, 0).isSolid());
|
||||||
}
|
}
|
||||||
|
if (x < 2) {
|
||||||
|
for (int y = 13; y < supportY; y++) {
|
||||||
|
assertTrue("Cave beneath support at " + x + "," + y, state(blocks, x, y, 0).isAir());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,6 +274,57 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
|||||||
assertTrue(state(blocks, 0, groundY + 1, 0).isAir());
|
assertTrue(state(blocks, 0, groundY + 1, 0).isAir());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void flattenPreservesCavesBelowIntactGroundAtTheSkirtEdge() {
|
||||||
|
StructureStart start = desertStart();
|
||||||
|
int groundY = start.getBoundingBox().minY() - 1;
|
||||||
|
int x = start.getBoundingBox().maxX() + 23;
|
||||||
|
int z = start.getBoundingBox().minZ();
|
||||||
|
BoundingBox area = new BoundingBox(x, 0, z, x, 127, z);
|
||||||
|
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||||
|
for (int y = groundY - 35; y <= groundY; y++) {
|
||||||
|
BlockState block = y >= groundY - 34 && y <= groundY - 13
|
||||||
|
? Blocks.CAVE_AIR.defaultBlockState() : Blocks.STONE.defaultBlockState();
|
||||||
|
put(blocks, x, y, z, block);
|
||||||
|
}
|
||||||
|
put(blocks, x, groundY - 1, z, Blocks.DIRT.defaultBlockState());
|
||||||
|
put(blocks, x, groundY, z, Blocks.GRASS_BLOCK.defaultBlockState());
|
||||||
|
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, StructureTerrainMode.FLATTEN);
|
||||||
|
((IrisStructureTerrain) target.terrain()).setHorizontalPadding(24).setFlattenRange(64);
|
||||||
|
|
||||||
|
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area, List.of(target), (px, pz) -> groundY);
|
||||||
|
|
||||||
|
for (int y = groundY - 34; y <= groundY - 13; y++) {
|
||||||
|
assertEquals("Cave at " + y, Blocks.CAVE_AIR.defaultBlockState(), state(blocks, x, y, z));
|
||||||
|
}
|
||||||
|
assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, x, groundY - 1, z));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void flattenBridgesTheFoundationGapWithoutFillingACaveBelowItsSupport() {
|
||||||
|
StructureStart start = desertStart();
|
||||||
|
int groundY = start.getBoundingBox().minY() - 1;
|
||||||
|
BoundingBox area = new BoundingBox(0, 0, 0, 0, 127, 0);
|
||||||
|
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||||
|
put(blocks, 0, groundY, 0, Blocks.GRASS_BLOCK.defaultBlockState());
|
||||||
|
for (int y = groundY - 11; y <= groundY - 9; y++) {
|
||||||
|
put(blocks, 0, y, 0, Blocks.STONE.defaultBlockState());
|
||||||
|
}
|
||||||
|
for (int y = groundY - 20; y <= groundY - 12; y++) {
|
||||||
|
put(blocks, 0, y, 0, Blocks.CAVE_AIR.defaultBlockState());
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area,
|
||||||
|
List.of(surfaceTarget(start, StructureTerrainMode.FLATTEN)), (x, z) -> groundY);
|
||||||
|
|
||||||
|
for (int y = groundY - 8; y < groundY; y++) {
|
||||||
|
assertEquals("Foundation at " + y, Blocks.STONE.defaultBlockState(), state(blocks, 0, y, 0));
|
||||||
|
}
|
||||||
|
for (int y = groundY - 20; y <= groundY - 12; y++) {
|
||||||
|
assertEquals("Cave at " + y, Blocks.CAVE_AIR.defaultBlockState(), state(blocks, 0, y, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void flattenLeavesBuriedAndSubmergedInstancesUntouched() {
|
public void flattenLeavesBuriedAndSubmergedInstancesUntouched() {
|
||||||
StructureStart start = desertStart();
|
StructureStart start = desertStart();
|
||||||
@@ -619,10 +676,18 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
|||||||
for (int z = 0; z <= 8; z++) {
|
for (int z = 0; z <= 8; z++) {
|
||||||
int groundY = plan.flattenedHeights().get(NativeStructureTemplateOccupancy.columnKey(x, z)) - 1;
|
int groundY = plan.flattenedHeights().get(NativeStructureTemplateOccupancy.columnKey(x, z)) - 1;
|
||||||
assertTrue("Village grade at " + x + "," + z + ": " + groundY, groundY >= 64 && groundY <= 70);
|
assertTrue("Village grade at " + x + "," + z + ": " + groundY, groundY >= 64 && groundY <= 70);
|
||||||
for (int y = 20; y <= groundY; y++) {
|
boolean originalFoundation = (x == 0 || x == 8) && (z == 0 || z == 8);
|
||||||
|
int supportY = originalFoundation ? 63 : 20;
|
||||||
|
for (int y = supportY; y <= groundY; y++) {
|
||||||
assertTrue("Disconnected village ground at " + x + "," + y + "," + z,
|
assertTrue("Disconnected village ground at " + x + "," + y + "," + z,
|
||||||
state(blocks, x, y, z).isSolid());
|
state(blocks, x, y, z).isSolid());
|
||||||
}
|
}
|
||||||
|
if (originalFoundation) {
|
||||||
|
for (int y = 21; y < supportY; y++) {
|
||||||
|
assertTrue("Cave beneath foundation at " + x + "," + y + "," + z,
|
||||||
|
state(blocks, x, y, z).isAir());
|
||||||
|
}
|
||||||
|
}
|
||||||
for (int y = groundY + 1; y <= 96; y++) {
|
for (int y = groundY + 1; y <= 96; y++) {
|
||||||
assertTrue("Roof remains above village at " + x + "," + y + "," + z,
|
assertTrue("Roof remains above village at " + x + "," + y + "," + z,
|
||||||
state(blocks, x, y, z).isAir());
|
state(blocks, x, y, z).isAir());
|
||||||
|
|||||||
+6
@@ -20,6 +20,7 @@ import net.minecraft.world.level.block.Blocks;
|
|||||||
import net.minecraft.world.level.block.RotatedPillarBlock;
|
import net.minecraft.world.level.block.RotatedPillarBlock;
|
||||||
import net.minecraft.world.level.block.LiquidBlock;
|
import net.minecraft.world.level.block.LiquidBlock;
|
||||||
import net.minecraft.world.level.levelgen.Heightmap;
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import org.junit.AfterClass;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -46,6 +47,11 @@ public final class NativeTransitionColumnTest {
|
|||||||
NativeBlockTags.bindHeightmapFixtures();
|
NativeBlockTags.bindHeightmapFixtures();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void restoreBlockTags() {
|
||||||
|
NativeBlockTags.restoreTags();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void customKeysUseCarrierForNativeColumnsAndHeightmaps() {
|
public void customKeysUseCarrierForNativeColumnsAndHeightmaps() {
|
||||||
String key = "itemsadder:rocks/ruby_ore";
|
String key = "itemsadder:rocks/ruby_ore";
|
||||||
|
|||||||
+17
@@ -1,21 +1,30 @@
|
|||||||
package art.arcane.iris.nativegen.v26_3_R1;
|
package art.arcane.iris.nativegen.v26_3_R1;
|
||||||
|
|
||||||
import net.minecraft.core.Holder;
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
import net.minecraft.core.registries.BuiltInRegistries;
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
import net.minecraft.tags.BlockTags;
|
import net.minecraft.tags.BlockTags;
|
||||||
import net.minecraft.tags.TagLoader;
|
import net.minecraft.tags.TagLoader;
|
||||||
|
import net.minecraft.tags.TagKey;
|
||||||
import net.minecraft.world.level.block.Block;
|
import net.minecraft.world.level.block.Block;
|
||||||
import net.minecraft.world.level.block.Blocks;
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public final class NativeBlockTags {
|
public final class NativeBlockTags {
|
||||||
|
private static Map<TagKey<Block>, List<Holder<Block>>> originalTags;
|
||||||
|
|
||||||
private NativeBlockTags() {
|
private NativeBlockTags() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void bindHeightmapFixtures() {
|
public static void bindHeightmapFixtures() {
|
||||||
|
originalTags = new HashMap<>();
|
||||||
|
for (HolderSet.Named<Block> tag : BuiltInRegistries.BLOCK.getTags().toList()) {
|
||||||
|
originalTags.put(tag.key(), tag.stream().toList());
|
||||||
|
}
|
||||||
List<Holder<Block>> solid = List.of(Blocks.STONE.builtInRegistryHolder(),
|
List<Holder<Block>> solid = List.of(Blocks.STONE.builtInRegistryHolder(),
|
||||||
Blocks.OAK_LOG.builtInRegistryHolder(), Blocks.SPRUCE_LOG.builtInRegistryHolder());
|
Blocks.OAK_LOG.builtInRegistryHolder(), Blocks.SPRUCE_LOG.builtInRegistryHolder());
|
||||||
List<Holder<Block>> canopy = List.of(Blocks.STONE.builtInRegistryHolder(),
|
List<Holder<Block>> canopy = List.of(Blocks.STONE.builtInRegistryHolder(),
|
||||||
@@ -25,4 +34,12 @@ public final class NativeBlockTags {
|
|||||||
BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP, canopy,
|
BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP, canopy,
|
||||||
BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP_NO_LEAVES, solid))).apply();
|
BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP_NO_LEAVES, solid))).apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void restoreTags() {
|
||||||
|
if (originalTags == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BuiltInRegistries.BLOCK.prepareTagReload(new TagLoader.LoadResult<>(Registries.BLOCK, originalTags)).apply();
|
||||||
|
originalTags = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+10
-6
@@ -3,6 +3,7 @@ package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
|||||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureWorldgenAccess;
|
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureWorldgenAccess;
|
||||||
|
|
||||||
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
|
||||||
import art.arcane.volmlib.util.structure.StructureTerrainMode;
|
import art.arcane.volmlib.util.structure.StructureTerrainMode;
|
||||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructurePostProcessor;
|
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructurePostProcessor;
|
||||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureTerrainIntegrator;
|
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureTerrainIntegrator;
|
||||||
@@ -230,8 +231,9 @@ public class NativeStructureWorldgenAccessTest {
|
|||||||
recording.world(), generationCenter,
|
recording.world(), generationCenter,
|
||||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||||
true,
|
true, true,
|
||||||
position -> false);
|
position -> false,
|
||||||
|
NativeBlockChangeSet.disabled());
|
||||||
int x = generationCenter.getMiddleBlockX();
|
int x = generationCenter.getMiddleBlockX();
|
||||||
int z = generationCenter.getMiddleBlockZ();
|
int z = generationCenter.getMiddleBlockZ();
|
||||||
|
|
||||||
@@ -248,8 +250,9 @@ public class NativeStructureWorldgenAccessTest {
|
|||||||
recording.world(), generationCenter(),
|
recording.world(), generationCenter(),
|
||||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||||
false,
|
false, true,
|
||||||
position::equals);
|
position::equals,
|
||||||
|
NativeBlockChangeSet.disabled());
|
||||||
|
|
||||||
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
|
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
|
||||||
assertFalse(access.ensureCanWrite(position));
|
assertFalse(access.ensureCanWrite(position));
|
||||||
@@ -319,8 +322,9 @@ public class NativeStructureWorldgenAccessTest {
|
|||||||
recording.world(), generationCenter(),
|
recording.world(), generationCenter(),
|
||||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||||
false,
|
false, true,
|
||||||
position -> false);
|
position -> false,
|
||||||
|
NativeBlockChangeSet.disabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ChunkPos generationCenter() {
|
private static ChunkPos generationCenter() {
|
||||||
|
|||||||
+6
@@ -34,6 +34,7 @@ import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
|||||||
import net.minecraft.world.level.levelgen.structure.structures.IglooPieces;
|
import net.minecraft.world.level.levelgen.structure.structures.IglooPieces;
|
||||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
|
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
|
||||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
|
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
|
||||||
|
import org.junit.AfterClass;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
@@ -68,6 +69,11 @@ public class WorldgenTerrainHeightmapsTest {
|
|||||||
NativeBlockTags.bindHeightmapFixtures();
|
NativeBlockTags.bindHeightmapFixtures();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void restoreBlockTags() {
|
||||||
|
NativeBlockTags.restoreTags();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authoringHeightmapsReadPlacedObjectsAndFluidsWithoutTerrainResolvers() {
|
public void authoringHeightmapsReadPlacedObjectsAndFluidsWithoutTerrainResolvers() {
|
||||||
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
||||||
|
|||||||
+4
@@ -13,6 +13,7 @@ import com.google.gson.JsonParser;
|
|||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.junit.AfterClass;
|
import org.junit.AfterClass;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -26,6 +27,9 @@ import static org.junit.Assert.assertNull;
|
|||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
|
|
||||||
public final class BareMobSpawnHistoryTest {
|
public final class BareMobSpawnHistoryTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
private static final String LEGACY_RENDERER_IDENTITY = "bukkit-v26_3_R1-generated-registry-json-v1";
|
private static final String LEGACY_RENDERER_IDENTITY = "bukkit-v26_3_R1-generated-registry-json-v1";
|
||||||
private static final String NATURAL_MOB_SPAWNS = "minecraft:gameplay/natural_mob_spawns";
|
private static final String NATURAL_MOB_SPAWNS = "minecraft:gameplay/natural_mob_spawns";
|
||||||
private static final GenerationRegistryContract.PhysicalResourceKey INSTALLED =
|
private static final GenerationRegistryContract.PhysicalResourceKey INSTALLED =
|
||||||
|
|||||||
+4
@@ -29,6 +29,7 @@ import net.minecraft.world.level.levelgen.structure.StructureStart;
|
|||||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
|
import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
|
||||||
@@ -49,6 +50,9 @@ import static org.mockito.Mockito.spy;
|
|||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
public class HeadlessChunkGeneratorTest {
|
public class HeadlessChunkGeneratorTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public TemporaryFolder temporary = new TemporaryFolder();
|
public TemporaryFolder temporary = new TemporaryFolder();
|
||||||
|
|
||||||
|
|||||||
+4
@@ -31,6 +31,7 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemp
|
|||||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
@@ -48,6 +49,9 @@ import static org.junit.Assert.assertThrows;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public final class HeadlessChunkLightingTest {
|
public final class HeadlessChunkLightingTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public TemporaryFolder temporary = new TemporaryFolder();
|
public TemporaryFolder temporary = new TemporaryFolder();
|
||||||
|
|
||||||
|
|||||||
+4
@@ -38,6 +38,7 @@ import net.minecraft.world.ticks.ScheduledTick;
|
|||||||
import org.bukkit.NamespacedKey;
|
import org.bukkit.NamespacedKey;
|
||||||
import org.bukkit.persistence.PersistentDataType;
|
import org.bukkit.persistence.PersistentDataType;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
@@ -54,6 +55,9 @@ import static org.mockito.Mockito.mock;
|
|||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class HeadlessChunkSerializationTest {
|
public class HeadlessChunkSerializationTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public TemporaryFolder temporary = new TemporaryFolder();
|
public TemporaryFolder temporary = new TemporaryFolder();
|
||||||
|
|
||||||
|
|||||||
+4
@@ -9,6 +9,7 @@ import net.minecraft.world.item.Items;
|
|||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import net.minecraft.world.level.block.Blocks;
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
|
|
||||||
@@ -18,6 +19,9 @@ import static org.junit.Assert.assertEquals;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public final class HeadlessNativeBootstrapTest {
|
public final class HeadlessNativeBootstrapTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void bindsNativeItemComponentsWithoutStartingServer() throws Exception {
|
public void bindsNativeItemComponentsWithoutStartingServer() throws Exception {
|
||||||
HeadlessNativeBootstrap.bindComponents(HeadlessNativeTestRegistries.get());
|
HeadlessNativeBootstrap.bindComponents(HeadlessNativeTestRegistries.get());
|
||||||
|
|||||||
+4
@@ -7,12 +7,16 @@ import net.minecraft.resources.Identifier;
|
|||||||
import net.minecraft.resources.ResourceKey;
|
import net.minecraft.resources.ResourceKey;
|
||||||
import net.minecraft.world.level.biome.Biome;
|
import net.minecraft.world.level.biome.Biome;
|
||||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public final class HeadlessNativeRegistriesTest {
|
public final class HeadlessNativeRegistriesTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void nativeStructureBiomeTagsUseTheLoadedBiomeHolders() throws Exception {
|
public void nativeStructureBiomeTagsUseTheLoadedBiomeHolders() throws Exception {
|
||||||
RegistryAccess registries = HeadlessNativeTestRegistries.get();
|
RegistryAccess registries = HeadlessNativeTestRegistries.get();
|
||||||
|
|||||||
+4
@@ -4,6 +4,7 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeRegistryAccess;
|
|||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -17,6 +18,9 @@ import static org.junit.Assert.assertFalse;
|
|||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
|
|
||||||
public final class HeadlessNativeRegistryTest {
|
public final class HeadlessNativeRegistryTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void bootstrap() {
|
public static void bootstrap() {
|
||||||
HeadlessNativeBootstrap.initialize();
|
HeadlessNativeBootstrap.initialize();
|
||||||
|
|||||||
+4
@@ -11,6 +11,7 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeTileReader;
|
|||||||
import net.minecraft.nbt.CompoundTag;
|
import net.minecraft.nbt.CompoundTag;
|
||||||
import net.minecraft.data.registries.VanillaRegistries;
|
import net.minecraft.data.registries.VanillaRegistries;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
@@ -23,6 +24,9 @@ import static org.junit.Assert.assertEquals;
|
|||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
|
|
||||||
public final class HeadlessNativeTileTest {
|
public final class HeadlessNativeTileTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void bootstrap() {
|
public static void bootstrap() {
|
||||||
HeadlessNativeBootstrap.initialize();
|
HeadlessNativeBootstrap.initialize();
|
||||||
|
|||||||
+4
@@ -17,6 +17,7 @@ import org.bukkit.Bukkit;
|
|||||||
import org.bukkit.NamespacedKey;
|
import org.bukkit.NamespacedKey;
|
||||||
import org.bukkit.persistence.PersistentDataType;
|
import org.bukkit.persistence.PersistentDataType;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
import org.spigotmc.SpigotWorldConfig;
|
import org.spigotmc.SpigotWorldConfig;
|
||||||
@@ -36,6 +37,9 @@ import static org.junit.Assert.assertSame;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public final class HeadlessRegionTerrainTest {
|
public final class HeadlessRegionTerrainTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public TemporaryFolder temporary = new TemporaryFolder();
|
public TemporaryFolder temporary = new TemporaryFolder();
|
||||||
|
|
||||||
|
|||||||
+4
@@ -24,6 +24,7 @@ import net.minecraft.world.level.block.state.BlockState;
|
|||||||
import net.minecraft.world.level.chunk.storage.RegionFile;
|
import net.minecraft.world.level.chunk.storage.RegionFile;
|
||||||
import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
|
import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
@@ -38,6 +39,9 @@ import static org.junit.Assert.assertThrows;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public final class NativeRegionTerrainWriterTest {
|
public final class NativeRegionTerrainWriterTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void bootstrap() {
|
public static void bootstrap() {
|
||||||
SharedConstants.tryDetectVersion();
|
SharedConstants.tryDetectVersion();
|
||||||
|
|||||||
+4
@@ -26,6 +26,7 @@ import net.minecraft.world.level.material.Fluids;
|
|||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.junit.AfterClass;
|
import org.junit.AfterClass;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -40,6 +41,9 @@ import static org.junit.Assert.assertThrows;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public final class OfflineNativeFeatureFeasibilityTest {
|
public final class OfflineNativeFeatureFeasibilityTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
private static HeadlessNativeRegistries loaded;
|
private static HeadlessNativeRegistries loaded;
|
||||||
private static RegistryAccess registries;
|
private static RegistryAccess registries;
|
||||||
private static DimensionType dimension;
|
private static DimensionType dimension;
|
||||||
|
|||||||
+4
@@ -1,6 +1,7 @@
|
|||||||
package art.arcane.iris.probe;
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
import org.mockito.MockedStatic;
|
import org.mockito.MockedStatic;
|
||||||
@@ -22,6 +23,9 @@ import static org.mockito.Mockito.mock;
|
|||||||
import static org.mockito.Mockito.mockStatic;
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
|
||||||
public final class OfflineRegionPublicationTest {
|
public final class OfflineRegionPublicationTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public TemporaryFolder temporary = new TemporaryFolder();
|
public TemporaryFolder temporary = new TemporaryFolder();
|
||||||
|
|
||||||
|
|||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import io.papermc.paper.registry.PaperRegistryAccess;
|
||||||
|
import org.junit.rules.TestRule;
|
||||||
|
import org.junit.runner.Description;
|
||||||
|
import org.junit.runners.model.Statement;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class PaperRegistryIsolation implements TestRule {
|
||||||
|
@Override
|
||||||
|
public Statement apply(Statement base, Description description) {
|
||||||
|
return new Statement() {
|
||||||
|
@Override
|
||||||
|
public void evaluate() throws Throwable {
|
||||||
|
Map<Object, Object> registries = registries();
|
||||||
|
Map<Object, Object> previous = new HashMap<>(registries);
|
||||||
|
registries.clear();
|
||||||
|
try {
|
||||||
|
base.evaluate();
|
||||||
|
} finally {
|
||||||
|
registries.clear();
|
||||||
|
registries.putAll(previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static Map<Object, Object> registries() throws ReflectiveOperationException {
|
||||||
|
Field field = PaperRegistryAccess.class.getDeclaredField("registries");
|
||||||
|
field.setAccessible(true);
|
||||||
|
return (Map<Object, Object>) field.get(PaperRegistryAccess.instance());
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
-8
@@ -9,6 +9,7 @@ import art.arcane.iris.structure.nativegen.NativeStructureVolumeIndex;
|
|||||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.structure.StructureTerrainSettings;
|
||||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import net.minecraft.SharedConstants;
|
import net.minecraft.SharedConstants;
|
||||||
@@ -69,7 +70,8 @@ public class NativeStructureVolumeIndexTest {
|
|||||||
StructureStart start = swampHut(0, 0, 4, 6);
|
StructureStart start = swampHut(0, 0, 4, 6);
|
||||||
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||||
|
|
||||||
KList<NativeStructureVolume> volumes = NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, start);
|
KList<NativeStructureVolume> volumes = NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, start,
|
||||||
|
StructureTerrainSettings.SOURCE, (x, z) -> 63);
|
||||||
|
|
||||||
assertEquals(1, volumes.size());
|
assertEquals(1, volumes.size());
|
||||||
NativeStructureVolume volume = volumes.getFirst();
|
NativeStructureVolume volume = volumes.getFirst();
|
||||||
@@ -84,8 +86,10 @@ public class NativeStructureVolumeIndexTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void invalidStartsContributeNoVolumes() {
|
public void invalidStartsContributeNoVolumes() {
|
||||||
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, StructureStart.INVALID_START));
|
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, StructureStart.INVALID_START,
|
||||||
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, null));
|
StructureTerrainSettings.SOURCE, (x, z) -> 63));
|
||||||
|
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, null,
|
||||||
|
StructureTerrainSettings.SOURCE, (x, z) -> 63));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -323,7 +327,7 @@ public class NativeStructureVolumeIndexTest {
|
|||||||
BlockingResolver coordination = new BlockingResolver();
|
BlockingResolver coordination = new BlockingResolver();
|
||||||
NativeStructureVolumeIndex.OriginResolver volumes = (engine, chunkX, chunkZ) ->
|
NativeStructureVolumeIndex.OriginResolver volumes = (engine, chunkX, chunkZ) ->
|
||||||
new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
|
new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
|
||||||
-256, 0, -256, 256, 255, 256));
|
-256, 0, -256, 256, 255, 256, null));
|
||||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting((engine, chunkX, chunkZ) -> {
|
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting((engine, chunkX, chunkZ) -> {
|
||||||
coordination.volumesAt(engine, chunkX, chunkZ);
|
coordination.volumesAt(engine, chunkX, chunkZ);
|
||||||
return volumes.volumesAt(engine, chunkX, chunkZ);
|
return volumes.volumesAt(engine, chunkX, chunkZ);
|
||||||
@@ -369,7 +373,7 @@ public class NativeStructureVolumeIndexTest {
|
|||||||
terrain.join();
|
terrain.join();
|
||||||
}
|
}
|
||||||
return new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
|
return new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
|
||||||
-256, 0, -256, 256, 100, 256));
|
-256, 0, -256, 256, 100, 256, null));
|
||||||
});
|
});
|
||||||
ExecutorService owner = Executors.newSingleThreadExecutor();
|
ExecutorService owner = Executors.newSingleThreadExecutor();
|
||||||
ForkJoinPool workers = new ForkJoinPool(2);
|
ForkJoinPool workers = new ForkJoinPool(2);
|
||||||
@@ -493,7 +497,13 @@ public class NativeStructureVolumeIndexTest {
|
|||||||
private static Map<?, ?> cache(NativeStructureVolumeIndex index, String name) throws Exception {
|
private static Map<?, ?> cache(NativeStructureVolumeIndex index, String name) throws Exception {
|
||||||
Field field = NativeStructureVolumeIndex.class.getDeclaredField(name);
|
Field field = NativeStructureVolumeIndex.class.getDeclaredField(name);
|
||||||
field.setAccessible(true);
|
field.setAccessible(true);
|
||||||
return (Map<?, ?>) field.get(index);
|
Object value = field.get(index);
|
||||||
|
if (value instanceof Map<?, ?> map) {
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
Field entries = value.getClass().getDeclaredField("entries");
|
||||||
|
entries.setAccessible(true);
|
||||||
|
return (Map<?, ?>) entries.get(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static StructureStart swampHut(int chunkX, int chunkZ, int x, int z) {
|
private static StructureStart swampHut(int chunkX, int chunkZ, int x, int z) {
|
||||||
@@ -544,7 +554,8 @@ public class NativeStructureVolumeIndexTest {
|
|||||||
if (chunkX != originChunkX || chunkZ != originChunkZ) {
|
if (chunkX != originChunkX || chunkZ != originChunkZ) {
|
||||||
return NativeStructureVolume.NONE;
|
return NativeStructureVolume.NONE;
|
||||||
}
|
}
|
||||||
return NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, swampHut(chunkX, chunkZ, 4, 6));
|
return NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, swampHut(chunkX, chunkZ, 4, 6),
|
||||||
|
StructureTerrainSettings.SOURCE, (x, z) -> 63);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,7 +607,7 @@ public class NativeStructureVolumeIndexTest {
|
|||||||
return new KList<>(new NativeStructureVolume(
|
return new KList<>(new NativeStructureVolume(
|
||||||
chunkX + ":" + chunkZ,
|
chunkX + ":" + chunkZ,
|
||||||
0, 0, 0,
|
0, 0, 0,
|
||||||
15, 255, 15));
|
15, 255, 15, null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private record ScopedEngine(IrisEngine engine, AtomicInteger scopes) {
|
private record ScopedEngine(IrisEngine engine, AtomicInteger scopes) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package art.arcane.iris;
|
package art.arcane.iris;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletionStage;
|
||||||
import art.arcane.volmlib.util.diagnostics.BukkitDebugDump;
|
import art.arcane.volmlib.util.diagnostics.BukkitDebugDump;
|
||||||
import art.arcane.iris.generation.runtime.IrisEngineEffects;
|
import art.arcane.iris.generation.runtime.IrisEngineEffects;
|
||||||
import art.arcane.iris.generation.runtime.IrisWorldManager;
|
import art.arcane.iris.generation.runtime.IrisWorldManager;
|
||||||
@@ -36,6 +37,7 @@ import art.arcane.iris.world.PendingWorldDeleteQueue;
|
|||||||
import art.arcane.iris.world.PendingWorldReplacementManager;
|
import art.arcane.iris.world.PendingWorldReplacementManager;
|
||||||
import art.arcane.iris.world.WorldRefusalReporter;
|
import art.arcane.iris.world.WorldRefusalReporter;
|
||||||
import art.arcane.iris.configuration.SettingsHotloadWatch;
|
import art.arcane.iris.configuration.SettingsHotloadWatch;
|
||||||
|
import art.arcane.iris.update.IrisUpdateNotifier;
|
||||||
import art.arcane.iris.pack.datapack.ServerConfigurator;
|
import art.arcane.iris.pack.datapack.ServerConfigurator;
|
||||||
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
||||||
import art.arcane.iris.pack.datapack.DatapackIngestService.StartupValidationOutcome;
|
import art.arcane.iris.pack.datapack.DatapackIngestService.StartupValidationOutcome;
|
||||||
@@ -131,8 +133,11 @@ import org.bukkit.command.Command;
|
|||||||
import org.bukkit.command.CommandSender;
|
import org.bukkit.command.CommandSender;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.Event;
|
import org.bukkit.event.Event;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
import org.bukkit.event.HandlerList;
|
import org.bukkit.event.HandlerList;
|
||||||
import org.bukkit.event.Listener;
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.server.PluginDisableEvent;
|
||||||
|
import org.bukkit.event.server.PluginEnableEvent;
|
||||||
import org.bukkit.generator.BiomeProvider;
|
import org.bukkit.generator.BiomeProvider;
|
||||||
import org.bukkit.generator.ChunkGenerator;
|
import org.bukkit.generator.ChunkGenerator;
|
||||||
import org.bukkit.plugin.IllegalPluginAccessException;
|
import org.bukkit.plugin.IllegalPluginAccessException;
|
||||||
@@ -204,6 +209,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
private volatile PlaceholderRegistration papiRegistration;
|
private volatile PlaceholderRegistration papiRegistration;
|
||||||
private volatile IrisPapiListener papiListener;
|
private volatile IrisPapiListener papiListener;
|
||||||
private volatile IrisPapiState papiState;
|
private volatile IrisPapiState papiState;
|
||||||
|
private volatile boolean papiLifecycleActive;
|
||||||
private KMap<Class<? extends IrisService>, IrisService> services;
|
private KMap<Class<? extends IrisService>, IrisService> services;
|
||||||
// Copy-on-write: mutated on the main thread during enable() and iterated by the JVM
|
// Copy-on-write: mutated on the main thread during enable() and iterated by the JVM
|
||||||
// shutdown-hook thread during teardown; a plain list would CME and abort the teardown.
|
// shutdown-hook thread during teardown; a plain list would CME and abort the teardown.
|
||||||
@@ -214,9 +220,9 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
private final PendingWorldDeleteQueue pendingWorldDeletes = new PendingWorldDeleteQueue(this);
|
private final PendingWorldDeleteQueue pendingWorldDeletes = new PendingWorldDeleteQueue(this);
|
||||||
private final PendingWorldReplacementManager pendingWorldReplacements = new PendingWorldReplacementManager(this);
|
private final PendingWorldReplacementManager pendingWorldReplacements = new PendingWorldReplacementManager(this);
|
||||||
private BukkitLanguageSwitcher languageSwitcher;
|
private BukkitLanguageSwitcher languageSwitcher;
|
||||||
|
private IrisUpdateNotifier updateNotifier;
|
||||||
private BukkitDebugDump debugDump;
|
private BukkitDebugDump debugDump;
|
||||||
private volatile SettingsHotloadWatch settingsHotloadWatch;
|
private volatile SettingsHotloadWatch settingsHotloadWatch;
|
||||||
private volatile Thread serverLifecycleThread;
|
|
||||||
private volatile ServerShutdownBoundary serverShutdownBoundary;
|
private volatile ServerShutdownBoundary serverShutdownBoundary;
|
||||||
|
|
||||||
public static VolmitSender getSender() {
|
public static VolmitSender getSender() {
|
||||||
@@ -636,6 +642,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
languageSwitcher = BukkitLanguageSwitcher.register(this, IrisLanguage.selections(),
|
languageSwitcher = BukkitLanguageSwitcher.register(this, IrisLanguage.selections(),
|
||||||
new BukkitLanguageSwitcher.Options("iris", "iris.all",
|
new BukkitLanguageSwitcher.Options("iris", "iris.all",
|
||||||
DirectorMiniMenu.Theme.irisGreen(), IrisLanguage.directorResolver(), IrisLanguage.editorOptions()));
|
DirectorMiniMenu.Theme.irisGreen(), IrisLanguage.directorResolver(), IrisLanguage.editorOptions()));
|
||||||
|
updateNotifier = new IrisUpdateNotifier(this);
|
||||||
PaperLibBootstrap.install();
|
PaperLibBootstrap.install();
|
||||||
SimdSupport.install();
|
SimdSupport.install();
|
||||||
timings.mark("bootstrap");
|
timings.mark("bootstrap");
|
||||||
@@ -824,7 +831,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
|
|
||||||
public void addShutdownHook() {
|
public void addShutdownHook() {
|
||||||
removeShutdownHook();
|
removeShutdownHook();
|
||||||
serverLifecycleThread = Thread.currentThread();
|
|
||||||
serverShutdownBoundary = INMS.isBound() ? INMS.get().createServerShutdownBoundary() : null;
|
serverShutdownBoundary = INMS.isBound() ? INMS.get().createServerShutdownBoundary() : null;
|
||||||
shutdownHook = new Thread(this::runShutdownHook, "Iris-ShutdownHook");
|
shutdownHook = new Thread(this::runShutdownHook, "Iris-ShutdownHook");
|
||||||
try {
|
try {
|
||||||
@@ -863,7 +869,14 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
*/
|
*/
|
||||||
private CompletableFuture<ManagedWorldLoader.ManagedWorldLoad> loadManagedWorld(String configuredWorldName) {
|
private CompletableFuture<ManagedWorldLoader.ManagedWorldLoad> loadManagedWorld(String configuredWorldName) {
|
||||||
return worldReconciler.loadWorld(ServerProperties.BUKKIT_YML, configuredWorldName)
|
return worldReconciler.loadWorld(ServerProperties.BUKKIT_YML, configuredWorldName)
|
||||||
.thenApply(result -> new ManagedWorldLoader.ManagedWorldLoad(result.succeeded(), result.message()));
|
.thenApply(this::managedWorldLoadResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ManagedWorldLoader.ManagedWorldLoad managedWorldLoadResult(BukkitWorldReconciler.LoadResult result) {
|
||||||
|
if (!result.succeeded() && result.failure() != null) {
|
||||||
|
reportError("Could not explicitly load Iris world " + result.worldKey() + ": " + result.message(), result.failure());
|
||||||
|
}
|
||||||
|
return new ManagedWorldLoader.ManagedWorldLoad(result.succeeded(), result.message());
|
||||||
}
|
}
|
||||||
|
|
||||||
public PendingWorldReplacementManager pendingWorldReplacements() {
|
public PendingWorldReplacementManager pendingWorldReplacements() {
|
||||||
@@ -927,6 +940,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
ServerConfigurator.restartAtStartupBoundary(restartReason);
|
ServerConfigurator.restartAtStartupBoundary(restartReason);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
papiLifecycleActive = true;
|
||||||
|
setupPapi();
|
||||||
reportLockedRuntime();
|
reportLockedRuntime();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -973,6 +988,12 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
|
preservePendingBlockDrops();
|
||||||
|
papiLifecycleActive = false;
|
||||||
|
if (updateNotifier != null) {
|
||||||
|
updateNotifier.close();
|
||||||
|
updateNotifier = null;
|
||||||
|
}
|
||||||
if (debugDump != null) {
|
if (debugDump != null) {
|
||||||
debugDump.close();
|
debugDump.close();
|
||||||
debugDump = null;
|
debugDump = null;
|
||||||
@@ -1011,20 +1032,23 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPreUnload(ReloadAware.PreUnloadReason reason) {
|
public CompletionStage<Void> commitReload(ReloadAware.PreUnloadReason reason) {
|
||||||
|
preservePendingBlockDrops();
|
||||||
|
papiLifecycleActive = false;
|
||||||
teardownPapi();
|
teardownPapi();
|
||||||
if (IrisToolbelt.isServerStopping()) {
|
if (IrisToolbelt.isServerStopping()) {
|
||||||
quiesceRuntimeForServerShutdown("pre-unload:" + reason);
|
quiesceRuntimeForServerShutdown("pre-unload:" + reason);
|
||||||
startPostStopFinisher();
|
startPostStopFinisher();
|
||||||
Iris.debug("Pre-unload hook deferred generator teardown until Paper closes its chunk schedulers.");
|
Iris.debug("Pre-unload hook deferred generator teardown until Paper closes its chunk schedulers.");
|
||||||
return;
|
return CompletableFuture.completedFuture(null);
|
||||||
}
|
}
|
||||||
if (alreadyDrained.get()) {
|
if (alreadyDrained.get()) {
|
||||||
Iris.debug("Pre-unload hook skipped; Iris already drained.");
|
Iris.debug("Pre-unload hook skipped; Iris already drained.");
|
||||||
return;
|
return CompletableFuture.completedFuture(null);
|
||||||
}
|
}
|
||||||
Iris.debug("BileTools pre-unload hook fired (" + reason + "). Freezing all Iris worlds.");
|
Iris.debug("BileTools pre-unload hook fired (" + reason + "). Freezing all Iris worlds.");
|
||||||
drainOnce("pre-unload:" + reason, 45L);
|
drainOnce("pre-unload:" + reason, 45L);
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1138,7 +1162,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
if (generator == null) {
|
if (generator == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
IrisToolbelt.beginWorldMaintenance(world, reason, true);
|
IrisToolbelt.beginWorldMaintenance(world, reason);
|
||||||
if (!deferredShutdownGenerators.contains(generator)) {
|
if (!deferredShutdownGenerators.contains(generator)) {
|
||||||
deferredShutdownGenerators.add(generator);
|
deferredShutdownGenerators.add(generator);
|
||||||
}
|
}
|
||||||
@@ -1150,52 +1174,32 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
if (!postStopFinisherStarted.compareAndSet(false, true)) {
|
if (!postStopFinisherStarted.compareAndSet(false, true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Thread activeServerThread = serverLifecycleThread;
|
Thread finisher = new Thread(() -> finishRuntimeAfterServerShutdown("post-server-stop"),
|
||||||
if (activeServerThread == null) {
|
"Iris-PostStop-Finisher");
|
||||||
Iris.warn("Iris could not start its post-stop runtime finisher because the server lifecycle thread is unavailable.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Thread finisher = new Thread(() -> {
|
|
||||||
if (!awaitServerThreadTermination(activeServerThread)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
finishDeferredRuntimeTeardown("post-server-stop", 30L);
|
|
||||||
}, "Iris-PostStop-Finisher");
|
|
||||||
finisher.setDaemon(false);
|
finisher.setDaemon(false);
|
||||||
finisher.start();
|
finisher.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
static boolean awaitServerThreadTermination(Thread serverThread) {
|
|
||||||
if (serverThread == null || serverThread == Thread.currentThread()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
serverThread.join();
|
|
||||||
return true;
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
Iris.reportError("Iris post-stop runtime finisher was interrupted.", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void runShutdownHook() {
|
private void runShutdownHook() {
|
||||||
if (startupBoundaryRestart.get()) {
|
if (startupBoundaryRestart.get()) {
|
||||||
finishDeferredRuntimeTeardown("startup-boundary-restart-hook", 30L);
|
finishDeferredRuntimeTeardown("startup-boundary-restart-hook", 30L);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
finishRuntimeAfterServerShutdown("shutdown-hook");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishRuntimeAfterServerShutdown(String reason) {
|
||||||
if (!awaitServerShutdownBoundary()) {
|
if (!awaitServerShutdownBoundary()) {
|
||||||
SHUTDOWN_ERRORS.println("[Iris] Paper did not reach its post-world-close boundary; retaining the runtime and plugin class loader until JVM exit.");
|
SHUTDOWN_ERRORS.println("[Iris] The server did not reach its post-world-close boundary; retaining the runtime and plugin class loader until JVM exit.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
finishDeferredRuntimeTeardown("shutdown-hook", 30L);
|
finishDeferredRuntimeTeardown(reason, 30L);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean awaitServerShutdownBoundary() {
|
private boolean awaitServerShutdownBoundary() {
|
||||||
ServerShutdownBoundary boundary = serverShutdownBoundary;
|
ServerShutdownBoundary boundary = serverShutdownBoundary;
|
||||||
if (boundary == null) {
|
if (boundary == null) {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return boundary.await(
|
return boundary.await(
|
||||||
@@ -1259,6 +1263,20 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
&& preservation.hasActiveResources();
|
&& preservation.hasActiveResources();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void preservePendingBlockDrops() {
|
||||||
|
for (World world : Bukkit.getWorlds()) {
|
||||||
|
PlatformChunkGenerator generator = IrisToolbelt.access(world);
|
||||||
|
if (generator != null && generator.getEngine() != null
|
||||||
|
&& generator.getEngine().getWorldManager() instanceof IrisWorldManager manager) {
|
||||||
|
manager.flushPendingBlockDropsAsync().whenComplete((ignored, failure) -> {
|
||||||
|
if (failure != null) {
|
||||||
|
Iris.reportError("Failed to preserve pending block loot before Iris teardown.", failure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void drainWorldGenerators(String reason, long timeoutSeconds) {
|
private void drainWorldGenerators(String reason, long timeoutSeconds) {
|
||||||
List<World> irisWorlds = new ArrayList<>();
|
List<World> irisWorlds = new ArrayList<>();
|
||||||
List<PlatformChunkGenerator> generators = new ArrayList<>();
|
List<PlatformChunkGenerator> generators = new ArrayList<>();
|
||||||
@@ -1313,8 +1331,23 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupPapi() {
|
@EventHandler
|
||||||
if (!PlaceholderRegistration.isPlaceholderApiEnabled()) {
|
public void onPlaceholderApiEnable(PluginEnableEvent event) {
|
||||||
|
if ("PlaceholderAPI".equals(event.getPlugin().getName())) {
|
||||||
|
setupPapi();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPlaceholderApiDisable(PluginDisableEvent event) {
|
||||||
|
if ("PlaceholderAPI".equals(event.getPlugin().getName())) {
|
||||||
|
teardownPapi();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void setupPapi() {
|
||||||
|
if (!papiLifecycleActive || papiRegistration != null
|
||||||
|
|| !PlaceholderRegistration.isPlaceholderApiEnabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1341,7 +1374,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
papiRegistration = registration;
|
papiRegistration = registration;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void teardownPapi() {
|
private synchronized void teardownPapi() {
|
||||||
IrisPapiListener listener = papiListener;
|
IrisPapiListener listener = papiListener;
|
||||||
papiListener = null;
|
papiListener = null;
|
||||||
|
|
||||||
@@ -1453,12 +1486,15 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
public int getIrisVersion() {
|
public int getIrisVersion() {
|
||||||
String input = getDescription().getVersion();
|
String input = getDescription().getVersion();
|
||||||
int hyphenIndex = input.indexOf('-');
|
int hyphenIndex = input.indexOf('-');
|
||||||
if (hyphenIndex != -1) {
|
String release = hyphenIndex < 0 ? input : input.substring(0, hyphenIndex);
|
||||||
String result = input.substring(0, hyphenIndex);
|
if (!release.matches("[0-9]+(?:\\.[0-9]+)*")) {
|
||||||
result = result.replaceAll("\\.", "");
|
return -1;
|
||||||
return Integer.parseInt(result);
|
}
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(release.replace(".", ""));
|
||||||
|
} catch (NumberFormatException invalid) {
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getMCVersion() {
|
public int getMCVersion() {
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import art.arcane.iris.localization.IrisLanguage;
|
|||||||
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
||||||
import art.arcane.iris.studio.object.ObjectStudioSaveService;
|
import art.arcane.iris.studio.object.ObjectStudioSaveService;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.locator.SubterrainLocator;
|
||||||
|
import art.arcane.iris.generation.subterrain.IrisSubterrainFamily;
|
||||||
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||||
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
||||||
import art.arcane.iris.structure.placement.StructureReachability;
|
import art.arcane.iris.structure.placement.StructureReachability;
|
||||||
@@ -72,7 +74,7 @@ import java.util.Set;
|
|||||||
|
|
||||||
@Director(name = "find", origin = DirectorOrigin.PLAYER, description = "Iris Find commands", descriptionKey = "iris.director.commandfind.director.iris_find_commands", aliases = "goto")
|
@Director(name = "find", origin = DirectorOrigin.PLAYER, description = "Iris Find commands", descriptionKey = "iris.director.commandfind.director.iris_find_commands", aliases = "goto")
|
||||||
public class CommandFind implements DirectorExecutor {
|
public class CommandFind implements DirectorExecutor {
|
||||||
@Director(description = "Find a biome", descriptionKey = "iris.director.commandfind.director.find_biome")
|
@Director(description = "Find a biome", sync = true, descriptionKey = "iris.director.commandfind.director.find_biome")
|
||||||
public void biome(
|
public void biome(
|
||||||
@Param(description = "The biome to look for", descriptionKey = "iris.director.commandfind.param.biome_look", customHandler = ReachableBiomeHandler.class)
|
@Param(description = "The biome to look for", descriptionKey = "iris.director.commandfind.param.biome_look", customHandler = ReachableBiomeHandler.class)
|
||||||
IrisBiome biome,
|
IrisBiome biome,
|
||||||
@@ -89,6 +91,78 @@ public class CommandFind implements DirectorExecutor {
|
|||||||
EngineBukkitOps.gotoBiome(e, biome, player(), teleport);
|
EngineBukkitOps.gotoBiome(e, biome, player(), teleport);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Director(description = "Find an authored underground feature by id or geometry family", sync = true)
|
||||||
|
public void subterrain(
|
||||||
|
@Param(description = "Feature definition id or family: tectonic_fault, cenote, lava_tube, travertine_terraces")
|
||||||
|
String feature,
|
||||||
|
@Param(description = "Search radius in blocks", defaultValue = "8192")
|
||||||
|
int radius,
|
||||||
|
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||||
|
boolean teleport
|
||||||
|
) {
|
||||||
|
String key = feature.trim();
|
||||||
|
IrisSubterrainFamily family = null;
|
||||||
|
try {
|
||||||
|
family = IrisSubterrainFamily.valueOf(key.toUpperCase(Locale.ROOT).replace('-', '_'));
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
}
|
||||||
|
locateSubterrain(new SubterrainLocator.Query(family == null ? key : "", family, ""), radius, teleport);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Director(name = "underground-biome", description = "Find a bounded authored underground biome", sync = true)
|
||||||
|
public void undergroundBiome(
|
||||||
|
@Param(description = "The underground biome to look for", customHandler = ReachableBiomeHandler.class)
|
||||||
|
IrisBiome biome,
|
||||||
|
@Param(description = "Search radius in blocks", defaultValue = "8192")
|
||||||
|
int radius,
|
||||||
|
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||||
|
boolean teleport
|
||||||
|
) {
|
||||||
|
locateSubterrain(new SubterrainLocator.Query("", null, biome.getLoadKey()), radius, teleport);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void locateSubterrain(SubterrainLocator.Query query, int radius, boolean teleport) {
|
||||||
|
Engine activeEngine = engine();
|
||||||
|
VolmitSender commandSender = sender();
|
||||||
|
Player target = player();
|
||||||
|
if (activeEngine == null || commandSender == null || target == null) {
|
||||||
|
if (commandSender != null) {
|
||||||
|
commandSender.sendMessage(C.RED + "Run this command from an Iris world.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (radius < 0 || radius > 32768) {
|
||||||
|
commandSender.sendMessage(C.RED + "Search radius must be between 0 and 32768 blocks.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Location origin = target.getLocation();
|
||||||
|
World world = origin.getWorld();
|
||||||
|
commandSender.sendMessage(C.GRAY + "Searching authored underground plans...");
|
||||||
|
J.a(() -> {
|
||||||
|
try {
|
||||||
|
SubterrainLocator.Result found = GenerationSemanticQueries.nearestSubterrain(activeEngine, query,
|
||||||
|
origin.getBlockX(), origin.getBlockY(), origin.getBlockZ(), radius).orElse(null);
|
||||||
|
if (found == null) {
|
||||||
|
sendStructureMessage(target, commandSender,
|
||||||
|
C.YELLOW + "No matching authored underground volume found within " + radius + " blocks.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String label = found.family().name().toLowerCase(Locale.ROOT) + " " + found.featureId();
|
||||||
|
if (!teleport) {
|
||||||
|
sendStructureMessage(target, commandSender, C.GREEN + "Found " + label + " at "
|
||||||
|
+ found.x() + ", " + found.y() + ", " + found.z() + ".");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
prepareStructureTeleport(target, world, commandSender, label,
|
||||||
|
new Location(world, found.x(), found.y(), found.z()), true);
|
||||||
|
} catch (Throwable error) {
|
||||||
|
sendStructureMessage(target, commandSender,
|
||||||
|
C.RED + "Could not locate authored underground volume: " + error.getMessage());
|
||||||
|
Iris.reportError("Could not locate authored underground volume.", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Director(description = "Find a region", descriptionKey = "iris.director.commandfind.director.find_region")
|
@Director(description = "Find a region", descriptionKey = "iris.director.commandfind.director.find_region")
|
||||||
public void region(
|
public void region(
|
||||||
@Param(description = "The region to look for", descriptionKey = "iris.director.commandfind.param.region_look", customHandler = ReachableRegionHandler.class)
|
@Param(description = "The region to look for", descriptionKey = "iris.director.commandfind.param.region_look", customHandler = ReachableRegionHandler.class)
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
|
|
||||||
package art.arcane.iris.command;
|
package art.arcane.iris.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.PackFingerprints;
|
||||||
|
|
||||||
import art.arcane.iris.Iris;
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.iris.pack.validation.PackCompatReport;
|
import art.arcane.iris.pack.validation.PackCompatReport;
|
||||||
import art.arcane.iris.pack.PackDirectoryResolver;
|
import art.arcane.iris.pack.PackDirectoryResolver;
|
||||||
@@ -174,7 +176,6 @@ public class CommandPack implements DirectorExecutor {
|
|||||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_CLEANUP_CANDIDATES_FOUND_PACK, MessageArgument.untrusted("pack", String.valueOf(pack))));
|
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_CLEANUP_CANDIDATES_FOUND_PACK, MessageArgument.untrusted("pack", String.valueOf(pack))));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PackValidationRegistry.remove(packFolder.getName());
|
|
||||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_QUARANTINED_CLEANUP_CANDIDATE_S_UNDER, MessageArgument.untrusted("size", String.valueOf(result.quarantinedPaths().size())), MessageArgument.untrusted("quarantinePath", String.valueOf(result.quarantinePath()))));
|
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_QUARANTINED_CLEANUP_CANDIDATE_S_UNDER, MessageArgument.untrusted("size", String.valueOf(result.quarantinedPaths().size())), MessageArgument.untrusted("quarantinePath", String.valueOf(result.quarantinePath()))));
|
||||||
reportPaths(s, result.quarantinedPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_QUARANTINED);
|
reportPaths(s, result.quarantinedPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_QUARANTINED);
|
||||||
return;
|
return;
|
||||||
@@ -230,7 +231,6 @@ public class CommandPack implements DirectorExecutor {
|
|||||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NOTHING_RESTORE_PACK, MessageArgument.untrusted("pack", String.valueOf(pack))));
|
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NOTHING_RESTORE_PACK, MessageArgument.untrusted("pack", String.valueOf(pack))));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PackValidationRegistry.remove(packFolder.getName());
|
|
||||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RESTORED_FILE_S_FROM, MessageArgument.untrusted("size", String.valueOf(result.restoredPaths().size())), MessageArgument.untrusted("dumpPath", String.valueOf(result.dumpPath()))));
|
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RESTORED_FILE_S_FROM, MessageArgument.untrusted("size", String.valueOf(result.restoredPaths().size())), MessageArgument.untrusted("dumpPath", String.valueOf(result.dumpPath()))));
|
||||||
reportPaths(s, result.restoredPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_RESTORED);
|
reportPaths(s, result.restoredPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_RESTORED);
|
||||||
return;
|
return;
|
||||||
@@ -363,47 +363,79 @@ public class CommandPack implements DirectorExecutor {
|
|||||||
return persisted == null || persisted.isBlank() ? "unknown" : persisted;
|
return persisted == null || persisted.isBlank() ? "unknown" : persisted;
|
||||||
}
|
}
|
||||||
|
|
||||||
private PackValidationResult runValidate(VolmitSender s, File packFolder) {
|
PackValidationResult runValidate(VolmitSender s, File packFolder) {
|
||||||
|
PackValidationRegistry.ValidationTicket ticket =
|
||||||
|
PackValidationRegistry.tryBeginValidation(packFolder.toPath());
|
||||||
|
PackValidationResult result;
|
||||||
|
String contentFingerprint = "";
|
||||||
|
String contextFingerprint = "";
|
||||||
try {
|
try {
|
||||||
PackValidationResult result = PackValidator.validate(packFolder);
|
if (ticket == null) {
|
||||||
PackValidationRegistry.publish(result);
|
throw new IllegalStateException("Pack content is currently being changed.");
|
||||||
reportResult(s, result);
|
}
|
||||||
return result;
|
contentFingerprint = PackFingerprints.computePackTreeFingerprint(packFolder);
|
||||||
|
contextFingerprint = PackValidationCache.contextFingerprint();
|
||||||
|
result = PackValidator.validate(packFolder);
|
||||||
|
if (!contentFingerprint.equals(PackFingerprints.computePackTreeFingerprint(packFolder))
|
||||||
|
|| !contextFingerprint.equals(PackValidationCache.contextFingerprint())) {
|
||||||
|
throw new IllegalStateException("Pack content or validation context changed during validation.");
|
||||||
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Iris.reportError("Pack validation failed for '" + packFolder.getName() + "'", e);
|
Iris.reportError("Pack validation failed for '" + packFolder.getName() + "'", e);
|
||||||
String detail = e.getMessage() == null || e.getMessage().isBlank()
|
String detail = e.getMessage() == null || e.getMessage().isBlank()
|
||||||
? e.getClass().getSimpleName()
|
? e.getClass().getSimpleName()
|
||||||
: e.getMessage();
|
: e.getMessage();
|
||||||
PackValidationResult result = new PackValidationResult(
|
result = new PackValidationResult(
|
||||||
packFolder.getName(),
|
packFolder.getName(),
|
||||||
List.of("Pack validation failed with " + e.getClass().getSimpleName() + ": " + detail),
|
List.of("Pack validation failed with " + e.getClass().getSimpleName() + ": " + detail),
|
||||||
List.of(),
|
List.of(),
|
||||||
System.currentTimeMillis());
|
System.currentTimeMillis());
|
||||||
PackValidationRegistry.publish(result);
|
contentFingerprint = "";
|
||||||
reportResult(s, result);
|
contextFingerprint = "";
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
if (PackValidationRegistry.publishIfCurrent(ticket, result, contentFingerprint, contextFingerprint)) {
|
||||||
|
PackValidationRegistry.publish(result);
|
||||||
|
} else {
|
||||||
|
result = new PackValidationResult(packFolder.getName(),
|
||||||
|
List.of("Pack content changed during validation."), List.of(), System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
reportResult(s, result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void persistValidationCache(File packsRoot) {
|
private void persistValidationCache(File packsRoot) {
|
||||||
List<File> packDirectories = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
|
try {
|
||||||
List<PackValidationResult> results = new ArrayList<>(packDirectories.size());
|
String contextFingerprint = PackValidationCache.contextFingerprint();
|
||||||
for (File packDirectory : packDirectories) {
|
PackFingerprints.PackContentSnapshot snapshot = PackFingerprints.computePackContentSnapshot(packsRoot);
|
||||||
PackValidationResult result = PackValidationRegistry.get(packDirectory.getName());
|
List<File> packDirectories = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
|
||||||
if (result == null) {
|
List<PackValidationResult> results = new ArrayList<>(packDirectories.size());
|
||||||
// The boot cache only loads when it covers every pack, so a partial write is
|
for (File packDirectory : packDirectories) {
|
||||||
// pointless - but say so instead of silently skipping the persist forever.
|
PackValidationResult result = PackValidationRegistry.getMatching(
|
||||||
Iris.warn("Pack validation cache not written: \"" + packDirectory.getName()
|
packDirectory.toPath(), snapshot.packContents().get(packDirectory.getName()));
|
||||||
+ "\" has no registered result. Run /iris pack validate all to repopulate it.");
|
if (result == null) {
|
||||||
|
Iris.warn("Pack validation cache not written: \"" + packDirectory.getName()
|
||||||
|
+ "\" has no validation matching its current content. Run /iris pack validate to refresh it.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
results.add(result);
|
||||||
|
}
|
||||||
|
if (!snapshot.content().equals(PackFingerprints.computePackContentSnapshot(packsRoot).content())
|
||||||
|
|| !contextFingerprint.equals(PackValidationCache.contextFingerprint())) {
|
||||||
|
Iris.warn("Pack validation cache not written because pack content or validation context changed.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
results.add(result);
|
for (int index = 0; index < packDirectories.size(); index++) {
|
||||||
}
|
File packDirectory = packDirectories.get(index);
|
||||||
try {
|
if (PackValidationRegistry.getMatching(packDirectory.toPath(),
|
||||||
|
snapshot.packContents().get(packDirectory.getName())) != results.get(index)) {
|
||||||
|
Iris.warn("Pack validation cache not written because validation changed during persistence.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
PackValidationCache.save(
|
PackValidationCache.save(
|
||||||
Iris.instance.getDataFile("cache", "pack-validation.json").toPath(),
|
Iris.instance.getDataFile("cache", "pack-validation.json").toPath(),
|
||||||
PackValidationCache.contentFingerprint(packsRoot),
|
snapshot.content(),
|
||||||
PackValidationCache.contextFingerprint(),
|
contextFingerprint,
|
||||||
results);
|
results);
|
||||||
} catch (IOException | RuntimeException e) {
|
} catch (IOException | RuntimeException e) {
|
||||||
Iris.reportError("Could not persist refreshed pack-validation results", e);
|
Iris.reportError("Could not persist refreshed pack-validation results", e);
|
||||||
|
|||||||
@@ -81,7 +81,6 @@ import org.bukkit.FluidCollisionMode;
|
|||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.inventory.InventoryType;
|
|
||||||
import org.bukkit.inventory.Inventory;
|
import org.bukkit.inventory.Inventory;
|
||||||
import org.bukkit.util.Vector;
|
import org.bukkit.util.Vector;
|
||||||
|
|
||||||
@@ -309,23 +308,33 @@ public class CommandStudio implements DirectorExecutor {
|
|||||||
O<Integer> ta = new O<>();
|
O<Integer> ta = new O<>();
|
||||||
ta.set(-1);
|
ta.set(-1);
|
||||||
|
|
||||||
var sender = sender();
|
VolmitSender sender = sender();
|
||||||
var player = player();
|
Player player = player();
|
||||||
var engine = engine();
|
Engine engine = engine();
|
||||||
|
|
||||||
ta.set(J.sr(() ->
|
ta.set(J.sr(() -> {
|
||||||
{
|
if (!J.runEntity(player, () -> {
|
||||||
if (!player.getOpenInventory().getType().equals(InventoryType.CHEST)) {
|
if (player.getOpenInventory().getTopInventory() != inv) {
|
||||||
|
J.csr(ta.get());
|
||||||
|
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_OPENED_INVENTORY));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!add) {
|
||||||
|
inv.clear();
|
||||||
|
}
|
||||||
|
Location location = player.getLocation();
|
||||||
|
try {
|
||||||
|
EngineBukkitOps.addItems(engine, true, inv, tables, InventorySlotType.STORAGE,
|
||||||
|
location.getWorld(), location.getBlockX(), location.getBlockY(), location.getBlockZ());
|
||||||
|
} catch (RuntimeException | Error failure) {
|
||||||
|
J.csr(ta.get());
|
||||||
|
Iris.reportError(failure);
|
||||||
|
sender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CANNOT_ADD_ITEMS_VIRTUAL_INVENTORY_BECAUSE,
|
||||||
|
MessageArgument.untrusted("value", String.valueOf(failure.getMessage()))));
|
||||||
|
}
|
||||||
|
}, 0, () -> J.csr(ta.get()))) {
|
||||||
J.csr(ta.get());
|
J.csr(ta.get());
|
||||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_OPENED_INVENTORY));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!add) {
|
|
||||||
inv.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
EngineBukkitOps.addItems(engine, true, inv, tables, InventorySlotType.STORAGE, player.getWorld(), player.getLocation().getBlockX(), player.getLocation().getBlockY(), player.getLocation().getBlockZ());
|
|
||||||
}, fast ? 5 : 35));
|
}, fast ? 5 : 35));
|
||||||
|
|
||||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_INVENTORY_NOW));
|
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_INVENTORY_NOW));
|
||||||
|
|||||||
+29
-1
@@ -6,6 +6,7 @@ import art.arcane.iris.generation.runtime.IrisTelemetrySnapshot;
|
|||||||
import art.arcane.iris.Iris;
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.iris.generation.runtime.EngineTelemetrySnapshot;
|
import art.arcane.iris.generation.runtime.EngineTelemetrySnapshot;
|
||||||
import art.arcane.iris.spi.IrisServices;
|
import art.arcane.iris.spi.IrisServices;
|
||||||
|
import art.arcane.iris.world.task.J;
|
||||||
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
||||||
import art.arcane.volmlib.integration.IntegrationHandshakeRequest;
|
import art.arcane.volmlib.integration.IntegrationHandshakeRequest;
|
||||||
import art.arcane.volmlib.integration.IntegrationHandshakeResponse;
|
import art.arcane.volmlib.integration.IntegrationHandshakeResponse;
|
||||||
@@ -13,6 +14,9 @@ import art.arcane.volmlib.integration.IntegrationHeartbeat;
|
|||||||
import art.arcane.volmlib.integration.IntegrationMetricDescriptor;
|
import art.arcane.volmlib.integration.IntegrationMetricDescriptor;
|
||||||
import art.arcane.volmlib.integration.IntegrationMetricGroup;
|
import art.arcane.volmlib.integration.IntegrationMetricGroup;
|
||||||
import art.arcane.volmlib.integration.IntegrationMetricSample;
|
import art.arcane.volmlib.integration.IntegrationMetricSample;
|
||||||
|
import art.arcane.volmlib.integration.IntegrationMetricPublisher;
|
||||||
|
import art.arcane.volmlib.integration.IntegrationMetricSnapshot;
|
||||||
|
import art.arcane.volmlib.integration.IntegrationSnapshotProvider;
|
||||||
import art.arcane.volmlib.integration.IntegrationMetricSchema;
|
import art.arcane.volmlib.integration.IntegrationMetricSchema;
|
||||||
import art.arcane.volmlib.integration.IntegrationProtocolNegotiator;
|
import art.arcane.volmlib.integration.IntegrationProtocolNegotiator;
|
||||||
import art.arcane.volmlib.integration.IntegrationProtocolVersion;
|
import art.arcane.volmlib.integration.IntegrationProtocolVersion;
|
||||||
@@ -30,7 +34,7 @@ import java.util.Optional;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
public class IrisIntegrationService implements IrisService, IntegrationServiceContract {
|
public class IrisIntegrationService implements IrisService, IntegrationSnapshotProvider {
|
||||||
private static final IntegrationProtocolVersion CURRENT_PROTOCOL = new IntegrationProtocolVersion(1, 2);
|
private static final IntegrationProtocolVersion CURRENT_PROTOCOL = new IntegrationProtocolVersion(1, 2);
|
||||||
private static final Set<IntegrationProtocolVersion> SUPPORTED_PROTOCOLS = Set.of(
|
private static final Set<IntegrationProtocolVersion> SUPPORTED_PROTOCOLS = Set.of(
|
||||||
new IntegrationProtocolVersion(1, 0),
|
new IntegrationProtocolVersion(1, 0),
|
||||||
@@ -41,6 +45,7 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
|
|||||||
"handshake",
|
"handshake",
|
||||||
"heartbeat",
|
"heartbeat",
|
||||||
"metrics",
|
"metrics",
|
||||||
|
IntegrationSnapshotProvider.CAPABILITY,
|
||||||
"metric-groups",
|
"metric-groups",
|
||||||
"iris-engine-metrics",
|
"iris-engine-metrics",
|
||||||
"iris-world-metrics"
|
"iris-world-metrics"
|
||||||
@@ -63,6 +68,9 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
|
|||||||
);
|
);
|
||||||
|
|
||||||
private final Supplier<IrisTelemetrySnapshot> telemetrySupplier;
|
private final Supplier<IrisTelemetrySnapshot> telemetrySupplier;
|
||||||
|
private final IntegrationMetricPublisher snapshots = new IntegrationMetricPublisher(
|
||||||
|
IntegrationMetricSchema.irisKeys().size(), 30_000L);
|
||||||
|
private int snapshotTaskId = -1;
|
||||||
|
|
||||||
public IrisIntegrationService() {
|
public IrisIntegrationService() {
|
||||||
this(IrisIntegrationService::currentTelemetry);
|
this(IrisIntegrationService::currentTelemetry);
|
||||||
@@ -77,11 +85,17 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
|
|||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
Bukkit.getServicesManager().register(IntegrationServiceContract.class, this, Iris.instance, ServicePriority.Normal);
|
Bukkit.getServicesManager().register(IntegrationServiceContract.class, this, Iris.instance, ServicePriority.Normal);
|
||||||
|
snapshotTaskId = J.ar(this::publishSnapshots, 20);
|
||||||
Iris.verbose("Integration provider registered for Iris");
|
Iris.verbose("Integration provider registered for Iris");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
|
if (snapshotTaskId != -1) {
|
||||||
|
J.car(snapshotTaskId);
|
||||||
|
snapshotTaskId = -1;
|
||||||
|
}
|
||||||
|
snapshots.clear();
|
||||||
Bukkit.getServicesManager().unregister(IntegrationServiceContract.class, this);
|
Bukkit.getServicesManager().unregister(IntegrationServiceContract.class, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +202,20 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
|
|||||||
return Map.copyOf(selected);
|
return Map.copyOf(selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IntegrationMetricSnapshot snapshotMetrics(Set<String> metricKeys) {
|
||||||
|
return snapshots.snapshotMetrics(metricKeys, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
void publishSnapshots() {
|
||||||
|
IntegrationMetricPublisher.Demand demand = snapshots.demandedKeys(System.currentTimeMillis());
|
||||||
|
if (demand.keys().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, IntegrationMetricSample> samples = sampleMetrics(demand.keys());
|
||||||
|
snapshots.publish(demand, System.currentTimeMillis(), samples);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<IntegrationMetricGroup> metricGroups() {
|
public List<IntegrationMetricGroup> metricGroups() {
|
||||||
IrisTelemetrySnapshot snapshot = safeSnapshot();
|
IrisTelemetrySnapshot snapshot = safeSnapshot();
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package art.arcane.iris.update;
|
||||||
|
|
||||||
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
|
import art.arcane.volmlib.util.update.BukkitUpdateService;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public final class IrisUpdateNotifier implements AutoCloseable {
|
||||||
|
private final Consumer<IrisSettings> settingsListener = this::reconfigure;
|
||||||
|
private final BukkitUpdateService updates;
|
||||||
|
private volatile boolean enabled;
|
||||||
|
private boolean closed;
|
||||||
|
|
||||||
|
public IrisUpdateNotifier(Plugin plugin) {
|
||||||
|
enabled = IrisSettings.get().getGeneral().isUpdateNotifications();
|
||||||
|
updates = BukkitUpdateService.register(plugin, new BukkitUpdateService.Options(
|
||||||
|
"VolmitSoftware", "Iris", "iris.update", () -> enabled));
|
||||||
|
IrisSettings.addSettingsListener(settingsListener);
|
||||||
|
reconfigure(IrisSettings.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void reconfigure(IrisSettings settings) {
|
||||||
|
if (closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
enabled = settings.getGeneral().isUpdateNotifications();
|
||||||
|
updates.reconfigure();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void close() {
|
||||||
|
closed = true;
|
||||||
|
IrisSettings.removeSettingsListener(settingsListener);
|
||||||
|
updates.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-18
@@ -18,8 +18,9 @@
|
|||||||
|
|
||||||
package art.arcane.iris.world;
|
package art.arcane.iris.world;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.PackFingerprints;
|
||||||
|
|
||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
import art.arcane.iris.pack.datapack.ServerConfigurator;
|
|
||||||
|
|
||||||
import art.arcane.iris.Iris;
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.iris.world.lifecycle.WorldLifecycleStaging;
|
import art.arcane.iris.world.lifecycle.WorldLifecycleStaging;
|
||||||
@@ -184,12 +185,12 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
}
|
}
|
||||||
List<String> packNames = packDirs.stream().map(File::getName).sorted().toList();
|
List<String> packNames = packDirs.stream().map(File::getName).sorted().toList();
|
||||||
Path cacheFile = IrisPlatforms.get().dataFile("cache", "pack-validation.json").toPath();
|
Path cacheFile = IrisPlatforms.get().dataFile("cache", "pack-validation.json").toPath();
|
||||||
ServerConfigurator.PackContentSnapshot contentSnapshot =
|
PackFingerprints.PackContentSnapshot contentSnapshot =
|
||||||
new ServerConfigurator.PackContentSnapshot("", Map.of());
|
new PackFingerprints.PackContentSnapshot("", Map.of());
|
||||||
String contextFingerprint = "";
|
String contextFingerprint = "";
|
||||||
Optional<List<PackValidationResult>> cached = Optional.empty();
|
Optional<List<PackValidationResult>> cached = Optional.empty();
|
||||||
try {
|
try {
|
||||||
contentSnapshot = ServerConfigurator.computePackContentSnapshot(packsRoot);
|
contentSnapshot = PackFingerprints.computePackContentSnapshot(packsRoot);
|
||||||
contextFingerprint = PackValidationCache.contextFingerprint();
|
contextFingerprint = PackValidationCache.contextFingerprint();
|
||||||
if (!externalContentChanged) {
|
if (!externalContentChanged) {
|
||||||
cached = PackValidationCache.load(
|
cached = PackValidationCache.load(
|
||||||
@@ -274,19 +275,19 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
private static FreshValidation validateStablePacks(
|
private static FreshValidation validateStablePacks(
|
||||||
File packsRoot,
|
File packsRoot,
|
||||||
List<File> initialPackDirs,
|
List<File> initialPackDirs,
|
||||||
ServerConfigurator.PackContentSnapshot initialSnapshot
|
PackFingerprints.PackContentSnapshot initialSnapshot
|
||||||
) {
|
) {
|
||||||
List<File> packDirs = initialPackDirs;
|
List<File> packDirs = initialPackDirs;
|
||||||
ServerConfigurator.PackContentSnapshot before = initialSnapshot;
|
PackFingerprints.PackContentSnapshot before = initialSnapshot;
|
||||||
for (int attempt = 0; attempt < VALIDATION_STABILITY_ATTEMPTS; attempt++) {
|
for (int attempt = 0; attempt < VALIDATION_STABILITY_ATTEMPTS; attempt++) {
|
||||||
List<String> packNames = packDirs.stream().map(File::getName).sorted().toList();
|
List<String> packNames = packDirs.stream().map(File::getName).sorted().toList();
|
||||||
List<PackValidationResult> results = validatePacks(packDirs);
|
List<PackValidationResult> results = validatePacks(packDirs);
|
||||||
ServerConfigurator.PackContentSnapshot after;
|
PackFingerprints.PackContentSnapshot after;
|
||||||
try {
|
try {
|
||||||
after = ServerConfigurator.computePackContentSnapshot(packsRoot);
|
after = PackFingerprints.computePackContentSnapshot(packsRoot);
|
||||||
} catch (RuntimeException exception) {
|
} catch (RuntimeException exception) {
|
||||||
Iris.reportError("Could not verify Iris pack bytes after validation", exception);
|
Iris.reportError("Could not verify Iris pack bytes after validation", exception);
|
||||||
after = new ServerConfigurator.PackContentSnapshot("", Map.of());
|
after = new PackFingerprints.PackContentSnapshot("", Map.of());
|
||||||
}
|
}
|
||||||
List<File> afterPackDirs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
|
List<File> afterPackDirs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
|
||||||
List<String> afterPackNames = afterPackDirs.stream().map(File::getName).sorted().toList();
|
List<String> afterPackNames = afterPackDirs.stream().map(File::getName).sorted().toList();
|
||||||
@@ -895,14 +896,10 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String dimensionTypeKey(IrisData data, IrisDimension dimension) {
|
private static String dimensionTypeKey(IrisData data, IrisDimension dimension) {
|
||||||
return IrisPlatforms.get()
|
return dimension.dimensionTypeResourceKey(
|
||||||
.registries()
|
IrisPlatforms.get().registries().generationRegistry(),
|
||||||
.generationRegistry()
|
data.getDataFolder().getName()
|
||||||
.dimensionTypeResourceKey(
|
);
|
||||||
data.getDataFolder().getName(),
|
|
||||||
dimension.getLoadKey(),
|
|
||||||
dimension.getDimensionTypeKey()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private record LoadedHistory(GenerationHistory history, File activePack) {
|
private record LoadedHistory(GenerationHistory history, File activePack) {
|
||||||
@@ -923,7 +920,7 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
|
|
||||||
private record FreshValidation(
|
private record FreshValidation(
|
||||||
List<File> packDirs,
|
List<File> packDirs,
|
||||||
ServerConfigurator.PackContentSnapshot contentSnapshot,
|
PackFingerprints.PackContentSnapshot contentSnapshot,
|
||||||
List<PackValidationResult> results,
|
List<PackValidationResult> results,
|
||||||
boolean stable
|
boolean stable
|
||||||
) {
|
) {
|
||||||
|
|||||||
+11
@@ -19,6 +19,10 @@
|
|||||||
package art.arcane.iris.world.runtime;
|
package art.arcane.iris.world.runtime;
|
||||||
|
|
||||||
import art.arcane.iris.api.world.IrisWorldPhase;
|
import art.arcane.iris.api.world.IrisWorldPhase;
|
||||||
|
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||||
|
import art.arcane.iris.integration.ExternalDataSVC;
|
||||||
|
import art.arcane.iris.spi.IrisServices;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockPoint;
|
||||||
import art.arcane.iris.pack.datapack.ServerConfigurator;
|
import art.arcane.iris.pack.datapack.ServerConfigurator;
|
||||||
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
||||||
import art.arcane.iris.world.event.IrisEngineHotloadEvent;
|
import art.arcane.iris.world.event.IrisEngineHotloadEvent;
|
||||||
@@ -51,6 +55,13 @@ import org.bukkit.World;
|
|||||||
import org.bukkit.WorldBorder;
|
import org.bukkit.WorldBorder;
|
||||||
|
|
||||||
public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
|
public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
|
||||||
|
@Override
|
||||||
|
public boolean removeCustomBlock(Engine engine, NativeBlockPoint position, CustomBlockOwnership.Entry entry) {
|
||||||
|
ExternalDataSVC service = IrisServices.getOrNull(ExternalDataSVC.class);
|
||||||
|
return service != null && service.removeGenerated(
|
||||||
|
BukkitWorldBinding.world(engine.getWorld()).getBlockAt(position.x(), position.y(), position.z()), entry);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<Void> flushSavedTerrainCapture(Engine engine) {
|
public CompletableFuture<Void> flushSavedTerrainCapture(Engine engine) {
|
||||||
return INMS.get().flushSavedTerrainCapture(BukkitWorldBinding.world(engine.getWorld()));
|
return INMS.get().flushSavedTerrainCapture(BukkitWorldBinding.world(engine.getWorld()));
|
||||||
|
|||||||
+18
-4
@@ -3,19 +3,33 @@ package art.arcane.iris.world.tree;
|
|||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.block.BlockBreakEvent;
|
import org.bukkit.event.block.BlockBreakEvent;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
final class RoutedBlockBreakEvent extends BlockBreakEvent implements BlockDropRouter {
|
final class RoutedBlockBreakEvent extends BlockBreakEvent implements BlockDropRouter {
|
||||||
private final TreeFellerSVC service;
|
private final TreeFellerSVC service;
|
||||||
private final FellingRun run;
|
private final FellingRun run;
|
||||||
|
private final ItemStack tool;
|
||||||
|
|
||||||
RoutedBlockBreakEvent(Block block, Player player, FellingRun run, TreeFellerSVC service) {
|
RoutedBlockBreakEvent(Options options) {
|
||||||
super(block, player);
|
super(options.block(), options.player());
|
||||||
this.run = run;
|
run = options.run();
|
||||||
this.service = service;
|
service = options.service();
|
||||||
|
tool = Objects.requireNonNull(options.tool(), "tool").clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<ItemStack> captureNativeDrops() {
|
||||||
|
return getBlock().getDrops(tool);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean routeDrop(Object drop) {
|
public boolean routeDrop(Object drop) {
|
||||||
return service.isServiceEnabled() && run.presentation.routeDrop(drop);
|
return service.isServiceEnabled() && run.presentation.routeDrop(drop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
record Options(Block block, Player player, FellingRun run, TreeFellerSVC service, ItemStack tool) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+2
-1
@@ -387,7 +387,8 @@ final class TreeFellingRunner {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
BlockBreakEvent probe = new RoutedBlockBreakEvent(block, run.candidate.player(), run, service);
|
BlockBreakEvent probe = new RoutedBlockBreakEvent(new RoutedBlockBreakEvent.Options(
|
||||||
|
block, run.candidate.player(), run, service, reservation.toolForDrops()));
|
||||||
service.managedEvents.add(probe);
|
service.managedEvents.add(probe);
|
||||||
try {
|
try {
|
||||||
Bukkit.getPluginManager().callEvent(probe);
|
Bukkit.getPluginManager().callEvent(probe);
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
|
|||||||
website: VolmitSoftware.com
|
website: VolmitSoftware.com
|
||||||
description: More than a Dimension!
|
description: More than a Dimension!
|
||||||
permissions:
|
permissions:
|
||||||
|
iris.update:
|
||||||
|
description: Allows viewing Iris updates in VolmLib's combined update reports.
|
||||||
|
default: op
|
||||||
iris.debugdump:
|
iris.debugdump:
|
||||||
description: Allows creating and optionally uploading diagnostic reports.
|
description: Allows creating and optionally uploading diagnostic reports.
|
||||||
default: op
|
default: op
|
||||||
@@ -25,34 +28,26 @@ permissions:
|
|||||||
dependencies:
|
dependencies:
|
||||||
server:
|
server:
|
||||||
PlaceholderAPI:
|
PlaceholderAPI:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
CraftEngine:
|
CraftEngine:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
Nexo:
|
Nexo:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
Oraxen:
|
Oraxen:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
ItemsAdder:
|
ItemsAdder:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
SCore:
|
SCore:
|
||||||
load: BEFORE
|
|
||||||
required: false
|
|
||||||
join-classpath: true
|
|
||||||
ExecutableItems:
|
|
||||||
load: BEFORE
|
|
||||||
required: false
|
|
||||||
join-classpath: false
|
|
||||||
MythicLib:
|
|
||||||
load: OMIT
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
@@ -61,11 +56,7 @@ dependencies:
|
|||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
eco:
|
eco:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
|
||||||
join-classpath: true
|
|
||||||
EcoItems:
|
|
||||||
load: BEFORE
|
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
MythicMobs:
|
MythicMobs:
|
||||||
@@ -77,7 +68,7 @@ dependencies:
|
|||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
KGenerators:
|
KGenerators:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
Multiverse-Core:
|
Multiverse-Core:
|
||||||
@@ -85,6 +76,6 @@ dependencies:
|
|||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
WorldEdit:
|
WorldEdit:
|
||||||
load: BEFORE
|
load: OMIT
|
||||||
required: false
|
required: false
|
||||||
join-classpath: true
|
join-classpath: true
|
||||||
@@ -3,18 +3,6 @@ version: ${version}
|
|||||||
main: ${main}
|
main: ${main}
|
||||||
folia-supported: true
|
folia-supported: true
|
||||||
load: STARTUP
|
load: STARTUP
|
||||||
softdepend:
|
|
||||||
- PlaceholderAPI
|
|
||||||
- CraftEngine
|
|
||||||
- Nexo
|
|
||||||
- Oraxen
|
|
||||||
- ItemsAdder
|
|
||||||
- SCore
|
|
||||||
- ExecutableItems
|
|
||||||
- eco
|
|
||||||
- EcoItems
|
|
||||||
- KGenerators
|
|
||||||
- WorldEdit
|
|
||||||
loadbefore: [ Multiverse-Core ]
|
loadbefore: [ Multiverse-Core ]
|
||||||
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
|
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
|
||||||
website: VolmitSoftware.com
|
website: VolmitSoftware.com
|
||||||
@@ -23,6 +11,9 @@ commands:
|
|||||||
iris:
|
iris:
|
||||||
aliases: [ ir, irs ]
|
aliases: [ ir, irs ]
|
||||||
permissions:
|
permissions:
|
||||||
|
iris.update:
|
||||||
|
description: Allows viewing Iris updates in VolmLib's combined update reports.
|
||||||
|
default: op
|
||||||
iris.debugdump:
|
iris.debugdump:
|
||||||
description: Allows creating and optionally uploading diagnostic reports.
|
description: Allows creating and optionally uploading diagnostic reports.
|
||||||
default: op
|
default: op
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "Flat",
|
||||||
|
"derivative": "minecraft:snowy_plains",
|
||||||
|
"vanillaDerivative": "minecraft:snowy_plains",
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"minHeight": 1,
|
||||||
|
"maxHeight": 1,
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:lime_wool"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minHeight": 384,
|
||||||
|
"maxHeight": 384,
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:green_concrete"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"generators": [
|
||||||
|
{
|
||||||
|
"generator": "flat",
|
||||||
|
"min": 96,
|
||||||
|
"max": 96
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"decorators": [
|
||||||
|
{
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "craftengine:default/flower_basket",
|
||||||
|
"data": {
|
||||||
|
"variant": "ground"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"chance": 1,
|
||||||
|
"forcePlace": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "CraftEngine Imported Feature Fixture",
|
||||||
|
"version": 1,
|
||||||
|
"mode": {
|
||||||
|
"type": "OVERWORLD"
|
||||||
|
},
|
||||||
|
"regions": [
|
||||||
|
"flat"
|
||||||
|
],
|
||||||
|
"focusRegion": "flat",
|
||||||
|
"focus": "flat",
|
||||||
|
"landChance": 1,
|
||||||
|
"environment": "NORMAL",
|
||||||
|
"dimensionHeight": {
|
||||||
|
"min": -64,
|
||||||
|
"max": 320
|
||||||
|
},
|
||||||
|
"logicalHeight": 384,
|
||||||
|
"fluidHeight": 0,
|
||||||
|
"bedrock": false,
|
||||||
|
"rockPalette": {
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:red_concrete"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"importedStructures": {
|
||||||
|
"disabled": [
|
||||||
|
"minecraft:"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"importedFeatures": {
|
||||||
|
"enabled": true,
|
||||||
|
"steps": [
|
||||||
|
"TOP_LAYER_MODIFICATION"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"interpolator": {
|
||||||
|
"function": "NONE",
|
||||||
|
"horizontalScale": 1
|
||||||
|
},
|
||||||
|
"seed": 310,
|
||||||
|
"composite": [
|
||||||
|
{
|
||||||
|
"seed": 310,
|
||||||
|
"style": {
|
||||||
|
"style": "FLAT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "Flat",
|
||||||
|
"landBiomes": [
|
||||||
|
"flat"
|
||||||
|
],
|
||||||
|
"seaBiomes": [
|
||||||
|
"flat"
|
||||||
|
],
|
||||||
|
"shoreBiomes": [
|
||||||
|
"flat"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
}
|
||||||
|
|
||||||
|
version = '1.0.0'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly files(providers.gradleProperty('irisJar').get())
|
||||||
|
compileOnly files(providers.gradleProperty('craftEngineJar').get())
|
||||||
|
compileOnly 'io.papermc.paper:paper-api:26.1.2.build.74-stable'
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain.languageVersion = JavaLanguageVersion.of(25)
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
java.srcDirs = ['src']
|
||||||
|
resources.srcDirs = ['resources']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(JavaCompile).configureEach {
|
||||||
|
options.compilerArgs += '-parameters'
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named('jar') {
|
||||||
|
archiveBaseName = 'IrisCraftEngineNativeFixture'
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
name: IrisCraftEngineNativeFixture
|
||||||
|
version: 1.0.0
|
||||||
|
main: art.arcane.iris.fixture.CraftEngineNativeFixture
|
||||||
|
api-version: '26.1.2'
|
||||||
|
folia-supported: true
|
||||||
|
depend: [Iris, CraftEngine]
|
||||||
|
commands:
|
||||||
|
iriscenative:
|
||||||
|
permission: iriscenative.run
|
||||||
|
permissions:
|
||||||
|
iriscenative.run:
|
||||||
|
default: op
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
rootProject.name = 'iris-craftengine-native-fixture'
|
||||||
+385
@@ -0,0 +1,385 @@
|
|||||||
|
package art.arcane.iris.fixture;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.block.IrisCustomData;
|
||||||
|
import art.arcane.iris.generation.block.TileData;
|
||||||
|
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
|
||||||
|
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.integration.ExternalDataSVC;
|
||||||
|
import art.arcane.iris.integration.Identifier;
|
||||||
|
import art.arcane.iris.platform.generation.EngineBukkitOps;
|
||||||
|
import art.arcane.iris.spi.IrisServices;
|
||||||
|
import art.arcane.iris.world.IrisCreator;
|
||||||
|
import art.arcane.iris.world.IrisToolbelt;
|
||||||
|
import art.arcane.iris.world.storage.matter.TileWrapper;
|
||||||
|
import art.arcane.iris.world.task.J;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
|
||||||
|
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||||
|
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||||
|
import art.arcane.volmlib.util.matter.Matter;
|
||||||
|
import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks;
|
||||||
|
import net.momirealms.craftengine.bukkit.api.CraftEngineFurniture;
|
||||||
|
import net.momirealms.craftengine.core.util.Key;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Chunk;
|
||||||
|
import org.bukkit.GameMode;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.NamespacedKey;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.entity.Entity;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
public final class CraftEngineNativeFixture extends JavaPlugin {
|
||||||
|
private static final NamespacedKey WORLD_KEY = new NamespacedKey("iris", "ce_native_probe");
|
||||||
|
private static final NamespacedKey FEATURE_WORLD_KEY = new NamespacedKey("iris", "ce_native_feature_probe");
|
||||||
|
private static final int TEST_Y = 180;
|
||||||
|
private static final int TEST_Z = 8;
|
||||||
|
private static final String BLOCK_ID = "craftengine:default/topaz_ore";
|
||||||
|
private static final String FURNITURE_ID = "craftengine:default/flower_basket[variant=ground]";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
|
||||||
|
if (!(sender instanceof Player player) || arguments.length != 1) {
|
||||||
|
sender.sendMessage("CE_FAILED Use /iriscenative prepare|pending|neighbor|loaded|features|restart from a player");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (arguments[0].equals("prepare")) {
|
||||||
|
player.setGameMode(GameMode.CREATIVE);
|
||||||
|
prepare(player);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (arguments[0].equals("features")) {
|
||||||
|
importedFeatures(player);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
World world = Bukkit.getWorld(WORLD_KEY);
|
||||||
|
if (world == null) {
|
||||||
|
fail(player, new IllegalStateException("Fixture world has not been created"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (arguments[0].equals("restart")) {
|
||||||
|
loadChunks().thenCompose(ignored -> J.runRegionFuture(world, 0, 0,
|
||||||
|
() -> later(player, world, () -> restarted(player, world))))
|
||||||
|
.whenComplete((ignored, failure) -> {
|
||||||
|
if (failure != null) {
|
||||||
|
fail(player, failure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
J.runRegionFuture(world, 0, 0, () -> {
|
||||||
|
switch (arguments[0]) {
|
||||||
|
case "pending" -> pending(player, world);
|
||||||
|
case "neighbor" -> neighbor(player, world);
|
||||||
|
case "loaded" -> invalidateLoaded(player, world, IrisToolbelt.access(world).getEngine());
|
||||||
|
default -> throw new IllegalArgumentException("Unknown fixture operation");
|
||||||
|
}
|
||||||
|
}).whenComplete((ignored, failure) -> {
|
||||||
|
if (failure != null) {
|
||||||
|
fail(player, failure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepare(Player player) {
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
if (Bukkit.getWorld(WORLD_KEY) == null) {
|
||||||
|
try {
|
||||||
|
new IrisCreator().name(WORLD_KEY.toString()).dimension("ce-native-fixture")
|
||||||
|
.seed(78264193L).create();
|
||||||
|
} catch (Exception failure) {
|
||||||
|
throw new IllegalStateException("Could not create CraftEngine fixture world", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).thenCompose(ignored -> loadChunks()).thenCompose(ignored -> {
|
||||||
|
World world = Bukkit.getWorld(WORLD_KEY);
|
||||||
|
return J.runRegionFuture(world, 0, 0, () -> preparePlatform(world));
|
||||||
|
}).thenAccept(ignored -> {
|
||||||
|
World world = Bukkit.getWorld(WORLD_KEY);
|
||||||
|
if (!J.runEntity(player, () -> player.teleportAsync(new Location(world, 8.5, TEST_Y + 3, 8.5))
|
||||||
|
.whenComplete((teleported, failure) -> {
|
||||||
|
if (failure != null || !Boolean.TRUE.equals(teleported)) {
|
||||||
|
fail(player, failure == null ? new IllegalStateException("Fixture teleport failed") : failure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player.sendMessage("CE_READY world=" + WORLD_KEY + " folia=" + J.isFolia());
|
||||||
|
}))) {
|
||||||
|
throw new IllegalStateException("Could not schedule fixture teleport");
|
||||||
|
}
|
||||||
|
}).exceptionally(failure -> {
|
||||||
|
fail(player, failure);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void importedFeatures(Player player) {
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
if (Bukkit.getWorld(FEATURE_WORLD_KEY) == null) {
|
||||||
|
try {
|
||||||
|
new IrisCreator().name(FEATURE_WORLD_KEY.toString()).dimension("ce-native-feature-fixture")
|
||||||
|
.seed(78264193L).create();
|
||||||
|
} catch (Exception failure) {
|
||||||
|
throw new IllegalStateException("Could not create imported feature fixture world", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).thenCompose(ignored -> {
|
||||||
|
World world = Bukkit.getWorld(FEATURE_WORLD_KEY);
|
||||||
|
return world.getChunkAtAsync(0, 0, true);
|
||||||
|
}).thenCompose(chunk -> J.runRegionFuture(chunk.getWorld(), 0, 0, () -> {
|
||||||
|
World world = chunk.getWorld();
|
||||||
|
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||||
|
int snow = 0;
|
||||||
|
int pending = 0;
|
||||||
|
for (int x = 0; x < 16; x++) {
|
||||||
|
for (int z = 0; z < 16; z++) {
|
||||||
|
if (world.getBlockAt(x, 97, z).getType() == Material.SNOW) {
|
||||||
|
snow++;
|
||||||
|
}
|
||||||
|
if (engine.getMantle().getMantle().get(x, 97 - engine.getMinHeight(), z, Identifier.class) != null) {
|
||||||
|
pending++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require(snow == 256, "Imported snow feature did not cover all 256 test columns: " + snow);
|
||||||
|
require(pending == 0, "Imported feature left pending furniture records: " + pending);
|
||||||
|
for (Entity entity : chunk.getEntities()) {
|
||||||
|
require(!CraftEngineFurniture.isFurniture(entity), "Imported snow feature left generated furniture");
|
||||||
|
}
|
||||||
|
if (!J.runEntity(player, () -> player.teleportAsync(new Location(world, 8.5, 101, 8.5))
|
||||||
|
.whenComplete((teleported, failure) -> {
|
||||||
|
if (failure != null || !Boolean.TRUE.equals(teleported)) {
|
||||||
|
fail(player, failure == null ? new IllegalStateException("Feature world teleport failed") : failure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player.sendMessage("CE_FEATURES snow=256 pending=0 furniture=0");
|
||||||
|
}))) {
|
||||||
|
throw new IllegalStateException("Could not schedule feature world teleport");
|
||||||
|
}
|
||||||
|
})).exceptionally(failure -> {
|
||||||
|
fail(player, failure);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<Void> loadChunks() {
|
||||||
|
World world = Bukkit.getWorld(WORLD_KEY);
|
||||||
|
require(world != null && IrisToolbelt.isIrisWorld(world), "Fixture must use a real Iris world");
|
||||||
|
List<CompletableFuture<Void>> chunks = new ArrayList<>(15);
|
||||||
|
for (int x = -1; x <= 3; x++) {
|
||||||
|
for (int z = -1; z <= 1; z++) {
|
||||||
|
chunks.add(world.getChunkAtAsync(x, z, true).thenCompose(chunk ->
|
||||||
|
J.runRegionFuture(world, chunk.getX(), chunk.getZ(), () -> chunk.addPluginChunkTicket(this))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CompletableFuture.allOf(chunks.toArray(CompletableFuture[]::new));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void preparePlatform(World world) {
|
||||||
|
require(CraftEngineBlocks.byId(Key.of("default:topaz_ore")) != null, "CraftEngine default block is missing");
|
||||||
|
require(CraftEngineFurniture.byId(Key.of("default:flower_basket")) != null, "CraftEngine furniture is missing");
|
||||||
|
for (int chunkX = 0; chunkX <= 2; chunkX++) {
|
||||||
|
for (Entity entity : world.getChunkAt(chunkX, 0).getEntities()) {
|
||||||
|
if (CraftEngineFurniture.isFurniture(entity)) {
|
||||||
|
CraftEngineFurniture.remove(entity, false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int x = 0; x <= 47; x++) {
|
||||||
|
for (int z = 2; z <= 14; z++) {
|
||||||
|
world.getBlockAt(x, TEST_Y - 1, z).setType(Material.STONE, false);
|
||||||
|
for (int y = TEST_Y; y <= TEST_Y + 5; y++) {
|
||||||
|
world.getBlockAt(x, y, z).setType(Material.AIR, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pending(Player player, World world) {
|
||||||
|
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||||
|
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||||
|
queue(engine, world, 2, BLOCK_ID);
|
||||||
|
queue(engine, world, 4, BLOCK_ID);
|
||||||
|
queue(engine, world, 6, BLOCK_ID);
|
||||||
|
queue(engine, world, 8, BLOCK_ID);
|
||||||
|
queue(engine, world, 10, FURNITURE_ID);
|
||||||
|
queue(engine, world, 12, FURNITURE_ID);
|
||||||
|
Block chest = world.getBlockAt(14, TEST_Y, TEST_Z);
|
||||||
|
chest.setType(Material.CHEST, false);
|
||||||
|
mantle.set(14, TEST_Y - engine.getMinHeight(), TEST_Z, new TileWrapper(TileData.getTileState(chest, false)));
|
||||||
|
world.getBlockAt(4, TEST_Y, TEST_Z).setType(Material.AIR, false);
|
||||||
|
world.getBlockAt(6, TEST_Y, TEST_Z).setType(Material.COBBLESTONE, false);
|
||||||
|
|
||||||
|
NativeBlockChangeSet changes = DeferredPlacementRecords.track(engine, bounds(world));
|
||||||
|
changes.add(8, TEST_Y, TEST_Z);
|
||||||
|
changes.add(12, TEST_Y - 1, TEST_Z);
|
||||||
|
changes.add(12, TEST_Y, TEST_Z);
|
||||||
|
changes.add(14, TEST_Y, TEST_Z);
|
||||||
|
world.getBlockAt(8, TEST_Y, TEST_Z).setType(Material.AIR, false);
|
||||||
|
world.getBlockAt(12, TEST_Y - 1, TEST_Z).setType(Material.AIR, false);
|
||||||
|
chest.setType(Material.COBBLESTONE, false);
|
||||||
|
DeferredPlacementRecords.invalidate(engine, changes);
|
||||||
|
require(record(engine, 8, Identifier.class) == null, "Native cut retained a pending custom record");
|
||||||
|
require(record(engine, 12, Identifier.class) == null, "Flatten column retained pending furniture");
|
||||||
|
require(record(engine, 14, TileWrapper.class) == null, "Native wall retained pending chest data");
|
||||||
|
require(record(engine, 2, Identifier.class) != null, "Native writes invalidated an untouched custom record");
|
||||||
|
replay(engine, world, 0);
|
||||||
|
CustomBlockOwnership ownership = record(engine, 10, CustomBlockOwnership.class);
|
||||||
|
require(record(engine, 2, Identifier.class) == null, "Custom materialization did not consume pending records");
|
||||||
|
require(CraftEngineBlocks.isCustomBlock(world.getBlockAt(2, TEST_Y, TEST_Z)), "Intact custom block was not placed");
|
||||||
|
require(world.getBlockAt(4, TEST_Y, TEST_Z).getType().isAir(), "Cut custom block was resurrected");
|
||||||
|
require(world.getBlockAt(6, TEST_Y, TEST_Z).getType() == Material.COBBLESTONE, "Custom block replaced native wall");
|
||||||
|
require(world.getBlockAt(8, TEST_Y, TEST_Z).getType().isAir(), "Invalidated custom block was resurrected");
|
||||||
|
require(chest.getType() == Material.COBBLESTONE, "Chest data replaced native wall");
|
||||||
|
later(player, world, () -> {
|
||||||
|
int intactFurniture = furnitureAt(world, 10);
|
||||||
|
require(intactFurniture == 1, "Intact furniture count is " + intactFurniture
|
||||||
|
+ "; pending=" + record(engine, 10, Identifier.class)
|
||||||
|
+ "; ownership=" + ownership
|
||||||
|
+ "; chunkEntities=" + world.getChunkAt(0, 0).getEntities().length);
|
||||||
|
require(furnitureAt(world, 12) == 0, "Flattened furniture was resurrected");
|
||||||
|
player.sendMessage("CE_PENDING intact=1 air=1 wall=1 invalidated=1 flatten=1 chest=1 furniture=1");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void neighbor(Player player, World world) {
|
||||||
|
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||||
|
queue(engine, world, 18, FURNITURE_ID);
|
||||||
|
queue(engine, world, 20, FURNITURE_ID);
|
||||||
|
queue(engine, world, 22, FURNITURE_ID);
|
||||||
|
queue(engine, world, 42, FURNITURE_ID);
|
||||||
|
replay(engine, world, 1);
|
||||||
|
replay(engine, world, 2);
|
||||||
|
world.getBlockAt(40, TEST_Y, TEST_Z).setType(Material.DIAMOND_BLOCK, false);
|
||||||
|
later(player, world, () -> {
|
||||||
|
require(furnitureAt(world, 18) == 1 && furnitureAt(world, 20) == 1
|
||||||
|
&& furnitureAt(world, 22) == 1, "Neighbor furniture did not materialize");
|
||||||
|
require(furnitureAt(world, 42) == 1 && record(engine, 42, CustomBlockOwnership.class) != null,
|
||||||
|
"Regeneration target furniture did not materialize with saved ownership");
|
||||||
|
if (!J.runEntity(player, () -> player.teleportAsync(new Location(world, 40.5, TEST_Y + 3, TEST_Z + 0.5))
|
||||||
|
.whenComplete((teleported, failure) -> {
|
||||||
|
if (failure != null || !Boolean.TRUE.equals(teleported)) {
|
||||||
|
fail(player, failure == null ? new IllegalStateException("Regeneration target teleport failed") : failure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player.sendMessage("CE_NEIGHBOR_READY chunk=2,0 furniture=3 target=1");
|
||||||
|
}))) {
|
||||||
|
throw new IllegalStateException("Could not schedule regeneration target teleport");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void invalidateLoaded(Player player, World world, Engine engine) {
|
||||||
|
require(world.getBlockAt(40, TEST_Y, TEST_Z).getType().isAir(), "Adjacent target chunk did not regenerate");
|
||||||
|
require(furnitureAt(world, 18) == 1 && furnitureAt(world, 20) == 1
|
||||||
|
&& furnitureAt(world, 22) == 1, "Neighbor furniture did not materialize");
|
||||||
|
NativeBlockChangeSet changes = DeferredPlacementRecords.track(engine, bounds(world));
|
||||||
|
changes.add(18, TEST_Y - 1, TEST_Z);
|
||||||
|
changes.add(18, TEST_Y, TEST_Z);
|
||||||
|
changes.add(22, TEST_Y, TEST_Z);
|
||||||
|
world.getBlockAt(18, TEST_Y - 1, TEST_Z).setType(Material.AIR, false);
|
||||||
|
world.getBlockAt(18, TEST_Y, TEST_Z).setType(Material.COBBLESTONE, false);
|
||||||
|
DeferredPlacementRecords.invalidate(engine, changes);
|
||||||
|
require(CraftEngineFurniture.place(new Location(world, 22.5, TEST_Y, TEST_Z + 0.5),
|
||||||
|
Key.of("default:flower_basket"), "ground", false) != null,
|
||||||
|
"Could not place replacement furniture before scheduled cleanup");
|
||||||
|
later(player, world, () -> {
|
||||||
|
require(furnitureAt(world, 42) == 0, "Regeneration retained target chunk furniture");
|
||||||
|
require(furnitureAt(world, 18) == 0, "Native write left materialized neighbor furniture");
|
||||||
|
require(furnitureAt(world, 20) == 1, "Native write removed untouched neighbor furniture");
|
||||||
|
require(furnitureAt(world, 22) == 1, "Native cleanup removed replacement furniture or retained the old one");
|
||||||
|
require(world.getBlockAt(18, TEST_Y, TEST_Z).getType() == Material.COBBLESTONE,
|
||||||
|
"Furniture cleanup replaced the native wall");
|
||||||
|
player.sendMessage("CE_LOADED removed=1 untouched=1 replacement=1 wall=1 target=1");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void restarted(Player player, World world) {
|
||||||
|
require(furnitureAt(world, 20) == 1, "Generated neighbor furniture did not survive restart");
|
||||||
|
require(furnitureAt(world, 10) == 1, "Untouched furniture did not survive restart");
|
||||||
|
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||||
|
NativeBlockChangeSet changes = DeferredPlacementRecords.track(engine, bounds(world));
|
||||||
|
changes.add(20, TEST_Y, TEST_Z);
|
||||||
|
DeferredPlacementRecords.invalidate(engine, changes);
|
||||||
|
later(player, world, () -> {
|
||||||
|
require(furnitureAt(world, 20) == 0, "Persisted furniture ownership was not invalidated after restart");
|
||||||
|
require(furnitureAt(world, 10) == 1, "Restart cleanup removed untouched furniture");
|
||||||
|
player.sendMessage("CE_RESTART removed=1 untouched=1");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void queue(Engine engine, World world, int x, String identifier) {
|
||||||
|
ExternalDataSVC service = IrisServices.get(ExternalDataSVC.class);
|
||||||
|
BlockData data = service.getBlockData(Identifier.fromString(identifier)).orElseThrow();
|
||||||
|
require(data instanceof IrisCustomData, "Iris did not resolve real CraftEngine content: " + identifier);
|
||||||
|
IrisCustomData custom = (IrisCustomData) data;
|
||||||
|
world.getBlockAt(x, TEST_Y, TEST_Z).setBlockData(custom.getBase(), false);
|
||||||
|
engine.getMantle().getMantle().set(x, TEST_Y - engine.getMinHeight(), TEST_Z, custom.getCustom());
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T record(Engine engine, int x, Class<T> type) {
|
||||||
|
return engine.getMantle().getMantle().get(x, TEST_Y - engine.getMinHeight(), TEST_Z, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void replay(Engine engine, World world, int chunkX) {
|
||||||
|
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||||
|
for (int x = chunkX - 1; x <= chunkX + 1; x++) {
|
||||||
|
for (int z = -1; z <= 1; z++) {
|
||||||
|
require(world.isChunkLoaded(x, z), "Replay neighbor is not loaded: " + x + "," + z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require(mantle.isLoaded(world.getChunkAt(chunkX, 0)), "Replay mantle chunk is not loaded: " + chunkX);
|
||||||
|
mantle.flag(chunkX, 0, MantleFlag.ETCHED, false);
|
||||||
|
mantle.flag(chunkX, 0, MantleFlag.TILE, false);
|
||||||
|
mantle.flag(chunkX, 0, MantleFlag.CUSTOM, false);
|
||||||
|
EngineBukkitOps.updateChunk(engine, world.getChunkAt(chunkX, 0), mantle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int furnitureAt(World world, int x) {
|
||||||
|
int count = 0;
|
||||||
|
for (Entity entity : world.getChunkAt(x >> 4, TEST_Z >> 4).getEntities()) {
|
||||||
|
Location location = entity.getLocation();
|
||||||
|
if (location.getBlockX() == x && location.getBlockY() == TEST_Y && location.getBlockZ() == TEST_Z
|
||||||
|
&& CraftEngineFurniture.getLoadedFurnitureByMetaEntity(entity) != null) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void later(Player player, World world, Runnable assertion) {
|
||||||
|
Bukkit.getRegionScheduler().runDelayed(this, new Location(world, 8, TEST_Y, TEST_Z), task -> {
|
||||||
|
try {
|
||||||
|
assertion.run();
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
fail(player, failure);
|
||||||
|
}
|
||||||
|
}, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
private NativeBlockChangeSet.Bounds bounds(World world) {
|
||||||
|
return new NativeBlockChangeSet.Bounds(-1, -1, 3, 1, world.getMinHeight(), world.getMaxHeight() - world.getMinHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void require(boolean condition, String message) {
|
||||||
|
if (!condition) {
|
||||||
|
throw new IllegalStateException(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fail(CommandSender sender, Throwable failure) {
|
||||||
|
getLogger().log(Level.SEVERE, "CraftEngine native placement assertion failed", failure);
|
||||||
|
sender.sendMessage("CE_FAILED " + failure.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "Flat",
|
||||||
|
"derivative": "minecraft:plains",
|
||||||
|
"vanillaDerivative": "minecraft:plains",
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"minHeight": 1,
|
||||||
|
"maxHeight": 1,
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:lime_wool"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minHeight": 384,
|
||||||
|
"maxHeight": 384,
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:green_concrete"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"generators": [
|
||||||
|
{
|
||||||
|
"generator": "flat",
|
||||||
|
"min": 96,
|
||||||
|
"max": 96
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "CraftEngine Native Fixture",
|
||||||
|
"version": 1,
|
||||||
|
"mode": {
|
||||||
|
"type": "OVERWORLD"
|
||||||
|
},
|
||||||
|
"regions": [
|
||||||
|
"flat"
|
||||||
|
],
|
||||||
|
"focusRegion": "flat",
|
||||||
|
"focus": "flat",
|
||||||
|
"landChance": 1,
|
||||||
|
"environment": "NORMAL",
|
||||||
|
"dimensionHeight": {
|
||||||
|
"min": -64,
|
||||||
|
"max": 320
|
||||||
|
},
|
||||||
|
"logicalHeight": 384,
|
||||||
|
"fluidHeight": 0,
|
||||||
|
"bedrock": false,
|
||||||
|
"rockPalette": {
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:red_concrete"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"importedStructures": {
|
||||||
|
"disabled": [
|
||||||
|
"minecraft:"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"interpolator": {
|
||||||
|
"function": "NONE",
|
||||||
|
"horizontalScale": 1
|
||||||
|
},
|
||||||
|
"seed": 310,
|
||||||
|
"composite": [
|
||||||
|
{
|
||||||
|
"seed": 310,
|
||||||
|
"style": {
|
||||||
|
"style": "FLAT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "Flat",
|
||||||
|
"landBiomes": [
|
||||||
|
"flat"
|
||||||
|
],
|
||||||
|
"seaBiomes": [
|
||||||
|
"flat"
|
||||||
|
],
|
||||||
|
"shoreBiomes": [
|
||||||
|
"flat"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { readFile } from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'iris-craftengine-native-restart',
|
||||||
|
description: 'Verify persisted generated furniture ownership after a full server restart.',
|
||||||
|
async run(context) {
|
||||||
|
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||||
|
context.expect(/^isolated=true\s*$/m.test(source), 'CraftEngine regression requires an isolated instance')
|
||||||
|
await context.step('native writes remove persisted ownership after restart', async () => {
|
||||||
|
const response = await context.command('/iriscenative restart', /CE_RESTART|CE_FAILED/, 60000)
|
||||||
|
context.expect(response.includes('CE_RESTART removed=1 untouched=1'), response)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { readFile } from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'iris-craftengine-native',
|
||||||
|
description: 'Verify deferred native invalidation and loaded neighbor cleanup with real Iris and CraftEngine.',
|
||||||
|
async run(context) {
|
||||||
|
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||||
|
context.expect(/^isolated=true\s*$/m.test(source), 'CraftEngine regression requires an isolated instance')
|
||||||
|
await context.step('create and enter a real Iris world with CraftEngine content', async () => {
|
||||||
|
const response = await context.command('/iriscenative prepare', /CE_READY|CE_FAILED/, 180000)
|
||||||
|
context.expect(response.includes('CE_READY'), response)
|
||||||
|
})
|
||||||
|
await context.step('native writes remove pending block, chest, and flatten furniture records', async () => {
|
||||||
|
const response = await context.command('/iriscenative pending', /CE_PENDING|CE_FAILED/, 60000)
|
||||||
|
context.expect(response.includes('CE_PENDING intact=1 air=1 wall=1 invalidated=1 flatten=1 chest=1 furniture=1'), response)
|
||||||
|
await context.waitUntil(() => {
|
||||||
|
const origin = context.bot.entity.position.floored()
|
||||||
|
const cut = context.bot.blockAt(origin.set(4, 180, 8))
|
||||||
|
const wall = context.bot.blockAt(origin.set(6, 180, 8))
|
||||||
|
const invalidated = context.bot.blockAt(origin.set(8, 180, 8))
|
||||||
|
const chest = context.bot.blockAt(origin.set(14, 180, 8))
|
||||||
|
return cut?.name === 'air' && wall?.name === 'cobblestone'
|
||||||
|
&& invalidated?.name === 'air' && chest?.name === 'cobblestone'
|
||||||
|
}, { label: 'Client observes retained native air and walls', timeoutMs: 30000, intervalMs: 100 })
|
||||||
|
})
|
||||||
|
await context.step('regeneration removes target furniture and preserves neighbor ownership', async () => {
|
||||||
|
const ready = await context.command('/iriscenative neighbor', /CE_NEIGHBOR_READY|CE_FAILED/, 30000)
|
||||||
|
context.expect(ready.includes('CE_NEIGHBOR_READY chunk=2,0 furniture=3 target=1'), ready)
|
||||||
|
const regenerated = await context.command('/iris developer regen radius=0', /Regen complete:|Regen failed:/, 90000)
|
||||||
|
context.expect(regenerated.includes('Regen complete:'), regenerated)
|
||||||
|
const response = await context.command('/iriscenative loaded', /CE_LOADED|CE_FAILED/, 30000)
|
||||||
|
context.expect(response.includes('CE_LOADED removed=1 untouched=1 replacement=1 wall=1 target=1'), response)
|
||||||
|
})
|
||||||
|
await context.step('imported snow features replace generated furniture records', async () => {
|
||||||
|
const response = await context.command('/iriscenative features', /CE_FEATURES|CE_FAILED/, 180000)
|
||||||
|
context.expect(response.includes('CE_FEATURES snow=256 pending=0 furniture=0'), response)
|
||||||
|
await context.waitUntil(() => {
|
||||||
|
const position = context.bot.entity.position.floored()
|
||||||
|
for (let x = 0; x < 16; x++) {
|
||||||
|
for (let z = 0; z < 16; z++) {
|
||||||
|
if (context.bot.blockAt(position.set(x, 97, z))?.name !== 'snow') return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}, { label: 'Client observes 256 imported snow layers', timeoutMs: 30000, intervalMs: 100 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "Flat",
|
||||||
|
"derivative": "minecraft:plains",
|
||||||
|
"vanillaDerivative": "minecraft:plains",
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"minHeight": 1,
|
||||||
|
"maxHeight": 1,
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:lime_wool"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minHeight": 384,
|
||||||
|
"maxHeight": 384,
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:green_concrete"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"generators": [
|
||||||
|
{
|
||||||
|
"generator": "flat",
|
||||||
|
"min": 96,
|
||||||
|
"max": 96
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"decorators": [
|
||||||
|
{
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:oak_stairs",
|
||||||
|
"data": {
|
||||||
|
"half": "bottom",
|
||||||
|
"facing": "north"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"chance": 1,
|
||||||
|
"forcePlace": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "Decorator Fixture",
|
||||||
|
"version": 1,
|
||||||
|
"mode": {
|
||||||
|
"type": "OVERWORLD"
|
||||||
|
},
|
||||||
|
"regions": [
|
||||||
|
"flat"
|
||||||
|
],
|
||||||
|
"focusRegion": "flat",
|
||||||
|
"focus": "flat",
|
||||||
|
"landChance": 1,
|
||||||
|
"environment": "NORMAL",
|
||||||
|
"dimensionHeight": {
|
||||||
|
"min": -64,
|
||||||
|
"max": 320
|
||||||
|
},
|
||||||
|
"logicalHeight": 384,
|
||||||
|
"fluidHeight": 0,
|
||||||
|
"bedrock": false,
|
||||||
|
"rockPalette": {
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:red_concrete"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"interpolator": {
|
||||||
|
"function": "NONE",
|
||||||
|
"horizontalScale": 1
|
||||||
|
},
|
||||||
|
"seed": 310,
|
||||||
|
"composite": [
|
||||||
|
{
|
||||||
|
"seed": 310,
|
||||||
|
"style": {
|
||||||
|
"style": "FLAT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "Flat",
|
||||||
|
"landBiomes": [
|
||||||
|
"flat"
|
||||||
|
],
|
||||||
|
"seaBiomes": [
|
||||||
|
"flat"
|
||||||
|
],
|
||||||
|
"shoreBiomes": [
|
||||||
|
"flat"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { readFile } from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'iris-decorators',
|
||||||
|
description: 'Verify authored stair decorations occupy one block and retain their half state.',
|
||||||
|
async run(context) {
|
||||||
|
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||||
|
context.expect(/^isolated=true\s*$/m.test(source), 'Decorator generation requires an isolated instance')
|
||||||
|
await context.step('create a world with authored stair decorations', async () => {
|
||||||
|
const response = await context.command('/iris create name=decorator_probe type=decorator-fixture seed=78264193',
|
||||||
|
/Successfully created your world|Failed to create/, 180000)
|
||||||
|
context.expect(response.includes('Successfully created your world'), response)
|
||||||
|
})
|
||||||
|
await context.step('client sees single bottom-half stair decorations', async () => {
|
||||||
|
let checked = 0
|
||||||
|
await context.waitUntil(() => {
|
||||||
|
checked = 0
|
||||||
|
const center = context.bot.entity.position.floored()
|
||||||
|
for (let x = -6; x <= 6; x++) {
|
||||||
|
for (let z = -6; z <= 6; z++) {
|
||||||
|
for (let y = -4; y <= 1; y++) {
|
||||||
|
const position = center.offset(x, y, z)
|
||||||
|
if (context.bot.blockAt(position)?.name !== 'lime_wool') continue
|
||||||
|
const decoration = context.bot.blockAt(position.offset(0, 1, 0))
|
||||||
|
const above = context.bot.blockAt(position.offset(0, 2, 0))
|
||||||
|
if (!decoration || !above) return false
|
||||||
|
if (decoration.name !== 'oak_stairs' || decoration.getProperties().half !== 'bottom'
|
||||||
|
|| above.name !== 'air') return false
|
||||||
|
checked++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return checked >= 100
|
||||||
|
}, { label: 'At least 100 authored stair columns with empty space above', timeoutMs: 60000, intervalMs: 250 })
|
||||||
|
context.expect(checked >= 100, 'Client verified generated decorator columns', { checked })
|
||||||
|
context.report.decoratorColumns = checked
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ name: IrisNativeLifecycleFixture
|
|||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
main: art.arcane.iris.fixture.NativeLifecycleFixture
|
main: art.arcane.iris.fixture.NativeLifecycleFixture
|
||||||
api-version: '26.1.2'
|
api-version: '26.1.2'
|
||||||
|
folia-supported: true
|
||||||
depend: [Iris]
|
depend: [Iris]
|
||||||
commands:
|
commands:
|
||||||
irislifecycle:
|
irislifecycle:
|
||||||
|
|||||||
+284
-7
@@ -1,11 +1,31 @@
|
|||||||
package art.arcane.iris.fixture;
|
package art.arcane.iris.fixture;
|
||||||
|
|
||||||
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.volmlib.nativelib.NativeAdapters;
|
import art.arcane.volmlib.nativelib.NativeAdapters;
|
||||||
import art.arcane.iris.world.runtime.RuntimeInjection;
|
import art.arcane.iris.world.runtime.RuntimeInjection;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||||
import art.arcane.iris.world.IrisCreator;
|
import art.arcane.iris.world.IrisCreator;
|
||||||
|
import art.arcane.iris.world.task.J;
|
||||||
import art.arcane.iris.world.IrisToolbelt;
|
import art.arcane.iris.world.IrisToolbelt;
|
||||||
|
import art.arcane.iris.world.IrisWorldStorage;
|
||||||
|
import art.arcane.iris.world.WorldCreatorCompat;
|
||||||
|
import art.arcane.iris.world.lifecycle.LifecycleOperationCoordinator;
|
||||||
|
import art.arcane.iris.platform.bootstrap.ServerProperties;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.block.Biome;
|
import org.bukkit.block.Biome;
|
||||||
|
import org.bukkit.block.Container;
|
||||||
|
import org.bukkit.loot.Lootable;
|
||||||
|
import org.bukkit.loot.LootTable;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.meta.MapMeta;
|
||||||
|
import org.bukkit.map.MapView;
|
||||||
|
import org.bukkit.generator.structure.Structure;
|
||||||
|
import org.bukkit.util.StructureSearchResult;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeTerrainAccess;
|
import art.arcane.volmlib.nativelib.terrain.NativeTerrainAccess;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeStructureReader;
|
import art.arcane.volmlib.nativelib.terrain.NativeStructureReader;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeWorldRuntime;
|
import art.arcane.volmlib.nativelib.terrain.NativeWorldRuntime;
|
||||||
@@ -19,6 +39,10 @@ import org.bukkit.command.CommandSender;
|
|||||||
import org.bukkit.generator.ChunkGenerator;
|
import org.bukkit.generator.ChunkGenerator;
|
||||||
import org.bukkit.plugin.java.JavaPlugin;
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.LinkOption;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
@@ -26,12 +50,16 @@ import java.util.logging.Level;
|
|||||||
public final class NativeLifecycleFixture extends JavaPlugin implements WorldRuntimeExecution {
|
public final class NativeLifecycleFixture extends JavaPlugin implements WorldRuntimeExecution {
|
||||||
private static final NamespacedKey WORLD_KEY = NamespacedKey.minecraft("native_lifecycle_probe");
|
private static final NamespacedKey WORLD_KEY = NamespacedKey.minecraft("native_lifecycle_probe");
|
||||||
private static final NamespacedKey TERRAIN_KEY = new NamespacedKey("iris", "native_terrain_probe");
|
private static final NamespacedKey TERRAIN_KEY = new NamespacedKey("iris", "native_terrain_probe");
|
||||||
|
private static final NamespacedKey COMMAND_KEY = new NamespacedKey("iris", "lifecycle_probe");
|
||||||
|
private static final NamespacedKey TREASURE_KEY = new NamespacedKey("iris", "treasure_map_probe");
|
||||||
private static final long WORLD_SEED = 78264193L;
|
private static final long WORLD_SEED = 78264193L;
|
||||||
private NativeWorldRuntime runtime;
|
private NativeWorldRuntime runtime;
|
||||||
|
private TreasureLootChecks treasureLootChecks;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
runtime = NativeAdapters.require(NativeWorldRuntime.class);
|
runtime = NativeAdapters.require(NativeWorldRuntime.class);
|
||||||
|
treasureLootChecks = new TreasureLootChecks(this);
|
||||||
NativeTerrainAccess terrain = NativeAdapters.require(NativeTerrainAccess.class);
|
NativeTerrainAccess terrain = NativeAdapters.require(NativeTerrainAccess.class);
|
||||||
getLogger().info("Native lifecycle ready: " + runtime.description()
|
getLogger().info("Native lifecycle ready: " + runtime.description()
|
||||||
+ "; terrain provider=" + terrain.getClass().getName());
|
+ "; terrain provider=" + terrain.getClass().getName());
|
||||||
@@ -44,18 +72,40 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
|
||||||
if (arguments.length != 1) {
|
if (arguments.length == 0) {
|
||||||
sender.sendMessage("Use /irislifecycle create|unload|status|structures|terrain-create|terrain-check|terrain-unload");
|
sender.sendMessage("Use /irislifecycle create|unload|status|structures|terrain-create|terrain-check|terrain-unload|lifecycle-status|lifecycle-entry");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
switch (arguments[0]) {
|
switch (arguments[0]) {
|
||||||
|
case "treasure-map-create" -> createTreasureMapWorld(sender);
|
||||||
|
case "treasure-map-limit" -> runTreasureRegion(sender, () -> checkTreasureMapLimit(sender));
|
||||||
|
case "treasure-map-chest" -> {
|
||||||
|
if (arguments.length != 3) {
|
||||||
|
throw new IllegalArgumentException("Use treasure-map-chest <destination> <CHEST|COPPER_CHEST>");
|
||||||
|
}
|
||||||
|
runTreasureRegion(sender, () -> prepareTreasureMapChest(sender, arguments[1], arguments[2]));
|
||||||
|
}
|
||||||
|
case "treasure-map-inspect" -> {
|
||||||
|
if (arguments.length != 2) {
|
||||||
|
throw new IllegalArgumentException("Use treasure-map-inspect <destination>");
|
||||||
|
}
|
||||||
|
runTreasureRegion(sender, () -> inspectTreasureMapChest(sender, arguments[1]));
|
||||||
|
}
|
||||||
|
case "treasure-map-check" -> runTreasureRegion(sender, () -> verifyTreasureMapLoot(sender));
|
||||||
|
case "treasure-extra-check" -> runTreasureRegion(sender, () -> treasureLootChecks.verify(sender));
|
||||||
|
case "treasure-region-check" -> runTreasureRegion(sender, () -> treasureLootChecks.verifyRegion(sender));
|
||||||
|
case "treasure-reload-before" -> runTreasureRegion(sender, () -> treasureLootChecks.verifyReload(sender, false));
|
||||||
|
case "treasure-reload-after" -> runTreasureRegion(sender, () -> treasureLootChecks.verifyReload(sender, true));
|
||||||
|
case "treasure-resources" -> treasureLootChecks.reportResourceReloads(sender);
|
||||||
case "create" -> create(sender);
|
case "create" -> create(sender);
|
||||||
case "unload" -> unload(sender);
|
case "unload" -> unload(sender);
|
||||||
case "structures" -> structures(sender);
|
case "structures" -> structures(sender);
|
||||||
case "terrain-create" -> createTerrain(sender);
|
case "terrain-create" -> createTerrain(sender);
|
||||||
case "terrain-check" -> checkTerrain(sender);
|
case "terrain-check" -> checkTerrain(sender);
|
||||||
case "terrain-unload" -> unloadTerrain(sender);
|
case "terrain-unload" -> unloadTerrain(sender);
|
||||||
|
case "lifecycle-status" -> lifecycleStatus(sender);
|
||||||
|
case "lifecycle-entry" -> lifecycleEntry(sender);
|
||||||
case "status" -> sender.sendMessage("NATIVE_STATUS available=" + runtime.available()
|
case "status" -> sender.sendMessage("NATIVE_STATUS available=" + runtime.available()
|
||||||
+ " loaded=" + (Bukkit.getWorld(WORLD_KEY) != null));
|
+ " loaded=" + (Bukkit.getWorld(WORLD_KEY) != null));
|
||||||
default -> throw new IllegalArgumentException("Unknown lifecycle operation");
|
default -> throw new IllegalArgumentException("Unknown lifecycle operation");
|
||||||
@@ -66,6 +116,232 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void createTreasureMapWorld(CommandSender sender) {
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
throw new IllegalStateException("Treasure map fixture requires a player");
|
||||||
|
}
|
||||||
|
runTreasureRegion(sender, () -> prepareTreasureMapWorld(player));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runTreasureRegion(CommandSender sender, Runnable task) {
|
||||||
|
ensureTreasureMapWorld()
|
||||||
|
.thenCompose(ignored -> J.runRegionFuture(Bukkit.getWorld(TREASURE_KEY), 0, 0, task))
|
||||||
|
.whenComplete((ignored, failure) -> {
|
||||||
|
if (failure != null) {
|
||||||
|
fail(sender, failure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<?> ensureTreasureMapWorld() {
|
||||||
|
if (Bukkit.getWorld(TREASURE_KEY) != null) {
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
|
if (WorldCreatorCompat.persistentLevelRoot(TREASURE_KEY).exists()) {
|
||||||
|
return Iris.instance.worldReconciler().loadWorld(ServerProperties.BUKKIT_YML, TREASURE_KEY.toString());
|
||||||
|
}
|
||||||
|
return runAsync(() -> {
|
||||||
|
try {
|
||||||
|
new IrisCreator().name(TREASURE_KEY.toString()).dimension("treasure-map-fixture")
|
||||||
|
.seed(WORLD_SEED).create();
|
||||||
|
} catch (Exception failure) {
|
||||||
|
throw new IllegalStateException("Treasure map fixture creation failed", failure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareTreasureMapWorld(Player player) {
|
||||||
|
World world = Bukkit.getWorld(TREASURE_KEY);
|
||||||
|
if (world == null || !IrisToolbelt.isIrisWorld(world) || world.getSeed() != WORLD_SEED
|
||||||
|
|| !"treasure-map-fixture".equals(IrisToolbelt.access(world).getEngine().getDimension().getLoadKey())) {
|
||||||
|
throw new IllegalStateException("Treasure map fixture world identity is invalid");
|
||||||
|
}
|
||||||
|
for (int x = 0; x <= 8; x++) {
|
||||||
|
for (int z = 0; z <= 8; z++) {
|
||||||
|
world.getBlockAt(x, 160, z).setType(Material.STONE, false);
|
||||||
|
for (int y = 161; y <= 164; y++) {
|
||||||
|
world.getBlockAt(x, y, z).setType(Material.AIR, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!J.runEntity(player, () -> player.teleportAsync(new Location(world, 0.5, 161, 0.5)).whenComplete((teleported, failure) -> {
|
||||||
|
if (failure != null) {
|
||||||
|
fail(player, failure);
|
||||||
|
} else if (!Boolean.TRUE.equals(teleported)) {
|
||||||
|
fail(player, new IllegalStateException("Treasure map fixture teleport failed"));
|
||||||
|
} else {
|
||||||
|
player.sendMessage("TREASURE_MAP_READY world=" + TREASURE_KEY
|
||||||
|
+ " version=" + Bukkit.getMinecraftVersion() + " folia=" + J.isFolia());
|
||||||
|
}
|
||||||
|
}))) {
|
||||||
|
throw new IllegalStateException("Could not schedule treasure map fixture teleport");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkTreasureMapLimit(CommandSender sender) {
|
||||||
|
World world = Bukkit.getWorld(TREASURE_KEY);
|
||||||
|
if (world == null) {
|
||||||
|
throw new IllegalStateException("Treasure map fixture world is absent");
|
||||||
|
}
|
||||||
|
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||||
|
IrisStructureLocator.LocateResult located = IrisStructureLocator.locateInPlacementRings(
|
||||||
|
engine, "minecraft:buried_treasure", 2, 2, 50, (x, z) -> true);
|
||||||
|
if (located.status() != IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||||
|
throw new IllegalStateException("Blocked treasure placement returned " + located.status());
|
||||||
|
}
|
||||||
|
sender.sendMessage("TREASURE_MAP_LIMIT status=" + located.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareTreasureMapChest(CommandSender sender, String destination, String material) {
|
||||||
|
World world = Bukkit.getWorld(TREASURE_KEY);
|
||||||
|
treasureMapChest(world, destination, material);
|
||||||
|
sender.sendMessage("TREASURE_MAP_CHEST destination=" + destination + " material=" + material);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Container treasureMapChest(World world, String destination, String material) {
|
||||||
|
requireTreasureDestination(destination);
|
||||||
|
if (!List.of("CHEST", "COPPER_CHEST").contains(material)) {
|
||||||
|
throw new IllegalArgumentException("Unknown treasure map container: " + material);
|
||||||
|
}
|
||||||
|
LootTable loot = Bukkit.getLootTable(new NamespacedKey("iris_qa", destination));
|
||||||
|
if (loot == null) {
|
||||||
|
throw new IllegalStateException("Treasure map loot table is absent: " + destination);
|
||||||
|
}
|
||||||
|
world.getBlockAt(2, 161, 2).setType(Material.AIR, false);
|
||||||
|
world.getBlockAt(2, 161, 2).setType(Material.valueOf(material), false);
|
||||||
|
Container chest = (Container) world.getBlockAt(2, 161, 2).getState();
|
||||||
|
((Lootable) chest).setLootTable(loot);
|
||||||
|
((Lootable) chest).setSeed(WORLD_SEED);
|
||||||
|
if (!chest.update(true, false)) {
|
||||||
|
throw new IllegalStateException("Treasure map loot chest was not installed");
|
||||||
|
}
|
||||||
|
return (Container) world.getBlockAt(2, 161, 2).getState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyTreasureMapLoot(CommandSender sender) {
|
||||||
|
checkTreasureMapLimit(sender);
|
||||||
|
World world = Bukkit.getWorld(TREASURE_KEY);
|
||||||
|
for (String destination : List.of("blocked", "disabled", "found", "mixed")) {
|
||||||
|
Inventory inventory = treasureMapChest(world, destination, "CHEST").getInventory();
|
||||||
|
inspectTreasureMapChest(sender, destination);
|
||||||
|
inventory.remove(Material.DIAMOND);
|
||||||
|
Inventory reopened = ((Container) world.getBlockAt(2, 161, 2).getState()).getInventory();
|
||||||
|
if (reopened.contains(Material.DIAMOND)
|
||||||
|
|| !(reopened.contains(Material.FILLED_MAP) || reopened.contains(Material.MAP))) {
|
||||||
|
throw new IllegalStateException("Reopening changed " + destination + " chest loot");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sender.sendMessage("TREASURE_MAP_CHECK destinations=4 retained=true");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void inspectTreasureMapChest(CommandSender sender, String destination) {
|
||||||
|
requireTreasureDestination(destination);
|
||||||
|
World world = Bukkit.getWorld(TREASURE_KEY);
|
||||||
|
Inventory inventory = ((Container) world.getBlockAt(2, 161, 2).getState()).getInventory();
|
||||||
|
ItemStack map = null;
|
||||||
|
int diamonds = 0;
|
||||||
|
for (ItemStack item : inventory.getContents()) {
|
||||||
|
if (item == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((item.getType() == Material.MAP || item.getType() == Material.FILLED_MAP)
|
||||||
|
&& item.getAmount() == 1 && map == null) {
|
||||||
|
map = item;
|
||||||
|
} else if (item.getType() == Material.DIAMOND) {
|
||||||
|
diamonds += item.getAmount();
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Unexpected chest loot: " + item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (map == null || diamonds != 1) {
|
||||||
|
throw new IllegalStateException("Invalid " + destination + " chest: map=" + map + " diamonds=" + diamonds);
|
||||||
|
}
|
||||||
|
boolean expectedLocated = destination.equals("found") || destination.equals("mixed");
|
||||||
|
MapView view = map.getItemMeta() instanceof MapMeta meta && meta.hasMapView() ? meta.getMapView() : null;
|
||||||
|
if ((view != null) != expectedLocated) {
|
||||||
|
throw new IllegalStateException("Unexpected map data for " + destination + ": " + map);
|
||||||
|
}
|
||||||
|
String result = "TREASURE_MAP_INSPECT destination=" + destination + " diamonds=1 located=" + expectedLocated;
|
||||||
|
if (view != null) {
|
||||||
|
StructureSearchResult target = world.locateNearestStructure(
|
||||||
|
new Location(world, 2, 161, 2), Structure.DESERT_PYRAMID, 50, false);
|
||||||
|
if (target == null || target.getStructure() != Structure.DESERT_PYRAMID) {
|
||||||
|
throw new IllegalStateException("Map has no verified pyramid target");
|
||||||
|
}
|
||||||
|
int targetX = target.getLocation().getBlockX();
|
||||||
|
int targetZ = target.getLocation().getBlockZ();
|
||||||
|
if (view.getWorld() != world || view.getScale() != MapView.Scale.CLOSEST
|
||||||
|
|| view.getCenterX() != Math.floorDiv(targetX + 64, 128) * 128
|
||||||
|
|| view.getCenterZ() != Math.floorDiv(targetZ + 64, 128) * 128) {
|
||||||
|
throw new IllegalStateException("Map data does not cover the verified pyramid at " + target.getLocation());
|
||||||
|
}
|
||||||
|
result += " mapId=" + view.getId() + " targetX=" + targetX + " targetZ=" + targetZ
|
||||||
|
+ " centerX=" + view.getCenterX() + " centerZ=" + view.getCenterZ();
|
||||||
|
}
|
||||||
|
sender.sendMessage(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireTreasureDestination(String destination) {
|
||||||
|
if (!List.of("blocked", "disabled", "found", "mixed").contains(destination)) {
|
||||||
|
throw new IllegalArgumentException("Unknown treasure map destination: " + destination);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void lifecycleStatus(CommandSender sender) throws Exception {
|
||||||
|
World world = Bukkit.getWorld(COMMAND_KEY);
|
||||||
|
Path storage = WorldCreatorCompat.persistentLevelRoot(COMMAND_KEY).toPath();
|
||||||
|
Path levelRoot = IrisWorldStorage.levelRoot().toPath();
|
||||||
|
Path registry = levelRoot.resolve("iris/worlds.json");
|
||||||
|
boolean registered = Files.exists(registry)
|
||||||
|
&& Files.readString(registry).contains("\"" + COMMAND_KEY + "\"");
|
||||||
|
String startupName = IrisWorldStorage.configuredWorldName(COMMAND_KEY, levelRoot.getFileName().toString());
|
||||||
|
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML);
|
||||||
|
boolean configured = configuration.getConfigurationSection("worlds") != null
|
||||||
|
&& configuration.getConfigurationSection("worlds").contains(startupName);
|
||||||
|
File quarantineRoot = levelRoot.resolve("dimensions/iris").toFile();
|
||||||
|
File[] quarantines = quarantineRoot.listFiles((directory, name) -> name.startsWith(".iris-delete-"));
|
||||||
|
int quarantineCount = quarantines == null ? 0 : quarantines.length;
|
||||||
|
Path queueFile = Bukkit.getPluginManager().getPlugin("Iris").getDataFolder().toPath()
|
||||||
|
.resolve("pending-world-deletes.txt");
|
||||||
|
List<String> queue = Files.exists(queueFile) ? Files.readAllLines(queueFile) : List.of();
|
||||||
|
boolean queued = quarantineCount > 0;
|
||||||
|
if (quarantines != null) {
|
||||||
|
for (File quarantine : quarantines) {
|
||||||
|
queued &= queue.contains(quarantine.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolean disk = Files.exists(storage, LinkOption.NOFOLLOW_LINKS);
|
||||||
|
String cleanup = disk ? "present" : quarantineCount == 0 ? "deleted" : queued ? "queued" : "unsafe";
|
||||||
|
boolean idle = true;
|
||||||
|
LifecycleOperationCoordinator coordinator = LifecycleOperationCoordinator.get();
|
||||||
|
for (LifecycleOperationCoordinator.Domain domain : LifecycleOperationCoordinator.Domain.values()) {
|
||||||
|
idle &= coordinator.active(domain).isEmpty();
|
||||||
|
}
|
||||||
|
sender.sendMessage("IRIS_LIFECYCLE loaded=" + (world != null) + " registered=" + registered
|
||||||
|
+ " configured=" + configured + " disk=" + disk + " quarantines=" + quarantineCount
|
||||||
|
+ " cleanup=" + cleanup + " idle=" + idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void lifecycleEntry(CommandSender sender) {
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
throw new IllegalArgumentException("Lifecycle entry requires a player");
|
||||||
|
}
|
||||||
|
World world = Bukkit.getWorld(COMMAND_KEY);
|
||||||
|
if (world == null || player.getWorld() != world || world.getSeed() != WORLD_SEED
|
||||||
|
|| !IrisToolbelt.isIrisWorld(world)) {
|
||||||
|
throw new IllegalStateException("Player did not enter the requested seeded Iris world");
|
||||||
|
}
|
||||||
|
Location feet = player.getLocation();
|
||||||
|
Material floor = world.getBlockAt(feet.getBlockX(), feet.getBlockY() - 1, feet.getBlockZ()).getType();
|
||||||
|
if (floor != Material.LIME_WOOL || !floor.isSolid()
|
||||||
|
|| !world.getBlockAt(feet.getBlockX(), feet.getBlockY(), feet.getBlockZ()).isPassable()
|
||||||
|
|| !world.getBlockAt(feet.getBlockX(), feet.getBlockY() + 1, feet.getBlockZ()).isPassable()) {
|
||||||
|
throw new IllegalStateException("Lifecycle player entry lacks a safe fixture floor");
|
||||||
|
}
|
||||||
|
sender.sendMessage("IRIS_LIFECYCLE_ENTRY world=" + COMMAND_KEY + " seed=" + world.getSeed()
|
||||||
|
+ " floor=lime_wool safe=true");
|
||||||
|
}
|
||||||
|
|
||||||
private void create(CommandSender sender) throws ReflectiveOperationException {
|
private void create(CommandSender sender) throws ReflectiveOperationException {
|
||||||
if (!runtime.available() || Bukkit.getWorld(WORLD_KEY) != null) {
|
if (!runtime.available() || Bukkit.getWorld(WORLD_KEY) != null) {
|
||||||
throw new IllegalStateException("Native runtime unavailable or probe world already loaded");
|
throw new IllegalStateException("Native runtime unavailable or probe world already loaded");
|
||||||
@@ -124,6 +400,7 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
|||||||
int y = world.getHighestBlockYAt(x, z);
|
int y = world.getHighestBlockYAt(x, z);
|
||||||
if (y < 90 || y > 100 || world.getBlockAt(x, y, z).getType() != Material.LIME_WOOL
|
if (y < 90 || y > 100 || world.getBlockAt(x, y, z).getType() != Material.LIME_WOOL
|
||||||
|| world.getBlockAt(x, y - 3, z).getType() != Material.GREEN_CONCRETE
|
|| world.getBlockAt(x, y - 3, z).getType() != Material.GREEN_CONCRETE
|
||||||
|
|| world.getBlockAt(x, world.getMinHeight(), z).getType() != Material.GREEN_CONCRETE
|
||||||
|| world.getBiome(x, y, z) != Biome.PLAINS) {
|
|| world.getBiome(x, y, z) != Biome.PLAINS) {
|
||||||
throw new IllegalStateException("Terrain column differs at " + x + "," + y + "," + z
|
throw new IllegalStateException("Terrain column differs at " + x + "," + y + "," + z
|
||||||
+ ": " + world.getBlockAt(x, y, z).getType());
|
+ ": " + world.getBlockAt(x, y, z).getType());
|
||||||
@@ -212,11 +489,11 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean regionized() {
|
public boolean regionized() {
|
||||||
return false;
|
return J.isFolia();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean primaryThread() {
|
public boolean primaryThread() {
|
||||||
return Bukkit.isPrimaryThread();
|
return J.isFolia() ? Bukkit.isGlobalTickThread() : Bukkit.isPrimaryThread();
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<Void> runGlobal(Runnable task) {
|
public CompletableFuture<Void> runGlobal(Runnable task) {
|
||||||
@@ -229,10 +506,10 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
|||||||
result.completeExceptionally(failure);
|
result.completeExceptionally(failure);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (Bukkit.isPrimaryThread()) {
|
if (primaryThread()) {
|
||||||
wrapped.run();
|
wrapped.run();
|
||||||
} else {
|
} else if (!J.runGlobal(wrapped)) {
|
||||||
Bukkit.getScheduler().runTask(this, wrapped);
|
result.completeExceptionally(new IllegalStateException("Could not schedule fixture task"));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
+259
@@ -0,0 +1,259 @@
|
|||||||
|
package art.arcane.iris.fixture;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
|
||||||
|
import art.arcane.iris.platform.generation.EngineBukkitOps;
|
||||||
|
import art.arcane.iris.world.IrisToolbelt;
|
||||||
|
import art.arcane.iris.world.loot.InventorySlotType;
|
||||||
|
import art.arcane.iris.world.loot.IrisVanillaLootTable;
|
||||||
|
import art.arcane.iris.world.loot.IrisLootTable;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||||
|
import art.arcane.iris.world.task.J;
|
||||||
|
import art.arcane.volmlib.util.matter.Matter;
|
||||||
|
import io.papermc.paper.event.world.StructuresLocateEvent;
|
||||||
|
import io.papermc.paper.event.server.ServerResourcesReloadedEvent;
|
||||||
|
import io.papermc.paper.math.Position;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.NamespacedKey;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.Container;
|
||||||
|
import org.bukkit.block.Chest;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.world.LootGenerateEvent;
|
||||||
|
import org.bukkit.generator.structure.Structure;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
import org.bukkit.util.StructureSearchResult;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
public final class TreasureLootChecks implements Listener {
|
||||||
|
private static final NamespacedKey WORLD_KEY = new NamespacedKey("iris", "treasure_map_probe");
|
||||||
|
private static final List<String> CHESTS = List.of(
|
||||||
|
"CHEST", "COPPER_CHEST", "EXPOSED_COPPER_CHEST", "WEATHERED_COPPER_CHEST",
|
||||||
|
"OXIDIZED_COPPER_CHEST", "WAXED_COPPER_CHEST", "WAXED_EXPOSED_COPPER_CHEST",
|
||||||
|
"WAXED_WEATHERED_COPPER_CHEST", "WAXED_OXIDIZED_COPPER_CHEST");
|
||||||
|
private final IrisVanillaLootTable reloadTable = new IrisVanillaLootTable("iris_qa:reload");
|
||||||
|
private final AtomicInteger resourceReloads = new AtomicInteger();
|
||||||
|
private LocateAction locateAction = LocateAction.PASS;
|
||||||
|
private int locateEvents;
|
||||||
|
private int lootEvents;
|
||||||
|
private boolean lootEventsOwned = true;
|
||||||
|
|
||||||
|
public TreasureLootChecks(JavaPlugin plugin) {
|
||||||
|
Bukkit.getPluginManager().registerEvents(this, plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void verify(CommandSender sender) {
|
||||||
|
World world = requireWorld();
|
||||||
|
verifyCopperLoot(world);
|
||||||
|
sender.sendMessage("TREASURE_COPPER_LOOT variants=8 normal=true diamonds=1");
|
||||||
|
verifyInventoryRollback(world);
|
||||||
|
sender.sendMessage("TREASURE_LOOT_ROLLBACK contents=true nativeTable=true seed=true retry=true");
|
||||||
|
verifyLocateEvents(world);
|
||||||
|
sender.sendMessage("TREASURE_LOCATE_EVENTS cancelled=true replaced=true calls=2");
|
||||||
|
sender.sendMessage("TREASURE_EXTRA_CHECK variants=9 rollback=true locateEvents=true");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void verifyRegion(CommandSender sender) {
|
||||||
|
World world = requireWorld();
|
||||||
|
if (!J.isOwnedByCurrentRegion(world, 0, 0)) {
|
||||||
|
throw new IllegalStateException("Fixture does not own its loot region");
|
||||||
|
}
|
||||||
|
lootEvents = 0;
|
||||||
|
lootEventsOwned = true;
|
||||||
|
verifyCopperLoot(world);
|
||||||
|
if (lootEvents != CHESTS.size() || !lootEventsOwned) {
|
||||||
|
throw new IllegalStateException("Loot event ownership failed: count=" + lootEvents
|
||||||
|
+ " owned=" + lootEventsOwned);
|
||||||
|
}
|
||||||
|
sender.sendMessage("TREASURE_REGION_LOOT copper=8 normal=1 events=" + lootEvents
|
||||||
|
+ " owned=true folia=" + J.isFolia());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onLoot(LootGenerateEvent event) {
|
||||||
|
if (event.getWorld().getKey().equals(WORLD_KEY) && event.isPlugin()) {
|
||||||
|
Location origin = event.getLootContext().getLocation();
|
||||||
|
lootEvents++;
|
||||||
|
lootEventsOwned &= J.isOwnedByCurrentRegion(event.getWorld(),
|
||||||
|
origin.getBlockX() >> 4, origin.getBlockZ() >> 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void verifyReload(CommandSender sender, boolean reloaded) {
|
||||||
|
World world = requireWorld();
|
||||||
|
Material expected = reloaded ? Material.EMERALD : Material.DIAMOND;
|
||||||
|
Collection<ItemStack> items = reloadTable.getLoot(
|
||||||
|
false, 783L, InventorySlotType.STORAGE, world, 2, 161, 2);
|
||||||
|
if (items.size() != 1 || items.iterator().next().getType() != expected) {
|
||||||
|
throw new IllegalStateException("Reloaded loot did not contain " + expected + ": " + items);
|
||||||
|
}
|
||||||
|
sender.sendMessage("TREASURE_RELOAD material=" + expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reportResourceReloads(CommandSender sender) {
|
||||||
|
sender.sendMessage("TREASURE_RESOURCES revision=" + resourceReloads.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onResourcesReloaded(ServerResourcesReloadedEvent event) {
|
||||||
|
resourceReloads.incrementAndGet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onLocate(StructuresLocateEvent event) {
|
||||||
|
if (!event.getWorld().getKey().equals(WORLD_KEY) || locateAction == LocateAction.PASS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
locateEvents++;
|
||||||
|
if (locateAction == LocateAction.CANCEL) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
} else {
|
||||||
|
event.setResult(new StructuresLocateEvent.Result(Position.block(123, 100, 456), Structure.DESERT_PYRAMID));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyCopperLoot(World world) {
|
||||||
|
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||||
|
Block block = world.getBlockAt(4, 161, 4);
|
||||||
|
MantleChunk<Matter> mantle = engine.getMantle().getMantle().useChunk(block.getChunk());
|
||||||
|
try {
|
||||||
|
for (String name : CHESTS) {
|
||||||
|
block.setType(Material.AIR, false);
|
||||||
|
block.setType(Material.valueOf(name), false);
|
||||||
|
if (!BukkitBlockResolution.isStorageChest(block.getBlockData())) {
|
||||||
|
throw new IllegalStateException("Chest is excluded from Iris loot: " + name);
|
||||||
|
}
|
||||||
|
EngineBukkitOps.update(engine, 4, 161, 4, block.getChunk(), mantle);
|
||||||
|
Container chest = (Container) block.getState();
|
||||||
|
int diamonds = 0;
|
||||||
|
for (ItemStack item : chest.getInventory().getContents()) {
|
||||||
|
if (item == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (item.getType() != Material.DIAMOND) {
|
||||||
|
throw new IllegalStateException("Unexpected " + name + " loot: " + item);
|
||||||
|
}
|
||||||
|
diamonds += item.getAmount();
|
||||||
|
}
|
||||||
|
if (diamonds != 1) {
|
||||||
|
throw new IllegalStateException(name + " contains " + diamonds + " diamonds");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
mantle.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyInventoryRollback(World world) {
|
||||||
|
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||||
|
Block block = world.getBlockAt(6, 161, 4);
|
||||||
|
block.setType(Material.AIR, false);
|
||||||
|
block.setType(Material.CHEST, false);
|
||||||
|
Chest chest = (Chest) block.getState();
|
||||||
|
chest.getBlockInventory().setItem(0, new ItemStack(Material.EMERALD, 3));
|
||||||
|
chest = (Chest) block.getState();
|
||||||
|
NamespacedKey key = new NamespacedKey("iris_qa", "reload");
|
||||||
|
chest.setLootTable(Bukkit.getLootTable(key));
|
||||||
|
chest.setSeed(913L);
|
||||||
|
if (!chest.update(true, false)) {
|
||||||
|
throw new IllegalStateException("Could not set native loot for rollback check");
|
||||||
|
}
|
||||||
|
Inventory inventory = chest.getBlockInventory();
|
||||||
|
IllegalStateException injected = new IllegalStateException("Fixture insertion interruption");
|
||||||
|
Inventory interrupted = (Inventory) Proxy.newProxyInstance(Inventory.class.getClassLoader(),
|
||||||
|
new Class<?>[]{Inventory.class}, (proxy, method, arguments) -> {
|
||||||
|
Object result;
|
||||||
|
try {
|
||||||
|
result = method.invoke(inventory, arguments);
|
||||||
|
} catch (InvocationTargetException failure) {
|
||||||
|
throw failure.getCause();
|
||||||
|
}
|
||||||
|
if (method.getName().equals("addItem")) {
|
||||||
|
throw injected;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
KList<IrisLootTable> tables = new KList<>();
|
||||||
|
tables.add(new IrisVanillaLootTable("iris_qa:reload"));
|
||||||
|
try {
|
||||||
|
EngineBukkitOps.addItems(engine, false, interrupted, tables, InventorySlotType.STORAGE,
|
||||||
|
world, 6, 161, 4);
|
||||||
|
throw new IllegalStateException("Interrupted insertion did not fail");
|
||||||
|
} catch (IllegalStateException failure) {
|
||||||
|
if (failure != injected) {
|
||||||
|
throw failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Chest restored = (Chest) block.getState();
|
||||||
|
if (restored.getLootTable() == null || !restored.getLootTable().getKey().equals(key)
|
||||||
|
|| restored.getSeed() != 913L) {
|
||||||
|
throw new IllegalStateException("Rollback lost the native loot table or seed");
|
||||||
|
}
|
||||||
|
ItemStack existing = restored.getSnapshotInventory().getItem(0);
|
||||||
|
if (existing == null || existing.getType() != Material.EMERALD || existing.getAmount() != 3) {
|
||||||
|
throw new IllegalStateException("Rollback lost the original contents: " + existing);
|
||||||
|
}
|
||||||
|
EngineBukkitOps.addItems(engine, false, restored.getBlockInventory(), tables, InventorySlotType.STORAGE,
|
||||||
|
world, 6, 161, 4);
|
||||||
|
int diamonds = 0;
|
||||||
|
int emeralds = 0;
|
||||||
|
for (ItemStack item : restored.getBlockInventory().getContents()) {
|
||||||
|
if (item == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (item.getType() == Material.DIAMOND) {
|
||||||
|
diamonds += item.getAmount();
|
||||||
|
} else if (item.getType() == Material.EMERALD) {
|
||||||
|
emeralds += item.getAmount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (diamonds != 2 || emeralds != 3 || ((Chest) block.getState()).getLootTable() != null) {
|
||||||
|
throw new IllegalStateException("Loot retry failed: diamonds=" + diamonds + " emeralds=" + emeralds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyLocateEvents(World world) {
|
||||||
|
Location origin = new Location(world, 2, 161, 2);
|
||||||
|
locateEvents = 0;
|
||||||
|
try {
|
||||||
|
locateAction = LocateAction.CANCEL;
|
||||||
|
if (world.locateNearestStructure(origin, Structure.DESERT_PYRAMID, 50, false) != null) {
|
||||||
|
throw new IllegalStateException("Cancelled structure search returned a target");
|
||||||
|
}
|
||||||
|
locateAction = LocateAction.REPLACE;
|
||||||
|
StructureSearchResult result = world.locateNearestStructure(origin, Structure.DESERT_PYRAMID, 50, false);
|
||||||
|
if (result == null || result.getStructure() != Structure.DESERT_PYRAMID
|
||||||
|
|| result.getLocation().getBlockX() != 123 || result.getLocation().getBlockY() != 100
|
||||||
|
|| result.getLocation().getBlockZ() != 456 || locateEvents != 2) {
|
||||||
|
throw new IllegalStateException("Structure search ignored event result: " + result);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
locateAction = LocateAction.PASS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private World requireWorld() {
|
||||||
|
World world = Bukkit.getWorld(WORLD_KEY);
|
||||||
|
if (world == null || !IrisToolbelt.isIrisWorld(world)) {
|
||||||
|
throw new IllegalStateException("Treasure map fixture world is absent");
|
||||||
|
}
|
||||||
|
return world;
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum LocateAction {
|
||||||
|
PASS, CANCEL, REPLACE
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,15 @@
|
|||||||
"block": "minecraft:lime_wool"
|
"block": "minecraft:lime_wool"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minHeight": 384,
|
||||||
|
"maxHeight": 384,
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:green_concrete"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"generators": [
|
"generators": [
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@
|
|||||||
"rockPalette": {
|
"rockPalette": {
|
||||||
"palette": [
|
"palette": [
|
||||||
{
|
{
|
||||||
"block": "minecraft:green_concrete"
|
"block": "minecraft:red_concrete"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { readFile } from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
const world = 'native-terrain-support'
|
||||||
|
const air = new Set(['air', 'cave_air', 'void_air'])
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'iris-native-terrain-support',
|
||||||
|
description: 'Verify native terrain fitting preserves buried caves and excludes trees from lowered ground.',
|
||||||
|
async run(context) {
|
||||||
|
const bot = context.bot
|
||||||
|
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||||
|
context.expect(/^isolated=true\s*$/m.test(source), 'Requires an isolated instance')
|
||||||
|
const position = bot.entity.position.clone()
|
||||||
|
const at = (x, y, z) => bot.blockAt(position.clone().set(x, y, z))
|
||||||
|
const teleport = async (x, y, z) => {
|
||||||
|
await context.command(`/minecraft:tp @s ${x + 0.5} ${y} ${z + 0.5}`, /Teleported/i, 60000)
|
||||||
|
await context.waitUntil(() => Math.abs(bot.entity.position.x - x - 0.5) < 1
|
||||||
|
&& Math.abs(bot.entity.position.z - z - 0.5) < 1 && at(x, y, z),
|
||||||
|
{ label: 'Coordinate chunk received', timeoutMs: 90000, intervalMs: 100 })
|
||||||
|
}
|
||||||
|
await context.step('enter the seeded overworld', async () => {
|
||||||
|
if (context.options.command !== 'reuse') {
|
||||||
|
await context.command(`/iris create name=${world} type=overworld seed=1337`,
|
||||||
|
/Successfully created your world/i, 240000)
|
||||||
|
}
|
||||||
|
await context.command(`/iris tp ${world}`)
|
||||||
|
await context.waitUntil(async () => {
|
||||||
|
const snapshot = await context.observe()
|
||||||
|
const player = snapshot.players.find(entry => entry.username === bot.username)
|
||||||
|
return snapshot.worlds.some(entry => entry.id === player?.world && entry.name.endsWith(world))
|
||||||
|
}, { label: 'Entered seeded overworld', timeoutMs: 90000, intervalMs: 100 })
|
||||||
|
await context.command('/minecraft:gamemode spectator @s', /Spectator Mode/i)
|
||||||
|
await context.command('/minecraft:seed', /Seed:.*1337/)
|
||||||
|
})
|
||||||
|
await context.step('preserve caves beneath the ruined portal terrain skirt', async () => {
|
||||||
|
await teleport(995, 44, -3611)
|
||||||
|
await context.waitUntil(() => at(990, 43, -3626) && at(992, 43, -3627),
|
||||||
|
{ label: 'Cave columns received', timeoutMs: 90000, intervalMs: 100 })
|
||||||
|
for (const [x, z, bottom, top] of [[990, -3626, 33, 54], [992, -3627, 34, 55]]) {
|
||||||
|
for (let y = bottom; y <= top; y++) {
|
||||||
|
const block = at(x, y, z)
|
||||||
|
context.expect(block && air.has(block.name), 'Native terrain fitting filled a buried cave',
|
||||||
|
{ x, y, z, material: block?.name })
|
||||||
|
}
|
||||||
|
await context.command(`/execute if block ${x} 43 ${z} minecraft:cave_air run say CAVE_PRESERVED_${x}`,
|
||||||
|
new RegExp(`CAVE_PRESERVED_${x}`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await context.step('exclude the pine whose support is removed by village grading', async () => {
|
||||||
|
await teleport(-2601, 174, -7063)
|
||||||
|
await context.waitUntil(() => at(-2601, 167, -7063),
|
||||||
|
{ label: 'Pine column received', timeoutMs: 90000, intervalMs: 100 })
|
||||||
|
const block = at(-2601, 167, -7063)
|
||||||
|
context.expect(air.has(block.name), 'Tree remains above terrain removed by native grading',
|
||||||
|
{ x: -2601, y: 167, z: -7063, material: block.name })
|
||||||
|
await context.command('/execute if block -2601 167 -7063 minecraft:air run say PINE_EXCLUDED',
|
||||||
|
/PINE_EXCLUDED/)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+235
-6
@@ -6,6 +6,11 @@ import art.arcane.iris.generation.runtime.BiomeEnvironment;
|
|||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.world.IrisCreator;
|
import art.arcane.iris.world.IrisCreator;
|
||||||
import art.arcane.iris.world.entity.IrisSpawner;
|
import art.arcane.iris.world.entity.IrisSpawner;
|
||||||
|
import art.arcane.iris.world.entity.IrisMarker;
|
||||||
|
import art.arcane.iris.world.entity.EntitySpawnSeed;
|
||||||
|
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||||
|
import art.arcane.iris.pack.value.IrisPosition;
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
import art.arcane.iris.world.entity.IrisEntitySpawn;
|
import art.arcane.iris.world.entity.IrisEntitySpawn;
|
||||||
import art.arcane.iris.world.entity.IrisEntity;
|
import art.arcane.iris.world.entity.IrisEntity;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||||
@@ -28,6 +33,7 @@ import org.bukkit.entity.Entity;
|
|||||||
import org.bukkit.entity.EntityType;
|
import org.bukkit.entity.EntityType;
|
||||||
import org.bukkit.entity.LivingEntity;
|
import org.bukkit.entity.LivingEntity;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.bukkit.event.EventHandler;
|
import org.bukkit.event.EventHandler;
|
||||||
import org.bukkit.event.EventPriority;
|
import org.bukkit.event.EventPriority;
|
||||||
import org.bukkit.event.Listener;
|
import org.bukkit.event.Listener;
|
||||||
@@ -38,9 +44,13 @@ import java.lang.reflect.Field;
|
|||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HexFormat;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
@@ -83,7 +93,7 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
|
|||||||
@Override
|
@Override
|
||||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
|
||||||
if (arguments.length != 1) {
|
if (arguments.length != 1) {
|
||||||
notifySender(sender, "Use /irisspawnqa setup|frogs|zombies|stress|check|surface|cave");
|
notifySender(sender, "Use /irisspawnqa setup|frogs|zombies|stress|check|seeded|completion|surface|cave");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -93,6 +103,8 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
|
|||||||
case "zombies" -> runRegion(sender, () -> spawnZombies(sender));
|
case "zombies" -> runRegion(sender, () -> spawnZombies(sender));
|
||||||
case "stress" -> runRegion(sender, () -> stress(sender));
|
case "stress" -> runRegion(sender, () -> stress(sender));
|
||||||
case "check" -> runRegion(sender, () -> check(sender));
|
case "check" -> runRegion(sender, () -> check(sender));
|
||||||
|
case "seeded" -> seeded(sender);
|
||||||
|
case "completion" -> completion(sender);
|
||||||
case "surface" -> view(sender, 67D);
|
case "surface" -> view(sender, 67D);
|
||||||
case "cave" -> view(sender, -46D);
|
case "cave" -> view(sender, -46D);
|
||||||
default -> throw new IllegalArgumentException("Unknown spawning operation");
|
default -> throw new IllegalArgumentException("Unknown spawning operation");
|
||||||
@@ -147,6 +159,9 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
|
|||||||
private void configureWorld(CommandSender sender, World world) {
|
private void configureWorld(CommandSender sender, World world) {
|
||||||
try {
|
try {
|
||||||
world.setDifficulty(Difficulty.NORMAL);
|
world.setDifficulty(Difficulty.NORMAL);
|
||||||
|
world.setTime(6000L);
|
||||||
|
world.setStorm(false);
|
||||||
|
world.setThundering(false);
|
||||||
world.setGameRule(GameRules.SPAWN_MOBS, true);
|
world.setGameRule(GameRules.SPAWN_MOBS, true);
|
||||||
List<CompletableFuture<Void>> chunks = new ArrayList<>();
|
List<CompletableFuture<Void>> chunks = new ArrayList<>();
|
||||||
for (int chunkX = -1; chunkX <= 1; chunkX++) {
|
for (int chunkX = -1; chunkX <= 1; chunkX++) {
|
||||||
@@ -176,6 +191,11 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
|
|||||||
Bukkit.getRegionScheduler().execute(this, world, chunkX, chunkZ, () -> {
|
Bukkit.getRegionScheduler().execute(this, world, chunkX, chunkZ, () -> {
|
||||||
try {
|
try {
|
||||||
chunk.addPluginChunkTicket(this);
|
chunk.addPluginChunkTicket(this);
|
||||||
|
for (Entity entity : chunk.getEntities()) {
|
||||||
|
if (entity instanceof LivingEntity && !(entity instanceof Player)) {
|
||||||
|
entity.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
retained.complete(null);
|
retained.complete(null);
|
||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
retained.completeExceptionally(error);
|
retained.completeExceptionally(error);
|
||||||
@@ -244,12 +264,218 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void completion(CommandSender sender) {
|
||||||
|
World world = requireWorld();
|
||||||
|
Bukkit.getGlobalRegionScheduler().execute(this, () -> {
|
||||||
|
world.setGameRule(GameRules.SPAWN_MOBS, false);
|
||||||
|
runRegion(sender, () -> beginCompletion(sender, world));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void beginCompletion(CommandSender sender, World world) {
|
||||||
|
try {
|
||||||
|
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||||
|
Object manager = engine.getWorldManager();
|
||||||
|
Field field = manager.getClass().getDeclaredField("chunkMaintenance");
|
||||||
|
field.setAccessible(true);
|
||||||
|
Object maintenance = field.get(manager);
|
||||||
|
Method run = maintenance.getClass().getDeclaredMethod("runInitialSpawn", World.class, int.class, int.class, Runnable.class);
|
||||||
|
run.setAccessible(true);
|
||||||
|
Field pending = maintenance.getClass().getDeclaredField("markerFlagQueue");
|
||||||
|
pending.setAccessible(true);
|
||||||
|
AtomicInteger callbacks = new AtomicInteger();
|
||||||
|
Runnable callback = () -> {
|
||||||
|
require(Bukkit.isOwnedByCurrentRegion(world, 0, 0), "Initial callback ran outside its owning region");
|
||||||
|
callbacks.incrementAndGet();
|
||||||
|
Chunk chunk = world.getChunkAt(0, 0);
|
||||||
|
invokeAmbient(runtimeSpawn(chunk, "seeded"), chunk, true);
|
||||||
|
};
|
||||||
|
run.invoke(maintenance, world, 0, 0, callback);
|
||||||
|
awaitSkippedInitial(sender, world, new CompletionRun(engine, maintenance, run, pending, callback, callbacks), 0);
|
||||||
|
} catch (ReflectiveOperationException failure) {
|
||||||
|
throw new IllegalStateException("Could not start initial completion test", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void awaitSkippedInitial(CommandSender sender, World world, CompletionRun state, int attempt) {
|
||||||
|
Bukkit.getRegionScheduler().runDelayed(this, world, 0, 0, task -> {
|
||||||
|
try {
|
||||||
|
require(state.callbacks().get() == 0, "Disabled mob spawning consumed the initial pass");
|
||||||
|
if (!((Set<?>) state.pending().get(state.maintenance())).isEmpty()) {
|
||||||
|
require(attempt < 100, "Skipped initial pass did not release its pending state");
|
||||||
|
awaitSkippedInitial(sender, world, state, attempt + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Bukkit.getGlobalRegionScheduler().execute(this, () -> {
|
||||||
|
world.setGameRule(GameRules.SPAWN_MOBS, true);
|
||||||
|
runRegion(sender, () -> retryInitialCompletion(sender, world, state));
|
||||||
|
});
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
fail(sender, failure);
|
||||||
|
}
|
||||||
|
}, 2L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void retryInitialCompletion(CommandSender sender, World world, CompletionRun state) {
|
||||||
|
try {
|
||||||
|
state.run().invoke(state.maintenance(), world, 0, 0, state.callback());
|
||||||
|
state.run().invoke(state.maintenance(), world, 0, 0, state.callback());
|
||||||
|
awaitInitialCompletion(sender, world, state, 0);
|
||||||
|
} catch (ReflectiveOperationException failure) {
|
||||||
|
throw new IllegalStateException("Could not retry initial spawning", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void awaitInitialCompletion(CommandSender sender, World world, CompletionRun state, int attempt) {
|
||||||
|
Bukkit.getRegionScheduler().runDelayed(this, world, 0, 0, task -> {
|
||||||
|
try {
|
||||||
|
if (state.callbacks().get() == 0 || !((Set<?>) state.pending().get(state.maintenance())).isEmpty()) {
|
||||||
|
require(attempt < 100, "Initial spawning did not complete after retry");
|
||||||
|
awaitInitialCompletion(sender, world, state, attempt + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
require(state.callbacks().get() == 1, "Concurrent initial requests spawned duplicate populations");
|
||||||
|
state.run().invoke(state.maintenance(), world, 0, 0, state.callback());
|
||||||
|
Bukkit.getAsyncScheduler().runNow(this, ignored -> {
|
||||||
|
try {
|
||||||
|
require(state.engine().getMantle().getMantle().hasFlag(0, 0, MantleFlag.INITIAL_SPAWNED_MARKER), "Initial completion was not persisted");
|
||||||
|
require(state.callbacks().get() == 1, "Completed initial pass ran again");
|
||||||
|
evidence(sender, "SPAWN_QA_COMPLETION retry=true callbacks=1 persisted=true owned=true");
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
fail(sender, failure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
fail(sender, failure);
|
||||||
|
}
|
||||||
|
}, 2L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void seeded(CommandSender sender) {
|
||||||
|
requireWorld();
|
||||||
|
List<List<String>> baseline = new ArrayList<>();
|
||||||
|
sampleInitial(0, false).thenCompose(first -> {
|
||||||
|
baseline.add(first);
|
||||||
|
return sampleInitial(1, false);
|
||||||
|
}).thenCompose(second -> {
|
||||||
|
baseline.add(second);
|
||||||
|
return sampleInitial(1, false);
|
||||||
|
}).thenCompose(repeated -> {
|
||||||
|
require(baseline.get(1).equals(repeated), "Second chunk changed after reversed generation order");
|
||||||
|
return sampleInitial(0, false);
|
||||||
|
}).thenCompose(repeated -> {
|
||||||
|
require(baseline.get(0).equals(repeated), "First chunk changed after reversed generation order");
|
||||||
|
return sampleInitial(0, true);
|
||||||
|
}).thenCompose(marker -> {
|
||||||
|
baseline.add(marker);
|
||||||
|
return sampleInitial(1, true);
|
||||||
|
}).thenCompose(marker -> {
|
||||||
|
baseline.add(marker);
|
||||||
|
return sampleInitial(1, true);
|
||||||
|
}).thenCompose(repeated -> {
|
||||||
|
require(baseline.get(3).equals(repeated), "Second marker changed after reversed generation order");
|
||||||
|
return sampleInitial(0, true);
|
||||||
|
}).whenComplete((repeated, failure) -> {
|
||||||
|
if (failure != null) {
|
||||||
|
fail(sender, failure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
require(baseline.get(2).equals(repeated), "First marker changed after reversed generation order");
|
||||||
|
evidence(sender, "SPAWN_QA_SEEDED chunks=2 markers=2 repeats=2 equipment=true attributes=true");
|
||||||
|
} catch (Throwable error) {
|
||||||
|
fail(sender, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<List<String>> sampleInitial(int chunkX, boolean marker) {
|
||||||
|
CompletableFuture<List<String>> result = new CompletableFuture<>();
|
||||||
|
World world = requireWorld();
|
||||||
|
Bukkit.getRegionScheduler().execute(this, world, chunkX, 0, () -> {
|
||||||
|
try {
|
||||||
|
Chunk chunk = world.getChunkAt(chunkX, 0);
|
||||||
|
for (Entity entity : chunk.getEntities()) {
|
||||||
|
if (entity instanceof LivingEntity && !(entity instanceof Player)) {
|
||||||
|
entity.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int x = 0; x < 16; x++) {
|
||||||
|
for (int z = 0; z < 16; z++) {
|
||||||
|
chunk.getBlock(x, 64, z).setType(Material.GRASS_BLOCK, false);
|
||||||
|
for (int y = 65; y <= 68; y++) {
|
||||||
|
chunk.getBlock(x, y, z).setType(Material.AIR, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RuntimeSpawn spawn = runtimeSpawn(chunk, "seeded");
|
||||||
|
IrisSpawner definition = spawn.environment().data().getSpawnerLoader().load("qa/seeded");
|
||||||
|
require(!definition.canSpawn(spawn.engine(), chunkX, 0), "Ongoing weather/time gate unexpectedly allowed spawning");
|
||||||
|
for (int i = 0; i < 100 + chunkX; i++) {
|
||||||
|
RNG.r.nextLong();
|
||||||
|
}
|
||||||
|
ambientInvocation.set(true);
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
LivingEntity witness = (LivingEntity) world.spawnEntity(new Location(world, (chunkX << 4) + 8.5, 65, 8.5), EntityType.ZOMBIE);
|
||||||
|
witness.setCustomName("Population witness");
|
||||||
|
witness.setAI(false);
|
||||||
|
witness.setGravity(false);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
ambientInvocation.remove();
|
||||||
|
}
|
||||||
|
if (marker) {
|
||||||
|
invokeInitialMarker(spawn, definition, new IrisPosition((chunkX << 4) + 5, 64, 5));
|
||||||
|
} else {
|
||||||
|
invokeAmbient(spawn, chunk, true);
|
||||||
|
}
|
||||||
|
List<String> snapshot = new ArrayList<>();
|
||||||
|
for (Entity entity : chunk.getEntities()) {
|
||||||
|
String name = entity.getCustomName();
|
||||||
|
if (!(entity instanceof LivingEntity living) || name == null || !name.startsWith("Seeded ")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ItemStack hand = living.getEquipment().getItemInMainHand();
|
||||||
|
ItemStack helmet = living.getEquipment().getHelmet();
|
||||||
|
require(hand.hasItemMeta() && hand.getItemMeta().hasAttributeModifiers(), "Spawned gear has no authored attributes");
|
||||||
|
require(helmet != null && helmet.getType() == Material.IRON_HELMET, "Spawned helmet was not authored equipment");
|
||||||
|
Location at = entity.getLocation();
|
||||||
|
snapshot.add(entity.getType() + ":" + at.getX() + ":" + at.getY() + ":" + at.getZ()
|
||||||
|
+ ":" + HexFormat.of().formatHex(hand.serializeAsBytes())
|
||||||
|
+ ":" + HexFormat.of().formatHex(helmet.serializeAsBytes()));
|
||||||
|
}
|
||||||
|
require(snapshot.size() >= 2 && snapshot.size() <= 4, "Initial population was blocked by runtime gates or cap: " + snapshot.size());
|
||||||
|
Collections.sort(snapshot);
|
||||||
|
result.complete(List.copyOf(snapshot));
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
result.completeExceptionally(failure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void invokeInitialMarker(RuntimeSpawn spawn, IrisSpawner definition, IrisPosition position) {
|
||||||
|
ambientInvocation.set(true);
|
||||||
|
try (BiomeEnvironment.Scope ignored = spawn.engine().openBiomeEnvironmentScope(spawn.environment())) {
|
||||||
|
Method method = spawn.spawner().getClass().getDeclaredMethod("spawnMarker", IrisPosition.class, IrisSpawner.class, IrisMarker.class, RNG.class, boolean.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
method.invoke(spawn.spawner(), position, definition, null, EntitySpawnSeed.marker(spawn.engine().getSeedManager().getEntity(), position.getX(), position.getY(), position.getZ()), true);
|
||||||
|
} catch (InvocationTargetException failure) {
|
||||||
|
throw new IllegalStateException("Initial marker spawning failed", failure.getCause());
|
||||||
|
} catch (ReflectiveOperationException failure) {
|
||||||
|
throw new IllegalStateException("Could not invoke initial marker spawning", failure);
|
||||||
|
} finally {
|
||||||
|
ambientInvocation.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void spawnFrogs(CommandSender sender) {
|
private void spawnFrogs(CommandSender sender) {
|
||||||
Chunk chunk = requireWorld().getChunkAt(0, 0);
|
Chunk chunk = requireWorld().getChunkAt(0, 0);
|
||||||
RuntimeSpawn spawn = runtimeSpawn(chunk, "frogs");
|
RuntimeSpawn spawn = runtimeSpawn(chunk, "frogs");
|
||||||
getLogger().info(spawnDiagnostics(spawn, chunk));
|
getLogger().info(spawnDiagnostics(spawn, chunk));
|
||||||
for (int attempt = 0; attempt < 12; attempt++) {
|
for (int attempt = 0; attempt < 12; attempt++) {
|
||||||
invokeAmbient(spawn, chunk);
|
invokeAmbient(spawn, chunk, false);
|
||||||
require(count(chunk, EntityType.FROG) <= FROG_CAPACITY, "Frog batch exceeded its category capacity");
|
require(count(chunk, EntityType.FROG) <= FROG_CAPACITY, "Frog batch exceeded its category capacity");
|
||||||
}
|
}
|
||||||
require(count(chunk, EntityType.FROG) == FROG_CAPACITY,
|
require(count(chunk, EntityType.FROG) == FROG_CAPACITY,
|
||||||
@@ -266,7 +492,7 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
|
|||||||
require(count(chunk, EntityType.ZOMBIE) == 0, "Cave phase already ran");
|
require(count(chunk, EntityType.ZOMBIE) == 0, "Cave phase already ran");
|
||||||
RuntimeSpawn spawn = runtimeSpawn(chunk, "cave-zombies");
|
RuntimeSpawn spawn = runtimeSpawn(chunk, "cave-zombies");
|
||||||
getLogger().info(spawnDiagnostics(spawn, chunk));
|
getLogger().info(spawnDiagnostics(spawn, chunk));
|
||||||
invokeAmbient(spawn, chunk);
|
invokeAmbient(spawn, chunk, false);
|
||||||
firstZombieBatch = count(chunk, EntityType.ZOMBIE);
|
firstZombieBatch = count(chunk, EntityType.ZOMBIE);
|
||||||
require(firstZombieBatch == ZOMBIE_CAPACITY,
|
require(firstZombieBatch == ZOMBIE_CAPACITY,
|
||||||
"Authored seven-zombie batch was not clamped to capacity two: " + spawnDiagnostics(spawn, chunk));
|
"Authored seven-zombie batch was not clamped to capacity two: " + spawnDiagnostics(spawn, chunk));
|
||||||
@@ -280,7 +506,7 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
|
|||||||
Chunk chunk = requireWorld().getChunkAt(0, 0);
|
Chunk chunk = requireWorld().getChunkAt(0, 0);
|
||||||
RuntimeSpawn spawn = runtimeSpawn(chunk, "cave-zombies");
|
RuntimeSpawn spawn = runtimeSpawn(chunk, "cave-zombies");
|
||||||
for (int attempt = 0; attempt < 50; attempt++) {
|
for (int attempt = 0; attempt < 50; attempt++) {
|
||||||
invokeAmbient(spawn, chunk);
|
invokeAmbient(spawn, chunk, false);
|
||||||
require(count(chunk, EntityType.ZOMBIE) == ZOMBIE_CAPACITY, "Saturated zombie category exceeded capacity");
|
require(count(chunk, EntityType.ZOMBIE) == ZOMBIE_CAPACITY, "Saturated zombie category exceeded capacity");
|
||||||
}
|
}
|
||||||
evidence(sender, "SPAWN_QA_STRESS attempts=50 zombies=" + count(chunk, EntityType.ZOMBIE));
|
evidence(sender, "SPAWN_QA_STRESS attempts=50 zombies=" + count(chunk, EntityType.ZOMBIE));
|
||||||
@@ -371,10 +597,10 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
|
|||||||
return diagnostics.toString();
|
return diagnostics.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void invokeAmbient(RuntimeSpawn spawn, Chunk chunk) {
|
private void invokeAmbient(RuntimeSpawn spawn, Chunk chunk, boolean initial) {
|
||||||
ambientInvocation.set(true);
|
ambientInvocation.set(true);
|
||||||
try (BiomeEnvironment.Scope ignored = spawn.engine().openBiomeEnvironmentScope(spawn.environment())) {
|
try (BiomeEnvironment.Scope ignored = spawn.engine().openBiomeEnvironmentScope(spawn.environment())) {
|
||||||
spawn.method().invoke(spawn.spawner(), chunk, false, spawn.environment());
|
spawn.method().invoke(spawn.spawner(), chunk, initial, spawn.environment());
|
||||||
} catch (InvocationTargetException failure) {
|
} catch (InvocationTargetException failure) {
|
||||||
throw new IllegalStateException("Iris ambient spawning failed", failure.getCause());
|
throw new IllegalStateException("Iris ambient spawning failed", failure.getCause());
|
||||||
} catch (ReflectiveOperationException failure) {
|
} catch (ReflectiveOperationException failure) {
|
||||||
@@ -467,6 +693,9 @@ public final class NaturalSpawningFixture extends JavaPlugin implements Listener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record CompletionRun(Engine engine, Object maintenance, Method run, Field pending, Runnable callback, AtomicInteger callbacks) {
|
||||||
|
}
|
||||||
|
|
||||||
private record RuntimeSpawn(Engine engine, Object spawner, Method method, BiomeEnvironment environment) {
|
private record RuntimeSpawn(Engine engine, Object spawner, Method method, BiomeEnvironment environment) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "Seeded population",
|
||||||
|
"derivative": "minecraft:plains",
|
||||||
|
"vanillaDerivative": "minecraft:plains",
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"minHeight": 1,
|
||||||
|
"maxHeight": 1,
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:grass_block"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"generators": [
|
||||||
|
{
|
||||||
|
"generator": "flat",
|
||||||
|
"min": 64,
|
||||||
|
"max": 64
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"entitySpawners": [
|
||||||
|
"qa/seeded"
|
||||||
|
]
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"type": "minecraft:husk",
|
||||||
|
"reason": "NATURAL",
|
||||||
|
"surface": "LAND",
|
||||||
|
"ai": false,
|
||||||
|
"aware": false,
|
||||||
|
"gravity": false,
|
||||||
|
"customName": "Seeded husk",
|
||||||
|
"invulnerable": true,
|
||||||
|
"helmet": {
|
||||||
|
"type": "IRON_HELMET",
|
||||||
|
"minDurability": 0.1,
|
||||||
|
"maxDurability": 0.9
|
||||||
|
},
|
||||||
|
"mainHand": {
|
||||||
|
"type": "IRON_SWORD",
|
||||||
|
"minDurability": 0.1,
|
||||||
|
"maxDurability": 0.9,
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"name": "seeded_damage",
|
||||||
|
"attribute": "minecraft:attack_damage",
|
||||||
|
"minAmount": 1.25,
|
||||||
|
"maxAmount": 6.75,
|
||||||
|
"chance": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"type": "minecraft:zombie",
|
||||||
|
"reason": "NATURAL",
|
||||||
|
"surface": "LAND",
|
||||||
|
"ai": false,
|
||||||
|
"aware": false,
|
||||||
|
"gravity": false,
|
||||||
|
"customName": "Seeded zombie",
|
||||||
|
"invulnerable": true,
|
||||||
|
"helmet": {
|
||||||
|
"type": "IRON_HELMET",
|
||||||
|
"minDurability": 0.1,
|
||||||
|
"maxDurability": 0.9
|
||||||
|
},
|
||||||
|
"mainHand": {
|
||||||
|
"type": "IRON_SWORD",
|
||||||
|
"minDurability": 0.1,
|
||||||
|
"maxDurability": 0.9,
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"name": "seeded_damage",
|
||||||
|
"attribute": "minecraft:attack_damage",
|
||||||
|
"minAmount": 1.25,
|
||||||
|
"maxAmount": 6.75,
|
||||||
|
"chance": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"group": "NORMAL",
|
||||||
|
"maxEntitiesPerChunk": 4,
|
||||||
|
"allowedLightLevels": {
|
||||||
|
"min": 0,
|
||||||
|
"max": 0
|
||||||
|
},
|
||||||
|
"weather": "DOWNFALL",
|
||||||
|
"timeBlock": {
|
||||||
|
"startHour": 20,
|
||||||
|
"endHour": 3
|
||||||
|
},
|
||||||
|
"maximumRate": {
|
||||||
|
"amount": 1,
|
||||||
|
"per": {
|
||||||
|
"days": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"initialSpawns": [
|
||||||
|
{
|
||||||
|
"entity": "qa/seeded-zombie",
|
||||||
|
"rarity": 1,
|
||||||
|
"minSpawns": 2,
|
||||||
|
"maxSpawns": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"entity": "qa/seeded-husk",
|
||||||
|
"rarity": 2,
|
||||||
|
"minSpawns": 2,
|
||||||
|
"maxSpawns": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -8,7 +8,8 @@ export default {
|
|||||||
context.expect(response.includes(expected) && !response.includes('SPAWN_QA_FAILED'), response)
|
context.expect(response.includes(expected) && !response.includes('SPAWN_QA_FAILED'), response)
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
const entities = name => Object.values(context.bot.entities).filter(entity => entity.name === name)
|
const entities = name => Object.values(context.bot.entities).filter(entity => entity.name === name
|
||||||
|
&& entity.position.x >= 0 && entity.position.x < 16 && entity.position.z >= 0 && entity.position.z < 16)
|
||||||
await context.step('prepare the synthetic Iris world and dark cave', async () => {
|
await context.step('prepare the synthetic Iris world and dark cave', async () => {
|
||||||
await command('setup', 'SPAWN_QA_SETUP world=iris:spawn_qa seed=78264193', 180000)
|
await command('setup', 'SPAWN_QA_SETUP world=iris:spawn_qa seed=78264193', 180000)
|
||||||
await wait(() => Math.abs(context.bot.entity.position.y - 67) < 1, 'Fixture world teleport')
|
await wait(() => Math.abs(context.bot.entity.position.y - 67) < 1, 'Fixture world teleport')
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "Flat",
|
||||||
|
"derivative": "minecraft:plains",
|
||||||
|
"vanillaDerivative": "minecraft:plains",
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"minHeight": 1,
|
||||||
|
"maxHeight": 1,
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:lime_wool"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minHeight": 384,
|
||||||
|
"maxHeight": 384,
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:green_concrete"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"generators": [
|
||||||
|
{
|
||||||
|
"generator": "flat",
|
||||||
|
"min": 64,
|
||||||
|
"max": 96
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "Noise Sources",
|
||||||
|
"version": 1,
|
||||||
|
"mode": {
|
||||||
|
"type": "OVERWORLD"
|
||||||
|
},
|
||||||
|
"regions": [
|
||||||
|
"flat"
|
||||||
|
],
|
||||||
|
"focusRegion": "flat",
|
||||||
|
"focus": "flat",
|
||||||
|
"landChance": 1,
|
||||||
|
"environment": "NORMAL",
|
||||||
|
"dimensionHeight": {
|
||||||
|
"min": -64,
|
||||||
|
"max": 320
|
||||||
|
},
|
||||||
|
"logicalHeight": 384,
|
||||||
|
"fluidHeight": 0,
|
||||||
|
"bedrock": false,
|
||||||
|
"rockPalette": {
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:red_concrete"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"expression": "0.5 + sin(x * 0.15) * 0.25 + cos(y * 0.13) * 0.25"
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"interpolator": {
|
||||||
|
"function": "NONE",
|
||||||
|
"horizontalScale": 1
|
||||||
|
},
|
||||||
|
"seed": 310,
|
||||||
|
"composite": [
|
||||||
|
{
|
||||||
|
"seed": 310,
|
||||||
|
"style": {
|
||||||
|
"expression": "terrain"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"seed": 311,
|
||||||
|
"style": {
|
||||||
|
"style": "CLOVER_HERMITE",
|
||||||
|
"zoom": 0.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "Flat",
|
||||||
|
"landBiomes": [
|
||||||
|
"flat"
|
||||||
|
],
|
||||||
|
"seaBiomes": [
|
||||||
|
"flat"
|
||||||
|
],
|
||||||
|
"shoreBiomes": [
|
||||||
|
"flat"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { readFile } from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'iris-noise',
|
||||||
|
description: 'Verify expression and interpolated noise produce varied, bounded terrain visible to a player.',
|
||||||
|
async run(context) {
|
||||||
|
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||||
|
context.expect(/^isolated=true\s*$/m.test(source), 'Noise generation requires an isolated instance')
|
||||||
|
await context.step('create a world with expression and Clover noise', async () => {
|
||||||
|
const response = await context.command('/iris create name=noise_probe type=noise-fixture seed=78264193',
|
||||||
|
/Successfully created your world|Failed to create/, 180000)
|
||||||
|
context.expect(response.includes('Successfully created your world'), response)
|
||||||
|
})
|
||||||
|
await context.step('client observes bounded terrain with varying heights', async () => {
|
||||||
|
let columns = 0
|
||||||
|
let minimum = Infinity
|
||||||
|
let maximum = -Infinity
|
||||||
|
await context.waitUntil(() => {
|
||||||
|
columns = 0
|
||||||
|
minimum = Infinity
|
||||||
|
maximum = -Infinity
|
||||||
|
const center = context.bot.entity.position.floored()
|
||||||
|
for (let x = -8; x <= 8; x++) {
|
||||||
|
for (let z = -8; z <= 8; z++) {
|
||||||
|
let surface = null
|
||||||
|
for (let y = 60; y <= 100; y++) {
|
||||||
|
const position = center.offset(x, y - center.y, z)
|
||||||
|
const block = context.bot.blockAt(position)
|
||||||
|
if (!block) return false
|
||||||
|
if (block.name === 'lime_wool') {
|
||||||
|
if (surface !== null || context.bot.blockAt(position.offset(0, 1, 0))?.name !== 'air') return false
|
||||||
|
surface = y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (surface === null || surface < 64 || surface > 96) return false
|
||||||
|
columns++
|
||||||
|
minimum = Math.min(minimum, surface)
|
||||||
|
maximum = Math.max(maximum, surface)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return columns === 289 && maximum - minimum >= 3
|
||||||
|
}, { label: '289 noise columns with bounded, varied surfaces', timeoutMs: 60000, intervalMs: 250 })
|
||||||
|
context.expect(columns === 289 && maximum - minimum >= 3, 'Client verified noise terrain', { columns, minimum, maximum })
|
||||||
|
context.report.noiseTerrain = { columns, minimum, maximum }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "Flat",
|
||||||
|
"derivative": "minecraft:plains",
|
||||||
|
"vanillaDerivative": "minecraft:plains",
|
||||||
|
"layers": [
|
||||||
|
{"minHeight": 1, "maxHeight": 1, "palette": [{"block": "minecraft:lime_wool"}]},
|
||||||
|
{"minHeight": 128, "maxHeight": 128, "palette": [{"block": "minecraft:stone"}]}
|
||||||
|
],
|
||||||
|
"generators": [{"generator": "flat", "min": 32, "max": 32}],
|
||||||
|
"objects": [{
|
||||||
|
"place": ["smolshroom1"],
|
||||||
|
"chance": 1,
|
||||||
|
"density": 8,
|
||||||
|
"mode": "ORGANIC_STILT",
|
||||||
|
"rotation": {"enabled": false},
|
||||||
|
"scale": {"size": 0.75, "interpolation": "NONE"},
|
||||||
|
"translate": {"y": 6},
|
||||||
|
"stiltSettings": {"organicMaxScan": 8, "organicJitter": 0, "organicScratch": 0}
|
||||||
|
}]
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "Object Scaling Fixture",
|
||||||
|
"version": 1,
|
||||||
|
"mode": {"type": "OVERWORLD"},
|
||||||
|
"regions": ["flat"],
|
||||||
|
"focusRegion": "flat",
|
||||||
|
"focus": "flat",
|
||||||
|
"landChance": 1,
|
||||||
|
"environment": "NORMAL",
|
||||||
|
"dimensionHeight": {"min": 0, "max": 128},
|
||||||
|
"logicalHeight": 128,
|
||||||
|
"fluidHeight": 0,
|
||||||
|
"bedrock": false,
|
||||||
|
"caveProfile": {"enabled": false}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"seed": 310,
|
||||||
|
"interpolator": {"function": "NONE", "horizontalScale": 1},
|
||||||
|
"composite": [{"seed": 310, "style": {"style": "FLAT"}}]
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "Flat",
|
||||||
|
"landBiomes": ["flat"],
|
||||||
|
"seaBiomes": ["flat"],
|
||||||
|
"shoreBiomes": ["flat"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { createHash } from 'node:crypto'
|
||||||
|
import { readFile, realpath } from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'iris-object-scaling',
|
||||||
|
description: 'Verify fractional mushroom scaling retains its cap and uses its stem for organic supports.',
|
||||||
|
async run(context) {
|
||||||
|
const directory = await realpath(context.server.directory)
|
||||||
|
const source = await readFile(path.join(directory, '.server-source'), 'utf8')
|
||||||
|
context.expect(/^isolated=true\s*$/m.test(source), 'Object scaling requires an isolated instance')
|
||||||
|
const pack = await realpath(path.join(directory, 'plugins', 'Iris', 'packs', 'object-scaling-fixture'))
|
||||||
|
context.expect(pack.startsWith(`${directory}${path.sep}`), 'Object scaling requires a local fixture pack')
|
||||||
|
const object = await readFile(path.join(pack, 'objects', 'smolshroom1.iob'))
|
||||||
|
const objectHash = createHash('sha256').update(object).digest('hex')
|
||||||
|
const biome = JSON.parse(await readFile(path.join(pack, 'biomes', 'flat.json'), 'utf8'))
|
||||||
|
const surfaceY = biome.generators[0].min
|
||||||
|
const originalStemY = surfaceY + biome.objects[0].translate.y
|
||||||
|
const expectedCapY = originalStemY + 1
|
||||||
|
const maximumExtension = biome.objects[0].stiltSettings.organicMaxScan
|
||||||
|
context.expect(surfaceY === 32 && expectedCapY === 39 && maximumExtension === 8,
|
||||||
|
'Expected the bounded organic support fixture', { surfaceY, expectedCapY, maximumExtension })
|
||||||
|
const world = context.options.command ?? 'object_scaling_probe'
|
||||||
|
context.expect(/^[a-z0-9_]+$/.test(world), 'Fixture world name must be a simple identifier')
|
||||||
|
await context.step('create the fractional mushroom fixture world', async () => {
|
||||||
|
const response = await context.command(`/iris create name=${world} type=object-scaling-fixture seed=1337`,
|
||||||
|
/Successfully created your world|World ready|Failed to create|Exception raised/i, 180000)
|
||||||
|
context.expect(/Successfully created your world|World ready/i.test(response), response)
|
||||||
|
await context.command(`/iris tp ${world}`)
|
||||||
|
await context.waitUntil(async () => {
|
||||||
|
const observation = await context.observe()
|
||||||
|
const player = observation.players.find(entry => entry.username === context.bot.username)
|
||||||
|
const current = observation.worlds.find(entry => entry.id === player?.world)
|
||||||
|
return current?.key === `iris:${world}` || current?.name.endsWith(world)
|
||||||
|
}, { label: 'Enter the scaling fixture world', timeoutMs: 60000, intervalMs: 100 })
|
||||||
|
await context.command('/minecraft:gamemode spectator @s')
|
||||||
|
await context.waitUntil(() => context.bot.game.gameMode === 'spectator',
|
||||||
|
{ label: 'Spectator mode', timeoutMs: 30000, intervalMs: 100 })
|
||||||
|
await context.command('/minecraft:tp @s 0.5 48 0.5', /Teleported/i, 30000)
|
||||||
|
})
|
||||||
|
const columns = []
|
||||||
|
await context.step('client sees one cap and continuous stem supports', async () => {
|
||||||
|
const position = context.bot.entity.position.clone()
|
||||||
|
await context.waitUntil(() => {
|
||||||
|
if (Math.abs(context.bot.entity.position.x - 0.5) > 1
|
||||||
|
|| Math.abs(context.bot.entity.position.z - 0.5) > 1) return false
|
||||||
|
for (const x of [-16, 0, 16, 31]) {
|
||||||
|
for (const z of [-16, 0, 16, 31]) {
|
||||||
|
for (const y of [0, 32, 63]) {
|
||||||
|
if (!context.bot.blockAt(position.set(x, y, z))) return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}, { label: 'Complete mushroom fixture chunks', timeoutMs: 60000, intervalMs: 100 })
|
||||||
|
for (let x = -16; x < 32; x++) {
|
||||||
|
for (let z = -16; z < 32; z++) {
|
||||||
|
const caps = []
|
||||||
|
const stems = []
|
||||||
|
for (let y = 1; y < 63; y++) {
|
||||||
|
const block = context.bot.blockAt(position.set(x, y, z))
|
||||||
|
context.expect(block !== null, 'Every sampled mushroom block must be loaded', { x, y, z })
|
||||||
|
if (block.name === 'red_mushroom_block') caps.push(y)
|
||||||
|
if (block.name === 'mushroom_stem') stems.push(y)
|
||||||
|
}
|
||||||
|
if (caps.length === 0) continue
|
||||||
|
context.expect(caps.length === 1, 'Mushroom cap was copied into its stem or support', { x, z, caps })
|
||||||
|
const capY = caps[0]
|
||||||
|
context.expect(capY === expectedCapY, 'Fractional scaling changed the expected mushroom height',
|
||||||
|
{ x, z, capY, expectedCapY })
|
||||||
|
const bottomY = stems[0]
|
||||||
|
const stemBlocks = stems.length
|
||||||
|
context.expect(bottomY >= originalStemY - maximumExtension && bottomY <= surfaceY + 1,
|
||||||
|
'Mushroom support must reach the surface within its configured extension limit',
|
||||||
|
{ x, z, capY, bottomY, surfaceY, maximumExtension, stems })
|
||||||
|
context.expect(stemBlocks === capY - bottomY
|
||||||
|
&& stems.every((y, index) => y === bottomY + index),
|
||||||
|
'Scaled mushroom must have a continuous stem from its cap to the ground', { x, z, capY, stems })
|
||||||
|
const supportY = bottomY - 1
|
||||||
|
const support = context.bot.blockAt(position.set(x, supportY, z))?.name
|
||||||
|
const above = context.bot.blockAt(position.set(x, capY + 1, z))?.name
|
||||||
|
context.expect(support === (supportY === surfaceY ? 'lime_wool' : 'stone'),
|
||||||
|
'Mushroom stem must terminate on the fixture terrain', { x, z, capY, supportY, support })
|
||||||
|
context.expect(above === 'air', 'Scaled mushroom has unexpected blocks above its cap', { x, z, capY, above })
|
||||||
|
columns.push({ x, z, surfaceY, capY, bottomY, stemBlocks,
|
||||||
|
buriedStemBlocks: Math.max(0, surfaceY - bottomY + 1) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.expect(columns.length >= 8, 'Expected at least eight scaled mushroom columns', { columns })
|
||||||
|
})
|
||||||
|
await context.step('server confirms the sampled stem and cap materials', async () => {
|
||||||
|
for (const [index, column] of columns.slice(0, 8).entries()) {
|
||||||
|
const { x, z, surfaceY, capY, bottomY } = column
|
||||||
|
const marker = `IRIS_SCALING_OK_${index}`
|
||||||
|
await context.command(`/minecraft:execute if block ${x} ${surfaceY + 1} ${z} minecraft:mushroom_stem`
|
||||||
|
+ ` if block ${x} ${bottomY} ${z} minecraft:mushroom_stem`
|
||||||
|
+ ` if block ${x} ${capY} ${z} minecraft:red_mushroom_block run minecraft:say ${marker}`,
|
||||||
|
new RegExp(marker), 10000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
context.report.objectScaling = { world, scale: 0.75, object: 'smolshroom1', objectHash,
|
||||||
|
surfaceY, maximumExtension, checkedColumns: columns.length,
|
||||||
|
serverConfirmedColumns: Math.min(8, columns.length), columns }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { createHash } from 'node:crypto'
|
||||||
|
import { readFile, writeFile } from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
const worlds = [
|
||||||
|
{ name: 'seed-selection-default', seed: '1337', argument: '' },
|
||||||
|
{ name: 'seed-selection-positive', seed: '78264193', argument: ' seed=78264193' },
|
||||||
|
{ name: 'seed-selection-negative', seed: '-734829104958217364', argument: ' seed=-734829104958217364' },
|
||||||
|
{ name: 'seed-selection-zero', seed: '0', argument: ' seed=0' }
|
||||||
|
]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'iris-seed-selection',
|
||||||
|
description: 'Verify omitted, positive, negative, and zero creation seeds against visible terrain and a cold restart.',
|
||||||
|
async run(context) {
|
||||||
|
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||||
|
context.expect(/^isolated=true\s*$/m.test(source), 'Seed selection requires an isolated instance')
|
||||||
|
const wait = (predicate, label) => context.waitUntil(predicate, { label, timeoutMs: 60000, intervalMs: 100 })
|
||||||
|
const restart = context.options.command === 'restart-check'
|
||||||
|
const stateFile = path.join(context.server.directory, '.seed-selection.json')
|
||||||
|
const saved = restart ? JSON.parse(await readFile(stateFile, 'utf8')) : undefined
|
||||||
|
const jarHash = createHash('sha256').update(await readFile(path.join(context.server.directory, 'plugins', 'Iris.jar'))).digest('hex')
|
||||||
|
const processId = (await context.observe()).processId
|
||||||
|
if (restart) {
|
||||||
|
context.expect(saved.processId !== processId, 'Restart-check requires a cold server restart')
|
||||||
|
context.expect(saved.jarHash === jarHash, 'Restart-check requires the same Iris jar')
|
||||||
|
}
|
||||||
|
const outputs = {}
|
||||||
|
for (const world of worlds) {
|
||||||
|
await context.step(`${restart ? 'Reload' : 'Create'} ${world.name} with seed ${world.seed}`, async () => {
|
||||||
|
if (!restart) {
|
||||||
|
await context.command(`/iris create name=${world.name} type=cohesion${world.argument}`,
|
||||||
|
/Successfully created your world/i, 180000)
|
||||||
|
}
|
||||||
|
await context.command(`/iris tp ${world.name}`)
|
||||||
|
await wait(async () => {
|
||||||
|
const observation = await context.observe()
|
||||||
|
const player = observation.players.find(entry => entry.username === context.bot.username)
|
||||||
|
const current = observation.worlds.find(entry => entry.id === player?.world)
|
||||||
|
return current?.key === `iris:${world.name}` || current?.name.endsWith(world.name)
|
||||||
|
}, `Enter ${world.name}`)
|
||||||
|
await context.command('/minecraft:gamemode spectator @s')
|
||||||
|
await wait(() => context.bot.game.gameMode === 'spectator', 'Spectator mode')
|
||||||
|
await context.command('/minecraft:seed', new RegExp(`Seed:\\s*\\[?${world.seed}\\]?(?:\\s|$)`))
|
||||||
|
const x = 1024
|
||||||
|
const z = -1024
|
||||||
|
await context.command(`/minecraft:tp @s ${x + 8.5} 48 ${z + 8.5}`, /Teleported/i, 30000)
|
||||||
|
await wait(() => Math.abs(context.bot.entity.position.x - x - 8.5) < 1
|
||||||
|
&& Math.abs(context.bot.entity.position.z - z - 8.5) < 1, 'Sample teleport')
|
||||||
|
const position = context.bot.entity.position.clone()
|
||||||
|
await wait(() => context.bot.blockAt(position.clone().set(x, 0, z))
|
||||||
|
&& context.bot.blockAt(position.clone().set(x + 15, 63, z + 15)), 'Complete sample chunk')
|
||||||
|
const digest = createHash('sha256')
|
||||||
|
let stone = 0
|
||||||
|
for (let dx = 0; dx < 16; dx++) {
|
||||||
|
for (let dz = 0; dz < 16; dz++) {
|
||||||
|
for (let y = 0; y < 64; y++) {
|
||||||
|
const block = context.bot.blockAt(position.set(x + dx, y, z + dz))
|
||||||
|
context.expect(block !== null, 'Missing sampled block', { x: x + dx, y, z: z + dz })
|
||||||
|
digest.update(`${block.stateId},`)
|
||||||
|
if (block.name === 'stone') stone++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.expect(stone > 1000, 'Fixture terrain was not generated', { world: world.name, stone })
|
||||||
|
const blocks = digest.digest('hex')
|
||||||
|
outputs[world.name] = { seed: world.seed, blocks }
|
||||||
|
if (restart) {
|
||||||
|
context.expect(saved.worlds[world.name].seed === world.seed, 'Seed expectation changed after restart')
|
||||||
|
context.expect(saved.worlds[world.name].blocks === blocks, 'Persisted terrain changed after restart',
|
||||||
|
{ world: world.name, before: saved.worlds[world.name].blocks, after: blocks })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const signatures = new Set(Object.values(outputs).map(output => output.blocks))
|
||||||
|
context.expect(signatures.size === worlds.length, 'Different requested seeds generated identical sample chunks', outputs)
|
||||||
|
context.report.seedSelection = { restart, jarHash, worlds: outputs }
|
||||||
|
if (!restart) {
|
||||||
|
await writeFile(stateFile, JSON.stringify({ processId, jarHash, worlds: outputs }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
export default {
|
||||||
|
name: 'iris-seeded-entities',
|
||||||
|
description: 'Replay initial chunk and marker populations in reversed order, comparing positions and complete equipment data.',
|
||||||
|
async run(context) {
|
||||||
|
const command = async (operation, expected, timeoutMs) => {
|
||||||
|
const response = await context.command(`/irisspawnqa ${operation}`, /SPAWN_QA_[A-Z]+(?:\s|$)/, timeoutMs)
|
||||||
|
context.expect(response.includes(expected) && !response.includes('SPAWN_QA_FAILED'), response)
|
||||||
|
}
|
||||||
|
await context.step('prepare a controlled Iris world', async () => {
|
||||||
|
await command('setup', 'SPAWN_QA_SETUP', 180000)
|
||||||
|
})
|
||||||
|
await context.step('replay initial mobs despite unrelated randomness and live spawn restrictions', async () => {
|
||||||
|
await command('seeded', 'SPAWN_QA_SEEDED chunks=2 markers=2 repeats=2 equipment=true attributes=true', 60000)
|
||||||
|
await context.waitUntil(() => Object.values(context.bot.entities).some(entity => entity.name === 'zombie' || entity.name === 'husk'), {
|
||||||
|
label: 'Generated mobs reached the client', timeoutMs: 10000, intervalMs: 100
|
||||||
|
})
|
||||||
|
context.report.seededEntities = { chunks: 2, markers: 2, repeats: 2, positions: true, equipmentBytes: true, attributeIds: true }
|
||||||
|
})
|
||||||
|
await context.step('retry a skipped initial pass and persist completion once', async () => {
|
||||||
|
await command('completion', 'SPAWN_QA_COMPLETION retry=true callbacks=1 persisted=true owned=true', 30000)
|
||||||
|
context.report.seededEntities.completionRetry = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "Flat",
|
||||||
|
"derivative": "minecraft:plains",
|
||||||
|
"vanillaDerivative": "minecraft:plains",
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"minHeight": 1,
|
||||||
|
"maxHeight": 1,
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:lime_wool"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minHeight": 384,
|
||||||
|
"maxHeight": 384,
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:stone"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"generators": [
|
||||||
|
{
|
||||||
|
"generator": "flat",
|
||||||
|
"min": 64,
|
||||||
|
"max": 64
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"objects": [
|
||||||
|
{
|
||||||
|
"place": [
|
||||||
|
"object"
|
||||||
|
],
|
||||||
|
"chance": 1,
|
||||||
|
"density": 4,
|
||||||
|
"mode": "CENTER_HEIGHT",
|
||||||
|
"rotation": {
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"name": "Structure Fixture",
|
||||||
|
"version": 1,
|
||||||
|
"mode": {
|
||||||
|
"type": "OVERWORLD"
|
||||||
|
},
|
||||||
|
"regions": [
|
||||||
|
"flat"
|
||||||
|
],
|
||||||
|
"focusRegion": "flat",
|
||||||
|
"focus": "flat",
|
||||||
|
"landChance": 1,
|
||||||
|
"environment": "NORMAL",
|
||||||
|
"dimensionHeight": {
|
||||||
|
"min": -64,
|
||||||
|
"max": 320
|
||||||
|
},
|
||||||
|
"logicalHeight": 384,
|
||||||
|
"fluidHeight": 0,
|
||||||
|
"bedrock": false,
|
||||||
|
"rockPalette": {
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:stone"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"structures": [
|
||||||
|
{
|
||||||
|
"structures": [
|
||||||
|
"surface"
|
||||||
|
],
|
||||||
|
"placementId": "surface",
|
||||||
|
"distribution": "DENSITY",
|
||||||
|
"density": 1,
|
||||||
|
"anchor": "SURFACE",
|
||||||
|
"terrain": {
|
||||||
|
"mode": "PRESERVE"
|
||||||
|
},
|
||||||
|
"stilt": {
|
||||||
|
"maxDepth": 16,
|
||||||
|
"palette": {
|
||||||
|
"palette": [
|
||||||
|
{
|
||||||
|
"block": "minecraft:diamond_block"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"structures": [
|
||||||
|
"buried"
|
||||||
|
],
|
||||||
|
"placementId": "buried",
|
||||||
|
"distribution": "DENSITY",
|
||||||
|
"density": 1,
|
||||||
|
"anchor": "HEIGHT_BAND",
|
||||||
|
"minHeight": 24,
|
||||||
|
"maxHeight": 24,
|
||||||
|
"terrain": {
|
||||||
|
"mode": "BORE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"interpolator": {
|
||||||
|
"function": "NONE",
|
||||||
|
"horizontalScale": 1
|
||||||
|
},
|
||||||
|
"seed": 310,
|
||||||
|
"composite": [
|
||||||
|
{
|
||||||
|
"seed": 310,
|
||||||
|
"style": {
|
||||||
|
"style": "FLAT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"object": "buried",
|
||||||
|
"connectors": [],
|
||||||
|
"rotatable": false,
|
||||||
|
"rules": {
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"object": "surface",
|
||||||
|
"connectors": [],
|
||||||
|
"rotatable": false,
|
||||||
|
"rules": {
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"pieces": [
|
||||||
|
{
|
||||||
|
"piece": "buried",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"pieces": [
|
||||||
|
{
|
||||||
|
"piece": "surface",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Loaded 100 of 1032 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user