mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 06:20:02 +00:00
Compare commits
122
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f64a7c6753 | ||
|
|
7756598057 | ||
|
|
75837d757b | ||
|
|
2690e0878b | ||
|
|
494cdb1a5c | ||
|
|
1d182769d8 | ||
|
|
584e8758a7 | ||
|
|
3766a40256 | ||
|
|
b7499d23f2 | ||
|
|
46d458e6e4 | ||
|
|
9f49d29e2b | ||
|
|
e3bc27d50c | ||
|
|
6900f270ee | ||
|
|
db0f98d103 | ||
|
|
547c52540a | ||
|
|
806a33976a | ||
|
|
065c6fed1d | ||
|
|
360f38a819 | ||
|
|
5204d94ebe | ||
|
|
48bfa7d15b | ||
|
|
2acfac36fc | ||
|
|
54213d4b62 | ||
|
|
ff6668a48b | ||
|
|
29cca99947 | ||
|
|
2d211cf106 | ||
|
|
3240d57a3b | ||
|
|
908dbcf4cc | ||
|
|
4703d473cc | ||
|
|
c3b3f1d8aa | ||
|
|
60eec4e2ee | ||
|
|
c6d7a814e0 | ||
|
|
45e00f4794 | ||
|
|
9d4c55d11a | ||
|
|
c62a31e16b | ||
|
|
18ba6e32ac | ||
|
|
f7f9f5f651 | ||
|
|
d9c5d00be3 | ||
|
|
97c67b794b | ||
|
|
c6e9f5a1ee | ||
|
|
1fe26e6288 | ||
|
|
0b4d860ff4 | ||
|
|
9d9acc7a3a | ||
|
|
04f2798c06 | ||
|
|
9cd470c7c8 | ||
|
|
8fbc006781 | ||
|
|
e38f956d66 | ||
|
|
67fd9046ba | ||
|
|
0bdb022f56 | ||
|
|
0fe2363feb | ||
|
|
af90765a92 | ||
|
|
a25d8ec367 | ||
|
|
fea9aa4f19 | ||
|
|
5c4bce0c1a | ||
|
|
23637beb0c | ||
|
|
b712cbf93d | ||
|
|
ae1a84702c | ||
|
|
6582d9d14d | ||
|
|
bf9e65252c | ||
|
|
d7cad729e4 | ||
|
|
cb008b6428 | ||
|
|
e91eb258aa | ||
|
|
544707f5d6 | ||
|
|
aa929290c0 | ||
|
|
a2e4ec7070 | ||
|
|
d2106806a5 | ||
|
|
ab51822fd4 | ||
|
|
d7a2a52c72 | ||
|
|
024214d7f0 | ||
|
|
ad73c33480 | ||
|
|
e8f8546b8a | ||
|
|
fd47240db9 | ||
|
|
be2fe12524 | ||
|
|
e376e1f59a | ||
|
|
b9409fb5ec | ||
|
|
d3defa5563 | ||
|
|
81f2d2332f | ||
|
|
14da077ce1 | ||
|
|
be30285718 | ||
|
|
a25233edbc | ||
|
|
4d2e52bcc7 | ||
|
|
943c8d67e5 | ||
|
|
fa996a2e24 | ||
|
|
2bfa9c4528 | ||
|
|
b526b480a7 | ||
|
|
ab69be59c7 | ||
|
|
b510f0dbfe | ||
|
|
7408d02bbe | ||
|
|
1acda886b3 | ||
|
|
4dec940311 | ||
|
|
df11ad2db0 | ||
|
|
f283d482a4 | ||
|
|
3f07db2b9e | ||
|
|
391d5346ae | ||
|
|
78de23e461 | ||
|
|
6cc3adf76b | ||
|
|
35e4e30ecd | ||
|
|
393ea10cdf | ||
|
|
037156ab7d | ||
|
|
29e7e0384d | ||
|
|
2d60898744 | ||
|
|
6c7896f2a4 | ||
|
|
b34e5e7676 | ||
|
|
2471066937 | ||
|
|
ab468a198d | ||
|
|
7aaf346ce3 | ||
|
|
65542be8d6 | ||
|
|
dcfadc8d1d | ||
|
|
b03f1c90e1 | ||
|
|
e39e59699e | ||
|
|
13730f207a | ||
|
|
217ed04f45 | ||
|
|
e3135d8b01 | ||
|
|
6ebb700be0 | ||
|
|
7c4a5e1937 | ||
|
|
62d7c7e18a | ||
|
|
c1732d50be | ||
|
|
1821fedb24 | ||
|
|
41142ee5b5 | ||
|
|
b6b01c88f5 | ||
|
|
3bd76d47a9 | ||
|
|
7282a46a26 | ||
|
|
e2430500cf |
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/
|
||||||
+7
-1
@@ -497,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) {
|
||||||
|
|||||||
+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 {
|
||||||
|
|||||||
+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\":[]}");
|
||||||
|
|||||||
+7
-1
@@ -497,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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
+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,6 +58,7 @@ 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) }
|
||||||
@@ -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/**')
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ 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) }
|
||||||
@@ -151,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)
|
||||||
@@ -177,7 +182,7 @@ dependencies {
|
|||||||
? minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
|
? minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
|
||||||
: minecraft.dependency("iris-${buildLane}", "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
|
||||||
@@ -214,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 ->
|
||||||
@@ -271,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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,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/**')
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
+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;
|
||||||
|
|||||||
+14
@@ -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,7 @@ 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.Objects;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
@@ -71,6 +73,8 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
private final PregenMantleBackpressure backpressure;
|
private final PregenMantleBackpressure backpressure;
|
||||||
private final PauseWhenEmptyGuard pauseGuard;
|
private final PauseWhenEmptyGuard pauseGuard;
|
||||||
private final LongSupplier clock;
|
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);
|
this(configuration, System::nanoTime);
|
||||||
@@ -80,6 +84,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
this.clock = Objects.requireNonNull(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();
|
||||||
@@ -121,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={}",
|
||||||
@@ -341,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
-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) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -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;
|
||||||
@@ -31,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;
|
||||||
@@ -89,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());
|
||||||
|
|||||||
+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.
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ 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) }
|
||||||
@@ -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/**')
|
||||||
|
|||||||
+22
-2
@@ -127,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'))
|
||||||
}
|
}
|
||||||
@@ -230,6 +230,7 @@ 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',
|
||||||
@@ -245,7 +246,8 @@ List<Object> moddedSharedInputs = []
|
|||||||
].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')
|
||||||
@@ -437,6 +439,21 @@ 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 = "Builds and verifies the Fabric ${minecraftVersion} jar."
|
description = "Builds and verifies the Fabric ${minecraftVersion} jar."
|
||||||
@@ -569,11 +586,14 @@ allprojects {
|
|||||||
layout.buildDirectory.set(layout.projectDirectory.dir("build/${runtimeOutputVariant}"))
|
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 {
|
||||||
|
|||||||
+16
-5
@@ -39,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'
|
||||||
|
|
||||||
@@ -135,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)
|
||||||
@@ -160,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)
|
||||||
@@ -176,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 {
|
||||||
@@ -474,8 +486,7 @@ tasks.named('shadowJar', ShadowJar).configure {
|
|||||||
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
|
||||||
|
|||||||
+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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,6 +373,7 @@ public class IrisSettings {
|
|||||||
public boolean useServerLaunchedGuis = true;
|
public boolean useServerLaunchedGuis = true;
|
||||||
public boolean maximumPregenGuiFPS = false;
|
public boolean maximumPregenGuiFPS = false;
|
||||||
public boolean colorMode = true;
|
public boolean colorMode = true;
|
||||||
|
public String theme = "auto";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package art.arcane.iris.generation.bake;
|
||||||
|
|
||||||
|
public enum BakePlane {
|
||||||
|
REGION(false),
|
||||||
|
BASE_BIOME(false),
|
||||||
|
BASE_HEIGHT(true),
|
||||||
|
NATURAL_HEIGHT(true),
|
||||||
|
NATURAL_TRUE_BIOME(false),
|
||||||
|
HEIGHT(true),
|
||||||
|
TRUE_BIOME(false),
|
||||||
|
ROCK(false),
|
||||||
|
FLUID(false),
|
||||||
|
CAVE_BIOME(false);
|
||||||
|
|
||||||
|
static final BakePlane[] PLANES = values();
|
||||||
|
|
||||||
|
private final boolean doubles;
|
||||||
|
|
||||||
|
BakePlane(boolean doubles) {
|
||||||
|
this.doubles = doubles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean doubles() {
|
||||||
|
return doubles;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package art.arcane.iris.generation.bake;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.cache.ConcurrentClockCache;
|
||||||
|
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class ChunkBakeCache {
|
||||||
|
public static final int MINIMUM_CAPACITY = 2_048;
|
||||||
|
static final int MAXIMUM_CAPACITY = 32_768;
|
||||||
|
static final long BYTES_PER_ENTRY = 13L * 1_024L;
|
||||||
|
static final int HEAP_SHARE = 32;
|
||||||
|
private static final int RECENT_SLOTS = 16;
|
||||||
|
private static final ThreadLocal<int[]> SPARSE = ThreadLocal.withInitial(() -> new int[1]);
|
||||||
|
|
||||||
|
private final PlaneSource source;
|
||||||
|
private final ConcurrentClockCache<ChunkPlanes> entries;
|
||||||
|
private final ThreadLocal<Recent> recent = ThreadLocal.withInitial(Recent::new);
|
||||||
|
private volatile int generation;
|
||||||
|
|
||||||
|
public ChunkBakeCache(PlaneSource source, int capacityChunks) {
|
||||||
|
this.source = Objects.requireNonNull(source, "Bake plane source");
|
||||||
|
this.entries = new ConcurrentClockCache<>(Math.max(16, capacityChunks));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int capacityFor(long maxMemory) {
|
||||||
|
long entries = Math.max(0L, maxMemory) / HEAP_SHARE / BYTES_PER_ENTRY;
|
||||||
|
return (int) Math.max(MINIMUM_CAPACITY, Math.min(MAXIMUM_CAPACITY, entries));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int capacity() {
|
||||||
|
return entries.capacity();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void clear() {
|
||||||
|
generation++;
|
||||||
|
entries.forEach(ChunkPlanes::release);
|
||||||
|
entries.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long[] census() {
|
||||||
|
long[] counts = new long[BakePlane.PLANES.length + 1];
|
||||||
|
entries.forEach(planes -> {
|
||||||
|
counts[BakePlane.PLANES.length]++;
|
||||||
|
for (BakePlane plane : BakePlane.PLANES) {
|
||||||
|
if (planes.planeIfPresent(plane) != null) {
|
||||||
|
counts[plane.ordinal()]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChunkPlanes.Plane planeIfPresent(BakePlane plane, int chunkX, int chunkZ) {
|
||||||
|
long key = key(chunkX, chunkZ);
|
||||||
|
int slot = Recent.slot(chunkX, chunkZ);
|
||||||
|
Recent last = recent.get();
|
||||||
|
int current = generation;
|
||||||
|
ChunkPlanes planes = last.get(key, slot, current);
|
||||||
|
if (planes == null) {
|
||||||
|
planes = entries.get(key);
|
||||||
|
if (planes == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
last.remember(key, slot, planes, current);
|
||||||
|
}
|
||||||
|
return planes.planeIfPresent(plane);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChunkPlanes.Plane plane(BakePlane plane, int chunkX, int chunkZ) {
|
||||||
|
return planes(chunkX, chunkZ).plane(plane);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object complete(BakePlane plane, int chunkX, int chunkZ) {
|
||||||
|
return remembered(chunkX, chunkZ).complete(plane, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void fillColumns(BakePlane plane, int chunkX, int chunkZ, long[] columns, Object target) {
|
||||||
|
remembered(chunkX, chunkZ).plane(plane).fillColumns(source, chunkX, chunkZ, columns, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean memoAllowed() {
|
||||||
|
return SPARSE.get()[0] == 0 && !source.mainThread();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void storeDouble(BakePlane plane, int blockX, int blockZ, double value, long revision) {
|
||||||
|
if (revision != ProvisionalSampling.revision()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
remembered(blockX >> 4, blockZ >> 4).plane(plane).storeDouble(((blockZ & 15) << 4) | (blockX & 15), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void storeObject(BakePlane plane, int blockX, int blockZ, Object value, long revision) {
|
||||||
|
if (revision != ProvisionalSampling.revision()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
remembered(blockX >> 4, blockZ >> 4).plane(plane).storeObject(((blockZ & 15) << 4) | (blockX & 15), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChunkPlanes planes(int chunkX, int chunkZ) {
|
||||||
|
long key = key(chunkX, chunkZ);
|
||||||
|
ChunkPlanes existing = entries.get(key);
|
||||||
|
return existing != null ? existing : entries.putIfAbsent(key, new ChunkPlanes(chunkX, chunkZ));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChunkPlanes remembered(int chunkX, int chunkZ) {
|
||||||
|
int current = generation;
|
||||||
|
ChunkPlanes planes = planes(chunkX, chunkZ);
|
||||||
|
recent.get().remember(key(chunkX, chunkZ), Recent.slot(chunkX, chunkZ), planes, current);
|
||||||
|
return planes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void enterSparse() {
|
||||||
|
SPARSE.get()[0]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void exitSparse() {
|
||||||
|
SPARSE.get()[0]--;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean inSparseScope() {
|
||||||
|
return SPARSE.get()[0] > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long key(int chunkX, int chunkZ) {
|
||||||
|
return ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Recent {
|
||||||
|
private final long[] keys = new long[RECENT_SLOTS];
|
||||||
|
private final ChunkPlanes[] planes = new ChunkPlanes[RECENT_SLOTS];
|
||||||
|
private int generation = -1;
|
||||||
|
|
||||||
|
private static int slot(int chunkX, int chunkZ) {
|
||||||
|
return ((chunkZ & 3) << 2) | (chunkX & 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChunkPlanes get(long key, int slot, int generation) {
|
||||||
|
return this.generation == generation && keys[slot] == key ? planes[slot] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void remember(long key, int slot, ChunkPlanes value, int generation) {
|
||||||
|
if (this.generation != generation) {
|
||||||
|
Arrays.fill(planes, null);
|
||||||
|
this.generation = generation;
|
||||||
|
}
|
||||||
|
keys[slot] = key;
|
||||||
|
planes[slot] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
package art.arcane.iris.generation.bake;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||||
|
|
||||||
|
import java.lang.invoke.MethodHandles;
|
||||||
|
import java.lang.invoke.VarHandle;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||||
|
|
||||||
|
public final class ChunkPlanes {
|
||||||
|
private static final VarHandle READY = MethodHandles.arrayElementVarHandle(long[].class);
|
||||||
|
|
||||||
|
private final int chunkX;
|
||||||
|
private final int chunkZ;
|
||||||
|
private final AtomicReferenceArray<Plane> planes = new AtomicReferenceArray<>(BakePlane.PLANES.length);
|
||||||
|
|
||||||
|
ChunkPlanes(int chunkX, int chunkZ) {
|
||||||
|
this.chunkX = chunkX;
|
||||||
|
this.chunkZ = chunkZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int chunkX() {
|
||||||
|
return chunkX;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int chunkZ() {
|
||||||
|
return chunkZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Plane planeIfPresent(BakePlane kind) {
|
||||||
|
return planes.getAcquire(kind.ordinal());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Plane plane(BakePlane kind) {
|
||||||
|
int slot = kind.ordinal();
|
||||||
|
Plane plane = planes.getAcquire(slot);
|
||||||
|
if (plane != null) {
|
||||||
|
return plane;
|
||||||
|
}
|
||||||
|
Plane created = new Plane(kind);
|
||||||
|
Plane witness = planes.compareAndExchangeRelease(slot, null, created);
|
||||||
|
return witness == null ? created : witness;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object complete(BakePlane kind, PlaneSource source) {
|
||||||
|
Plane plane = plane(kind);
|
||||||
|
while (true) {
|
||||||
|
if (plane.complete) {
|
||||||
|
return plane.values;
|
||||||
|
}
|
||||||
|
Pending running = plane.filling.get();
|
||||||
|
if (running != null) {
|
||||||
|
if (running.owner == Thread.currentThread()) {
|
||||||
|
return plane.unpublishedFill(source, chunkX, chunkZ);
|
||||||
|
}
|
||||||
|
running.await();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Pending pending = new Pending(Thread.currentThread());
|
||||||
|
if (!plane.filling.compareAndSet(null, pending)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return plane.fill(source, chunkX, chunkZ);
|
||||||
|
} finally {
|
||||||
|
plane.filling.set(null);
|
||||||
|
pending.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void release() {
|
||||||
|
for (int slot = 0; slot < planes.length(); slot++) {
|
||||||
|
planes.setRelease(slot, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Plane {
|
||||||
|
private final BakePlane kind;
|
||||||
|
private final Object values;
|
||||||
|
private final long[] ready = new long[4];
|
||||||
|
private final AtomicReference<Pending> filling = new AtomicReference<>();
|
||||||
|
private volatile boolean complete;
|
||||||
|
|
||||||
|
private Plane(BakePlane kind) {
|
||||||
|
this.kind = kind;
|
||||||
|
this.values = kind.doubles() ? new double[256] : new Object[256];
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean ready(int index) {
|
||||||
|
return complete || ((long) READY.getAcquire(ready, index >>> 6) & (1L << (index & 63))) != 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double doubleAt(int index) {
|
||||||
|
return ((double[]) values)[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object objectAt(int index) {
|
||||||
|
return ((Object[]) values)[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void storeDouble(int index, double value) {
|
||||||
|
((double[]) values)[index] = value;
|
||||||
|
markReady(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void storeObject(int index, Object value) {
|
||||||
|
((Object[]) values)[index] = value;
|
||||||
|
markReady(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markReady(int index) {
|
||||||
|
READY.getAndBitwiseOrRelease(ready, index >>> 6, 1L << (index & 63));
|
||||||
|
}
|
||||||
|
|
||||||
|
private long[] missing() {
|
||||||
|
long[] missing = new long[4];
|
||||||
|
for (int word = 0; word < 4; word++) {
|
||||||
|
missing[word] = ~(long) READY.getAcquire(ready, word);
|
||||||
|
}
|
||||||
|
return missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object fill(PlaneSource source, int chunkX, int chunkZ) {
|
||||||
|
long[] missing = missing();
|
||||||
|
Object computed = kind.doubles() ? new double[256] : new Object[256];
|
||||||
|
long revision = ProvisionalSampling.revision();
|
||||||
|
source.fill(kind, chunkX, chunkZ, missing, computed);
|
||||||
|
if (revision != ProvisionalSampling.revision()) {
|
||||||
|
return merge(missing, computed);
|
||||||
|
}
|
||||||
|
for (int index = 0; index < 256; index++) {
|
||||||
|
if ((missing[index >>> 6] & (1L << (index & 63))) != 0L) {
|
||||||
|
if (kind.doubles()) {
|
||||||
|
((double[]) values)[index] = ((double[]) computed)[index];
|
||||||
|
} else {
|
||||||
|
((Object[]) values)[index] = ((Object[]) computed)[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int word = 0; word < 4; word++) {
|
||||||
|
READY.setRelease(ready, word, -1L);
|
||||||
|
}
|
||||||
|
complete = true;
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
void fillColumns(PlaneSource source, int chunkX, int chunkZ, long[] columns, Object target) {
|
||||||
|
long[] missing = new long[4];
|
||||||
|
boolean any = false;
|
||||||
|
if (!complete) {
|
||||||
|
for (int word = 0; word < 4; word++) {
|
||||||
|
missing[word] = columns[word] & ~(long) READY.getAcquire(ready, word);
|
||||||
|
any |= missing[word] != 0L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!any) {
|
||||||
|
copyColumns(columns, values, target);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long revision = ProvisionalSampling.revision();
|
||||||
|
source.fill(kind, chunkX, chunkZ, missing, target);
|
||||||
|
long[] known = new long[4];
|
||||||
|
for (int word = 0; word < 4; word++) {
|
||||||
|
known[word] = columns[word] & ~missing[word];
|
||||||
|
}
|
||||||
|
copyColumns(known, values, target);
|
||||||
|
if (revision == ProvisionalSampling.revision()) {
|
||||||
|
publish(missing, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void publish(long[] missing, Object computed) {
|
||||||
|
copyColumns(missing, computed, values);
|
||||||
|
for (int word = 0; word < 4; word++) {
|
||||||
|
if (missing[word] != 0L) {
|
||||||
|
READY.getAndBitwiseOrRelease(ready, word, missing[word]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copyColumns(long[] columns, Object from, Object to) {
|
||||||
|
for (int word = 0; word < 4; word++) {
|
||||||
|
long bits = columns[word];
|
||||||
|
while (bits != 0L) {
|
||||||
|
int index = (word << 6) | Long.numberOfTrailingZeros(bits);
|
||||||
|
if (kind.doubles()) {
|
||||||
|
((double[]) to)[index] = ((double[]) from)[index];
|
||||||
|
} else {
|
||||||
|
((Object[]) to)[index] = ((Object[]) from)[index];
|
||||||
|
}
|
||||||
|
bits &= bits - 1L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object unpublishedFill(PlaneSource source, int chunkX, int chunkZ) {
|
||||||
|
long[] missing = missing();
|
||||||
|
Object computed = kind.doubles() ? new double[256] : new Object[256];
|
||||||
|
source.fill(kind, chunkX, chunkZ, missing, computed);
|
||||||
|
return merge(missing, computed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object merge(long[] missing, Object computed) {
|
||||||
|
for (int index = 0; index < 256; index++) {
|
||||||
|
if ((missing[index >>> 6] & (1L << (index & 63))) == 0L) {
|
||||||
|
if (kind.doubles()) {
|
||||||
|
((double[]) computed)[index] = ((double[]) values)[index];
|
||||||
|
} else {
|
||||||
|
((Object[]) computed)[index] = ((Object[]) values)[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return computed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Pending {
|
||||||
|
private final Thread owner;
|
||||||
|
private boolean done;
|
||||||
|
|
||||||
|
private Pending(Thread owner) {
|
||||||
|
this.owner = owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void complete() {
|
||||||
|
done = true;
|
||||||
|
notifyAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void await() {
|
||||||
|
boolean interrupted = false;
|
||||||
|
while (!done) {
|
||||||
|
try {
|
||||||
|
wait();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
interrupted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (interrupted) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package art.arcane.iris.generation.bake;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.runtime.GenerationCounter;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.LongAdder;
|
||||||
|
|
||||||
|
public final class GeneratorEvaluations {
|
||||||
|
private static final LongAdder BAKED_CHUNKS = new LongAdder();
|
||||||
|
private static final LongAdder BAKED = new LongAdder();
|
||||||
|
private static final LongAdder BATCHED = new LongAdder();
|
||||||
|
private static final LongAdder SCALAR = new LongAdder();
|
||||||
|
|
||||||
|
private GeneratorEvaluations() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bakedChunk(long evaluations) {
|
||||||
|
if (!GenerationCounter.ENABLED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BAKED_CHUNKS.increment();
|
||||||
|
BAKED.add(evaluations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void batched(long evaluations) {
|
||||||
|
if (!GenerationCounter.ENABLED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BATCHED.add(evaluations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void scalar(int evaluations) {
|
||||||
|
if (!GenerationCounter.ENABLED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SCALAR.add(evaluations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Snapshot snapshot() {
|
||||||
|
return new Snapshot(BAKED_CHUNKS.sum(), BAKED.sum(), BATCHED.sum(), SCALAR.sum());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void reset() {
|
||||||
|
BAKED_CHUNKS.reset();
|
||||||
|
BAKED.reset();
|
||||||
|
BATCHED.reset();
|
||||||
|
SCALAR.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Snapshot(long bakedChunks, long bakedEvaluations, long batchedEvaluations, long scalarEvaluations) {
|
||||||
|
public double bakedEvaluationsPerChunk() {
|
||||||
|
return bakedChunks == 0L ? 0D : (double) bakedEvaluations / bakedChunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Snapshot minus(Snapshot earlier) {
|
||||||
|
return new Snapshot(bakedChunks - earlier.bakedChunks, bakedEvaluations - earlier.bakedEvaluations,
|
||||||
|
batchedEvaluations - earlier.batchedEvaluations, scalarEvaluations - earlier.scalarEvaluations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package art.arcane.iris.generation.bake;
|
||||||
|
|
||||||
|
public interface PlaneSource {
|
||||||
|
void fill(BakePlane plane, int chunkX, int chunkZ, long[] missing, Object values);
|
||||||
|
|
||||||
|
boolean mainThread();
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package art.arcane.iris.generation.bake;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.noise.GeneratorProgram;
|
||||||
|
import art.arcane.iris.generation.noise.IrisGenerator;
|
||||||
|
import art.arcane.iris.generation.noise.TerrainBatchScratch;
|
||||||
|
import art.arcane.volmlib.util.math.M;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public final class TerrainProgram {
|
||||||
|
private static final int SKIPPED = 0;
|
||||||
|
private static final int CONSTANT = 1;
|
||||||
|
private static final int ACTIVE = 2;
|
||||||
|
|
||||||
|
private final IrisGenerator[][] generators;
|
||||||
|
private final GeneratorProgram[][] programs;
|
||||||
|
private final long heightSeed;
|
||||||
|
|
||||||
|
private TerrainProgram(IrisGenerator[][] generators, GeneratorProgram[][] programs, long heightSeed) {
|
||||||
|
this.generators = generators;
|
||||||
|
this.programs = programs;
|
||||||
|
this.heightSeed = heightSeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TerrainProgram compile(IrisGenerator[][] generators, long heightSeed) {
|
||||||
|
GeneratorProgram[][] programs = new GeneratorProgram[generators.length][];
|
||||||
|
for (int group = 0; group < generators.length; group++) {
|
||||||
|
programs[group] = new GeneratorProgram[generators[group].length];
|
||||||
|
for (int index = 0; index < generators[group].length; index++) {
|
||||||
|
IrisGenerator generator = generators[group][index];
|
||||||
|
programs[group][index] = generator.supportsBatch() ? generator.compileProgram(heightSeed) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new TerrainProgram(generators, programs, heightSeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long heightSeed() {
|
||||||
|
return heightSeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double average(int group, TerrainBatchScratch scratch, double low, double high, double x, double z) {
|
||||||
|
IrisGenerator[] members = generators[group];
|
||||||
|
if (members.length == 0) {
|
||||||
|
return 0D;
|
||||||
|
}
|
||||||
|
if (!Double.isFinite(low) || !Double.isFinite(high)) {
|
||||||
|
throw new IllegalStateException("Terrain generator bounds were not finite at " + x + "," + z
|
||||||
|
+ " (generator=" + members[0].getLoadKey() + ", bounds=" + low + ".." + high + ").");
|
||||||
|
}
|
||||||
|
double height = 0D;
|
||||||
|
if (low == high) {
|
||||||
|
for (int member = 0; member < members.length; member++) {
|
||||||
|
height += low;
|
||||||
|
}
|
||||||
|
return height / members.length;
|
||||||
|
}
|
||||||
|
GeneratorEvaluations.scalar(members.length);
|
||||||
|
GeneratorProgram[] compiled = programs[group];
|
||||||
|
for (int member = 0; member < members.length; member++) {
|
||||||
|
GeneratorProgram program = compiled[member];
|
||||||
|
double noise = program != null ? program.height(scratch, x, z) : members[member].getHeight(x, z, heightSeed);
|
||||||
|
if (!Double.isFinite(noise)) {
|
||||||
|
throw new IllegalStateException("Terrain generator noise was not finite at " + x + "," + z
|
||||||
|
+ " (generator=" + members[member].getLoadKey() + ", noise=" + noise
|
||||||
|
+ ", bounds=" + low + ".." + high + ").");
|
||||||
|
}
|
||||||
|
height += M.lerp(low, high, noise);
|
||||||
|
}
|
||||||
|
return height / members.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int groups() {
|
||||||
|
return generators.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int groupSize(int group) {
|
||||||
|
return generators[group].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long accumulate(int group, TerrainBatchScratch scratch, double[] xs, double[] zs, int count,
|
||||||
|
double[] lows, double[] highs, double[] heights) {
|
||||||
|
IrisGenerator[] members = generators[group];
|
||||||
|
if (members.length == 0 || count == 0) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
int mark = scratch.mark();
|
||||||
|
int indexMark = scratch.indexMark();
|
||||||
|
try {
|
||||||
|
return accumulate(members, group, scratch, xs, zs, count, lows, highs, heights);
|
||||||
|
} finally {
|
||||||
|
scratch.reset(mark);
|
||||||
|
scratch.resetIndices(indexMark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long accumulate(IrisGenerator[] members, int group, TerrainBatchScratch scratch, double[] xs, double[] zs,
|
||||||
|
int count, double[] lows, double[] highs, double[] heights) {
|
||||||
|
int[] modes = scratch.indices(count);
|
||||||
|
int activeCount = 0;
|
||||||
|
for (int index = 0; index < count; index++) {
|
||||||
|
double low = lows[index];
|
||||||
|
double high = highs[index];
|
||||||
|
if (low == 0D && high == 0D) {
|
||||||
|
modes[index] = SKIPPED;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!Double.isFinite(low) || !Double.isFinite(high)) {
|
||||||
|
throw new IllegalStateException("Terrain generator bounds were not finite at " + xs[index] + "," + zs[index]
|
||||||
|
+ " (generator=" + members[0].getLoadKey() + ", bounds=" + low + ".." + high + ").");
|
||||||
|
}
|
||||||
|
if (low == high) {
|
||||||
|
modes[index] = CONSTANT;
|
||||||
|
double sum = 0D;
|
||||||
|
for (int member = 0; member < members.length; member++) {
|
||||||
|
sum += low;
|
||||||
|
}
|
||||||
|
heights[index] += sum / members.length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
modes[index] = ACTIVE;
|
||||||
|
activeCount++;
|
||||||
|
}
|
||||||
|
if (activeCount == 0) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
double[] activeX = scratch.buffer(activeCount);
|
||||||
|
double[] activeZ = scratch.buffer(activeCount);
|
||||||
|
double[] activeLow = scratch.buffer(activeCount);
|
||||||
|
double[] activeHigh = scratch.buffer(activeCount);
|
||||||
|
int[] activeIndex = scratch.indices(activeCount);
|
||||||
|
int cursor = 0;
|
||||||
|
for (int index = 0; index < count; index++) {
|
||||||
|
if (modes[index] == ACTIVE) {
|
||||||
|
activeX[cursor] = xs[index];
|
||||||
|
activeZ[cursor] = zs[index];
|
||||||
|
activeLow[cursor] = lows[index];
|
||||||
|
activeHigh[cursor] = highs[index];
|
||||||
|
activeIndex[cursor++] = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
double[] sums = scratch.buffer(activeCount);
|
||||||
|
double[] noise = scratch.buffer(activeCount);
|
||||||
|
Arrays.fill(sums, 0, activeCount, 0D);
|
||||||
|
GeneratorProgram[] compiled = programs[group];
|
||||||
|
for (int member = 0; member < members.length; member++) {
|
||||||
|
GeneratorProgram program = compiled[member];
|
||||||
|
if (program != null) {
|
||||||
|
program.heights(scratch, activeX, activeZ, activeCount, noise);
|
||||||
|
} else {
|
||||||
|
for (int index = 0; index < activeCount; index++) {
|
||||||
|
noise[index] = members[member].getHeight(activeX[index], activeZ[index], heightSeed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int index = 0; index < activeCount; index++) {
|
||||||
|
if (!Double.isFinite(noise[index])) {
|
||||||
|
throw new IllegalStateException("Terrain generator noise was not finite at " + activeX[index] + "," + activeZ[index]
|
||||||
|
+ " (generator=" + members[member].getLoadKey() + ", noise=" + noise[index]
|
||||||
|
+ ", bounds=" + activeLow[index] + ".." + activeHigh[index] + ").");
|
||||||
|
}
|
||||||
|
sums[index] += M.lerp(activeLow[index], activeHigh[index], noise[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int index = 0; index < activeCount; index++) {
|
||||||
|
heights[activeIndex[index]] += sums[index] / members.length;
|
||||||
|
}
|
||||||
|
return (long) activeCount * members.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ import art.arcane.iris.generation.decoration.IrisDepositVariant;
|
|||||||
import art.arcane.iris.generation.decoration.IrisOreGenerator;
|
import art.arcane.iris.generation.decoration.IrisOreGenerator;
|
||||||
import art.arcane.iris.generation.decoration.IrisOreBands;
|
import art.arcane.iris.generation.decoration.IrisOreBands;
|
||||||
import art.arcane.iris.generation.decoration.IrisProceduralObjects;
|
import art.arcane.iris.generation.decoration.IrisProceduralObjects;
|
||||||
import art.arcane.iris.generation.hydrology.IrisRiverPolicy;
|
import art.arcane.iris.generation.hydrology.config.IrisHydrologyPolicy;
|
||||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||||
import art.arcane.iris.generation.noise.NoiseStyle;
|
import art.arcane.iris.generation.noise.NoiseStyle;
|
||||||
import art.arcane.iris.generation.terrain.InferredType;
|
import art.arcane.iris.generation.terrain.InferredType;
|
||||||
@@ -161,8 +161,8 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
|
|||||||
private int lockLayersMax = 7;
|
private int lockLayersMax = 7;
|
||||||
@Description("Profile-driven 3D cave configuration")
|
@Description("Profile-driven 3D cave configuration")
|
||||||
private IrisCaveProfile caveProfile = new IrisCaveProfile();
|
private IrisCaveProfile caveProfile = new IrisCaveProfile();
|
||||||
@Description("Biome-level river placement, routing, profile, biome, and geometry policy. Omit to inherit region and dimension settings.")
|
@Description("Water settings for this biome: rainfall, pond density, where streams and ponds may form, and the fluid that fills them. A value left unset comes from the region, then from the dimension.")
|
||||||
private IrisRiverPolicy riverPolicy = null;
|
private IrisHydrologyPolicy hydrologyPolicy = null;
|
||||||
@MinNumber(1)
|
@MinNumber(1)
|
||||||
@MaxNumber(512)
|
@MaxNumber(512)
|
||||||
@Description("The rarity of this biome (integer)")
|
@Description("The rarity of this biome (integer)")
|
||||||
@@ -515,7 +515,10 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public CNG getChildrenGenerator(RNG random, int sig, double scale) {
|
public CNG getChildrenGenerator(RNG random, int sig, double scale) {
|
||||||
long seed = deriveChildStyleSeed(random, sig).getSeed();
|
return getChildrenGenerator(deriveChildStyleSeed(random, sig).getSeed(), scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CNG getChildrenGenerator(long seed, double scale) {
|
||||||
IrisData data = getLoader();
|
IrisData data = getLoader();
|
||||||
Engine engine = data == null ? null : data.getEngine();
|
Engine engine = data == null ? null : data.getEngine();
|
||||||
CachedChildGenerator recent = recentChildGenerator;
|
CachedChildGenerator recent = recentChildGenerator;
|
||||||
@@ -533,6 +536,10 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
|
|||||||
return random.nextParallelRNG(signature * CHILDREN_SEED_SALT);
|
return random.nextParallelRNG(signature * CHILDREN_SEED_SALT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static long childStyleSeed(RNG random, int signature) {
|
||||||
|
return deriveChildStyleSeed(random, signature).getSeed();
|
||||||
|
}
|
||||||
|
|
||||||
public KList<NativeBlockState> generateLayers(IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) {
|
public KList<NativeBlockState> generateLayers(IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) {
|
||||||
return IrisBiomeLayerGenerator.generateLayers(this, dim, wx, wz, random, maxDepth, height, rdata, complex);
|
return IrisBiomeLayerGenerator.generateLayers(this, dim, wx, wz, random, maxDepth, height, rdata, complex);
|
||||||
}
|
}
|
||||||
@@ -573,6 +580,10 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
|
|||||||
return IrisBiomeLayerGenerator.getLayerSeaHeightGenerators(this, rng, data);
|
return IrisBiomeLayerGenerator.getLayerSeaHeightGenerators(this, rng, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public KList<CNG> getCaveCeilingLayerHeightGenerators(RNG rng, IrisData data) {
|
||||||
|
return IrisBiomeLayerGenerator.getCaveCeilingLayerHeightGenerators(this, rng, data);
|
||||||
|
}
|
||||||
|
|
||||||
public NativeBlockState getSurfaceBlock(int x, int z, RNG rng, IrisData idm) {
|
public NativeBlockState getSurfaceBlock(int x, int z, RNG rng, IrisData idm) {
|
||||||
return IrisBiomeLayerGenerator.getSurfaceBlock(this, x, z, rng, idm);
|
return IrisBiomeLayerGenerator.getSurfaceBlock(this, x, z, rng, idm);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,8 +148,7 @@ final class IrisBiomeLayerGenerator {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
KList<CNG> heightGenerators = getHeightGenerators(
|
KList<CNG> heightGenerators = getCaveCeilingLayerHeightGenerators(biome, random, rdata);
|
||||||
biome.getLayerCeilingHeightGenerators(), ceilingLayers, 7235, random, rdata);
|
|
||||||
|
|
||||||
for (int i = 0; i < layerCount; i++) {
|
for (int i = 0; i < layerCount; i++) {
|
||||||
IrisBiomePaletteLayer layer = ceilingLayers.get(i);
|
IrisBiomePaletteLayer layer = ceilingLayers.get(i);
|
||||||
@@ -317,6 +316,10 @@ final class IrisBiomeLayerGenerator {
|
|||||||
return getHeightGenerators(biome.getLayerSeaHeightGenerators(), biome.getSeaLayers(), 7735, rng, data);
|
return getHeightGenerators(biome.getLayerSeaHeightGenerators(), biome.getSeaLayers(), 7735, rng, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static KList<CNG> getCaveCeilingLayerHeightGenerators(IrisBiome biome, RNG rng, IrisData data) {
|
||||||
|
return getHeightGenerators(biome.getLayerCeilingHeightGenerators(), biome.getCaveCeilingLayers(), 7235, rng, data);
|
||||||
|
}
|
||||||
|
|
||||||
private static KList<CNG> getHeightGenerators(IrisBiomeLayerHeightCache cache,
|
private static KList<CNG> getHeightGenerators(IrisBiomeLayerHeightCache cache,
|
||||||
KList<IrisBiomePaletteLayer> layers,
|
KList<IrisBiomePaletteLayer> layers,
|
||||||
int seedOffset, RNG rng, IrisData data) {
|
int seedOffset, RNG rng, IrisData data) {
|
||||||
|
|||||||
@@ -1,169 +0,0 @@
|
|||||||
/*
|
|
||||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
|
||||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package art.arcane.iris.generation.chunk;
|
|
||||||
|
|
||||||
import art.arcane.iris.platform.bukkit.nms.INMS;
|
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockVolume;
|
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
|
||||||
import art.arcane.iris.generation.block.BoundBlockState;
|
|
||||||
import art.arcane.iris.generation.block.IrisCustomData;
|
|
||||||
import art.arcane.volmlib.util.hunk.storage.StorageHunk;
|
|
||||||
import org.bukkit.block.data.BlockData;
|
|
||||||
import org.bukkit.generator.ChunkGenerator.ChunkData;
|
|
||||||
|
|
||||||
import java.lang.invoke.MethodHandles;
|
|
||||||
import java.lang.invoke.VarHandle;
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plain-array block buffer for one chunk. Stages write disjoint columns when they fan out and every hand-off
|
|
||||||
* between threads is a join, so the cells need no per-access fences. Each column tracks its highest stored y;
|
|
||||||
* a raise is a CAS so concurrent writers to one column can never lose the maximum.
|
|
||||||
*/
|
|
||||||
public class ChunkDataHunkHolder extends StorageHunk<NativeBlockState> implements NativeBlockVolume, ColumnExtent {
|
|
||||||
private static final BoundBlockState AIR = BoundBlockState.of("AIR");
|
|
||||||
private static final VarHandle COLUMN_TOPS = MethodHandles.arrayElementVarHandle(int[].class);
|
|
||||||
|
|
||||||
private final ChunkData chunk;
|
|
||||||
private final NativeBlockState[] data;
|
|
||||||
private final int[] columnTops;
|
|
||||||
private final int zStride;
|
|
||||||
|
|
||||||
public ChunkDataHunkHolder(ChunkData chunk) {
|
|
||||||
super(16, chunk.getMaxHeight() - chunk.getMinHeight(), 16);
|
|
||||||
this.chunk = chunk;
|
|
||||||
this.zStride = 16 * getHeight();
|
|
||||||
this.data = new NativeBlockState[zStride * 16];
|
|
||||||
this.columnTops = new int[256];
|
|
||||||
Arrays.fill(columnTops, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getWidth() {
|
|
||||||
return 16;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getDepth() {
|
|
||||||
return 16;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setRaw(int x, int y, int z, NativeBlockState t) {
|
|
||||||
// Block-hunk contract: null means "no write", never "erase" — ChunkDataHunkView and
|
|
||||||
// the modded ModdedBlockBuffer already discard nulls, and storing one here made the
|
|
||||||
// Bukkit output diverge from the modded loaders for the same engine emission.
|
|
||||||
if (t == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
data[index(x, y, z)] = t;
|
|
||||||
int column = (z << 4) | x;
|
|
||||||
int top = (int) COLUMN_TOPS.getOpaque(columnTops, column);
|
|
||||||
while (y > top) {
|
|
||||||
int witness = (int) COLUMN_TOPS.compareAndExchange(columnTops, column, top, y);
|
|
||||||
if (witness == top) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
top = witness;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public NativeBlockState getRaw(int x, int y, int z) {
|
|
||||||
if (y < 0 || y >= getHeight()) {
|
|
||||||
return AIR.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
NativeBlockState b = data[index(x, y, z)];
|
|
||||||
|
|
||||||
return b != null ? b : AIR.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public NativeBlockState getStoredRaw(int x, int y, int z) {
|
|
||||||
return data[index(x, y, z)];
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int highestStoredY(int x, int z) {
|
|
||||||
return (int) COLUMN_TOPS.getOpaque(columnTops, (z << 4) | x);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int index(int x, int y, int z) {
|
|
||||||
return (z * zStride) + (y << 4) + x;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void apply() {
|
|
||||||
applyTo(chunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void applyTo(ChunkData target) {
|
|
||||||
if (INMS.get().applyChunkDataBlocks(target, this)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int height = getHeight();
|
|
||||||
for (int x = 0; x < getWidth(); x++) {
|
|
||||||
for (int z = 0; z < getDepth(); z++) {
|
|
||||||
BlockData activeBlock = null;
|
|
||||||
int runStart = -1;
|
|
||||||
|
|
||||||
for (int y = 0; y < height; y++) {
|
|
||||||
NativeBlockState state = getStoredRaw(x, y, z);
|
|
||||||
BlockData block = state == null ? null : (BlockData) state.nativeHandle();
|
|
||||||
// Custom wrappers are not real Bukkit data; write the vanilla base like the
|
|
||||||
// NMS fast path (NMSBinding.applyChunkDataBlocks) does.
|
|
||||||
if (block instanceof IrisCustomData custom) {
|
|
||||||
block = custom.getBase();
|
|
||||||
}
|
|
||||||
if (block == null) {
|
|
||||||
flushRun(target, x, z, runStart, y, activeBlock);
|
|
||||||
activeBlock = null;
|
|
||||||
runStart = -1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeBlock != null && activeBlock.equals(block)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
flushRun(target, x, z, runStart, y, activeBlock);
|
|
||||||
activeBlock = block;
|
|
||||||
runStart = y;
|
|
||||||
}
|
|
||||||
|
|
||||||
flushRun(target, x, z, runStart, height, activeBlock);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void flushRun(ChunkData target, int x, int z, int startY, int endY, BlockData block) {
|
|
||||||
if (block == null || startY < 0 || endY <= startY) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int minY = target.getMinHeight();
|
|
||||||
if (endY - startY == 1) {
|
|
||||||
target.setBlock(x, startY + minY, z, block);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
target.setRegion(x, startY + minY, z, x + 1, endY + minY, z + 1, block);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||||
|
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package art.arcane.iris.generation.chunk;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.block.IrisCustomData;
|
||||||
|
import art.arcane.iris.platform.bukkit.nms.INMS;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
import org.bukkit.generator.ChunkGenerator.ChunkData;
|
||||||
|
|
||||||
|
public final class ChunkDataOutput {
|
||||||
|
private ChunkDataOutput() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void apply(ChunkData target, SectionBuffer blocks) {
|
||||||
|
if (INMS.get().applyChunkDataBlocks(target, blocks)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int height = blocks.getHeight();
|
||||||
|
for (int x = 0; x < 16; x++) {
|
||||||
|
for (int z = 0; z < 16; z++) {
|
||||||
|
BlockData activeBlock = null;
|
||||||
|
int runStart = -1;
|
||||||
|
|
||||||
|
for (int y = 0; y < height; y++) {
|
||||||
|
NativeBlockState state = blocks.getStoredRaw(x, y, z);
|
||||||
|
BlockData block = state == null ? null : (BlockData) state.nativeHandle();
|
||||||
|
if (block instanceof IrisCustomData custom) {
|
||||||
|
block = custom.getBase();
|
||||||
|
}
|
||||||
|
if (block == null) {
|
||||||
|
flushRun(target, x, z, runStart, y, activeBlock);
|
||||||
|
activeBlock = null;
|
||||||
|
runStart = -1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeBlock != null && activeBlock.equals(block)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
flushRun(target, x, z, runStart, y, activeBlock);
|
||||||
|
activeBlock = block;
|
||||||
|
runStart = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
flushRun(target, x, z, runStart, height, activeBlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void flushRun(ChunkData target, int x, int z, int startY, int endY, BlockData block) {
|
||||||
|
if (block == null || startY < 0 || endY <= startY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int minY = target.getMinHeight();
|
||||||
|
if (endY - startY == 1) {
|
||||||
|
target.setBlock(x, startY + minY, z, block);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.setRegion(x, startY + minY, z, x + 1, endY + minY, z + 1, block);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ package art.arcane.iris.generation.chunk;
|
|||||||
|
|
||||||
import art.arcane.volmlib.util.hunk.Hunk;
|
import art.arcane.volmlib.util.hunk.Hunk;
|
||||||
|
|
||||||
public final class ColumnExtentListeningHunk<T> implements Hunk<T>, ColumnExtent {
|
public final class ColumnExtentListeningHunk<T> implements Hunk<T>, ColumnExtent, ColumnRunHunk<T> {
|
||||||
private final Hunk<T> source;
|
private final Hunk<T> source;
|
||||||
private final Listener<T> listener;
|
private final Listener<T> listener;
|
||||||
|
|
||||||
@@ -35,6 +35,12 @@ public final class ColumnExtentListeningHunk<T> implements Hunk<T>, ColumnExtent
|
|||||||
source.setRaw(x, y, z, t);
|
source.setRaw(x, y, z, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fillColumnRun(int x, int z, int yFromInclusive, int yToInclusive, T value) {
|
||||||
|
listener.onRun(x, z, yFromInclusive, yToInclusive, value);
|
||||||
|
ColumnRunHunk.fillColumnRun(source, x, z, yFromInclusive, yToInclusive, value);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public T getRaw(int x, int y, int z) {
|
public T getRaw(int x, int y, int z) {
|
||||||
return source.getRaw(x, y, z);
|
return source.getRaw(x, y, z);
|
||||||
@@ -63,5 +69,11 @@ public final class ColumnExtentListeningHunk<T> implements Hunk<T>, ColumnExtent
|
|||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface Listener<T> {
|
public interface Listener<T> {
|
||||||
void onWrite(int x, int y, int z, T value);
|
void onWrite(int x, int y, int z, T value);
|
||||||
|
|
||||||
|
default void onRun(int x, int z, int yFromInclusive, int yToInclusive, T value) {
|
||||||
|
for (int y = yToInclusive; y >= yFromInclusive; y--) {
|
||||||
|
onWrite(x, y, z, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||||
|
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package art.arcane.iris.generation.chunk;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.util.hunk.Hunk;
|
||||||
|
|
||||||
|
public interface ColumnRunHunk<T> {
|
||||||
|
void fillColumnRun(int x, int z, int yFromInclusive, int yToInclusive, T value);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
static <T> void fillColumnRun(Hunk<T> hunk, int x, int z, int yFromInclusive, int yToInclusive, T value) {
|
||||||
|
if (hunk instanceof ColumnRunHunk<?> runs) {
|
||||||
|
((ColumnRunHunk<T>) runs).fillColumnRun(x, z, yFromInclusive, yToInclusive, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int y = yToInclusive; y >= yFromInclusive; y--) {
|
||||||
|
hunk.setRaw(x, y, z, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||||
|
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package art.arcane.iris.generation.chunk;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.block.BoundBlockState;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeSectionVolume;
|
||||||
|
import art.arcane.volmlib.util.hunk.Hunk;
|
||||||
|
|
||||||
|
import java.lang.invoke.MethodHandles;
|
||||||
|
import java.lang.invoke.VarHandle;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public final class SectionBuffer implements Hunk<NativeBlockState>, NativeSectionVolume, ColumnExtent, ColumnRunHunk<NativeBlockState> {
|
||||||
|
private static final BoundBlockState AIR = BoundBlockState.of("AIR");
|
||||||
|
private static final VarHandle SECTIONS = MethodHandles.arrayElementVarHandle(short[][].class);
|
||||||
|
private static final VarHandle COLUMN_TOPS = MethodHandles.arrayElementVarHandle(int[].class);
|
||||||
|
private static final int SECTION_CELLS = 4096;
|
||||||
|
private static final int MAX_PALETTE_ID = 0xFFFF;
|
||||||
|
private static final int INITIAL_PALETTE_CAPACITY = 64;
|
||||||
|
private static final int COLUMN_SLICE_SHIFT = 5;
|
||||||
|
|
||||||
|
private final int height;
|
||||||
|
private final short[][] sections;
|
||||||
|
private final int[] columnTops;
|
||||||
|
private final Object paletteLock = new Object();
|
||||||
|
private PaletteEntry[] slots;
|
||||||
|
private NativeBlockState[] states;
|
||||||
|
private int paletteSize;
|
||||||
|
|
||||||
|
public SectionBuffer(int height) {
|
||||||
|
if (height <= 0) {
|
||||||
|
throw new IllegalArgumentException("Unsupported section buffer height " + height);
|
||||||
|
}
|
||||||
|
this.height = height;
|
||||||
|
this.sections = new short[(height + 15) >> 4][];
|
||||||
|
this.columnTops = new int[16 << COLUMN_SLICE_SHIFT];
|
||||||
|
Arrays.fill(columnTops, -1);
|
||||||
|
this.slots = new PaletteEntry[INITIAL_PALETTE_CAPACITY << 1];
|
||||||
|
this.states = new NativeBlockState[INITIAL_PALETTE_CAPACITY];
|
||||||
|
this.paletteSize = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getWidth() {
|
||||||
|
return 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getHeight() {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getDepth() {
|
||||||
|
return 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int paletteId(NativeBlockState state) {
|
||||||
|
if (state == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
PaletteEntry[] table = slots;
|
||||||
|
int mask = table.length - 1;
|
||||||
|
int slot = System.identityHashCode(state) & mask;
|
||||||
|
PaletteEntry entry;
|
||||||
|
while ((entry = table[slot]) != null) {
|
||||||
|
if (entry.state == state) {
|
||||||
|
return entry.id;
|
||||||
|
}
|
||||||
|
slot = (slot + 1) & mask;
|
||||||
|
}
|
||||||
|
return insert(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRaw(int x, int y, int z, NativeBlockState t) {
|
||||||
|
if (t == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
writableSection(y >> 4)[((y & 15) << 8) | (z << 4) | x] = (short) paletteId(t);
|
||||||
|
raiseTop((x << COLUMN_SLICE_SHIFT) | z, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NativeBlockState getRaw(int x, int y, int z) {
|
||||||
|
if (y < 0 || y >= height) {
|
||||||
|
return AIR.get();
|
||||||
|
}
|
||||||
|
short[] section = sections[y >> 4];
|
||||||
|
if (section != null) {
|
||||||
|
int id = section[((y & 15) << 8) | (z << 4) | x] & 0xFFFF;
|
||||||
|
if (id != 0) {
|
||||||
|
return state(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AIR.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NativeBlockState getStoredRaw(int x, int y, int z) {
|
||||||
|
if (y < 0 || y >= height) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
short[] section = sections[y >> 4];
|
||||||
|
if (section == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int id = section[((y & 15) << 8) | (z << 4) | x] & 0xFFFF;
|
||||||
|
return id == 0 ? null : state(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fillColumnRun(int x, int z, int yFromInclusive, int yToInclusive, NativeBlockState value) {
|
||||||
|
if (value == null || yToInclusive < yFromInclusive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fillColumnRun(x, z, yFromInclusive, yToInclusive, paletteId(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void fillColumnRun(int localX, int localZ, int yFromInclusive, int yToInclusive, int paletteId) {
|
||||||
|
if (paletteId == 0 || yToInclusive < yFromInclusive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
short value = (short) paletteId;
|
||||||
|
int column = (localZ << 4) | localX;
|
||||||
|
int y = yFromInclusive;
|
||||||
|
while (y <= yToInclusive) {
|
||||||
|
int sectionIndex = y >> 4;
|
||||||
|
short[] section = writableSection(sectionIndex);
|
||||||
|
int last = Math.min(yToInclusive, (sectionIndex << 4) | 15);
|
||||||
|
int end = ((last & 15) << 8) | column;
|
||||||
|
for (int index = ((y & 15) << 8) | column; index <= end; index += 256) {
|
||||||
|
section[index] = value;
|
||||||
|
}
|
||||||
|
y = last + 1;
|
||||||
|
}
|
||||||
|
raiseTop((localX << COLUMN_SLICE_SHIFT) | localZ, yToInclusive);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int highestStoredY(int x, int z) {
|
||||||
|
return (int) COLUMN_TOPS.getOpaque(columnTops, (x << COLUMN_SLICE_SHIFT) | z);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int sectionCount() {
|
||||||
|
return sections.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public short[] section(int sectionIndex) {
|
||||||
|
return sections[sectionIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NativeBlockState[] palette() {
|
||||||
|
synchronized (paletteLock) {
|
||||||
|
return Arrays.copyOf(states, paletteSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private short[] writableSection(int sectionIndex) {
|
||||||
|
short[] section = sections[sectionIndex];
|
||||||
|
if (section != null) {
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
short[] created = new short[SECTION_CELLS];
|
||||||
|
short[] witness = (short[]) SECTIONS.compareAndExchange(sections, sectionIndex, (short[]) null, created);
|
||||||
|
return witness == null ? created : witness;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void raiseTop(int column, int y) {
|
||||||
|
int top = (int) COLUMN_TOPS.getOpaque(columnTops, column);
|
||||||
|
while (y > top) {
|
||||||
|
int witness = (int) COLUMN_TOPS.compareAndExchange(columnTops, column, top, y);
|
||||||
|
if (witness == top) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
top = witness;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private NativeBlockState state(int id) {
|
||||||
|
NativeBlockState[] current = states;
|
||||||
|
if (id < current.length) {
|
||||||
|
NativeBlockState state = current[id];
|
||||||
|
if (state != null) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
synchronized (paletteLock) {
|
||||||
|
return states[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int insert(NativeBlockState state) {
|
||||||
|
synchronized (paletteLock) {
|
||||||
|
PaletteEntry[] table = slots;
|
||||||
|
int mask = table.length - 1;
|
||||||
|
int slot = System.identityHashCode(state) & mask;
|
||||||
|
PaletteEntry entry;
|
||||||
|
while ((entry = table[slot]) != null) {
|
||||||
|
if (entry.state == state) {
|
||||||
|
return entry.id;
|
||||||
|
}
|
||||||
|
slot = (slot + 1) & mask;
|
||||||
|
}
|
||||||
|
int id = paletteSize;
|
||||||
|
if (id > MAX_PALETTE_ID) {
|
||||||
|
throw new IllegalStateException("A chunk wrote more than " + MAX_PALETTE_ID + " distinct block state instances");
|
||||||
|
}
|
||||||
|
NativeBlockState[] current = states;
|
||||||
|
if (id == current.length) {
|
||||||
|
current = Arrays.copyOf(current, current.length << 1);
|
||||||
|
}
|
||||||
|
current[id] = state;
|
||||||
|
states = current;
|
||||||
|
paletteSize = id + 1;
|
||||||
|
PaletteEntry created = new PaletteEntry(state, id);
|
||||||
|
if (id << 1 >= table.length) {
|
||||||
|
slots = rehash(table, created);
|
||||||
|
} else {
|
||||||
|
table[slot] = created;
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PaletteEntry[] rehash(PaletteEntry[] table, PaletteEntry created) {
|
||||||
|
PaletteEntry[] grown = new PaletteEntry[table.length << 1];
|
||||||
|
for (PaletteEntry entry : table) {
|
||||||
|
if (entry != null) {
|
||||||
|
place(grown, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
place(grown, created);
|
||||||
|
return grown;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void place(PaletteEntry[] table, PaletteEntry entry) {
|
||||||
|
int mask = table.length - 1;
|
||||||
|
int slot = System.identityHashCode(entry.state) & mask;
|
||||||
|
while (table[slot] != null) {
|
||||||
|
slot = (slot + 1) & mask;
|
||||||
|
}
|
||||||
|
table[slot] = entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record PaletteEntry(NativeBlockState state, int id) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ public class MultiBurst extends MultiBurstSupport {
|
|||||||
private final AtomicInteger parallelismBaseline = new AtomicInteger();
|
private final AtomicInteger parallelismBaseline = new AtomicInteger();
|
||||||
|
|
||||||
public MultiBurst() {
|
public MultiBurst() {
|
||||||
this("Iris");
|
this("Iris", Thread.MIN_PRIORITY, MultiBurstSupport::performanceProcessors);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MultiBurst(String name) {
|
public MultiBurst(String name) {
|
||||||
@@ -140,8 +140,15 @@ public class MultiBurst extends MultiBurstSupport {
|
|||||||
|
|
||||||
static final class HydrologyBurst extends MultiBurst {
|
static final class HydrologyBurst extends MultiBurst {
|
||||||
HydrologyBurst() {
|
HydrologyBurst() {
|
||||||
super("Iris Hydrology", () -> Math.max(2,
|
super("Iris Hydrology", HydrologyBurst::plannerParallelism);
|
||||||
IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism())));
|
}
|
||||||
|
|
||||||
|
static int plannerParallelism() {
|
||||||
|
return plannerParallelism(MultiBurstSupport.performanceProcessors(), Runtime.getRuntime().availableProcessors());
|
||||||
|
}
|
||||||
|
|
||||||
|
static int plannerParallelism(int performanceProcessors, int availableProcessors) {
|
||||||
|
return Math.max(2, Math.min(Math.max(4, performanceProcessors), availableProcessors));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import art.arcane.iris.generation.runtime.DimensionStackLayout;
|
|||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||||
import art.arcane.iris.generation.runtime.EngineMetrics;
|
import art.arcane.iris.generation.runtime.EngineMetrics;
|
||||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
|
||||||
import art.arcane.iris.generation.biome.FloatingIslandBoundarySampler;
|
import art.arcane.iris.generation.biome.FloatingIslandBoundarySampler;
|
||||||
import art.arcane.iris.generation.biome.IrisBiome;
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
import art.arcane.iris.generation.mantle.CaveTerrainSnapshot;
|
import art.arcane.iris.generation.mantle.CaveTerrainSnapshot;
|
||||||
@@ -17,8 +17,6 @@ import art.arcane.volmlib.nativelib.terrain.NativeBiome;
|
|||||||
import art.arcane.volmlib.util.hunk.Hunk;
|
import art.arcane.volmlib.util.hunk.Hunk;
|
||||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
public class ChunkContext {
|
public class ChunkContext {
|
||||||
@@ -27,6 +25,7 @@ public class ChunkContext {
|
|||||||
private FloatingBiomeOverlay floatingBiomes;
|
private FloatingBiomeOverlay floatingBiomes;
|
||||||
private CaveTerrainSnapshot caveTerrain;
|
private CaveTerrainSnapshot caveTerrain;
|
||||||
private TransitionTerrainColumn[] transitionTerrain;
|
private TransitionTerrainColumn[] transitionTerrain;
|
||||||
|
private volatile ChunkWaterRaster waterRaster;
|
||||||
private final int x;
|
private final int x;
|
||||||
private final int z;
|
private final int z;
|
||||||
private final IrisComplex complex;
|
private final IrisComplex complex;
|
||||||
@@ -91,42 +90,11 @@ public class ChunkContext {
|
|||||||
PrefillPlan resolvedPlan = prefillPlan == null ? PrefillPlan.NO_CAVE : prefillPlan;
|
PrefillPlan resolvedPlan = prefillPlan == null ? PrefillPlan.NO_CAVE : prefillPlan;
|
||||||
boolean capturePrefillMetric = metrics != null;
|
boolean capturePrefillMetric = metrics != null;
|
||||||
long totalStartNanos = capturePrefillMetric ? System.nanoTime() : 0L;
|
long totalStartNanos = capturePrefillMetric ? System.nanoTime() : 0L;
|
||||||
IrisHydrologyRuntime hydrologyRuntime = complex.getHydrologyRuntime();
|
if (shouldPrefillAsync(resolvedPlan, complex.isPlatformMainThread())) {
|
||||||
if (hydrologyRuntime != null && (resolvedPlan.height || resolvedPlan.biome)) {
|
prefillAsync(resolvedPlan);
|
||||||
hydrologyRuntime.prepareChunkColumns(x, z);
|
|
||||||
}
|
|
||||||
List<Runnable> fillTasks = new ArrayList<>(6);
|
|
||||||
if (resolvedPlan.height) {
|
|
||||||
fillTasks.add(() -> height.fillRounded(roundedHeight));
|
|
||||||
}
|
|
||||||
if (resolvedPlan.biome) {
|
|
||||||
fillTasks.add(new PrefillFillTask(biome));
|
|
||||||
}
|
|
||||||
if (resolvedPlan.rock) {
|
|
||||||
fillTasks.add(new PrefillFillTask(rock));
|
|
||||||
}
|
|
||||||
if (resolvedPlan.fluid) {
|
|
||||||
fillTasks.add(new PrefillFillTask(fluid));
|
|
||||||
}
|
|
||||||
if (resolvedPlan.region) {
|
|
||||||
fillTasks.add(new PrefillFillTask(region));
|
|
||||||
}
|
|
||||||
if (resolvedPlan.cave) {
|
|
||||||
fillTasks.add(new PrefillFillTask(cave));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!shouldPrefillAsync(resolvedPlan, fillTasks.size())) {
|
|
||||||
for (Runnable fillTask : fillTasks) {
|
|
||||||
fillTask.run();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
List<CompletableFuture<Void>> futures = new ArrayList<>(fillTasks.size());
|
prefillSurface(resolvedPlan);
|
||||||
for (Runnable fillTask : fillTasks) {
|
prefillPalettes(resolvedPlan);
|
||||||
futures.add(CompletableFuture.runAsync(fillTask, MultiBurst.burst));
|
|
||||||
}
|
|
||||||
for (CompletableFuture<Void> future : futures) {
|
|
||||||
future.join();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (capturePrefillMetric) {
|
if (capturePrefillMetric) {
|
||||||
metrics.getContextPrefill().put((System.nanoTime() - totalStartNanos) / 1_000_000D);
|
metrics.getContextPrefill().put((System.nanoTime() - totalStartNanos) / 1_000_000D);
|
||||||
@@ -134,14 +102,49 @@ public class ChunkContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static boolean shouldPrefillAsync(PrefillPlan prefillPlan, int fillTaskCount) {
|
static boolean shouldPrefillAsync(PrefillPlan prefillPlan, boolean platformMainThread) {
|
||||||
if (prefillPlan == PrefillPlan.NATURAL_TERRAIN || fillTaskCount <= 1 || !IrisPlatforms.isBound()) {
|
if (!platformMainThread || prefillPlan == PrefillPlan.NATURAL_TERRAIN || prefillPlan == PrefillPlan.NONE
|
||||||
|
|| !IrisPlatforms.isBound()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return !MultiBurst.burst.ownsCurrentThread();
|
return !MultiBurst.burst.ownsCurrentThread();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void prefillSurface(PrefillPlan prefillPlan) {
|
||||||
|
if (prefillPlan.height || prefillPlan.biome) {
|
||||||
|
waterRaster();
|
||||||
|
}
|
||||||
|
if (prefillPlan.height) {
|
||||||
|
height.fillRounded(roundedHeight);
|
||||||
|
}
|
||||||
|
if (prefillPlan.biome) {
|
||||||
|
biome.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prefillPalettes(PrefillPlan prefillPlan) {
|
||||||
|
if (prefillPlan.rock) {
|
||||||
|
rock.fill();
|
||||||
|
}
|
||||||
|
if (prefillPlan.fluid) {
|
||||||
|
fluid.fill();
|
||||||
|
}
|
||||||
|
if (prefillPlan.region) {
|
||||||
|
region.fill();
|
||||||
|
}
|
||||||
|
if (prefillPlan.cave) {
|
||||||
|
cave.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prefillAsync(PrefillPlan prefillPlan) {
|
||||||
|
CompletableFuture<Void> surface = CompletableFuture.runAsync(() -> prefillSurface(prefillPlan), MultiBurst.burst);
|
||||||
|
CompletableFuture<Void> palettes = CompletableFuture.runAsync(() -> prefillPalettes(prefillPlan), MultiBurst.burst);
|
||||||
|
surface.join();
|
||||||
|
palettes.join();
|
||||||
|
}
|
||||||
|
|
||||||
public void setTerrainBiomeOutput(Hunk<NativeBiome> terrainBiomes) {
|
public void setTerrainBiomeOutput(Hunk<NativeBiome> terrainBiomes) {
|
||||||
this.terrainBiomes = terrainBiomes;
|
this.terrainBiomes = terrainBiomes;
|
||||||
}
|
}
|
||||||
@@ -206,6 +209,18 @@ public class ChunkContext {
|
|||||||
return complex;
|
return complex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ChunkWaterRaster waterRaster() {
|
||||||
|
ChunkWaterRaster known = waterRaster;
|
||||||
|
if (known != null) {
|
||||||
|
return known;
|
||||||
|
}
|
||||||
|
ChunkWaterRaster raster = complex.getHydrologyRuntime().raster(x >> 4, z >> 4);
|
||||||
|
if (!raster.provisional()) {
|
||||||
|
waterRaster = raster;
|
||||||
|
}
|
||||||
|
return raster;
|
||||||
|
}
|
||||||
|
|
||||||
public FloatingIslandBoundarySampler getFloatingIslandBoundarySampler() {
|
public FloatingIslandBoundarySampler getFloatingIslandBoundarySampler() {
|
||||||
return floatingIslandBoundarySampler;
|
return floatingIslandBoundarySampler;
|
||||||
}
|
}
|
||||||
@@ -295,17 +310,4 @@ public class ChunkContext {
|
|||||||
|
|
||||||
public record TransitionTerrainColumn(Terrain3DColumn solids, int fluidHeight) {
|
public record TransitionTerrainColumn(Terrain3DColumn solids, int fluidHeight) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class PrefillFillTask implements Runnable {
|
|
||||||
private final ChunkedDataCache<?> dataCache;
|
|
||||||
|
|
||||||
private PrefillFillTask(ChunkedDataCache<?> dataCache) {
|
|
||||||
this.dataCache = dataCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
dataCache.fill();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -542,7 +542,7 @@ final class DecoratorCore {
|
|||||||
return stack;
|
return stack;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean allowsSurface(IrisDecorator decorator, NativeBlockState surface, IrisData data) {
|
static boolean allowsSurface(IrisDecorator decorator, NativeBlockState surface, IrisData data) {
|
||||||
return decorator.isForcePlace()
|
return decorator.isForcePlace()
|
||||||
|| (decorator.getWhitelist() == null || matchesPalette(decorator.getWhitelistArray(data), surface))
|
|| (decorator.getWhitelist() == null || matchesPalette(decorator.getWhitelistArray(data), surface))
|
||||||
&& (decorator.getBlacklist() == null || !matchesPalette(decorator.getBlacklistArray(data), surface));
|
&& (decorator.getBlacklist() == null || !matchesPalette(decorator.getBlacklistArray(data), surface));
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||||
|
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package art.arcane.iris.generation.decoration;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
|
||||||
|
public final class DepositRandom extends RNG {
|
||||||
|
private static final long MULTIPLIER = 0x5DEECE66DL;
|
||||||
|
private static final long ADDEND = 0xBL;
|
||||||
|
private static final long MASK = (1L << 48) - 1L;
|
||||||
|
private static final int JUMP_TABLE_SIZE = 48;
|
||||||
|
private static final long[] JUMP_MULTIPLIERS = new long[JUMP_TABLE_SIZE];
|
||||||
|
private static final long[] JUMP_ADDENDS = new long[JUMP_TABLE_SIZE];
|
||||||
|
|
||||||
|
static {
|
||||||
|
long multiplier = MULTIPLIER;
|
||||||
|
long addend = ADDEND;
|
||||||
|
for (int bit = 0; bit < JUMP_TABLE_SIZE; bit++) {
|
||||||
|
JUMP_MULTIPLIERS[bit] = multiplier;
|
||||||
|
JUMP_ADDENDS[bit] = addend;
|
||||||
|
addend = (addend * multiplier + addend) & MASK;
|
||||||
|
multiplier = (multiplier * multiplier) & MASK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long state;
|
||||||
|
|
||||||
|
public DepositRandom() {
|
||||||
|
super(0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DepositRandom(long seed) {
|
||||||
|
super(seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSeed(long seed) {
|
||||||
|
state = (seed ^ MULTIPLIER) & MASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int next(int bits) {
|
||||||
|
long advanced = (state * MULTIPLIER + ADDEND) & MASK;
|
||||||
|
state = advanced;
|
||||||
|
return (int) (advanced >>> (48 - bits));
|
||||||
|
}
|
||||||
|
|
||||||
|
public long state() {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void restore(long state) {
|
||||||
|
this.state = state & MASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void skip(long steps) {
|
||||||
|
long remaining = steps;
|
||||||
|
long advanced = state;
|
||||||
|
for (int bit = 0; remaining != 0L && bit < JUMP_TABLE_SIZE; bit++, remaining >>>= 1) {
|
||||||
|
if ((remaining & 1L) != 0L) {
|
||||||
|
advanced = (advanced * JUMP_MULTIPLIERS[bit] + JUMP_ADDENDS[bit]) & MASK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state = advanced;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,390 @@
|
|||||||
|
package art.arcane.iris.generation.decoration;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.block.B;
|
||||||
|
import art.arcane.iris.generation.block.IrisBlockData;
|
||||||
|
import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
|
||||||
|
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
import art.arcane.volmlib.util.hunk.Hunk;
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class HydrologyVegetation {
|
||||||
|
private static final long SALT = 0x4859445256454745L;
|
||||||
|
private static final long GOLDEN = 0x9e3779b97f4a7c15L;
|
||||||
|
private static final int[][] SIDES = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||||
|
|
||||||
|
private final long seed;
|
||||||
|
private final Engine engine;
|
||||||
|
private final RNG floorRng;
|
||||||
|
private final RNG surfaceRng;
|
||||||
|
private final RNG shoreRng;
|
||||||
|
|
||||||
|
public HydrologyVegetation(long seed, Engine engine) {
|
||||||
|
this.seed = seed ^ SALT;
|
||||||
|
this.engine = engine;
|
||||||
|
this.floorRng = new RNG(DecoratorCore.partSeed(this.seed, IrisDecorationPart.SEA_FLOOR));
|
||||||
|
this.surfaceRng = new RNG(DecoratorCore.partSeed(this.seed, IrisDecorationPart.SEA_SURFACE));
|
||||||
|
this.shoreRng = new RNG(DecoratorCore.partSeed(this.seed, IrisDecorationPart.SHORE_LINE));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void decorate(Hunk<NativeBlockState> output, int localX, int localZ, int x, int z, WaterRole role, Water water, IrisData data) {
|
||||||
|
Placement placement = placement(role);
|
||||||
|
if (placement == Placement.NONE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Decorators decorators = water.decorators(x, z);
|
||||||
|
if (decorators.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int ground = water.ground(x, z);
|
||||||
|
if (ground < 0 || ground + 1 >= output.getHeight()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Column column = new Column(output, localX, localZ, x, z, ground, water, data);
|
||||||
|
if (placement == Placement.SHORE) {
|
||||||
|
decorateShore(column, decorators.shore);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int head = water.head(x, z);
|
||||||
|
if (head <= ground || head + 1 >= output.getHeight()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
decorateFloor(column, head, decorators.floor);
|
||||||
|
if (placement == Placement.STILL && decorators.surface.length > 0 && still(water, x, z, head)) {
|
||||||
|
decorateSurface(column, head, decorators.surface);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Placement placement(WaterRole role) {
|
||||||
|
return switch (role) {
|
||||||
|
case CHANNEL, POND -> Placement.STILL;
|
||||||
|
case CASCADE, WATERFALL -> Placement.FLOOR;
|
||||||
|
case BANK, SHORE, POND_SHORE -> Placement.SHORE;
|
||||||
|
case NONE, DAM, MOUTH, INLET -> Placement.NONE;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void decorateFloor(Column column, int head, IrisDecorator[] bucket) {
|
||||||
|
int room = head - column.ground - 1;
|
||||||
|
if (room < 1 || bucket.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IrisDecorator decorator = pick(bucket, floorRng, column, IrisDecorationPart.SEA_FLOOR);
|
||||||
|
NativeBlockState support = column.output.get(column.localX, column.ground, column.localZ);
|
||||||
|
if (decorator == null || !B.isSolid(support) || !column.accepts(decorator, support)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
column.place(decorator, floorRng, column.ground + 1, room, Slot.FLOOR, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void decorateSurface(Column column, int head, IrisDecorator[] bucket) {
|
||||||
|
NativeBlockState support = column.output.get(column.localX, head, column.localZ);
|
||||||
|
if (!isWaterSource(support)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IrisDecorator decorator = pick(bucket, surfaceRng, column, IrisDecorationPart.SEA_SURFACE);
|
||||||
|
if (decorator == null || !column.accepts(decorator, support)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
column.place(decorator, surfaceRng, head + 1, column.output.getHeight() - head - 1, Slot.SURFACE, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void decorateShore(Column column, IrisDecorator[] bucket) {
|
||||||
|
NativeBlockState support = column.output.get(column.localX, column.ground, column.localZ);
|
||||||
|
if (bucket.length == 0 || support == null || !support.isSolid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IrisDecorator decorator = pick(bucket, shoreRng, column, IrisDecorationPart.SHORE_LINE);
|
||||||
|
if (decorator == null || !column.accepts(decorator, support)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
column.place(decorator, shoreRng, column.ground + 1, column.output.getHeight() - column.ground - 1, Slot.SHORE, support);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IrisDecorator pick(IrisDecorator[] bucket, RNG partRng, Column column, IrisDecorationPart part) {
|
||||||
|
IrisDecorator picked = null;
|
||||||
|
long choice = seed + part.ordinal();
|
||||||
|
int count = 0;
|
||||||
|
for (IrisDecorator decorator : bucket) {
|
||||||
|
if (!decorator.passesChanceGate(partRng, column.x, column.z, column.data)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
choice = mix(choice ^ mix(column.x * 0x632be59bd9b4e019L + column.z * 0x8cb92ba72f3d8dd7L + count));
|
||||||
|
if (count == 1 || Long.remainderUnsigned(choice, count) == 0L) {
|
||||||
|
picked = decorator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (picked == null || picked.isForcePlace() || picked.getSlopeCondition().isDefault()
|
||||||
|
|| picked.getSlopeCondition().isValid(column.water.slope(column.x, column.z))) {
|
||||||
|
return picked;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean still(Water water, int x, int z, int head) {
|
||||||
|
for (int[] side : SIDES) {
|
||||||
|
int neighbourX = x + side[0];
|
||||||
|
int neighbourZ = z + side[1];
|
||||||
|
int neighbourHead = water.head(neighbourX, neighbourZ);
|
||||||
|
if (neighbourHead != head && water.ground(neighbourX, neighbourZ) < neighbourHead) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isWaterSource(NativeBlockState state) {
|
||||||
|
if (state == null || !state.isWater()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String level = IrisProceduralBlocks.propertyValue(state, "level");
|
||||||
|
return level == null || level.equals("0");
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean holdsWater(NativeBlockState state) {
|
||||||
|
return B.isFluid(state) || B.isWaterLogged(state) || IrisSurfaceDecorator.isAquaticPlacement(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean plannedWater(Water water, int x, int z, int y) {
|
||||||
|
int ground = water.ground(x, z);
|
||||||
|
int top = water.head(x, z);
|
||||||
|
if (y <= ground || top <= ground) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int[] side : SIDES) {
|
||||||
|
int neighbourHead = water.head(x + side[0], z + side[1]);
|
||||||
|
if (neighbourHead > top && water.ground(x + side[0], z + side[1]) < neighbourHead) {
|
||||||
|
top = neighbourHead;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return y <= top;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean twoBlock(NativeBlockState state) {
|
||||||
|
String half = IrisProceduralBlocks.propertyValue(state, "half");
|
||||||
|
return "lower".equals(half) || "upper".equals(half);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long mix(long value) {
|
||||||
|
long mixed = (value + GOLDEN ^ (value + GOLDEN >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||||
|
mixed = (mixed ^ (mixed >>> 27)) * 0x94d049bb133111ebL;
|
||||||
|
return mixed ^ (mixed >>> 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Water {
|
||||||
|
int ground(int x, int z);
|
||||||
|
|
||||||
|
int head(int x, int z);
|
||||||
|
|
||||||
|
double slope(int x, int z);
|
||||||
|
|
||||||
|
Decorators decorators(int x, int z);
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum Placement {
|
||||||
|
NONE,
|
||||||
|
SHORE,
|
||||||
|
FLOOR,
|
||||||
|
STILL
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum Slot {
|
||||||
|
FLOOR,
|
||||||
|
SURFACE,
|
||||||
|
SHORE
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class Column {
|
||||||
|
private final Hunk<NativeBlockState> output;
|
||||||
|
private final int localX;
|
||||||
|
private final int localZ;
|
||||||
|
private final int x;
|
||||||
|
private final int z;
|
||||||
|
private final int ground;
|
||||||
|
private final Water water;
|
||||||
|
private final IrisData data;
|
||||||
|
|
||||||
|
private Column(Hunk<NativeBlockState> output, int localX, int localZ, int x, int z, int ground, Water water, IrisData data) {
|
||||||
|
this.output = output;
|
||||||
|
this.localX = localX;
|
||||||
|
this.localZ = localZ;
|
||||||
|
this.x = x;
|
||||||
|
this.z = z;
|
||||||
|
this.ground = ground;
|
||||||
|
this.water = water;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean accepts(IrisDecorator decorator, NativeBlockState support) {
|
||||||
|
return decorator.isForcePlace() || DecoratorCore.allowsSurface(decorator, support, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int stack(IrisDecorator decorator, RNG rng, int room) {
|
||||||
|
return decorator.isStacking() ? Math.min(room, DecoratorCore.computeStack(decorator, rng, x, z, data, room)) : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void place(IrisDecorator decorator, RNG rng, int baseY, int room, Slot slot, NativeBlockState support) {
|
||||||
|
NativeBlockState first = decorator.pickBlockData(rng, data, x, z);
|
||||||
|
if (first == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (twoBlock(first)) {
|
||||||
|
if (room >= 2) {
|
||||||
|
placePair(decorator, first, baseY, slot, support);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int stack = stack(decorator, rng, room);
|
||||||
|
NativeBlockState sampled = first;
|
||||||
|
boolean sampledTop = false;
|
||||||
|
for (int index = 0; index < stack; index++) {
|
||||||
|
int y = baseY + index;
|
||||||
|
boolean top = (stack == 1 ? 1D : (double) index / (stack - 1)) >= decorator.getTopThreshold();
|
||||||
|
if (top != sampledTop) {
|
||||||
|
sampled = top ? decorator.pickBlockDataTop(rng, data, x, z) : decorator.pickBlockData(rng, data, x, z);
|
||||||
|
sampledTop = top;
|
||||||
|
}
|
||||||
|
NativeBlockState block = sampled == null ? null : fit(sampled, y, slot, decorator, index == 0 ? support : null);
|
||||||
|
if (block == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
output.set(localX, y, localZ, block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void placePair(IrisDecorator decorator, NativeBlockState block, int y, Slot slot, NativeBlockState support) {
|
||||||
|
NativeBlockState lower = fit(block.withProperty("half", "lower"), y, slot, decorator, support);
|
||||||
|
NativeBlockState upper = lower == null ? null : fit(block.withProperty("half", "upper"), y + 1, slot, decorator, null);
|
||||||
|
if (upper != null) {
|
||||||
|
output.set(localX, y, localZ, lower);
|
||||||
|
output.set(localX, y + 1, localZ, upper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private NativeBlockState fit(NativeBlockState block, int y, Slot slot, IrisDecorator decorator, NativeBlockState support) {
|
||||||
|
NativeBlockState existing = output.get(localX, y, localZ);
|
||||||
|
if (slot == Slot.FLOOR) {
|
||||||
|
NativeBlockState placed = IrisProceduralBlocks.normalizeWaterlogging(block, existing, true);
|
||||||
|
return isWaterSource(existing) && IrisSurfaceDecorator.isAquaticPlacement(placed) ? placed : null;
|
||||||
|
}
|
||||||
|
if (!B.isAir(existing) || holdsWater(block)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (slot == Slot.SURFACE) {
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
if (besideWater(y) || support != null && !DecoratorCore.isValidShorelineSupport(decorator, block, support)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return IrisSugarCane.canPlace(block, output, localX, y, localZ, x, z, engine) ? block : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean besideWater(int y) {
|
||||||
|
for (int[] side : SIDES) {
|
||||||
|
int neighbourX = localX + side[0];
|
||||||
|
int neighbourZ = localZ + side[1];
|
||||||
|
if (neighbourX >= 0 && neighbourX < output.getWidth() && neighbourZ >= 0 && neighbourZ < output.getDepth()) {
|
||||||
|
if (holdsWater(output.get(neighbourX, y, neighbourZ))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (plannedWater(water, x + side[0], z + side[1], y)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Decorators {
|
||||||
|
public static final Decorators EMPTY = new Decorators(new IrisDecorator[0], new IrisDecorator[0], new IrisDecorator[0]);
|
||||||
|
|
||||||
|
private final IrisDecorator[] floor;
|
||||||
|
private final IrisDecorator[] surface;
|
||||||
|
private final IrisDecorator[] shore;
|
||||||
|
|
||||||
|
private Decorators(IrisDecorator[] floor, IrisDecorator[] surface, IrisDecorator[] shore) {
|
||||||
|
this.floor = floor;
|
||||||
|
this.surface = surface;
|
||||||
|
this.shore = shore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Decorators compile(List<IrisDecorator> decorators, IrisData data, String path) {
|
||||||
|
HydrologySettings.requireDecorators(decorators, path);
|
||||||
|
if (decorators == null || decorators.isEmpty()) {
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
List<IrisDecorator> floor = new ArrayList<>();
|
||||||
|
List<IrisDecorator> surface = new ArrayList<>();
|
||||||
|
List<IrisDecorator> shore = new ArrayList<>();
|
||||||
|
for (int index = 0; index < decorators.size(); index++) {
|
||||||
|
IrisDecorator decorator = decorators.get(index);
|
||||||
|
String field = path + "[" + index + "]";
|
||||||
|
IrisDecorationPart part = decorator.getPartOf();
|
||||||
|
requirePlaced(decorator.getPalette(), data, field + ".palette", part);
|
||||||
|
requirePlaced(decorator.getTopPalette(), data, field + ".topPalette", part);
|
||||||
|
requireKnown(decorator.getWhitelist(), data, field + ".whitelist");
|
||||||
|
requireKnown(decorator.getBlacklist(), data, field + ".blacklist");
|
||||||
|
switch (part) {
|
||||||
|
case SEA_FLOOR -> floor.add(decorator);
|
||||||
|
case SEA_SURFACE -> surface.add(decorator);
|
||||||
|
default -> shore.add(decorator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Decorators(floor.toArray(new IrisDecorator[0]), surface.toArray(new IrisDecorator[0]), shore.toArray(new IrisDecorator[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean empty() {
|
||||||
|
return floor.length == 0 && surface.length == 0 && shore.length == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void requirePlaced(List<IrisBlockData> blocks, IrisData data, String path, IrisDecorationPart part) {
|
||||||
|
if (blocks == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int index = 0; index < blocks.size(); index++) {
|
||||||
|
String field = path + "[" + index + "]";
|
||||||
|
String name = blocks.get(index).getBlock();
|
||||||
|
NativeBlockState state = requireKnown(blocks.get(index), data, field);
|
||||||
|
if (state.isFluid()) {
|
||||||
|
throw new IllegalArgumentException(field + " names the fluid '" + name + "'; water decorators never place fluids");
|
||||||
|
}
|
||||||
|
if (part == IrisDecorationPart.SEA_FLOOR && !waterCompatible(state)) {
|
||||||
|
throw new IllegalArgumentException(field + " names '" + name + "', which cannot stand in water; SEA_FLOOR decorators place"
|
||||||
|
+ " only waterloggable or water plants such as seagrass, kelp, coral and sea pickles");
|
||||||
|
}
|
||||||
|
if (part != IrisDecorationPart.SEA_FLOOR && holdsWater(state)) {
|
||||||
|
throw new IllegalArgumentException(field + " names '" + name + "', which holds water; " + part
|
||||||
|
+ " decorators grow in air and place only blocks without water, never waterlogged blocks, seagrass, kelp, sea pickles or coral");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void requireKnown(List<IrisBlockData> blocks, IrisData data, String path) {
|
||||||
|
if (blocks == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int index = 0; index < blocks.size(); index++) {
|
||||||
|
requireKnown(blocks.get(index), data, path + "[" + index + "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NativeBlockState requireKnown(IrisBlockData block, IrisData data, String path) {
|
||||||
|
NativeBlockState state = block.getBlockData(data);
|
||||||
|
if (state == null || state.isAir()) {
|
||||||
|
throw new IllegalArgumentException(path + " names the block '" + block.getBlock()
|
||||||
|
+ "', which does not exist on this server; water decorators never place air");
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean waterCompatible(NativeBlockState state) {
|
||||||
|
return IrisSurfaceDecorator.isAquaticPlacement(state) || IrisProceduralBlocks.propertyValue(state, "waterlogged") != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+324
-67
@@ -39,6 +39,7 @@ import art.arcane.volmlib.util.collection.KList;
|
|||||||
import art.arcane.volmlib.util.collection.KSet;
|
import art.arcane.volmlib.util.collection.KSet;
|
||||||
import art.arcane.volmlib.util.math.BlockPosition;
|
import art.arcane.volmlib.util.math.BlockPosition;
|
||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
@@ -57,6 +58,7 @@ import lombok.experimental.Accessors;
|
|||||||
public class IrisDepositGenerator {
|
public class IrisDepositGenerator {
|
||||||
private static final int FOOTPRINT_MAXIMUM_SIZE = 64;
|
private static final int FOOTPRINT_MAXIMUM_SIZE = 64;
|
||||||
private static final ThreadLocal<FootprintCells> FOOTPRINT_CELLS = ThreadLocal.withInitial(FootprintCells::new);
|
private static final ThreadLocal<FootprintCells> FOOTPRINT_CELLS = ThreadLocal.withInitial(FootprintCells::new);
|
||||||
|
private static final double[][] NODE_SINES = nodeSines();
|
||||||
private static final long FNV_OFFSET_BASIS = 0xcbf29ce484222325L;
|
private static final long FNV_OFFSET_BASIS = 0xcbf29ce484222325L;
|
||||||
private static final long FNV_PRIME = 0x100000001b3L;
|
private static final long FNV_PRIME = 0x100000001b3L;
|
||||||
|
|
||||||
@@ -282,28 +284,52 @@ public class IrisDepositGenerator {
|
|||||||
* only for clumps it keeps. Returns null without drawing when the shape has no footprint path.
|
* only for clumps it keeps. Returns null without drawing when the shape has no footprint path.
|
||||||
*/
|
*/
|
||||||
public ClumpFootprint sampleClumpFootprint(RNG rng, IrisData rdata) {
|
public ClumpFootprint sampleClumpFootprint(RNG rng, IrisData rdata) {
|
||||||
if (shape != IrisDepositShape.VANILLA_ELLIPSOID || minSize > FOOTPRINT_MAXIMUM_SIZE
|
if (!usesClumpFootprints(rdata)) {
|
||||||
|| maxSize > FOOTPRINT_MAXIMUM_SIZE) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
int paletteSize = getBlockData(rdata).size();
|
|
||||||
if (paletteSize == 0) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
KList<NativeBlockState> resolvedPalette = getBlockData(rdata);
|
||||||
|
int paletteSize = resolvedPalette.size();
|
||||||
int size = rng.i(minSize, maxSize + 1);
|
int size = rng.i(minSize, maxSize + 1);
|
||||||
if (size <= 0) {
|
if (size <= 0) {
|
||||||
return ClumpFootprint.EMPTY;
|
return ClumpFootprint.EMPTY;
|
||||||
}
|
}
|
||||||
FootprintCells cells = FOOTPRINT_CELLS.get();
|
FootprintCells cells = FOOTPRINT_CELLS.get();
|
||||||
cells.reset();
|
cells.reset();
|
||||||
vanillaEllipsoidCells(rng, size, cells);
|
if (paletteSize == 1) {
|
||||||
|
emitEllipsoidRows(rng, size, cells);
|
||||||
|
if (cells.count == 0) {
|
||||||
|
return ClumpFootprint.EMPTY;
|
||||||
|
}
|
||||||
|
skipUniformPicks(rng, cells.count);
|
||||||
|
} else {
|
||||||
|
emitEllipsoidCells(rng, size, cells, cells.scratch);
|
||||||
if (cells.count == 0) {
|
if (cells.count == 0) {
|
||||||
return ClumpFootprint.EMPTY;
|
return ClumpFootprint.EMPTY;
|
||||||
}
|
}
|
||||||
for (int cell = 0; cell < cells.count; cell++) {
|
for (int cell = 0; cell < cells.count; cell++) {
|
||||||
rng.i(0, paletteSize);
|
cells.picks[cell] = rng.i(0, paletteSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ClumpFootprint(cells.extentX * 2 + 1, cells.minY, cells, cells.stamp, resolvedPalette);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean usesClumpFootprints(IrisData rdata) {
|
||||||
|
return shape == IrisDepositShape.VANILLA_ELLIPSOID && minSize <= FOOTPRINT_MAXIMUM_SIZE
|
||||||
|
&& maxSize <= FOOTPRINT_MAXIMUM_SIZE && !getBlockData(rdata).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int lowestFootprintOffset(int size) {
|
||||||
|
return -4 - (Math.max(0, size) + 15) / 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void skipUniformPicks(RNG rng, int count) {
|
||||||
|
if (rng instanceof DepositRandom random) {
|
||||||
|
random.skip(2L * count);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int cell = 0; cell < count; cell++) {
|
||||||
|
rng.nextDouble();
|
||||||
}
|
}
|
||||||
return new ClumpFootprint(cells.extentX * 2 + 1, cells.minY, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -312,15 +338,87 @@ public class IrisDepositGenerator {
|
|||||||
* test is bit-identical to evaluating {@code nx * nx + ny * ny + nz * nz} in place.
|
* test is bit-identical to evaluating {@code nx * nx + ny * ny + nz * nz} in place.
|
||||||
*/
|
*/
|
||||||
static void vanillaEllipsoidCells(RNG rng, int size, CellSink sink) {
|
static void vanillaEllipsoidCells(RNG rng, int size, CellSink sink) {
|
||||||
|
emitEllipsoidCells(rng, size, sink, new EllipsoidScratch());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void emitEllipsoidCells(RNG rng, int size, CellSink sink, EllipsoidScratch scratch) {
|
||||||
|
double[] nodes = ellipsoidNodes(rng, size, scratch);
|
||||||
|
for (int node = 0; node < size; node++) {
|
||||||
|
if (!scratch.sphere(nodes, node)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double[] squaredY = scratch.squaredY;
|
||||||
|
double[] squaredZ = scratch.squaredZ;
|
||||||
|
for (int x = scratch.minX; x <= scratch.maxX; x++) {
|
||||||
|
double nx = (x + 0.5D - scratch.centerX) / scratch.radius;
|
||||||
|
double squaredX = nx * nx;
|
||||||
|
if (squaredX >= 1D) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int y = 0; y < scratch.spanY; y++) {
|
||||||
|
double squaredXY = squaredX + squaredY[y];
|
||||||
|
if (squaredXY >= 1D) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int z = 0; z < scratch.spanZ; z++) {
|
||||||
|
if (squaredXY + squaredZ[z] < 1D) {
|
||||||
|
sink.add(x, scratch.minY + y, scratch.minZ + z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void emitEllipsoidRows(RNG rng, int size, FootprintCells cells) {
|
||||||
|
EllipsoidScratch scratch = cells.scratch;
|
||||||
|
double[] nodes = ellipsoidNodes(rng, size, scratch);
|
||||||
|
for (int node = 0; node < size; node++) {
|
||||||
|
if (!scratch.sphere(nodes, node)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double[] squaredY = scratch.squaredY;
|
||||||
|
double[] squaredZ = scratch.squaredZ;
|
||||||
|
int zShift = scratch.minZ + FootprintCells.OFFSET;
|
||||||
|
for (int x = scratch.minX; x <= scratch.maxX; x++) {
|
||||||
|
double nx = (x + 0.5D - scratch.centerX) / scratch.radius;
|
||||||
|
double squaredX = nx * nx;
|
||||||
|
if (squaredX >= 1D) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int y = 0; y < scratch.spanY; y++) {
|
||||||
|
double squaredXY = squaredX + squaredY[y];
|
||||||
|
if (squaredXY >= 1D) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long row = 0L;
|
||||||
|
for (int z = 0; z < scratch.spanZ; z++) {
|
||||||
|
if (squaredXY + squaredZ[z] < 1D) {
|
||||||
|
row |= 1L << (zShift + z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (row != 0L) {
|
||||||
|
cells.addRow(x, scratch.minY + y, row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cells.finishRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double[] ellipsoidNodes(RNG rng, int size, EllipsoidScratch scratch) {
|
||||||
float angle = rng.nextFloat() * (float) Math.PI;
|
float angle = rng.nextFloat() * (float) Math.PI;
|
||||||
float reach = size / 8F;
|
float reach = size / 8F;
|
||||||
double startX = Math.sin(angle) * reach;
|
double sine = Math.sin(angle);
|
||||||
double endX = -Math.sin(angle) * reach;
|
double cosine = Math.cos(angle);
|
||||||
double startZ = Math.cos(angle) * reach;
|
double startX = sine * reach;
|
||||||
double endZ = -Math.cos(angle) * reach;
|
double endX = -sine * reach;
|
||||||
|
double startZ = cosine * reach;
|
||||||
|
double endZ = -cosine * reach;
|
||||||
double startY = rng.nextInt(3) - 2;
|
double startY = rng.nextInt(3) - 2;
|
||||||
double endY = rng.nextInt(3) - 2;
|
double endY = rng.nextInt(3) - 2;
|
||||||
double[] nodes = new double[size * 4];
|
double[] nodes = scratch.nodes(size * 4);
|
||||||
|
double[] sines = size < NODE_SINES.length ? NODE_SINES[size] : null;
|
||||||
|
|
||||||
for (int i = 0; i < size; i++) {
|
for (int i = 0; i < size; i++) {
|
||||||
float progress = (float) i / size;
|
float progress = (float) i / size;
|
||||||
@@ -328,7 +426,8 @@ public class IrisDepositGenerator {
|
|||||||
nodes[i * 4] = startX + (endX - startX) * progress;
|
nodes[i * 4] = startX + (endX - startX) * progress;
|
||||||
nodes[i * 4 + 1] = startY + (endY - startY) * progress;
|
nodes[i * 4 + 1] = startY + (endY - startY) * progress;
|
||||||
nodes[i * 4 + 2] = startZ + (endZ - startZ) * progress;
|
nodes[i * 4 + 2] = startZ + (endZ - startZ) * progress;
|
||||||
nodes[i * 4 + 3] = ((Math.sin(Math.PI * progress) + 1D) * radiusNoise + 1D) / 2D;
|
double arc = sines == null ? Math.sin(Math.PI * progress) : sines[i];
|
||||||
|
nodes[i * 4 + 3] = ((arc + 1D) * radiusNoise + 1D) / 2D;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < size - 1; i++) {
|
for (int i = 0; i < size - 1; i++) {
|
||||||
@@ -353,59 +452,20 @@ public class IrisDepositGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
double[] squaredY = new double[16];
|
private static double[][] nodeSines() {
|
||||||
double[] squaredZ = new double[16];
|
double[][] sines = new double[FOOTPRINT_MAXIMUM_SIZE + 1][];
|
||||||
|
for (int size = 1; size <= FOOTPRINT_MAXIMUM_SIZE; size++) {
|
||||||
|
double[] values = new double[size];
|
||||||
for (int i = 0; i < size; i++) {
|
for (int i = 0; i < size; i++) {
|
||||||
double radius = nodes[i * 4 + 3];
|
float progress = (float) i / size;
|
||||||
if (radius < 0D) {
|
values[i] = Math.sin(Math.PI * progress);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
double centerX = nodes[i * 4];
|
|
||||||
double centerY = nodes[i * 4 + 1];
|
|
||||||
double centerZ = nodes[i * 4 + 2];
|
|
||||||
int minX = (int) Math.floor(centerX - radius);
|
|
||||||
int maxX = Math.max((int) Math.floor(centerX + radius), minX);
|
|
||||||
int minY = (int) Math.floor(centerY - radius);
|
|
||||||
int maxY = Math.max((int) Math.floor(centerY + radius), minY);
|
|
||||||
int minZ = (int) Math.floor(centerZ - radius);
|
|
||||||
int maxZ = Math.max((int) Math.floor(centerZ + radius), minZ);
|
|
||||||
int spanY = maxY - minY + 1;
|
|
||||||
int spanZ = maxZ - minZ + 1;
|
|
||||||
if (spanY > squaredY.length) {
|
|
||||||
squaredY = new double[spanY];
|
|
||||||
}
|
|
||||||
if (spanZ > squaredZ.length) {
|
|
||||||
squaredZ = new double[spanZ];
|
|
||||||
}
|
|
||||||
for (int y = 0; y < spanY; y++) {
|
|
||||||
double ny = (minY + y + 0.5D - centerY) / radius;
|
|
||||||
squaredY[y] = ny * ny;
|
|
||||||
}
|
|
||||||
for (int z = 0; z < spanZ; z++) {
|
|
||||||
double nz = (minZ + z + 0.5D - centerZ) / radius;
|
|
||||||
squaredZ[z] = nz * nz;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int x = minX; x <= maxX; x++) {
|
|
||||||
double nx = (x + 0.5D - centerX) / radius;
|
|
||||||
double squaredX = nx * nx;
|
|
||||||
if (squaredX >= 1D) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (int y = 0; y < spanY; y++) {
|
|
||||||
double squaredXY = squaredX + squaredY[y];
|
|
||||||
if (squaredXY >= 1D) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (int z = 0; z < spanZ; z++) {
|
|
||||||
if (squaredXY + squaredZ[z] < 1D) {
|
|
||||||
sink.add(x, minY + y, minZ + z);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
sines[size] = values;
|
||||||
}
|
}
|
||||||
|
return sines;
|
||||||
}
|
}
|
||||||
|
|
||||||
IrisObject generateVanillaScattered(RNG rng, IrisData rdata, int size) {
|
IrisObject generateVanillaScattered(RNG rng, IrisData rdata, int size) {
|
||||||
@@ -630,8 +690,134 @@ public class IrisDepositGenerator {
|
|||||||
void add(int x, int y, int z);
|
void add(int x, int y, int z);
|
||||||
}
|
}
|
||||||
|
|
||||||
public record ClumpFootprint(int width, int minY, boolean empty) {
|
public static final class ClumpFootprint {
|
||||||
static final ClumpFootprint EMPTY = new ClumpFootprint(1, 0, true);
|
static final ClumpFootprint EMPTY = new ClumpFootprint(1, 0, null, 0L, null);
|
||||||
|
|
||||||
|
private final int width;
|
||||||
|
private final int minY;
|
||||||
|
private final FootprintCells source;
|
||||||
|
private final long stamp;
|
||||||
|
private final KList<NativeBlockState> palette;
|
||||||
|
|
||||||
|
private ClumpFootprint(int width, int minY, FootprintCells source, long stamp, KList<NativeBlockState> palette) {
|
||||||
|
this.width = width;
|
||||||
|
this.minY = minY;
|
||||||
|
this.source = source;
|
||||||
|
this.stamp = stamp;
|
||||||
|
this.palette = palette;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int width() {
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int minY() {
|
||||||
|
return minY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean empty() {
|
||||||
|
return source == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClumpCells cells() {
|
||||||
|
if (source == null) {
|
||||||
|
return ClumpCells.EMPTY;
|
||||||
|
}
|
||||||
|
if (source.stamp != stamp) {
|
||||||
|
throw new IllegalStateException("Deposit footprint cells were resampled before they were read");
|
||||||
|
}
|
||||||
|
return source.toCells(palette);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class ClumpCells {
|
||||||
|
static final ClumpCells EMPTY = new ClumpCells(new int[0], new NativeBlockState[0], null);
|
||||||
|
|
||||||
|
private final int[] positions;
|
||||||
|
private final NativeBlockState[] blocks;
|
||||||
|
private final NativeBlockState uniform;
|
||||||
|
|
||||||
|
private ClumpCells(int[] positions, NativeBlockState[] blocks, NativeBlockState uniform) {
|
||||||
|
this.positions = positions;
|
||||||
|
this.blocks = blocks;
|
||||||
|
this.uniform = uniform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int size() {
|
||||||
|
return positions.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int x(int cell) {
|
||||||
|
return (positions[cell] >> 12) - FootprintCells.OFFSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int y(int cell) {
|
||||||
|
return ((positions[cell] >> 6) & (FootprintCells.AXIS - 1)) - FootprintCells.OFFSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int z(int cell) {
|
||||||
|
return (positions[cell] & (FootprintCells.AXIS - 1)) - FootprintCells.OFFSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NativeBlockState block(int cell) {
|
||||||
|
return blocks == null ? uniform : blocks[cell];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class EllipsoidScratch {
|
||||||
|
private double[] nodes = new double[256];
|
||||||
|
private double[] squaredY = new double[16];
|
||||||
|
private double[] squaredZ = new double[16];
|
||||||
|
private double centerX;
|
||||||
|
private double radius;
|
||||||
|
private int minX;
|
||||||
|
private int maxX;
|
||||||
|
private int minY;
|
||||||
|
private int spanY;
|
||||||
|
private int minZ;
|
||||||
|
private int spanZ;
|
||||||
|
|
||||||
|
private double[] nodes(int length) {
|
||||||
|
if (nodes.length < length) {
|
||||||
|
nodes = new double[length];
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sphere(double[] values, int node) {
|
||||||
|
double sphereRadius = values[node * 4 + 3];
|
||||||
|
if (sphereRadius < 0D) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
double sphereX = values[node * 4];
|
||||||
|
double sphereY = values[node * 4 + 1];
|
||||||
|
double sphereZ = values[node * 4 + 2];
|
||||||
|
minX = (int) Math.floor(sphereX - sphereRadius);
|
||||||
|
maxX = Math.max((int) Math.floor(sphereX + sphereRadius), minX);
|
||||||
|
minY = (int) Math.floor(sphereY - sphereRadius);
|
||||||
|
int maxY = Math.max((int) Math.floor(sphereY + sphereRadius), minY);
|
||||||
|
minZ = (int) Math.floor(sphereZ - sphereRadius);
|
||||||
|
int maxZ = Math.max((int) Math.floor(sphereZ + sphereRadius), minZ);
|
||||||
|
spanY = maxY - minY + 1;
|
||||||
|
spanZ = maxZ - minZ + 1;
|
||||||
|
if (spanY > squaredY.length) {
|
||||||
|
squaredY = new double[spanY];
|
||||||
|
}
|
||||||
|
if (spanZ > squaredZ.length) {
|
||||||
|
squaredZ = new double[spanZ];
|
||||||
|
}
|
||||||
|
for (int y = 0; y < spanY; y++) {
|
||||||
|
double ny = (minY + y + 0.5D - sphereY) / sphereRadius;
|
||||||
|
squaredY[y] = ny * ny;
|
||||||
|
}
|
||||||
|
for (int z = 0; z < spanZ; z++) {
|
||||||
|
double nz = (minZ + z + 0.5D - sphereZ) / sphereRadius;
|
||||||
|
squaredZ[z] = nz * nz;
|
||||||
|
}
|
||||||
|
centerX = sphereX;
|
||||||
|
radius = sphereRadius;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class FootprintCells implements CellSink {
|
private static final class FootprintCells implements CellSink {
|
||||||
@@ -639,10 +825,16 @@ public class IrisDepositGenerator {
|
|||||||
private static final int AXIS = 64;
|
private static final int AXIS = 64;
|
||||||
private final long[] occupied = new long[AXIS * AXIS * AXIS / 64];
|
private final long[] occupied = new long[AXIS * AXIS * AXIS / 64];
|
||||||
private final int[] touched = new int[AXIS * AXIS * AXIS / 64];
|
private final int[] touched = new int[AXIS * AXIS * AXIS / 64];
|
||||||
|
private final EllipsoidScratch scratch = new EllipsoidScratch();
|
||||||
|
private int[] order = new int[256];
|
||||||
|
private int[] picks = new int[256];
|
||||||
|
private long[] sortKeys = new long[256];
|
||||||
private int touchedCount;
|
private int touchedCount;
|
||||||
private int count;
|
private int count;
|
||||||
private int extentX;
|
private int extentX;
|
||||||
private int minY;
|
private int minY;
|
||||||
|
private boolean uniform;
|
||||||
|
private long stamp;
|
||||||
|
|
||||||
private void reset() {
|
private void reset() {
|
||||||
for (int index = 0; index < touchedCount; index++) {
|
for (int index = 0; index < touchedCount; index++) {
|
||||||
@@ -652,6 +844,8 @@ public class IrisDepositGenerator {
|
|||||||
count = 0;
|
count = 0;
|
||||||
extentX = 0;
|
extentX = 0;
|
||||||
minY = Integer.MAX_VALUE;
|
minY = Integer.MAX_VALUE;
|
||||||
|
uniform = false;
|
||||||
|
stamp++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -667,9 +861,72 @@ public class IrisDepositGenerator {
|
|||||||
touched[touchedCount++] = word;
|
touched[touchedCount++] = word;
|
||||||
}
|
}
|
||||||
occupied[word] = current | bit;
|
occupied[word] = current | bit;
|
||||||
count++;
|
if (count == order.length) {
|
||||||
|
order = Arrays.copyOf(order, count * 2);
|
||||||
|
picks = Arrays.copyOf(picks, count * 2);
|
||||||
|
sortKeys = new long[count * 2];
|
||||||
|
}
|
||||||
|
order[count++] = index;
|
||||||
extentX = Math.max(extentX, Math.abs(x));
|
extentX = Math.max(extentX, Math.abs(x));
|
||||||
minY = Math.min(minY, y);
|
minY = Math.min(minY, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addRow(int x, int y, long row) {
|
||||||
|
int word = (x + OFFSET) * AXIS + (y + OFFSET);
|
||||||
|
long current = occupied[word];
|
||||||
|
if (current == 0L) {
|
||||||
|
touched[touchedCount++] = word;
|
||||||
|
}
|
||||||
|
occupied[word] = current | row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishRows() {
|
||||||
|
int cells = 0;
|
||||||
|
int extent = 0;
|
||||||
|
int lowest = Integer.MAX_VALUE;
|
||||||
|
for (int index = 0; index < touchedCount; index++) {
|
||||||
|
int word = touched[index];
|
||||||
|
cells += Long.bitCount(occupied[word]);
|
||||||
|
extent = Math.max(extent, Math.abs(word / AXIS - OFFSET));
|
||||||
|
lowest = Math.min(lowest, word % AXIS - OFFSET);
|
||||||
|
}
|
||||||
|
count = cells;
|
||||||
|
extentX = extent;
|
||||||
|
minY = lowest;
|
||||||
|
uniform = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClumpCells toCells(KList<NativeBlockState> palette) {
|
||||||
|
if (uniform) {
|
||||||
|
return toUniformCells(palette.get(0));
|
||||||
|
}
|
||||||
|
for (int cell = 0; cell < count; cell++) {
|
||||||
|
sortKeys[cell] = ((long) order[cell] << 32) | picks[cell];
|
||||||
|
}
|
||||||
|
Arrays.sort(sortKeys, 0, count);
|
||||||
|
int[] positions = new int[count];
|
||||||
|
NativeBlockState[] blocks = new NativeBlockState[count];
|
||||||
|
for (int cell = 0; cell < count; cell++) {
|
||||||
|
long key = sortKeys[cell];
|
||||||
|
positions[cell] = (int) (key >>> 32);
|
||||||
|
blocks[cell] = palette.get((int) key);
|
||||||
|
}
|
||||||
|
return new ClumpCells(positions, blocks, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClumpCells toUniformCells(NativeBlockState block) {
|
||||||
|
Arrays.sort(touched, 0, touchedCount);
|
||||||
|
int[] positions = new int[count];
|
||||||
|
int cell = 0;
|
||||||
|
for (int index = 0; index < touchedCount; index++) {
|
||||||
|
int word = touched[index];
|
||||||
|
long row = occupied[word];
|
||||||
|
while (row != 0L) {
|
||||||
|
positions[cell++] = (word << 6) | Long.numberOfTrailingZeros(row);
|
||||||
|
row &= row - 1L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ClumpCells(positions, null, block);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package art.arcane.iris.generation.decoration;
|
package art.arcane.iris.generation.decoration;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.pack.loading.IrisData;
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
import art.arcane.iris.pack.value.IrisRange;
|
import art.arcane.iris.pack.value.IrisRange;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
@@ -79,6 +80,14 @@ public final class IrisOreBands {
|
|||||||
return minY <= maxY;
|
return minY <= maxY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int lowestY() {
|
||||||
|
return minY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int highestY() {
|
||||||
|
return maxY;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean contains(int y) {
|
public boolean contains(int y) {
|
||||||
return y >= minY && y <= maxY;
|
return y >= minY && y <= maxY;
|
||||||
}
|
}
|
||||||
@@ -86,10 +95,10 @@ public final class IrisOreBands {
|
|||||||
/**
|
/**
|
||||||
* The ore placed at a Y inside {@link #contains(int)}, or null when no generator places one.
|
* The ore placed at a Y inside {@link #contains(int)}, or null when no generator places one.
|
||||||
*/
|
*/
|
||||||
public NativeBlockState generate(int x, int y, int z, RNG rng, IrisData data) {
|
public NativeBlockState generate(int x, int y, int z, RNG rng, IrisData data, Engine engine) {
|
||||||
IrisOreGenerator[] candidates = byY == null ? generators : byY[y - minY];
|
IrisOreGenerator[] candidates = byY == null ? generators : byY[y - minY];
|
||||||
for (IrisOreGenerator candidate : candidates) {
|
for (IrisOreGenerator candidate : candidates) {
|
||||||
NativeBlockState ore = candidate.generate(x, y, z, rng, data);
|
NativeBlockState ore = candidate.generate(x, y, z, rng, data, engine);
|
||||||
if (ore != null) {
|
if (ore != null) {
|
||||||
return ore;
|
return ore;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ public class IrisOreGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public NativeBlockState generate(int x, int y, int z, RNG rng, IrisData data) {
|
public NativeBlockState generate(int x, int y, int z, RNG rng, IrisData data) {
|
||||||
|
return generate(x, y, z, rng, data, data == null ? null : data.getEngine());
|
||||||
|
}
|
||||||
|
|
||||||
|
public NativeBlockState generate(int x, int y, int z, RNG rng, IrisData data, Engine engine) {
|
||||||
if (threshold <= 0 || !range.contains(y)) {
|
if (threshold <= 0 || !range.contains(y)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -73,7 +77,7 @@ public class IrisOreGenerator {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chance(rng, data).sample(x, y, z) > threshold) {
|
if (chance(rng, data, engine).sample(x, y, z) > threshold) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,8 +85,11 @@ public class IrisOreGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private OreChance chance(RNG rng, IrisData data) {
|
private OreChance chance(RNG rng, IrisData data) {
|
||||||
|
return chance(rng, data, data == null ? null : data.getEngine());
|
||||||
|
}
|
||||||
|
|
||||||
|
private OreChance chance(RNG rng, IrisData data, Engine engine) {
|
||||||
long seed = rng.getSeed();
|
long seed = rng.getSeed();
|
||||||
Engine engine = data == null ? null : data.getEngine();
|
|
||||||
CachedChance recent = recentChance;
|
CachedChance recent = recentChance;
|
||||||
if (recent != null && recent.key.seed == seed && recent.key.data == data && recent.key.engine == engine) {
|
if (recent != null && recent.key.seed == seed && recent.key.data == data && recent.key.engine == engine) {
|
||||||
return recent.chance;
|
return recent.chance;
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package art.arcane.iris.generation.decoration;
|
|||||||
|
|
||||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
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.generation.hydrology.HydrologyColumnLayer;
|
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
|
||||||
import art.arcane.iris.generation.biome.IrisBiome;
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
import art.arcane.volmlib.util.hunk.Hunk;
|
import art.arcane.volmlib.util.hunk.Hunk;
|
||||||
@@ -71,21 +69,18 @@ final class IrisSugarCane {
|
|||||||
|
|
||||||
private static boolean plannedWater(Engine engine, int x, int y, int z) {
|
private static boolean plannedWater(Engine engine, int x, int y, int z) {
|
||||||
IrisComplex complex = engine.getComplex();
|
IrisComplex complex = engine.getComplex();
|
||||||
HydrologyColumnSample sample = complex.sampleHydrologyColumn(x, z);
|
|
||||||
HydrologyColumnLayer fluid = sample == null ? null : sample.primarySurfaceFluidLayerOrNull();
|
|
||||||
if (fluid != null) {
|
|
||||||
return y > fluid.bedY() && y <= fluid.fluidHeadY()
|
|
||||||
&& supportsAdjacent(complex.resolveHydrologyFluid(fluid.profileKey(), x, z));
|
|
||||||
}
|
|
||||||
int terrainHeight = (int) Math.round(complex.getHeightStream().getDouble(x, z));
|
int terrainHeight = (int) Math.round(complex.getHeightStream().getDouble(x, z));
|
||||||
int fluidHeight = (int) Math.round(complex.getRiverWaterSurfaceStream().getDouble(x, z));
|
int fluidHeight = (int) Math.round(complex.getWaterHeadStream().getDouble(x, z));
|
||||||
if (y <= terrainHeight || y > fluidHeight) {
|
if (y <= terrainHeight || y > fluidHeight) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (complex.getWaterRoleStream().get(x, z).wet()) {
|
||||||
|
return supportsAdjacent(complex.resolveWaterFluid(x, z));
|
||||||
|
}
|
||||||
IrisBiome biome = complex.getTrueBiomeStream().get(x, z);
|
IrisBiome biome = complex.getTrueBiomeStream().get(x, z);
|
||||||
KList<NativeBlockState> layers = biome.generateSeaLayers(x, z,
|
KList<NativeBlockState> layers = biome.generateSeaLayers(x, z,
|
||||||
new RNG(engine.getSeedManager().getTerrain()), fluidHeight - terrainHeight, engine.getData());
|
new RNG(engine.getSeedManager().getTerrain()), fluidHeight - terrainHeight, engine.getData());
|
||||||
int depth = fluidHeight - y;
|
int depth = fluidHeight - y;
|
||||||
return supportsAdjacent(layers.hasIndex(depth) ? layers.get(depth) : complex.resolveSurfaceFluid(x, z));
|
return supportsAdjacent(layers.hasIndex(depth) ? layers.get(depth) : complex.resolveSeaFluid(x, z));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import art.arcane.iris.generation.hydrology.cave.CavePosition;
|
|
||||||
|
|
||||||
record CaveCandidateBounds(
|
|
||||||
int entryX,
|
|
||||||
int entryY,
|
|
||||||
int entryZ,
|
|
||||||
int waterHeadY,
|
|
||||||
int horizontalRadius,
|
|
||||||
int maximumDepth,
|
|
||||||
int dryHeadroom
|
|
||||||
) {
|
|
||||||
boolean containsColumn(int x, int z) {
|
|
||||||
long deltaX = (long) x - entryX;
|
|
||||||
long deltaZ = (long) z - entryZ;
|
|
||||||
long radiusSquared = (long) horizontalRadius * horizontalRadius;
|
|
||||||
return deltaX * deltaX + deltaZ * deltaZ <= radiusSquared;
|
|
||||||
}
|
|
||||||
|
|
||||||
int minimumY() {
|
|
||||||
return Math.toIntExact((long) entryY - maximumDepth);
|
|
||||||
}
|
|
||||||
|
|
||||||
int maximumY() {
|
|
||||||
return Math.max(entryY, Math.addExact(waterHeadY, dryHeadroom + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean contains(CavePosition position) {
|
|
||||||
if (!containsColumn(position.x(), position.z())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return position.y() >= minimumY() && position.y() <= maximumY();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import art.arcane.iris.generation.hydrology.cave.CavePosition;
|
|
||||||
import art.arcane.iris.generation.hydrology.cave.CavePositionIndex;
|
|
||||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction;
|
|
||||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCandidate;
|
|
||||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveFluidPolicy;
|
|
||||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveMode;
|
|
||||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlannerSettings;
|
|
||||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveSource;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
final class CaveCandidateBuilder {
|
|
||||||
private final RiverCourse course;
|
|
||||||
private final HydraulicSegment representative;
|
|
||||||
private final LinkedHashMap<CavePosition, HydrologyCaveAction> actions;
|
|
||||||
private final LinkedHashSet<CavePosition> openings;
|
|
||||||
private final CavePositionIndex openingIndex;
|
|
||||||
private final List<CaveSurfaceOpening> surfaceOpenings;
|
|
||||||
private int minimumX;
|
|
||||||
private int maximumX;
|
|
||||||
private int minimumY;
|
|
||||||
private int maximumY;
|
|
||||||
private int minimumZ;
|
|
||||||
private int maximumZ;
|
|
||||||
|
|
||||||
CaveCandidateBuilder(RiverCourse course, HydraulicSegment representative) {
|
|
||||||
this.course = course;
|
|
||||||
this.representative = representative;
|
|
||||||
this.actions = new LinkedHashMap<>();
|
|
||||||
this.openings = new LinkedHashSet<>();
|
|
||||||
this.openingIndex = new CavePositionIndex();
|
|
||||||
this.surfaceOpenings = HydrologyCaveCourseFilter.surfaceOpenings(course);
|
|
||||||
this.minimumX = Integer.MAX_VALUE;
|
|
||||||
this.maximumX = Integer.MIN_VALUE;
|
|
||||||
this.minimumY = Integer.MAX_VALUE;
|
|
||||||
this.maximumY = Integer.MIN_VALUE;
|
|
||||||
this.minimumZ = Integer.MAX_VALUE;
|
|
||||||
this.maximumZ = Integer.MIN_VALUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
RiverCourse course() {
|
|
||||||
return course;
|
|
||||||
}
|
|
||||||
|
|
||||||
private HydraulicSegment representative() {
|
|
||||||
return representative;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isEmpty() {
|
|
||||||
return actions.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
void addAction(CavePosition position, HydrologyCaveAction action) {
|
|
||||||
HydrologyCaveAction existing = actions.get(position);
|
|
||||||
if (existing == null || actionPriority(action) < actionPriority(existing)) {
|
|
||||||
actions.put(position, action);
|
|
||||||
}
|
|
||||||
include(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
void addOpening(CavePosition position) {
|
|
||||||
if (openingIndex.add(position.x(), position.y(), position.z())) {
|
|
||||||
openings.add(position);
|
|
||||||
}
|
|
||||||
include(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
void addOpening(int x, int y, int z) {
|
|
||||||
if (openingIndex.add(x, y, z)) {
|
|
||||||
openings.add(new CavePosition(x, y, z));
|
|
||||||
}
|
|
||||||
include(x, y, z);
|
|
||||||
}
|
|
||||||
|
|
||||||
void addOpeningNeighborhood(CavePosition position) {
|
|
||||||
addOpening(position);
|
|
||||||
for (int[] offset : HydrologyCaveCourseFilter.NEIGHBORS) {
|
|
||||||
addOpening(
|
|
||||||
position.x() + offset[0],
|
|
||||||
position.y() + offset[1],
|
|
||||||
position.z() + offset[2]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void addAdjacentSurfaceOpenings(
|
|
||||||
HydrologyFootprintCompiler.ValidationRaster validation
|
|
||||||
) {
|
|
||||||
ArrayList<CavePosition> actionPositions = new ArrayList<>(actions.keySet());
|
|
||||||
for (CavePosition position : actionPositions) {
|
|
||||||
for (int[] offset : HydrologyCaveCourseFilter.HORIZONTAL_NEIGHBORS) {
|
|
||||||
int neighborX = position.x() + offset[0];
|
|
||||||
int neighborZ = position.z() + offset[1];
|
|
||||||
if (actions.containsKey(new CavePosition(neighborX, position.y(), neighborZ))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
HydrologyColumnSample neighbor = validation.surfaceColumnAt(neighborX, neighborZ);
|
|
||||||
if (neighbor == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (HydrologyColumnLayer layer : neighbor.layers()) {
|
|
||||||
if (layer.feature().courseId() == course.id() && layer.channel() && layer.fluidOwned()
|
|
||||||
&& layer.bedY() < position.y() && position.y() <= layer.fluidHeadY()) {
|
|
||||||
addOpening(neighborX, position.y(), neighborZ);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void addSurfaceOpening(HydrologyColumnLayer layer, CavePosition position) {
|
|
||||||
for (CaveSurfaceOpening opening : surfaceOpenings) {
|
|
||||||
if (!opening.matches(layer, position)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (opening.includeNeighborhood()) {
|
|
||||||
addOpening(position);
|
|
||||||
for (int[] offset : HydrologyCaveCourseFilter.NEIGHBORS) {
|
|
||||||
int y = position.y() + offset[1];
|
|
||||||
if (y >= opening.minimumY()) {
|
|
||||||
addOpening(position.x() + offset[0], y, position.z() + offset[2]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
addOpening(position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void include(CavePosition position) {
|
|
||||||
include(position.x(), position.y(), position.z());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void include(int x, int y, int z) {
|
|
||||||
minimumX = Math.min(minimumX, x);
|
|
||||||
maximumX = Math.max(maximumX, x);
|
|
||||||
minimumY = Math.min(minimumY, y);
|
|
||||||
maximumY = Math.max(maximumY, y);
|
|
||||||
minimumZ = Math.min(minimumZ, z);
|
|
||||||
maximumZ = Math.max(maximumZ, z);
|
|
||||||
}
|
|
||||||
|
|
||||||
HydrologyCaveCandidate build(HydrologyCaveCourseFilter.Options options) {
|
|
||||||
HydrologyPoint start = representative.start();
|
|
||||||
int waterHead = representative.upstreamHeadY();
|
|
||||||
int entryY = Math.max(waterHead, maximumY);
|
|
||||||
CavePosition entry = new CavePosition(start.x(), entryY, start.z());
|
|
||||||
CavePosition target = new CavePosition(start.x(), waterHead, start.z());
|
|
||||||
int horizontalRadius = HydrologyCaveCourseFilter.horizontalRadius(
|
|
||||||
entry.x(),
|
|
||||||
entry.z(),
|
|
||||||
minimumX,
|
|
||||||
maximumX,
|
|
||||||
minimumZ,
|
|
||||||
maximumZ
|
|
||||||
);
|
|
||||||
int maximumDepth = Math.max(1, entryY - minimumY + 2);
|
|
||||||
int dryHeadroom = Math.max(0, maximumY - waterHead);
|
|
||||||
int volume = Math.max(1, actions.size());
|
|
||||||
HydrologyCavePlannerSettings settings = new HydrologyCavePlannerSettings(
|
|
||||||
horizontalRadius,
|
|
||||||
maximumDepth,
|
|
||||||
volume,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
dryHeadroom,
|
|
||||||
HydrologyCaveFluidPolicy.REJECT_EXISTING,
|
|
||||||
horizontalRadius,
|
|
||||||
maximumDepth
|
|
||||||
);
|
|
||||||
return new HydrologyCaveCandidate(
|
|
||||||
new HydrologyCaveSource(
|
|
||||||
course.id(),
|
|
||||||
entry,
|
|
||||||
target,
|
|
||||||
waterHead,
|
|
||||||
HydrologyCaveMode.GENERATED_GROTTO
|
|
||||||
),
|
|
||||||
course.profileKey(),
|
|
||||||
settings,
|
|
||||||
options.connectToExistingCaves() && course.type() == RiverCourseType.UNDERGROUND,
|
|
||||||
actions,
|
|
||||||
openings
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int actionPriority(HydrologyCaveAction action) {
|
|
||||||
return switch (action) {
|
|
||||||
case WET_SOURCE -> 0;
|
|
||||||
case FALLING_FLUID -> 1;
|
|
||||||
case DRY_AIR -> 2;
|
|
||||||
case SEAL_GUARD -> 3;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
-395
@@ -1,395 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import art.arcane.iris.generation.hydrology.cave.CavePosition;
|
|
||||||
import art.arcane.iris.generation.hydrology.cave.CaveVoxelView;
|
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
final class CaveCandidateSpanBuilder {
|
|
||||||
private final RiverCourse course;
|
|
||||||
private final HydraulicSegment representative;
|
|
||||||
private final LinkedHashMap<Long, CaveSpanSet> actions;
|
|
||||||
private final LinkedHashMap<Long, CaveSpanSet> openings;
|
|
||||||
private final LinkedHashMap<Long, CaveSpanSet> coastalGrottoActions;
|
|
||||||
private final LinkedHashMap<Long, CaveSpanSet> inlandGrottoActions;
|
|
||||||
private final List<CaveSurfaceOpening> surfaceOpenings;
|
|
||||||
private int minimumX;
|
|
||||||
private int maximumX;
|
|
||||||
private int minimumY;
|
|
||||||
private int maximumY;
|
|
||||||
private int minimumZ;
|
|
||||||
private int maximumZ;
|
|
||||||
|
|
||||||
CaveCandidateSpanBuilder(RiverCourse course, HydraulicSegment representative) {
|
|
||||||
this.course = course;
|
|
||||||
this.representative = representative;
|
|
||||||
this.actions = new LinkedHashMap<>();
|
|
||||||
this.openings = new LinkedHashMap<>();
|
|
||||||
this.coastalGrottoActions = new LinkedHashMap<>();
|
|
||||||
this.inlandGrottoActions = new LinkedHashMap<>();
|
|
||||||
this.surfaceOpenings = HydrologyCaveCourseFilter.surfaceOpenings(course);
|
|
||||||
this.minimumX = Integer.MAX_VALUE;
|
|
||||||
this.maximumX = Integer.MIN_VALUE;
|
|
||||||
this.minimumY = Integer.MAX_VALUE;
|
|
||||||
this.maximumY = Integer.MIN_VALUE;
|
|
||||||
this.minimumZ = Integer.MAX_VALUE;
|
|
||||||
this.maximumZ = Integer.MIN_VALUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
RiverCourse course() {
|
|
||||||
return course;
|
|
||||||
}
|
|
||||||
|
|
||||||
HydraulicSegment representative() {
|
|
||||||
return representative;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isEmpty() {
|
|
||||||
return actions.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean allowsIntentionalSurfaceExposure() {
|
|
||||||
return course.surfaceSinkholeContinuation();
|
|
||||||
}
|
|
||||||
|
|
||||||
void addAction(int x, int z, HydrologyColumnLayer layer) {
|
|
||||||
int minimumActionY = layer.bedY() + 1;
|
|
||||||
int maximumActionY = layer.ceilingY();
|
|
||||||
if (minimumActionY > maximumActionY) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
addSpan(actions, x, z, minimumActionY, maximumActionY);
|
|
||||||
if (layer.feature().type() == HydrologyFeatureType.COASTAL_GROTTO) {
|
|
||||||
addSpan(coastalGrottoActions, x, z, minimumActionY, maximumActionY);
|
|
||||||
} else if (layer.feature().type() == HydrologyFeatureType.INLAND_GROTTO) {
|
|
||||||
addSpan(inlandGrottoActions, x, z, minimumActionY, maximumActionY);
|
|
||||||
}
|
|
||||||
include(x, minimumActionY, z);
|
|
||||||
include(x, maximumActionY, z);
|
|
||||||
for (CaveSurfaceOpening opening : surfaceOpenings) {
|
|
||||||
if (!opening.matchesColumn(layer, x, z)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
int minimumOpeningY = Math.max(minimumActionY, opening.minimumY());
|
|
||||||
if (minimumOpeningY > maximumActionY) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!opening.includeNeighborhood()) {
|
|
||||||
addOpening(x, z, minimumOpeningY, maximumActionY);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
addOpening(x, z, Math.max(minimumOpeningY - 1, opening.minimumY()), maximumActionY + 1);
|
|
||||||
for (int[] offset : HydrologyCaveCourseFilter.HORIZONTAL_NEIGHBORS) {
|
|
||||||
addOpening(x + offset[0], z + offset[1], minimumOpeningY, maximumActionY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void addOceanOpening(int x, int z, int minimumOpeningY, int maximumOpeningY) {
|
|
||||||
if (minimumOpeningY <= maximumOpeningY) {
|
|
||||||
addOpening(x, z, minimumOpeningY, maximumOpeningY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void addOpeningNeighborhood(
|
|
||||||
int x,
|
|
||||||
int z,
|
|
||||||
int minimumOpeningY,
|
|
||||||
int maximumOpeningY
|
|
||||||
) {
|
|
||||||
if (minimumOpeningY > maximumOpeningY) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
addOpening(x, z, minimumOpeningY - 1, maximumOpeningY + 1);
|
|
||||||
for (int[] offset : HydrologyCaveCourseFilter.HORIZONTAL_NEIGHBORS) {
|
|
||||||
addOpening(x + offset[0], z + offset[1], minimumOpeningY, maximumOpeningY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void addAdjacentSurfaceOpenings(
|
|
||||||
HydrologyFootprintCompiler.ValidationRaster validation
|
|
||||||
) {
|
|
||||||
for (Map.Entry<Long, CaveSpanSet> entry : actions.entrySet()) {
|
|
||||||
int x = RiverFootprint.unpackX(entry.getKey());
|
|
||||||
int z = RiverFootprint.unpackZ(entry.getKey());
|
|
||||||
for (int[] offset : HydrologyCaveCourseFilter.HORIZONTAL_NEIGHBORS) {
|
|
||||||
int neighborX = x + offset[0];
|
|
||||||
int neighborZ = z + offset[1];
|
|
||||||
HydrologyColumnSample neighbor = validation.surfaceColumnAt(neighborX, neighborZ);
|
|
||||||
if (neighbor == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (HydrologyColumnLayer layer : neighbor.layers()) {
|
|
||||||
if (layer.feature().courseId() != course.id() || !layer.channel() || !layer.fluidOwned()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (CaveYSpan span : entry.getValue().spans()) {
|
|
||||||
int minimumY = Math.max(span.minimumY(), layer.bedY() + 1);
|
|
||||||
int maximumY = Math.min(span.maximumY(), layer.fluidHeadY());
|
|
||||||
if (minimumY <= maximumY) {
|
|
||||||
addAdjacentFluidOpening(neighborX, neighborZ, minimumY, maximumY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addAdjacentFluidOpening(int x, int z, int minimumY, int maximumY) {
|
|
||||||
CaveSpanSet owned = actions.get(RiverFootprint.pack(x, z));
|
|
||||||
if (owned == null) {
|
|
||||||
addOpening(x, z, minimumY, maximumY);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
int cursor = minimumY;
|
|
||||||
for (CaveYSpan span : owned.spans()) {
|
|
||||||
if (span.maximumY() < cursor) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (span.minimumY() > maximumY) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (cursor < span.minimumY()) {
|
|
||||||
addOpening(x, z, cursor, Math.min(maximumY, span.minimumY() - 1));
|
|
||||||
}
|
|
||||||
cursor = Math.max(cursor, span.maximumY() + 1);
|
|
||||||
if (cursor > maximumY) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addOpening(x, z, cursor, maximumY);
|
|
||||||
}
|
|
||||||
|
|
||||||
HydrologyFeatureType oversizedGrotto(HydrologyCaveCourseFilter.Options options) {
|
|
||||||
if (positionCount(coastalGrottoActions)
|
|
||||||
> options.maximumVolume(HydrologyFeatureType.COASTAL_GROTTO)) {
|
|
||||||
return HydrologyFeatureType.COASTAL_GROTTO;
|
|
||||||
}
|
|
||||||
if (positionCount(inlandGrottoActions)
|
|
||||||
> options.maximumVolume(HydrologyFeatureType.INLAND_GROTTO)) {
|
|
||||||
return HydrologyFeatureType.INLAND_GROTTO;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
long positionCount() {
|
|
||||||
return positionCount(actions);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean exposed(CaveVoxelView view) {
|
|
||||||
CaveCandidateBounds bounds = bounds();
|
|
||||||
for (Map.Entry<Long, CaveSpanSet> entry : actions.entrySet()) {
|
|
||||||
int x = RiverFootprint.unpackX(entry.getKey());
|
|
||||||
int z = RiverFootprint.unpackZ(entry.getKey());
|
|
||||||
CaveSpanSet actionSpans = entry.getValue();
|
|
||||||
CaveSpanSet openingSpans = openings.get(entry.getKey());
|
|
||||||
if (exposedDifference(actionSpans, null, openingSpans, x, z, bounds, view)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
for (CaveYSpan span : actionSpans.spans()) {
|
|
||||||
int lowerBoundary = span.minimumY() - 1;
|
|
||||||
if (!actionSpans.contains(lowerBoundary)
|
|
||||||
&& !contains(openingSpans, lowerBoundary)
|
|
||||||
&& exposed(x, lowerBoundary, z, bounds, view)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
int upperBoundary = span.maximumY() + 1;
|
|
||||||
if (!actionSpans.contains(upperBoundary)
|
|
||||||
&& !contains(openingSpans, upperBoundary)
|
|
||||||
&& exposed(x, upperBoundary, z, bounds, view)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (int[] offset : HydrologyCaveCourseFilter.HORIZONTAL_NEIGHBORS) {
|
|
||||||
int neighborX = x + offset[0];
|
|
||||||
int neighborZ = z + offset[1];
|
|
||||||
long neighborKey = RiverFootprint.pack(neighborX, neighborZ);
|
|
||||||
if (exposedDifference(
|
|
||||||
actionSpans,
|
|
||||||
actions.get(neighborKey),
|
|
||||||
openings.get(neighborKey),
|
|
||||||
neighborX,
|
|
||||||
neighborZ,
|
|
||||||
bounds,
|
|
||||||
view
|
|
||||||
)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean exposedDifference(
|
|
||||||
CaveSpanSet source,
|
|
||||||
CaveSpanSet excludedActions,
|
|
||||||
CaveSpanSet excludedOpenings,
|
|
||||||
int x,
|
|
||||||
int z,
|
|
||||||
CaveCandidateBounds bounds,
|
|
||||||
CaveVoxelView view
|
|
||||||
) {
|
|
||||||
for (CaveYSpan span : source.spans()) {
|
|
||||||
if (exposedDifference(
|
|
||||||
span,
|
|
||||||
excludedActions,
|
|
||||||
excludedOpenings,
|
|
||||||
x,
|
|
||||||
z,
|
|
||||||
bounds,
|
|
||||||
view
|
|
||||||
)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean exposedDifference(
|
|
||||||
CaveYSpan source,
|
|
||||||
CaveSpanSet excludedActions,
|
|
||||||
CaveSpanSet excludedOpenings,
|
|
||||||
int x,
|
|
||||||
int z,
|
|
||||||
CaveCandidateBounds bounds,
|
|
||||||
CaveVoxelView view
|
|
||||||
) {
|
|
||||||
List<CaveYSpan> actionSpans = excludedActions == null ? List.of() : excludedActions.spans();
|
|
||||||
List<CaveYSpan> openingSpans = excludedOpenings == null ? List.of() : excludedOpenings.spans();
|
|
||||||
int actionIndex = 0;
|
|
||||||
int openingIndex = 0;
|
|
||||||
long cursor = source.minimumY();
|
|
||||||
while (cursor <= source.maximumY()) {
|
|
||||||
while (actionIndex < actionSpans.size()
|
|
||||||
&& actionSpans.get(actionIndex).maximumY() < cursor) {
|
|
||||||
actionIndex++;
|
|
||||||
}
|
|
||||||
while (openingIndex < openingSpans.size()
|
|
||||||
&& openingSpans.get(openingIndex).maximumY() < cursor) {
|
|
||||||
openingIndex++;
|
|
||||||
}
|
|
||||||
CaveYSpan action = actionIndex < actionSpans.size() ? actionSpans.get(actionIndex) : null;
|
|
||||||
CaveYSpan opening = openingIndex < openingSpans.size() ? openingSpans.get(openingIndex) : null;
|
|
||||||
CaveYSpan excluded = first(action, opening);
|
|
||||||
if (excluded == null || excluded.minimumY() > source.maximumY()) {
|
|
||||||
return exposed(x, (int) cursor, source.maximumY(), z, bounds, view);
|
|
||||||
}
|
|
||||||
if (cursor < excluded.minimumY()
|
|
||||||
&& exposed(x, (int) cursor, excluded.minimumY() - 1, z, bounds, view)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
cursor = Math.max(cursor, (long) excluded.maximumY() + 1L);
|
|
||||||
if (excluded == action) {
|
|
||||||
actionIndex++;
|
|
||||||
} else {
|
|
||||||
openingIndex++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private CaveYSpan first(CaveYSpan first, CaveYSpan second) {
|
|
||||||
if (first == null) {
|
|
||||||
return second;
|
|
||||||
}
|
|
||||||
if (second == null) {
|
|
||||||
return first;
|
|
||||||
}
|
|
||||||
return first.minimumY() <= second.minimumY() ? first : second;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean exposed(
|
|
||||||
int x,
|
|
||||||
int minimumY,
|
|
||||||
int maximumY,
|
|
||||||
int z,
|
|
||||||
CaveCandidateBounds bounds,
|
|
||||||
CaveVoxelView view
|
|
||||||
) {
|
|
||||||
if (!bounds.containsColumn(x, z)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
int boundedMinimumY = Math.max(minimumY, bounds.minimumY());
|
|
||||||
int boundedMaximumY = Math.min(maximumY, bounds.maximumY());
|
|
||||||
return boundedMinimumY <= boundedMaximumY
|
|
||||||
&& view.hasAboveTerrainSurface(x, z, boundedMinimumY, boundedMaximumY);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean exposed(
|
|
||||||
int x,
|
|
||||||
int y,
|
|
||||||
int z,
|
|
||||||
CaveCandidateBounds bounds,
|
|
||||||
CaveVoxelView view
|
|
||||||
) {
|
|
||||||
CavePosition position = new CavePosition(x, y, z);
|
|
||||||
return bounds.contains(position)
|
|
||||||
&& view.isInWorld(position)
|
|
||||||
&& view.isAboveTerrainSurface(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
private CaveCandidateBounds bounds() {
|
|
||||||
HydrologyPoint start = representative.start();
|
|
||||||
int waterHead = representative.upstreamHeadY();
|
|
||||||
int entryY = Math.max(waterHead, maximumY);
|
|
||||||
int horizontalRadius = HydrologyCaveCourseFilter.horizontalRadius(
|
|
||||||
start.x(),
|
|
||||||
start.z(),
|
|
||||||
minimumX,
|
|
||||||
maximumX,
|
|
||||||
minimumZ,
|
|
||||||
maximumZ
|
|
||||||
);
|
|
||||||
int maximumDepth = Math.max(1, entryY - minimumY + 2);
|
|
||||||
int dryHeadroom = Math.max(0, maximumY - waterHead);
|
|
||||||
return new CaveCandidateBounds(
|
|
||||||
start.x(),
|
|
||||||
entryY,
|
|
||||||
start.z(),
|
|
||||||
waterHead,
|
|
||||||
horizontalRadius,
|
|
||||||
maximumDepth,
|
|
||||||
dryHeadroom
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addOpening(int x, int z, int minimumOpeningY, int maximumOpeningY) {
|
|
||||||
addSpan(openings, x, z, minimumOpeningY, maximumOpeningY);
|
|
||||||
include(x, minimumOpeningY, z);
|
|
||||||
include(x, maximumOpeningY, z);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void include(int x, int y, int z) {
|
|
||||||
minimumX = Math.min(minimumX, x);
|
|
||||||
maximumX = Math.max(maximumX, x);
|
|
||||||
minimumY = Math.min(minimumY, y);
|
|
||||||
maximumY = Math.max(maximumY, y);
|
|
||||||
minimumZ = Math.min(minimumZ, z);
|
|
||||||
maximumZ = Math.max(maximumZ, z);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void addSpan(
|
|
||||||
Map<Long, CaveSpanSet> spans,
|
|
||||||
int x,
|
|
||||||
int z,
|
|
||||||
int minimumY,
|
|
||||||
int maximumY
|
|
||||||
) {
|
|
||||||
spans.computeIfAbsent(RiverFootprint.pack(x, z), (Long ignored) -> new CaveSpanSet())
|
|
||||||
.add(minimumY, maximumY);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean contains(CaveSpanSet spans, int y) {
|
|
||||||
return spans != null && spans.contains(y);
|
|
||||||
}
|
|
||||||
|
|
||||||
static long positionCount(Map<Long, CaveSpanSet> spans) {
|
|
||||||
long count = 0L;
|
|
||||||
for (CaveSpanSet spanSet : spans.values()) {
|
|
||||||
count += spanSet.positionCount();
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
record CaveCourseGraph(
|
|
||||||
List<DrainageNode> nodes,
|
|
||||||
List<DrainageEdge> edges,
|
|
||||||
List<RiverOutlet> outlets
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
final class CaveSpanSet {
|
|
||||||
private int[] pending;
|
|
||||||
private int size;
|
|
||||||
private List<CaveYSpan> normalized;
|
|
||||||
|
|
||||||
CaveSpanSet() {
|
|
||||||
this.pending = new int[4];
|
|
||||||
this.normalized = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void add(int minimumY, int maximumY) {
|
|
||||||
if (minimumY > maximumY) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
int requiredLength = Math.multiplyExact(size + 1, 2);
|
|
||||||
if (requiredLength > pending.length) {
|
|
||||||
pending = Arrays.copyOf(pending, Math.multiplyExact(pending.length, 2));
|
|
||||||
}
|
|
||||||
pending[size * 2] = minimumY;
|
|
||||||
pending[size * 2 + 1] = maximumY;
|
|
||||||
size++;
|
|
||||||
normalized = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<CaveYSpan> spans() {
|
|
||||||
if (normalized != null) {
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
sortPending();
|
|
||||||
ArrayList<CaveYSpan> merged = new ArrayList<>(size);
|
|
||||||
for (int index = 0; index < size; index++) {
|
|
||||||
CaveYSpan span = new CaveYSpan(pending[index * 2], pending[index * 2 + 1]);
|
|
||||||
if (merged.isEmpty()) {
|
|
||||||
merged.add(span);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
CaveYSpan previous = merged.getLast();
|
|
||||||
if ((long) span.minimumY() > (long) previous.maximumY() + 1L) {
|
|
||||||
merged.add(span);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
merged.set(
|
|
||||||
merged.size() - 1,
|
|
||||||
new CaveYSpan(previous.minimumY(), Math.max(previous.maximumY(), span.maximumY()))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
normalized = List.copyOf(merged);
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sortPending() {
|
|
||||||
for (int index = 1; index < size; index++) {
|
|
||||||
int minimumY = pending[index * 2];
|
|
||||||
int maximumY = pending[index * 2 + 1];
|
|
||||||
int insertionIndex = index;
|
|
||||||
while (insertionIndex > 0) {
|
|
||||||
int previousMinimumY = pending[(insertionIndex - 1) * 2];
|
|
||||||
int previousMaximumY = pending[(insertionIndex - 1) * 2 + 1];
|
|
||||||
if (previousMinimumY < minimumY
|
|
||||||
|| previousMinimumY == minimumY && previousMaximumY <= maximumY) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
pending[insertionIndex * 2] = previousMinimumY;
|
|
||||||
pending[insertionIndex * 2 + 1] = previousMaximumY;
|
|
||||||
insertionIndex--;
|
|
||||||
}
|
|
||||||
pending[insertionIndex * 2] = minimumY;
|
|
||||||
pending[insertionIndex * 2 + 1] = maximumY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean contains(int y) {
|
|
||||||
List<CaveYSpan> spans = spans();
|
|
||||||
int minimumIndex = 0;
|
|
||||||
int maximumIndex = spans.size() - 1;
|
|
||||||
while (minimumIndex <= maximumIndex) {
|
|
||||||
int index = (minimumIndex + maximumIndex) >>> 1;
|
|
||||||
CaveYSpan span = spans.get(index);
|
|
||||||
if (y < span.minimumY()) {
|
|
||||||
maximumIndex = index - 1;
|
|
||||||
} else if (y > span.maximumY()) {
|
|
||||||
minimumIndex = index + 1;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
long positionCount() {
|
|
||||||
long count = 0L;
|
|
||||||
for (CaveYSpan span : spans()) {
|
|
||||||
count += (long) span.maximumY() - span.minimumY() + 1L;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCandidate;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
record CaveSurfaceComposition(
|
|
||||||
HydrologyCaveCandidate candidate,
|
|
||||||
boolean accepted
|
|
||||||
) {
|
|
||||||
static CaveSurfaceComposition accepted(HydrologyCaveCandidate candidate) {
|
|
||||||
return new CaveSurfaceComposition(Objects.requireNonNull(candidate), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
static CaveSurfaceComposition rejected() {
|
|
||||||
return new CaveSurfaceComposition(null, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import art.arcane.iris.generation.hydrology.cave.CavePosition;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
record CaveSurfaceOpening(
|
|
||||||
long segmentId,
|
|
||||||
HydrologyPoint point,
|
|
||||||
double interiorX,
|
|
||||||
double interiorZ,
|
|
||||||
long radiusSquared,
|
|
||||||
int minimumY,
|
|
||||||
boolean includeNeighborhood,
|
|
||||||
boolean surfaceFall
|
|
||||||
) {
|
|
||||||
static CaveSurfaceOpening create(
|
|
||||||
HydraulicSegment segment,
|
|
||||||
boolean start,
|
|
||||||
long radiusSquared,
|
|
||||||
int minimumY,
|
|
||||||
boolean includeNeighborhood
|
|
||||||
) {
|
|
||||||
List<HydrologyPoint> centerline = segment.centerline();
|
|
||||||
int boundaryIndex = start ? 0 : centerline.size() - 1;
|
|
||||||
int interiorIndex = start
|
|
||||||
? Math.min(1, centerline.size() - 1)
|
|
||||||
: Math.max(0, centerline.size() - 2);
|
|
||||||
HydrologyPoint point = centerline.get(boundaryIndex);
|
|
||||||
HydrologyPoint interior = centerline.get(interiorIndex);
|
|
||||||
double deltaX = interior.x() - point.x();
|
|
||||||
double deltaZ = interior.z() - point.z();
|
|
||||||
double lengthSquared = deltaX * deltaX + deltaZ * deltaZ;
|
|
||||||
double scale = 0D;
|
|
||||||
if (includeNeighborhood) {
|
|
||||||
scale = lengthSquared <= radiusSquared
|
|
||||||
? 1D
|
|
||||||
: StrictMath.sqrt(radiusSquared / lengthSquared);
|
|
||||||
}
|
|
||||||
return new CaveSurfaceOpening(
|
|
||||||
segment.id(),
|
|
||||||
point,
|
|
||||||
point.x() + deltaX * scale,
|
|
||||||
point.z() + deltaZ * scale,
|
|
||||||
radiusSquared,
|
|
||||||
minimumY,
|
|
||||||
includeNeighborhood,
|
|
||||||
segment.type().isSurface() && segment.fallingFluid()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean matches(HydrologyColumnLayer layer, CavePosition position) {
|
|
||||||
return matchesColumn(layer, position.x(), position.z())
|
|
||||||
&& position.y() >= minimumY;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean matchesColumn(HydrologyColumnLayer layer, int x, int z) {
|
|
||||||
if (surfaceFall) {
|
|
||||||
return layer.fallingFluid() && layer.feature().segmentId() == segmentId;
|
|
||||||
}
|
|
||||||
return (includeNeighborhood || layer.feature().segmentId() == segmentId)
|
|
||||||
&& distanceSquared(x, z) <= radiusSquared;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double distanceSquared(int x, int z) {
|
|
||||||
double pathX = interiorX - point.x();
|
|
||||||
double pathZ = interiorZ - point.z();
|
|
||||||
double pathLengthSquared = pathX * pathX + pathZ * pathZ;
|
|
||||||
double positionX = x - point.x();
|
|
||||||
double positionZ = z - point.z();
|
|
||||||
double progress = pathLengthSquared == 0D
|
|
||||||
? 0D
|
|
||||||
: (positionX * pathX + positionZ * pathZ) / pathLengthSquared;
|
|
||||||
double clampedProgress = Math.max(0D, Math.min(1D, progress));
|
|
||||||
double deltaX = positionX - pathX * clampedProgress;
|
|
||||||
double deltaZ = positionZ - pathZ * clampedProgress;
|
|
||||||
return deltaX * deltaX + deltaZ * deltaZ;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
record CaveYSpan(int minimumY, int maximumY) {
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
record CompiledGraph(
|
|
||||||
List<DrainageNode> nodes,
|
|
||||||
List<DrainageEdge> edges,
|
|
||||||
List<RiverOutlet> outlets,
|
|
||||||
Map<Integer, DrainageEdge> edgeByUpstream
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
record CourseEdgeKey(RiverCourseType type, long edgeId) {
|
|
||||||
CourseEdgeKey {
|
|
||||||
Objects.requireNonNull(type, "type");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
record CrossTileBlockers(
|
|
||||||
List<HydrologyCrossTileCaveAdmission.RankedClaim> caveClaims,
|
|
||||||
List<HydrologyCrossTileSurfaceAdmission.RankedClaim> surfaceClaims
|
|
||||||
) {
|
|
||||||
CrossTileBlockers {
|
|
||||||
caveClaims = List.copyOf(Objects.requireNonNull(caveClaims, "caveClaims"));
|
|
||||||
surfaceClaims = List.copyOf(Objects.requireNonNull(surfaceClaims, "surfaceClaims"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
interface CrossTileDraftAdmission {
|
|
||||||
void prepare();
|
|
||||||
|
|
||||||
CrossTilePublicationAdmission admit(HydrologyCaveCourseFilter.Result result);
|
|
||||||
}
|
|
||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
record CrossTilePublicationAdmission(
|
|
||||||
HydrologyCaveCourseFilter.Result result,
|
|
||||||
List<HydrologyDiagnosticCandidate> diagnostics,
|
|
||||||
boolean rejectedCourses
|
|
||||||
) {
|
|
||||||
CrossTilePublicationAdmission {
|
|
||||||
Objects.requireNonNull(result, "result");
|
|
||||||
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
record CrossTileRejectedCourse(RiverCourse course, long winnerSourceId) {
|
|
||||||
CrossTileRejectedCourse {
|
|
||||||
Objects.requireNonNull(course, "course");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
record CrossTileRejectionKey(long courseId, long winnerCourseId) {
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
record CrossTileResolution(
|
|
||||||
HydrologyOwnerDraft draft,
|
|
||||||
List<CrossTileRejectedCourse> observedRejections,
|
|
||||||
int iterations,
|
|
||||||
int ownerCount,
|
|
||||||
long resolutionNanos
|
|
||||||
) {
|
|
||||||
CrossTileResolution {
|
|
||||||
Objects.requireNonNull(draft, "draft");
|
|
||||||
observedRejections = List.copyOf(Objects.requireNonNull(observedRejections, "observedRejections"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-73
@@ -1,73 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
final class CrossTileResolutionContext {
|
|
||||||
final Map<HydrologyTileKey, CrossTileResolvedOwner> resolved;
|
|
||||||
private final Set<HydrologyTileKey> resolving;
|
|
||||||
final HydrologyTileKey root;
|
|
||||||
private final long maximumTileOffset;
|
|
||||||
private final int maximumOwners;
|
|
||||||
int iterations;
|
|
||||||
|
|
||||||
CrossTileResolutionContext(
|
|
||||||
HydrologyTileKey root,
|
|
||||||
long maximumTileOffset,
|
|
||||||
int maximumOwners
|
|
||||||
) {
|
|
||||||
this.resolved = new HashMap<>();
|
|
||||||
this.resolving = new HashSet<>();
|
|
||||||
this.root = Objects.requireNonNull(root, "root");
|
|
||||||
this.maximumTileOffset = maximumTileOffset;
|
|
||||||
this.maximumOwners = maximumOwners;
|
|
||||||
}
|
|
||||||
|
|
||||||
CrossTileResolvedOwner resolved(HydrologyTileKey key) {
|
|
||||||
return resolved.get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
void remember(HydrologyTileKey key, CrossTileResolvedOwner owner) {
|
|
||||||
long offsetX = Math.abs((long) key.tileX() - root.tileX());
|
|
||||||
long offsetZ = Math.abs((long) key.tileZ() - root.tileZ());
|
|
||||||
if (offsetX > maximumTileOffset || offsetZ > maximumTileOffset) {
|
|
||||||
throw new IllegalStateException("Cross-tile owner dependency exceeded its color-ranked geometry bound.");
|
|
||||||
}
|
|
||||||
CrossTileResolvedOwner existing = resolved.putIfAbsent(key, owner);
|
|
||||||
if (existing != null
|
|
||||||
&& !existing.withoutFootprintCompiler().equals(owner.withoutFootprintCompiler())) {
|
|
||||||
throw new IllegalStateException("Cross-tile owner resolution produced inconsistent cached results for "
|
|
||||||
+ key.tileX() + "," + key.tileZ() + " while planning " + root.tileX() + "," + root.tileZ() + ".");
|
|
||||||
}
|
|
||||||
if (resolved.size() > maximumOwners) {
|
|
||||||
throw new IllegalStateException("Cross-tile owner dependencies exceeded their color-ranked count bound.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean begin(HydrologyTileKey key) {
|
|
||||||
return resolving.add(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
void end(HydrologyTileKey key) {
|
|
||||||
resolving.remove(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
void recordIteration() {
|
|
||||||
iterations++;
|
|
||||||
}
|
|
||||||
|
|
||||||
int iterations() {
|
|
||||||
return iterations;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ownerCount() {
|
|
||||||
return resolved.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
HydrologyTileKey root() {
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
record CrossTileResolvedOwner(
|
|
||||||
HydrologyOwnerDraft draft,
|
|
||||||
List<CrossTileRejectedCourse> observedRejections
|
|
||||||
) {
|
|
||||||
CrossTileResolvedOwner {
|
|
||||||
Objects.requireNonNull(draft, "draft");
|
|
||||||
observedRejections = List.copyOf(Objects.requireNonNull(observedRejections, "observedRejections"));
|
|
||||||
}
|
|
||||||
|
|
||||||
CrossTileResolvedOwner withoutFootprintCompiler() {
|
|
||||||
HydrologyOwnerDraft uncachedDraft = draft.withoutFootprintCompiler();
|
|
||||||
return uncachedDraft == draft
|
|
||||||
? this
|
|
||||||
: new CrossTileResolvedOwner(uncachedDraft, observedRejections);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
enum DraftPhase {
|
|
||||||
CONTEXT,
|
|
||||||
SELECT,
|
|
||||||
SETTLE,
|
|
||||||
PUBLISH
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wall-clock breakdown of one owner draft for the debug log: sampling and routing context, source
|
|
||||||
* selection, settling, publication passes, and the time spent waiting on lower-rank neighbour drafts.
|
|
||||||
*/
|
|
||||||
final class DraftProfile {
|
|
||||||
private final long[] phaseNanos = new long[DraftPhase.values().length];
|
|
||||||
private long dependencyNanos;
|
|
||||||
private int dependencies;
|
|
||||||
private int admissions;
|
|
||||||
int earlyOwners;
|
|
||||||
int routeCalls;
|
|
||||||
int routeSolves;
|
|
||||||
long routeSolveNanos;
|
|
||||||
int rasterCalls;
|
|
||||||
long rasterNanos;
|
|
||||||
int filterCalls;
|
|
||||||
long filterNanos;
|
|
||||||
|
|
||||||
long record(DraftPhase phase, long started) {
|
|
||||||
long now = System.nanoTime();
|
|
||||||
phaseNanos[phase.ordinal()] += now - started;
|
|
||||||
return now;
|
|
||||||
}
|
|
||||||
|
|
||||||
void recordDependencies(int count, long started) {
|
|
||||||
dependencyNanos += System.nanoTime() - started;
|
|
||||||
dependencies += count;
|
|
||||||
admissions++;
|
|
||||||
}
|
|
||||||
|
|
||||||
void log(HydrologyTileKey key, int rank, long totalNanos) {
|
|
||||||
IrisLogging.debug(
|
|
||||||
"Hydrology owner %d,%d rank=%d drafted in %dms: context=%dms select=%dms settle=%dms publish=%dms deps=%d wait=%dms admissions=%d earlyOwners=%d routes=%d reuses=%d routeSolve=%dms rasters=%d raster=%dms filters=%d filter=%dms on %s",
|
|
||||||
key.tileX(),
|
|
||||||
key.tileZ(),
|
|
||||||
rank,
|
|
||||||
totalNanos / 1_000_000L,
|
|
||||||
phaseNanos[DraftPhase.CONTEXT.ordinal()] / 1_000_000L,
|
|
||||||
phaseNanos[DraftPhase.SELECT.ordinal()] / 1_000_000L,
|
|
||||||
phaseNanos[DraftPhase.SETTLE.ordinal()] / 1_000_000L,
|
|
||||||
phaseNanos[DraftPhase.PUBLISH.ordinal()] / 1_000_000L,
|
|
||||||
dependencies,
|
|
||||||
dependencyNanos / 1_000_000L,
|
|
||||||
admissions,
|
|
||||||
earlyOwners,
|
|
||||||
routeCalls,
|
|
||||||
routeCalls - routeSolves,
|
|
||||||
routeSolveNanos / 1_000_000L,
|
|
||||||
rasterCalls,
|
|
||||||
rasterNanos / 1_000_000L,
|
|
||||||
filterCalls,
|
|
||||||
filterNanos / 1_000_000L,
|
|
||||||
Thread.currentThread().getName()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public record DrainageEdge(
|
|
||||||
long id,
|
|
||||||
long upstreamNodeId,
|
|
||||||
long downstreamNodeId,
|
|
||||||
long outletId,
|
|
||||||
double cost,
|
|
||||||
int contributingSurfaceSources,
|
|
||||||
int contributingUndergroundSources,
|
|
||||||
List<HydrologyPoint> centerline
|
|
||||||
) {
|
|
||||||
public DrainageEdge {
|
|
||||||
if (upstreamNodeId == downstreamNodeId) {
|
|
||||||
throw new IllegalArgumentException("A drainage edge cannot point to the same node.");
|
|
||||||
}
|
|
||||||
if (!Double.isFinite(cost) || cost <= 0D) {
|
|
||||||
throw new IllegalArgumentException("Drainage edge cost must be finite and positive.");
|
|
||||||
}
|
|
||||||
if (contributingSurfaceSources < 0 || contributingUndergroundSources < 0) {
|
|
||||||
throw new IllegalArgumentException("Contributing source counts cannot be negative.");
|
|
||||||
}
|
|
||||||
Objects.requireNonNull(centerline, "centerline");
|
|
||||||
centerline = List.copyOf(centerline);
|
|
||||||
if (centerline.size() < 2) {
|
|
||||||
throw new IllegalArgumentException("A drainage edge requires at least two centerline points.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int totalContributingSources() {
|
|
||||||
return contributingSurfaceSources + contributingUndergroundSources;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public record DrainageNode(
|
|
||||||
long id,
|
|
||||||
int x,
|
|
||||||
int z,
|
|
||||||
HydrologyTerrainSample terrain,
|
|
||||||
double potential,
|
|
||||||
long outletId
|
|
||||||
) {
|
|
||||||
public DrainageNode {
|
|
||||||
Objects.requireNonNull(terrain, "terrain");
|
|
||||||
if (!Double.isFinite(potential) || potential < 0D) {
|
|
||||||
throw new IllegalArgumentException("Drainage potential must be finite and non-negative.");
|
|
||||||
}
|
|
||||||
if (terrain.ocean()) {
|
|
||||||
throw new IllegalArgumentException("Drainage nodes must remain landward of the first ocean column.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public HydrologyPoint naturalPoint() {
|
|
||||||
return new HydrologyPoint(x, terrain.naturalHeight(), z);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
record FootprintCenterlineProjection(
|
|
||||||
int pointIndex,
|
|
||||||
double x,
|
|
||||||
double z,
|
|
||||||
double distance,
|
|
||||||
int flowX,
|
|
||||||
int flowZ
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
record FootprintCourseRasterKey(
|
|
||||||
long courseId,
|
|
||||||
RiverCourseType courseType,
|
|
||||||
String profileKey,
|
|
||||||
List<HydraulicSegment> segments
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
record FootprintFeatureKey(
|
|
||||||
long courseId,
|
|
||||||
long segmentId,
|
|
||||||
HydrologyFeatureType type,
|
|
||||||
int role,
|
|
||||||
int flowX,
|
|
||||||
int flowZ
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
record FootprintLayerShape(
|
|
||||||
int channelRadius,
|
|
||||||
double shoreWidth,
|
|
||||||
double gradingWidth,
|
|
||||||
int bed,
|
|
||||||
int fluidHead,
|
|
||||||
int ceiling,
|
|
||||||
boolean ellipsoid,
|
|
||||||
boolean archedChannel,
|
|
||||||
boolean roundedSurfaceBed,
|
|
||||||
boolean organicBoundary,
|
|
||||||
boolean fallingThroat,
|
|
||||||
boolean inlet
|
|
||||||
) {
|
|
||||||
double totalRadius() {
|
|
||||||
return channelRadius + shoreWidth + gradingWidth;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
final class FootprintMutableColumn {
|
|
||||||
private final int x;
|
|
||||||
private final int z;
|
|
||||||
private final int naturalHeight;
|
|
||||||
private final int seaLevel;
|
|
||||||
private final boolean ocean;
|
|
||||||
private final String parentBiomeKey;
|
|
||||||
private HydrologyColumnLayer singleLayer;
|
|
||||||
private LinkedHashMap<Long, HydrologyColumnLayer> layers;
|
|
||||||
|
|
||||||
FootprintMutableColumn(int x, int z, HydrologyTerrainSample terrain, int seaLevel) {
|
|
||||||
this.x = x;
|
|
||||||
this.z = z;
|
|
||||||
this.naturalHeight = terrain.naturalHeight();
|
|
||||||
this.seaLevel = seaLevel;
|
|
||||||
this.ocean = terrain.ocean();
|
|
||||||
this.parentBiomeKey = terrain.parentBiomeKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
FootprintMutableColumn(HydrologyColumnSample sample) {
|
|
||||||
this.x = sample.x();
|
|
||||||
this.z = sample.z();
|
|
||||||
this.naturalHeight = sample.naturalHeight();
|
|
||||||
this.seaLevel = sample.seaLevel();
|
|
||||||
this.ocean = sample.ocean();
|
|
||||||
this.parentBiomeKey = sample.parentBiomeKey();
|
|
||||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
|
||||||
add(layer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void add(HydrologyColumnLayer layer) {
|
|
||||||
if (layers == null) {
|
|
||||||
if (singleLayer == null) {
|
|
||||||
singleLayer = layer;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (singleLayer.feature().id() == layer.feature().id()) {
|
|
||||||
singleLayer = merge(singleLayer, layer);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
layers = new LinkedHashMap<>();
|
|
||||||
layers.put(singleLayer.feature().id(), singleLayer);
|
|
||||||
singleLayer = null;
|
|
||||||
}
|
|
||||||
HydrologyColumnLayer existing = layers.get(layer.feature().id());
|
|
||||||
layers.put(layer.feature().id(), existing == null ? layer : merge(existing, layer));
|
|
||||||
}
|
|
||||||
|
|
||||||
HydrologyColumnSample build() {
|
|
||||||
List<HydrologyColumnLayer> builtLayers;
|
|
||||||
if (layers != null) {
|
|
||||||
builtLayers = resolveSurfaceHeadConflicts(new ArrayList<>(layers.values()));
|
|
||||||
} else if (singleLayer != null) {
|
|
||||||
builtLayers = List.of(singleLayer);
|
|
||||||
} else {
|
|
||||||
builtLayers = List.of();
|
|
||||||
}
|
|
||||||
return new HydrologyColumnSample(
|
|
||||||
x,
|
|
||||||
z,
|
|
||||||
naturalHeight,
|
|
||||||
seaLevel,
|
|
||||||
ocean,
|
|
||||||
parentBiomeKey,
|
|
||||||
builtLayers
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<HydrologyColumnLayer> resolveSurfaceHeadConflicts(List<HydrologyColumnLayer> candidates) {
|
|
||||||
HashMap<Long, HydrologyColumnLayer> selectedByCourse = new HashMap<>();
|
|
||||||
for (HydrologyColumnLayer candidate : candidates) {
|
|
||||||
if (!conflictingSurfaceCandidate(candidate)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
long courseId = candidate.feature().courseId();
|
|
||||||
HydrologyColumnLayer selected = selectedByCourse.get(courseId);
|
|
||||||
if (selected == null || prefersSurfaceLayer(candidate, selected)) {
|
|
||||||
selectedByCourse.put(courseId, candidate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (selectedByCourse.isEmpty()) {
|
|
||||||
return candidates;
|
|
||||||
}
|
|
||||||
ArrayList<HydrologyColumnLayer> resolved = new ArrayList<>(candidates.size());
|
|
||||||
for (HydrologyColumnLayer candidate : candidates) {
|
|
||||||
HydrologyColumnLayer selected = selectedByCourse.get(candidate.feature().courseId());
|
|
||||||
if (!conflictingSurfaceCandidate(candidate)
|
|
||||||
|| selected == null
|
|
||||||
|| candidate.fluidHeadY() == selected.fluidHeadY()) {
|
|
||||||
resolved.add(candidate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return List.copyOf(resolved);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean conflictingSurfaceCandidate(HydrologyColumnLayer layer) {
|
|
||||||
return layer.feature().type().isSurface()
|
|
||||||
&& layer.channel()
|
|
||||||
&& layer.connectedFluid()
|
|
||||||
&& layer.fluidOwned()
|
|
||||||
&& !layer.fallingFluid();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean prefersSurfaceLayer(HydrologyColumnLayer candidate, HydrologyColumnLayer selected) {
|
|
||||||
if (candidate.receivingPool() != selected.receivingPool()) {
|
|
||||||
return candidate.receivingPool();
|
|
||||||
}
|
|
||||||
if (candidate.fluidHeadY() != selected.fluidHeadY()) {
|
|
||||||
return candidate.fluidHeadY() > selected.fluidHeadY();
|
|
||||||
}
|
|
||||||
return candidate.feature().id() < selected.feature().id();
|
|
||||||
}
|
|
||||||
|
|
||||||
void merge(HydrologyColumnSample sample) {
|
|
||||||
if (sample.x() != x
|
|
||||||
|| sample.z() != z
|
|
||||||
|| sample.naturalHeight() != naturalHeight
|
|
||||||
|| sample.seaLevel() != seaLevel
|
|
||||||
|| sample.ocean() != ocean
|
|
||||||
|| !sample.parentBiomeKey().equals(parentBiomeKey)) {
|
|
||||||
throw new IllegalStateException("Hydrology course footprints disagree on terrain metadata at "
|
|
||||||
+ x + "," + z + ".");
|
|
||||||
}
|
|
||||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
|
||||||
add(layer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private HydrologyColumnLayer merge(HydrologyColumnLayer first, HydrologyColumnLayer second) {
|
|
||||||
if (first.equals(second)) {
|
|
||||||
return first;
|
|
||||||
}
|
|
||||||
HydrologyFeatureRef feature = first.feature().y() <= second.feature().y()
|
|
||||||
? first.feature()
|
|
||||||
: second.feature();
|
|
||||||
boolean drySurfaceBlend = first.feature().type().isSurface()
|
|
||||||
&& second.feature().type().isSurface()
|
|
||||||
&& !first.channel()
|
|
||||||
&& !second.channel();
|
|
||||||
int bedY = drySurfaceBlend
|
|
||||||
? Math.max(first.bedY(), second.bedY())
|
|
||||||
: Math.min(first.bedY(), second.bedY());
|
|
||||||
return new HydrologyColumnLayer(
|
|
||||||
feature,
|
|
||||||
bedY,
|
|
||||||
Math.max(first.fluidHeadY(), second.fluidHeadY()),
|
|
||||||
Math.max(first.ceilingY(), second.ceilingY()),
|
|
||||||
first.channel() || second.channel(),
|
|
||||||
first.shore() || second.shore(),
|
|
||||||
first.grading() || second.grading(),
|
|
||||||
first.connectedFluid() || second.connectedFluid(),
|
|
||||||
first.fallingFluid() || second.fallingFluid(),
|
|
||||||
first.receivingPool() || second.receivingPool(),
|
|
||||||
(first.terrainOwned() || second.terrainOwned()) && (!drySurfaceBlend || bedY != naturalHeight),
|
|
||||||
first.fluidOwned() || second.fluidOwned(),
|
|
||||||
first.oceanApron() || second.oceanApron(),
|
|
||||||
first.profileKey(),
|
|
||||||
first.surfaceBiomeKey(),
|
|
||||||
first.mouthBiomeKey(),
|
|
||||||
first.shoreBiomeKey(),
|
|
||||||
first.bankBiomeKey(),
|
|
||||||
first.floodedCaveBiomeKey()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
record FootprintRasterStencil(
|
|
||||||
int[] deltaXs,
|
|
||||||
int[] deltaZs,
|
|
||||||
double[] distances
|
|
||||||
) {
|
|
||||||
int size() {
|
|
||||||
return deltaXs.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-37
@@ -1,37 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
final class FootprintRasterStencilBuilder {
|
|
||||||
private int[] deltaXs;
|
|
||||||
private int[] deltaZs;
|
|
||||||
private double[] distances;
|
|
||||||
private int size;
|
|
||||||
|
|
||||||
FootprintRasterStencilBuilder() {
|
|
||||||
this.deltaXs = new int[64];
|
|
||||||
this.deltaZs = new int[64];
|
|
||||||
this.distances = new double[64];
|
|
||||||
}
|
|
||||||
|
|
||||||
void add(int deltaX, int deltaZ, double distance) {
|
|
||||||
if (size == deltaXs.length) {
|
|
||||||
int expandedSize = Math.multiplyExact(size, 2);
|
|
||||||
deltaXs = Arrays.copyOf(deltaXs, expandedSize);
|
|
||||||
deltaZs = Arrays.copyOf(deltaZs, expandedSize);
|
|
||||||
distances = Arrays.copyOf(distances, expandedSize);
|
|
||||||
}
|
|
||||||
deltaXs[size] = deltaX;
|
|
||||||
deltaZs[size] = deltaZ;
|
|
||||||
distances[size] = distance;
|
|
||||||
size++;
|
|
||||||
}
|
|
||||||
|
|
||||||
FootprintRasterStencil build() {
|
|
||||||
return new FootprintRasterStencil(
|
|
||||||
Arrays.copyOf(deltaXs, size),
|
|
||||||
Arrays.copyOf(deltaZs, size),
|
|
||||||
Arrays.copyOf(distances, size)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
record FootprintRasterStencilKey(
|
|
||||||
int channelRadius,
|
|
||||||
double shoreWidth,
|
|
||||||
double gradingWidth
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import art.arcane.iris.generation.hydrology.surface.SurfaceFootprint;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
record FootprintValidationCourseRaster(
|
|
||||||
long courseId,
|
|
||||||
List<HydrologyColumnSample> columns,
|
|
||||||
SurfaceFootprint surface
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public record HydraulicChannelProfile(double[] widths, double[] depths) {
|
|
||||||
public HydraulicChannelProfile {
|
|
||||||
Objects.requireNonNull(widths, "widths");
|
|
||||||
Objects.requireNonNull(depths, "depths");
|
|
||||||
if (widths.length == 0 || widths.length != depths.length) {
|
|
||||||
throw new IllegalArgumentException("Channel dimensions require equally sized, nonempty station arrays.");
|
|
||||||
}
|
|
||||||
widths = widths.clone();
|
|
||||||
depths = depths.clone();
|
|
||||||
for (int station = 0; station < widths.length; station++) {
|
|
||||||
if (!Double.isFinite(widths[station]) || widths[station] <= 0D
|
|
||||||
|| !Double.isFinite(depths[station]) || depths[station] <= 0D) {
|
|
||||||
throw new IllegalArgumentException("Channel dimensions must be finite and positive.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static HydraulicChannelProfile uniform(double width, double depth) {
|
|
||||||
return new HydraulicChannelProfile(new double[]{width}, new double[]{depth});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static HydraulicChannelProfile range(double[] widths, double[] depths, int from, int to) {
|
|
||||||
Objects.checkFromToIndex(from, to, widths.length);
|
|
||||||
Objects.checkFromToIndex(from, to, depths.length);
|
|
||||||
return new HydraulicChannelProfile(Arrays.copyOfRange(widths, from, to), Arrays.copyOfRange(depths, from, to));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public double[] widths() {
|
|
||||||
return widths.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public double[] depths() {
|
|
||||||
return depths.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int size() {
|
|
||||||
return widths.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double widthAt(int station) {
|
|
||||||
return widths[widths.length == 1 ? 0 : station];
|
|
||||||
}
|
|
||||||
|
|
||||||
public double depthAt(int station) {
|
|
||||||
return depths[depths.length == 1 ? 0 : station];
|
|
||||||
}
|
|
||||||
|
|
||||||
public double maximumWidth() {
|
|
||||||
double maximum = 0D;
|
|
||||||
for (double width : widths) {
|
|
||||||
maximum = Math.max(maximum, width);
|
|
||||||
}
|
|
||||||
return maximum;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double maximumDepth() {
|
|
||||||
double maximum = 0D;
|
|
||||||
for (double depth : depths) {
|
|
||||||
maximum = Math.max(maximum, depth);
|
|
||||||
}
|
|
||||||
return maximum;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object other) {
|
|
||||||
return other instanceof HydraulicChannelProfile profile
|
|
||||||
&& Arrays.equals(widths, profile.widths)
|
|
||||||
&& Arrays.equals(depths, profile.depths);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return 31 * Arrays.hashCode(widths) + Arrays.hashCode(depths);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "HydraulicChannelProfile[widths=" + Arrays.toString(widths) + ", depths=" + Arrays.toString(depths) + "]";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public record HydraulicSegment(
|
|
||||||
long id,
|
|
||||||
long courseId,
|
|
||||||
HydrologyFeatureType type,
|
|
||||||
int upstreamHeadY,
|
|
||||||
int downstreamHeadY,
|
|
||||||
int width,
|
|
||||||
int depth,
|
|
||||||
boolean fallingFluid,
|
|
||||||
boolean receivingPool,
|
|
||||||
List<HydrologyPoint> centerline,
|
|
||||||
HydraulicChannelProfile channelProfile
|
|
||||||
) {
|
|
||||||
public HydraulicSegment {
|
|
||||||
Objects.requireNonNull(type, "type");
|
|
||||||
Objects.requireNonNull(centerline, "centerline");
|
|
||||||
Objects.requireNonNull(channelProfile, "channelProfile");
|
|
||||||
centerline = List.copyOf(centerline);
|
|
||||||
if (centerline.isEmpty()) {
|
|
||||||
throw new IllegalArgumentException("A hydraulic segment requires a centerline.");
|
|
||||||
}
|
|
||||||
if (upstreamHeadY < downstreamHeadY) {
|
|
||||||
throw new IllegalArgumentException("Hydraulic heads cannot rise downstream.");
|
|
||||||
}
|
|
||||||
if (width <= 0 || depth <= 0) {
|
|
||||||
throw new IllegalArgumentException("Hydraulic segment width and depth must be positive.");
|
|
||||||
}
|
|
||||||
if (channelProfile.size() != 1 && channelProfile.size() != centerline.size()) {
|
|
||||||
throw new IllegalArgumentException("Channel dimensions must cover every centerline station.");
|
|
||||||
}
|
|
||||||
if (channelProfile.maximumWidth() > width || channelProfile.maximumDepth() > depth) {
|
|
||||||
throw new IllegalArgumentException("Hydraulic segment bounds must contain its channel dimensions.");
|
|
||||||
}
|
|
||||||
int drop = upstreamHeadY - downstreamHeadY;
|
|
||||||
if (fallingFluid && drop == 0) {
|
|
||||||
throw new IllegalArgumentException("Falling fluid requires a positive head transition.");
|
|
||||||
}
|
|
||||||
if (drop > 0 && !type.isDrop()) {
|
|
||||||
throw new IllegalArgumentException("A positive head transition requires a drop segment type.");
|
|
||||||
}
|
|
||||||
if (drop == 0 && type.isDrop()) {
|
|
||||||
throw new IllegalArgumentException("A drop segment type requires a positive head transition.");
|
|
||||||
}
|
|
||||||
if (drop > 0 && !fallingFluid) {
|
|
||||||
if (centerline.getFirst().y() != upstreamHeadY || centerline.getLast().y() != downstreamHeadY) {
|
|
||||||
throw new IllegalArgumentException("A graded head transition must span its declared heads.");
|
|
||||||
}
|
|
||||||
int previousHead = upstreamHeadY;
|
|
||||||
for (HydrologyPoint point : centerline) {
|
|
||||||
if (point.y() > previousHead || point.y() < downstreamHeadY) {
|
|
||||||
throw new IllegalArgumentException("A graded head transition must descend monotonically.");
|
|
||||||
}
|
|
||||||
previousHead = point.y();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int drop() {
|
|
||||||
return upstreamHeadY - downstreamHeadY;
|
|
||||||
}
|
|
||||||
|
|
||||||
public HydrologyPoint start() {
|
|
||||||
return centerline.getFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
public HydrologyPoint end() {
|
|
||||||
return centerline.getLast();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,325 +0,0 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
|
||||||
|
|
||||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
|
||||||
import art.arcane.volmlib.util.cache.CacheKey;
|
|
||||||
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
|
||||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
|
||||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.concurrent.CountDownLatch;
|
|
||||||
import java.util.concurrent.ForkJoinPool;
|
|
||||||
import java.util.concurrent.RejectedExecutionException;
|
|
||||||
|
|
||||||
final class HydrologyBasisCache {
|
|
||||||
private static final int MAXIMUM_ENTRIES = 32_768;
|
|
||||||
private static final int MAXIMUM_STRIPES = 64;
|
|
||||||
private final Stripe[] full;
|
|
||||||
private final Stripe[] withoutSlope;
|
|
||||||
|
|
||||||
HydrologyBasisCache() {
|
|
||||||
this(MAXIMUM_ENTRIES);
|
|
||||||
}
|
|
||||||
|
|
||||||
HydrologyBasisCache(int maximumEntries) {
|
|
||||||
if (maximumEntries < 1 || Integer.bitCount(maximumEntries) != 1) {
|
|
||||||
throw new IllegalArgumentException("Terrain basis cache size must be a positive power of two.");
|
|
||||||
}
|
|
||||||
int stripeCount = Math.min(MAXIMUM_STRIPES, maximumEntries);
|
|
||||||
full = new Stripe[stripeCount];
|
|
||||||
withoutSlope = new Stripe[stripeCount];
|
|
||||||
for (int index = 0; index < stripeCount; index++) {
|
|
||||||
full[index] = new Stripe(maximumEntries / stripeCount);
|
|
||||||
withoutSlope[index] = new Stripe(maximumEntries / stripeCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HydrologyTerrainSample sample(int x, int z, HydrologyNaturalTerrainSampler sampler) {
|
|
||||||
return sample(x, z, sampler, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
HydrologyTerrainSample sampleWithoutSlope(int x, int z, HydrologyNaturalTerrainSampler sampler) {
|
|
||||||
return sample(x, z, sampler, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void seed(Long2ObjectMap<HydrologyTerrainSample> samples) {
|
|
||||||
seed(samples, full);
|
|
||||||
}
|
|
||||||
|
|
||||||
HydrologyTerrainSample[] sampleWithoutSlopeBatch(long[] coordinates, int count,
|
|
||||||
HydrologyNaturalTerrainSampler sampler) {
|
|
||||||
Objects.checkFromIndexSize(0, count, coordinates.length);
|
|
||||||
if (count > HydrologyTerrainSampler.MAXIMUM_BATCH_SIZE) {
|
|
||||||
throw new IllegalArgumentException("Terrain basis batch exceeds its sample limit");
|
|
||||||
}
|
|
||||||
HydrologyTerrainSample[] samples = new HydrologyTerrainSample[count];
|
|
||||||
Pending[] pending = new Pending[count];
|
|
||||||
long[] ownedCoordinates = new long[count];
|
|
||||||
Pending[] owned = new Pending[count];
|
|
||||||
int ownedCount = count;
|
|
||||||
Completion completion = new Completion();
|
|
||||||
int[] next = new int[count];
|
|
||||||
int[] heads = groupByStripe(coordinates, count, next);
|
|
||||||
try {
|
|
||||||
for (int stripeIndex = 0; stripeIndex < withoutSlope.length; stripeIndex++) {
|
|
||||||
if (heads[stripeIndex] == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Stripe stripe = withoutSlope[stripeIndex];
|
|
||||||
synchronized (stripe) {
|
|
||||||
for (int position = heads[stripeIndex]; position != 0; position = next[position - 1]) {
|
|
||||||
int index = position - 1;
|
|
||||||
long packed = coordinates[index];
|
|
||||||
HydrologyTerrainSample cached = stripe.samples.getAndMoveToFirst(packed);
|
|
||||||
if (cached != null || stripe.samples.containsKey(packed)) {
|
|
||||||
samples[index] = cached;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Pending loading = stripe.pending.get(packed);
|
|
||||||
if (loading == null) {
|
|
||||||
loading = new Pending(completion);
|
|
||||||
stripe.pending.put(packed, loading);
|
|
||||||
ownedCoordinates[index] = packed;
|
|
||||||
owned[index] = loading;
|
|
||||||
}
|
|
||||||
pending[index] = loading;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
int claimed = 0;
|
|
||||||
for (int index = 0; index < count; index++) {
|
|
||||||
if (owned[index] != null) {
|
|
||||||
ownedCoordinates[claimed] = ownedCoordinates[index];
|
|
||||||
owned[claimed++] = owned[index];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ownedCount = claimed;
|
|
||||||
if (ownedCount > 0) {
|
|
||||||
loadBatch(ownedCoordinates, owned, ownedCount, sampler);
|
|
||||||
}
|
|
||||||
} catch (RuntimeException | Error failure) {
|
|
||||||
for (int index = 0; index < ownedCount; index++) {
|
|
||||||
if (owned[index] != null) {
|
|
||||||
fail(withoutSlope, ownedCoordinates[index], owned[index], failure);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
completion.failure = failure;
|
|
||||||
throw failure;
|
|
||||||
} finally {
|
|
||||||
completion.complete.countDown();
|
|
||||||
}
|
|
||||||
for (int index = 0; index < count; index++) {
|
|
||||||
if (pending[index] != null) {
|
|
||||||
samples[index] = pending[index].await();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return samples;
|
|
||||||
}
|
|
||||||
|
|
||||||
void seedWithoutSlope(Long2ObjectMap<HydrologyTerrainSample> samples) {
|
|
||||||
seed(samples, withoutSlope);
|
|
||||||
}
|
|
||||||
|
|
||||||
private HydrologyTerrainSample sample(int x, int z, HydrologyNaturalTerrainSampler sampler, boolean omitSlope) {
|
|
||||||
long packed = RiverFootprint.pack(x, z);
|
|
||||||
Stripe[] stripes = omitSlope ? withoutSlope : full;
|
|
||||||
Stripe stripe = stripe(stripes, packed);
|
|
||||||
Pending loading;
|
|
||||||
boolean owner = false;
|
|
||||||
synchronized (stripe) {
|
|
||||||
HydrologyTerrainSample cached = stripe.samples.getAndMoveToFirst(packed);
|
|
||||||
if (cached != null || stripe.samples.containsKey(packed)) {
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
loading = stripe.pending.get(packed);
|
|
||||||
if (loading == null) {
|
|
||||||
loading = new Pending(new Completion());
|
|
||||||
stripe.pending.put(packed, loading);
|
|
||||||
owner = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (owner) {
|
|
||||||
try {
|
|
||||||
HydrologyTerrainSample sampled = ProvisionalSampling.memoizable(() -> omitSlope
|
|
||||||
? sampler.sampleBasisWithoutSlope(x, z) : sampler.sampleBasis(x, z));
|
|
||||||
complete(stripes, packed, loading, sampled, false);
|
|
||||||
} catch (ProvisionalSampling.Unmemoizable provisional) {
|
|
||||||
complete(stripes, packed, loading, provisional.value(), true);
|
|
||||||
} catch (RuntimeException | Error failure) {
|
|
||||||
fail(stripes, packed, loading, failure);
|
|
||||||
throw failure;
|
|
||||||
} finally {
|
|
||||||
loading.completion.complete.countDown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return loading.await();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadBatch(long[] coordinates, Pending[] pending, int count, HydrologyNaturalTerrainSampler sampler) {
|
|
||||||
HydrologyTerrainSample[] samples;
|
|
||||||
boolean provisional = false;
|
|
||||||
try {
|
|
||||||
samples = ProvisionalSampling.memoizable(() -> sampler.sampleBasisWithoutSlopeBatch(coordinates, count));
|
|
||||||
} catch (ProvisionalSampling.Unmemoizable value) {
|
|
||||||
samples = value.value();
|
|
||||||
provisional = true;
|
|
||||||
}
|
|
||||||
if (samples.length < count) {
|
|
||||||
throw new IllegalStateException("Terrain basis batch returned too few samples");
|
|
||||||
}
|
|
||||||
int[] next = new int[count];
|
|
||||||
int[] heads = groupByStripe(coordinates, count, next);
|
|
||||||
for (int stripeIndex = 0; stripeIndex < withoutSlope.length; stripeIndex++) {
|
|
||||||
if (heads[stripeIndex] == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Stripe stripe = withoutSlope[stripeIndex];
|
|
||||||
synchronized (stripe) {
|
|
||||||
for (int position = heads[stripeIndex]; position != 0; position = next[position - 1]) {
|
|
||||||
int index = position - 1;
|
|
||||||
complete(stripe, coordinates[index], pending[index], samples[index], provisional);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int[] groupByStripe(long[] coordinates, int count, int[] next) {
|
|
||||||
int[] heads = new int[withoutSlope.length];
|
|
||||||
for (int index = count - 1; index >= 0; index--) {
|
|
||||||
int stripeIndex = (int) CacheKey.mix(coordinates[index]) & (withoutSlope.length - 1);
|
|
||||||
next[index] = heads[stripeIndex];
|
|
||||||
heads[stripeIndex] = index + 1;
|
|
||||||
}
|
|
||||||
return heads;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void complete(Stripe[] stripes, long packed, Pending pending, HydrologyTerrainSample sample,
|
|
||||||
boolean provisional) {
|
|
||||||
Stripe stripe = stripe(stripes, packed);
|
|
||||||
synchronized (stripe) {
|
|
||||||
complete(stripe, packed, pending, sample, provisional);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void complete(Stripe stripe, long packed, Pending pending, HydrologyTerrainSample sample,
|
|
||||||
boolean provisional) {
|
|
||||||
if (stripe.samples.containsKey(packed)) {
|
|
||||||
sample = stripe.samples.getAndMoveToFirst(packed);
|
|
||||||
provisional = false;
|
|
||||||
} else if (!provisional) {
|
|
||||||
stripe.put(packed, sample);
|
|
||||||
}
|
|
||||||
stripe.pending.remove(packed);
|
|
||||||
pending.sample = sample;
|
|
||||||
pending.provisional = provisional;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void fail(Stripe[] stripes, long packed, Pending pending, Throwable failure) {
|
|
||||||
Stripe stripe = stripe(stripes, packed);
|
|
||||||
synchronized (stripe) {
|
|
||||||
if (stripe.pending.remove(packed, pending)) {
|
|
||||||
pending.completion.failure = failure;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void seed(Long2ObjectMap<HydrologyTerrainSample> samples, Stripe[] stripes) {
|
|
||||||
for (Long2ObjectMap.Entry<HydrologyTerrainSample> entry : samples.long2ObjectEntrySet()) {
|
|
||||||
Stripe stripe = stripe(stripes, entry.getLongKey());
|
|
||||||
synchronized (stripe) {
|
|
||||||
stripe.put(entry.getLongKey(), entry.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Stripe stripe(Stripe[] stripes, long packed) {
|
|
||||||
return stripes[(int) CacheKey.mix(packed) & (stripes.length - 1)];
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class Stripe {
|
|
||||||
private final int maximumEntries;
|
|
||||||
private final Long2ObjectLinkedOpenHashMap<HydrologyTerrainSample> samples;
|
|
||||||
private final Long2ObjectOpenHashMap<Pending> pending = new Long2ObjectOpenHashMap<>();
|
|
||||||
|
|
||||||
private Stripe(int maximumEntries) {
|
|
||||||
this.maximumEntries = maximumEntries;
|
|
||||||
samples = new Long2ObjectLinkedOpenHashMap<>(Math.min(16, maximumEntries));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void put(long packed, HydrologyTerrainSample value) {
|
|
||||||
if (samples.size() >= maximumEntries && !samples.containsKey(packed)) {
|
|
||||||
samples.removeLast();
|
|
||||||
}
|
|
||||||
samples.putAndMoveToFirst(packed, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class Pending {
|
|
||||||
private final Completion completion;
|
|
||||||
private HydrologyTerrainSample sample;
|
|
||||||
private boolean provisional;
|
|
||||||
|
|
||||||
private Pending(Completion completion) {
|
|
||||||
this.completion = completion;
|
|
||||||
}
|
|
||||||
|
|
||||||
private HydrologyTerrainSample await() {
|
|
||||||
completion.await();
|
|
||||||
if (provisional) {
|
|
||||||
ProvisionalSampling.mark();
|
|
||||||
}
|
|
||||||
return sample;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class Completion implements ForkJoinPool.ManagedBlocker {
|
|
||||||
private final Thread owner = Thread.currentThread();
|
|
||||||
private final CountDownLatch complete = new CountDownLatch(1);
|
|
||||||
private Throwable failure;
|
|
||||||
|
|
||||||
private void await() {
|
|
||||||
if (!isReleasable() && owner == Thread.currentThread()) {
|
|
||||||
throw new IllegalStateException("Recursive terrain basis sampling");
|
|
||||||
}
|
|
||||||
boolean interrupted = false;
|
|
||||||
boolean managed = true;
|
|
||||||
try {
|
|
||||||
while (!isReleasable()) {
|
|
||||||
try {
|
|
||||||
if (managed) {
|
|
||||||
ForkJoinPool.managedBlock(this);
|
|
||||||
} else {
|
|
||||||
complete.await();
|
|
||||||
}
|
|
||||||
} catch (InterruptedException interruption) {
|
|
||||||
interrupted = true;
|
|
||||||
managed = false;
|
|
||||||
} catch (RejectedExecutionException unavailable) {
|
|
||||||
managed = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (interrupted) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (failure instanceof Error error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
if (failure instanceof RuntimeException runtime) {
|
|
||||||
throw runtime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean block() throws InterruptedException {
|
|
||||||
complete.await();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isReleasable() {
|
|
||||||
return complete.getCount() == 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loaded 100 of 1137 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user