mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 06:20:02 +00:00
Compare commits
159
Commits
No files matched your search
@@ -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
|
||||||
@@ -81,3 +81,5 @@ BUILDS/
|
|||||||
/docs/
|
/docs/
|
||||||
/.perf/
|
/.perf/
|
||||||
/.codegraph/
|
/.codegraph/
|
||||||
|
|
||||||
|
.superpowers/
|
||||||
@@ -248,34 +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: the Bukkit jar
|
This builds and verifies the Bukkit plugin, API, SPI and the Fabric, Forge and NeoForge jars
|
||||||
and a Fabric, Forge and NeoForge jar for every Minecraft version in `gradle/mod-targets.json`
|
for every Minecraft version declared in `gradle/mod-targets.json`. Artifacts stay in the
|
||||||
(1.21.1, 26.1.2, 26.2 and 26.3):
|
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`, and `build<Loader><mc>` for one mod jar, for example
|
Multiplexor reads `gradle/multiplexor.gradle` to discover targets and build each in a
|
||||||
`buildFabric26_2` or `buildNeoForge1_21_1`. `buildFabric`, `buildForge` and `buildNeoforge` export the
|
separate source sandbox. Use its project build and deployment commands to export jars
|
||||||
26.3 jars. `./gradlew checkAll` runs every check, then builds and verifies all twelve mod jars and runs their
|
or install them into managed test instances. Gradle builds do not copy artifacts to
|
||||||
unit tests; Forge 1.21.1 only compiles its tests because Forge 52 cannot bootstrap Minecraft outside
|
external destinations.
|
||||||
FML. The
|
|
||||||
SPI jar (the pure-JVM adapter/platform contract, not the stable plugin API) is built to `spi/build/libs/` by
|
|
||||||
`./gradlew :spi:jar`.
|
|
||||||
|
|
||||||
`./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 every verified platform jar 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) {
|
||||||
|
|||||||
+30
-12
@@ -2,8 +2,9 @@ package art.arcane.iris.probe;
|
|||||||
|
|
||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
|
import art.arcane.iris.generation.hydrology.engine.HydrologyPregeneration;
|
||||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
import art.arcane.iris.generation.hydrology.plan.WindowKey;
|
||||||
|
import art.arcane.iris.world.pregen.PregenHydrology;
|
||||||
import art.arcane.iris.world.pregen.PregenPerformanceProfile;
|
import art.arcane.iris.world.pregen.PregenPerformanceProfile;
|
||||||
|
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
@@ -31,11 +32,16 @@ import java.nio.file.Path;
|
|||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.nio.file.StandardOpenOption;
|
import java.nio.file.StandardOpenOption;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.HexFormat;
|
import java.util.HexFormat;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
public final class OfflineRegionGenerator {
|
public final class OfflineRegionGenerator {
|
||||||
|
private static final int CHUNKS_PER_HYDROLOGY_REGION = WindowKey.SIZE >> 4;
|
||||||
|
|
||||||
private OfflineRegionGenerator() {
|
private OfflineRegionGenerator() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,14 +103,13 @@ public final class OfflineRegionGenerator {
|
|||||||
PregenPerformanceProfile.apply(session.engine());
|
PregenPerformanceProfile.apply(session.engine());
|
||||||
System.out.println("generationProfile=pregen noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize()
|
System.out.println("generationProfile=pregen noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize()
|
||||||
+ " burstParallelism=" + MultiBurst.burst.parallelism() + " multicore=true");
|
+ " burstParallelism=" + MultiBurst.burst.parallelism() + " multicore=true");
|
||||||
IrisHydrologyRuntime hydrology = session.engine().getComplex().getHydrologyRuntime();
|
try (HydrologyPregeneration pregeneration = PregenHydrology.open(session.engine());
|
||||||
try (HydrologyTileCache.PregenerationScope hydrologyScope = hydrology == null ? null
|
|
||||||
: hydrology.preparePregeneration(configuration.hydrologyArea());
|
|
||||||
HeadlessTerrainContext context = HeadlessTerrainContext.create(new HeadlessTerrainContext.Options(
|
HeadlessTerrainContext context = HeadlessTerrainContext.create(new HeadlessTerrainContext.Options(
|
||||||
session.engine(), runtime, staged.resolve("templates"), configuration.levelKey(),
|
session.engine(), runtime, staged.resolve("templates"), configuration.levelKey(),
|
||||||
configuration.noiseSettings(), spigot));
|
configuration.noiseSettings(), spigot));
|
||||||
RegionGenerationWindow workers = new RegionGenerationWindow(configuration.parallelism(),
|
RegionGenerationWindow workers = new RegionGenerationWindow(configuration.parallelism(),
|
||||||
RegionGenerationWindow.Policy.STANDALONE)) {
|
RegionGenerationWindow.Policy.STANDALONE)) {
|
||||||
|
PregenHydrology.prepareAll(pregeneration, configuration.hydrologyRegions());
|
||||||
HeadlessRegionTerrain.Session rolling = new HeadlessRegionTerrain.Session(workers);
|
HeadlessRegionTerrain.Session rolling = new HeadlessRegionTerrain.Session(workers);
|
||||||
Path regionDirectory = session.engine().getTarget().getWorld().worldFolder().toPath().resolve("region");
|
Path regionDirectory = session.engine().getTarget().getWorld().worldFolder().toPath().resolve("region");
|
||||||
Files.createDirectory(regionDirectory);
|
Files.createDirectory(regionDirectory);
|
||||||
@@ -239,14 +244,27 @@ public final class OfflineRegionGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HydrologyTileCache.PregenerationArea hydrologyArea() {
|
List<long[]> hydrologyRegions() {
|
||||||
int halo = NativeRegionPlan.terrainHalo();
|
int halo = NativeRegionPlan.terrainHalo();
|
||||||
long minimumX = ((long) chunkX - halo) * 16L;
|
Set<Long> seen = new HashSet<>();
|
||||||
long minimumZ = ((long) chunkZ - halo) * 16L;
|
List<long[]> regions = new ArrayList<>();
|
||||||
long maximumX = ((long) chunkX + (long) width * regionsX + halo) * 16L - 1L;
|
for (int row = 0; row < regionsZ; row++) {
|
||||||
long maximumZ = ((long) chunkZ + (long) width * regionsZ + halo) * 16L - 1L;
|
for (int column = 0; column < regionsX; column++) {
|
||||||
return new HydrologyTileCache.PregenerationArea(Math.toIntExact((minimumX + maximumX) / 2L),
|
int regionX = row % 2 == 0 ? column : regionsX - column - 1;
|
||||||
Math.toIntExact((minimumZ + maximumZ) / 2L), minimumX, minimumZ, maximumX, maximumZ);
|
int minimumX = Math.floorDiv(chunkX + regionX * width - halo, CHUNKS_PER_HYDROLOGY_REGION);
|
||||||
|
int maximumX = Math.floorDiv(chunkX + (regionX + 1) * width - 1 + halo, CHUNKS_PER_HYDROLOGY_REGION);
|
||||||
|
int minimumZ = Math.floorDiv(chunkZ + row * width - halo, CHUNKS_PER_HYDROLOGY_REGION);
|
||||||
|
int maximumZ = Math.floorDiv(chunkZ + (row + 1) * width - 1 + halo, CHUNKS_PER_HYDROLOGY_REGION);
|
||||||
|
for (int hydrologyZ = minimumZ; hydrologyZ <= maximumZ; hydrologyZ++) {
|
||||||
|
for (int hydrologyX = minimumX; hydrologyX <= maximumX; hydrologyX++) {
|
||||||
|
if (seen.add(((long) hydrologyX << 32) | (hydrologyZ & 0xFFFFFFFFL))) {
|
||||||
|
regions.add(new long[]{hydrologyX, hydrologyZ});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return regions;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Configuration parse(String[] arguments) throws IOException {
|
static Configuration parse(String[] arguments) throws IOException {
|
||||||
|
|||||||
+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_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() {
|
||||||
|
|||||||
+1
-1
@@ -137,7 +137,7 @@ public class HeadlessChunkGeneratorTest {
|
|||||||
}
|
}
|
||||||
Files.writeString(pack.resolve("dimensions/main.json"), """
|
Files.writeString(pack.resolve("dimensions/main.json"), """
|
||||||
{"name":"Native columns","focus":"swamp","regions":["owned"],"fluidHeight":40,"logicalHeight":256,
|
{"name":"Native columns","focus":"swamp","regions":["owned"],"fluidHeight":40,"logicalHeight":256,
|
||||||
"landChance":1,"carvingEnabled":false,"hydrology":{"rivers":{"enabled":false}}}
|
"landChance":1,"carvingEnabled":false}
|
||||||
""");
|
""");
|
||||||
Files.writeString(pack.resolve("regions/owned.json"), "{\"landBiomes\":[\"swamp\"]}");
|
Files.writeString(pack.resolve("regions/owned.json"), "{\"landBiomes\":[\"swamp\"]}");
|
||||||
Files.writeString(pack.resolve("generators/flat.json"), "{\"composite\":[]}");
|
Files.writeString(pack.resolve("generators/flat.json"), "{\"composite\":[]}");
|
||||||
|
|||||||
+5
-7
@@ -1,7 +1,6 @@
|
|||||||
package art.arcane.iris.probe;
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
import art.arcane.iris.structure.nativegen.NativeStructureVolumeIndex;
|
import art.arcane.iris.structure.nativegen.NativeStructureVolumeIndex;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
|
|
||||||
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import net.minecraft.nbt.CompoundTag;
|
import net.minecraft.nbt.CompoundTag;
|
||||||
@@ -68,11 +67,10 @@ public final class HeadlessRegionTerrainTest {
|
|||||||
OfflineRegionGenerator.Configuration export = new OfflineRegionGenerator.Configuration(
|
OfflineRegionGenerator.Configuration export = new OfflineRegionGenerator.Configuration(
|
||||||
workspace.pack(), "main", 69420L, -3, 2, 1, 2, 3, 4, configuration,
|
workspace.pack(), "main", 69420L, -3, 2, 1, 2, 3, 4, configuration,
|
||||||
"probe", Level.OVERWORLD, NoiseGeneratorSettings.OVERWORLD, root.resolve("export"));
|
"probe", Level.OVERWORLD, NoiseGeneratorSettings.OVERWORLD, root.resolve("export"));
|
||||||
HydrologyTileCache.PregenerationArea bounds = export.hydrologyArea();
|
List<long[]> regions = export.hydrologyRegions();
|
||||||
assertEquals(-80L, bounds.minimumBlockX());
|
assertEquals(2, regions.size());
|
||||||
assertEquals(0L, bounds.minimumBlockZ());
|
assertArrayEquals(new long[]{-1L, 0L}, regions.get(0));
|
||||||
assertEquals(15L, bounds.maximumBlockX());
|
assertArrayEquals(new long[]{0L, 0L}, regions.get(1));
|
||||||
assertEquals(111L, bounds.maximumBlockZ());
|
|
||||||
assertEquals(1, result.targetChunks());
|
assertEquals(1, result.targetChunks());
|
||||||
assertEquals(529, result.chunks().size());
|
assertEquals(529, result.chunks().size());
|
||||||
long terrain = result.chunks().stream().filter(chunk -> chunk.getPersistedStatus() == ChunkStatus.TERRAIN).count();
|
long terrain = result.chunks().stream().filter(chunk -> chunk.getPersistedStatus() == ChunkStatus.TERRAIN).count();
|
||||||
@@ -189,7 +187,7 @@ public final class HeadlessRegionTerrainTest {
|
|||||||
}
|
}
|
||||||
Files.writeString(pack.resolve("dimensions/main.json"), """
|
Files.writeString(pack.resolve("dimensions/main.json"), """
|
||||||
{"name":"Native region","focus":"swamp","regions":["owned"],"fluidHeight":40,"logicalHeight":256,
|
{"name":"Native region","focus":"swamp","regions":["owned"],"fluidHeight":40,"logicalHeight":256,
|
||||||
"landChance":1,"carvingEnabled":false,"hydrology":{"rivers":{"enabled":false}}}
|
"landChance":1,"carvingEnabled":false}
|
||||||
""");
|
""");
|
||||||
Files.writeString(pack.resolve("regions/owned.json"), "{\"landBiomes\":[\"swamp\"]}");
|
Files.writeString(pack.resolve("regions/owned.json"), "{\"landBiomes\":[\"swamp\"]}");
|
||||||
Files.writeString(pack.resolve("generators/flat.json"), "{\"composite\":[]}");
|
Files.writeString(pack.resolve("generators/flat.json"), "{\"composite\":[]}");
|
||||||
|
|||||||
+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) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ dependencies {
|
|||||||
// log4j-core Filter interface, so its constructor reference needs the type resolvable.
|
// log4j-core Filter interface, so its constructor reference needs the type resolvable.
|
||||||
compileOnly(libs.log4j.core)
|
compileOnly(libs.log4j.core)
|
||||||
testImplementation('junit:junit:4.13.2')
|
testImplementation('junit:junit:4.13.2')
|
||||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
testImplementation(libs.mockito.core)
|
||||||
testImplementation(libs.paper.api)
|
testImplementation(libs.paper.api)
|
||||||
testImplementation(libs.bstats)
|
testImplementation(libs.bstats)
|
||||||
testImplementation(project(':core'))
|
testImplementation(project(':core'))
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import art.arcane.iris.integration.MultiverseCoreLink;
|
|||||||
import art.arcane.iris.platform.bukkit.nms.INMS;
|
import art.arcane.iris.platform.bukkit.nms.INMS;
|
||||||
import art.arcane.volmlib.nativelib.terrain.ServerShutdownBoundary;
|
import art.arcane.volmlib.nativelib.terrain.ServerShutdownBoundary;
|
||||||
import art.arcane.iris.studio.view.BukkitGuiHost;
|
import art.arcane.iris.studio.view.BukkitGuiHost;
|
||||||
|
import art.arcane.iris.studio.view.GuiHost;
|
||||||
import art.arcane.iris.studio.view.PregeneratorJob;
|
import art.arcane.iris.studio.view.PregeneratorJob;
|
||||||
import art.arcane.iris.platform.bukkit.BoardSVC;
|
import art.arcane.iris.platform.bukkit.BoardSVC;
|
||||||
import art.arcane.iris.command.CommandSVC;
|
import art.arcane.iris.command.CommandSVC;
|
||||||
@@ -988,6 +989,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
|
pendingWorldReplacements.awaitStagingForShutdown();
|
||||||
|
GuiHost.shutdown();
|
||||||
preservePendingBlockDrops();
|
preservePendingBlockDrops();
|
||||||
papiLifecycleActive = false;
|
papiLifecycleActive = false;
|
||||||
if (updateNotifier != null) {
|
if (updateNotifier != null) {
|
||||||
@@ -1004,7 +1007,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
IrisLanguage.shutdown();
|
IrisLanguage.shutdown();
|
||||||
teardownPapi();
|
teardownPapi();
|
||||||
boolean deferToServerStop = FailedEnableLock.defersTeardownToServerStop(enableFailed, IrisToolbelt.isServerStopping());
|
boolean deferToServerStop = FailedEnableLock.defersTeardownToServerStop(enableFailed, IrisToolbelt.isServerStopping())
|
||||||
|
&& INMS.get().deferPluginClassLoaderClose();
|
||||||
boolean restartingAtStartupBoundary = startupBoundaryRestart.get();
|
boolean restartingAtStartupBoundary = startupBoundaryRestart.get();
|
||||||
if (restartingAtStartupBoundary) {
|
if (restartingAtStartupBoundary) {
|
||||||
teardownRuntime("startup-boundary-restart", 30L);
|
teardownRuntime("startup-boundary-restart", 30L);
|
||||||
@@ -1036,7 +1040,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
preservePendingBlockDrops();
|
preservePendingBlockDrops();
|
||||||
papiLifecycleActive = false;
|
papiLifecycleActive = false;
|
||||||
teardownPapi();
|
teardownPapi();
|
||||||
if (IrisToolbelt.isServerStopping()) {
|
if (IrisToolbelt.isServerStopping() && INMS.get().deferPluginClassLoaderClose()) {
|
||||||
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.");
|
||||||
@@ -1128,7 +1132,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
|
|
||||||
private void quiesceRuntimeForServerShutdown(String reason) {
|
private void quiesceRuntimeForServerShutdown(String reason) {
|
||||||
serverStopTeardownDeferred.set(true);
|
serverStopTeardownDeferred.set(true);
|
||||||
INMS.get().deferPluginClassLoaderClose();
|
|
||||||
JigsawStudioService jigsawStudioService = IrisServices.getOrNull(JigsawStudioService.class);
|
JigsawStudioService jigsawStudioService = IrisServices.getOrNull(JigsawStudioService.class);
|
||||||
if (jigsawStudioService != null) {
|
if (jigsawStudioService != null) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ 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;
|
||||||
import art.arcane.iris.world.history.GenerationSemanticQueries;
|
import art.arcane.iris.world.history.GenerationSemanticQueries;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery;
|
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
|
||||||
import art.arcane.iris.generation.biome.IrisBiome;
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
||||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||||
@@ -196,9 +195,9 @@ public class CommandFind implements DirectorExecutor {
|
|||||||
EngineBukkitOps.gotoPOI(e, type, player(), teleport);
|
EngineBukkitOps.gotoPOI(e, type, player(), teleport);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Director(description = "Find an accepted hydrology feature", sync = true)
|
@Director(description = "Find a hydrology water feature", sync = true)
|
||||||
public void river(
|
public void river(
|
||||||
@Param(description = "Feature type: surface, waterfall, underground, grotto, mouth, or a deep-fluid id", customHandler = HydrologyTypeHandler.class)
|
@Param(description = "Feature type: stream, river, major_river, cascade, waterfall, mouth, pond, lake, dam, sea_inlet or cave_pool", customHandler = HydrologyTypeHandler.class)
|
||||||
String type,
|
String type,
|
||||||
@Param(description = "Should you be teleported", defaultValue = "true")
|
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||||
boolean teleport
|
boolean teleport
|
||||||
@@ -212,42 +211,32 @@ public class CommandFind implements DirectorExecutor {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
IrisHydrologyRuntime runtime = activeEngine.getComplex().getHydrologyRuntime();
|
HydrologyFeatureType featureType;
|
||||||
HydrologyFeatureQuery query;
|
|
||||||
try {
|
try {
|
||||||
query = HydrologyFeatureQuery.parse(type);
|
featureType = HydrologyFeatureType.parse(type);
|
||||||
} catch (IllegalArgumentException error) {
|
} catch (IllegalArgumentException error) {
|
||||||
commandSender.sendMessage(C.RED + error.getMessage());
|
commandSender.sendMessage(C.RED + error.getMessage());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
String label = featureType.key().replace('_', ' ');
|
||||||
Location origin = target.getLocation();
|
Location origin = target.getLocation();
|
||||||
World originWorld = origin.getWorld();
|
World originWorld = origin.getWorld();
|
||||||
int requestedDistance = runtime == null
|
int maximumDistance = GenerationSemanticQueries.RIVER_SEARCH_DISTANCE;
|
||||||
? 8192
|
commandSender.sendMessage(C.GRAY + GenerationSemanticQueries.riverSearchMessage(featureType, maximumDistance));
|
||||||
: Math.min(8192, runtime.settings().routing().tileSize() * 15);
|
|
||||||
int maximumDistance = runtime == null
|
|
||||||
? requestedDistance
|
|
||||||
: runtime.maximumFeatureSearchDistance(
|
|
||||||
origin.getBlockX(), origin.getBlockZ(), requestedDistance);
|
|
||||||
commandSender.sendMessage(C.GRAY + "Searching accepted hydrology plans for " + type + "...");
|
|
||||||
J.a(() -> {
|
J.a(() -> {
|
||||||
try {
|
try {
|
||||||
GenerationSemanticQueries.RiverResult feature = GenerationSemanticQueries.nearestRiver(
|
GenerationSemanticQueries.RiverResult feature = GenerationSemanticQueries.nearestRiver(
|
||||||
activeEngine,
|
activeEngine,
|
||||||
query,
|
featureType,
|
||||||
origin.getBlockX(),
|
origin.getBlockX(),
|
||||||
origin.getBlockZ(),
|
origin.getBlockZ(),
|
||||||
maximumDistance,
|
maximumDistance
|
||||||
(int visited) -> sendStructureMessage(target, commandSender,
|
|
||||||
C.GRAY + "Searched " + visited + " hydrology tiles for " + type + "...")
|
|
||||||
).orElse(null);
|
).orElse(null);
|
||||||
if (feature == null) {
|
if (feature == null) {
|
||||||
sendStructureMessage(target, commandSender,
|
sendStructureMessage(target, commandSender,
|
||||||
C.YELLOW + "No accepted " + type + " hydrology feature found within "
|
C.YELLOW + GenerationSemanticQueries.riverNotFoundMessage(featureType, maximumDistance));
|
||||||
+ maximumDistance + " blocks.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String label = type + " hydrology feature";
|
|
||||||
if (!teleport) {
|
if (!teleport) {
|
||||||
sendStructureMessage(target, commandSender,
|
sendStructureMessage(target, commandSender,
|
||||||
C.GREEN + "Found " + label + " at " + feature.x() + ", " + feature.y() + ", " + feature.z() + ".");
|
C.GREEN + "Found " + label + " at " + feature.x() + ", " + feature.y() + ", " + feature.z() + ".");
|
||||||
@@ -259,8 +248,9 @@ public class CommandFind implements DirectorExecutor {
|
|||||||
target, originWorld, commandSender, label, destination, true);
|
target, originWorld, commandSender, label, destination, true);
|
||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
sendStructureMessage(target, commandSender,
|
sendStructureMessage(target, commandSender,
|
||||||
C.RED + "Could not locate " + type + " hydrology: " + error.getClass().getSimpleName());
|
C.RED + "Could not search for " + label + ": " + error.getClass().getSimpleName()
|
||||||
Iris.reportError("Could not locate accepted hydrology feature '" + type + "'.", error);
|
+ (error.getMessage() == null ? "" : " - " + error.getMessage()));
|
||||||
|
Iris.reportError("Could not locate hydrology feature '" + featureType.key() + "'.", error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,10 +21,9 @@ package art.arcane.iris.command;
|
|||||||
import art.arcane.iris.Iris;
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
|
import art.arcane.iris.studio.map.MapWindow;
|
||||||
|
import art.arcane.iris.studio.map.PaletteExplorerStart;
|
||||||
import art.arcane.iris.studio.view.GuiHost;
|
import art.arcane.iris.studio.view.GuiHost;
|
||||||
import art.arcane.iris.studio.view.ImageMapStudioGUI;
|
|
||||||
import art.arcane.iris.studio.view.NoiseExplorerGUI;
|
|
||||||
import art.arcane.iris.studio.view.VisionGUI;
|
|
||||||
import art.arcane.iris.pack.loading.IrisData;
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
import art.arcane.iris.studio.workspace.IrisProject;
|
import art.arcane.iris.studio.workspace.IrisProject;
|
||||||
import art.arcane.iris.studio.workspace.IrisCodeWorkspace;
|
import art.arcane.iris.studio.workspace.IrisCodeWorkspace;
|
||||||
@@ -98,7 +97,9 @@ import java.util.Objects;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import art.arcane.iris.localization.IrisLanguage;
|
import art.arcane.iris.localization.IrisLanguage;
|
||||||
import art.arcane.iris.localization.BukkitCommandMessages;
|
import art.arcane.iris.localization.BukkitCommandMessages;
|
||||||
@@ -255,15 +256,10 @@ public class CommandStudio implements DirectorExecutor {
|
|||||||
long seed
|
long seed
|
||||||
) {
|
) {
|
||||||
if (noGUI()) return;
|
if (noGUI()) return;
|
||||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_NOISE_EXPLORER));
|
VolmitSender commandSender = sender();
|
||||||
|
UUID openerId = commandSender.isPlayer() ? commandSender.player().getUniqueId() : null;
|
||||||
if (generator == null) {
|
MapWindow.NoiseSelection selection = new MapWindow.NoiseSelection(generator == null ? null : generator.getLoadKey(), generator, seed);
|
||||||
NoiseExplorerGUI.launch();
|
withPlayerContext(opener -> openNoiseAtPlayer(commandSender, openerId, opener, selection));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String generatorKey = generator.getLoadKey();
|
|
||||||
NoiseExplorerGUI.launchGeneratorKey(generatorKey, generator, seed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Director(description = "Open the image-map studio (External GUI)", descriptionKey = "iris.director.commandstudio.director.open_image_map_studio_external_gui", aliases = {"imap"})
|
@Director(description = "Open the image-map studio (External GUI)", descriptionKey = "iris.director.commandstudio.director.open_image_map_studio_external_gui", aliases = {"imap"})
|
||||||
@@ -279,8 +275,7 @@ public class CommandStudio implements DirectorExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Engine activeEngine = IrisToolbelt.access(world).getEngine();
|
requestMapStart(world, MapWindow::openImageMaps);
|
||||||
ImageMapStudioGUI.launch(activeEngine);
|
|
||||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_IMAGE_MAP_STUDIO));
|
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_IMAGE_MAP_STUDIO));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,11 +415,61 @@ public class CommandStudio implements DirectorExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
UUID openerId = sender().isPlayer() ? player().getUniqueId() : null;
|
requestMapStart(world, MapWindow::openMap);
|
||||||
VisionGUI.launch(IrisToolbelt.access(world).getEngine(), openerId);
|
|
||||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_MAP));
|
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_MAP));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The map opens where the player stands when they are in that world, otherwise at the world origin.
|
||||||
|
private PaletteExplorerStart mapStartIn(World world, Player opener, UUID openerId) {
|
||||||
|
Location center = opener != null && opener.getWorld().equals(world) ? opener.getLocation() : null;
|
||||||
|
return new PaletteExplorerStart(
|
||||||
|
IrisToolbelt.access(world).getEngine(),
|
||||||
|
world.getUID(),
|
||||||
|
openerId,
|
||||||
|
null,
|
||||||
|
center == null ? 0D : center.getX(),
|
||||||
|
center == null ? 0D : center.getZ()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestMapStart(World world, Consumer<PaletteExplorerStart> launch) {
|
||||||
|
UUID openerId = sender().isPlayer() ? player().getUniqueId() : null;
|
||||||
|
withPlayerContext(opener -> launch.accept(mapStartIn(world, opener, openerId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openNoiseAtPlayer(VolmitSender commandSender, UUID openerId, Player opener, MapWindow.NoiseSelection selection) {
|
||||||
|
World world = opener == null ? null : opener.getWorld();
|
||||||
|
PlatformChunkGenerator worldGenerator = world == null ? null : IrisToolbelt.access(world);
|
||||||
|
Engine engine = worldGenerator == null ? GuiHost.get().findActiveEngine() : worldGenerator.getEngine();
|
||||||
|
if (engine == null) {
|
||||||
|
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_NEED_BE_SPECIFY_IRIS_GENERATED_WORLD));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PaletteExplorerStart start = worldGenerator == null
|
||||||
|
? new PaletteExplorerStart(engine, null, openerId, null, 0D, 0D)
|
||||||
|
: mapStartIn(world, opener, openerId);
|
||||||
|
MapWindow.openNoise(start, selection);
|
||||||
|
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_NOISE_EXPLORER));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void withPlayerContext(Consumer<Player> action) {
|
||||||
|
Player opener = sender().isPlayer() ? player() : null;
|
||||||
|
if (opener == null) {
|
||||||
|
action.accept(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AtomicBoolean completed = new AtomicBoolean();
|
||||||
|
Consumer<Player> complete = player -> {
|
||||||
|
if (completed.compareAndSet(false, true)) {
|
||||||
|
action.accept(player);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Runnable retired = () -> complete.accept(null);
|
||||||
|
if (!J.runEntity(opener, () -> complete.accept(opener.isOnline() ? opener : null), 0, retired)) {
|
||||||
|
retired.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Director(description = "Profiles the performance of a dimension", descriptionKey = "iris.director.commandstudio.director.profiles_performance_dimension", origin = DirectorOrigin.PLAYER)
|
@Director(description = "Profiles the performance of a dimension", descriptionKey = "iris.director.commandstudio.director.profiles_performance_dimension", origin = DirectorOrigin.PLAYER)
|
||||||
public void profile(
|
public void profile(
|
||||||
@Param(description = "The dimension to profile", descriptionKey = "iris.director.commandstudio.param.dimension_profile", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
@Param(description = "The dimension to profile", descriptionKey = "iris.director.commandstudio.param.dimension_profile", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
||||||
|
|||||||
+31
-34
@@ -17,8 +17,7 @@ import art.arcane.iris.platform.bukkit.terrain.IrisSampleLimits;
|
|||||||
import art.arcane.iris.platform.bukkit.terrain.IrisSurfaceClassifier;
|
import art.arcane.iris.platform.bukkit.terrain.IrisSurfaceClassifier;
|
||||||
import art.arcane.iris.platform.bukkit.terrain.IrisWorldInfoFactory;
|
import art.arcane.iris.platform.bukkit.terrain.IrisWorldInfoFactory;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
|
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
|
||||||
import art.arcane.iris.generation.terrain.InferredType;
|
import art.arcane.iris.generation.terrain.InferredType;
|
||||||
import art.arcane.iris.generation.biome.IrisBiome;
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||||
@@ -108,15 +107,15 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
int surface = engine.getHeight(blockX, blockZ);
|
int surface = engine.getHeight(blockX, blockZ);
|
||||||
HydrologyColumnSample hydrology = hydrologySample(engine, blockX, blockZ);
|
WaterRole role = engine.getComplex().getWaterRoleStream().get(blockX, blockZ);
|
||||||
int fluid = (int) Math.round(
|
int fluid = (int) Math.round(
|
||||||
engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ));
|
engine.getComplex().getWaterHeadStream().get(blockX, blockZ));
|
||||||
InferredType inferredType = null;
|
InferredType inferredType = null;
|
||||||
if (IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid)) {
|
if (IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid)) {
|
||||||
IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ);
|
IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ);
|
||||||
inferredType = biome == null ? null : biome.getInferredType();
|
inferredType = biome == null ? null : biome.getInferredType();
|
||||||
}
|
}
|
||||||
return IrisSurfaceClassifier.classify(surface, fluid, inferredType, hydrology);
|
return IrisSurfaceClassifier.classify(surface, fluid, inferredType, role);
|
||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
reportQueryFault("surfaceKind", world, error);
|
reportQueryFault("surfaceKind", world, error);
|
||||||
return IrisSurfaceKind.UNKNOWN;
|
return IrisSurfaceKind.UNKNOWN;
|
||||||
@@ -266,12 +265,12 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
HydrologyColumnSample hydrology = wantRiver
|
WaterRole role = wantRiver
|
||||||
? hydrologySample(engine, blockX, blockZ)
|
? engine.getComplex().getWaterRoleStream().get(blockX, blockZ)
|
||||||
: null;
|
: WaterRole.NONE;
|
||||||
int surface = wantHeight || wantKind ? engine.getHeight(blockX, blockZ) : 0;
|
int surface = wantHeight || wantKind ? engine.getHeight(blockX, blockZ) : 0;
|
||||||
int fluid = wantRiver
|
int fluid = wantRiver
|
||||||
? (int) Math.round(engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ))
|
? (int) Math.round(engine.getComplex().getWaterHeadStream().get(blockX, blockZ))
|
||||||
: engine.getDimension().getFluidHeight();
|
: engine.getDimension().getFluidHeight();
|
||||||
boolean needsBiome = wantBiome
|
boolean needsBiome = wantBiome
|
||||||
|| (wantKind && IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid));
|
|| (wantKind && IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid));
|
||||||
@@ -281,29 +280,23 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
|||||||
surface,
|
surface,
|
||||||
fluid,
|
fluid,
|
||||||
biome == null ? null : biome.getInferredType(),
|
biome == null ? null : biome.getInferredType(),
|
||||||
hydrology
|
role
|
||||||
)
|
)
|
||||||
: IrisSurfaceKind.UNKNOWN;
|
: IrisSurfaceKind.UNKNOWN;
|
||||||
String biomeKey = wantBiome && biome != null ? biome.getLoadKey() : null;
|
String biomeKey = wantBiome && biome != null ? biome.getLoadKey() : null;
|
||||||
int natural = wantNaturalHeight
|
int natural = wantNaturalHeight
|
||||||
? (int) Math.round(engine.getComplex().getNaturalHeightStream().get(blockX, blockZ)) + minHeight
|
? (int) Math.round(engine.getComplex().getNaturalHeightStream().get(blockX, blockZ)) + minHeight
|
||||||
: IrisColumnSample.UNAVAILABLE_HEIGHT;
|
: IrisColumnSample.UNAVAILABLE_HEIGHT;
|
||||||
HydrologyColumnLayer surfaceLayer = hydrology == null
|
|
||||||
? null
|
|
||||||
: hydrology.primarySurfaceLayer().orElse(null);
|
|
||||||
boolean riverPresent = surfaceLayer != null;
|
|
||||||
IrisRiverState riverState = wantRiverState
|
IrisRiverState riverState = wantRiverState
|
||||||
? riverState(hydrology)
|
? riverState(role)
|
||||||
: IrisRiverState.NONE;
|
: IrisRiverState.NONE;
|
||||||
double riverDistance = wantRiverDistance && riverPresent
|
double riverDistance = wantRiverDistance
|
||||||
? engine.getComplex().getRiverDistanceStream().get(blockX, blockZ)
|
? riverDistance(role)
|
||||||
: IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE;
|
: IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE;
|
||||||
int riverFlow = wantRiverFlow && riverPresent
|
int riverFlow = wantRiverFlow
|
||||||
? (int) Math.round(engine.getComplex().getRiverFlowStream().get(blockX, blockZ))
|
? riverFlow(role)
|
||||||
: IrisColumnSample.UNAVAILABLE_RIVER_FLOW;
|
: IrisColumnSample.UNAVAILABLE_RIVER_FLOW;
|
||||||
int riverWaterSurfaceY = wantRiverWaterSurface
|
int riverWaterSurfaceY = wantRiverWaterSurface && role.wet()
|
||||||
&& surfaceLayer != null
|
|
||||||
&& surfaceLayer.connectedFluid()
|
|
||||||
? fluid + minHeight
|
? fluid + minHeight
|
||||||
: IrisColumnSample.UNAVAILABLE_HEIGHT;
|
: IrisColumnSample.UNAVAILABLE_HEIGHT;
|
||||||
sink.accept(new IrisColumnSample(
|
sink.accept(new IrisColumnSample(
|
||||||
@@ -327,21 +320,25 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static IrisRiverState riverState(HydrologyColumnSample hydrology) {
|
static IrisRiverState riverState(WaterRole role) {
|
||||||
HydrologyColumnLayer layer = hydrology == null
|
return role.wet() ? IrisRiverState.WET : IrisRiverState.NONE;
|
||||||
? null
|
|
||||||
: hydrology.primarySurfaceLayer().orElse(null);
|
|
||||||
if (layer == null || !layer.channel()) {
|
|
||||||
return IrisRiverState.NONE;
|
|
||||||
}
|
|
||||||
return layer.connectedFluid() ? IrisRiverState.WET : IrisRiverState.DRY;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HydrologyColumnSample hydrologySample(Engine engine, int blockX, int blockZ) {
|
static double riverDistance(WaterRole role) {
|
||||||
if (engine.getComplex().getHydrologyRuntime() == null) {
|
return switch (role) {
|
||||||
return null;
|
case NONE -> IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE;
|
||||||
|
case CHANNEL, POND, MOUTH, CASCADE, WATERFALL, INLET -> 0D;
|
||||||
|
case SHORE, POND_SHORE -> 1D;
|
||||||
|
case BANK, DAM -> 2D;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return engine.getComplex().getHydrologyRuntime().sample(blockX, blockZ).orElse(null);
|
|
||||||
|
static int riverFlow(WaterRole role) {
|
||||||
|
return switch (role) {
|
||||||
|
case NONE, SHORE, POND_SHORE, BANK, DAM -> IrisColumnSample.UNAVAILABLE_RIVER_FLOW;
|
||||||
|
case CHANNEL, MOUTH, CASCADE, WATERFALL -> 1;
|
||||||
|
case POND, INLET -> 0;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Optional<String> key(IrisBiome biome) {
|
private static Optional<String> key(IrisBiome biome) {
|
||||||
|
|||||||
+8
-18
@@ -1,8 +1,7 @@
|
|||||||
package art.arcane.iris.platform.bukkit.terrain;
|
package art.arcane.iris.platform.bukkit.terrain;
|
||||||
|
|
||||||
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
|
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
|
||||||
import art.arcane.iris.generation.terrain.InferredType;
|
import art.arcane.iris.generation.terrain.InferredType;
|
||||||
|
|
||||||
public final class IrisSurfaceClassifier {
|
public final class IrisSurfaceClassifier {
|
||||||
@@ -29,25 +28,16 @@ public final class IrisSurfaceClassifier {
|
|||||||
int engineSurfaceHeight,
|
int engineSurfaceHeight,
|
||||||
int engineFluidHeight,
|
int engineFluidHeight,
|
||||||
InferredType inferredType,
|
InferredType inferredType,
|
||||||
HydrologyColumnSample hydrology
|
WaterRole role
|
||||||
) {
|
) {
|
||||||
if (engineSurfaceHeight <= 0) {
|
if (engineSurfaceHeight <= 0) {
|
||||||
return IrisSurfaceKind.VOID;
|
return IrisSurfaceKind.VOID;
|
||||||
}
|
}
|
||||||
HydrologyColumnLayer layer = hydrology == null
|
return switch (role) {
|
||||||
? null
|
case NONE -> classify(engineSurfaceHeight, engineFluidHeight, inferredType);
|
||||||
: hydrology.primarySurfaceLayer().orElse(null);
|
case SHORE, POND_SHORE -> IrisSurfaceKind.RIVER_SHORE;
|
||||||
if (layer != null) {
|
case BANK, DAM -> IrisSurfaceKind.LAND;
|
||||||
if (layer.shore()) {
|
case CHANNEL, POND, MOUTH, CASCADE, WATERFALL, INLET -> IrisSurfaceKind.RIVER;
|
||||||
return IrisSurfaceKind.RIVER_SHORE;
|
};
|
||||||
}
|
|
||||||
if (layer.channel()) {
|
|
||||||
return layer.connectedFluid() ? IrisSurfaceKind.RIVER : IrisSurfaceKind.DRY_CHANNEL;
|
|
||||||
}
|
|
||||||
if (layer.grading()) {
|
|
||||||
return IrisSurfaceKind.LAND;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return classify(engineSurfaceHeight, engineFluidHeight, inferredType);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+39
-3
@@ -23,6 +23,8 @@ import art.arcane.iris.world.event.IrisEngineHotloadEvent;
|
|||||||
import art.arcane.iris.world.IrisToolbelt;
|
import art.arcane.iris.world.IrisToolbelt;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||||
|
import art.arcane.iris.studio.StudioSVC;
|
||||||
|
import art.arcane.iris.studio.workspace.IrisProject;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.event.EventHandler;
|
import org.bukkit.event.EventHandler;
|
||||||
@@ -40,10 +42,47 @@ public final class BukkitGuiHost implements GuiHost.Provider {
|
|||||||
GuiHost.set(new BukkitGuiHost());
|
GuiHost.set(new BukkitGuiHost());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The engine of the open studio project, which reads the pack folder being edited, or else the first
|
||||||
|
* Iris world. A normal Iris world runs on its own copy of the pack, made when the world was created.
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Engine findActiveEngine() {
|
public Engine findActiveEngine() {
|
||||||
|
Engine studioEngine = findStudioEngine();
|
||||||
|
if (studioEngine != null) {
|
||||||
|
return studioEngine;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
for (World world : new ArrayList<>(Bukkit.getWorlds())) {
|
for (World world : new ArrayList<>(Bukkit.getWorlds())) {
|
||||||
|
Engine engine = engineOf(world);
|
||||||
|
if (engine != null) {
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Engine findEngine(UUID worldId) {
|
||||||
|
World world = worldId == null ? null : Bukkit.getWorld(worldId);
|
||||||
|
return world == null ? null : engineOf(world);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Engine findStudioEngine() {
|
||||||
|
try {
|
||||||
|
StudioSVC studio = Iris.service(StudioSVC.class);
|
||||||
|
IrisProject project = studio == null || !studio.isProjectOpen() ? null : studio.getActiveProject();
|
||||||
|
PlatformChunkGenerator provider = project == null ? null : project.getActiveProvider();
|
||||||
|
Engine engine = provider == null ? null : provider.getEngine();
|
||||||
|
return engine == null || engine.isClosed() ? null : engine;
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Engine engineOf(World world) {
|
||||||
try {
|
try {
|
||||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||||
if (access != null && access.getEngine() != null && !access.getEngine().isClosed()) {
|
if (access != null && access.getEngine() != null && !access.getEngine().isClosed()) {
|
||||||
@@ -51,9 +90,6 @@ public final class BukkitGuiHost implements GuiHost.Provider {
|
|||||||
}
|
}
|
||||||
} catch (Throwable ignored) {
|
} catch (Throwable ignored) {
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+83
-40
@@ -18,23 +18,19 @@
|
|||||||
|
|
||||||
package art.arcane.iris.studio.view;
|
package art.arcane.iris.studio.view;
|
||||||
|
|
||||||
import art.arcane.volmlib.nativelib.view.WorldMarker;
|
|
||||||
|
|
||||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.studio.render.RenderType;
|
|
||||||
import art.arcane.iris.world.IrisWorld;
|
import art.arcane.iris.world.IrisWorld;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.world.task.J;
|
import art.arcane.iris.world.task.J;
|
||||||
import art.arcane.volmlib.util.format.Form;
|
import art.arcane.volmlib.util.format.Form;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.entity.LivingEntity;
|
import org.bukkit.entity.LivingEntity;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -56,7 +52,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
|||||||
private final AtomicBoolean playerRefreshQueued = new AtomicBoolean();
|
private final AtomicBoolean playerRefreshQueued = new AtomicBoolean();
|
||||||
private final AtomicLong teleportSequence = new AtomicLong();
|
private final AtomicLong teleportSequence = new AtomicLong();
|
||||||
private final AtomicReference<VisionTeleportRequest> latestTeleport = new AtomicReference<>();
|
private final AtomicReference<VisionTeleportRequest> latestTeleport = new AtomicReference<>();
|
||||||
private volatile List<WorldMarker> playerMarkers = List.of();
|
private volatile List<GuiMarker> playerMarkers = List.of();
|
||||||
|
|
||||||
public BukkitVisionOverlay(Engine engine, UUID openerId) {
|
public BukkitVisionOverlay(Engine engine, UUID openerId) {
|
||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
@@ -68,7 +64,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
|||||||
* built by a server thread.
|
* built by a server thread.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public List<WorldMarker> players() {
|
public List<GuiMarker> players() {
|
||||||
queuePlayerRefresh();
|
queuePlayerRefresh();
|
||||||
return playerMarkers;
|
return playerMarkers;
|
||||||
}
|
}
|
||||||
@@ -78,26 +74,48 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean scheduled = J.runGlobal(() -> {
|
boolean scheduled = J.runGlobal(this::refreshPlayers);
|
||||||
try {
|
|
||||||
List<WorldMarker> markers = new ArrayList<>();
|
|
||||||
for (Player player : BukkitWorldBinding.players(engine.getWorld())) {
|
|
||||||
Location at = player.getLocation();
|
|
||||||
markers.add(WorldMarker.player(player.getName(), at.getX(), at.getZ()));
|
|
||||||
}
|
|
||||||
playerMarkers = List.copyOf(markers);
|
|
||||||
} finally {
|
|
||||||
playerRefreshQueued.set(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!scheduled) {
|
if (!scheduled) {
|
||||||
playerRefreshQueued.set(false);
|
playerRefreshQueued.set(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void refreshPlayers() {
|
||||||
|
World world = BukkitWorldBinding.world(engine.getWorld());
|
||||||
|
List<Player> players = BukkitWorldBinding.players(engine.getWorld());
|
||||||
|
if (players.isEmpty() || world == null) {
|
||||||
|
playerMarkers = List.of();
|
||||||
|
playerRefreshQueued.set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<GuiMarker> markers = Collections.synchronizedList(new ArrayList<>(players.size()));
|
||||||
|
AtomicInteger pending = new AtomicInteger(players.size());
|
||||||
|
for (Player player : players) {
|
||||||
|
AtomicBoolean completed = new AtomicBoolean();
|
||||||
|
Runnable complete = () -> {
|
||||||
|
if (completed.compareAndSet(false, true) && pending.decrementAndGet() == 0) {
|
||||||
|
playerMarkers = List.copyOf(markers);
|
||||||
|
playerRefreshQueued.set(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (!J.runEntity(player, () -> {
|
||||||
|
try {
|
||||||
|
if (player.isOnline() && player.getWorld() == world) {
|
||||||
|
Location at = player.getLocation();
|
||||||
|
markers.add(GuiMarker.player(player.getName(), at.getX(), at.getZ(), at.getYaw()));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
complete.run();
|
||||||
|
}
|
||||||
|
}, 0, complete)) {
|
||||||
|
complete.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void requestEntities(Consumer<List<WorldMarker>> sink) {
|
public void requestEntities(Consumer<List<GuiMarker>> sink) {
|
||||||
J.runGlobal(() -> {
|
J.runGlobal(() -> {
|
||||||
IrisWorld target = engine.getWorld();
|
IrisWorld target = engine.getWorld();
|
||||||
World world = BukkitWorldBinding.world(target);
|
World world = BukkitWorldBinding.world(target);
|
||||||
@@ -118,7 +136,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<WorldMarker> collected = Collections.synchronizedList(new ArrayList<>(living.size()));
|
List<GuiMarker> collected = Collections.synchronizedList(new ArrayList<>(living.size()));
|
||||||
AtomicInteger pending = new AtomicInteger(living.size());
|
AtomicInteger pending = new AtomicInteger(living.size());
|
||||||
Runnable complete = () -> {
|
Runnable complete = () -> {
|
||||||
if (pending.decrementAndGet() == 0) {
|
if (pending.decrementAndGet() == 0) {
|
||||||
@@ -144,22 +162,22 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private WorldMarker marker(LivingEntity entity, Location at) {
|
private GuiMarker marker(LivingEntity entity, Location at) {
|
||||||
String label = Form.capitalizeWords(entity.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "));
|
String label = Form.capitalizeWords(entity.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "));
|
||||||
double maxHealth = 0;
|
double maxHealth = 0;
|
||||||
try {
|
try {
|
||||||
maxHealth = entity.getAttribute(MAX_HEALTH).getValue();
|
maxHealth = entity.getAttribute(MAX_HEALTH).getValue();
|
||||||
} catch (Throwable ignored) {
|
} catch (Throwable ignored) {
|
||||||
}
|
}
|
||||||
return WorldMarker.entity(label, at.getX(), at.getY(), at.getZ(), entity.getHealth(), maxHealth);
|
return GuiMarker.entity(label, at.getX(), at.getY(), at.getZ(), entity.getHealth(), maxHealth);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void teleport(double worldX, double worldZ) {
|
public void teleport(double worldX, double worldZ) {
|
||||||
VisionTeleportRequest request = new VisionTeleportRequest(
|
VisionTeleportRequest request = new VisionTeleportRequest(
|
||||||
teleportSequence.incrementAndGet(),
|
teleportSequence.incrementAndGet(),
|
||||||
VisionGUI.floorWorldCoordinate(worldX),
|
(int) StrictMath.floor(worldX),
|
||||||
VisionGUI.floorWorldCoordinate(worldZ));
|
(int) StrictMath.floor(worldZ));
|
||||||
latestTeleport.set(request);
|
latestTeleport.set(request);
|
||||||
startTeleport(request);
|
startTeleport(request);
|
||||||
}
|
}
|
||||||
@@ -212,6 +230,46 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void requestOpenerPosition(Consumer<OpenerPosition> sink) {
|
||||||
|
AtomicBoolean completed = new AtomicBoolean();
|
||||||
|
Consumer<OpenerPosition> complete = position -> {
|
||||||
|
if (completed.compareAndSet(false, true)) {
|
||||||
|
sink.accept(position);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (openerId == null) {
|
||||||
|
complete.accept(OpenerPosition.OFFLINE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean scheduled = J.runGlobal(() -> {
|
||||||
|
Player player = Bukkit.getPlayer(openerId);
|
||||||
|
if (player == null) {
|
||||||
|
complete.accept(OpenerPosition.OFFLINE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Runnable retired = () -> complete.accept(OpenerPosition.OFFLINE);
|
||||||
|
if (!J.runEntity(player, () -> complete.accept(positionOf(player,
|
||||||
|
BukkitWorldBinding.world(engine.getWorld()))), 0, retired)) {
|
||||||
|
retired.run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!scheduled) {
|
||||||
|
complete.accept(OpenerPosition.OFFLINE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OpenerPosition positionOf(Player player, World world) {
|
||||||
|
if (!player.isOnline()) {
|
||||||
|
return OpenerPosition.OFFLINE;
|
||||||
|
}
|
||||||
|
if (world == null || player.getWorld() != world) {
|
||||||
|
return OpenerPosition.IN_OTHER_WORLD;
|
||||||
|
}
|
||||||
|
Location at = player.getLocation();
|
||||||
|
return OpenerPosition.inWorld(at.getX(), at.getY(), at.getZ());
|
||||||
|
}
|
||||||
|
|
||||||
private Player selectPlayer(List<Player> players) {
|
private Player selectPlayer(List<Player> players) {
|
||||||
if (openerId == null) {
|
if (openerId == null) {
|
||||||
return players.isEmpty() ? null : players.get(0);
|
return players.isEmpty() ? null : players.get(0);
|
||||||
@@ -316,21 +374,6 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
|||||||
+ blockX + "," + blockZ + ".", failure);
|
+ blockX + "," + blockZ + ".", failure);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public String openInEditor(double worldX, double worldZ, RenderType type) {
|
|
||||||
IrisComplex complex = engine.getComplex();
|
|
||||||
File file = switch (type) {
|
|
||||||
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT, RIVER ->
|
|
||||||
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
|
|
||||||
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
|
|
||||||
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
|
|
||||||
case REGION -> complex.getRegionStream().get(worldX, worldZ).openInVSCode();
|
|
||||||
case CAVE_LAND -> complex.getCaveBiomeStream().get(worldX, worldZ).openInVSCode();
|
|
||||||
default -> null;
|
|
||||||
};
|
|
||||||
return file == null ? null : file.getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class VisionTeleportRequest {
|
private static final class VisionTeleportRequest {
|
||||||
private final long sequence;
|
private final long sequence;
|
||||||
private final int blockX;
|
private final int blockX;
|
||||||
|
|||||||
+59
-1
@@ -58,6 +58,7 @@ import java.util.Set;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
@@ -78,6 +79,9 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
private volatile UUID overworldEntryAwaitingVerification;
|
private volatile UUID overworldEntryAwaitingVerification;
|
||||||
private volatile World overworldEntryWorld;
|
private volatile World overworldEntryWorld;
|
||||||
private volatile boolean paperEntryListenerRegistered;
|
private volatile boolean paperEntryListenerRegistered;
|
||||||
|
private final Object stagingMonitor = new Object();
|
||||||
|
private boolean stagingAdmissionOpen = true;
|
||||||
|
private StagingAdmission activeStaging;
|
||||||
|
|
||||||
public PendingWorldReplacementManager(Iris plugin) {
|
public PendingWorldReplacementManager(Iris plugin) {
|
||||||
this.plugin = Objects.requireNonNull(plugin, "plugin");
|
this.plugin = Objects.requireNonNull(plugin, "plugin");
|
||||||
@@ -135,7 +139,8 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
LifecycleOperationCoordinator.Domain.WORLD_MUTATION,
|
LifecycleOperationCoordinator.Domain.WORLD_MUTATION,
|
||||||
LifecycleOperationCoordinator.OperationKind.WORLD_REPLACE,
|
LifecycleOperationCoordinator.OperationKind.WORLD_REPLACE,
|
||||||
requiredWorldKey.toString()
|
requiredWorldKey.toString()
|
||||||
); WorldReplacementProgress progress = WorldReplacementProgress.start(
|
); StagingAdmission ignoredAdmission = admitStaging(requiredWorldKey);
|
||||||
|
WorldReplacementProgress progress = WorldReplacementProgress.start(
|
||||||
requiredSender, requiredWorldKey.toString())) {
|
requiredSender, requiredWorldKey.toString())) {
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
IrisStartupValidation.requireWorldReplacementStagingReady();
|
IrisStartupValidation.requireWorldReplacementStagingReady();
|
||||||
@@ -271,6 +276,29 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void awaitStagingForShutdown() {
|
||||||
|
StagingAdmission staging;
|
||||||
|
synchronized (stagingMonitor) {
|
||||||
|
stagingAdmissionOpen = false;
|
||||||
|
staging = activeStaging;
|
||||||
|
}
|
||||||
|
if (staging == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Iris.warn("Waiting for the replacement of " + staging.worldKey + " to finish staging before Iris shuts down.");
|
||||||
|
boolean interrupted = false;
|
||||||
|
while (staging.finished.getCount() > 0L) {
|
||||||
|
try {
|
||||||
|
staging.finished.await();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
interrupted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (interrupted) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public synchronized void processPendingStartupReplacements() {
|
public synchronized void processPendingStartupReplacements() {
|
||||||
ArrayList<String> failures = new ArrayList<>();
|
ArrayList<String> failures = new ArrayList<>();
|
||||||
ArrayList<String> restartBoundaries = new ArrayList<>();
|
ArrayList<String> restartBoundaries = new ArrayList<>();
|
||||||
@@ -1048,6 +1076,17 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
return plugin.getDataFolder().toPath().toAbsolutePath().normalize();
|
return plugin.getDataFolder().toPath().toAbsolutePath().normalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private StagingAdmission admitStaging(NamespacedKey worldKey) throws IOException {
|
||||||
|
synchronized (stagingMonitor) {
|
||||||
|
if (!stagingAdmissionOpen) {
|
||||||
|
throw new IOException("Iris is shutting down. Run the replacement again after the server starts.");
|
||||||
|
}
|
||||||
|
StagingAdmission staging = new StagingAdmission(worldKey);
|
||||||
|
activeStaging = staging;
|
||||||
|
return staging;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private synchronized void loadOverworldEntryGuard() throws IOException {
|
private synchronized void loadOverworldEntryGuard() throws IOException {
|
||||||
ExactWorldSlotPathPolicy.Target target = resolveTarget(OVERWORLD_KEY);
|
ExactWorldSlotPathPolicy.Target target = resolveTarget(OVERWORLD_KEY);
|
||||||
Optional<WorldReplacementEntryGuard.Entry> loaded = WorldReplacementEntryGuard.load(target.worldDirectory());
|
Optional<WorldReplacementEntryGuard.Entry> loaded = WorldReplacementEntryGuard.load(target.worldDirectory());
|
||||||
@@ -1218,6 +1257,25 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
record SavedLocationInspection(boolean safe, Throwable failure) {
|
record SavedLocationInspection(boolean safe, Throwable failure) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class StagingAdmission implements AutoCloseable {
|
||||||
|
private final NamespacedKey worldKey;
|
||||||
|
private final CountDownLatch finished = new CountDownLatch(1);
|
||||||
|
|
||||||
|
private StagingAdmission(NamespacedKey worldKey) {
|
||||||
|
this.worldKey = worldKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
synchronized (stagingMonitor) {
|
||||||
|
if (activeStaging == this) {
|
||||||
|
activeStaging = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finished.countDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final class RestartBoundaryRequired extends IOException {
|
private static final class RestartBoundaryRequired extends IOException {
|
||||||
private RestartBoundaryRequired(String message) {
|
private RestartBoundaryRequired(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
|
|||||||
+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()));
|
||||||
|
|||||||
@@ -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,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/)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,143 @@
|
|||||||
|
import { readFile, realpath } from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
const nonGround = new Set(['air', 'cave_air', 'void_air', 'water', 'lava', 'nether_wart_block', 'warped_wart_block'])
|
||||||
|
const chunkOffsets = [-56, -40, -24, -8, 8, 24, 40, 56]
|
||||||
|
|
||||||
|
function isWood(block) {
|
||||||
|
return block !== null && /_(wood|hyphae|log|stem)$/.test(block.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGround(block) {
|
||||||
|
return block !== null && block.boundingBox === 'block' && !nonGround.has(block.name)
|
||||||
|
&& !block.name.endsWith('_leaves') && !isWood(block)
|
||||||
|
}
|
||||||
|
|
||||||
|
function positionKey(position) {
|
||||||
|
return `${position.x},${position.y},${position.z}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function groundSpread(bot, base) {
|
||||||
|
const heights = []
|
||||||
|
for (const x of [-3, 0, 3]) {
|
||||||
|
for (const z of [-3, 0, 3]) {
|
||||||
|
for (let y = base.y + 6; y >= base.y - 8; y--) {
|
||||||
|
if (!isGround(bot.blockAt(base.offset(x, y - base.y, z)))) continue
|
||||||
|
heights.push(y)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return heights.length === 9 ? Math.max(...heights) - Math.min(...heights) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function inspectTrees(context) {
|
||||||
|
const bot = context.bot
|
||||||
|
const center = bot.entity.position.floored()
|
||||||
|
const woodIds = Object.values(bot.registry.blocksByName).filter(isWood).map(block => block.id)
|
||||||
|
const positions = bot.findBlocks({ matching: woodIds, point: center, maxDistance: 64, count: 20000 })
|
||||||
|
const columns = new Map()
|
||||||
|
for (const position of positions) {
|
||||||
|
const key = `${position.x},${position.z}`
|
||||||
|
if (!columns.has(key)) columns.set(key, [])
|
||||||
|
columns.get(key).push(position)
|
||||||
|
}
|
||||||
|
const trees = []
|
||||||
|
for (const column of columns.values()) {
|
||||||
|
column.sort((first, second) => first.y - second.y)
|
||||||
|
const base = column[0]
|
||||||
|
const height = column[column.length - 1].y - base.y + 1
|
||||||
|
if (column.length < 8 || height < 8 || column.length / height < 0.65) continue
|
||||||
|
if (column.some(position => position.distanceTo(center) >= 60)) continue
|
||||||
|
if (![0, 1, 2, 3].every(y => isWood(bot.blockAt(base.offset(0, y, 0))))) continue
|
||||||
|
const upperGaps = []
|
||||||
|
for (let y = base.y; y < base.y + height; y++) {
|
||||||
|
const block = bot.blockAt(base.offset(0, y - base.y, 0))
|
||||||
|
if (block === null || !isWood(block)) upperGaps.push({ y, block: block?.name ?? null })
|
||||||
|
}
|
||||||
|
const grounded = isGround(bot.blockAt(base.offset(0, -1, 0)))
|
||||||
|
const spread = groundSpread(bot, base)
|
||||||
|
trees.push({ base: { x: base.x, y: base.y, z: base.z }, woodBlocks: column.length,
|
||||||
|
height, grounded, terrainSpread: spread, upperGaps })
|
||||||
|
}
|
||||||
|
return trees
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'iris-tree-grounding',
|
||||||
|
description: 'Verify grounded spindle bases, continuous overlapping trunks, and paired pack geometry.',
|
||||||
|
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), 'Tree generation requires an isolated instance')
|
||||||
|
const packs = await realpath(path.join(directory, 'plugins', 'Iris', 'packs'))
|
||||||
|
context.expect(packs.startsWith(`${directory}${path.sep}`), 'Tree tests require local pack copies')
|
||||||
|
context.report.treeWorlds = []
|
||||||
|
context.report.treeGrounding = []
|
||||||
|
context.report.treeContinuity = []
|
||||||
|
for (const pack of ['overworld', 'underworld']) {
|
||||||
|
const dimension = JSON.parse(await readFile(path.join(packs, pack, 'dimensions', `${pack}.json`), 'utf8'))
|
||||||
|
context.expect(dimension.focus === 'estranged/spindlewood', 'Pack must focus on spindlewood', { pack })
|
||||||
|
await context.step(`create ${pack} spindle world`, async () => {
|
||||||
|
const world = `tree_${pack}_${Date.now().toString(36)}`
|
||||||
|
const response = await context.command(`/iris create name=${world} type=${pack} seed=78264193`,
|
||||||
|
/Successfully created your world|Failed to create/, 240000)
|
||||||
|
context.expect(response.includes('Successfully created your world'), response)
|
||||||
|
context.report.treeWorlds.push({ pack, name: world, seed: 78264193, biome: dimension.focus })
|
||||||
|
if (context.bot.game.gameMode !== 'spectator') {
|
||||||
|
await context.command('/minecraft:gamemode spectator', /spectator/i, 30000)
|
||||||
|
}
|
||||||
|
await context.waitUntil(() => context.bot.game.gameMode === 'spectator',
|
||||||
|
{ label: 'Spectator mode for terrain inspection', timeoutMs: 30000, intervalMs: 100 })
|
||||||
|
})
|
||||||
|
await context.step(`${pack} trees connect to flat and sloped terrain`, async () => {
|
||||||
|
const origin = context.bot.entity.position.floored()
|
||||||
|
const inspected = new Map()
|
||||||
|
for (const offset of [0, 80, 160]) {
|
||||||
|
await context.command(`/minecraft:tp @s ${origin.x + offset + 0.5} ${origin.y + 12} ${origin.z + 0.5}`,
|
||||||
|
/Teleported/i, 30000)
|
||||||
|
await context.waitUntil(() => {
|
||||||
|
const center = context.bot.entity.position.floored()
|
||||||
|
if (Math.abs(center.x - origin.x - offset) > 1) return false
|
||||||
|
return chunkOffsets.every(x => chunkOffsets.every(z => context.bot.blockAt(center.offset(x, 0, z)) !== null))
|
||||||
|
}, { label: 'Complete terrain neighborhood', timeoutMs: 90000, intervalMs: 250 })
|
||||||
|
const trees = inspectTrees(context)
|
||||||
|
for (const tree of trees) {
|
||||||
|
context.expect(tree.grounded, 'Generated spindle trunk base has no terrain connection', { pack, ...tree })
|
||||||
|
inspected.set(positionKey(tree.base), tree)
|
||||||
|
}
|
||||||
|
if (inspected.size >= 8 && [...inspected.values()].some(tree => tree.terrainSpread >= 2)) break
|
||||||
|
}
|
||||||
|
const trees = [...inspected.values()]
|
||||||
|
const sloped = trees.filter(tree => tree.terrainSpread >= 2)
|
||||||
|
context.expect(trees.length >= 8, 'Expected at least eight generated spindle trunk bases', { pack, trees })
|
||||||
|
context.expect(sloped.length > 0, 'Expected a grounded tree on uneven terrain', { pack, trees })
|
||||||
|
context.report.treeGrounding.push({ pack, trees: trees.length, sloped: sloped.length, samples: trees })
|
||||||
|
})
|
||||||
|
await context.step(`${pack} overlapping canopy preserves the seeded main trunk`, async () => {
|
||||||
|
await context.command('/minecraft:tp @s 0.5 88 0.5', /Teleported/i, 30000)
|
||||||
|
await context.waitUntil(() => {
|
||||||
|
const center = context.bot.entity.position.floored()
|
||||||
|
return Math.abs(center.x) < 2 && Math.abs(center.z) < 2
|
||||||
|
&& context.bot.blockAt(center.offset(-7 - center.x, 69 - center.y, 6 - center.z)) !== null
|
||||||
|
}, { label: 'Seeded trunk neighborhood', timeoutMs: 90000, intervalMs: 250 })
|
||||||
|
const center = context.bot.entity.position.floored()
|
||||||
|
const blocks = []
|
||||||
|
for (let y = 69; y <= 83; y++) {
|
||||||
|
const block = context.bot.blockAt(center.offset(-7 - center.x, y - center.y, 6 - center.z))
|
||||||
|
context.expect(isWood(block), 'Overlapping canopy interrupted the seeded main trunk',
|
||||||
|
{ pack, x: -7, y, z: 6, block: block?.name ?? null })
|
||||||
|
blocks.push({ y, block: block.name })
|
||||||
|
}
|
||||||
|
context.report.treeContinuity.push({ pack, x: -7, z: 6, blocks })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await context.step('paired packs retain matching sampled tree geometry', async () => {
|
||||||
|
const geometry = context.report.treeGrounding.map(result => result.samples.map(tree => ({
|
||||||
|
base: tree.base, woodBlocks: tree.woodBlocks, height: tree.height, terrainSpread: tree.terrainSpread
|
||||||
|
})).sort((first, second) => positionKey(first.base).localeCompare(positionKey(second.base))))
|
||||||
|
context.expect(JSON.stringify(geometry[0]) === JSON.stringify(geometry[1]),
|
||||||
|
'Paired spindle packs produced different sampled geometry', { geometry })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+47
-2
@@ -16,6 +16,7 @@ import art.arcane.iris.world.IrisWorld;
|
|||||||
import art.arcane.iris.world.WorldMaintenance;
|
import art.arcane.iris.world.WorldMaintenance;
|
||||||
import art.arcane.iris.world.runtime.BukkitEnginePlatformHooks;
|
import art.arcane.iris.world.runtime.BukkitEnginePlatformHooks;
|
||||||
import art.arcane.iris.world.task.J;
|
import art.arcane.iris.world.task.J;
|
||||||
|
import art.arcane.volmlib.integration.ReloadAware;
|
||||||
import art.arcane.volmlib.nativelib.terrain.ServerShutdownBoundary;
|
import art.arcane.volmlib.nativelib.terrain.ServerShutdownBoundary;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.NamespacedKey;
|
import org.bukkit.NamespacedKey;
|
||||||
@@ -37,9 +38,11 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyLong;
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||||
|
import static org.mockito.Mockito.doCallRealMethod;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.mockStatic;
|
import static org.mockito.Mockito.mockStatic;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
@@ -109,8 +112,6 @@ public class IrisShutdownMaintenanceTest {
|
|||||||
}
|
}
|
||||||
verify(fixture.generator).quiesceForServerShutdown();
|
verify(fixture.generator).quiesceForServerShutdown();
|
||||||
verify(fixture.generator, never()).closeAsync();
|
verify(fixture.generator, never()).closeAsync();
|
||||||
fixture.nms.verify(INMS::get);
|
|
||||||
verify(fixture.binding).deferPluginClassLoaderClose();
|
|
||||||
verify(fixture.binding, never()).releasePluginClassLoaderClose();
|
verify(fixture.binding, never()).releasePluginClassLoaderClose();
|
||||||
}
|
}
|
||||||
assertEquals(0, sessions.activeLeases());
|
assertEquals(0, sessions.activeLeases());
|
||||||
@@ -151,6 +152,37 @@ public class IrisShutdownMaintenanceTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void serverStopPreUnloadDefersTeardownWhileTheClassLoaderIsHeld() throws Exception {
|
||||||
|
try (Fixture fixture = new Fixture()) {
|
||||||
|
when(fixture.binding.deferPluginClassLoaderClose()).thenReturn(true);
|
||||||
|
setField(fixture.plugin, "postStopFinisherStarted", new AtomicBoolean(true));
|
||||||
|
try {
|
||||||
|
fixture.commitReloadDuringServerStop();
|
||||||
|
verify(fixture.generator).quiesceForServerShutdown();
|
||||||
|
verify(fixture.generator, never()).closeAsync();
|
||||||
|
assertTrue(fixture.serverStopTeardownDeferred());
|
||||||
|
} finally {
|
||||||
|
WorldMaintenance.endWorldMaintenance(fixture.identity, "pre-unload:HOT_UNLOAD");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void serverStopPreUnloadDrainsInlineWhenTheClassLoaderCannotBeHeld() throws Exception {
|
||||||
|
try (Fixture fixture = new Fixture()) {
|
||||||
|
when(fixture.binding.deferPluginClassLoaderClose()).thenReturn(false);
|
||||||
|
try {
|
||||||
|
fixture.commitReloadDuringServerStop();
|
||||||
|
verify(fixture.generator).closeAsync();
|
||||||
|
verify(fixture.generator, never()).quiesceForServerShutdown();
|
||||||
|
assertFalse(fixture.serverStopTeardownDeferred());
|
||||||
|
} finally {
|
||||||
|
WorldMaintenance.endWorldMaintenance(fixture.identity, "pre-unload:HOT_UNLOAD", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void setField(Iris plugin, String name, Object value) throws ReflectiveOperationException {
|
private static void setField(Iris plugin, String name, Object value) throws ReflectiveOperationException {
|
||||||
Field field = Iris.class.getDeclaredField(name);
|
Field field = Iris.class.getDeclaredField(name);
|
||||||
field.setAccessible(true);
|
field.setAccessible(true);
|
||||||
@@ -206,6 +238,19 @@ public class IrisShutdownMaintenanceTest {
|
|||||||
method.invoke(plugin, "manual_disable", 5L);
|
method.invoke(plugin, "manual_disable", 5L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void commitReloadDuringServerStop() throws ReflectiveOperationException {
|
||||||
|
setField(plugin, "alreadyDrained", new AtomicBoolean());
|
||||||
|
toolbelt.when(IrisToolbelt::isServerStopping).thenReturn(true);
|
||||||
|
doCallRealMethod().when(plugin).commitReload(any());
|
||||||
|
plugin.commitReload(ReloadAware.PreUnloadReason.HOT_UNLOAD);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean serverStopTeardownDeferred() throws ReflectiveOperationException {
|
||||||
|
Field field = Iris.class.getDeclaredField("serverStopTeardownDeferred");
|
||||||
|
field.setAccessible(true);
|
||||||
|
return ((AtomicBoolean) field.get(plugin)).get();
|
||||||
|
}
|
||||||
|
|
||||||
private void startFinisher() throws ReflectiveOperationException {
|
private void startFinisher() throws ReflectiveOperationException {
|
||||||
Method method = Iris.class.getDeclaredMethod("startPostStopFinisher");
|
Method method = Iris.class.getDeclaredMethod("startPostStopFinisher");
|
||||||
method.setAccessible(true);
|
method.setAccessible(true);
|
||||||
|
|||||||
+77
-10
@@ -1,7 +1,7 @@
|
|||||||
package art.arcane.iris.command;
|
package art.arcane.iris.command;
|
||||||
|
|
||||||
import art.arcane.iris.Iris;
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
|
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||||
import art.arcane.iris.localization.IrisLanguage;
|
import art.arcane.iris.localization.IrisLanguage;
|
||||||
@@ -22,6 +22,7 @@ import java.util.OptionalLong;
|
|||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
@@ -45,7 +46,7 @@ public class CommandFindHydrologyTest {
|
|||||||
@Test
|
@Test
|
||||||
public void worldChangeDuringSearchDoesNotLoadOrTeleportIntoTheNewWorld() {
|
public void worldChangeDuringSearchDoesNotLoadOrTeleportIntoTheNewWorld() {
|
||||||
try (Fixture fixture = new Fixture()) {
|
try (Fixture fixture = new Fixture()) {
|
||||||
fixture.command.river("surface", true);
|
fixture.command.river("river", true);
|
||||||
when(fixture.player.getWorld()).thenReturn(mock(World.class));
|
when(fixture.player.getWorld()).thenReturn(mock(World.class));
|
||||||
fixture.search.get().run();
|
fixture.search.get().run();
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ public class CommandFindHydrologyTest {
|
|||||||
@Test
|
@Test
|
||||||
public void disconnectDuringSearchDoesNotLoadTheDestination() {
|
public void disconnectDuringSearchDoesNotLoadTheDestination() {
|
||||||
try (Fixture fixture = new Fixture()) {
|
try (Fixture fixture = new Fixture()) {
|
||||||
fixture.command.river("surface", true);
|
fixture.command.river("river", true);
|
||||||
when(fixture.player.isOnline()).thenReturn(false);
|
when(fixture.player.isOnline()).thenReturn(false);
|
||||||
fixture.search.get().run();
|
fixture.search.get().run();
|
||||||
|
|
||||||
@@ -69,7 +70,7 @@ public class CommandFindHydrologyTest {
|
|||||||
@Test
|
@Test
|
||||||
public void worldChangeWhileLoadingTheDestinationCancelsTeleport() {
|
public void worldChangeWhileLoadingTheDestinationCancelsTeleport() {
|
||||||
try (Fixture fixture = new Fixture()) {
|
try (Fixture fixture = new Fixture()) {
|
||||||
fixture.command.river("surface", true);
|
fixture.command.river("river", true);
|
||||||
fixture.search.get().run();
|
fixture.search.get().run();
|
||||||
when(fixture.player.getWorld()).thenReturn(mock(World.class));
|
when(fixture.player.getWorld()).thenReturn(mock(World.class));
|
||||||
fixture.chunk.complete(mock(Chunk.class));
|
fixture.chunk.complete(mock(Chunk.class));
|
||||||
@@ -82,7 +83,7 @@ public class CommandFindHydrologyTest {
|
|||||||
@Test
|
@Test
|
||||||
public void successIsReportedOnlyAfterTeleportCompletes() {
|
public void successIsReportedOnlyAfterTeleportCompletes() {
|
||||||
try (Fixture fixture = new Fixture()) {
|
try (Fixture fixture = new Fixture()) {
|
||||||
fixture.command.river("surface", true);
|
fixture.command.river("river", true);
|
||||||
fixture.search.get().run();
|
fixture.search.get().run();
|
||||||
fixture.chunk.complete(mock(Chunk.class));
|
fixture.chunk.complete(mock(Chunk.class));
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ public class CommandFindHydrologyTest {
|
|||||||
@Test
|
@Test
|
||||||
public void rejectedTeleportReportsFailureWithoutSuccess() {
|
public void rejectedTeleportReportsFailureWithoutSuccess() {
|
||||||
try (Fixture fixture = new Fixture()) {
|
try (Fixture fixture = new Fixture()) {
|
||||||
fixture.command.river("surface", true);
|
fixture.command.river("river", true);
|
||||||
fixture.search.get().run();
|
fixture.search.get().run();
|
||||||
fixture.chunk.complete(mock(Chunk.class));
|
fixture.chunk.complete(mock(Chunk.class));
|
||||||
fixture.teleport.complete(false);
|
fixture.teleport.complete(false);
|
||||||
@@ -109,7 +110,7 @@ public class CommandFindHydrologyTest {
|
|||||||
public void failedTeleportReportsTheFullCauseWithoutSuccess() {
|
public void failedTeleportReportsTheFullCauseWithoutSuccess() {
|
||||||
try (Fixture fixture = new Fixture()) {
|
try (Fixture fixture = new Fixture()) {
|
||||||
IllegalStateException failure = new IllegalStateException("destination unloaded");
|
IllegalStateException failure = new IllegalStateException("destination unloaded");
|
||||||
fixture.command.river("surface", true);
|
fixture.command.river("river", true);
|
||||||
fixture.search.get().run();
|
fixture.search.get().run();
|
||||||
fixture.chunk.complete(mock(Chunk.class));
|
fixture.chunk.complete(mock(Chunk.class));
|
||||||
fixture.teleport.completeExceptionally(failure);
|
fixture.teleport.completeExceptionally(failure);
|
||||||
@@ -120,6 +121,72 @@ public class CommandFindHydrologyTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void everyFeatureTypeIsSearchedOffTheCommandThreadAtTheRuntimeRadius() {
|
||||||
|
for (HydrologyFeatureType type : HydrologyFeatureType.values()) {
|
||||||
|
try (Fixture fixture = new Fixture()) {
|
||||||
|
fixture.command.river(type.key(), false);
|
||||||
|
fixture.queries.verify(() -> GenerationSemanticQueries.nearestRiver(any(), any(), anyInt(), anyInt(), anyInt()), never());
|
||||||
|
fixture.search.get().run();
|
||||||
|
|
||||||
|
fixture.queries.verify(() -> GenerationSemanticQueries.nearestRiver(fixture.engine, type, 0, 0,
|
||||||
|
GenerationSemanticQueries.RIVER_SEARCH_DISTANCE));
|
||||||
|
verify(fixture.sender).sendMessage(contains("Found " + type.key().replace('_', ' ') + " at 32, 63, 32."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unknownTypeIsRejectedWithTheAcceptedTypesBeforeAnySearch() {
|
||||||
|
try (Fixture fixture = new Fixture()) {
|
||||||
|
fixture.command.river("surface", true);
|
||||||
|
|
||||||
|
verify(fixture.sender).sendMessage(contains("Use one of: stream, river, major_river, cascade, waterfall, mouth, pond, lake, dam, sea_inlet, cave_pool."));
|
||||||
|
assertNull(fixture.search.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void missingCavePoolIsReportedAsNotFoundInGeneratedChunks() {
|
||||||
|
try (Fixture fixture = new Fixture()) {
|
||||||
|
fixture.queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(fixture.engine), any(), eq(0), eq(0), anyInt()))
|
||||||
|
.thenReturn(Optional.empty());
|
||||||
|
fixture.command.river("cave_pool", true);
|
||||||
|
fixture.search.get().run();
|
||||||
|
|
||||||
|
verify(fixture.sender).sendMessage(contains("Searching generated chunks for the nearest cave pool within 7168 blocks"));
|
||||||
|
verify(fixture.sender).sendMessage(contains("No cave pool found in generated chunks within 7168 blocks."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void missingFeatureIsReportedAsNotFound() {
|
||||||
|
try (Fixture fixture = new Fixture()) {
|
||||||
|
fixture.queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(fixture.engine), any(), eq(0), eq(0), anyInt()))
|
||||||
|
.thenReturn(Optional.empty());
|
||||||
|
fixture.command.river("sea_inlet", true);
|
||||||
|
fixture.search.get().run();
|
||||||
|
|
||||||
|
verify(fixture.sender).sendMessage(contains("No sea inlet found within 7168 blocks."));
|
||||||
|
fixture.platform.verify(() -> BukkitPlatform.chunkAtAsync(any(World.class), anyInt(), anyInt(), eq(true)), never());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void plannerFailureIsReportedAsAnErrorNotAsNotFound() {
|
||||||
|
try (Fixture fixture = new Fixture()) {
|
||||||
|
IllegalStateException failure = new IllegalStateException("window planner exploded");
|
||||||
|
fixture.queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(fixture.engine), any(), eq(0), eq(0), anyInt()))
|
||||||
|
.thenThrow(failure);
|
||||||
|
fixture.command.river("lake", true);
|
||||||
|
fixture.search.get().run();
|
||||||
|
|
||||||
|
verify(fixture.sender).sendMessage(contains("Could not search for lake: IllegalStateException - window planner exploded"));
|
||||||
|
verify(fixture.sender, never()).sendMessage(contains("No lake found"));
|
||||||
|
fixture.logging.verify(() -> Iris.reportError(any(String.class), eq(failure)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final class Fixture implements AutoCloseable {
|
private static final class Fixture implements AutoCloseable {
|
||||||
private final CommandFind command = mock(CommandFind.class, CALLS_REAL_METHODS);
|
private final CommandFind command = mock(CommandFind.class, CALLS_REAL_METHODS);
|
||||||
private final Engine engine = mock(Engine.class);
|
private final Engine engine = mock(Engine.class);
|
||||||
@@ -132,7 +199,7 @@ public class CommandFindHydrologyTest {
|
|||||||
private final CompletableFuture<Boolean> teleport = new CompletableFuture<>();
|
private final CompletableFuture<Boolean> teleport = new CompletableFuture<>();
|
||||||
private final MockedStatic<J> scheduler = mockStatic(J.class);
|
private final MockedStatic<J> scheduler = mockStatic(J.class);
|
||||||
private final MockedStatic<BukkitPlatform> platform = mockStatic(BukkitPlatform.class);
|
private final MockedStatic<BukkitPlatform> platform = mockStatic(BukkitPlatform.class);
|
||||||
private final MockedStatic<GenerationSemanticQueries> queries = mockStatic(GenerationSemanticQueries.class);
|
private final MockedStatic<GenerationSemanticQueries> queries = mockStatic(GenerationSemanticQueries.class, CALLS_REAL_METHODS);
|
||||||
private final MockedStatic<IrisLanguage> language = mockStatic(IrisLanguage.class, invocation -> "teleported");
|
private final MockedStatic<IrisLanguage> language = mockStatic(IrisLanguage.class, invocation -> "teleported");
|
||||||
private final MockedStatic<Iris> logging = mockStatic(Iris.class);
|
private final MockedStatic<Iris> logging = mockStatic(Iris.class);
|
||||||
|
|
||||||
@@ -160,10 +227,10 @@ public class CommandFindHydrologyTest {
|
|||||||
});
|
});
|
||||||
platform.when(() -> BukkitPlatform.chunkAtAsync(any(World.class), anyInt(), anyInt(), eq(true))).thenReturn(chunk);
|
platform.when(() -> BukkitPlatform.chunkAtAsync(any(World.class), anyInt(), anyInt(), eq(true))).thenReturn(chunk);
|
||||||
platform.when(() -> BukkitPlatform.teleportAsync(eq(player), any(Location.class))).thenReturn(teleport);
|
platform.when(() -> BukkitPlatform.teleportAsync(eq(player), any(Location.class))).thenReturn(teleport);
|
||||||
queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(engine), any(), eq(0), eq(0), anyInt(), any()))
|
queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(engine), any(), eq(0), eq(0), anyInt()))
|
||||||
.thenReturn(Optional.of(new GenerationSemanticQueries.RiverResult(
|
.thenReturn(Optional.of(new GenerationSemanticQueries.RiverResult(
|
||||||
GenerationSemanticQueries.RiverSource.ACTIVE_PREDICTION,
|
GenerationSemanticQueries.RiverSource.ACTIVE_PREDICTION,
|
||||||
HydrologyFeatureType.RIFFLE, Optional.empty(), 1L, 32, 63, 32, OptionalLong.empty())));
|
HydrologyFeatureType.RIVER, 1L, 32, 63, 32, OptionalLong.empty())));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+5
-5
@@ -20,26 +20,26 @@ import static org.mockito.Mockito.when;
|
|||||||
|
|
||||||
public class CommandObjectFluidHeightTest {
|
public class CommandObjectFluidHeightTest {
|
||||||
@Test
|
@Test
|
||||||
public void absoluteObjectPlacementShiftsTheColumnRiverHeadFromNegativeMinY() {
|
public void absoluteObjectPlacementShiftsTheColumnWaterHeadFromNegativeMinY() {
|
||||||
World world = mock(World.class);
|
World world = mock(World.class);
|
||||||
Engine engine = mock(Engine.class);
|
Engine engine = mock(Engine.class);
|
||||||
IrisDimension dimension = mock(IrisDimension.class);
|
IrisDimension dimension = mock(IrisDimension.class);
|
||||||
IrisComplex complex = mock(IrisComplex.class);
|
IrisComplex complex = mock(IrisComplex.class);
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
ProceduralStream<Double> riverHead = mock(ProceduralStream.class);
|
ProceduralStream<Double> waterHead = mock(ProceduralStream.class);
|
||||||
Map<Block, BlockData> future = new HashMap<>();
|
Map<Block, BlockData> future = new HashMap<>();
|
||||||
|
|
||||||
when(engine.getMinHeight()).thenReturn(-64);
|
when(engine.getMinHeight()).thenReturn(-64);
|
||||||
when(engine.getDimension()).thenReturn(dimension);
|
when(engine.getDimension()).thenReturn(dimension);
|
||||||
when(engine.getComplex()).thenReturn(complex);
|
when(engine.getComplex()).thenReturn(complex);
|
||||||
when(dimension.getFluidHeight()).thenReturn(127);
|
when(dimension.getFluidHeight()).thenReturn(127);
|
||||||
when(complex.getRiverWaterSurfaceStream()).thenReturn(riverHead);
|
when(complex.getWaterHeadStream()).thenReturn(waterHead);
|
||||||
when(riverHead.get(12, -7)).thenReturn(131D);
|
when(waterHead.get(12, -7)).thenReturn(131D);
|
||||||
|
|
||||||
IObjectPlacer placer = CommandObject.createPlacer(world, future, engine);
|
IObjectPlacer placer = CommandObject.createPlacer(world, future, engine);
|
||||||
|
|
||||||
assertEquals(63, placer.getFluidHeight());
|
assertEquals(63, placer.getFluidHeight());
|
||||||
assertEquals(67, placer.getFluidHeight(12, -7));
|
assertEquals(67, placer.getFluidHeight(12, -7));
|
||||||
verify(riverHead).get(12, -7);
|
verify(waterHead).get(12, -7);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
package art.arcane.iris.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||||
|
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||||
|
import art.arcane.iris.studio.map.MapWindow;
|
||||||
|
import art.arcane.iris.studio.map.PaletteExplorerStart;
|
||||||
|
import art.arcane.iris.studio.view.GuiHost;
|
||||||
|
import art.arcane.iris.world.IrisToolbelt;
|
||||||
|
import art.arcane.iris.world.task.J;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.ArgumentMatchers.same;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class StudioMapPositionTest {
|
||||||
|
@Test
|
||||||
|
public void mapCapturesItsStartingPositionOnThePlayerScheduler() {
|
||||||
|
IrisSettings previous = IrisSettings.settings;
|
||||||
|
IrisSettings.settings = new IrisSettings();
|
||||||
|
IrisSettings.settings.getGui().setUseServerLaunchedGuis(true);
|
||||||
|
World world = mock(World.class);
|
||||||
|
Player player = mock(Player.class);
|
||||||
|
VolmitSender sender = mock(VolmitSender.class);
|
||||||
|
PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class);
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
UUID playerId = UUID.randomUUID();
|
||||||
|
UUID worldId = UUID.randomUUID();
|
||||||
|
AtomicBoolean owned = new AtomicBoolean();
|
||||||
|
AtomicReference<Runnable> task = new AtomicReference<>();
|
||||||
|
AtomicReference<PaletteExplorerStart> start = new AtomicReference<>();
|
||||||
|
when(world.getUID()).thenReturn(worldId);
|
||||||
|
when(player.getUniqueId()).thenReturn(playerId);
|
||||||
|
when(player.isOnline()).thenReturn(true);
|
||||||
|
when(player.getWorld()).thenAnswer(invocation -> {
|
||||||
|
assertTrue("world read must use the player scheduler", owned.get());
|
||||||
|
return world;
|
||||||
|
});
|
||||||
|
when(player.getLocation()).thenAnswer(invocation -> {
|
||||||
|
assertTrue("location read must use the player scheduler", owned.get());
|
||||||
|
return new Location(world, 33.25D, 91D, -72.5D);
|
||||||
|
});
|
||||||
|
when(sender.isPlayer()).thenReturn(true);
|
||||||
|
when(sender.player()).thenReturn(player);
|
||||||
|
when(generator.getEngine()).thenReturn(engine);
|
||||||
|
CommandStudio command = new CommandStudio() {
|
||||||
|
@Override
|
||||||
|
public VolmitSender sender() {
|
||||||
|
return sender;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
try (MockedStatic<GuiHost> host = mockStatic(GuiHost.class);
|
||||||
|
MockedStatic<IrisToolbelt> toolbelt = mockStatic(IrisToolbelt.class);
|
||||||
|
MockedStatic<J> scheduling = mockStatic(J.class);
|
||||||
|
MockedStatic<MapWindow> windows = mockStatic(MapWindow.class)) {
|
||||||
|
host.when(GuiHost::isAvailable).thenReturn(true);
|
||||||
|
toolbelt.when(() -> IrisToolbelt.isIrisWorld(world)).thenReturn(true);
|
||||||
|
toolbelt.when(() -> IrisToolbelt.access(world)).thenReturn(generator);
|
||||||
|
scheduling.when(() -> J.runEntity(same(player), any(Runnable.class), eq(0), any(Runnable.class)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
task.set(invocation.getArgument(1));
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
windows.when(() -> MapWindow.openMap(any(PaletteExplorerStart.class))).thenAnswer(invocation -> {
|
||||||
|
start.set(invocation.getArgument(0));
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
command.map(world);
|
||||||
|
assertNull(start.get());
|
||||||
|
owned.set(true);
|
||||||
|
task.get().run();
|
||||||
|
|
||||||
|
assertEquals(engine, start.get().engine());
|
||||||
|
assertEquals(playerId, start.get().playerId());
|
||||||
|
assertEquals(worldId, start.get().worldId());
|
||||||
|
assertEquals(33.25D, start.get().centerX(), 0D);
|
||||||
|
assertEquals(-72.5D, start.get().centerZ(), 0D);
|
||||||
|
} finally {
|
||||||
|
IrisSettings.settings = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
-45
@@ -9,16 +9,18 @@ import art.arcane.iris.configuration.IrisSettings;
|
|||||||
import art.arcane.iris.generation.biome.IrisBiome;
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
import art.arcane.iris.generation.runtime.BiomeEnvironment;
|
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.generation.runtime.IrisComplex;
|
||||||
|
import art.arcane.iris.generation.terrain.InferredType;
|
||||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||||
import art.arcane.iris.pack.loading.IrisData;
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
|
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureRef;
|
import art.arcane.iris.api.terrain.IrisColumnSample;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
|
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||||
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.generator.ChunkGenerator;
|
import org.bukkit.generator.ChunkGenerator;
|
||||||
@@ -26,6 +28,7 @@ import org.junit.Test;
|
|||||||
import org.mockito.MockedStatic;
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -112,10 +115,32 @@ public class IrisTerrainSVCTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void acceptedHydrologyMapsToThePublicDiagnosticStates() {
|
public void waterRolesMapToThePublicRiverFields() {
|
||||||
assertEquals(IrisRiverState.NONE, IrisTerrainSVC.riverState(null));
|
for (WaterRole role : WaterRole.values()) {
|
||||||
assertEquals(IrisRiverState.WET, IrisTerrainSVC.riverState(river(true)));
|
assertEquals(role.name(), role.wet() ? IrisRiverState.WET : IrisRiverState.NONE, IrisTerrainSVC.riverState(role));
|
||||||
assertEquals(IrisRiverState.DRY, IrisTerrainSVC.riverState(river(false)));
|
double distance = switch (role) {
|
||||||
|
case NONE -> IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE;
|
||||||
|
case CHANNEL, POND, MOUTH, CASCADE, WATERFALL, INLET -> 0D;
|
||||||
|
case SHORE, POND_SHORE -> 1D;
|
||||||
|
case BANK, DAM -> 2D;
|
||||||
|
};
|
||||||
|
assertEquals(role.name(), distance, IrisTerrainSVC.riverDistance(role), 0D);
|
||||||
|
int flow = switch (role) {
|
||||||
|
case NONE, SHORE, POND_SHORE, BANK, DAM -> IrisColumnSample.UNAVAILABLE_RIVER_FLOW;
|
||||||
|
case CHANNEL, MOUTH, CASCADE, WATERFALL -> 1;
|
||||||
|
case POND, INLET -> 0;
|
||||||
|
};
|
||||||
|
assertEquals(role.name(), flow, IrisTerrainSVC.riverFlow(role));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void columnSamplesReportTheWaterRoleOfEachColumn() throws Exception {
|
||||||
|
assertEquals(List.of("RIVER", "WET", "0.0", "1", "6"), sampled(WaterRole.CHANNEL, 70));
|
||||||
|
assertEquals(List.of("RIVER", "WET", "0.0", "0", "6"), sampled(WaterRole.POND, 70));
|
||||||
|
assertEquals(List.of("RIVER_SHORE", "NONE", "1.0", "-1", "unavailable"), sampled(WaterRole.SHORE, 50));
|
||||||
|
assertEquals(List.of("LAND", "NONE", "2.0", "-1", "unavailable"), sampled(WaterRole.BANK, 50));
|
||||||
|
assertEquals(List.of("LAND", "NONE", "NaN", "-1", "unavailable"), sampled(WaterRole.NONE, 50));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -219,6 +244,33 @@ public class IrisTerrainSVCTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<String> sampled(WaterRole role, int head) throws Exception {
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
IrisComplex complex = mock(IrisComplex.class);
|
||||||
|
IrisBiome biome = mock(IrisBiome.class);
|
||||||
|
when(biome.getInferredType()).thenReturn(InferredType.LAND);
|
||||||
|
when(engine.getComplex()).thenReturn(complex);
|
||||||
|
when(engine.getDimension()).thenReturn(mock(IrisDimension.class));
|
||||||
|
when(engine.getHeight(0, 0)).thenReturn(60);
|
||||||
|
when(engine.getMinHeight()).thenReturn(-64);
|
||||||
|
when(engine.getSurfaceBiome(0, 0)).thenReturn(biome);
|
||||||
|
when(complex.getWaterRoleStream()).thenReturn(ProceduralStream.of((x, z) -> role, Interpolated.of(value -> 0D, value -> role)));
|
||||||
|
when(complex.getWaterHeadStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> (double) head));
|
||||||
|
IrisColumnQuery query = IrisColumnQuery.rect(0, 0, 0, 0, 1, EnumSet.of(IrisColumnField.SURFACE_KIND,
|
||||||
|
IrisColumnField.RIVER_STATE, IrisColumnField.RIVER_DISTANCE, IrisColumnField.RIVER_FLOW,
|
||||||
|
IrisColumnField.RIVER_WATER_SURFACE_Y));
|
||||||
|
List<IrisColumnSample> samples = new ArrayList<>();
|
||||||
|
try (MockedStatic<IrisSettings> settings = mockStatic(IrisSettings.class)) {
|
||||||
|
settings.when(IrisSettings::get).thenReturn(new IrisSettings());
|
||||||
|
assertTrue(enabledService().sampleColumns(world(engine), query, samples::add));
|
||||||
|
}
|
||||||
|
assertEquals(1, samples.size());
|
||||||
|
IrisColumnSample sample = samples.get(0);
|
||||||
|
return List.of(sample.surfaceKind().name(), sample.riverState().name(), String.valueOf(sample.riverDistance()),
|
||||||
|
String.valueOf(sample.riverFlow()),
|
||||||
|
sample.riverWaterSurfaceY() == IrisColumnSample.UNAVAILABLE_HEIGHT ? "unavailable" : String.valueOf(sample.riverWaterSurfaceY()));
|
||||||
|
}
|
||||||
|
|
||||||
private static World world(Engine engine) {
|
private static World world(Engine engine) {
|
||||||
World world = mock(World.class);
|
World world = mock(World.class);
|
||||||
ChunkGenerator generator = mock(ChunkGenerator.class, withSettings().extraInterfaces(PlatformChunkGenerator.class));
|
ChunkGenerator generator = mock(ChunkGenerator.class, withSettings().extraInterfaces(PlatformChunkGenerator.class));
|
||||||
@@ -235,41 +287,4 @@ public class IrisTerrainSVCTest {
|
|||||||
((AtomicBoolean) enabled.get(service)).set(true);
|
((AtomicBoolean) enabled.get(service)).set(true);
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HydrologyColumnSample river(boolean connectedFluid) {
|
|
||||||
HydrologyFeatureRef feature = new HydrologyFeatureRef(
|
|
||||||
1L,
|
|
||||||
HydrologyFeatureType.SURFACE_POOL,
|
|
||||||
2L,
|
|
||||||
3L,
|
|
||||||
0,
|
|
||||||
63,
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
HydrologyColumnLayer layer = new HydrologyColumnLayer(
|
|
||||||
feature,
|
|
||||||
60,
|
|
||||||
63,
|
|
||||||
63,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
connectedFluid,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
connectedFluid,
|
|
||||||
false,
|
|
||||||
"water",
|
|
||||||
"river",
|
|
||||||
"mouth",
|
|
||||||
"shore",
|
|
||||||
"dry",
|
|
||||||
"cave"
|
|
||||||
);
|
|
||||||
return new HydrologyColumnSample(0, 0, 70, 63, false, "parent", List.of(layer));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+18
-93
@@ -1,15 +1,10 @@
|
|||||||
package art.arcane.iris.platform.bukkit.terrain;
|
package art.arcane.iris.platform.bukkit.terrain;
|
||||||
|
|
||||||
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
|
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureRef;
|
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
|
|
||||||
import art.arcane.iris.generation.terrain.InferredType;
|
import art.arcane.iris.generation.terrain.InferredType;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
@@ -69,98 +64,28 @@ public class IrisSurfaceClassifierTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void acceptedSurfaceRolesOverrideGenericOceanAndLandKinds() {
|
public void waterRolesOverrideGenericOceanAndLandKinds() {
|
||||||
assertEquals(IrisSurfaceKind.RIVER, IrisSurfaceClassifier.classify(
|
for (WaterRole role : WaterRole.values()) {
|
||||||
60,
|
IrisSurfaceKind expected = switch (role) {
|
||||||
63,
|
case NONE -> IrisSurfaceKind.OCEAN;
|
||||||
InferredType.SEA,
|
case SHORE, POND_SHORE -> IrisSurfaceKind.RIVER_SHORE;
|
||||||
hydrology(HydrologyFeatureType.SURFACE_POOL, true, false, true)
|
case BANK, DAM -> IrisSurfaceKind.LAND;
|
||||||
));
|
case CHANNEL, POND, MOUTH, CASCADE, WATERFALL, INLET -> IrisSurfaceKind.RIVER;
|
||||||
assertEquals(IrisSurfaceKind.RIVER, IrisSurfaceClassifier.classify(
|
};
|
||||||
60,
|
assertEquals(role.name(), expected, IrisSurfaceClassifier.classify(60, 63, InferredType.SEA, role));
|
||||||
63,
|
}
|
||||||
InferredType.SEA,
|
|
||||||
hydrology(HydrologyFeatureType.MOUTH, true, false, true)
|
|
||||||
));
|
|
||||||
assertEquals(IrisSurfaceKind.RIVER_SHORE, IrisSurfaceClassifier.classify(
|
|
||||||
64,
|
|
||||||
63,
|
|
||||||
InferredType.SHORE,
|
|
||||||
hydrology(HydrologyFeatureType.SURFACE_POOL, false, true, false)
|
|
||||||
));
|
|
||||||
assertEquals(IrisSurfaceKind.DRY_CHANNEL, IrisSurfaceClassifier.classify(
|
|
||||||
60,
|
|
||||||
60,
|
|
||||||
InferredType.LAND,
|
|
||||||
hydrology(HydrologyFeatureType.SURFACE_POOL, true, false, false)
|
|
||||||
));
|
|
||||||
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(
|
|
||||||
60,
|
|
||||||
60,
|
|
||||||
InferredType.LAND,
|
|
||||||
hydrology(HydrologyFeatureType.SURFACE_POOL, false, false, false)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void voidClassificationWinsOverAcceptedRiverGeometry() {
|
public void voidClassificationWinsOverWaterRoles() {
|
||||||
assertEquals(IrisSurfaceKind.VOID, IrisSurfaceClassifier.classify(
|
for (WaterRole role : WaterRole.values()) {
|
||||||
0,
|
assertEquals(role.name(), IrisSurfaceKind.VOID, IrisSurfaceClassifier.classify(0, 63, InferredType.LAND, role));
|
||||||
63,
|
}
|
||||||
InferredType.LAND,
|
|
||||||
hydrology(HydrologyFeatureType.SURFACE_POOL, true, false, true)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void absentRejectedCandidatesDoNotCreatePublicRiverKinds() {
|
public void columnsWithoutAWaterRoleKeepTheGenericKinds() {
|
||||||
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(
|
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(64, 63, InferredType.LAND, WaterRole.NONE));
|
||||||
64,
|
assertEquals(IrisSurfaceKind.SHORE, IrisSurfaceClassifier.classify(64, 63, InferredType.SHORE, WaterRole.NONE));
|
||||||
63,
|
|
||||||
InferredType.LAND,
|
|
||||||
null
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static HydrologyColumnSample hydrology(
|
|
||||||
HydrologyFeatureType type,
|
|
||||||
boolean channel,
|
|
||||||
boolean shore,
|
|
||||||
boolean connectedFluid
|
|
||||||
) {
|
|
||||||
HydrologyFeatureRef feature = new HydrologyFeatureRef(
|
|
||||||
1L,
|
|
||||||
type,
|
|
||||||
2L,
|
|
||||||
3L,
|
|
||||||
0,
|
|
||||||
63,
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
HydrologyColumnLayer layer = new HydrologyColumnLayer(
|
|
||||||
feature,
|
|
||||||
60,
|
|
||||||
63,
|
|
||||||
63,
|
|
||||||
channel,
|
|
||||||
shore,
|
|
||||||
!channel && !shore,
|
|
||||||
connectedFluid,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
connectedFluid && channel,
|
|
||||||
false,
|
|
||||||
"water",
|
|
||||||
"river",
|
|
||||||
"mouth",
|
|
||||||
"shore",
|
|
||||||
"dry",
|
|
||||||
"cave"
|
|
||||||
);
|
|
||||||
return new HydrologyColumnSample(0, 0, 70, 63, false, "parent", List.of(layer));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+165
@@ -0,0 +1,165 @@
|
|||||||
|
package art.arcane.iris.studio.view;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||||
|
import art.arcane.iris.world.IrisWorld;
|
||||||
|
import art.arcane.iris.world.task.J;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.ArgumentMatchers.same;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class BukkitOpenerPositionTest {
|
||||||
|
@Test
|
||||||
|
public void playerMarkersAreCapturedOnTheirEntitySchedulers() {
|
||||||
|
try (Harness harness = new Harness()) {
|
||||||
|
harness.binding.when(() -> BukkitWorldBinding.players(harness.target)).thenReturn(List.of(harness.player));
|
||||||
|
when(harness.player.getName()).thenReturn("Explorer");
|
||||||
|
assertTrue(harness.overlay.players().isEmpty());
|
||||||
|
|
||||||
|
harness.runEntity();
|
||||||
|
|
||||||
|
List<GuiMarker> markers = harness.overlay.players();
|
||||||
|
assertEquals(1, markers.size());
|
||||||
|
assertEquals("Explorer", markers.getFirst().label());
|
||||||
|
assertEquals(18.5D, markers.getFirst().worldX(), 0D);
|
||||||
|
assertEquals(-4.25D, markers.getFirst().worldZ(), 0D);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void positionIsCapturedOnlyOnTheOwningEntityTask() {
|
||||||
|
try (Harness harness = new Harness()) {
|
||||||
|
harness.overlay.requestOpenerPosition(harness.results::add);
|
||||||
|
assertTrue(harness.results.isEmpty());
|
||||||
|
|
||||||
|
harness.runEntity();
|
||||||
|
|
||||||
|
assertEquals(List.of(OpenerPosition.inWorld(18.5D, 72D, -4.25D)), harness.results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void retiredEntityCompletesTheRequestOnce() {
|
||||||
|
try (Harness harness = new Harness()) {
|
||||||
|
harness.overlay.requestOpenerPosition(harness.results::add);
|
||||||
|
assertTrue("retirement callback must be registered", harness.retired != null);
|
||||||
|
|
||||||
|
harness.retired.run();
|
||||||
|
harness.retired.run();
|
||||||
|
|
||||||
|
assertEquals(List.of(OpenerPosition.OFFLINE), harness.results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void disconnectBeforeCaptureCompletesAsOfflineAndReconnectCanRefresh() {
|
||||||
|
try (Harness harness = new Harness()) {
|
||||||
|
harness.overlay.requestOpenerPosition(harness.results::add);
|
||||||
|
when(harness.player.isOnline()).thenReturn(false);
|
||||||
|
harness.runEntity();
|
||||||
|
assertEquals(List.of(OpenerPosition.OFFLINE), harness.results);
|
||||||
|
|
||||||
|
when(harness.player.isOnline()).thenReturn(true);
|
||||||
|
harness.overlay.requestOpenerPosition(harness.results::add);
|
||||||
|
harness.runEntity();
|
||||||
|
assertEquals(List.of(OpenerPosition.OFFLINE, OpenerPosition.inWorld(18.5D, 72D, -4.25D)), harness.results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectedEntityTaskCompletesOnceEvenIfRetirementAlreadyRan() {
|
||||||
|
try (Harness harness = new Harness()) {
|
||||||
|
harness.reject = true;
|
||||||
|
harness.overlay.requestOpenerPosition(harness.results::add);
|
||||||
|
assertEquals(List.of(OpenerPosition.OFFLINE), harness.results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void changingWorldBeforeCaptureReportsOtherWorld() {
|
||||||
|
try (Harness harness = new Harness()) {
|
||||||
|
harness.overlay.requestOpenerPosition(harness.results::add);
|
||||||
|
when(harness.player.getWorld()).thenReturn(mock(World.class));
|
||||||
|
harness.runEntity();
|
||||||
|
assertEquals(List.of(OpenerPosition.IN_OTHER_WORLD), harness.results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Harness implements AutoCloseable {
|
||||||
|
private final Player player = mock(Player.class);
|
||||||
|
private final World world = mock(World.class);
|
||||||
|
private final Engine engine = mock(Engine.class);
|
||||||
|
private final IrisWorld target = mock(IrisWorld.class);
|
||||||
|
private final UUID opener = UUID.randomUUID();
|
||||||
|
private final List<OpenerPosition> results = new ArrayList<>();
|
||||||
|
private final MockedStatic<J> scheduling = mockStatic(J.class);
|
||||||
|
private final MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||||
|
private final MockedStatic<BukkitWorldBinding> binding = mockStatic(BukkitWorldBinding.class);
|
||||||
|
private final BukkitVisionOverlay overlay = new BukkitVisionOverlay(engine, opener);
|
||||||
|
private Runnable task;
|
||||||
|
private Runnable retired;
|
||||||
|
private boolean entityOwned;
|
||||||
|
private boolean reject;
|
||||||
|
|
||||||
|
private Harness() {
|
||||||
|
when(engine.getWorld()).thenReturn(target);
|
||||||
|
when(player.isOnline()).thenReturn(true);
|
||||||
|
when(player.getWorld()).thenReturn(world);
|
||||||
|
when(player.getLocation()).thenAnswer(invocation -> {
|
||||||
|
assertTrue("location read outside owning entity task", entityOwned);
|
||||||
|
return new Location(world, 18.5D, 72D, -4.25D);
|
||||||
|
});
|
||||||
|
bukkit.when(() -> Bukkit.getPlayer(opener)).thenReturn(player);
|
||||||
|
binding.when(() -> BukkitWorldBinding.world(target)).thenReturn(world);
|
||||||
|
scheduling.when(() -> J.runGlobal(any(Runnable.class))).thenAnswer(invocation -> {
|
||||||
|
invocation.getArgument(0, Runnable.class).run();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
scheduling.when(() -> J.runEntity(same(player), any(Runnable.class))).thenAnswer(invocation -> {
|
||||||
|
task = invocation.getArgument(1);
|
||||||
|
return !reject;
|
||||||
|
});
|
||||||
|
scheduling.when(() -> J.runEntity(same(player), any(Runnable.class), eq(0), any(Runnable.class)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
task = invocation.getArgument(1);
|
||||||
|
retired = invocation.getArgument(3);
|
||||||
|
if (reject) {
|
||||||
|
retired.run();
|
||||||
|
}
|
||||||
|
return !reject;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runEntity() {
|
||||||
|
entityOwned = true;
|
||||||
|
try {
|
||||||
|
task.run();
|
||||||
|
} finally {
|
||||||
|
entityOwned = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
binding.close();
|
||||||
|
bukkit.close();
|
||||||
|
scheduling.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+55
@@ -11,12 +11,16 @@ import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
|||||||
import org.bukkit.NamespacedKey;
|
import org.bukkit.NamespacedKey;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
@@ -42,4 +46,55 @@ public class PendingWorldReplacementAdmissionTest {
|
|||||||
executor.awaitTermination(5L, TimeUnit.SECONDS);
|
executor.awaitTermination(5L, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shutdownRejectsReplacementsThatStartAfterIt() {
|
||||||
|
PendingWorldReplacementManager manager = new PendingWorldReplacementManager(mock(Iris.class));
|
||||||
|
manager.awaitStagingForShutdown();
|
||||||
|
|
||||||
|
IOException rejected = assertThrows(IOException.class, () -> manager.stageReplacement(
|
||||||
|
mock(VolmitSender.class), NamespacedKey.minecraft("overworld"), mock(IrisDimension.class), null));
|
||||||
|
|
||||||
|
assertTrue(rejected.getMessage().contains("shutting down"));
|
||||||
|
assertTrue(LifecycleOperationCoordinator.get().isIdle());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shutdownWaitsForTheReplacementBeingStaged() throws Exception {
|
||||||
|
PendingWorldReplacementManager manager = new PendingWorldReplacementManager(mock(Iris.class));
|
||||||
|
AtomicReference<Throwable> stagingFailure = new AtomicReference<>();
|
||||||
|
Thread staging = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
manager.stageReplacement(mock(VolmitSender.class), NamespacedKey.minecraft("overworld"),
|
||||||
|
mock(IrisDimension.class), null);
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
stagingFailure.set(failure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Thread shutdown = new Thread(manager::awaitStagingForShutdown);
|
||||||
|
synchronized (manager) {
|
||||||
|
staging.start();
|
||||||
|
awaitState(staging, Thread.State.BLOCKED);
|
||||||
|
shutdown.start();
|
||||||
|
awaitState(shutdown, Thread.State.WAITING);
|
||||||
|
assertTrue(staging.isAlive());
|
||||||
|
}
|
||||||
|
shutdown.join(TimeUnit.SECONDS.toMillis(10L));
|
||||||
|
staging.join(TimeUnit.SECONDS.toMillis(10L));
|
||||||
|
|
||||||
|
assertFalse(shutdown.isAlive());
|
||||||
|
assertFalse(staging.isAlive());
|
||||||
|
assertTrue(stagingFailure.get() instanceof Exception);
|
||||||
|
assertTrue(LifecycleOperationCoordinator.get().isIdle());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void awaitState(Thread thread, Thread.State state) throws InterruptedException {
|
||||||
|
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
|
||||||
|
while (thread.getState() != state) {
|
||||||
|
if (System.nanoTime() > deadline) {
|
||||||
|
throw new AssertionError(thread.getName() + " never reached " + state + "; it is " + thread.getState());
|
||||||
|
}
|
||||||
|
Thread.sleep(5L);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -58,10 +58,11 @@ IrisFileOperations irisFileOperations = objects.newInstance(IrisFileOperations)
|
|||||||
|
|
||||||
apply from: file('../../gradle/modded-generation-build-revision.gradle')
|
apply from: file('../../gradle/modded-generation-build-revision.gradle')
|
||||||
apply from: file('../../gradle/volmlib-stamp.gradle')
|
apply from: file('../../gradle/volmlib-stamp.gradle')
|
||||||
|
apply from: file('../../gradle/java21-runtime-exclusions.gradle')
|
||||||
|
|
||||||
Properties rootProperties = new Properties()
|
Properties rootProperties = new Properties()
|
||||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.4'))
|
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.5'))
|
||||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
||||||
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(irisModTarget.fabricLoader.toString())
|
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(irisModTarget.fabricLoader.toString())
|
||||||
Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
||||||
@@ -130,6 +131,8 @@ configurations.named('bundle').configure {
|
|||||||
exclude(group: 'de.crazydev22.slimjar.helper')
|
exclude(group: 'de.crazydev22.slimjar.helper')
|
||||||
exclude(group: 'de.crazydev22.slimjar')
|
exclude(group: 'de.crazydev22.slimjar')
|
||||||
exclude(group: 'io.github.slimjar')
|
exclude(group: 'io.github.slimjar')
|
||||||
|
// The game ships its own LWJGL; without the modules it leaves out, the map's 3D views draw in software.
|
||||||
|
exclude(group: 'org.lwjgl')
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations.compileClasspath.extendsFrom(configurations.devBundle)
|
configurations.compileClasspath.extendsFrom(configurations.devBundle)
|
||||||
@@ -152,7 +155,7 @@ dependencies {
|
|||||||
String modConfiguration = irisModTarget.fabricRemap ? 'modImplementation' : 'implementation'
|
String modConfiguration = irisModTarget.fabricRemap ? 'modImplementation' : 'implementation'
|
||||||
add(modConfiguration, "net.fabricmc:fabric-loader:${fabricLoaderVersion}")
|
add(modConfiguration, "net.fabricmc:fabric-loader:${fabricLoaderVersion}")
|
||||||
testImplementation('junit:junit:4.13.2')
|
testImplementation('junit:junit:4.13.2')
|
||||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
testImplementation(libs.mockito.core)
|
||||||
testImplementation(testFixtures('art.arcane:core'))
|
testImplementation(testFixtures('art.arcane:core'))
|
||||||
testImplementation(libs.spigot) {
|
testImplementation(libs.spigot) {
|
||||||
transitive = false
|
transitive = false
|
||||||
@@ -215,7 +218,9 @@ dependencies {
|
|||||||
libs.caffeine,
|
libs.caffeine,
|
||||||
'com.fulcrumgenomics:jlibdeflate:0.2.0',
|
'com.fulcrumgenomics:jlibdeflate:0.2.0',
|
||||||
libs.dom4j,
|
libs.dom4j,
|
||||||
libs.jaxen
|
libs.jaxen,
|
||||||
|
libs.flatlaf,
|
||||||
|
libs.flatlaf.extras
|
||||||
]
|
]
|
||||||
shared.each { Object notation ->
|
shared.each { Object notation ->
|
||||||
['bundle', 'devBundle'].each { String configuration ->
|
['bundle', 'devBundle'].each { String configuration ->
|
||||||
@@ -235,6 +240,8 @@ tasks.named('compileJava', JavaCompile).configure {
|
|||||||
options.release.set((int) irisModTarget.java)
|
options.release.set((int) irisModTarget.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply from: file('../../gradle/mockito-agent.gradle')
|
||||||
|
|
||||||
tasks.named('test').configure {
|
tasks.named('test').configure {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,8 +329,7 @@ tasks.named('shadowJar', ShadowJar).configure {
|
|||||||
configurations = [project.configurations.named('bundle').get()]
|
configurations = [project.configurations.named('bundle').get()]
|
||||||
mergeServiceFiles()
|
mergeServiceFiles()
|
||||||
if ((int) irisModTarget.java == 21) {
|
if ((int) irisModTarget.java == 21) {
|
||||||
exclude('art/arcane/volmlib/util/noise/NativeBillowLoader*.class')
|
exclude(java21RuntimeExclusions)
|
||||||
exclude('art/arcane/volmlib/util/noise/BillowBatch*.class')
|
|
||||||
}
|
}
|
||||||
exclude('META-INF/maven/**')
|
exclude('META-INF/maven/**')
|
||||||
exclude('META-INF/proguard/**')
|
exclude('META-INF/proguard/**')
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ plugins {
|
|||||||
|
|
||||||
rootProject.name = 'iris-fabric'
|
rootProject.name = 'iris-fabric'
|
||||||
|
|
||||||
|
apply from: new File(settingsDir, '../../gradle/runtime-cache.settings.gradle').canonicalFile
|
||||||
|
|
||||||
dependencyResolutionManagement {
|
dependencyResolutionManagement {
|
||||||
versionCatalogs {
|
versionCatalogs {
|
||||||
libs {
|
libs {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||||
|
import net.minecraftforge.gradle.shadow.net.minecraftforge.gradleutils.shared.EnhancedPlugin
|
||||||
import org.gradle.api.file.DeleteSpec
|
import org.gradle.api.file.DeleteSpec
|
||||||
import org.gradle.api.file.FileCopyDetails
|
import org.gradle.api.file.FileCopyDetails
|
||||||
import org.gradle.api.file.FileSystemOperations
|
import org.gradle.api.file.FileSystemOperations
|
||||||
@@ -56,12 +57,24 @@ IrisFileOperations irisFileOperations = objects.newInstance(IrisFileOperations)
|
|||||||
|
|
||||||
apply from: file('../../gradle/modded-generation-build-revision.gradle')
|
apply from: file('../../gradle/modded-generation-build-revision.gradle')
|
||||||
apply from: file('../../gradle/volmlib-stamp.gradle')
|
apply from: file('../../gradle/volmlib-stamp.gradle')
|
||||||
|
apply from: file('../../gradle/java21-runtime-exclusions.gradle')
|
||||||
|
|
||||||
Properties rootProperties = new Properties()
|
Properties rootProperties = new Properties()
|
||||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.4'))
|
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.5'))
|
||||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
||||||
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(irisModTarget.forge.toString())
|
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(irisModTarget.forge.toString())
|
||||||
|
String buildLane = gradle.extensions.extraProperties.has('irisBuildLane')
|
||||||
|
? gradle.extensions.extraProperties.get('irisBuildLane').toString() : null
|
||||||
|
if (buildLane != null) {
|
||||||
|
File laneCache = providers.gradleProperty('multiplexorSandbox').getOrElse('false').toBoolean()
|
||||||
|
? new File(gradle.gradleUserHomeDir, "caches/iris-forge/${buildLane}")
|
||||||
|
: file(".gradle/iris-lanes/${buildLane}")
|
||||||
|
plugins.withType(EnhancedPlugin).configureEach { EnhancedPlugin<?> forgePlugin ->
|
||||||
|
forgePlugin.globalCaches().set(new File(laneCache, 'forgegradle'))
|
||||||
|
}
|
||||||
|
minecraft.mavenizerOutput.set(new File(laneCache, 'mavenizer/repo'))
|
||||||
|
}
|
||||||
Closure<String> loaderDisplayVersion = { String loaderVersion ->
|
Closure<String> loaderDisplayVersion = { String loaderVersion ->
|
||||||
String coordinatePrefix = "${minecraftVersion}-"
|
String coordinatePrefix = "${minecraftVersion}-"
|
||||||
if (loaderVersion.startsWith(coordinatePrefix)) {
|
if (loaderVersion.startsWith(coordinatePrefix)) {
|
||||||
@@ -139,12 +152,16 @@ configurations.named('bundle').configure {
|
|||||||
exclude(group: 'de.crazydev22.slimjar.helper')
|
exclude(group: 'de.crazydev22.slimjar.helper')
|
||||||
exclude(group: 'de.crazydev22.slimjar')
|
exclude(group: 'de.crazydev22.slimjar')
|
||||||
exclude(group: 'io.github.slimjar')
|
exclude(group: 'io.github.slimjar')
|
||||||
|
// The game ships its own LWJGL; without the modules it leaves out, the map's 3D views draw in software.
|
||||||
|
exclude(group: 'org.lwjgl')
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations.named('runBundle').configure {
|
configurations.named('runBundle').configure {
|
||||||
exclude(group: 'de.crazydev22.slimjar.helper')
|
exclude(group: 'de.crazydev22.slimjar.helper')
|
||||||
exclude(group: 'de.crazydev22.slimjar')
|
exclude(group: 'de.crazydev22.slimjar')
|
||||||
exclude(group: 'io.github.slimjar')
|
exclude(group: 'io.github.slimjar')
|
||||||
|
// The game ships its own LWJGL; without the modules it leaves out, the map's 3D views draw in software.
|
||||||
|
exclude(group: 'org.lwjgl')
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations.compileClasspath.extendsFrom(configurations.devBundle)
|
configurations.compileClasspath.extendsFrom(configurations.devBundle)
|
||||||
@@ -161,9 +178,11 @@ configurations.testRuntimeClasspath.extendsFrom(configurations.runtimeClasspath,
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
|
implementation(buildLane == null
|
||||||
|
? minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
|
||||||
|
: minecraft.dependency("iris-${buildLane}", "net.minecraftforge:forge:${forgeVersion}"))
|
||||||
testImplementation('junit:junit:4.13.2')
|
testImplementation('junit:junit:4.13.2')
|
||||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
testImplementation(libs.mockito.core)
|
||||||
testImplementation(testFixtures('art.arcane:core'))
|
testImplementation(testFixtures('art.arcane:core'))
|
||||||
testImplementation(libs.spigot) {
|
testImplementation(libs.spigot) {
|
||||||
transitive = false
|
transitive = false
|
||||||
@@ -200,7 +219,9 @@ dependencies {
|
|||||||
libs.caffeine,
|
libs.caffeine,
|
||||||
'com.fulcrumgenomics:jlibdeflate:0.2.0',
|
'com.fulcrumgenomics:jlibdeflate:0.2.0',
|
||||||
libs.dom4j,
|
libs.dom4j,
|
||||||
libs.jaxen
|
libs.jaxen,
|
||||||
|
libs.flatlaf,
|
||||||
|
libs.flatlaf.extras
|
||||||
]
|
]
|
||||||
shared.each { Object notation ->
|
shared.each { Object notation ->
|
||||||
['bundle', 'devBundle', 'runBundle'].each { String configuration ->
|
['bundle', 'devBundle', 'runBundle'].each { String configuration ->
|
||||||
@@ -257,6 +278,8 @@ tasks.named('compileJava', JavaCompile).configure {
|
|||||||
options.release.set((int) irisModTarget.java)
|
options.release.set((int) irisModTarget.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply from: file('../../gradle/mockito-agent.gradle')
|
||||||
|
|
||||||
tasks.named('test').configure {
|
tasks.named('test').configure {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,8 +318,7 @@ tasks.named('shadowJar', ShadowJar).configure {
|
|||||||
from(sourceSets.main.output)
|
from(sourceSets.main.output)
|
||||||
mergeServiceFiles()
|
mergeServiceFiles()
|
||||||
if ((int) irisModTarget.java == 21) {
|
if ((int) irisModTarget.java == 21) {
|
||||||
exclude('art/arcane/volmlib/util/noise/NativeBillowLoader*.class')
|
exclude(java21RuntimeExclusions)
|
||||||
exclude('art/arcane/volmlib/util/noise/BillowBatch*.class')
|
|
||||||
}
|
}
|
||||||
exclude('META-INF/maven/**')
|
exclude('META-INF/maven/**')
|
||||||
exclude('META-INF/proguard/**')
|
exclude('META-INF/proguard/**')
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ plugins {
|
|||||||
|
|
||||||
rootProject.name = 'iris-forge'
|
rootProject.name = 'iris-forge'
|
||||||
|
|
||||||
|
apply from: new File(settingsDir, '../../gradle/runtime-cache.settings.gradle').canonicalFile
|
||||||
|
|
||||||
dependencyResolutionManagement {
|
dependencyResolutionManagement {
|
||||||
versionCatalogs {
|
versionCatalogs {
|
||||||
libs {
|
libs {
|
||||||
|
|||||||
+4
-2
@@ -57,6 +57,8 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedChunkGenera
|
|||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeGeneratorContext;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeGeneratorContext;
|
||||||
|
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||||
|
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||||
|
import art.arcane.volmlib.util.matter.Matter;
|
||||||
|
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedStructureStage;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedStructureStage;
|
||||||
import art.arcane.iris.world.safeguard.GenerationRefusalNotice;
|
import art.arcane.iris.world.safeguard.GenerationRefusalNotice;
|
||||||
@@ -829,12 +831,12 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
|
|||||||
importedFeatures.invalidate();
|
importedFeatures.invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateRegeneratedChunk(int chunkX, int chunkZ) {
|
public void updateRegeneratedChunk(Mantle<Matter> mantle, int chunkX, int chunkZ) {
|
||||||
ModdedChunkUpdateService service = ModdedEngineBootstrap.services().service(ModdedChunkUpdateService.class);
|
ModdedChunkUpdateService service = ModdedEngineBootstrap.services().service(ModdedChunkUpdateService.class);
|
||||||
if (service == null) {
|
if (service == null) {
|
||||||
throw new IllegalStateException("Iris chunk update service is unavailable during regeneration");
|
throw new IllegalStateException("Iris chunk update service is unavailable during regeneration");
|
||||||
}
|
}
|
||||||
service.updateRegeneratedChunk(engine(), boundLevel(), chunkX, chunkZ);
|
service.updateRegeneratedChunk(engine(), boundLevel(), mantle, chunkX, chunkZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
public NativeBiomeResolver regenBiomeResolver() {
|
public NativeBiomeResolver regenBiomeResolver() {
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ public final class ModdedEngineBootstrap {
|
|||||||
failure = runStopStage(failure, "wand service", ModdedWandService::clearAll);
|
failure = runStopStage(failure, "wand service", ModdedWandService::clearAll);
|
||||||
failure = runStopStage(failure, "block break handler", ModdedBlockBreakHandler::clear);
|
failure = runStopStage(failure, "block break handler", ModdedBlockBreakHandler::clear);
|
||||||
failure = runStopStage(failure, "studio commands", ModdedStudioCommands::clear);
|
failure = runStopStage(failure, "studio commands", ModdedStudioCommands::clear);
|
||||||
|
failure = runStopStage(failure, "gui windows", GuiHost::shutdown);
|
||||||
failure = runStopStage(failure, "gui host", ModdedGuiHost::clear);
|
failure = runStopStage(failure, "gui host", ModdedGuiHost::clear);
|
||||||
failure = runStopStage(failure, "services", () -> services().disableAll());
|
failure = runStopStage(failure, "services", () -> services().disableAll());
|
||||||
reportStopFailure("stopping", failure);
|
reportStopFailure("stopping", failure);
|
||||||
|
|||||||
+12
@@ -2,6 +2,7 @@ package art.arcane.iris.modded;
|
|||||||
|
|
||||||
import art.arcane.volmlib.nativelib.terrain.structure.StructureStagePolicy;
|
import art.arcane.volmlib.nativelib.terrain.structure.StructureStagePolicy;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
|
||||||
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
|
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
|
||||||
import art.arcane.iris.structure.nativegen.GenerationWritePolicy;
|
import art.arcane.iris.structure.nativegen.GenerationWritePolicy;
|
||||||
import art.arcane.iris.structure.nativegen.IrisStructurePolicy;
|
import art.arcane.iris.structure.nativegen.IrisStructurePolicy;
|
||||||
@@ -13,6 +14,7 @@ import art.arcane.iris.structure.nativegen.StructureLocateSearch;
|
|||||||
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedStructureStage;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedStructureStage;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureVolumeSource;
|
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureVolumeSource;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockPositionPredicate;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockPositionPredicate;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
import art.arcane.volmlib.nativelib.terrain.structure.StructureLocateSearchAccess;
|
import art.arcane.volmlib.nativelib.terrain.structure.StructureLocateSearchAccess;
|
||||||
@@ -107,6 +109,16 @@ final class ModdedNativeStructurePolicy implements NativeModdedStructureStage.Po
|
|||||||
context.getDimensionStackContext(), context.getMinHeight());
|
context.getDimensionStackContext(), context.getMinHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NativeBlockChangeSet trackBlockChanges(Engine context, NativeBlockChangeSet.Bounds bounds) {
|
||||||
|
return DeferredPlacementRecords.track(context, bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void invalidateBlockChanges(Engine context, NativeBlockChangeSet changes) {
|
||||||
|
DeferredPlacementRecords.invalidate(context, changes);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IntBinaryOperator surfaceHeight(Engine context) {
|
public IntBinaryOperator surfaceHeight(Engine context) {
|
||||||
return (x, z) -> Engine.hostHeight(context, x, z, true) + context.getMinHeight();
|
return (x, z) -> Engine.hostHeight(context, x, z, true) + context.getMinHeight();
|
||||||
|
|||||||
+25
-4
@@ -19,6 +19,8 @@
|
|||||||
package art.arcane.iris.modded.api;
|
package art.arcane.iris.modded.api;
|
||||||
|
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||||
|
import art.arcane.iris.integration.Identifier;
|
||||||
import art.arcane.iris.modded.ModdedIrisLog;
|
import art.arcane.iris.modded.ModdedIrisLog;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockPlacement;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockPlacement;
|
||||||
@@ -297,11 +299,11 @@ public final class ModdedCustomContentRegistry {
|
|||||||
* consulted for that position. An unparseable key or no matching provider is logged and skipped. Called on the
|
* consulted for that position. An unparseable key or no matching provider is logged and skipped. Called on the
|
||||||
* server thread with the chunk loaded.
|
* server thread with the chunk loaded.
|
||||||
*/
|
*/
|
||||||
public static void processBlockPlacement(Engine engine, String key, Supplier<NativeBlockPlacement> placement) {
|
public static CustomBlockOwnership.Entry processBlockPlacement(Engine engine, String key, Supplier<NativeBlockPlacement> placement) {
|
||||||
String base = parseIdentifier(key);
|
String base = parseIdentifier(key);
|
||||||
if (base == null) {
|
if (base == null) {
|
||||||
ModdedIrisLog.warn("Iris deferred custom block placement rejected invalid id {}", key);
|
ModdedIrisLog.warn("Iris deferred custom block placement rejected invalid id {}", key);
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
Map<String, String> state = parseState(key);
|
Map<String, String> state = parseState(key);
|
||||||
for (ModdedDataProvider provider : PROVIDERS) {
|
for (ModdedDataProvider provider : PROVIDERS) {
|
||||||
@@ -309,14 +311,33 @@ public final class ModdedCustomContentRegistry {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
provider.processBlockPlacement(new ModdedBlockPlacementContext(
|
String token = provider.processBlockPlacement(new ModdedBlockPlacementContext(
|
||||||
engine, placement.get(), base, state));
|
engine, placement.get(), base, state));
|
||||||
|
int properties = key.indexOf('[');
|
||||||
|
String qualifiedKey = base.replace(':', '/') + (properties < 0 ? "" : key.substring(properties));
|
||||||
|
return token == null ? null : new CustomBlockOwnership.Entry(
|
||||||
|
new Identifier(provider.modId(), qualifiedKey), token, false);
|
||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
ModdedIrisLog.error("Iris custom content provider '{}' failed post-placement for {}", provider.modId(), key, error);
|
ModdedIrisLog.error("Iris custom content provider '{}' failed post-placement for {}", provider.modId(), key, error);
|
||||||
}
|
}
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
ModdedIrisLog.warn("Iris deferred custom block placement has no provider for {}", key);
|
ModdedIrisLog.warn("Iris deferred custom block placement has no provider for {}", key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean removeGenerated(Engine engine, CustomBlockOwnership.Entry entry,
|
||||||
|
Supplier<NativeBlockPlacement> placement) {
|
||||||
|
for (ModdedDataProvider provider : PROVIDERS) {
|
||||||
|
if (provider.modId().equals(entry.identifier().namespace()) && provider.isReady()) {
|
||||||
|
String storedKey = entry.identifier().key();
|
||||||
|
int separator = storedKey.indexOf('/');
|
||||||
|
String key = storedKey.substring(0, separator) + ":" + storedKey.substring(separator + 1);
|
||||||
|
return provider.removeGenerated(new ModdedBlockPlacementContext(
|
||||||
|
engine, placement.get(), parseIdentifier(key), parseState(key)), entry.token());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+6
-1
@@ -95,7 +95,12 @@ public interface ModdedDataProvider {
|
|||||||
* Called on the server thread, once per deferred position, by the first provider that claims the identifier -
|
* Called on the server thread, once per deferred position, by the first provider that claims the identifier -
|
||||||
* later providers are not consulted for that position. Only fires for states returned as deferred.
|
* later providers are not consulted for that position. Only fires for states returned as deferred.
|
||||||
*/
|
*/
|
||||||
default void processBlockPlacement(ModdedBlockPlacementContext context) {
|
default String processBlockPlacement(ModdedBlockPlacementContext context) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
default boolean removeGenerated(ModdedBlockPlacementContext context, String token) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+2
-20
@@ -25,7 +25,7 @@ import art.arcane.iris.generation.runtime.Engine;
|
|||||||
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;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery;
|
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||||
import art.arcane.iris.world.history.GenerationFindCatalog;
|
import art.arcane.iris.world.history.GenerationFindCatalog;
|
||||||
import art.arcane.iris.generation.biome.IrisBiome;
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
||||||
@@ -61,7 +61,7 @@ final class ModdedCommandSuggestions {
|
|||||||
static final SuggestionProvider<NativeCommandSource> OBJECT_KEYS = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestObjectKeys(context, builder);
|
static final SuggestionProvider<NativeCommandSource> OBJECT_KEYS = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestObjectKeys(context, builder);
|
||||||
static final SuggestionProvider<NativeCommandSource> STRUCTURE_KEYS = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestStructureKeys(context, builder);
|
static final SuggestionProvider<NativeCommandSource> STRUCTURE_KEYS = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestStructureKeys(context, builder);
|
||||||
static final SuggestionProvider<NativeCommandSource> POI_TYPES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> NativeCommandRegistration.suggest(List.of("buried_treasure"), builder);
|
static final SuggestionProvider<NativeCommandSource> POI_TYPES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> NativeCommandRegistration.suggest(List.of("buried_treasure"), builder);
|
||||||
static final SuggestionProvider<NativeCommandSource> HYDROLOGY_TYPES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestHydrologyTypes(context, builder);
|
static final SuggestionProvider<NativeCommandSource> HYDROLOGY_TYPES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> NativeCommandRegistration.suggest(HydrologyFeatureType.keys(), builder);
|
||||||
static final SuggestionProvider<NativeCommandSource> PACK_NAMES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestPackNames(context, builder);
|
static final SuggestionProvider<NativeCommandSource> PACK_NAMES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestPackNames(context, builder);
|
||||||
static final SuggestionProvider<NativeCommandSource> DIMENSION_NAMES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestDimensionNames(context, builder);
|
static final SuggestionProvider<NativeCommandSource> DIMENSION_NAMES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestDimensionNames(context, builder);
|
||||||
|
|
||||||
@@ -87,24 +87,6 @@ final class ModdedCommandSuggestions {
|
|||||||
return builder.buildFuture();
|
return builder.buildFuture();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CompletableFuture<Suggestions> suggestHydrologyTypes(
|
|
||||||
CommandContext<NativeCommandSource> context,
|
|
||||||
SuggestionsBuilder builder
|
|
||||||
) {
|
|
||||||
ModdedCommandFeedback.tab(context.getSource());
|
|
||||||
try {
|
|
||||||
Engine engine = IrisModdedCommands.engineFor(context.getSource().world());
|
|
||||||
if (engine != null && engine.getComplex().getHydrologyRuntime() != null) {
|
|
||||||
return NativeCommandRegistration.suggest(
|
|
||||||
engine.getComplex().getHydrologyRuntime().featureQueryKeys(), builder);
|
|
||||||
}
|
|
||||||
} catch (Throwable error) {
|
|
||||||
warnTabFailure("hydrology types", context.getSource(), error);
|
|
||||||
}
|
|
||||||
return NativeCommandRegistration.suggest(
|
|
||||||
HydrologyFeatureQuery.suggestions(List.of()), builder);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Set<String> reachableBiomeKeys(Engine engine) {
|
static Set<String> reachableBiomeKeys(Engine engine) {
|
||||||
return reachableBiomeKeys(GenerationFindCatalog.biomes(engine));
|
return reachableBiomeKeys(GenerationFindCatalog.biomes(engine));
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-22
@@ -39,8 +39,7 @@ import art.arcane.iris.structure.placement.StructureReachability;
|
|||||||
import art.arcane.iris.generation.runtime.WrongEngineBroException;
|
import art.arcane.iris.generation.runtime.WrongEngineBroException;
|
||||||
import art.arcane.iris.world.history.GenerationSemanticQueries;
|
import art.arcane.iris.world.history.GenerationSemanticQueries;
|
||||||
import art.arcane.iris.world.history.GenerationFindCatalog;
|
import art.arcane.iris.world.history.GenerationFindCatalog;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery;
|
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
|
||||||
import art.arcane.iris.generation.biome.IrisBiome;
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
||||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||||
@@ -143,39 +142,31 @@ final class ModdedLocateCommands {
|
|||||||
IrisModdedCommands.fail(source, "This dimension is not generated by Iris.");
|
IrisModdedCommands.fail(source, "This dimension is not generated by Iris.");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
IrisHydrologyRuntime runtime = engine.getComplex().getHydrologyRuntime();
|
HydrologyFeatureType featureType;
|
||||||
HydrologyFeatureQuery query;
|
|
||||||
try {
|
try {
|
||||||
query = HydrologyFeatureQuery.parse(type);
|
featureType = HydrologyFeatureType.parse(type);
|
||||||
} catch (IllegalArgumentException error) {
|
} catch (IllegalArgumentException error) {
|
||||||
IrisModdedCommands.fail(source, error.getMessage());
|
IrisModdedCommands.fail(source, error.getMessage());
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
String label = featureType.key().replace('_', ' ');
|
||||||
int originX = player.blockX();
|
int originX = player.blockX();
|
||||||
int originZ = player.blockZ();
|
int originZ = player.blockZ();
|
||||||
int requestedDistance = runtime == null
|
int maximumDistance = GenerationSemanticQueries.RIVER_SEARCH_DISTANCE;
|
||||||
? 8192
|
|
||||||
: Math.min(8192, runtime.settings().routing().tileSize() * 15);
|
|
||||||
int maximumDistance = runtime == null
|
|
||||||
? requestedDistance
|
|
||||||
: runtime.maximumFeatureSearchDistance(originX, originZ, requestedDistance);
|
|
||||||
NativeModdedServer server = source.server();
|
NativeModdedServer server = source.server();
|
||||||
IrisModdedCommands.ok(source, "Searching accepted hydrology plans for " + type + "...");
|
IrisModdedCommands.ok(source, GenerationSemanticQueries.riverSearchMessage(featureType, maximumDistance));
|
||||||
MultiBurst.burst.completeValueAsync(() -> GenerationSemanticQueries.nearestRiver(
|
MultiBurst.burst.completeValueAsync(() -> GenerationSemanticQueries.nearestRiver(
|
||||||
engine, query, originX, originZ, maximumDistance,
|
engine, featureType, originX, originZ, maximumDistance).orElse(null))
|
||||||
(int visited) -> server.execute(() -> IrisModdedCommands.ok(source,
|
|
||||||
"Searched " + visited + " hydrology tiles for " + type + "..."))).orElse(null))
|
|
||||||
.whenComplete((GenerationSemanticQueries.RiverResult feature, Throwable error) -> server.execute(() -> {
|
.whenComplete((GenerationSemanticQueries.RiverResult feature, Throwable error) -> server.execute(() -> {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
ModdedIrisLog.error("Hydrology locate failed for {}", type, error);
|
Throwable cause = error instanceof CompletionException && error.getCause() != null ? error.getCause() : error;
|
||||||
IrisModdedCommands.fail(source,
|
ModdedIrisLog.error("Hydrology locate failed for {}", featureType.key(), cause);
|
||||||
"Could not locate " + type + " hydrology: " + error.getClass().getSimpleName());
|
IrisModdedCommands.fail(source, "Could not search for " + label + ": " + cause.getClass().getSimpleName()
|
||||||
|
+ (cause.getMessage() == null ? "" : " - " + cause.getMessage()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (feature == null) {
|
if (feature == null) {
|
||||||
IrisModdedCommands.fail(source,
|
IrisModdedCommands.fail(source, GenerationSemanticQueries.riverNotFoundMessage(featureType, maximumDistance));
|
||||||
"No accepted " + type + " hydrology feature found within "
|
|
||||||
+ maximumDistance + " blocks.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
teleportToStructure(
|
teleportToStructure(
|
||||||
@@ -185,7 +176,7 @@ final class ModdedLocateCommands {
|
|||||||
feature.x(),
|
feature.x(),
|
||||||
feature.y() + 2,
|
feature.y() + 2,
|
||||||
feature.z(),
|
feature.z(),
|
||||||
type + " hydrology feature"
|
label
|
||||||
);
|
);
|
||||||
}));
|
}));
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
+26
-4
@@ -23,6 +23,7 @@ import art.arcane.volmlib.nativelib.terrain.NativePregenRuntime;
|
|||||||
import art.arcane.volmlib.nativelib.terrain.NativePregenRuntime.ChunkLoad;
|
import art.arcane.volmlib.nativelib.terrain.NativePregenRuntime.ChunkLoad;
|
||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
import art.arcane.iris.world.pregen.MantleHeapPressure;
|
import art.arcane.iris.world.pregen.MantleHeapPressure;
|
||||||
|
import art.arcane.iris.world.pregen.PregenHydrology;
|
||||||
import art.arcane.iris.world.pregen.PregenListener;
|
import art.arcane.iris.world.pregen.PregenListener;
|
||||||
import art.arcane.iris.world.pregen.PregenMantleBackpressure;
|
import art.arcane.iris.world.pregen.PregenMantleBackpressure;
|
||||||
import art.arcane.iris.world.pregen.PregeneratorMethod;
|
import art.arcane.iris.world.pregen.PregeneratorMethod;
|
||||||
@@ -30,6 +31,8 @@ import art.arcane.iris.generation.runtime.Engine;
|
|||||||
import art.arcane.iris.modded.ModdedGenPool;
|
import art.arcane.iris.modded.ModdedGenPool;
|
||||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.Semaphore;
|
import java.util.concurrent.Semaphore;
|
||||||
@@ -39,6 +42,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.function.LongSupplier;
|
||||||
|
|
||||||
public final class ModdedPregenMethod implements PregeneratorMethod {
|
public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||||
private static final int ADAPTIVE_TIMEOUT_STEP = 3;
|
private static final int ADAPTIVE_TIMEOUT_STEP = 3;
|
||||||
@@ -68,10 +72,19 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
private final int timeoutSeconds;
|
private final int timeoutSeconds;
|
||||||
private final PregenMantleBackpressure backpressure;
|
private final PregenMantleBackpressure backpressure;
|
||||||
private final PauseWhenEmptyGuard pauseGuard;
|
private final PauseWhenEmptyGuard pauseGuard;
|
||||||
|
private final LongSupplier clock;
|
||||||
|
private final AtomicBoolean closing = new AtomicBoolean();
|
||||||
|
private final PregenHydrology hydrology;
|
||||||
|
|
||||||
public ModdedPregenMethod(Configuration configuration) {
|
public ModdedPregenMethod(Configuration configuration) {
|
||||||
|
this(configuration, System::nanoTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
ModdedPregenMethod(Configuration configuration, LongSupplier clock) {
|
||||||
|
this.clock = Objects.requireNonNull(clock);
|
||||||
runtime = configuration.runtime();
|
runtime = configuration.runtime();
|
||||||
engine = configuration.engine();
|
engine = configuration.engine();
|
||||||
|
hydrology = new PregenHydrology(() -> engine);
|
||||||
sync = configuration.sync();
|
sync = configuration.sync();
|
||||||
pauseGuard = new PauseWhenEmptyGuard(runtime);
|
pauseGuard = new PauseWhenEmptyGuard(runtime);
|
||||||
IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen();
|
IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen();
|
||||||
@@ -113,6 +126,8 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
|
closing.set(true);
|
||||||
|
hydrology.close();
|
||||||
try {
|
try {
|
||||||
awaitPendingChunks();
|
awaitPendingChunks();
|
||||||
ModdedIrisLog.info("Iris modded pregen closing: dim={} completed={} pending={} peakInFlight={} finalLimit={}",
|
ModdedIrisLog.info("Iris modded pregen closing: dim={} completed={} pending={} peakInFlight={} finalLimit={}",
|
||||||
@@ -178,7 +193,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
if (sync || runtime.isServerThread()) {
|
if (sync || runtime.isServerThread()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(timeoutSeconds)
|
long deadline = clock.getAsLong() + TimeUnit.SECONDS.toNanos(timeoutSeconds)
|
||||||
+ TimeUnit.MILLISECONDS.toNanos(FINAL_SAVE_TIMEOUT_MILLIS);
|
+ TimeUnit.MILLISECONDS.toNanos(FINAL_SAVE_TIMEOUT_MILLIS);
|
||||||
boolean interrupted = false;
|
boolean interrupted = false;
|
||||||
try {
|
try {
|
||||||
@@ -186,7 +201,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
if (finalSaveOnServerThread.get() || !runtime.isRunning()) {
|
if (finalSaveOnServerThread.get() || !runtime.isRunning()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (System.nanoTime() >= deadline) {
|
if (clock.getAsLong() >= deadline) {
|
||||||
throw new IllegalStateException("Iris pregen chunk callbacks did not finish for "
|
throw new IllegalStateException("Iris pregen chunk callbacks did not finish for "
|
||||||
+ runtime.worldIdentity() + ": " + inFlight.get() + " pending");
|
+ runtime.worldIdentity() + ": " + inFlight.get() + " pending");
|
||||||
}
|
}
|
||||||
@@ -263,14 +278,14 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void awaitFinalSave(FinalSaveRequest request) {
|
private void awaitFinalSave(FinalSaveRequest request) {
|
||||||
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(FINAL_SAVE_TIMEOUT_MILLIS);
|
long deadline = clock.getAsLong() + TimeUnit.MILLISECONDS.toNanos(FINAL_SAVE_TIMEOUT_MILLIS);
|
||||||
boolean interrupted = false;
|
boolean interrupted = false;
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
if (deferFinalSaveIfRequested()) {
|
if (deferFinalSaveIfRequested()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
long remainingNanos = deadline - System.nanoTime();
|
long remainingNanos = deadline - clock.getAsLong();
|
||||||
if (remainingNanos <= 0L) {
|
if (remainingNanos <= 0L) {
|
||||||
ModdedIrisLog.warn("Iris pregen level save did not complete in time for {}", runtime.worldIdentity());
|
ModdedIrisLog.warn("Iris pregen level save did not complete in time for {}", runtime.worldIdentity());
|
||||||
return;
|
return;
|
||||||
@@ -333,6 +348,13 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRegionsAhead(List<long[]> regions) {
|
||||||
|
if (!closing.get()) {
|
||||||
|
hydrology.prepare(regions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getMethod(int x, int z) {
|
public String getMethod(int x, int z) {
|
||||||
return "Modded";
|
return "Modded";
|
||||||
|
|||||||
+19
-2
@@ -22,6 +22,7 @@ import art.arcane.iris.modded.ModdedIrisLog;
|
|||||||
import art.arcane.iris.world.WorldMaintenance;
|
import art.arcane.iris.world.WorldMaintenance;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.generation.mantle.EngineMantle;
|
import art.arcane.iris.generation.mantle.EngineMantle;
|
||||||
|
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
|
||||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||||
import art.arcane.iris.modded.ModdedBlockBuffer;
|
import art.arcane.iris.modded.ModdedBlockBuffer;
|
||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
@@ -36,6 +37,9 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandSource;
|
|||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeChunkRegeneration;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeChunkRegeneration;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||||
|
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||||
|
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||||
|
import art.arcane.volmlib.util.matter.Matter;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
@@ -110,10 +114,12 @@ public final class ModdedRegen {
|
|||||||
|
|
||||||
private void resetMantleMargin() {
|
private void resetMantleMargin() {
|
||||||
EngineMantle engineMantle = engine.getMantle();
|
EngineMantle engineMantle = engine.getMantle();
|
||||||
|
Mantle<Matter> mantle = engineMantle.getMantle();
|
||||||
int margin = radius + Math.max(engineMantle.getRadius(), engineMantle.getRealRadius()) + 1;
|
int margin = radius + Math.max(engineMantle.getRadius(), engineMantle.getRealRadius()) + 1;
|
||||||
for (int dx = -margin; dx <= margin; dx++) {
|
for (int dx = -margin; dx <= margin; dx++) {
|
||||||
for (int dz = -margin; dz <= margin; dz++) {
|
for (int dz = -margin; dz <= margin; dz++) {
|
||||||
engineMantle.getMantle().deleteChunk(centerX + dx, centerZ + dz);
|
DeferredPlacementRecords.resetMantleChunk(mantle, centerX + dx, centerZ + dz,
|
||||||
|
Math.abs(dx) <= radius && Math.abs(dz) <= radius);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -199,10 +205,21 @@ public final class ModdedRegen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void apply(int chunkX, int chunkZ, ModdedBlockBuffer blocks, Hunk<NativeBiome> biomes) {
|
private void apply(int chunkX, int chunkZ, ModdedBlockBuffer blocks, Hunk<NativeBiome> biomes) {
|
||||||
|
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||||
regeneration.apply(new NativeChunkRegeneration.Replacement(chunkX, chunkZ,
|
regeneration.apply(new NativeChunkRegeneration.Replacement(chunkX, chunkZ,
|
||||||
engine.getMinHeight(), engine.getMaxHeight() - engine.getMinHeight(),
|
engine.getMinHeight(), engine.getMaxHeight() - engine.getMinHeight(),
|
||||||
blocks::rawOrNull, generator.regenBiomeResolver()),
|
blocks::rawOrNull, generator.regenBiomeResolver()),
|
||||||
() -> generator.updateRegeneratedChunk(chunkX, chunkZ));
|
() -> completeReplacement(mantle, chunkX, chunkZ));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void completeReplacement(Mantle<Matter> mantle, int chunkX, int chunkZ) {
|
||||||
|
MantleChunk<Matter> records = mantle.useChunk(chunkX, chunkZ);
|
||||||
|
try {
|
||||||
|
DeferredPlacementRecords.invalidateCustomBlockOwnership(records);
|
||||||
|
} finally {
|
||||||
|
records.release();
|
||||||
|
}
|
||||||
|
generator.updateRegeneratedChunk(mantle, chunkX, chunkZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ok(String message) {
|
private void ok(String message) {
|
||||||
|
|||||||
+19
-9
@@ -23,10 +23,9 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldView;
|
|||||||
|
|
||||||
import art.arcane.iris.modded.ModdedIrisLog;
|
import art.arcane.iris.modded.ModdedIrisLog;
|
||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
|
import art.arcane.iris.studio.map.MapWindow;
|
||||||
|
import art.arcane.iris.studio.map.PaletteExplorerStart;
|
||||||
import art.arcane.iris.studio.view.GuiHost;
|
import art.arcane.iris.studio.view.GuiHost;
|
||||||
import art.arcane.iris.studio.view.ImageMapStudioGUI;
|
|
||||||
import art.arcane.iris.studio.view.NoiseExplorerGUI;
|
|
||||||
import art.arcane.iris.studio.view.VisionGUI;
|
|
||||||
import art.arcane.iris.pack.loading.IrisData;
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
import art.arcane.iris.pack.StructurePackageClosure;
|
import art.arcane.iris.pack.StructurePackageClosure;
|
||||||
import art.arcane.iris.pack.ImageMapPackageClosure;
|
import art.arcane.iris.pack.ImageMapPackageClosure;
|
||||||
@@ -38,6 +37,7 @@ import art.arcane.iris.generation.biome.IrisBiome;
|
|||||||
import art.arcane.iris.generation.biome.IrisBiomeGeneratorLink;
|
import art.arcane.iris.generation.biome.IrisBiomeGeneratorLink;
|
||||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
import art.arcane.iris.world.entity.IrisEntitySpawn;
|
import art.arcane.iris.world.entity.IrisEntitySpawn;
|
||||||
|
import art.arcane.iris.world.IrisWorld;
|
||||||
import art.arcane.iris.generation.noise.IrisGenerator;
|
import art.arcane.iris.generation.noise.IrisGenerator;
|
||||||
import art.arcane.iris.pack.PackExportClosure;
|
import art.arcane.iris.pack.PackExportClosure;
|
||||||
import art.arcane.iris.world.entity.IrisEntity;
|
import art.arcane.iris.world.entity.IrisEntity;
|
||||||
@@ -247,8 +247,9 @@ public final class ModdedStudioCommands {
|
|||||||
NativeProtocolPlayer player = source.player();
|
NativeProtocolPlayer player = source.player();
|
||||||
ModdedGuiHost.bindContext(worldView(source.server(), level), engine, player == null ? null : player.id());
|
ModdedGuiHost.bindContext(worldView(source.server(), level), engine, player == null ? null : player.id());
|
||||||
}
|
}
|
||||||
if (generatorKey == null || generatorKey.isBlank()) {
|
Engine mapEngine = engine != null ? engine : GuiHost.get().findActiveEngine();
|
||||||
NoiseExplorerGUI.launch();
|
if ((generatorKey == null || generatorKey.isBlank()) && mapEngine != null) {
|
||||||
|
MapWindow.openNoise(mapStart(mapEngine, source.player()), new MapWindow.NoiseSelection(null, null, seed));
|
||||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_NOISE_EXPLORER_ON_SERVER_DISPLAY));
|
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_NOISE_EXPLORER_ON_SERVER_DISPLAY));
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -262,7 +263,7 @@ public final class ModdedStudioCommands {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
String selectedGeneratorKey = generatorKey.trim();
|
String selectedGeneratorKey = generatorKey.trim();
|
||||||
NoiseExplorerGUI.launchGeneratorKey(selectedGeneratorKey, generator, seed);
|
MapWindow.openNoise(mapStart(engine, source.player()), new MapWindow.NoiseSelection(selectedGeneratorKey, generator, seed));
|
||||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_NOISE_EXPLORER_GENERATOR_SEED, MessageArgument.untrusted("value", generatorKey.trim()), MessageArgument.untrusted("seed", seed)));
|
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_NOISE_EXPLORER_GENERATOR_SEED, MessageArgument.untrusted("value", generatorKey.trim()), MessageArgument.untrusted("seed", seed)));
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -280,11 +281,20 @@ public final class ModdedStudioCommands {
|
|||||||
}
|
}
|
||||||
NativeProtocolPlayer player = source.player();
|
NativeProtocolPlayer player = source.player();
|
||||||
ModdedGuiHost.bindContext(worldView(source.server(), level), engine, player == null ? null : player.id());
|
ModdedGuiHost.bindContext(worldView(source.server(), level), engine, player == null ? null : player.id());
|
||||||
VisionGUI.launch(engine, player == null ? null : player.id());
|
MapWindow.openMap(mapStart(engine, player));
|
||||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_VISION_MAP_ON_SERVER_DISPLAY, MessageArgument.untrusted("value", level.name())));
|
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_VISION_MAP_ON_SERVER_DISPLAY, MessageArgument.untrusted("value", level.name())));
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Modded worlds have no Bukkit world id; the map finds the engine again through the bound GUI context.
|
||||||
|
private static PaletteExplorerStart mapStart(Engine engine, NativeProtocolPlayer player) {
|
||||||
|
IrisWorld world = engine.getWorld();
|
||||||
|
NativeWorld level = world == null ? null : world.platformWorld();
|
||||||
|
boolean inWorld = player != null && player.connected() && level != null && player.inWorld(level);
|
||||||
|
return new PaletteExplorerStart(engine, null, player == null ? null : player.id(), null,
|
||||||
|
inWorld ? player.x() : 0D, inWorld ? player.z() : 0D);
|
||||||
|
}
|
||||||
|
|
||||||
private static int imageMap(NativeCommandSource source) {
|
private static int imageMap(NativeCommandSource source) {
|
||||||
NativeWorld level = source.world();
|
NativeWorld level = source.world();
|
||||||
Engine engine = IrisModdedCommands.engineFor(level);
|
Engine engine = IrisModdedCommands.engineFor(level);
|
||||||
@@ -294,13 +304,13 @@ public final class ModdedStudioCommands {
|
|||||||
));
|
));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (!ImageMapStudioGUI.isAvailable()) {
|
if (!GuiHost.isAvailable() || !IrisSettings.get().getGui().isUseServerLaunchedGuis()) {
|
||||||
IrisModdedCommands.fail(source, guiUnavailableMessage());
|
IrisModdedCommands.fail(source, guiUnavailableMessage());
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
NativeProtocolPlayer player = source.player();
|
NativeProtocolPlayer player = source.player();
|
||||||
ModdedGuiHost.bindContext(worldView(source.server(), level), engine, player == null ? null : player.id());
|
ModdedGuiHost.bindContext(worldView(source.server(), level), engine, player == null ? null : player.id());
|
||||||
ImageMapStudioGUI.launch(engine);
|
MapWindow.openImageMaps(mapStart(engine, player));
|
||||||
IrisModdedCommands.ok(source, IrisLanguage.plain(
|
IrisModdedCommands.ok(source, IrisLanguage.plain(
|
||||||
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_IMAGE_MAP_STUDIO_ON_SERVER_DISPLAY,
|
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_IMAGE_MAP_STUDIO_ON_SERVER_DISPLAY,
|
||||||
MessageArgument.untrusted("value", level.name())
|
MessageArgument.untrusted("value", level.name())
|
||||||
|
|||||||
+48
-26
@@ -18,21 +18,22 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded.command;
|
package art.arcane.iris.modded.command;
|
||||||
|
|
||||||
import art.arcane.iris.studio.view.GuiHost;
|
import art.arcane.iris.studio.view.GuiMarker;
|
||||||
import art.arcane.volmlib.nativelib.view.WorldMarker;
|
|
||||||
import art.arcane.iris.studio.view.GuiOverlay;
|
import art.arcane.iris.studio.view.GuiOverlay;
|
||||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
import art.arcane.iris.studio.view.OpenerPosition;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.studio.render.RenderType;
|
|
||||||
import art.arcane.iris.modded.ModdedIrisLog;
|
import art.arcane.iris.modded.ModdedIrisLog;
|
||||||
import art.arcane.iris.modded.ModdedTeleportDeadline;
|
import art.arcane.iris.modded.ModdedTeleportDeadline;
|
||||||
|
import art.arcane.iris.world.IrisWorld;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||||
|
import art.arcane.volmlib.nativelib.view.WorldView;
|
||||||
|
|
||||||
import java.awt.Desktop;
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import art.arcane.volmlib.nativelib.view.WorldView;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public final class ModdedVisionOverlay implements GuiOverlay {
|
public final class ModdedVisionOverlay implements GuiOverlay {
|
||||||
@@ -46,14 +47,33 @@ public final class ModdedVisionOverlay implements GuiOverlay {
|
|||||||
opener = context.opener();
|
opener = context.opener();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WorldView doesn't report which way a player faces, so their markers all face south.
|
||||||
@Override
|
@Override
|
||||||
public List<WorldMarker> players() {
|
public List<GuiMarker> players() {
|
||||||
return world.players();
|
return world.players().stream()
|
||||||
|
.map(player -> GuiMarker.player(player.label(), player.worldX(), player.worldZ(), 0F))
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void requestEntities(Consumer<List<WorldMarker>> sink) {
|
public void requestEntities(Consumer<List<GuiMarker>> sink) {
|
||||||
world.requestEntities(sink);
|
world.requestEntities(entities -> sink.accept(entities.stream()
|
||||||
|
.map(entity -> GuiMarker.entity(entity.label(), entity.worldX(), entity.worldY(), entity.worldZ(),
|
||||||
|
entity.health(), entity.maxHealth()))
|
||||||
|
.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void requestOpenerPosition(Consumer<OpenerPosition> sink) {
|
||||||
|
if (opener == null) {
|
||||||
|
sink.accept(OpenerPosition.OFFLINE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
world.execute(() -> sink.accept(openerPosition()));
|
||||||
|
} catch (RejectedExecutionException failure) {
|
||||||
|
sink.accept(OpenerPosition.OFFLINE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -80,23 +100,25 @@ public final class ModdedVisionOverlay implements GuiOverlay {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private OpenerPosition openerPosition() {
|
||||||
public String openInEditor(double worldX, double worldZ, RenderType type) {
|
IrisWorld target = engine.getWorld();
|
||||||
if (!GuiHost.isAvailable() || !Desktop.isDesktopSupported()) {
|
if (target == null || engine.isClosing() || engine.isClosed()) {
|
||||||
return null;
|
return OpenerPosition.OFFLINE;
|
||||||
}
|
}
|
||||||
IrisComplex complex = engine.getComplex();
|
NativeWorld level = target.platformWorld();
|
||||||
File file = switch (type) {
|
if (level == null) {
|
||||||
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT, RIVER ->
|
return OpenerPosition.OFFLINE;
|
||||||
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
|
|
||||||
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
|
|
||||||
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
|
|
||||||
case REGION -> complex.getRegionStream().get(worldX, worldZ).openInVSCode();
|
|
||||||
case CAVE_LAND -> complex.getCaveBiomeStream().get(worldX, worldZ).openInVSCode();
|
|
||||||
default -> null;
|
|
||||||
};
|
|
||||||
return file == null ? null : file.getName();
|
|
||||||
}
|
}
|
||||||
|
NativeProtocolPlayer player = NativeModdedServer.forWorld(level).player(opener);
|
||||||
|
if (player == null || !player.connected()) {
|
||||||
|
return OpenerPosition.OFFLINE;
|
||||||
|
}
|
||||||
|
if (!player.inWorld(level)) {
|
||||||
|
return OpenerPosition.IN_OTHER_WORLD;
|
||||||
|
}
|
||||||
|
return OpenerPosition.inWorld(player.x(), player.y(), player.z());
|
||||||
|
}
|
||||||
|
|
||||||
public record Context(WorldView world, Engine engine, UUID opener) {
|
public record Context(WorldView world, Engine engine, UUID opener) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+85
-20
@@ -28,13 +28,17 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockProperties;
|
|||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
import art.arcane.iris.studio.view.PregeneratorJob;
|
import art.arcane.iris.studio.view.PregeneratorJob;
|
||||||
import art.arcane.iris.integration.Identifier;
|
import art.arcane.iris.integration.Identifier;
|
||||||
|
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||||
|
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
|
||||||
import art.arcane.iris.generation.cache.Cache;
|
import art.arcane.iris.generation.cache.Cache;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.generation.block.TileData;
|
import art.arcane.iris.generation.block.TileData;
|
||||||
|
import art.arcane.iris.generation.decoration.IrisProceduralBlocks;
|
||||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||||
import art.arcane.iris.modded.ModdedBlockResolution;
|
import art.arcane.iris.modded.ModdedBlockResolution;
|
||||||
import art.arcane.iris.modded.ModdedLootApplier;
|
import art.arcane.iris.modded.ModdedLootApplier;
|
||||||
import art.arcane.iris.modded.ModdedTileData;
|
import art.arcane.iris.modded.ModdedTileData;
|
||||||
|
import art.arcane.iris.modded.api.ModdedBlockData;
|
||||||
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
|
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockPlacement;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockPlacement;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
@@ -50,6 +54,8 @@ import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
|||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
@@ -162,8 +168,8 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
|||||||
updatePassChunk(engine, level, (int) chunkKey, (int) (chunkKey >> 32)));
|
updatePassChunk(engine, level, (int) chunkKey, (int) (chunkKey >> 32)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateRegeneratedChunk(Engine engine, NativeWorld level, int chunkX, int chunkZ) {
|
public void updateRegeneratedChunk(Engine engine, NativeWorld level, Mantle<Matter> mantle, int chunkX, int chunkZ) {
|
||||||
updateChunk(engine, level, chunkX, chunkZ, false);
|
updateChunk(engine, level, mantle, chunkX, chunkZ, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -172,7 +178,7 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
|||||||
*/
|
*/
|
||||||
private void updatePassChunk(Engine engine, NativeWorld level, int chunkX, int chunkZ) {
|
private void updatePassChunk(Engine engine, NativeWorld level, int chunkX, int chunkZ) {
|
||||||
try {
|
try {
|
||||||
updateChunk(engine, level, chunkX, chunkZ, true);
|
updateChunk(engine, level, engine.getMantle().getMantle(), chunkX, chunkZ, true);
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
if (engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
|
if (engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
|
||||||
throw failure;
|
throw failure;
|
||||||
@@ -184,7 +190,8 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateChunk(Engine engine, NativeWorld level, int chunkX, int chunkZ, boolean requireNeighbors) {
|
private void updateChunk(Engine engine, NativeWorld level, Mantle<Matter> mantle,
|
||||||
|
int chunkX, int chunkZ, boolean requireNeighbors) {
|
||||||
if (requireNeighbors) {
|
if (requireNeighbors) {
|
||||||
for (int x = -1; x <= 1; x++) {
|
for (int x = -1; x <= 1; x++) {
|
||||||
for (int z = -1; z <= 1; z++) {
|
for (int z = -1; z <= 1; z++) {
|
||||||
@@ -195,11 +202,11 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
|
||||||
if (!mantle.isChunkLoaded(chunkX, chunkZ)) {
|
if (!mantle.isChunkLoaded(chunkX, chunkZ)) {
|
||||||
warmupMantleChunk(mantle, chunkX, chunkZ);
|
warmupMantleChunk(mantle, chunkX, chunkZ);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
DeferredPlacementRecords.removeInvalidated(engine, mantle, chunkX, chunkZ);
|
||||||
if (mantle.hasFlag(chunkX, chunkZ, MantleFlag.ETCHED)) {
|
if (mantle.hasFlag(chunkX, chunkZ, MantleFlag.ETCHED)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -225,30 +232,51 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
|||||||
int baseX = chunkX << 4;
|
int baseX = chunkX << 4;
|
||||||
int baseZ = chunkZ << 4;
|
int baseZ = chunkZ << 4;
|
||||||
int minHeight = engine.getWorld().minHeight();
|
int minHeight = engine.getWorld().minHeight();
|
||||||
materializeDeferredSlice(chunk, TileWrapper.class, () ->
|
materializeDeferredSlice(chunk, TileWrapper.class, () -> {
|
||||||
chunk.iterate(TileWrapper.class, (Integer x, Integer yf, Integer z, TileWrapper v) -> {
|
for (DeferredPlacementRecords.Pending<TileWrapper> record : DeferredPlacementRecords.snapshot(chunk, TileWrapper.class)) {
|
||||||
int y = yf + minHeight;
|
if (!DeferredPlacementRecords.isPending(chunk, TileWrapper.class, record)) {
|
||||||
if (y < level.minHeight() || y >= level.maxHeight() - 1) {
|
continue;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
applyTile(level, baseX + (x & 15), y, baseZ + (z & 15), v.getData());
|
NativeBlockPoint position = record.position();
|
||||||
}));
|
int y = position.y() + minHeight;
|
||||||
|
if (y < level.minHeight() || y >= level.maxHeight() - 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
applyTile(level, baseX + (position.x() & 15), y, baseZ + (position.z() & 15), record.value().getData());
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void runCustomPass(Engine engine, NativeWorld level, int chunkX, int chunkZ, MantleChunk<Matter> chunk) {
|
private void runCustomPass(Engine engine, NativeWorld level, int chunkX, int chunkZ, MantleChunk<Matter> chunk) {
|
||||||
int baseX = chunkX << 4;
|
int baseX = chunkX << 4;
|
||||||
int baseZ = chunkZ << 4;
|
int baseZ = chunkZ << 4;
|
||||||
int minHeight = engine.getWorld().minHeight();
|
int minHeight = engine.getWorld().minHeight();
|
||||||
materializeDeferredSlice(chunk, Identifier.class, () ->
|
StaleCarriers stale = new StaleCarriers();
|
||||||
chunk.iterate(Identifier.class, (Integer x, Integer yf, Integer z, Identifier identifier) -> {
|
materializeDeferredSlice(chunk, Identifier.class, () -> {
|
||||||
int y = yf + minHeight;
|
for (DeferredPlacementRecords.Pending<Identifier> record : DeferredPlacementRecords.snapshot(chunk, Identifier.class)) {
|
||||||
if (y < level.minHeight() || y >= level.maxHeight() - 1) {
|
if (!DeferredPlacementRecords.isPending(chunk, Identifier.class, record)) {
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
NativeBlockPoint position = new NativeBlockPoint(baseX + (x & 15), y, baseZ + (z & 15));
|
NativeBlockPoint local = record.position();
|
||||||
ModdedCustomContentRegistry.processBlockPlacement(engine, identifier.toString(),
|
int y = local.y() + minHeight;
|
||||||
|
if (y < level.minHeight() || y >= level.maxHeight() - 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int blockX = baseX + (local.x() & 15);
|
||||||
|
int blockZ = baseZ + (local.z() & 15);
|
||||||
|
String key = record.value().toString();
|
||||||
|
if (stale.carrierReplaced(key, level.getBlock(blockX, y, blockZ))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
NativeBlockPoint position = new NativeBlockPoint(blockX, y, blockZ);
|
||||||
|
CustomBlockOwnership.Entry ownership = ModdedCustomContentRegistry.processBlockPlacement(engine, key,
|
||||||
() -> NativeWorldMaintenance.placement(level, position));
|
() -> NativeWorldMaintenance.placement(level, position));
|
||||||
}));
|
if (ownership != null) {
|
||||||
|
DeferredPlacementRecords.remember(engine, chunk, local, ownership);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
stale.report(level.name(), chunkX, chunkZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void materializeDeferredSlice(MantleChunk<Matter> chunk, Class<?> sliceType, Runnable materializer) {
|
static void materializeDeferredSlice(MantleChunk<Matter> chunk, Class<?> sliceType, Runnable materializer) {
|
||||||
@@ -386,4 +414,41 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
|||||||
NativeWorldMaintenance.refresh(level, pos);
|
NativeWorldMaintenance.refresh(level, pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class StaleCarriers {
|
||||||
|
private static final String UNCHECKED = "";
|
||||||
|
|
||||||
|
private final Map<String, String> carriers = new HashMap<>();
|
||||||
|
private int skipped;
|
||||||
|
|
||||||
|
private boolean carrierReplaced(String key, NativeBlockState live) {
|
||||||
|
String carrier = carriers.get(key);
|
||||||
|
if (carrier == null) {
|
||||||
|
carrier = carrierKey(key);
|
||||||
|
carriers.put(key, carrier);
|
||||||
|
}
|
||||||
|
if (carrier.equals(UNCHECKED) || carrier.equals(IrisProceduralBlocks.materialKey(live))
|
||||||
|
|| ((carrier.equals("minecraft:fire") || carrier.equals("minecraft:soul_fire")) && live.isAir())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
skipped++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void report(String levelName, int chunkX, int chunkZ) {
|
||||||
|
if (skipped == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IrisLogging.debug("Skipped %d stale custom block records at chunk %d,%d in %s",
|
||||||
|
skipped, chunkX, chunkZ, levelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String carrierKey(String key) {
|
||||||
|
ModdedBlockData resolved = ModdedCustomContentRegistry.resolveBlock(key);
|
||||||
|
if (resolved == null || resolved.state().isAir()) {
|
||||||
|
return UNCHECKED;
|
||||||
|
}
|
||||||
|
return IrisProceduralBlocks.materialKey(resolved.state());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+10
@@ -28,6 +28,10 @@ import art.arcane.iris.modded.ModdedSavedTerrainCapture;
|
|||||||
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import art.arcane.iris.generation.runtime.EnginePlatformHooks;
|
import art.arcane.iris.generation.runtime.EnginePlatformHooks;
|
||||||
|
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||||
|
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockPoint;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldMaintenance;
|
||||||
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
||||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
import art.arcane.iris.generation.terrain.IrisDimensionRuntimeContract;
|
import art.arcane.iris.generation.terrain.IrisDimensionRuntimeContract;
|
||||||
@@ -72,6 +76,12 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
|||||||
private volatile ExecutorService executor;
|
private volatile ExecutorService executor;
|
||||||
private long lastPollAt;
|
private long lastPollAt;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean removeCustomBlock(Engine engine, NativeBlockPoint position, CustomBlockOwnership.Entry entry) {
|
||||||
|
return ModdedCustomContentRegistry.removeGenerated(engine, entry,
|
||||||
|
() -> NativeWorldMaintenance.placement(engine.getWorld().platformWorld(), position));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<Void> flushSavedTerrainCapture(Engine engine) {
|
public CompletableFuture<Void> flushSavedTerrainCapture(Engine engine) {
|
||||||
return ModdedSavedTerrainCapture.flush(engine);
|
return ModdedSavedTerrainCapture.flush(engine);
|
||||||
|
|||||||
+41
-6
@@ -3,6 +3,7 @@ package art.arcane.iris.modded;
|
|||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedNativeStructureWorldgenAccess;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedNativeStructureWorldgenAccess;
|
||||||
|
|
||||||
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 ModdedNativeStructureWorldgenAccessTest {
|
|||||||
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 ModdedNativeStructureWorldgenAccessTest {
|
|||||||
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));
|
||||||
@@ -270,6 +273,37 @@ public class ModdedNativeStructureWorldgenAccessTest {
|
|||||||
assertEquals(1, recording.mutations);
|
assertEquals(1, recording.mutations);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void acceptedWritesAndReportedChangesAreTrackedOutsideProtectedCells() {
|
||||||
|
RecordingDelegate recording = new RecordingDelegate();
|
||||||
|
BlockPos protectedCell = generationCenter().getMiddleBlockPosition(70);
|
||||||
|
WorldGenLevel delegate = recording.world();
|
||||||
|
NativeBlockChangeSet changes = NativeBlockChangeSet.create(
|
||||||
|
ModdedNativeStructureWorldgenAccess.changeBounds(delegate, generationCenter()));
|
||||||
|
ModdedNativeStructureWorldgenAccess access = ModdedNativeStructureWorldgenAccess.create(
|
||||||
|
delegate, generationCenter(),
|
||||||
|
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||||
|
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||||
|
false, true,
|
||||||
|
protectedCell::equals,
|
||||||
|
changes);
|
||||||
|
BlockPos written = protectedCell.above();
|
||||||
|
BlockPos removed = protectedCell.east();
|
||||||
|
BlockPos reported = protectedCell.above(2);
|
||||||
|
|
||||||
|
assertFalse(access.setBlock(protectedCell, Blocks.STONE.defaultBlockState(), 2));
|
||||||
|
assertTrue(access.setBlock(written, Blocks.STONE.defaultBlockState(), 2));
|
||||||
|
assertTrue(access.removeBlock(removed, false));
|
||||||
|
access.recordBlockChange(reported.getX(), reported.getY(), reported.getZ());
|
||||||
|
access.recordBlockChange(protectedCell.getX(), protectedCell.getY(), protectedCell.getZ());
|
||||||
|
|
||||||
|
assertEquals(3, changes.size());
|
||||||
|
assertTrue(changes.contains(written.getX(), written.getY(), written.getZ()));
|
||||||
|
assertTrue(changes.contains(removed.getX(), removed.getY(), removed.getZ()));
|
||||||
|
assertTrue(changes.contains(reported.getX(), reported.getY(), reported.getZ()));
|
||||||
|
assertFalse(changes.contains(protectedCell.getX(), protectedCell.getY(), protectedCell.getZ()));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void statuslessChunkReadsPreserveWorldgenDelegateSemantics() {
|
public void statuslessChunkReadsPreserveWorldgenDelegateSemantics() {
|
||||||
RecordingDelegate recording = new RecordingDelegate();
|
RecordingDelegate recording = new RecordingDelegate();
|
||||||
@@ -319,8 +353,9 @@ public class ModdedNativeStructureWorldgenAccessTest {
|
|||||||
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() {
|
||||||
|
|||||||
+32
@@ -1,5 +1,9 @@
|
|||||||
package art.arcane.iris.modded.api;
|
package art.arcane.iris.modded.api;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.integration.Identifier;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockPlacement;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeEntityRuntime;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeEntityRuntime;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeSpawnedEntity;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeSpawnedEntity;
|
||||||
import net.minecraft.SharedConstants;
|
import net.minecraft.SharedConstants;
|
||||||
@@ -9,6 +13,7 @@ import org.junit.Test;
|
|||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
@@ -20,6 +25,33 @@ import static org.mockito.Mockito.verify;
|
|||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class ModdedCustomContentRegistryTest {
|
public class ModdedCustomContentRegistryTest {
|
||||||
|
@Test
|
||||||
|
public void removalReturnsToOriginalProviderWithTokenAndPlacementState() {
|
||||||
|
ModdedDataProvider provider = mock(ModdedDataProvider.class);
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
NativeBlockPlacement placement = mock(NativeBlockPlacement.class);
|
||||||
|
ModdedBlockPlacementContext context = new ModdedBlockPlacementContext(
|
||||||
|
engine, placement, "provider_test:furniture/chair", Map.of("variant", "oak"));
|
||||||
|
when(provider.modId()).thenReturn("ownership_test");
|
||||||
|
when(provider.isReady()).thenReturn(true);
|
||||||
|
when(provider.isValidProvider("provider_test:furniture/chair", ModdedDataType.BLOCK)).thenReturn(true);
|
||||||
|
when(provider.processBlockPlacement(context)).thenReturn("entity-token");
|
||||||
|
when(provider.removeGenerated(context, "entity-token")).thenReturn(true);
|
||||||
|
ModdedCustomContentRegistry.Discovery discovery = ModdedCustomContentRegistry.discover(List.of(provider));
|
||||||
|
try {
|
||||||
|
CustomBlockOwnership.Entry ownership = ModdedCustomContentRegistry.processBlockPlacement(engine,
|
||||||
|
"provider_test:furniture/chair[variant=oak]", () -> placement);
|
||||||
|
|
||||||
|
assertEquals(new CustomBlockOwnership.Entry(
|
||||||
|
Identifier.fromString("ownership_test:provider_test/furniture/chair[variant=oak]"),
|
||||||
|
"entity-token", false), ownership);
|
||||||
|
assertTrue(ModdedCustomContentRegistry.removeGenerated(engine, ownership, () -> placement));
|
||||||
|
verify(provider).removeGenerated(context, "entity-token");
|
||||||
|
} finally {
|
||||||
|
discovery.rollback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void bootstrapRegistries() {
|
public static void bootstrapRegistries() {
|
||||||
SharedConstants.tryDetectVersion();
|
SharedConstants.tryDetectVersion();
|
||||||
|
|||||||
+149
@@ -0,0 +1,149 @@
|
|||||||
|
package art.arcane.iris.modded.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
|
||||||
|
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureQuery;
|
||||||
|
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureRef;
|
||||||
|
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.world.history.GenerationSemanticQueries;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandSource;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeStructureQueries;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.MockedConstruction;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockConstruction;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class ModdedLocateRiverTest {
|
||||||
|
private static final int DISTANCE = GenerationSemanticQueries.RIVER_SEARCH_DISTANCE;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unknownTypeFailsBeforeSearching() {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
try (MockedStatic<IrisModdedCommands> commands = fixture.commands()) {
|
||||||
|
assertEquals(0, ModdedLocateCommands.gotoRiver(fixture.source, "ocean_trench"));
|
||||||
|
|
||||||
|
commands.verify(() -> IrisModdedCommands.fail(eq(fixture.source), anyString()));
|
||||||
|
commands.verify(() -> IrisModdedCommands.ok(eq(fixture.source), anyString()), never());
|
||||||
|
verify(fixture.server, never()).execute(any());
|
||||||
|
verify(fixture.runtime, never()).nearestFeature(anyInt(), anyInt(), any(), anyInt(), any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void missingFeatureReportsNotFound() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
when(fixture.runtime.nearestFeature(anyInt(), anyInt(), any(), anyInt(), any())).thenReturn(Optional.empty());
|
||||||
|
try (MockedStatic<IrisModdedCommands> commands = fixture.commands()) {
|
||||||
|
assertEquals(1, ModdedLocateCommands.gotoRiver(fixture.source, "river"));
|
||||||
|
fixture.finishOnServerThread();
|
||||||
|
|
||||||
|
commands.verify(() -> IrisModdedCommands.fail(fixture.source, "No river found within " + DISTANCE + " blocks."));
|
||||||
|
verify(fixture.player, never()).teleport(any(), anyDouble(), anyDouble(), anyDouble());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void plannerFailureIsReportedAsAnErrorNotAsNotFound() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
when(fixture.runtime.nearestFeature(anyInt(), anyInt(), any(), anyInt(), any()))
|
||||||
|
.thenThrow(new IllegalStateException("window planner exploded"));
|
||||||
|
try (MockedStatic<IrisModdedCommands> commands = fixture.commands()) {
|
||||||
|
assertEquals(1, ModdedLocateCommands.gotoRiver(fixture.source, "major_river"));
|
||||||
|
fixture.finishOnServerThread();
|
||||||
|
|
||||||
|
commands.verify(() -> IrisModdedCommands.fail(fixture.source,
|
||||||
|
"Could not search for major river: IllegalStateException - window planner exploded"));
|
||||||
|
commands.verify(() -> IrisModdedCommands.fail(eq(fixture.source), eq("No major river found within " + DISTANCE + " blocks.")),
|
||||||
|
never());
|
||||||
|
verify(fixture.player, never()).teleport(any(), anyDouble(), anyDouble(), anyDouble());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void foundFeatureTeleportsAboveItFromThePlayersPosition() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
when(fixture.runtime.nearestFeature(anyInt(), anyInt(), any(), anyInt(), any()))
|
||||||
|
.thenReturn(Optional.of(new HydrologyFeatureRef(9L, HydrologyFeatureType.WATERFALL, 400, 130, -250)));
|
||||||
|
when(fixture.player.teleport(fixture.world, 400.5D, 68D, -249.5D)).thenReturn(true);
|
||||||
|
try (MockedStatic<IrisModdedCommands> commands = fixture.commands();
|
||||||
|
MockedConstruction<NativeStructureQueries> queries = mockConstruction(NativeStructureQueries.class)) {
|
||||||
|
assertEquals(1, ModdedLocateCommands.gotoRiver(fixture.source, "waterfall"));
|
||||||
|
fixture.finishOnServerThread();
|
||||||
|
|
||||||
|
ArgumentCaptor<HydrologyFeatureQuery> query = ArgumentCaptor.forClass(HydrologyFeatureQuery.class);
|
||||||
|
verify(fixture.runtime).nearestFeature(eq(120), eq(-75), query.capture(), eq(DISTANCE), any());
|
||||||
|
assertEquals(HydrologyFeatureQuery.of(HydrologyFeatureType.WATERFALL), query.getValue());
|
||||||
|
verify(queries.constructed().getFirst()).loadChunk(400 >> 4, -250 >> 4);
|
||||||
|
verify(fixture.player).teleport(fixture.world, 400.5D, 68D, -249.5D);
|
||||||
|
commands.verify(() -> IrisModdedCommands.ok(eq(fixture.source), anyString()), times(2));
|
||||||
|
commands.verify(() -> IrisModdedCommands.fail(eq(fixture.source), anyString()), never());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Fixture {
|
||||||
|
private final NativeCommandSource source = mock(NativeCommandSource.class);
|
||||||
|
private final NativeWorld world = mock(NativeWorld.class);
|
||||||
|
private final NativeProtocolPlayer player = mock(NativeProtocolPlayer.class);
|
||||||
|
private final NativeModdedServer server = mock(NativeModdedServer.class);
|
||||||
|
private final Engine engine = mock(Engine.class);
|
||||||
|
private final HydrologyRuntime runtime = mock(HydrologyRuntime.class);
|
||||||
|
private final CompletableFuture<Runnable> completion = new CompletableFuture<>();
|
||||||
|
|
||||||
|
private Fixture() {
|
||||||
|
IrisComplex complex = mock(IrisComplex.class);
|
||||||
|
IrisDimension dimension = mock(IrisDimension.class);
|
||||||
|
when(source.player()).thenReturn(player);
|
||||||
|
when(source.world()).thenReturn(world);
|
||||||
|
when(source.server()).thenReturn(server);
|
||||||
|
when(player.blockX()).thenReturn(120);
|
||||||
|
when(player.blockZ()).thenReturn(-75);
|
||||||
|
when(player.connected()).thenReturn(true);
|
||||||
|
when(player.inWorld(world)).thenReturn(true);
|
||||||
|
when(world.minHeight()).thenReturn(-64);
|
||||||
|
when(world.maxHeight()).thenReturn(320);
|
||||||
|
when(engine.getComplex()).thenReturn(complex);
|
||||||
|
when(engine.getDimension()).thenReturn(dimension);
|
||||||
|
when(dimension.getMinHeight()).thenReturn(-64);
|
||||||
|
when(complex.getHydrologyRuntime()).thenReturn(runtime);
|
||||||
|
when(runtime.enabled()).thenReturn(true);
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
completion.complete(invocation.getArgument(0));
|
||||||
|
return null;
|
||||||
|
}).when(server).execute(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
private MockedStatic<IrisModdedCommands> commands() {
|
||||||
|
MockedStatic<IrisModdedCommands> commands = mockStatic(IrisModdedCommands.class);
|
||||||
|
commands.when(() -> IrisModdedCommands.engineFor(world)).thenReturn(engine);
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishOnServerThread() throws Exception {
|
||||||
|
completion.get(10L, TimeUnit.SECONDS).run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+41
-7
@@ -1,6 +1,7 @@
|
|||||||
package art.arcane.iris.modded.command;
|
package art.arcane.iris.modded.command;
|
||||||
|
|
||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
|
import art.arcane.iris.generation.hydrology.engine.HydrologyPregeneration;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.modded.ModdedGenPool;
|
import art.arcane.iris.modded.ModdedGenPool;
|
||||||
import art.arcane.iris.world.pregen.MantleHeapPressure;
|
import art.arcane.iris.world.pregen.MantleHeapPressure;
|
||||||
@@ -11,8 +12,10 @@ import org.junit.After;
|
|||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.InOrder;
|
||||||
import org.mockito.MockedStatic;
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
@@ -21,6 +24,7 @@ import java.util.concurrent.Future;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
@@ -30,9 +34,11 @@ import static org.junit.Assert.assertThrows;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyList;
|
||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.doAnswer;
|
||||||
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
||||||
import static org.mockito.Mockito.doThrow;
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
import static org.mockito.Mockito.inOrder;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.mockStatic;
|
import static org.mockito.Mockito.mockStatic;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
@@ -88,6 +94,25 @@ public class ModdedPregenMethodLifecycleTest {
|
|||||||
verify(runtime, never()).execute(any());
|
verify(runtime, never()).execute(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void closeWithdrawsQueuedHydrologyPrefetchAndRefusesLaterAnnouncements() {
|
||||||
|
HydrologyPregeneration epoch = mock(HydrologyPregeneration.class);
|
||||||
|
when(engine.getComplex().getHydrologyRuntime().openPregeneration()).thenReturn(epoch);
|
||||||
|
when(runtime.isServerThread()).thenReturn(true);
|
||||||
|
assertTrue(method.deferFinalSaveToServerThread());
|
||||||
|
List<long[]> ahead = List.of(new long[]{0L, 0L}, new long[]{1L, 0L});
|
||||||
|
|
||||||
|
method.onRegionsAhead(ahead);
|
||||||
|
method.close();
|
||||||
|
method.onRegionsAhead(ahead);
|
||||||
|
|
||||||
|
InOrder order = inOrder(epoch);
|
||||||
|
order.verify(epoch).prepare(ahead);
|
||||||
|
order.verify(epoch).close();
|
||||||
|
verify(epoch, times(1)).prepare(anyList());
|
||||||
|
method.completeDeferredFinalSave();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void asynchronousCallerCannotClaimTheShutdownSave() {
|
public void asynchronousCallerCannotClaimTheShutdownSave() {
|
||||||
assertFalse(method.deferFinalSaveToServerThread());
|
assertFalse(method.deferFinalSaveToServerThread());
|
||||||
@@ -97,6 +122,9 @@ public class ModdedPregenMethodLifecycleTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void finalSaveWaitsForEveryCallbackBeyondFiveSeconds() throws Exception {
|
public void finalSaveWaitsForEveryCallbackBeyondFiveSeconds() throws Exception {
|
||||||
|
AtomicLong clock = new AtomicLong();
|
||||||
|
CountDownLatch started = new CountDownLatch(1);
|
||||||
|
CountDownLatch advanced = new CountDownLatch(2);
|
||||||
CompletableFuture<ChunkLoad> load = new CompletableFuture<>();
|
CompletableFuture<ChunkLoad> load = new CompletableFuture<>();
|
||||||
CompletableFuture<ChunkLoad> failedLoad = new CompletableFuture<>();
|
CompletableFuture<ChunkLoad> failedLoad = new CompletableFuture<>();
|
||||||
PregenListener listener = mock(PregenListener.class);
|
PregenListener listener = mock(PregenListener.class);
|
||||||
@@ -110,16 +138,22 @@ public class ModdedPregenMethodLifecycleTest {
|
|||||||
}).when(runtime).execute(any());
|
}).when(runtime).execute(any());
|
||||||
try (MockedStatic<MantleHeapPressure> pressure = mockStatic(MantleHeapPressure.class)) {
|
try (MockedStatic<MantleHeapPressure> pressure = mockStatic(MantleHeapPressure.class)) {
|
||||||
pressure.when(MantleHeapPressure::memoryBudgetBytes).thenReturn(512L << 20);
|
pressure.when(MantleHeapPressure::memoryBudgetBytes).thenReturn(512L << 20);
|
||||||
method = new ModdedPregenMethod(new ModdedPregenMethod.Configuration(runtime, engine, false));
|
method = new ModdedPregenMethod(new ModdedPregenMethod.Configuration(runtime, engine, false), () -> {
|
||||||
|
long now = clock.get();
|
||||||
|
if (now >= TimeUnit.SECONDS.toNanos(6)) {
|
||||||
|
advanced.countDown();
|
||||||
|
} else {
|
||||||
|
started.countDown();
|
||||||
|
}
|
||||||
|
return now;
|
||||||
|
});
|
||||||
method.generateChunk(2, 3, listener);
|
method.generateChunk(2, 3, listener);
|
||||||
method.generateChunk(4, 5, listener);
|
method.generateChunk(4, 5, listener);
|
||||||
CountDownLatch started = new CountDownLatch(1);
|
Future<?> closing = executor.submit(method::close);
|
||||||
Future<?> closing = executor.submit(() -> {
|
|
||||||
started.countDown();
|
|
||||||
method.close();
|
|
||||||
});
|
|
||||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||||
assertThrows(TimeoutException.class, () -> closing.get(6, TimeUnit.SECONDS));
|
clock.set(TimeUnit.SECONDS.toNanos(6));
|
||||||
|
assertTrue(advanced.await(5, TimeUnit.SECONDS));
|
||||||
|
assertFalse(closing.isDone());
|
||||||
verify(runtime, never()).save();
|
verify(runtime, never()).save();
|
||||||
verify(engine, never()).syncGenerationHistory();
|
verify(engine, never()).syncGenerationHistory();
|
||||||
load.complete(new ChunkLoad(true, null));
|
load.complete(new ChunkLoad(true, null));
|
||||||
|
|||||||
+73
@@ -1,25 +1,98 @@
|
|||||||
package art.arcane.iris.modded.command;
|
package art.arcane.iris.modded.command;
|
||||||
|
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.studio.view.OpenerPosition;
|
||||||
|
import art.arcane.iris.world.IrisWorld;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||||
import art.arcane.volmlib.nativelib.view.WorldView;
|
import art.arcane.volmlib.nativelib.view.WorldView;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
import org.mockito.invocation.InvocationOnMock;
|
import org.mockito.invocation.InvocationOnMock;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class ModdedVisionOverlayTest {
|
public class ModdedVisionOverlayTest {
|
||||||
|
@Test
|
||||||
|
public void rejectedServerTaskCompletesThePositionRequestAsOffline() {
|
||||||
|
WorldView world = mock(WorldView.class);
|
||||||
|
doThrow(new RejectedExecutionException("Server stopped")).when(world).execute(any(Runnable.class));
|
||||||
|
ModdedVisionOverlay overlay = new ModdedVisionOverlay(new ModdedVisionOverlay.Context(world,
|
||||||
|
mock(Engine.class), UUID.randomUUID()));
|
||||||
|
AtomicReference<OpenerPosition> position = new AtomicReference<>();
|
||||||
|
|
||||||
|
overlay.requestOpenerPosition(position::set);
|
||||||
|
|
||||||
|
assertEquals(OpenerPosition.OFFLINE, position.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void openerPositionRunsOnTheServerAndTracksDisconnectAndWorldChanges() {
|
||||||
|
WorldView world = mock(WorldView.class);
|
||||||
|
AtomicReference<Runnable> scheduled = new AtomicReference<>();
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
scheduled.set(invocation.getArgument(0));
|
||||||
|
return null;
|
||||||
|
}).when(world).execute(any(Runnable.class));
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
IrisWorld irisWorld = mock(IrisWorld.class);
|
||||||
|
NativeWorld level = mock(NativeWorld.class);
|
||||||
|
NativeModdedServer server = mock(NativeModdedServer.class);
|
||||||
|
NativeProtocolPlayer player = mock(NativeProtocolPlayer.class);
|
||||||
|
UUID opener = UUID.randomUUID();
|
||||||
|
when(engine.getWorld()).thenReturn(irisWorld);
|
||||||
|
when(irisWorld.platformWorld()).thenReturn(level);
|
||||||
|
when(server.player(opener)).thenReturn(player);
|
||||||
|
when(player.connected()).thenReturn(true);
|
||||||
|
when(player.inWorld(level)).thenReturn(true);
|
||||||
|
when(player.x()).thenReturn(21.5D);
|
||||||
|
when(player.y()).thenReturn(85.25D);
|
||||||
|
when(player.z()).thenReturn(-39.5D);
|
||||||
|
ModdedVisionOverlay overlay = new ModdedVisionOverlay(new ModdedVisionOverlay.Context(world, engine, opener));
|
||||||
|
AtomicReference<OpenerPosition> position = new AtomicReference<>();
|
||||||
|
try (MockedStatic<NativeModdedServer> nativeServer = mockStatic(NativeModdedServer.class)) {
|
||||||
|
nativeServer.when(() -> NativeModdedServer.forWorld(level)).thenReturn(server);
|
||||||
|
|
||||||
|
overlay.requestOpenerPosition(position::set);
|
||||||
|
assertNull("position reads must wait for the server task", position.get());
|
||||||
|
scheduled.get().run();
|
||||||
|
assertEquals(OpenerPosition.inWorld(21.5D, 85.25D, -39.5D), position.get());
|
||||||
|
|
||||||
|
overlay.requestOpenerPosition(position::set);
|
||||||
|
when(player.connected()).thenReturn(false);
|
||||||
|
scheduled.get().run();
|
||||||
|
assertEquals(OpenerPosition.OFFLINE, position.get());
|
||||||
|
|
||||||
|
when(player.connected()).thenReturn(true);
|
||||||
|
when(player.inWorld(level)).thenReturn(false);
|
||||||
|
overlay.requestOpenerPosition(position::set);
|
||||||
|
scheduled.get().run();
|
||||||
|
assertEquals(OpenerPosition.IN_OTHER_WORLD, position.get());
|
||||||
|
|
||||||
|
when(player.inWorld(level)).thenReturn(true);
|
||||||
|
overlay.requestOpenerPosition(position::set);
|
||||||
|
scheduled.get().run();
|
||||||
|
assertEquals(OpenerPosition.inWorld(21.5D, 85.25D, -39.5D), position.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Vision click can land anywhere in the world, so the destination chunk is usually cold and gets the same
|
* A Vision click can land anywhere in the world, so the destination chunk is usually cold and gets the same
|
||||||
* bounded deadline as /iris tp.
|
* bounded deadline as /iris tp.
|
||||||
|
|||||||
+244
@@ -0,0 +1,244 @@
|
|||||||
|
package art.arcane.iris.modded.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.mantle.EngineMantle;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.runtime.EngineMetrics;
|
||||||
|
import art.arcane.iris.integration.Identifier;
|
||||||
|
import art.arcane.iris.modded.api.ModdedBlockData;
|
||||||
|
import art.arcane.iris.modded.api.ModdedBlockPlacementContext;
|
||||||
|
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
|
||||||
|
import art.arcane.iris.modded.api.ModdedDataProvider;
|
||||||
|
import art.arcane.iris.modded.api.ModdedDataType;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.world.IrisWorld;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||||
|
import art.arcane.volmlib.util.function.Consumer4;
|
||||||
|
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||||
|
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||||
|
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||||
|
import art.arcane.volmlib.util.mantle.runtime.MantleDataAdapter;
|
||||||
|
import art.arcane.volmlib.util.mantle.runtime.MantleHooks;
|
||||||
|
import art.arcane.volmlib.util.matter.Matter;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.NoteBlock;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
import org.mockito.invocation.InvocationOnMock;
|
||||||
|
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class ModdedChunkUpdateServiceCarrierTest {
|
||||||
|
private static final String MOD_ID = "iris_carrier_guard_test";
|
||||||
|
private static final String LAMP = MOD_ID + ":lamp";
|
||||||
|
private static final String CHAIR = MOD_ID + ":chair";
|
||||||
|
private static final String FIRE = MOD_ID + ":fire";
|
||||||
|
private static final String SOUL_FIRE = MOD_ID + ":soul_fire";
|
||||||
|
private static final List<BlockPos> PLACED = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
ModdedCustomContentRegistry.register(new CarrierProvider());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void clearPlacements() {
|
||||||
|
PLACED.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void staleRecordsAreSkippedWhileIntactAndAirCarrierRecordsArePlaced() {
|
||||||
|
RecordMantleChunk chunk = new RecordMantleChunk(List.of(
|
||||||
|
new Placed(1, LAMP),
|
||||||
|
new Placed(2, LAMP),
|
||||||
|
new Placed(3, LAMP),
|
||||||
|
new Placed(4, CHAIR)));
|
||||||
|
NativeWorld level = level(Map.of(
|
||||||
|
1, Blocks.NOTE_BLOCK.defaultBlockState().setValue(NoteBlock.NOTE, 7),
|
||||||
|
2, Blocks.AIR.defaultBlockState(),
|
||||||
|
3, Blocks.COBBLESTONE.defaultBlockState(),
|
||||||
|
4, Blocks.COBBLESTONE.defaultBlockState()));
|
||||||
|
List<String> debug = new ArrayList<>();
|
||||||
|
Engine engine = engine(chunk);
|
||||||
|
|
||||||
|
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||||
|
logging.when(() -> IrisLogging.debug(anyString(), any(Object[].class))).thenAnswer((InvocationOnMock invocation) -> {
|
||||||
|
Object[] arguments = invocation.getArguments();
|
||||||
|
debug.add(String.format((String) arguments[0], Arrays.copyOfRange(arguments, 1, arguments.length)));
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
new ModdedChunkUpdateService().updateRegeneratedChunk(engine, level, engine.getMantle().getMantle(), 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(List.of(new BlockPos(1, 6, 1), new BlockPos(4, 6, 4)), PLACED);
|
||||||
|
assertTrue(chunk.isFlagged(MantleFlag.ETCHED));
|
||||||
|
assertTrue(chunk.customDeleted());
|
||||||
|
assertEquals(1, debug.size());
|
||||||
|
assertTrue(debug.getFirst(), debug.getFirst().contains("2 stale"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void extinguishedFireCarriersArePlacedWhileCoveredCarriersAreSkipped() {
|
||||||
|
RecordMantleChunk chunk = new RecordMantleChunk(List.of(
|
||||||
|
new Placed(1, FIRE), new Placed(2, SOUL_FIRE),
|
||||||
|
new Placed(3, FIRE), new Placed(4, SOUL_FIRE)));
|
||||||
|
NativeWorld level = level(Map.of(
|
||||||
|
1, Blocks.AIR.defaultBlockState(),
|
||||||
|
2, Blocks.CAVE_AIR.defaultBlockState(),
|
||||||
|
3, Blocks.COBBLESTONE.defaultBlockState(),
|
||||||
|
4, Blocks.COBBLESTONE.defaultBlockState()));
|
||||||
|
Engine engine = engine(chunk);
|
||||||
|
|
||||||
|
new ModdedChunkUpdateService().updateRegeneratedChunk(engine, level, engine.getMantle().getMantle(), 0, 0);
|
||||||
|
|
||||||
|
assertEquals(List.of(new BlockPos(1, 6, 1), new BlockPos(2, 6, 2)), PLACED);
|
||||||
|
assertTrue(chunk.customDeleted());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NativeWorld level(Map<Integer, BlockState> live) {
|
||||||
|
ServerLevel serverLevel = mock(ServerLevel.class);
|
||||||
|
when(serverLevel.getBlockState(any(BlockPos.class))).thenReturn(Blocks.STONE.defaultBlockState());
|
||||||
|
NativeWorld level = mock(NativeWorld.class);
|
||||||
|
when(level.name()).thenReturn("example:carriers");
|
||||||
|
when(level.minHeight()).thenReturn(-64);
|
||||||
|
when(level.maxHeight()).thenReturn(320);
|
||||||
|
when(level.nativeHandle()).thenReturn(serverLevel);
|
||||||
|
for (Map.Entry<Integer, BlockState> entry : live.entrySet()) {
|
||||||
|
int offset = entry.getKey();
|
||||||
|
when(level.getBlock(offset, 6, offset)).thenReturn(ModdedBlockState.of(entry.getValue(), null));
|
||||||
|
}
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Engine engine(RecordMantleChunk chunk) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Mantle<Matter> mantle = mock(Mantle.class);
|
||||||
|
when(mantle.isChunkLoaded(0, 0)).thenReturn(true);
|
||||||
|
when(mantle.hasFlag(0, 0, MantleFlag.ETCHED)).thenAnswer((InvocationOnMock invocation) -> chunk.isFlagged(MantleFlag.ETCHED));
|
||||||
|
when(mantle.useChunk(0, 0)).thenAnswer((InvocationOnMock invocation) -> chunk.use());
|
||||||
|
IrisWorld world = mock(IrisWorld.class);
|
||||||
|
when(world.minHeight()).thenReturn(-64);
|
||||||
|
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||||
|
when(engineMantle.getMantle()).thenReturn(mantle);
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
when(engine.getWorld()).thenReturn(world);
|
||||||
|
when(engine.getMetrics()).thenReturn(new EngineMetrics(8));
|
||||||
|
when(engine.getMantle()).thenReturn(engineMantle);
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static MantleDataAdapter<Matter> emptyAdapter() {
|
||||||
|
return (MantleDataAdapter<Matter>) Proxy.newProxyInstance(
|
||||||
|
MantleDataAdapter.class.getClassLoader(),
|
||||||
|
new Class<?>[]{MantleDataAdapter.class},
|
||||||
|
(proxy, method, arguments) -> {
|
||||||
|
throw new UnsupportedOperationException(method.getName());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Placed(int offset, String key) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RecordMantleChunk extends MantleChunk<Matter> {
|
||||||
|
private final List<Placed> records;
|
||||||
|
private boolean customDeleted;
|
||||||
|
|
||||||
|
private RecordMantleChunk(List<Placed> records) {
|
||||||
|
super(1, 0, 0, emptyAdapter(), MantleHooks.NONE);
|
||||||
|
this.records = records;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> void iterate(Class<T> type, Consumer4<Integer, Integer, Integer, T> iterator) {
|
||||||
|
if (type != Identifier.class) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (Placed record : records) {
|
||||||
|
iterator.accept(record.offset(), 70, record.offset(), type.cast(Identifier.fromString(record.key())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T get(int x, int y, int z, Class<T> type) {
|
||||||
|
if (type == Identifier.class && y == 70 && x == z) {
|
||||||
|
for (Placed record : records) {
|
||||||
|
if (record.offset() == x) {
|
||||||
|
return type.cast(Identifier.fromString(record.key()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteSlices(Class<?> type) {
|
||||||
|
if (type == Identifier.class) {
|
||||||
|
customDeleted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean customDeleted() {
|
||||||
|
return customDeleted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class CarrierProvider implements ModdedDataProvider {
|
||||||
|
@Override
|
||||||
|
public String modId() {
|
||||||
|
return MOD_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<String> getTypes(ModdedDataType type) {
|
||||||
|
return type == ModdedDataType.BLOCK ? List.of(LAMP, CHAIR, FIRE, SOUL_FIRE) : List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValidProvider(String id, ModdedDataType type) {
|
||||||
|
return type == ModdedDataType.BLOCK && getTypes(type).contains(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ModdedBlockData getBlockData(String blockId, Map<String, String> state) {
|
||||||
|
BlockState carrier = switch (blockId) {
|
||||||
|
case CHAIR -> Blocks.AIR.defaultBlockState();
|
||||||
|
case FIRE -> Blocks.FIRE.defaultBlockState();
|
||||||
|
case SOUL_FIRE -> Blocks.SOUL_FIRE.defaultBlockState();
|
||||||
|
default -> Blocks.NOTE_BLOCK.defaultBlockState();
|
||||||
|
};
|
||||||
|
return ModdedBlockData.deferred(ModdedBlockState.of(carrier, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String processBlockPlacement(ModdedBlockPlacementContext context) {
|
||||||
|
PLACED.add(context.placement().position());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
-5
@@ -68,6 +68,29 @@ import static org.mockito.Mockito.verify;
|
|||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class ModdedChunkUpdateServiceTest {
|
public class ModdedChunkUpdateServiceTest {
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void regeneratedCleanupUsesSuppliedMantleAfterCurrentRuntimeChanges() {
|
||||||
|
Mantle<Matter> selected = mock(Mantle.class);
|
||||||
|
Mantle<Matter> replacement = mock(Mantle.class);
|
||||||
|
MantleDataAdapter<Matter> adapter = mock(MantleDataAdapter.class);
|
||||||
|
MantleChunk<Matter> chunk = new MantleChunk<>(1, 0, 0, adapter, MantleHooks.NONE);
|
||||||
|
when(selected.isChunkLoaded(0, 0)).thenReturn(true);
|
||||||
|
when(selected.useChunk(0, 0)).thenAnswer(invocation -> chunk.use());
|
||||||
|
when(selected.hasFlag(0, 0, MantleFlag.ETCHED)).thenReturn(true);
|
||||||
|
NativeWorld level = mock(NativeWorld.class);
|
||||||
|
when(level.isChunkLoaded(0, 0)).thenReturn(true);
|
||||||
|
Engine engine = engineWith(replacement);
|
||||||
|
when(engine.getWorld().platformWorld()).thenReturn(level);
|
||||||
|
|
||||||
|
new ModdedChunkUpdateService().updateRegeneratedChunk(engine, level, selected, 0, 0);
|
||||||
|
|
||||||
|
verify(selected).useChunk(0, 0);
|
||||||
|
verify(replacement, never()).isChunkLoaded(anyInt(), anyInt());
|
||||||
|
verify(replacement, never()).useChunk(anyInt(), anyInt());
|
||||||
|
assertFalse(chunk.inUse());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void fallingFluidUpdateRetainsLevelEightStateAndRequestsPhysics() {
|
public void fallingFluidUpdateRetainsLevelEightStateAndRequestsPhysics() {
|
||||||
SharedConstants.tryDetectVersion();
|
SharedConstants.tryDetectVersion();
|
||||||
@@ -154,7 +177,7 @@ public class ModdedChunkUpdateServiceTest {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
service.updateRegeneratedChunk(engine, level, 0, 0);
|
service.updateRegeneratedChunk(engine, level, engine.getMantle().getMantle(), 0, 0);
|
||||||
|
|
||||||
assertFalse(chunk.isFlagged(MantleFlag.ETCHED));
|
assertFalse(chunk.isFlagged(MantleFlag.ETCHED));
|
||||||
assertFalse(chunk.isFlagged(MantleFlag.UPDATE));
|
assertFalse(chunk.isFlagged(MantleFlag.UPDATE));
|
||||||
@@ -162,7 +185,7 @@ public class ModdedChunkUpdateServiceTest {
|
|||||||
logging.verify(() -> IrisLogging.reportError(any(Throwable.class)), never());
|
logging.verify(() -> IrisLogging.reportError(any(Throwable.class)), never());
|
||||||
logging.verify(() -> IrisLogging.reportError(anyString(), any(Throwable.class)), never());
|
logging.verify(() -> IrisLogging.reportError(anyString(), any(Throwable.class)), never());
|
||||||
|
|
||||||
service.updateRegeneratedChunk(engine, level, 0, 0);
|
service.updateRegeneratedChunk(engine, level, engine.getMantle().getMantle(), 0, 0);
|
||||||
|
|
||||||
assertTrue(chunk.isFlagged(MantleFlag.ETCHED));
|
assertTrue(chunk.isFlagged(MantleFlag.ETCHED));
|
||||||
assertTrue(chunk.deleted());
|
assertTrue(chunk.deleted());
|
||||||
@@ -187,7 +210,7 @@ public class ModdedChunkUpdateServiceTest {
|
|||||||
});
|
});
|
||||||
|
|
||||||
assertThrows(SavedBiomeUnavailableException.class,
|
assertThrows(SavedBiomeUnavailableException.class,
|
||||||
() -> new ModdedChunkUpdateService().updateRegeneratedChunk(engine, level, 0, 0));
|
() -> new ModdedChunkUpdateService().updateRegeneratedChunk(engine, level, engine.getMantle().getMantle(), 0, 0));
|
||||||
|
|
||||||
assertFalse(chunk.isFlagged(MantleFlag.ETCHED));
|
assertFalse(chunk.isFlagged(MantleFlag.ETCHED));
|
||||||
assertFalse(chunk.isFlagged(MantleFlag.UPDATE));
|
assertFalse(chunk.isFlagged(MantleFlag.UPDATE));
|
||||||
@@ -215,13 +238,13 @@ public class ModdedChunkUpdateServiceTest {
|
|||||||
});
|
});
|
||||||
|
|
||||||
assertSame(failure, assertThrows(IllegalStateException.class,
|
assertSame(failure, assertThrows(IllegalStateException.class,
|
||||||
() -> service.updateRegeneratedChunk(engine, level, 0, 0)));
|
() -> service.updateRegeneratedChunk(engine, level, engine.getMantle().getMantle(), 0, 0)));
|
||||||
assertFalse(chunk.isFlagged(MantleFlag.UPDATE));
|
assertFalse(chunk.isFlagged(MantleFlag.UPDATE));
|
||||||
assertFalse(chunk.isFlagged(MantleFlag.ETCHED));
|
assertFalse(chunk.isFlagged(MantleFlag.ETCHED));
|
||||||
assertFalse(chunk.deleted());
|
assertFalse(chunk.deleted());
|
||||||
verify(chunk.slice, never()).set(2, 5, 2, null);
|
verify(chunk.slice, never()).set(2, 5, 2, null);
|
||||||
|
|
||||||
service.updateRegeneratedChunk(engine, level, 0, 0);
|
service.updateRegeneratedChunk(engine, level, engine.getMantle().getMantle(), 0, 0);
|
||||||
|
|
||||||
assertTrue(chunk.isFlagged(MantleFlag.ETCHED));
|
assertTrue(chunk.isFlagged(MantleFlag.ETCHED));
|
||||||
assertTrue(chunk.deleted());
|
assertTrue(chunk.deleted());
|
||||||
|
|||||||
@@ -136,6 +136,18 @@
|
|||||||
"replacement": "when(level.getMaxBuildHeight()).thenReturn(320);"
|
"replacement": "when(level.getMaxBuildHeight()).thenReturn(320);"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"art/arcane/iris/modded/api/ModdedCustomContentRegistryTest.java": [
|
||||||
|
{
|
||||||
|
"pattern": "\\bResourceLocation\\b",
|
||||||
|
"replacement": "Identifier"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"art/arcane/iris/modded/service/ModdedChunkUpdateServiceCarrierTest.java": [
|
||||||
|
{
|
||||||
|
"pattern": "\\bResourceLocation\\b",
|
||||||
|
"replacement": "Identifier"
|
||||||
|
}
|
||||||
|
],
|
||||||
"art/arcane/volmlib/nativelib/minecraft26_2/modded/NativeSpawnPopulationTest.java": [
|
"art/arcane/volmlib/nativelib/minecraft26_2/modded/NativeSpawnPopulationTest.java": [
|
||||||
{
|
{
|
||||||
"pattern": "when\\(level\\.getMaxY\\(\\)\\)\\.thenReturn\\(320\\);",
|
"pattern": "when\\(level\\.getMaxY\\(\\)\\)\\.thenReturn\\(320\\);",
|
||||||
|
|||||||
@@ -56,10 +56,11 @@ IrisFileOperations irisFileOperations = objects.newInstance(IrisFileOperations)
|
|||||||
|
|
||||||
apply from: file('../../gradle/modded-generation-build-revision.gradle')
|
apply from: file('../../gradle/modded-generation-build-revision.gradle')
|
||||||
apply from: file('../../gradle/volmlib-stamp.gradle')
|
apply from: file('../../gradle/volmlib-stamp.gradle')
|
||||||
|
apply from: file('../../gradle/java21-runtime-exclusions.gradle')
|
||||||
|
|
||||||
Properties rootProperties = new Properties()
|
Properties rootProperties = new Properties()
|
||||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.4'))
|
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.5'))
|
||||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
||||||
String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse(irisModTarget.neoForge.toString())
|
String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse(irisModTarget.neoForge.toString())
|
||||||
Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
||||||
@@ -131,6 +132,8 @@ configurations.named('bundle').configure {
|
|||||||
exclude(group: 'de.crazydev22.slimjar.helper')
|
exclude(group: 'de.crazydev22.slimjar.helper')
|
||||||
exclude(group: 'de.crazydev22.slimjar')
|
exclude(group: 'de.crazydev22.slimjar')
|
||||||
exclude(group: 'io.github.slimjar')
|
exclude(group: 'io.github.slimjar')
|
||||||
|
// The game ships its own LWJGL; without the modules it leaves out, the map's 3D views draw in software.
|
||||||
|
exclude(group: 'org.lwjgl')
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations.compileClasspath.extendsFrom(configurations.devBundle)
|
configurations.compileClasspath.extendsFrom(configurations.devBundle)
|
||||||
@@ -140,7 +143,7 @@ configurations.testRuntimeClasspath.extendsFrom(configurations.runtimeClasspath,
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation('junit:junit:4.13.2')
|
testImplementation('junit:junit:4.13.2')
|
||||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
testImplementation(libs.mockito.core)
|
||||||
testImplementation(testFixtures('art.arcane:core'))
|
testImplementation(testFixtures('art.arcane:core'))
|
||||||
testImplementation(libs.spigot) {
|
testImplementation(libs.spigot) {
|
||||||
transitive = false
|
transitive = false
|
||||||
@@ -172,7 +175,9 @@ dependencies {
|
|||||||
libs.caffeine,
|
libs.caffeine,
|
||||||
'com.fulcrumgenomics:jlibdeflate:0.2.0',
|
'com.fulcrumgenomics:jlibdeflate:0.2.0',
|
||||||
libs.dom4j,
|
libs.dom4j,
|
||||||
libs.jaxen
|
libs.jaxen,
|
||||||
|
libs.flatlaf,
|
||||||
|
libs.flatlaf.extras
|
||||||
]
|
]
|
||||||
add('bundle', libs.asm)
|
add('bundle', libs.asm)
|
||||||
add('devBundle', 'org.ow2.asm:asm') {
|
add('devBundle', 'org.ow2.asm:asm') {
|
||||||
@@ -260,6 +265,8 @@ tasks.named('compileJava', JavaCompile).configure {
|
|||||||
options.release.set((int) irisModTarget.java)
|
options.release.set((int) irisModTarget.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply from: file('../../gradle/mockito-agent.gradle')
|
||||||
|
|
||||||
tasks.named('test').configure {
|
tasks.named('test').configure {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
classpath = classpath.minus(developmentModLibraries).plus(sourceSets.modLibraries.output)
|
classpath = classpath.minus(developmentModLibraries).plus(sourceSets.modLibraries.output)
|
||||||
@@ -295,8 +302,7 @@ tasks.named('shadowJar', ShadowJar).configure {
|
|||||||
from(sourceSets.main.output)
|
from(sourceSets.main.output)
|
||||||
mergeServiceFiles()
|
mergeServiceFiles()
|
||||||
if ((int) irisModTarget.java == 21) {
|
if ((int) irisModTarget.java == 21) {
|
||||||
exclude('art/arcane/volmlib/util/noise/NativeBillowLoader*.class')
|
exclude(java21RuntimeExclusions)
|
||||||
exclude('art/arcane/volmlib/util/noise/BillowBatch*.class')
|
|
||||||
}
|
}
|
||||||
exclude('META-INF/maven/**')
|
exclude('META-INF/maven/**')
|
||||||
exclude('META-INF/proguard/**')
|
exclude('META-INF/proguard/**')
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ plugins {
|
|||||||
|
|
||||||
rootProject.name = 'iris-neoforge'
|
rootProject.name = 'iris-neoforge'
|
||||||
|
|
||||||
|
apply from: new File(settingsDir, '../../gradle/runtime-cache.settings.gradle').canonicalFile
|
||||||
|
|
||||||
dependencyResolutionManagement {
|
dependencyResolutionManagement {
|
||||||
versionCatalogs {
|
versionCatalogs {
|
||||||
libs {
|
libs {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
./gradlew buildAllToOut --no-parallel -PuseLocalVolmLib=false "$@"
|
./gradlew buildAll "$@"
|
||||||
+125
-230
@@ -9,9 +9,6 @@ import org.gradle.api.plugins.JavaPluginExtension
|
|||||||
import org.gradle.api.provider.Provider
|
import org.gradle.api.provider.Provider
|
||||||
import org.gradle.jvm.toolchain.JavaLauncher
|
import org.gradle.jvm.toolchain.JavaLauncher
|
||||||
import org.gradle.jvm.toolchain.JavaToolchainService
|
import org.gradle.jvm.toolchain.JavaToolchainService
|
||||||
import org.gradle.api.file.CopySpec
|
|
||||||
import org.gradle.api.file.DeleteSpec
|
|
||||||
import org.gradle.api.file.FileSystemOperations
|
|
||||||
import org.gradle.api.file.FileTree
|
import org.gradle.api.file.FileTree
|
||||||
import org.gradle.api.plugins.JavaPlugin
|
import org.gradle.api.plugins.JavaPlugin
|
||||||
import org.gradle.api.initialization.IncludedBuild
|
import org.gradle.api.initialization.IncludedBuild
|
||||||
@@ -20,8 +17,6 @@ import org.gradle.api.tasks.compile.JavaCompile
|
|||||||
import org.gradle.jvm.tasks.Jar
|
import org.gradle.jvm.tasks.Jar
|
||||||
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||||
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||||
* Copyright (c) 2021 Arcane Arts (Volmit Software)
|
* Copyright (c) 2021 Arcane Arts (Volmit Software)
|
||||||
@@ -59,15 +54,8 @@ apply plugin: 'art.arcane.volmit-packaging'
|
|||||||
apply from: file('gradle/runtime-relocations.gradle')
|
apply from: file('gradle/runtime-relocations.gradle')
|
||||||
apply from: file('gradle/volmlib-stamp.gradle')
|
apply from: file('gradle/volmlib-stamp.gradle')
|
||||||
|
|
||||||
interface IrisFileOperations {
|
|
||||||
@Inject
|
|
||||||
FileSystemOperations getFileSystemOperations()
|
|
||||||
}
|
|
||||||
|
|
||||||
IrisFileOperations irisFileOperations = objects.newInstance(IrisFileOperations)
|
|
||||||
|
|
||||||
group = 'art.arcane'
|
group = 'art.arcane'
|
||||||
version = providers.gradleProperty('irisVersion').getOrElse('4.3.4')
|
version = providers.gradleProperty('irisVersion').getOrElse('4.3.5')
|
||||||
|
|
||||||
project(':core') {
|
project(':core') {
|
||||||
group = 'art.arcane'
|
group = 'art.arcane'
|
||||||
@@ -99,41 +87,11 @@ Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
|||||||
return "Iris v${project.version} [${platform}] ${targetVersion}.jar"
|
return "Iris v${project.version} [${platform}] ${targetVersion}.jar"
|
||||||
}
|
}
|
||||||
String bukkitArtifactName = irisArtifactName('CraftBukkit', bukkitMinecraftRange)
|
String bukkitArtifactName = irisArtifactName('CraftBukkit', bukkitMinecraftRange)
|
||||||
String fabricArtifactName = irisArtifactName('Fabric', "${minecraftVersion}+${loaderDisplayVersion(fabricLoaderVersion)}")
|
|
||||||
String forgeArtifactName = irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}")
|
|
||||||
String neoForgeArtifactName = irisArtifactName('NeoForge', "${minecraftVersion}+${loaderDisplayVersion(neoForgeVersion)}")
|
|
||||||
Map<String, String> runtimeDownloadRelocations = (Map<String, String>) ext.runtimeDownloadRelocations
|
Map<String, String> runtimeDownloadRelocations = (Map<String, String>) ext.runtimeDownloadRelocations
|
||||||
List<String> runtimeDownloadedPrefixes = (List<String>) ext.runtimeDownloadedPrefixes
|
List<String> runtimeDownloadedPrefixes = (List<String>) ext.runtimeDownloadedPrefixes
|
||||||
File pluginOutDirectory = file(providers.gradleProperty('pluginOutDirectory').getOrElse('../PluginOuts'))
|
|
||||||
File legacyBukkitJar = layout.buildDirectory.file("libs/Iris-${project.version}.jar").get().asFile
|
|
||||||
File bukkitArtifact = layout.buildDirectory.file("libs/${bukkitArtifactName}").get().asFile
|
File bukkitArtifact = layout.buildDirectory.file("libs/${bukkitArtifactName}").get().asFile
|
||||||
File consumerBukkitArtifact = layout.buildDirectory.file("consumer/${bukkitArtifactName}").get().asFile
|
|
||||||
File fabricArtifact = layout.projectDirectory.file("adapters/fabric/build/${minecraftVersion}/libs/${fabricArtifactName}").asFile
|
|
||||||
File forgeArtifact = layout.projectDirectory.file("adapters/forge/build/${minecraftVersion}/libs/${forgeArtifactName}").asFile
|
|
||||||
File neoForgeArtifact = layout.projectDirectory.file("adapters/neoforge/build/${minecraftVersion}/libs/${neoForgeArtifactName}").asFile
|
|
||||||
File consumerDirectory = layout.buildDirectory.dir('consumer').get().asFile
|
|
||||||
FileTree staleConsumerBukkitJars = fileTree(consumerDirectory) {
|
|
||||||
include('Iris v* [CraftBukkit] *.jar')
|
|
||||||
}
|
|
||||||
FileTree staleBukkitOutJars = fileTree(pluginOutDirectory) {
|
|
||||||
include('Iris v* [CraftBukkit] *.jar')
|
|
||||||
}
|
|
||||||
String selectedModdedSuffix = minecraftVersion.replace('.', '_')
|
|
||||||
apply plugin: ApiGenerator
|
apply plugin: ApiGenerator
|
||||||
|
|
||||||
// Where `buildAll` drops the per-platform jars for a local test server. Use the approved sibling
|
|
||||||
// Multiplexor workspace when it exists, with a repo-local fallback for fresh clones.
|
|
||||||
File multiplexorConsumerLocation = new File(rootDir, '../../[Minecraft Server]/consumers').canonicalFile
|
|
||||||
String defaultConsumerLocation = multiplexorConsumerLocation.isDirectory()
|
|
||||||
? multiplexorConsumerLocation.absolutePath
|
|
||||||
: layout.buildDirectory.dir('consumers').get().asFile.absolutePath
|
|
||||||
String consumerLocation = providers.gradleProperty('location')
|
|
||||||
.getOrElse(defaultConsumerLocation)
|
|
||||||
String bukkitConsumerPath = "${consumerLocation}/plugin-consumers/dropins/plugins"
|
|
||||||
String fabricConsumerPath = "${consumerLocation}/fabric-mod-consumers/dropins/mods"
|
|
||||||
String forgeConsumerPath = "${consumerLocation}/forge-mod-consumers/dropins/mods"
|
|
||||||
String neoForgeConsumerPath = "${consumerLocation}/neoforge-mod-consumers/dropins/mods"
|
|
||||||
|
|
||||||
List<String> nmsBindings = ['v26_2_R1', 'v26_3_R1']
|
List<String> nmsBindings = ['v26_2_R1', 'v26_3_R1']
|
||||||
ext.nativeDevelopmentAdapters = nmsBindings
|
ext.nativeDevelopmentAdapters = nmsBindings
|
||||||
nmsBindings.each { String key ->
|
nmsBindings.each { String key ->
|
||||||
@@ -169,7 +127,7 @@ nmsBindings.each { String key ->
|
|||||||
compileOnly(rootProject.libs.annotations)
|
compileOnly(rootProject.libs.annotations)
|
||||||
compileOnly(rootProject.libs.byteBuddy.core)
|
compileOnly(rootProject.libs.byteBuddy.core)
|
||||||
testImplementation('junit:junit:4.13.2')
|
testImplementation('junit:junit:4.13.2')
|
||||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
testImplementation(rootProject.libs.mockito.core)
|
||||||
if (key == 'v26_3_R1') {
|
if (key == 'v26_3_R1') {
|
||||||
testImplementation(project(':probe'))
|
testImplementation(project(':probe'))
|
||||||
}
|
}
|
||||||
@@ -240,65 +198,14 @@ tasks.named('jar', Jar).configure {
|
|||||||
exclude('com/fren_gor/**')
|
exclude('com/fren_gor/**')
|
||||||
|
|
||||||
}
|
}
|
||||||
doFirst {
|
|
||||||
irisFileOperations.fileSystemOperations.delete { DeleteSpec spec -> spec.delete(legacyBukkitJar) }
|
|
||||||
}
|
|
||||||
archiveFileName.set(bukkitArtifactName)
|
archiveFileName.set(bukkitArtifactName)
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('iris', Copy) {
|
tasks.register('buildBukkit') {
|
||||||
group = 'iris'
|
group = 'build'
|
||||||
dependsOn('packedJar')
|
description = 'Builds and verifies the packed Bukkit plugin.'
|
||||||
from({ tasks.named('packedJar').get().outputJar })
|
dependsOn('packedJar', 'check', ':core:check', ':spi:check', ':adapters:bukkit:plugin:check')
|
||||||
rename { String ignored -> bukkitArtifactName }
|
dependsOn(nmsBindings.collect { String binding -> ":adapters:bukkit:nms:${binding}:check".toString() })
|
||||||
into(layout.buildDirectory.dir('consumer'))
|
|
||||||
doFirst {
|
|
||||||
irisFileOperations.fileSystemOperations.delete { DeleteSpec spec -> spec.delete(staleConsumerBukkitJars) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register('buildSwiftSwamp') {
|
|
||||||
group = 'iris'
|
|
||||||
description = 'Builds every platform jar and drops them in the local BUILDS output directory.'
|
|
||||||
outputs.upToDateWhen { false }
|
|
||||||
dependsOn('iris', "verifyFabric${selectedModdedSuffix}", "verifyForge${selectedModdedSuffix}",
|
|
||||||
"verifyNeoForge${selectedModdedSuffix}")
|
|
||||||
File swiftSwampOut = file('C://VolmitSoftware/BUILDS')
|
|
||||||
Map<String, File> swiftSwampSources = [
|
|
||||||
'Iris.jar' : consumerBukkitArtifact,
|
|
||||||
'Iris-Fabric.jar' : fabricArtifact,
|
|
||||||
'Iris-Forge.jar' : forgeArtifact,
|
|
||||||
'Iris-Neoforge.jar': neoForgeArtifact
|
|
||||||
]
|
|
||||||
doLast {
|
|
||||||
swiftSwampSources.each { String target, File source ->
|
|
||||||
irisFileOperations.fileSystemOperations.copy { CopySpec spec ->
|
|
||||||
spec.from(source)
|
|
||||||
spec.into(swiftSwampOut)
|
|
||||||
spec.rename { String ignored -> target }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
println('')
|
|
||||||
println('=== Iris buildSwiftSwamp -> ' + swiftSwampOut + ' ===')
|
|
||||||
['Iris.jar', 'Iris-Fabric.jar', 'Iris-Forge.jar', 'Iris-Neoforge.jar'].each { String jar ->
|
|
||||||
File produced = new File(swiftSwampOut, jar)
|
|
||||||
println(String.format(' %-24s %8d KB', jar, (long) (produced.length() / 1024)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register('buildBukkit', Copy) {
|
|
||||||
group = 'iris'
|
|
||||||
dependsOn('packedJar')
|
|
||||||
from({ tasks.named('packedJar').get().outputJar })
|
|
||||||
rename { String ignored -> bukkitArtifactName }
|
|
||||||
into(pluginOutDirectory)
|
|
||||||
File legacyOutJar = new File(pluginOutDirectory, "Iris-${project.version}+mc${minecraftVersion}.jar")
|
|
||||||
doFirst {
|
|
||||||
irisFileOperations.fileSystemOperations.delete { DeleteSpec spec ->
|
|
||||||
spec.delete(staleBukkitOutJars, legacyOutJar)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The three loader jars come out of nested standalone Gradle builds driven through Exec. The outer
|
// The three loader jars come out of nested standalone Gradle builds driven through Exec. The outer
|
||||||
@@ -310,6 +217,7 @@ List<Object> moddedSharedInputs = []
|
|||||||
'adapters/modded-common/src',
|
'adapters/modded-common/src',
|
||||||
'adapters/client-common/src',
|
'adapters/client-common/src',
|
||||||
'core/src/main',
|
'core/src/main',
|
||||||
|
'core/src/testFixtures',
|
||||||
'core/build.gradle',
|
'core/build.gradle',
|
||||||
'core/agent/src',
|
'core/agent/src',
|
||||||
'core/agent/build.gradle',
|
'core/agent/build.gradle',
|
||||||
@@ -322,16 +230,24 @@ List<Object> moddedSharedInputs = []
|
|||||||
'gradle/libs.versions.toml',
|
'gradle/libs.versions.toml',
|
||||||
'gradle/volmlib-resolution.settings.gradle',
|
'gradle/volmlib-resolution.settings.gradle',
|
||||||
'gradle/volmlib-stamp.gradle',
|
'gradle/volmlib-stamp.gradle',
|
||||||
|
'gradle/java21-runtime-exclusions.gradle',
|
||||||
'gradle/generation-build-revision.gradle',
|
'gradle/generation-build-revision.gradle',
|
||||||
'gradle/modded-generation-build-revision.gradle',
|
'gradle/modded-generation-build-revision.gradle',
|
||||||
'gradle/native-minecraft-sources.gradle',
|
'gradle/native-minecraft-sources.gradle',
|
||||||
'gradle/mod-target.gradle',
|
'gradle/mod-target.gradle',
|
||||||
'gradle/mod-targets.json',
|
'gradle/mod-targets.json',
|
||||||
'gradle/modded-version-sources.gradle'
|
'gradle/runtime-cache.settings.gradle',
|
||||||
|
'gradle/nested-build.init.gradle',
|
||||||
|
'gradle/modded-version-sources.gradle',
|
||||||
|
'gradle/wrapper/gradle-wrapper.properties',
|
||||||
|
'gradle/wrapper/gradle-wrapper.jar',
|
||||||
|
'gradlew',
|
||||||
|
'gradlew.bat'
|
||||||
].each { String path -> moddedSharedInputs.add(layout.projectDirectory.file(path).asFile) }
|
].each { String path -> moddedSharedInputs.add(layout.projectDirectory.file(path).asFile) }
|
||||||
if (localVolmLibCheckout != null) {
|
if (localVolmLibCheckout != null) {
|
||||||
moddedSharedInputs.add(fileTree(localVolmLibCheckout) {
|
moddedSharedInputs.add(fileTree(localVolmLibCheckout) {
|
||||||
include('shared/src/main/**', 'native-api/src/main/**', 'native-terrain-api/src/main/**',
|
include('shared/src/main/**', 'shared/src/nativeNoise/**', 'shared/native/noise/prebuilt/**',
|
||||||
|
'native-api/src/main/**', 'native-terrain-api/src/main/**',
|
||||||
'native/**/src/main/**',
|
'native/**/src/main/**',
|
||||||
'native/minecraft-common/src/**', 'packaging/src/main/**',
|
'native/minecraft-common/src/**', 'packaging/src/main/**',
|
||||||
'**/build.gradle', 'settings.gradle', 'gradle.properties', 'gradle/libs.versions.toml')
|
'**/build.gradle', 'settings.gradle', 'gradle.properties', 'gradle/libs.versions.toml')
|
||||||
@@ -341,11 +257,11 @@ if (localVolmLibCheckout != null) {
|
|||||||
|
|
||||||
// The published master-SNAPSHOT is resolved inside the nested build, so no outer input changes when it
|
// The published master-SNAPSHOT is resolved inside the nested build, so no outer input changes when it
|
||||||
// moves. Without a local checkout the Exec tasks always run and the nested build decides what is stale.
|
// moves. Without a local checkout the Exec tasks always run and the nested build decides what is stale.
|
||||||
Closure<Void> declareVolmLibInputs = { Exec task ->
|
Closure<Void> declareVolmLibInputs = { IrisModdedBuild task ->
|
||||||
task.inputs.property('volmLibStamp', volmLibStamp)
|
task.inputs.property('volmLibStamp', volmLibStamp)
|
||||||
String nativeBillow = providers.gradleProperty('includeNativeBillow').getOrElse('false')
|
String nativeBillow = providers.gradleProperty('includeNativeBillow').getOrElse('false')
|
||||||
task.inputs.property('includeNativeBillow', nativeBillow)
|
task.inputs.property('includeNativeBillow', nativeBillow)
|
||||||
task.environment('ORG_GRADLE_PROJECT_includeNativeBillow', nativeBillow)
|
task.environment.put('ORG_GRADLE_PROJECT_includeNativeBillow', nativeBillow)
|
||||||
if (localVolmLibCheckout == null) {
|
if (localVolmLibCheckout == null) {
|
||||||
task.outputs.upToDateWhen { false }
|
task.outputs.upToDateWhen { false }
|
||||||
}
|
}
|
||||||
@@ -410,10 +326,40 @@ TaskProvider<Task> verifyModdedMatrix = tasks.register('verifyModdedMatrix') {
|
|||||||
group = 'verification'
|
group = 'verification'
|
||||||
description = 'Builds, tests and verifies the Fabric, Forge and NeoForge jars of every supported Minecraft mod target.'
|
description = 'Builds, tests and verifies the Fabric, Forge and NeoForge jars of every supported Minecraft mod target.'
|
||||||
}
|
}
|
||||||
List<TaskProvider<Copy>> moddedExports = []
|
Map<String, TaskProvider<IrisModdedBuild>> selectedModdedBuilds = [:]
|
||||||
Map<String, TaskProvider<Exec>> selectedModdedBuilds = [:]
|
int matrixParallelism = Math.min(gradle.startParameter.maxWorkerCount, Math.max(1,
|
||||||
Map<String, TaskProvider<Copy>> selectedModdedExports = [:]
|
providers.gradleProperty('moddedBuildParallelism').getOrElse(
|
||||||
TaskProvider<Exec> previousMatrixBuild = null
|
Math.min(12, Math.max(1, Runtime.runtime.availableProcessors() - 4)).toString()).toInteger()))
|
||||||
|
Provider<IrisModdedBuildService> matrixWorkers = gradle.sharedServices.registerIfAbsent(
|
||||||
|
'irisModdedBuilds', IrisModdedBuildService) {
|
||||||
|
maxParallelUsages.set(matrixParallelism)
|
||||||
|
}
|
||||||
|
List<String> nestedBuildArguments = [gradle.startParameter.buildCacheEnabled ? '--build-cache' : '--no-build-cache']
|
||||||
|
String paperweightSources = providers.gradleProperty('paperweight.experimental.genSources').orNull
|
||||||
|
if (paperweightSources != null) {
|
||||||
|
nestedBuildArguments.add("-Ppaperweight.experimental.genSources=${paperweightSources}".toString())
|
||||||
|
}
|
||||||
|
boolean multiplexorSandbox = providers.gradleProperty('multiplexorSandbox').getOrElse('false').toBoolean()
|
||||||
|
int nestedWorkers = multiplexorSandbox
|
||||||
|
? Math.max(1, gradle.startParameter.maxWorkerCount.intdiv(matrixParallelism))
|
||||||
|
: Math.min(2, gradle.startParameter.maxWorkerCount)
|
||||||
|
int nestedProcessors = multiplexorSandbox ? nestedWorkers : 2
|
||||||
|
if (multiplexorSandbox) {
|
||||||
|
nestedBuildArguments.addAll(['--no-daemon', '-PmultiplexorSandbox=true'])
|
||||||
|
}
|
||||||
|
String nestedJvmArguments = providers.gradleProperty('org.gradle.jvmargs').getOrElse('') +
|
||||||
|
" -XX:ActiveProcessorCount=${nestedProcessors}"
|
||||||
|
if (gradle.startParameter.rerunTasks) {
|
||||||
|
nestedBuildArguments.add('--rerun-tasks')
|
||||||
|
}
|
||||||
|
if (gradle.startParameter.refreshDependencies) {
|
||||||
|
nestedBuildArguments.add('--refresh-dependencies')
|
||||||
|
}
|
||||||
|
for (File initScript : gradle.startParameter.initScripts) {
|
||||||
|
nestedBuildArguments.addAll(['--init-script', initScript.absolutePath])
|
||||||
|
}
|
||||||
|
Map<Integer, TaskProvider<IrisModdedBuild>> daemonSlots = [:]
|
||||||
|
int nextDaemonSlot = 0
|
||||||
irisModTargets.each { String targetMinecraft, Map<String, Object> target ->
|
irisModTargets.each { String targetMinecraft, Map<String, Object> target ->
|
||||||
String suffix = targetMinecraft.replace('.', '_')
|
String suffix = targetMinecraft.replace('.', '_')
|
||||||
boolean selected = targetMinecraft == minecraftVersion
|
boolean selected = targetMinecraft == minecraftVersion
|
||||||
@@ -424,67 +370,68 @@ irisModTargets.each { String targetMinecraft, Map<String, Object> target ->
|
|||||||
String artifactName = irisArtifactName(label, "${targetMinecraft}+${displayedLoader}")
|
String artifactName = irisArtifactName(label, "${targetMinecraft}+${displayedLoader}")
|
||||||
File artifact = file("adapters/${loader}/build/${targetMinecraft}/libs/${artifactName}")
|
File artifact = file("adapters/${loader}/build/${targetMinecraft}/libs/${artifactName}")
|
||||||
boolean unitTests = ((List<String>) (target.testLoaders ?: moddedLoaderLabels.keySet().toList())).contains(loader)
|
boolean unitTests = ((List<String>) (target.testLoaders ?: moddedLoaderLabels.keySet().toList())).contains(loader)
|
||||||
TaskProvider<Exec> buildTarget = tasks.register("verify${label}${suffix}", Exec) {
|
int daemonSlot = nextDaemonSlot++ % matrixParallelism
|
||||||
|
TaskProvider<IrisModdedBuild> buildTarget = project(':adapters').tasks.register("verify${label}${suffix}", IrisModdedBuild) {
|
||||||
group = 'verification'
|
group = 'verification'
|
||||||
description = "Builds, tests and verifies the ${label} ${targetMinecraft} jar in its own Gradle build."
|
description = "Builds, tests and verifies the ${label} ${targetMinecraft} jar in its own Gradle build."
|
||||||
workingDir = file("adapters/${loader}")
|
usesService(matrixWorkers)
|
||||||
List<String> command = [file(moddedWrapperScript).absolutePath, 'assemble', 'compileTestJava',
|
workingDirectory.set(rootProject.file("adapters/${loader}"))
|
||||||
|
getLabel().set("${label} ${targetMinecraft}".toString())
|
||||||
|
artifactFile.set(artifact)
|
||||||
|
logFile.set(rootProject.layout.buildDirectory.file("reports/modded/${loader}-${targetMinecraft}.log"))
|
||||||
|
requiredEntries.set(commonModdedArtifactEntries + moddedLoaderEntries[loader])
|
||||||
|
forbiddenCoreClasses.set(bukkitCoupledCoreClasses)
|
||||||
|
javaRuntime.set((int) target.java)
|
||||||
|
List<String> command = [rootProject.file(moddedWrapperScript).absolutePath, 'assemble', 'compileTestJava',
|
||||||
'verifyLoggingPolicy', 'verifyNativeClassBoundary', '--console=plain',
|
'verifyLoggingPolicy', 'verifyNativeClassBoundary', '--console=plain',
|
||||||
"-PirisVersion=${project.version}", "-PminecraftVersion=${targetMinecraft}",
|
"--max-workers=${nestedWorkers}",
|
||||||
|
"-Dorg.gradle.jvmargs=${nestedJvmArguments.trim()}",
|
||||||
|
"-Dorg.gradle.daemon.registry.base=${rootProject.file(".gradle/iris-daemons/slot-${daemonSlot}").absolutePath}",
|
||||||
|
'-Dorg.gradle.daemon.idletimeout=3600000',
|
||||||
|
"-PirisBuildLane=${loader}-${targetMinecraft}",
|
||||||
|
'--project-cache-dir', rootProject.file("adapters/${loader}/.gradle/iris-lanes/${loader}-${targetMinecraft}").absolutePath,
|
||||||
|
'--init-script', rootProject.file('gradle/nested-build.init.gradle').absolutePath,
|
||||||
|
"-PirisVersion=${rootProject.version}", "-PminecraftVersion=${targetMinecraft}",
|
||||||
"-P${moddedLoaderProperties[loader]}=${loaderPin}",
|
"-P${moddedLoaderProperties[loader]}=${loaderPin}",
|
||||||
"-PuseLocalVolmLib=${localVolmLibCheckout != null}"]
|
"-PuseLocalVolmLib=${localVolmLibCheckout != null}"]
|
||||||
if (unitTests) {
|
if (unitTests) {
|
||||||
command.add('test')
|
command.add('test')
|
||||||
}
|
}
|
||||||
commandLine(command)
|
command.add("-PtestForks=${providers.gradleProperty('moddedTestForks').getOrElse('1')}")
|
||||||
|
command.addAll(nestedBuildArguments)
|
||||||
|
commandLine.set(command.collect { Object argument -> argument.toString() })
|
||||||
if (localVolmLibCheckout != null) {
|
if (localVolmLibCheckout != null) {
|
||||||
environment('ORG_GRADLE_PROJECT_localVolmLibDirectory', localVolmLibCheckout.absolutePath)
|
environment.put('ORG_GRADLE_PROJECT_localVolmLibDirectory', localVolmLibCheckout.absolutePath)
|
||||||
}
|
}
|
||||||
inputs.files(moddedSharedInputs).withPropertyName('moddedSharedSources')
|
inputs.files(moddedSharedInputs).withPropertyName('moddedSharedSources')
|
||||||
.withPathSensitivity(PathSensitivity.RELATIVE)
|
.withPathSensitivity(PathSensitivity.RELATIVE)
|
||||||
inputs.files(file("adapters/${loader}/src"), file("adapters/${loader}/build.gradle"),
|
inputs.files(rootProject.file("adapters/${loader}/src"), rootProject.file("adapters/${loader}/build.gradle"),
|
||||||
file("adapters/${loader}/settings.gradle")).withPropertyName('adapterSources')
|
rootProject.file("adapters/${loader}/settings.gradle"), rootProject.file("adapters/${loader}/gradle.properties"))
|
||||||
|
.withPropertyName('adapterSources')
|
||||||
|
inputs.files(gradle.startParameter.allInitScripts).withPropertyName('initScripts')
|
||||||
|
.withPathSensitivity(PathSensitivity.NONE)
|
||||||
|
outputs.upToDateWhen { !gradle.startParameter.refreshDependencies }
|
||||||
inputs.properties(target)
|
inputs.properties(target)
|
||||||
inputs.property('loaderVersion', loaderPin)
|
inputs.property('loaderVersion', loaderPin)
|
||||||
inputs.property('irisVersion', project.version.toString())
|
inputs.property('irisVersion', rootProject.version.toString())
|
||||||
declareVolmLibInputs(it)
|
declareVolmLibInputs(it)
|
||||||
outputs.file(artifact)
|
|
||||||
if (unitTests) {
|
if (unitTests) {
|
||||||
outputs.dir(file("adapters/${loader}/build/${targetMinecraft}/test-results/test"))
|
outputs.dir(rootProject.file("adapters/${loader}/build/${targetMinecraft}/test-results/test"))
|
||||||
}
|
|
||||||
doLast {
|
|
||||||
ModdedArtifactVerifier.verify(artifact, commonModdedArtifactEntries + moddedLoaderEntries[loader],
|
|
||||||
bukkitCoupledCoreClasses)
|
|
||||||
ModdedArtifactVerifier.verifyJavaRuntime(artifact, (int) target.java)
|
|
||||||
logger.lifecycle("Verified ${artifact.name}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (previousMatrixBuild != null) {
|
TaskProvider<IrisModdedBuild> previousSlotBuild = daemonSlots[daemonSlot]
|
||||||
TaskProvider<Exec> preceding = previousMatrixBuild
|
if (previousSlotBuild != null) {
|
||||||
buildTarget.configure { mustRunAfter(preceding) }
|
buildTarget.configure { mustRunAfter(previousSlotBuild) }
|
||||||
}
|
}
|
||||||
previousMatrixBuild = buildTarget
|
daemonSlots[daemonSlot] = buildTarget
|
||||||
verifyModdedMatrix.configure { dependsOn(buildTarget) }
|
tasks.register("verify${label}${suffix}") {
|
||||||
File legacyOutJar = new File(pluginOutDirectory, "Iris-${project.version}+mc${targetMinecraft}-${loader}.jar")
|
group = 'verification'
|
||||||
FileTree staleTargetJars = fileTree(pluginOutDirectory) {
|
description = "Builds, tests and verifies the ${label} ${targetMinecraft} jar."
|
||||||
include("Iris v* [${label}] ${targetMinecraft}+*.jar")
|
|
||||||
}
|
|
||||||
TaskProvider<Copy> export = tasks.register("build${label}${suffix}", Copy) {
|
|
||||||
group = 'iris'
|
|
||||||
description = "Exports the verified ${label} ${targetMinecraft} jar to the plugin output directory."
|
|
||||||
dependsOn(buildTarget)
|
dependsOn(buildTarget)
|
||||||
from(artifact)
|
|
||||||
into(pluginOutDirectory)
|
|
||||||
doFirst {
|
|
||||||
irisFileOperations.fileSystemOperations.delete { DeleteSpec spec ->
|
|
||||||
spec.delete(staleTargetJars, legacyOutJar)
|
|
||||||
}
|
}
|
||||||
}
|
verifyModdedMatrix.configure { dependsOn(buildTarget) }
|
||||||
}
|
|
||||||
moddedExports.add(export)
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
selectedModdedBuilds[loader] = buildTarget
|
selectedModdedBuilds[loader] = buildTarget
|
||||||
selectedModdedExports[loader] = export
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -492,38 +439,37 @@ if (selectedModdedBuilds.isEmpty()) {
|
|||||||
throw new GradleException("Minecraft ${minecraftVersion} is not a mod target in gradle/mod-targets.json")
|
throw new GradleException("Minecraft ${minecraftVersion} is not a mod target in gradle/mod-targets.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.register('prepareRelease', Sync) {
|
||||||
|
group = 'distribution'
|
||||||
|
description = 'Builds, verifies and stages one selected mod loader jar for publication.'
|
||||||
|
Provider<IrisModdedBuild> releaseBuild = providers.provider {
|
||||||
|
String releaseLoader = providers.gradleProperty('releaseLoader').orNull
|
||||||
|
if (!moddedLoaderLabels.containsKey(releaseLoader)) {
|
||||||
|
throw new GradleException('prepareRelease requires -PreleaseLoader=fabric, forge or neoforge')
|
||||||
|
}
|
||||||
|
return selectedModdedBuilds[releaseLoader].get()
|
||||||
|
}
|
||||||
|
dependsOn(releaseBuild)
|
||||||
|
from(releaseBuild.flatMap { IrisModdedBuild task -> task.artifactFile })
|
||||||
|
into(layout.buildDirectory.dir('release'))
|
||||||
|
}
|
||||||
|
|
||||||
tasks.register('buildFabric') {
|
tasks.register('buildFabric') {
|
||||||
group = 'iris'
|
group = 'iris'
|
||||||
description = "Exports the verified Fabric ${minecraftVersion} jar."
|
description = "Builds and verifies the Fabric ${minecraftVersion} jar."
|
||||||
dependsOn(selectedModdedExports.fabric)
|
dependsOn(selectedModdedBuilds.fabric)
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('buildForge') {
|
tasks.register('buildForge') {
|
||||||
group = 'iris'
|
group = 'iris'
|
||||||
description = "Exports the verified Forge ${minecraftVersion} jar."
|
description = "Builds and verifies the Forge ${minecraftVersion} jar."
|
||||||
dependsOn(selectedModdedExports.forge)
|
dependsOn(selectedModdedBuilds.forge)
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('buildNeoforge') {
|
tasks.register('buildNeoforge') {
|
||||||
group = 'iris'
|
group = 'iris'
|
||||||
description = "Exports the verified NeoForge ${minecraftVersion} jar."
|
description = "Builds and verifies the NeoForge ${minecraftVersion} jar."
|
||||||
dependsOn(selectedModdedExports.neoforge)
|
dependsOn(selectedModdedBuilds.neoforge)
|
||||||
}
|
|
||||||
|
|
||||||
gradle.projectsEvaluated {
|
|
||||||
List<Task> sharedOutputTasks = allprojects.collectMany { Project candidate ->
|
|
||||||
candidate.tasks.findAll { Task task ->
|
|
||||||
task instanceof JavaCompile || task instanceof Jar || task instanceof Test
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tasks.withType(Exec).matching { Task task -> task.name.startsWith('verify') }.configureEach {
|
|
||||||
mustRunAfter(sharedOutputTasks)
|
|
||||||
mustRunAfter(tasks.named('buildBukkit'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named('check').configure {
|
|
||||||
dependsOn(selectedModdedBuilds.fabric)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('checkAll') {
|
tasks.register('checkAll') {
|
||||||
@@ -608,67 +554,10 @@ tasks.named('buildBukkit').configure {
|
|||||||
dependsOn('verifyBukkitArtifact')
|
dependsOn('verifyBukkitArtifact')
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named('iris').configure {
|
|
||||||
dependsOn('verifyBukkitArtifact')
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register('buildAllToOut') {
|
|
||||||
group = 'iris'
|
|
||||||
description = 'Builds and verifies the Bukkit jar and every mod target jar into the plugin output directory.'
|
|
||||||
dependsOn('buildBukkit', ':spi:jar')
|
|
||||||
dependsOn(moddedExports)
|
|
||||||
doLast {
|
|
||||||
File[] jars = pluginOutDirectory.listFiles((java.io.FileFilter) { File f -> f.name.startsWith('Iris v') && f.name.endsWith('.jar') })
|
|
||||||
println('')
|
|
||||||
println('=== Iris buildAllToOut -> ' + pluginOutDirectory + ' ===')
|
|
||||||
if (jars != null) {
|
|
||||||
jars.sort { it.name }.each { File f ->
|
|
||||||
println(String.format(' %-60s %8d KB', f.name, (long) (f.length() / 1024)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register('buildAll') {
|
tasks.register('buildAll') {
|
||||||
group = 'iris'
|
group = 'build'
|
||||||
outputs.upToDateWhen { false }
|
description = 'Builds and verifies the Bukkit plugin, API, SPI and every supported mod target.'
|
||||||
dependsOn('iris', 'buildAllToOut')
|
dependsOn('buildBukkit', 'irisApi', ':spi:jar', verifyModdedMatrix)
|
||||||
Map<String, List<Object>> consumerDrops = [
|
|
||||||
(bukkitConsumerPath) : ['Iris.jar', consumerBukkitArtifact],
|
|
||||||
(fabricConsumerPath) : ['Iris-Fabric.jar', fabricArtifact],
|
|
||||||
(forgeConsumerPath) : ['Iris-Forge.jar', forgeArtifact],
|
|
||||||
(neoForgeConsumerPath): ['Iris-Neoforge.jar', neoForgeArtifact]
|
|
||||||
]
|
|
||||||
doLast {
|
|
||||||
consumerDrops.each { String consumerPath, List<Object> drop ->
|
|
||||||
String keep = (String) drop.get(0)
|
|
||||||
File source = (File) drop.get(1)
|
|
||||||
irisFileOperations.fileSystemOperations.delete { DeleteSpec spec ->
|
|
||||||
consumerDrops.values().each { List<Object> other ->
|
|
||||||
String name = (String) other.get(0)
|
|
||||||
if (name != keep) {
|
|
||||||
spec.delete(new File(consumerPath, name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
irisFileOperations.fileSystemOperations.copy { CopySpec spec ->
|
|
||||||
spec.from(source)
|
|
||||||
spec.into(consumerPath)
|
|
||||||
spec.rename { String ignored -> keep }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
println('')
|
|
||||||
println("=== Iris buildAll -> ${consumerLocation} ===")
|
|
||||||
println(" ${bukkitConsumerPath}/Iris.jar")
|
|
||||||
println(" ${fabricConsumerPath}/Iris-Fabric.jar")
|
|
||||||
println(" ${forgeConsumerPath}/Iris-Forge.jar")
|
|
||||||
println(" ${neoForgeConsumerPath}/Iris-Neoforge.jar")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tasks.register('buildPsychoLT') {
|
|
||||||
group = 'development'
|
|
||||||
description = 'Builds all verified Iris platform jars and stages them in the local test-server dropins and PluginOuts.'
|
|
||||||
dependsOn('buildAll')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('irisDev', Copy) {
|
tasks.register('irisDev', Copy) {
|
||||||
@@ -688,19 +577,23 @@ if (!(irisRuntimeJava in [21, 25])) {
|
|||||||
throw new GradleException('irisRuntimeJava must be 21 or 25')
|
throw new GradleException('irisRuntimeJava must be 21 or 25')
|
||||||
}
|
}
|
||||||
Set<String> sharedRuntimeProjects = [':core', ':core:agent', ':spi'] as Set<String>
|
Set<String> sharedRuntimeProjects = [':core', ':core:agent', ':spi'] as Set<String>
|
||||||
|
String runtimeOutputVariant = gradle.extensions.extraProperties.get('irisRuntimeOutputVariant').toString()
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
apply plugin: 'java'
|
apply plugin: 'java'
|
||||||
boolean java21Runtime = irisRuntimeJava == 21 && sharedRuntimeProjects.contains(project.path)
|
boolean java21Runtime = irisRuntimeJava == 21 && sharedRuntimeProjects.contains(project.path)
|
||||||
if (java21Runtime) {
|
if (!runtimeOutputVariant.isEmpty() && sharedRuntimeProjects.contains(project.path)) {
|
||||||
layout.buildDirectory.set(layout.projectDirectory.dir('build/java21'))
|
layout.buildDirectory.set(layout.projectDirectory.dir("build/${runtimeOutputVariant}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply from: rootProject.file('gradle/mockito-agent.gradle')
|
||||||
|
|
||||||
tasks.withType(Test).configureEach {
|
tasks.withType(Test).configureEach {
|
||||||
int moduleForks = project.path.startsWith(':adapters:bukkit:') ? Math.min(2, testForks)
|
int moduleForks = project.path.startsWith(':adapters:bukkit:') ? Math.min(2, testForks)
|
||||||
: project.path == ':probe' ? Math.min(4, testForks) : testForks
|
: project.path == ':probe' ? Math.min(4, testForks) : testForks
|
||||||
maxParallelForks = Math.min(gradle.startParameter.maxWorkerCount, moduleForks)
|
maxParallelForks = Math.min(gradle.startParameter.maxWorkerCount, moduleForks)
|
||||||
jvmArgs('-XX:ParallelGCThreads=2', '-XX:ConcGCThreads=1', '-XX:CICompilerCount=2')
|
jvmArgs('-XX:ParallelGCThreads=2', '-XX:ConcGCThreads=1', '-XX:CICompilerCount=2')
|
||||||
|
timeout = java.time.Duration.ofMinutes(60)
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
@@ -1038,3 +931,5 @@ tasks.withType(PackedJar).configureEach {
|
|||||||
NativeRuntimeArtifacts.verifyPacked(task.outputJar.get().asFile, nativeRuntimeModules, embeddedNatives())
|
NativeRuntimeArtifacts.verifyPacked(task.outputJar.get().asFile, nativeRuntimeModules, embeddedNatives())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply from: file('gradle/multiplexor.gradle')
|
||||||
@@ -21,11 +21,14 @@ repositories {
|
|||||||
// Nothing else asks buildSrc for `test`: Gradle only requests the jar that carries the build logic.
|
// Nothing else asks buildSrc for `test`: Gradle only requests the jar that carries the build logic.
|
||||||
// Gating the jar on the tests is what makes them run at all, and it costs nothing once they are green.
|
// Gating the jar on the tests is what makes them run at all, and it costs nothing once they are green.
|
||||||
tasks.named('jar').configure {
|
tasks.named('jar').configure {
|
||||||
|
if (!gradle.extensions.extraProperties.has('irisBuildLane')) {
|
||||||
dependsOn(tasks.named('test'))
|
dependsOn(tasks.named('test'))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tasks.named('test').configure {
|
tasks.named('test').configure {
|
||||||
jvmArgs('--add-opens=java.base/java.lang=ALL-UNNAMED')
|
jvmArgs('--add-opens=java.base/java.lang=ALL-UNNAMED')
|
||||||
|
jvmArgs('-XX:ParallelGCThreads=2', '-XX:ConcGCThreads=1', '-XX:CICompilerCount=2')
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
@@ -37,11 +37,6 @@ public class ApiGenerator implements Plugin<Project> {
|
|||||||
throw new GradleException("Publishing extension not found");
|
throw new GradleException("Publishing extension not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
publishing.getRepositories().maven(repository -> {
|
|
||||||
repository.setName("deployDir");
|
|
||||||
repository.setUrl(targetDirectory(target).toURI());
|
|
||||||
});
|
|
||||||
|
|
||||||
publishing.getPublications().create("maven", MavenPublication.class, publication -> {
|
publishing.getPublications().create("maven", MavenPublication.class, publication -> {
|
||||||
publication.setGroupId(target.getGroup().toString());
|
publication.setGroupId(target.getGroup().toString());
|
||||||
publication.setArtifactId("iris");
|
publication.setArtifactId("iris");
|
||||||
@@ -49,27 +44,19 @@ public class ApiGenerator implements Plugin<Project> {
|
|||||||
publication.artifact(task);
|
publication.artifact(task);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static File targetDirectory(Project project) {
|
|
||||||
String dir = System.getenv("DEPLOY_DIR");
|
|
||||||
if (dir == null) {
|
|
||||||
return project.getLayout().getBuildDirectory().dir("api").get().getAsFile();
|
|
||||||
}
|
|
||||||
return new File(dir);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class GenerateApiTask extends DefaultTask {
|
abstract class GenerateApiTask extends DefaultTask {
|
||||||
public GenerateApiTask() {
|
public GenerateApiTask() {
|
||||||
setGroup("iris");
|
setGroup("iris");
|
||||||
dependsOn("jar");
|
dependsOn("jar");
|
||||||
finalizedBy("publishMavenPublicationToDeployDirRepository");
|
|
||||||
doLast(task -> getLogger().lifecycle("The API is located at " + getOutputFile().get().getAsFile().getAbsolutePath()));
|
doLast(task -> getLogger().lifecycle("The API is located at " + getOutputFile().get().getAsFile().getAbsolutePath()));
|
||||||
|
|
||||||
TaskProvider<Jar> jarTask = getProject().getTasks().named("jar", Jar.class);
|
TaskProvider<Jar> jarTask = getProject().getTasks().named("jar", Jar.class);
|
||||||
getInputFile().convention(jarTask.flatMap(Jar::getArchiveFile));
|
getInputFile().convention(jarTask.flatMap(Jar::getArchiveFile));
|
||||||
getOutputFile().convention(getProject().getLayout().file(getInputFile().getLocationOnly().map(input ->
|
getOutputFile().convention(getProject().getLayout().file(getInputFile().getLocationOnly().map(input ->
|
||||||
new File(ApiGenerator.targetDirectory(getProject()), input.getAsFile().getName()))));
|
new File(getProject().getLayout().getBuildDirectory().dir("api").get().getAsFile(),
|
||||||
|
input.getAsFile().getName()))));
|
||||||
}
|
}
|
||||||
|
|
||||||
@InputFile
|
@InputFile
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import org.gradle.api.DefaultTask;
|
||||||
|
import org.gradle.api.file.DirectoryProperty;
|
||||||
|
import org.gradle.api.file.RegularFileProperty;
|
||||||
|
import org.gradle.api.provider.ListProperty;
|
||||||
|
import org.gradle.api.provider.MapProperty;
|
||||||
|
import org.gradle.api.provider.Property;
|
||||||
|
import org.gradle.api.provider.SetProperty;
|
||||||
|
import org.gradle.api.tasks.Input;
|
||||||
|
import org.gradle.api.tasks.Internal;
|
||||||
|
import org.gradle.api.tasks.LocalState;
|
||||||
|
import org.gradle.api.tasks.OutputFile;
|
||||||
|
import org.gradle.api.tasks.TaskAction;
|
||||||
|
import org.gradle.workers.WorkerExecutor;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public abstract class IrisModdedBuild extends DefaultTask {
|
||||||
|
public IrisModdedBuild() {
|
||||||
|
getEnvironment().convention(Map.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input
|
||||||
|
public abstract Property<String> getLabel();
|
||||||
|
|
||||||
|
@Internal
|
||||||
|
public abstract DirectoryProperty getWorkingDirectory();
|
||||||
|
|
||||||
|
@Input
|
||||||
|
public abstract ListProperty<String> getCommandLine();
|
||||||
|
|
||||||
|
@Input
|
||||||
|
public abstract MapProperty<String, String> getEnvironment();
|
||||||
|
|
||||||
|
@Input
|
||||||
|
public abstract ListProperty<String> getRequiredEntries();
|
||||||
|
|
||||||
|
@Input
|
||||||
|
public abstract SetProperty<String> getForbiddenCoreClasses();
|
||||||
|
|
||||||
|
@Input
|
||||||
|
public abstract Property<Integer> getJavaRuntime();
|
||||||
|
|
||||||
|
@OutputFile
|
||||||
|
public abstract RegularFileProperty getArtifactFile();
|
||||||
|
|
||||||
|
@LocalState
|
||||||
|
public abstract RegularFileProperty getLogFile();
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
protected abstract WorkerExecutor getWorkerExecutor();
|
||||||
|
|
||||||
|
@TaskAction
|
||||||
|
public void build() {
|
||||||
|
getWorkerExecutor().noIsolation().submit(IrisModdedBuildAction.class, parameters -> {
|
||||||
|
parameters.getLabel().set(getLabel());
|
||||||
|
parameters.getWorkingDirectory().set(getWorkingDirectory());
|
||||||
|
parameters.getCommandLine().set(getCommandLine());
|
||||||
|
parameters.getEnvironment().set(getEnvironment());
|
||||||
|
parameters.getRequiredEntries().set(getRequiredEntries());
|
||||||
|
parameters.getForbiddenCoreClasses().set(getForbiddenCoreClasses());
|
||||||
|
parameters.getJavaRuntime().set(getJavaRuntime());
|
||||||
|
parameters.getArtifactFile().set(getArtifactFile());
|
||||||
|
parameters.getLogFile().set(getLogFile());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import org.gradle.api.GradleException;
|
||||||
|
import org.gradle.api.logging.Logger;
|
||||||
|
import org.gradle.api.logging.Logging;
|
||||||
|
import org.gradle.process.ExecOperations;
|
||||||
|
import org.gradle.workers.WorkAction;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public abstract class IrisModdedBuildAction implements WorkAction<IrisModdedBuildParameters> {
|
||||||
|
private static final Logger LOGGER = Logging.getLogger(IrisModdedBuildAction.class);
|
||||||
|
private static final int LOG_TAIL_BYTES = 16 * 1024;
|
||||||
|
private static final int LOG_TAIL_LINES = 40;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
protected abstract ExecOperations getExecOperations();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute() {
|
||||||
|
IrisModdedBuildParameters parameters = getParameters();
|
||||||
|
String label = parameters.getLabel().get();
|
||||||
|
Path log = parameters.getLogFile().get().getAsFile().toPath();
|
||||||
|
long started = System.nanoTime();
|
||||||
|
LOGGER.lifecycle("{} started; log: {}", label, log);
|
||||||
|
try {
|
||||||
|
Files.createDirectories(log.getParent());
|
||||||
|
try (OutputStream output = Files.newOutputStream(log)) {
|
||||||
|
getExecOperations().exec(spec -> {
|
||||||
|
spec.setWorkingDir(parameters.getWorkingDirectory().get().getAsFile());
|
||||||
|
spec.commandLine(parameters.getCommandLine().get());
|
||||||
|
spec.environment(parameters.getEnvironment().get());
|
||||||
|
spec.setStandardOutput(output);
|
||||||
|
spec.setErrorOutput(output);
|
||||||
|
}).assertNormalExitValue();
|
||||||
|
}
|
||||||
|
ModdedArtifactVerifier.verify(parameters.getArtifactFile().get().getAsFile(),
|
||||||
|
parameters.getRequiredEntries().get(), parameters.getForbiddenCoreClasses().get());
|
||||||
|
ModdedArtifactVerifier.verifyJavaRuntime(parameters.getArtifactFile().get().getAsFile(),
|
||||||
|
parameters.getJavaRuntime().get());
|
||||||
|
LOGGER.lifecycle("{} succeeded in {} ms", label,
|
||||||
|
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - started));
|
||||||
|
} catch (Exception failure) {
|
||||||
|
appendFailure(log, failure);
|
||||||
|
String details;
|
||||||
|
try {
|
||||||
|
details = logTail(log);
|
||||||
|
} catch (IOException unreadable) {
|
||||||
|
failure.addSuppressed(unreadable);
|
||||||
|
details = "Log could not be read.";
|
||||||
|
}
|
||||||
|
LOGGER.error(label + " failed; log: " + log + System.lineSeparator() + details, failure);
|
||||||
|
throw new GradleException(label + " failed; log: " + log, failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String logTail(Path log) throws IOException {
|
||||||
|
try (RandomAccessFile input = new RandomAccessFile(log.toFile(), "r")) {
|
||||||
|
long offset = Math.max(0L, input.length() - LOG_TAIL_BYTES);
|
||||||
|
input.seek(offset);
|
||||||
|
byte[] bytes = new byte[(int) (input.length() - offset)];
|
||||||
|
input.readFully(bytes);
|
||||||
|
String[] lines = new String(bytes, StandardCharsets.UTF_8).split("\\R");
|
||||||
|
return String.join(System.lineSeparator(), Arrays.copyOfRange(lines,
|
||||||
|
Math.max(0, lines.length - LOG_TAIL_LINES), lines.length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void appendFailure(Path log, Exception failure) {
|
||||||
|
try (PrintWriter output = new PrintWriter(Files.newBufferedWriter(log, StandardCharsets.UTF_8,
|
||||||
|
StandardOpenOption.CREATE, StandardOpenOption.APPEND))) {
|
||||||
|
failure.printStackTrace(output);
|
||||||
|
} catch (IOException unwritable) {
|
||||||
|
failure.addSuppressed(unwritable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import org.gradle.api.file.DirectoryProperty;
|
||||||
|
import org.gradle.api.file.RegularFileProperty;
|
||||||
|
import org.gradle.api.provider.ListProperty;
|
||||||
|
import org.gradle.api.provider.MapProperty;
|
||||||
|
import org.gradle.api.provider.Property;
|
||||||
|
import org.gradle.api.provider.SetProperty;
|
||||||
|
import org.gradle.workers.WorkParameters;
|
||||||
|
|
||||||
|
public interface IrisModdedBuildParameters extends WorkParameters {
|
||||||
|
Property<String> getLabel();
|
||||||
|
|
||||||
|
DirectoryProperty getWorkingDirectory();
|
||||||
|
|
||||||
|
ListProperty<String> getCommandLine();
|
||||||
|
|
||||||
|
MapProperty<String, String> getEnvironment();
|
||||||
|
|
||||||
|
ListProperty<String> getRequiredEntries();
|
||||||
|
|
||||||
|
SetProperty<String> getForbiddenCoreClasses();
|
||||||
|
|
||||||
|
Property<Integer> getJavaRuntime();
|
||||||
|
|
||||||
|
RegularFileProperty getArtifactFile();
|
||||||
|
|
||||||
|
RegularFileProperty getLogFile();
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import org.gradle.api.services.BuildService;
|
||||||
|
import org.gradle.api.services.BuildServiceParameters;
|
||||||
|
|
||||||
|
public abstract class IrisModdedBuildService implements BuildService<BuildServiceParameters.None> {
|
||||||
|
}
|
||||||
@@ -231,11 +231,11 @@ public final class GenerationBuildRevision {
|
|||||||
throw new IOException("Duplicate dependency artifact entry: " + name);
|
throw new IOException("Duplicate dependency artifact entry: " + name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
byte[] buffer = new byte[16 * 1024];
|
||||||
for (Map.Entry<String, ZipEntry> entry : entries.entrySet()) {
|
for (Map.Entry<String, ZipEntry> entry : entries.entrySet()) {
|
||||||
update(digest, entry.getKey());
|
update(digest, entry.getKey());
|
||||||
try (InputStream input = zip.getInputStream(entry.getValue())) {
|
try (InputStream input = zip.getInputStream(entry.getValue())) {
|
||||||
MessageDigest content = digest();
|
MessageDigest content = digest();
|
||||||
byte[] buffer = new byte[16 * 1024];
|
|
||||||
int read;
|
int read;
|
||||||
while ((read = input.read(buffer)) != -1) {
|
while ((read = input.read(buffer)) != -1) {
|
||||||
content.update(buffer, 0, read);
|
content.update(buffer, 0, read);
|
||||||
|
|||||||
@@ -50,9 +50,12 @@ public class ApiGeneratorTest {
|
|||||||
|
|
||||||
assertEquals(jar.getArchiveFile().get().getAsFile(), api.getInputFile().get().getAsFile());
|
assertEquals(jar.getArchiveFile().get().getAsFile(), api.getInputFile().get().getAsFile());
|
||||||
assertEquals(jar.getArchiveFileName().get(), api.getOutputFile().get().getAsFile().getName());
|
assertEquals(jar.getArchiveFileName().get(), api.getOutputFile().get().getAsFile().getName());
|
||||||
assertEquals(ApiGenerator.targetDirectory(project), api.getOutputFile().get().getAsFile().getParentFile());
|
assertEquals(project.getLayout().getBuildDirectory().dir("api").get().getAsFile(),
|
||||||
|
api.getOutputFile().get().getAsFile().getParentFile());
|
||||||
|
assertTrue(api.getFinalizedBy().getDependencies(api).isEmpty());
|
||||||
|
|
||||||
PublishingExtension publishing = project.getExtensions().getByType(PublishingExtension.class);
|
PublishingExtension publishing = project.getExtensions().getByType(PublishingExtension.class);
|
||||||
|
assertTrue(publishing.getRepositories().isEmpty());
|
||||||
MavenPublication publication = (MavenPublication) publishing.getPublications().getByName("maven");
|
MavenPublication publication = (MavenPublication) publishing.getPublications().getByName("maven");
|
||||||
assertEquals("art.arcane", publication.getGroupId());
|
assertEquals("art.arcane", publication.getGroupId());
|
||||||
assertEquals("iris", publication.getArtifactId());
|
assertEquals("iris", publication.getArtifactId());
|
||||||
|
|||||||
@@ -58,6 +58,22 @@ public class GenerationBuildRevisionTest {
|
|||||||
GenerationBuildRevision.capture(fixture.options(), dependencies)));
|
GenerationBuildRevision.capture(fixture.options(), dependencies)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void hashesLargeShortAndEmptyEntriesIndependently() throws Exception {
|
||||||
|
Path dependency = temporary.newFile("multiple-entries.jar").toPath();
|
||||||
|
Map<String, String> contents = Map.of("example/Large.class", "abc".repeat(11_000),
|
||||||
|
"example/Short.class", "tail", "example/Zero.class", "");
|
||||||
|
try (ZipOutputStream output = new ZipOutputStream(Files.newOutputStream(dependency))) {
|
||||||
|
for (String name : List.of("example/Zero.class", "example/Short.class", "example/Large.class")) {
|
||||||
|
output.putNextEntry(new ZipEntry(name));
|
||||||
|
output.write(contents.get(name).getBytes(StandardCharsets.UTF_8));
|
||||||
|
output.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertEquals("49217af891afa3111d1f70a390acacc8bad700528b61b1ecc5637c060aeb52cd",
|
||||||
|
GenerationBuildRevision.artifactHash(dependency));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void rejectsChangedAddedAndDeletedGenerationSources() throws Exception {
|
public void rejectsChangedAddedAndDeletedGenerationSources() throws Exception {
|
||||||
Fixture fixture = fixture();
|
Fixture fixture = fixture();
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import org.gradle.api.Project;
|
||||||
|
import org.gradle.testfixtures.ProjectBuilder;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class IrisModdedBuildTest {
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporary = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void capturesTheCompleteTargetConfiguration() throws Exception {
|
||||||
|
Project project = ProjectBuilder.builder().withProjectDir(temporary.newFolder()).build();
|
||||||
|
IrisModdedBuild task = project.getTasks().create("verifyFabric", IrisModdedBuild.class);
|
||||||
|
task.getLabel().set("Fabric 26.3");
|
||||||
|
task.getWorkingDirectory().set(project.getLayout().getProjectDirectory());
|
||||||
|
task.getCommandLine().set(List.of("gradlew", "assemble", "test"));
|
||||||
|
task.getRequiredEntries().set(List.of("fabric.mod.json"));
|
||||||
|
task.getForbiddenCoreClasses().set(Set.of("org/bukkit/Test"));
|
||||||
|
task.getJavaRuntime().set(25);
|
||||||
|
task.getArtifactFile().set(project.getLayout().getProjectDirectory().file("build/mod.jar"));
|
||||||
|
task.getLogFile().set(project.getLayout().getProjectDirectory().file("build/reports/matrix/fabric.log"));
|
||||||
|
|
||||||
|
assertEquals(Map.of(), task.getEnvironment().get());
|
||||||
|
assertEquals(List.of("gradlew", "assemble", "test"), task.getCommandLine().get());
|
||||||
|
assertEquals(List.of("fabric.mod.json"), task.getRequiredEntries().get());
|
||||||
|
assertEquals(Set.of("org/bukkit/Test"), task.getForbiddenCoreClasses().get());
|
||||||
|
assertEquals(Integer.valueOf(25), task.getJavaRuntime().get());
|
||||||
|
assertEquals(Set.of(task.getArtifactFile().get().getAsFile()), task.getOutputs().getFiles().getFiles());
|
||||||
|
assertFalse(task.getOutputs().getFiles().contains(task.getLogFile().get().getAsFile()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failureSummaryRetainsTheFinalLinesWithoutReadingAnEntireLog() throws Exception {
|
||||||
|
Path log = temporary.newFile().toPath();
|
||||||
|
StringBuilder content = new StringBuilder("discarded first line\n");
|
||||||
|
for (int index = 0; index < 2000; index++) {
|
||||||
|
content.append("task output ").append(index).append('\n');
|
||||||
|
}
|
||||||
|
content.append("final failure\n");
|
||||||
|
Files.writeString(log, content);
|
||||||
|
|
||||||
|
String tail = IrisModdedBuildAction.logTail(log);
|
||||||
|
|
||||||
|
assertEquals(40, tail.lines().count());
|
||||||
|
assertTrue(tail.endsWith("final failure"));
|
||||||
|
assertFalse(tail.contains("discarded first line"));
|
||||||
|
assertTrue(tail.length() <= 16 * 1024);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failureSummaryAcceptsEmptyAndUnterminatedLogs() throws Exception {
|
||||||
|
Path log = temporary.newFile().toPath();
|
||||||
|
assertEquals("", IrisModdedBuildAction.logTail(log));
|
||||||
|
Files.writeString(log, "first\r\nlast");
|
||||||
|
assertEquals("first" + System.lineSeparator() + "last", IrisModdedBuildAction.logTail(log));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failureSummaryBoundsAnUnbrokenFailureLine() throws Exception {
|
||||||
|
Path log = temporary.newFile().toPath();
|
||||||
|
Files.writeString(log, "x".repeat(100_000) + "final failure");
|
||||||
|
|
||||||
|
String tail = IrisModdedBuildAction.logTail(log);
|
||||||
|
|
||||||
|
assertEquals(16 * 1024, tail.length());
|
||||||
|
assertTrue(tail.endsWith("final failure"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
-7
@@ -1,3 +1,5 @@
|
|||||||
|
import groovy.json.JsonSlurper
|
||||||
|
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||||
import io.github.slimjar.resolver.data.Mirror
|
import io.github.slimjar.resolver.data.Mirror
|
||||||
import org.gradle.api.attributes.java.TargetJvmVersion
|
import org.gradle.api.attributes.java.TargetJvmVersion
|
||||||
import org.gradle.api.file.FileTreeElement
|
import org.gradle.api.file.FileTreeElement
|
||||||
@@ -37,6 +39,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
apply from: rootProject.file('gradle/runtime-relocations.gradle')
|
apply from: rootProject.file('gradle/runtime-relocations.gradle')
|
||||||
|
apply from: rootProject.file('gradle/java21-runtime-exclusions.gradle')
|
||||||
|
|
||||||
def lib = 'art.arcane.iris.util'
|
def lib = 'art.arcane.iris.util'
|
||||||
|
|
||||||
@@ -133,6 +136,13 @@ dependencies {
|
|||||||
implementation('com.moandjiezana.toml:toml4j:0.7.2') {
|
implementation('com.moandjiezana.toml:toml4j:0.7.2') {
|
||||||
transitive = false
|
transitive = false
|
||||||
}
|
}
|
||||||
|
// Shaded: the runtime downloader can neither fetch the natives classifiers nor relocate lwjgl's own jar.
|
||||||
|
implementation(libs.lwjgl)
|
||||||
|
implementation(libs.lwjgl.opengl)
|
||||||
|
implementation(libs.lwjgl.egl)
|
||||||
|
implementation(libs.lwjgl.jawt)
|
||||||
|
runtimeOnly(variantOf(libs.lwjgl) { classifier('natives-linux') })
|
||||||
|
runtimeOnly(variantOf(libs.lwjgl.opengl) { classifier('natives-linux') })
|
||||||
// Dynamically Loaded
|
// Dynamically Loaded
|
||||||
slim(libs.gson)
|
slim(libs.gson)
|
||||||
slim(libs.lru)
|
slim(libs.lru)
|
||||||
@@ -158,13 +168,15 @@ dependencies {
|
|||||||
slim(libs.byteBuddy.agent)
|
slim(libs.byteBuddy.agent)
|
||||||
slim(libs.dom4j)
|
slim(libs.dom4j)
|
||||||
slim(libs.jaxen)
|
slim(libs.jaxen)
|
||||||
|
slim(libs.flatlaf)
|
||||||
|
slim(libs.flatlaf.extras)
|
||||||
|
|
||||||
testFixturesImplementation('junit:junit:4.13.2')
|
testFixturesImplementation('junit:junit:4.13.2')
|
||||||
testFixturesImplementation('org.mockito:mockito-core:5.23.0')
|
testFixturesImplementation(libs.mockito.core)
|
||||||
testFixturesCompileOnly(libs.paper.api)
|
testFixturesCompileOnly(libs.paper.api)
|
||||||
|
|
||||||
testImplementation('junit:junit:4.13.2')
|
testImplementation('junit:junit:4.13.2')
|
||||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
testImplementation(libs.mockito.core)
|
||||||
testImplementation(libs.paper.api)
|
testImplementation(libs.paper.api)
|
||||||
testImplementation(libs.log4j.core)
|
testImplementation(libs.log4j.core)
|
||||||
testRuntimeOnly(libs.paper.api)
|
testRuntimeOnly(libs.paper.api)
|
||||||
@@ -174,8 +186,10 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.named('test').configure {
|
tasks.named('test').configure {
|
||||||
maxHeapSize = '1g'
|
timeout = java.time.Duration.ofMinutes(60)
|
||||||
|
maxHeapSize = '2g'
|
||||||
jvmArgs('-XX:+ExitOnOutOfMemoryError')
|
jvmArgs('-XX:+ExitOnOutOfMemoryError')
|
||||||
|
systemProperty('iris.generation.counters', 'true')
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
@@ -197,8 +211,16 @@ slimJar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TaskProvider<Task> slimJarTask = tasks.named('slimJar')
|
||||||
|
tasks.withType(ShadowJar).configureEach {
|
||||||
|
dependsOn(slimJarTask)
|
||||||
|
}
|
||||||
|
|
||||||
def embeddedAgentJar = project(':core:agent').tasks.named('jar', Jar)
|
def embeddedAgentJar = project(':core:agent').tasks.named('jar', Jar)
|
||||||
tasks.named('processResources').configure {
|
tasks.named('processResources').configure { Task resources ->
|
||||||
|
resources.setFinalizedBy(resources.finalizedBy.getDependencies(resources).findAll { Task finalizer ->
|
||||||
|
finalizer != slimJarTask.get()
|
||||||
|
})
|
||||||
from(embeddedAgentJar.map { it.archiveFile }) {
|
from(embeddedAgentJar.map { it.archiveFile }) {
|
||||||
rename { String ignored -> 'agent.jar' }
|
rename { String ignored -> 'agent.jar' }
|
||||||
}
|
}
|
||||||
@@ -217,6 +239,18 @@ def generateTemplates = tasks.register('generateTemplates', Copy) {
|
|||||||
return 'development'
|
return 'development'
|
||||||
},
|
},
|
||||||
commit: providers.provider {
|
commit: providers.provider {
|
||||||
|
String sourceHashesJson = providers.environmentVariable('MULTIPLEXOR_SOURCE_HASHES').orNull
|
||||||
|
if (sourceHashesJson != null) {
|
||||||
|
Object sourceHashes = new JsonSlurper().parseText(sourceHashesJson)
|
||||||
|
if (!(sourceHashes instanceof Map)) {
|
||||||
|
throw new GradleException('MULTIPLEXOR_SOURCE_HASHES must map project paths to SHA-256 source fingerprints')
|
||||||
|
}
|
||||||
|
Object sourceHash = sourceHashes.get(rootProject.projectDir.canonicalPath)
|
||||||
|
if (!(sourceHash instanceof String) || !(sourceHash ==~ /[a-f0-9]{64}/)) {
|
||||||
|
throw new GradleException("MULTIPLEXOR_SOURCE_HASHES requires a SHA-256 source fingerprint for ${rootProject.projectDir.canonicalPath}")
|
||||||
|
}
|
||||||
|
return "source-${sourceHash}"
|
||||||
|
}
|
||||||
String commitId = null
|
String commitId = null
|
||||||
Exception failure = null
|
Exception failure = null
|
||||||
try {
|
try {
|
||||||
@@ -447,13 +481,12 @@ List<String> dependencyBuildMetadata = [
|
|||||||
|
|
||||||
String languageTomlSupportClassPattern = 'art/arcane/volmlib/util/config/(?:TomlCodec|ConfigJson|ConfigExposePolicy|ConfigDoc|ConfigDescription|ConfigDocumentation)(?:\\$[^/]+)?\\.class'
|
String languageTomlSupportClassPattern = 'art/arcane/volmlib/util/config/(?:TomlCodec|ConfigJson|ConfigExposePolicy|ConfigDoc|ConfigDescription|ConfigDocumentation)(?:\\$[^/]+)?\\.class'
|
||||||
|
|
||||||
tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar).configure {
|
tasks.named('shadowJar', ShadowJar).configure {
|
||||||
inputs.property('languageTomlSupportClassPattern', languageTomlSupportClassPattern)
|
inputs.property('languageTomlSupportClassPattern', languageTomlSupportClassPattern)
|
||||||
dependsOn(embeddedAgentJar)
|
dependsOn(embeddedAgentJar)
|
||||||
mergeServiceFiles()
|
mergeServiceFiles()
|
||||||
if (providers.gradleProperty('irisRuntimeJava').getOrElse(rootProject.irisModTarget.java.toString()) == '21') {
|
if (providers.gradleProperty('irisRuntimeJava').getOrElse(rootProject.irisModTarget.java.toString()) == '21') {
|
||||||
exclude('art/arcane/volmlib/util/noise/NativeBillowLoader*.class')
|
exclude(java21RuntimeExclusions)
|
||||||
exclude('art/arcane/volmlib/util/noise/BillowBatch*.class')
|
|
||||||
}
|
}
|
||||||
// minimize() stays off. Caffeine reaches its 520 generated cache and node classes through
|
// minimize() stays off. Caffeine reaches its 520 generated cache and node classes through
|
||||||
// MethodHandles.Lookup.findClass on a computed name, and the VolmLib command, GUI and hotload
|
// MethodHandles.Lookup.findClass on a computed name, and the VolmLib command, GUI and hotload
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ art/arcane/iris/generation/block/IrisCustomData.java
|
|||||||
art/arcane/iris/generation/block/LegacyTileData.java
|
art/arcane/iris/generation/block/LegacyTileData.java
|
||||||
art/arcane/iris/generation/block/TileData.java
|
art/arcane/iris/generation/block/TileData.java
|
||||||
art/arcane/iris/generation/chunk/ChunkBiomeHunkView.java
|
art/arcane/iris/generation/chunk/ChunkBiomeHunkView.java
|
||||||
art/arcane/iris/generation/chunk/ChunkDataHunkHolder.java
|
|
||||||
art/arcane/iris/generation/chunk/ChunkDataHunkView.java
|
art/arcane/iris/generation/chunk/ChunkDataHunkView.java
|
||||||
|
art/arcane/iris/generation/chunk/ChunkDataOutput.java
|
||||||
art/arcane/iris/generation/chunk/ChunkHunkView.java
|
art/arcane/iris/generation/chunk/ChunkHunkView.java
|
||||||
art/arcane/iris/generation/chunk/LinkedTerrainChunk.java
|
art/arcane/iris/generation/chunk/LinkedTerrainChunk.java
|
||||||
art/arcane/iris/generation/chunk/TerrainChunk.java
|
art/arcane/iris/generation/chunk/TerrainChunk.java
|
||||||
|
|||||||
+51
@@ -15,6 +15,7 @@ import net.momirealms.craftengine.bukkit.api.CraftEngineFurniture;
|
|||||||
import net.momirealms.craftengine.bukkit.api.CraftEngineItems;
|
import net.momirealms.craftengine.bukkit.api.CraftEngineItems;
|
||||||
import net.momirealms.craftengine.bukkit.api.event.CraftEngineReloadEvent;
|
import net.momirealms.craftengine.bukkit.api.event.CraftEngineReloadEvent;
|
||||||
import net.momirealms.craftengine.bukkit.item.BukkitItemDefinition;
|
import net.momirealms.craftengine.bukkit.item.BukkitItemDefinition;
|
||||||
|
import net.momirealms.craftengine.bukkit.entity.furniture.BukkitFurniture;
|
||||||
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
|
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
|
||||||
import net.momirealms.craftengine.core.block.BlockDefinition;
|
import net.momirealms.craftengine.core.block.BlockDefinition;
|
||||||
import net.momirealms.craftengine.core.block.ImmutableBlockState;
|
import net.momirealms.craftengine.core.block.ImmutableBlockState;
|
||||||
@@ -24,10 +25,12 @@ import net.momirealms.craftengine.core.entity.furniture.FurnitureVariant;
|
|||||||
import net.momirealms.craftengine.core.registry.Holder;
|
import net.momirealms.craftengine.core.registry.Holder;
|
||||||
import net.momirealms.craftengine.core.util.Key;
|
import net.momirealms.craftengine.core.util.Key;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Chunk;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
|
import org.bukkit.entity.Entity;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.bukkit.plugin.Plugin;
|
import org.bukkit.plugin.Plugin;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -37,6 +40,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.MissingResourceException;
|
import java.util.MissingResourceException;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
@@ -48,12 +52,59 @@ import static org.mockito.ArgumentMatchers.eq;
|
|||||||
import static org.mockito.Mockito.doReturn;
|
import static org.mockito.Mockito.doReturn;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.mockStatic;
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.spy;
|
import static org.mockito.Mockito.spy;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class CraftEngineDataProviderTest {
|
public class CraftEngineDataProviderTest {
|
||||||
|
@Test
|
||||||
|
public void furniturePlacementReturnsExactEntityTokenAndCleanupKeepsReplacement() {
|
||||||
|
FurnitureDefinition definition = furnitureDefinition();
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
SeedManager seeds = mock(SeedManager.class);
|
||||||
|
when(engine.getSeedManager()).thenReturn(seeds);
|
||||||
|
Block block = mock(Block.class);
|
||||||
|
when(block.getLocation()).thenAnswer(invocation -> new Location(null, 0, 70, 0));
|
||||||
|
Chunk chunk = mock(Chunk.class);
|
||||||
|
when(block.getChunk()).thenReturn(chunk);
|
||||||
|
when(chunk.isEntitiesLoaded()).thenReturn(true);
|
||||||
|
Entity original = mock(Entity.class);
|
||||||
|
Entity replacement = mock(Entity.class);
|
||||||
|
UUID originalId = UUID.randomUUID();
|
||||||
|
when(original.getUniqueId()).thenReturn(originalId);
|
||||||
|
when(replacement.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||||
|
when(chunk.getEntities()).thenReturn(new Entity[]{replacement, original});
|
||||||
|
BukkitFurniture placed = mock(BukkitFurniture.class);
|
||||||
|
when(placed.baseEntity()).thenReturn(original);
|
||||||
|
CraftEngineDataProvider provider = new CraftEngineDataProvider();
|
||||||
|
try (MockedStatic<CraftEngineBlocks> blocks = mockStatic(CraftEngineBlocks.class);
|
||||||
|
MockedStatic<CraftEngineFurniture> furniture = mockStatic(CraftEngineFurniture.class)) {
|
||||||
|
furniture.when(() -> CraftEngineFurniture.byId(Key.of("test:chair"))).thenReturn(definition);
|
||||||
|
furniture.when(() -> CraftEngineFurniture.place(any(Location.class), eq(definition), eq("alpha"), eq(false)))
|
||||||
|
.thenReturn(placed);
|
||||||
|
furniture.when(() -> CraftEngineFurniture.remove(original, false, false)).thenReturn(true);
|
||||||
|
|
||||||
|
String token = provider.processUpdate(engine, block, Identifier.fromString("test:chair"));
|
||||||
|
|
||||||
|
assertEquals(originalId.toString(), token);
|
||||||
|
assertTrue(provider.removeGenerated(block, token));
|
||||||
|
furniture.verify(() -> CraftEngineFurniture.remove(original, false, false));
|
||||||
|
furniture.verify(() -> CraftEngineFurniture.remove(replacement, false, false), never());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void furnitureCleanupWaitsForEntitiesToLoad() {
|
||||||
|
Block block = mock(Block.class);
|
||||||
|
Chunk chunk = mock(Chunk.class);
|
||||||
|
when(block.getChunk()).thenReturn(chunk);
|
||||||
|
|
||||||
|
assertFalse(new CraftEngineDataProvider().removeGenerated(block, UUID.randomUUID().toString()));
|
||||||
|
verify(chunk, never()).getEntities();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void startupWaitsForReloadEventAndEmptyLoadedRegistriesAreReady() {
|
public void startupWaitsForReloadEventAndEmptyLoadedRegistriesAreReady() {
|
||||||
CraftEngineDataProvider provider = spy(new CraftEngineDataProvider());
|
CraftEngineDataProvider provider = spy(new CraftEngineDataProvider());
|
||||||
|
|||||||
+198
@@ -0,0 +1,198 @@
|
|||||||
|
package art.arcane.iris.platform.generation;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.block.IrisCustomData;
|
||||||
|
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.runtime.SeedManager;
|
||||||
|
import art.arcane.iris.integration.ExternalDataProvider;
|
||||||
|
import art.arcane.iris.integration.ExternalDataSVC;
|
||||||
|
import art.arcane.iris.integration.Identifier;
|
||||||
|
import art.arcane.iris.integration.data.CraftEngineDataProvider;
|
||||||
|
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
|
||||||
|
import art.arcane.iris.platform.registry.RegistryUtil;
|
||||||
|
import art.arcane.iris.spi.IrisServices;
|
||||||
|
import art.arcane.iris.testsupport.BukkitTestServer;
|
||||||
|
import art.arcane.iris.world.IrisWorld;
|
||||||
|
import art.arcane.volmlib.util.function.Consumer4;
|
||||||
|
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||||
|
import art.arcane.volmlib.util.matter.Matter;
|
||||||
|
import art.arcane.volmlib.util.matter.MatterSlice;
|
||||||
|
import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks;
|
||||||
|
import net.momirealms.craftengine.bukkit.api.CraftEngineFurniture;
|
||||||
|
import net.momirealms.craftengine.bukkit.api.CraftEngineItems;
|
||||||
|
import net.momirealms.craftengine.bukkit.api.event.CraftEngineReloadEvent;
|
||||||
|
import net.momirealms.craftengine.bukkit.entity.furniture.BukkitFurniture;
|
||||||
|
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
|
||||||
|
import net.momirealms.craftengine.core.block.BlockDefinition;
|
||||||
|
import net.momirealms.craftengine.core.block.ImmutableBlockState;
|
||||||
|
import net.momirealms.craftengine.core.entity.furniture.FurnitureDefinition;
|
||||||
|
import net.momirealms.craftengine.core.entity.furniture.FurnitureVariant;
|
||||||
|
import net.momirealms.craftengine.core.registry.Holder;
|
||||||
|
import net.momirealms.craftengine.core.util.Key;
|
||||||
|
import org.bukkit.Chunk;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
import org.bukkit.entity.Entity;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
import org.mockito.invocation.InvocationOnMock;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class CraftEngineStaleRecordTest {
|
||||||
|
@Test
|
||||||
|
public void stalePlacementRecordsAreSkippedAndIntactOnesPlaced() throws Exception {
|
||||||
|
BukkitTestServer.install();
|
||||||
|
BlockDefinition lampDefinition = mock(BlockDefinition.class);
|
||||||
|
ImmutableBlockState lamp = mock(ImmutableBlockState.class);
|
||||||
|
when(lamp.owner()).thenReturn(Holder.direct(lampDefinition));
|
||||||
|
when(lamp.toString()).thenReturn("test:lamp");
|
||||||
|
when(lampDefinition.defaultState()).thenReturn(lamp);
|
||||||
|
FurnitureDefinition chairDefinition = mock(FurnitureDefinition.class);
|
||||||
|
when(chairDefinition.id()).thenReturn(Key.of("test:chair"));
|
||||||
|
when(chairDefinition.variants()).thenReturn(Map.of("alpha", mock(FurnitureVariant.class)));
|
||||||
|
BlockData noteBlock = blockData(Material.NOTE_BLOCK);
|
||||||
|
BlockData air = blockData(Material.AIR);
|
||||||
|
CraftEngineDataProvider provider = new CraftEngineDataProvider();
|
||||||
|
ExternalDataSVC service = new ExternalDataSVC();
|
||||||
|
activate(service, provider);
|
||||||
|
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
when(engine.getWorld()).thenReturn(mock(IrisWorld.class));
|
||||||
|
SeedManager seeds = mock(SeedManager.class);
|
||||||
|
when(engine.getSeedManager()).thenReturn(seeds);
|
||||||
|
World world = mock(World.class);
|
||||||
|
when(world.getMinHeight()).thenReturn(0);
|
||||||
|
when(world.getMaxHeight()).thenReturn(256);
|
||||||
|
Chunk chunk = mock(Chunk.class);
|
||||||
|
when(chunk.getWorld()).thenReturn(world);
|
||||||
|
block(chunk, 1, Material.NOTE_BLOCK);
|
||||||
|
block(chunk, 2, Material.AIR);
|
||||||
|
block(chunk, 3, Material.COBBLESTONE);
|
||||||
|
block(chunk, 4, Material.COBBLESTONE);
|
||||||
|
|
||||||
|
try (MockedStatic<BukkitCraftEngine> craftEngine = mockStatic(BukkitCraftEngine.class);
|
||||||
|
MockedStatic<CraftEngineBlocks> blocks = mockStatic(CraftEngineBlocks.class);
|
||||||
|
MockedStatic<CraftEngineFurniture> furniture = mockStatic(CraftEngineFurniture.class);
|
||||||
|
MockedStatic<CraftEngineItems> items = mockStatic(CraftEngineItems.class);
|
||||||
|
MockedStatic<RegistryUtil> registry = materialRegistry();
|
||||||
|
MockedStatic<BukkitBlockResolution> resolution = mockStatic(BukkitBlockResolution.class);
|
||||||
|
MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
|
||||||
|
resolution.when(BukkitBlockResolution::getAir).thenReturn(air);
|
||||||
|
services.when(() -> IrisServices.get(ExternalDataSVC.class)).thenReturn(service);
|
||||||
|
blocks.when(CraftEngineBlocks::loadedBlocks).thenReturn(Map.of(Key.of("test:lamp"), lampDefinition));
|
||||||
|
furniture.when(CraftEngineFurniture::loadedFurniture).thenReturn(Map.of(Key.of("test:chair"), chairDefinition));
|
||||||
|
items.when(CraftEngineItems::loadedItems).thenReturn(Map.of());
|
||||||
|
blocks.when(() -> CraftEngineBlocks.byId(Key.of("test:lamp"))).thenReturn(lampDefinition);
|
||||||
|
blocks.when(() -> CraftEngineBlocks.getBukkitBlockData(lamp)).thenReturn(noteBlock);
|
||||||
|
blocks.when(() -> CraftEngineBlocks.place(any(Location.class), eq(lamp), eq(false))).thenReturn(true);
|
||||||
|
furniture.when(() -> CraftEngineFurniture.byId(Key.of("test:chair"))).thenReturn(chairDefinition);
|
||||||
|
BukkitFurniture placed = mock(BukkitFurniture.class);
|
||||||
|
Entity entity = mock(Entity.class);
|
||||||
|
when(placed.baseEntity()).thenReturn(entity);
|
||||||
|
when(entity.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||||
|
furniture.when(() -> CraftEngineFurniture.place(any(Location.class), eq(chairDefinition), eq("alpha"), eq(false)))
|
||||||
|
.thenReturn(placed);
|
||||||
|
provider.onReload(mock(CraftEngineReloadEvent.class));
|
||||||
|
Identifier lampRecord = generatedRecord(service, "test:lamp");
|
||||||
|
Identifier chairRecord = generatedRecord(service, "test:chair");
|
||||||
|
assertEquals("craftengine:test/lamp", lampRecord.toString());
|
||||||
|
assertEquals("craftengine:test/chair[variant=alpha]", chairRecord.toString());
|
||||||
|
|
||||||
|
EngineBukkitOps.materializeCustomBlocks(engine, chunk, records(
|
||||||
|
new Placed(1, lampRecord),
|
||||||
|
new Placed(2, lampRecord),
|
||||||
|
new Placed(3, lampRecord),
|
||||||
|
new Placed(4, chairRecord)));
|
||||||
|
|
||||||
|
blocks.verify(() -> CraftEngineBlocks.place(any(Location.class), eq(lamp), eq(false)), times(1));
|
||||||
|
blocks.verify(() -> CraftEngineBlocks.place(location(1), lamp, false));
|
||||||
|
blocks.verify(() -> CraftEngineBlocks.place(location(2), lamp, false), never());
|
||||||
|
blocks.verify(() -> CraftEngineBlocks.place(location(3), lamp, false), never());
|
||||||
|
furniture.verify(() -> CraftEngineFurniture.place(location(4), chairDefinition, "alpha", false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MockedStatic<RegistryUtil> materialRegistry() {
|
||||||
|
MockedStatic<RegistryUtil> registry = mockStatic(RegistryUtil.class);
|
||||||
|
registry.when(() -> RegistryUtil.find(Material.class, "grass", "short_grass")).thenReturn(Material.SHORT_GRASS);
|
||||||
|
return registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Identifier generatedRecord(ExternalDataSVC service, String key) {
|
||||||
|
BlockData resolved = service.getBlockData(Identifier.fromString(key)).orElseThrow();
|
||||||
|
return ((IrisCustomData) resolved).getCustom();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockData blockData(Material material) {
|
||||||
|
BlockData data = mock(BlockData.class);
|
||||||
|
when(data.getMaterial()).thenReturn(material);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Block block(Chunk chunk, int offset, Material type) {
|
||||||
|
Block block = mock(Block.class);
|
||||||
|
when(block.getType()).thenReturn(type);
|
||||||
|
when(block.getX()).thenReturn(offset);
|
||||||
|
when(block.getY()).thenReturn(10);
|
||||||
|
when(block.getZ()).thenReturn(offset);
|
||||||
|
when(block.getLocation()).thenAnswer((InvocationOnMock invocation) -> location(offset));
|
||||||
|
when(chunk.getBlock(offset, 10, offset)).thenReturn(block);
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Location location(int offset) {
|
||||||
|
return new Location(null, offset, 10, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static MantleChunk<Matter> records(Placed... records) {
|
||||||
|
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||||
|
Matter section = mock(Matter.class);
|
||||||
|
MatterSlice<CustomBlockOwnership> ownership = mock(MatterSlice.class);
|
||||||
|
MatterSlice<Identifier> deferred = mock(MatterSlice.class);
|
||||||
|
when(chunk.getOrCreate(anyInt())).thenReturn(section);
|
||||||
|
when(section.<CustomBlockOwnership>slice(CustomBlockOwnership.class)).thenReturn(ownership);
|
||||||
|
when(section.getSlice(Identifier.class)).thenReturn(deferred);
|
||||||
|
for (Placed record : records) {
|
||||||
|
when(chunk.get(record.offset(), 10, record.offset(), Identifier.class)).thenReturn(record.identifier());
|
||||||
|
when(deferred.get(record.offset(), 10, record.offset())).thenReturn(record.identifier());
|
||||||
|
}
|
||||||
|
doAnswer((InvocationOnMock invocation) -> {
|
||||||
|
Consumer4<Integer, Integer, Integer, Identifier> iterator = invocation.getArgument(1);
|
||||||
|
for (Placed record : records) {
|
||||||
|
iterator.accept(record.offset(), 10, record.offset(), record.identifier());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}).when(chunk).iterate(eq(Identifier.class), any());
|
||||||
|
return chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static void activate(ExternalDataSVC service, ExternalDataProvider provider) throws Exception {
|
||||||
|
Field field = ExternalDataSVC.class.getDeclaredField("activeProviders");
|
||||||
|
field.setAccessible(true);
|
||||||
|
((List<ExternalDataProvider>) field.get(service)).add(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Placed(int offset, Identifier identifier) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-23
@@ -1,25 +1,14 @@
|
|||||||
package art.arcane.iris.command.specialhandlers;
|
package art.arcane.iris.command.specialhandlers;
|
||||||
|
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery;
|
|
||||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
|
||||||
import art.arcane.iris.command.DirectorParameterHandler;
|
import art.arcane.iris.command.DirectorParameterHandler;
|
||||||
|
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
|
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public final class HydrologyTypeHandler implements DirectorParameterHandler<String> {
|
public final class HydrologyTypeHandler implements DirectorParameterHandler<String> {
|
||||||
@Override
|
@Override
|
||||||
public KList<String> getPossibilities() {
|
public KList<String> getPossibilities() {
|
||||||
Engine activeEngine = engine();
|
return new KList<>(HydrologyFeatureType.keys());
|
||||||
if (activeEngine == null) {
|
|
||||||
return new KList<>(HydrologyFeatureQuery.suggestions(List.of()));
|
|
||||||
}
|
|
||||||
IrisHydrologyRuntime runtime = activeEngine.getComplex().getHydrologyRuntime();
|
|
||||||
return new KList<>(runtime == null
|
|
||||||
? HydrologyFeatureQuery.suggestions(List.of())
|
|
||||||
: runtime.featureQueryKeys());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -29,16 +18,11 @@ public final class HydrologyTypeHandler implements DirectorParameterHandler<Stri
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String parse(String input, boolean force) throws DirectorParsingException {
|
public String parse(String input, boolean force) throws DirectorParsingException {
|
||||||
String normalized = input == null ? "" : input.trim();
|
try {
|
||||||
if (normalized.isEmpty()) {
|
return HydrologyFeatureType.parse(input).key();
|
||||||
throw new DirectorParsingException("Hydrology feature type must not be blank.");
|
} catch (IllegalArgumentException error) {
|
||||||
|
throw new DirectorParsingException(error.getMessage());
|
||||||
}
|
}
|
||||||
for (String possibility : getPossibilities(normalized)) {
|
|
||||||
if (possibility.equalsIgnoreCase(normalized)) {
|
|
||||||
return possibility;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return normalized;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -48,6 +32,6 @@ public final class HydrologyTypeHandler implements DirectorParameterHandler<Stri
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getRandomDefault() {
|
public String getRandomDefault() {
|
||||||
return "surface";
|
return HydrologyFeatureType.RIVER.key();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loaded 100 of 1259 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user