mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +00:00
Compare commits
101
Commits
No files matched your search
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Iris",
|
||||
"platform": "modded",
|
||||
"versionProperty": "irisVersion",
|
||||
"betaTag": "modded-beta",
|
||||
"targets": "gradle/mod-targets.json",
|
||||
"volmlibRef": "master"
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import tomllib
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import zipfile
|
||||
|
||||
|
||||
def read_config(root: Path) -> dict:
|
||||
return json.loads((root / '.github/release.json').read_text())
|
||||
|
||||
|
||||
def targets(root: Path, config: dict) -> list[dict[str, str]]:
|
||||
if config['platform'] == 'bukkit':
|
||||
return [{'id': 'bukkit', 'loader': 'bukkit', 'minecraft': ''}]
|
||||
manifest: Path = root / config['targets']
|
||||
if manifest.is_file():
|
||||
versions: dict = json.loads(manifest.read_text())
|
||||
entries: list[tuple[str, list[str]]] = [(version, ['fabric', 'forge', 'neoforge']) for version in versions]
|
||||
else:
|
||||
entries = []
|
||||
for path in sorted(manifest.glob('*.json')):
|
||||
target: dict = json.loads(path.read_text())
|
||||
if path.stem != target['minecraft']:
|
||||
raise ValueError(f'Minecraft manifest does not match {path.name}')
|
||||
entries.append((target['minecraft'], list(target['loaders'])))
|
||||
matrix: list[dict[str, str]] = []
|
||||
for version, loaders in entries:
|
||||
if not re.fullmatch(r'[0-9]+(?:\.[0-9]+)+', version):
|
||||
raise ValueError(f'Invalid Minecraft version: {version}')
|
||||
if set(loaders) != {'fabric', 'forge', 'neoforge'}:
|
||||
raise ValueError(f'Incomplete loader matrix for {version}')
|
||||
for loader in sorted(loaders):
|
||||
matrix.append({'id': f'{loader}-{version}', 'loader': loader, 'minecraft': version})
|
||||
if not matrix:
|
||||
raise ValueError('Release matrix is empty')
|
||||
return matrix
|
||||
|
||||
|
||||
def plan(root: Path, config: dict, event: dict, environment: dict) -> dict:
|
||||
branch: str = event.get('repository', {}).get('default_branch', '')
|
||||
if (environment.get('GITHUB_EVENT_NAME') != 'push' or event.get('deleted')
|
||||
or not branch or event.get('ref') != f'refs/heads/{branch}'):
|
||||
return {'channel': 'none'}
|
||||
commit: dict = event.get('head_commit') or {}
|
||||
message: str = commit.get('message', '')
|
||||
beta: bool = 'B+' in message
|
||||
stable: bool = 'V+' in message
|
||||
if beta and stable:
|
||||
raise ValueError('Use exactly one release marker: B+ or V+')
|
||||
if not beta and not stable:
|
||||
return {'channel': 'none'}
|
||||
sha: str = environment['GITHUB_SHA']
|
||||
if event.get('after') != sha or commit.get('id') != sha or not re.fullmatch(r'[a-f0-9]{40}', sha):
|
||||
raise ValueError('Release commit does not match the pushed HEAD')
|
||||
key: str = config['versionProperty']
|
||||
properties: str = (root / 'gradle.properties').read_text()
|
||||
values: list[str] = re.findall(rf'^{re.escape(key)}\s*=\s*(\S+)\s*$', properties, re.MULTILINE)
|
||||
if len(values) != 1 or not re.fullmatch(r'(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)', values[0]):
|
||||
raise ValueError(f'{key} must declare one stable major.minor.patch version')
|
||||
run: int = int(environment['GITHUB_RUN_NUMBER'])
|
||||
attempt: int = int(environment['GITHUB_RUN_ATTEMPT'])
|
||||
if run < 1 or attempt < 1:
|
||||
raise ValueError('Invalid workflow run identity')
|
||||
version: str = f'{values[0]}-beta.{run}.{attempt}' if beta else values[0]
|
||||
return {
|
||||
'channel': 'beta' if beta else 'stable',
|
||||
'version': version,
|
||||
'version_property': key,
|
||||
'tag': config['betaTag'] if beta else f'v{version}',
|
||||
'sha': sha,
|
||||
'run': run,
|
||||
'attempt': attempt,
|
||||
'matrix': {'include': targets(root, config)},
|
||||
'volmlib_ref': config['volmlibRef'],
|
||||
'wormholes_ref': config.get('wormholesRef', ''),
|
||||
}
|
||||
|
||||
|
||||
def collect_artifacts(directory: Path, release: dict) -> dict[str, bytes]:
|
||||
expected: dict[str, dict] = {f"release-{entry['id']}": entry for entry in release['matrix']['include']}
|
||||
if {path.name for path in directory.iterdir()} != set(expected):
|
||||
raise ValueError('Downloaded artifacts do not match the complete release matrix')
|
||||
assets: dict[str, bytes] = {}
|
||||
descriptors: dict[str, str] = {'bukkit': 'plugin.yml', 'fabric': 'fabric.mod.json',
|
||||
'forge': 'META-INF/mods.toml', 'neoforge': 'META-INF/neoforge.mods.toml'}
|
||||
for name, target in expected.items():
|
||||
folder: Path = directory / name
|
||||
paths: list[Path] = list(folder.iterdir()) if folder.is_dir() and not folder.is_symlink() else []
|
||||
if len(paths) != 1 or paths[0].suffix != '.jar' or paths[0].is_symlink() or not paths[0].is_file():
|
||||
raise ValueError(f'{name} must contain exactly one runtime jar')
|
||||
path: Path = paths[0]
|
||||
loader: str = target['loader']
|
||||
suffix: str = re.escape(release['version']) + r'-bukkit\.jar'
|
||||
if loader != 'bukkit':
|
||||
display: str = {'fabric': 'Fabric', 'forge': 'Forge', 'neoforge': 'NeoForge'}[loader]
|
||||
suffix = (r'v' + re.escape(release['version']) + r' \[' + display + r'\] '
|
||||
+ re.escape(target['minecraft']) + r'\+[^/\s]+\.jar')
|
||||
if not re.fullmatch(r'[A-Za-z][A-Za-z0-9]*' + ('-' if loader == 'bukkit' else ' ') + suffix, path.name):
|
||||
raise ValueError(f'Artifact does not match the release target: {path.name}')
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
entries: set[str] = set(archive.namelist())
|
||||
descriptor: str = descriptors[target['loader']]
|
||||
if descriptor not in entries:
|
||||
raise ValueError(f'{path.name} is missing {descriptor}')
|
||||
excluded: set[str] = set(descriptors.values()) - {descriptor}
|
||||
if 'velocity-plugin.json' in entries or entries.intersection(excluded):
|
||||
raise ValueError(f'{path.name} contains an excluded platform')
|
||||
contents: str = archive.read(descriptor).decode()
|
||||
if loader == 'bukkit':
|
||||
match: re.Match | None = re.search(r'^version:\s*[\'\"]?([^\s\'\"]+)', contents, re.MULTILINE)
|
||||
version: str | None = match[1] if match else None
|
||||
elif loader == 'fabric':
|
||||
metadata: dict = json.loads(contents)
|
||||
version = metadata.get('version')
|
||||
if metadata.get('depends', {}).get('minecraft') not in (target['minecraft'], '~' + target['minecraft']):
|
||||
raise ValueError(f'{path.name} declares a different Minecraft target')
|
||||
else:
|
||||
metadata = tomllib.loads(contents)
|
||||
mods: list[dict] = metadata.get('mods', [])
|
||||
version = mods[0].get('version') if len(mods) == 1 else None
|
||||
dependencies: list[dict] = metadata.get('dependencies', {}).get(mods[0].get('modId'), []) if len(mods) == 1 else []
|
||||
minecraft: list[str] = [entry.get('versionRange') for entry in dependencies if entry.get('modId') == 'minecraft']
|
||||
if minecraft != ['[' + target['minecraft'] + ']']:
|
||||
raise ValueError(f'{path.name} declares a different Minecraft target')
|
||||
if version != release['version']:
|
||||
raise ValueError(f'{path.name} declares a different product version')
|
||||
asset_name: str = re.sub(r'[^A-Za-z0-9._-]+', '-', path.name)
|
||||
if asset_name in assets:
|
||||
raise ValueError(f'Duplicate release asset: {asset_name}')
|
||||
assets[asset_name] = path.read_bytes()
|
||||
checksums: str = ''.join(f'{hashlib.sha256(data).hexdigest()} {name}\n' for name, data in sorted(assets.items()))
|
||||
assets[f"SHA256SUMS-{release['version']}.txt"] = checksums.encode()
|
||||
return assets
|
||||
|
||||
|
||||
class GitHub:
|
||||
def __init__(self, repository: str, token: str):
|
||||
if not re.fullmatch(r'[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+', repository):
|
||||
raise ValueError('Invalid GitHub repository')
|
||||
self.repository = repository
|
||||
self.token = token
|
||||
|
||||
def request(self, method: str, path: str, payload: dict | bytes | None = None, missing: bool = False) -> dict | list | None:
|
||||
url: str = path if path.startswith('https://uploads.github.com/') else f'https://api.github.com/repos/{self.repository}/{path}'
|
||||
binary: bool = isinstance(payload, bytes)
|
||||
data: bytes | None = payload if binary else json.dumps(payload).encode() if payload is not None else None
|
||||
request: urllib.request.Request = urllib.request.Request(url, data=data, method=method, headers={
|
||||
'Authorization': f'Bearer {self.token}',
|
||||
'Accept': 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2026-03-10',
|
||||
'Content-Type': 'application/octet-stream' if binary else 'application/json',
|
||||
'User-Agent': 'Volmit-release',
|
||||
})
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=120) as response:
|
||||
body: bytes = response.read()
|
||||
return json.loads(body) if body else None
|
||||
except urllib.error.HTTPError as error:
|
||||
if missing and error.code == 404:
|
||||
return None
|
||||
raise RuntimeError(f'GitHub {method} {urllib.parse.urlsplit(url).path}: HTTP {error.code}') from error
|
||||
|
||||
def tag_commit(self, tag: str) -> str | None:
|
||||
reference: dict | None = self.request('GET', f'git/ref/tags/{tag}', missing=True)
|
||||
if reference is None:
|
||||
return None
|
||||
target: dict = reference['object']
|
||||
for _ in range(8):
|
||||
if target['type'] == 'commit':
|
||||
return target['sha']
|
||||
if target['type'] != 'tag':
|
||||
break
|
||||
target = self.request('GET', f"git/tags/{target['sha']}")['object']
|
||||
raise ValueError(f'{tag} does not resolve to a commit')
|
||||
|
||||
def assets(self, release_id: int) -> list[dict]:
|
||||
result: list[dict] = []
|
||||
page: int = 1
|
||||
while True:
|
||||
batch: list[dict] = self.request('GET', f'releases/{release_id}/assets?per_page=100&page={page}')
|
||||
result.extend(batch)
|
||||
if len(batch) < 100:
|
||||
return result
|
||||
page += 1
|
||||
|
||||
|
||||
def resolve_dependency(repository: str, reference: str, token: str) -> str:
|
||||
commit: dict = GitHub(repository, token).request('GET', f'commits/{urllib.parse.quote(reference, safe="")}')
|
||||
sha: str = commit['sha']
|
||||
if not re.fullmatch(r'[a-f0-9]{40}', sha):
|
||||
raise ValueError(f'{repository} did not resolve to a commit')
|
||||
return sha
|
||||
|
||||
|
||||
def release_metadata(existing: dict) -> dict:
|
||||
match: re.Match | None = re.search(r'<!-- release: (.+) -->', existing.get('body') or '')
|
||||
if match is None:
|
||||
raise ValueError('Existing release has no publication identity; refusing to overwrite it')
|
||||
return json.loads(match[1])
|
||||
|
||||
|
||||
def verify_assets(existing: list[dict], assets: dict[str, bytes]) -> bool:
|
||||
return {entry['name']: entry.get('digest') for entry in existing} == {
|
||||
name: 'sha256:' + hashlib.sha256(data).hexdigest() for name, data in assets.items()}
|
||||
|
||||
|
||||
def publish(github: GitHub, config: dict, release: dict, assets: dict[str, bytes]) -> str:
|
||||
tag: str = release['tag']
|
||||
beta: bool = release['channel'] == 'beta'
|
||||
existing: dict | None = github.request('GET', f'releases/tags/{tag}', missing=True)
|
||||
commit: str | None = github.tag_commit(tag)
|
||||
if not beta and commit is not None and commit != release['sha']:
|
||||
raise ValueError(f'{tag} already points to another commit')
|
||||
if existing is not None:
|
||||
metadata: dict = release_metadata(existing)
|
||||
if bool(existing['prerelease']) != beta:
|
||||
raise ValueError('Existing release belongs to a different release channel')
|
||||
if beta and (metadata['run'], metadata['attempt']) > (release['run'], release['attempt']):
|
||||
return 'Skipped older beta run'
|
||||
if not beta and metadata['sha'] != release['sha']:
|
||||
raise ValueError('Stable release belongs to another commit')
|
||||
if not existing['draft'] and commit == release['sha'] and metadata['version'] == release['version']:
|
||||
if verify_assets(github.assets(existing['id']), assets):
|
||||
return 'Release already published with identical artifacts'
|
||||
if not beta:
|
||||
raise ValueError('Published release artifacts differ; refusing to overwrite them')
|
||||
if existing.get('immutable'):
|
||||
raise ValueError('Rolling beta requires mutable releases in repository settings')
|
||||
if not beta and not existing['draft']:
|
||||
raise ValueError('Stable release is already published')
|
||||
elif beta and commit is not None:
|
||||
raise ValueError('Beta tag exists without a release; refusing to move it')
|
||||
identity: dict = {key: release[key] for key in ('version', 'sha', 'run', 'attempt', 'volmlib_ref', 'wormholes_ref') if key in release}
|
||||
platform: str = 'Modded' if config['platform'] == 'modded' else 'CraftBukkit'
|
||||
body: str = (f"{platform} jars for {config['name']} {release['version']}.\n\n"
|
||||
f"Commit: `{release['sha']}`\n\n"
|
||||
f"<!-- release: {json.dumps(identity, sort_keys=True)} -->")
|
||||
attributes: dict = {
|
||||
'tag_name': tag, 'target_commitish': release['sha'],
|
||||
'name': f"{config['name']} {platform} {'Beta' if beta else release['version']}",
|
||||
'body': body, 'prerelease': beta, 'make_latest': 'false',
|
||||
}
|
||||
if existing is None:
|
||||
existing = github.request('POST', 'releases', {**attributes, 'draft': True})
|
||||
elif beta:
|
||||
existing = github.request('PATCH', f"releases/{existing['id']}", {**attributes, 'draft': True})
|
||||
uploaded: list[dict] = github.assets(existing['id'])
|
||||
by_name: dict[str, dict] = {entry['name']: entry for entry in uploaded}
|
||||
for name, data in assets.items():
|
||||
digest: str = 'sha256:' + hashlib.sha256(data).hexdigest()
|
||||
if name in by_name:
|
||||
if by_name[name].get('state') == 'starter':
|
||||
github.request('DELETE', f"releases/assets/{by_name[name]['id']}")
|
||||
elif by_name[name].get('digest') != digest:
|
||||
raise ValueError(f'Existing asset differs: {name}')
|
||||
else:
|
||||
continue
|
||||
upload_url: str = existing['upload_url'].split('{')[0] + '?' + urllib.parse.urlencode({'name': name})
|
||||
receipt: dict = github.request('POST', upload_url, data)
|
||||
if receipt.get('digest') != digest or receipt['name'] != name:
|
||||
raise ValueError(f'Upload verification failed: {name}')
|
||||
for entry in uploaded:
|
||||
if entry['name'] not in assets:
|
||||
github.request('DELETE', f"releases/assets/{entry['id']}")
|
||||
commit = github.tag_commit(tag)
|
||||
if commit is None:
|
||||
github.request('POST', 'git/refs', {'ref': f'refs/tags/{tag}', 'sha': release['sha']})
|
||||
elif commit != release['sha']:
|
||||
if not beta:
|
||||
raise ValueError('Stable tag changed during publication')
|
||||
github.request('PATCH', f'git/refs/tags/{tag}', {'sha': release['sha'], 'force': True})
|
||||
if not beta and config['platform'] == 'bukkit':
|
||||
latest: dict | None = github.request('GET', 'releases/latest', missing=True)
|
||||
match: re.Match | None = re.match(r'^v?(\d+)\.(\d+)\.(\d+)(?:$|-)', latest['tag_name']) if latest else None
|
||||
if latest is None or (match and tuple(map(int, release['version'].split('.'))) > tuple(map(int, match.groups()))):
|
||||
attributes['make_latest'] = 'true'
|
||||
github.request('PATCH', f"releases/{existing['id']}", {**attributes, 'draft': False})
|
||||
return f"Published {tag} at {release['sha']}"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser: argparse.ArgumentParser = argparse.ArgumentParser()
|
||||
parser.add_argument('command', choices=['plan', 'publish'])
|
||||
parser.add_argument('--artifacts', type=Path)
|
||||
arguments: argparse.Namespace = parser.parse_args()
|
||||
root: Path = Path(__file__).resolve().parent.parent
|
||||
config: dict = read_config(root)
|
||||
event: dict = json.loads(Path(os.environ['GITHUB_EVENT_PATH']).read_text())
|
||||
environment: dict = dict(os.environ)
|
||||
if arguments.command == 'publish':
|
||||
environment['GITHUB_RUN_ATTEMPT'] = environment['RELEASE_ATTEMPT']
|
||||
release: dict = plan(root, config, event, environment)
|
||||
if arguments.command == 'plan':
|
||||
if release['channel'] != 'none':
|
||||
release['volmlib_ref'] = resolve_dependency('VolmitSoftware/VolmLib', config['volmlibRef'], environment['GH_TOKEN'])
|
||||
if release['wormholes_ref']:
|
||||
release['wormholes_ref'] = resolve_dependency('VolmitSoftware/Wormholes', config['wormholesRef'], environment['GH_TOKEN'])
|
||||
with Path(os.environ['GITHUB_OUTPUT']).open('a') as output:
|
||||
for key, value in release.items():
|
||||
output.write(f'{key}={json.dumps(value, separators=(",", ":")) if isinstance(value, dict) else value}\n')
|
||||
print(f"Release channel: {release['channel']}")
|
||||
elif release['channel'] != 'none':
|
||||
if arguments.artifacts is None:
|
||||
raise ValueError('Publication requires the complete artifact directory')
|
||||
for key in ('volmlib_ref', 'wormholes_ref'):
|
||||
if release[key]:
|
||||
resolved: str = environment[f'RELEASE_{key.upper()}']
|
||||
if not re.fullmatch(r'[a-f0-9]{40}', resolved):
|
||||
raise ValueError(f'{key} is not a resolved dependency commit')
|
||||
release[key] = resolved
|
||||
github: GitHub = GitHub(os.environ['GITHUB_REPOSITORY'], os.environ['GH_TOKEN'])
|
||||
print(publish(github, config, release, collect_artifacts(arguments.artifacts, release)))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except (ValueError, RuntimeError) as error:
|
||||
print(f'::error::{error}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,718 @@
|
||||
import copy
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
import urllib.parse
|
||||
import zipfile
|
||||
|
||||
|
||||
SPEC = importlib.util.spec_from_file_location('release', Path(__file__).with_name('release.py'))
|
||||
release = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(release)
|
||||
|
||||
SHA = 'a' * 40
|
||||
OTHER_SHA = 'b' * 40
|
||||
|
||||
|
||||
def digest(data: bytes) -> str:
|
||||
return 'sha256:' + hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
class FakeGitHub:
|
||||
def __init__(self) -> None:
|
||||
self.tags: dict[str, str] = {}
|
||||
self.releases: dict[int, dict] = {}
|
||||
self.uploads: dict[int, dict[str, dict]] = {}
|
||||
self.writes: list[tuple[str, str]] = []
|
||||
self.latest: dict | None = None
|
||||
self.fail_upload: str | None = None
|
||||
self.fail_delete: int | None = None
|
||||
self.invalid_receipt: bool = False
|
||||
self.next_asset: int = 1
|
||||
|
||||
def seed(self, publication: dict, assets: dict[str, bytes], draft: bool = False,
|
||||
immutable: bool = False) -> dict:
|
||||
identity: dict = {key: publication[key] for key in ('sha', 'version', 'run', 'attempt')}
|
||||
record: dict = {
|
||||
'id': len(self.releases) + 1,
|
||||
'tag_name': publication['tag'],
|
||||
'body': '<!-- release: ' + json.dumps(identity) + ' -->',
|
||||
'draft': draft,
|
||||
'prerelease': publication['channel'] == 'beta',
|
||||
'immutable': immutable,
|
||||
}
|
||||
record['upload_url'] = f"https://uploads.github.com/repos/example/product/releases/{record['id']}/assets{{?name,label}}"
|
||||
self.releases[record['id']] = record
|
||||
self.uploads[record['id']] = {}
|
||||
for name, data in assets.items():
|
||||
self.add_asset(record['id'], name, data)
|
||||
self.tags[publication['tag']] = publication['sha']
|
||||
return record
|
||||
|
||||
def add_asset(self, identifier: int, name: str, data: bytes) -> dict:
|
||||
receipt: dict = {'id': self.next_asset, 'name': name, 'digest': digest(data)}
|
||||
self.next_asset += 1
|
||||
self.uploads[identifier][name] = receipt
|
||||
return copy.deepcopy(receipt)
|
||||
|
||||
def tag_commit(self, tag: str) -> str | None:
|
||||
return self.tags.get(tag)
|
||||
|
||||
def assets(self, identifier: int) -> list[dict]:
|
||||
return copy.deepcopy(list(self.uploads[identifier].values()))
|
||||
|
||||
def request(self, method: str, path: str, payload: dict | bytes | None = None,
|
||||
missing: bool = False) -> dict | list | None:
|
||||
if method != 'GET':
|
||||
self.writes.append((method, path))
|
||||
if method == 'GET' and path.startswith('releases/tags/'):
|
||||
tag: str = path.removeprefix('releases/tags/')
|
||||
return copy.deepcopy(next((record for record in self.releases.values()
|
||||
if record['tag_name'] == tag), None))
|
||||
if method == 'GET' and path == 'releases/latest':
|
||||
return copy.deepcopy(self.latest)
|
||||
if method == 'POST' and path == 'releases':
|
||||
identifier: int = len(self.releases) + 1
|
||||
record: dict = copy.deepcopy(payload)
|
||||
record.update({'id': identifier, 'immutable': False,
|
||||
'upload_url': f'https://uploads.github.com/repos/example/product/releases/{identifier}/assets{{?name,label}}'})
|
||||
self.releases[identifier] = record
|
||||
self.uploads[identifier] = {}
|
||||
return copy.deepcopy(record)
|
||||
if method == 'POST' and path.startswith('https://uploads.github.com/'):
|
||||
address: urllib.parse.SplitResult = urllib.parse.urlsplit(path)
|
||||
name: str = urllib.parse.parse_qs(address.query)['name'][0]
|
||||
if name == self.fail_upload:
|
||||
raise RuntimeError('Upload unavailable')
|
||||
identifier: int = int(address.path.split('/')[-2])
|
||||
receipt: dict = self.add_asset(identifier, name, payload)
|
||||
if self.invalid_receipt:
|
||||
receipt['digest'] = digest(b'corrupted')
|
||||
return receipt
|
||||
if method == 'POST' and path == 'git/refs':
|
||||
tag: str = payload['ref'].removeprefix('refs/tags/')
|
||||
if tag in self.tags:
|
||||
raise RuntimeError('Tag already exists')
|
||||
self.tags[tag] = payload['sha']
|
||||
return {'object': {'type': 'commit', 'sha': payload['sha']}}
|
||||
if method == 'PATCH' and path.startswith('git/refs/tags/'):
|
||||
self.tags[path.removeprefix('git/refs/tags/')] = payload['sha']
|
||||
return None
|
||||
if method == 'DELETE' and path.startswith('releases/assets/'):
|
||||
identifier: int = int(path.rsplit('/', 1)[1])
|
||||
if identifier == self.fail_delete:
|
||||
raise RuntimeError('Delete unavailable')
|
||||
for uploaded in self.uploads.values():
|
||||
for name, entry in list(uploaded.items()):
|
||||
if entry['id'] == identifier:
|
||||
del uploaded[name]
|
||||
return None
|
||||
raise AssertionError(f'Unknown asset: {identifier}')
|
||||
if method == 'PATCH' and path.startswith('releases/'):
|
||||
identifier: int = int(path.rsplit('/', 1)[1])
|
||||
self.releases[identifier].update(copy.deepcopy(payload))
|
||||
if payload.get('make_latest') == 'true':
|
||||
self.latest = copy.deepcopy(self.releases[identifier])
|
||||
return copy.deepcopy(self.releases[identifier])
|
||||
raise AssertionError(f'Unexpected API request: {method} {path}')
|
||||
|
||||
|
||||
class ReleaseTestCase(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
temporary: tempfile.TemporaryDirectory = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
self.root: Path = Path(temporary.name)
|
||||
self.config: dict = {'name': 'Example', 'platform': 'bukkit', 'versionProperty': 'exampleVersion',
|
||||
'betaTag': 'bukkit-beta', 'volmlibRef': 'c' * 40}
|
||||
(self.root / 'gradle.properties').write_text('exampleVersion=1.2.3\n')
|
||||
self.event: dict = {'repository': {'default_branch': 'main'}, 'ref': 'refs/heads/main',
|
||||
'after': SHA, 'head_commit': {'id': SHA, 'message': 'V+ Publish'}}
|
||||
self.environment: dict = {'GITHUB_EVENT_NAME': 'push', 'GITHUB_SHA': SHA,
|
||||
'GITHUB_RUN_NUMBER': '17', 'GITHUB_RUN_ATTEMPT': '2'}
|
||||
network = patch.object(release.urllib.request, 'urlopen', side_effect=AssertionError('Network is forbidden'))
|
||||
network.start()
|
||||
self.addCleanup(network.stop)
|
||||
|
||||
def publication(self, channel: str = 'stable', **changes) -> dict:
|
||||
result: dict = {
|
||||
'channel': channel, 'version': '1.2.3-beta.17.2' if channel == 'beta' else '1.2.3',
|
||||
'tag': 'bukkit-beta' if channel == 'beta' else 'v1.2.3', 'sha': SHA, 'run': 17, 'attempt': 2,
|
||||
'matrix': {'include': [{'id': 'bukkit', 'loader': 'bukkit', 'minecraft': ''}]},
|
||||
}
|
||||
result.update(changes)
|
||||
return result
|
||||
|
||||
def matrix(self, versions: tuple[str, ...] = ('1.20.1', '1.21.1')) -> list[dict]:
|
||||
return [{'id': f'{loader}-{version}', 'loader': loader, 'minecraft': version}
|
||||
for version in versions for loader in ('fabric', 'forge', 'neoforge')]
|
||||
|
||||
def artifact_tree(self, publication: dict, extra: tuple[str, ...] = ()) -> Path:
|
||||
directory: Path = self.root / 'artifacts'
|
||||
directory.mkdir(exist_ok=True)
|
||||
descriptors: dict[str, str] = {'bukkit': 'plugin.yml', 'fabric': 'fabric.mod.json',
|
||||
'forge': 'META-INF/mods.toml', 'neoforge': 'META-INF/neoforge.mods.toml'}
|
||||
for target in publication['matrix']['include']:
|
||||
folder: Path = directory / f"release-{target['id']}"
|
||||
folder.mkdir()
|
||||
loader: str = target['loader']
|
||||
if loader == 'bukkit':
|
||||
filename: str = f"Example-{publication['version']}-bukkit.jar"
|
||||
metadata: str = f"name: Example\nversion: {publication['version']}\nmain: example.Plugin\n"
|
||||
else:
|
||||
display: str = {'fabric': 'Fabric', 'forge': 'Forge', 'neoforge': 'NeoForge'}[loader]
|
||||
filename = f"Example v{publication['version']} [{display}] {target['minecraft']}+1.0.0.jar"
|
||||
if loader == 'fabric':
|
||||
metadata = json.dumps({'schemaVersion': 1, 'id': 'example', 'version': publication['version'],
|
||||
'depends': {'minecraft': '~' + target['minecraft']}})
|
||||
else:
|
||||
metadata = (f"modLoader = 'javafml'\nloaderVersion = '[1,)'\nlicense = 'MIT'\n"
|
||||
f"[[mods]]\nmodId = 'example'\nversion = '{publication['version']}'\n"
|
||||
f"[[dependencies.example]]\nmodId = 'minecraft'\n"
|
||||
f"versionRange = '[{target['minecraft']}]'\n")
|
||||
path: Path = folder / filename
|
||||
with zipfile.ZipFile(path, 'w') as archive:
|
||||
archive.writestr(descriptors[loader], metadata)
|
||||
for name in extra:
|
||||
archive.writestr(name, '')
|
||||
return directory
|
||||
|
||||
|
||||
class PlanTests(ReleaseTestCase):
|
||||
def test_markers_are_case_sensitive(self) -> None:
|
||||
for message, channel in [('B+ Beta', 'beta'), ('V+ Release', 'stable'),
|
||||
('b+ beta', 'none'), ('v+ release', 'none'), ('Fix a bug', 'none')]:
|
||||
with self.subTest(message=message):
|
||||
self.event['head_commit']['message'] = message
|
||||
result: dict = release.plan(self.root, self.config, self.event, self.environment)
|
||||
self.assertEqual(channel, result['channel'])
|
||||
if channel == 'beta':
|
||||
self.assertEqual('1.2.3-beta.17.2', result['version'])
|
||||
self.assertEqual('bukkit-beta', result['tag'])
|
||||
elif channel == 'stable':
|
||||
self.assertEqual('1.2.3', result['version'])
|
||||
self.assertEqual('v1.2.3', result['tag'])
|
||||
|
||||
def test_both_markers_fail(self) -> None:
|
||||
self.event['head_commit']['message'] = 'B+ first\nV+ second'
|
||||
with self.assertRaises(ValueError):
|
||||
release.plan(self.root, self.config, self.event, self.environment)
|
||||
|
||||
def test_only_pushed_default_branch_head_can_release(self) -> None:
|
||||
cases: list[tuple[dict, dict]] = [
|
||||
({'ref': 'refs/heads/feature'}, {}), ({'ref': 'refs/tags/V+1.2.3'}, {}),
|
||||
({'deleted': True}, {}), ({'repository': {}}, {}),
|
||||
({'head_commit': None}, {}), ({}, {'GITHUB_EVENT_NAME': 'pull_request'}),
|
||||
]
|
||||
for event_changes, environment_changes in cases:
|
||||
with self.subTest(event=event_changes, environment=environment_changes):
|
||||
self.assertEqual({'channel': 'none'}, release.plan(
|
||||
self.root, self.config, {**self.event, **event_changes},
|
||||
{**self.environment, **environment_changes}))
|
||||
|
||||
def test_head_identity_must_match_event_and_environment(self) -> None:
|
||||
for field in ('after', 'commit', 'environment'):
|
||||
with self.subTest(field=field):
|
||||
event: dict = copy.deepcopy(self.event)
|
||||
environment: dict = self.environment.copy()
|
||||
if field == 'commit':
|
||||
event['head_commit']['id'] = OTHER_SHA
|
||||
elif field == 'environment':
|
||||
environment['GITHUB_SHA'] = 'not-a-sha'
|
||||
else:
|
||||
event['after'] = OTHER_SHA
|
||||
with self.assertRaises(ValueError):
|
||||
release.plan(self.root, self.config, event, environment)
|
||||
|
||||
def test_old_commits_do_not_trigger_release(self) -> None:
|
||||
self.event['commits'] = [{'id': OTHER_SHA, 'message': 'V+ Release'}]
|
||||
self.event['head_commit']['message'] = 'Fix a bug'
|
||||
self.assertEqual({'channel': 'none'}, release.plan(self.root, self.config, self.event, self.environment))
|
||||
|
||||
def test_invalid_or_duplicate_versions_fail(self) -> None:
|
||||
for value in ('', 'exampleVersion=01.2.3\n', 'exampleVersion=1.2\n',
|
||||
'exampleVersion=1.2.3-beta.1\n', 'exampleVersion=1.2.3\nexampleVersion=2.3.4\n'):
|
||||
with self.subTest(value=value):
|
||||
(self.root / 'gradle.properties').write_text(value)
|
||||
with self.assertRaises(ValueError):
|
||||
release.plan(self.root, self.config, self.event, self.environment)
|
||||
|
||||
def test_invalid_run_identity_fails(self) -> None:
|
||||
for key in ('GITHUB_RUN_NUMBER', 'GITHUB_RUN_ATTEMPT'):
|
||||
with self.subTest(key=key), self.assertRaises(ValueError):
|
||||
release.plan(self.root, self.config, self.event, {**self.environment, key: '0'})
|
||||
|
||||
def test_minecraft_file_matrix_contains_all_loaders(self) -> None:
|
||||
(self.root / 'targets.json').write_text(json.dumps({'1.20.1': {}, '1.21.1': {}}))
|
||||
config: dict = {**self.config, 'platform': 'modded', 'targets': 'targets.json'}
|
||||
self.assertEqual(self.matrix(), release.targets(self.root, config))
|
||||
|
||||
def test_minecraft_directory_matrix_matches_files(self) -> None:
|
||||
folder: Path = self.root / 'targets'
|
||||
folder.mkdir()
|
||||
(folder / '1.20.1.json').write_text(json.dumps({'minecraft': '1.20.1',
|
||||
'loaders': ['fabric', 'forge', 'neoforge']}))
|
||||
config: dict = {**self.config, 'platform': 'modded', 'targets': 'targets'}
|
||||
self.assertEqual(self.matrix(('1.20.1',)), release.targets(self.root, config))
|
||||
for minecraft, loaders in [('1.20.2', ['fabric', 'forge', 'neoforge']), ('1.20.1', ['fabric'])]:
|
||||
with self.subTest(minecraft=minecraft, loaders=loaders):
|
||||
(folder / '1.20.1.json').write_text(json.dumps({'minecraft': minecraft, 'loaders': loaders}))
|
||||
with self.assertRaises(ValueError):
|
||||
release.targets(self.root, config)
|
||||
|
||||
def test_empty_or_invalid_minecraft_matrix_fails(self) -> None:
|
||||
for content in ({}, {'invalid': {}}):
|
||||
with self.subTest(content=content):
|
||||
(self.root / 'targets.json').write_text(json.dumps(content))
|
||||
with self.assertRaises(ValueError):
|
||||
release.targets(self.root, {**self.config, 'platform': 'modded', 'targets': 'targets.json'})
|
||||
|
||||
|
||||
class ArtifactTests(ReleaseTestCase):
|
||||
def test_complete_modded_matrix_and_checksums(self) -> None:
|
||||
publication: dict = self.publication(matrix={'include': self.matrix()})
|
||||
assets: dict[str, bytes] = release.collect_artifacts(self.artifact_tree(publication), publication)
|
||||
self.assertEqual(7, len(assets))
|
||||
checksums: bytes = assets.pop('SHA256SUMS-1.2.3.txt')
|
||||
self.assertEqual(''.join(f'{hashlib.sha256(data).hexdigest()} {name}\n'
|
||||
for name, data in sorted(assets.items())).encode(), checksums)
|
||||
|
||||
def test_missing_or_extra_matrix_target_fails(self) -> None:
|
||||
publication: dict = self.publication()
|
||||
directory: Path = self.artifact_tree(publication)
|
||||
(directory / 'release-extra').mkdir()
|
||||
with self.assertRaises(ValueError):
|
||||
release.collect_artifacts(directory, publication)
|
||||
(directory / 'release-extra').rmdir()
|
||||
(directory / 'release-bukkit').rename(directory / 'release-wrong')
|
||||
with self.assertRaises(ValueError):
|
||||
release.collect_artifacts(directory, publication)
|
||||
|
||||
def test_intermediate_artifact_beside_runtime_fails(self) -> None:
|
||||
publication: dict = self.publication()
|
||||
directory: Path = self.artifact_tree(publication)
|
||||
(directory / 'release-bukkit' / 'sources.jar').write_bytes(b'sources')
|
||||
with self.assertRaises(ValueError):
|
||||
release.collect_artifacts(directory, publication)
|
||||
|
||||
def test_symbolic_link_artifacts_fail(self) -> None:
|
||||
publication: dict = self.publication()
|
||||
directory: Path = self.artifact_tree(publication)
|
||||
path: Path = next((directory / 'release-bukkit').iterdir())
|
||||
target: Path = self.root / path.name
|
||||
path.rename(target)
|
||||
path.symlink_to(target)
|
||||
with self.assertRaises(ValueError):
|
||||
release.collect_artifacts(directory, publication)
|
||||
|
||||
def test_bukkit_is_forbidden_in_modded_release(self) -> None:
|
||||
publication: dict = self.publication(matrix={'include': self.matrix(('1.20.1',))})
|
||||
with self.assertRaises(ValueError):
|
||||
release.collect_artifacts(self.artifact_tree(publication, ('plugin.yml',)), publication)
|
||||
|
||||
def test_velocity_is_forbidden_in_bukkit_release(self) -> None:
|
||||
publication: dict = self.publication()
|
||||
with self.assertRaises(ValueError):
|
||||
release.collect_artifacts(self.artifact_tree(publication, ('velocity-plugin.json',)), publication)
|
||||
|
||||
def test_modded_descriptor_is_forbidden_in_bukkit_release(self) -> None:
|
||||
publication: dict = self.publication()
|
||||
with self.assertRaises(ValueError):
|
||||
release.collect_artifacts(self.artifact_tree(publication, ('fabric.mod.json',)), publication)
|
||||
|
||||
def test_required_platform_descriptor_is_checked(self) -> None:
|
||||
publication: dict = self.publication()
|
||||
directory: Path = self.artifact_tree(publication)
|
||||
path: Path = next((directory / 'release-bukkit').iterdir())
|
||||
with zipfile.ZipFile(path, 'w') as archive:
|
||||
archive.writestr('fabric.mod.json', '{}')
|
||||
with self.assertRaises(ValueError):
|
||||
release.collect_artifacts(directory, publication)
|
||||
|
||||
def test_wrong_version_and_embedded_version_suffix_fail(self) -> None:
|
||||
publication: dict = self.publication()
|
||||
directory: Path = self.artifact_tree(publication)
|
||||
path: Path = next((directory / 'release-bukkit').iterdir())
|
||||
for version in ('9.8.7', '11.2.3'):
|
||||
with self.subTest(version=version):
|
||||
changed: Path = path.with_name(f'Example-{version}-bukkit.jar')
|
||||
path.rename(changed)
|
||||
with self.assertRaises(ValueError):
|
||||
release.collect_artifacts(directory, publication)
|
||||
changed.rename(path)
|
||||
|
||||
def test_descriptor_version_must_match_release(self) -> None:
|
||||
publication: dict = self.publication()
|
||||
directory: Path = self.artifact_tree(publication)
|
||||
path: Path = next((directory / 'release-bukkit').iterdir())
|
||||
with zipfile.ZipFile(path, 'w') as archive:
|
||||
archive.writestr('plugin.yml', 'name: Example\nversion: 1.2.4\nmain: example.Plugin\n')
|
||||
with self.assertRaises(ValueError):
|
||||
release.collect_artifacts(directory, publication)
|
||||
|
||||
def test_modded_descriptor_versions_must_match_release(self) -> None:
|
||||
publication: dict = self.publication(matrix={'include': self.matrix(('1.20.1',))})
|
||||
directory: Path = self.artifact_tree(publication)
|
||||
for folder in directory.iterdir():
|
||||
with self.subTest(target=folder.name):
|
||||
path: Path = next(folder.iterdir())
|
||||
original: bytes = path.read_bytes()
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
descriptor: str = archive.namelist()[0]
|
||||
contents: str = archive.read(descriptor).decode().replace('1.2.3', '1.2.4')
|
||||
with zipfile.ZipFile(path, 'w') as archive:
|
||||
archive.writestr(descriptor, contents)
|
||||
with self.assertRaises(ValueError):
|
||||
release.collect_artifacts(directory, publication)
|
||||
path.write_bytes(original)
|
||||
|
||||
def test_modded_filename_must_match_minecraft_target(self) -> None:
|
||||
publication: dict = self.publication(matrix={'include': self.matrix(('1.20.1',))})
|
||||
directory: Path = self.artifact_tree(publication)
|
||||
path: Path = next((directory / 'release-fabric-1.20.1').iterdir())
|
||||
path.rename(path.with_name(path.name.replace('1.20.1', '1.21.1')))
|
||||
with self.assertRaises(ValueError):
|
||||
release.collect_artifacts(directory, publication)
|
||||
|
||||
def test_modded_metadata_must_match_minecraft_target(self) -> None:
|
||||
publication: dict = self.publication(matrix={'include': self.matrix(('1.20.1',))})
|
||||
directory: Path = self.artifact_tree(publication)
|
||||
for folder in directory.iterdir():
|
||||
with self.subTest(target=folder.name):
|
||||
path: Path = next(folder.iterdir())
|
||||
original: bytes = path.read_bytes()
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
descriptor: str = archive.namelist()[0]
|
||||
contents: str = archive.read(descriptor).decode().replace('1.20.1', '1.21.1')
|
||||
with zipfile.ZipFile(path, 'w') as archive:
|
||||
archive.writestr(descriptor, contents)
|
||||
with self.assertRaises(ValueError):
|
||||
release.collect_artifacts(directory, publication)
|
||||
path.write_bytes(original)
|
||||
|
||||
|
||||
class PublicationTests(ReleaseTestCase):
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.github: FakeGitHub = FakeGitHub()
|
||||
self.assets: dict[str, bytes] = {'Example-1.2.3-bukkit.jar': b'jar', 'SHA256SUMS-1.2.3.txt': b'checksums'}
|
||||
|
||||
def test_new_stable_release_is_published_at_head(self) -> None:
|
||||
release.publish(self.github, self.config, self.publication(), self.assets)
|
||||
record: dict = self.github.releases[1]
|
||||
self.assertEqual(SHA, self.github.tags['v1.2.3'])
|
||||
self.assertFalse(record['draft'])
|
||||
self.assertFalse(record['prerelease'])
|
||||
self.assertEqual('true', record['make_latest'])
|
||||
self.assertEqual(set(self.assets), set(self.github.uploads[1]))
|
||||
|
||||
def test_stable_existing_tag_collision_has_no_writes(self) -> None:
|
||||
self.github.tags['v1.2.3'] = OTHER_SHA
|
||||
with self.assertRaises(ValueError):
|
||||
release.publish(self.github, self.config, self.publication(), self.assets)
|
||||
self.assertEqual([], self.github.writes)
|
||||
self.assertEqual(OTHER_SHA, self.github.tags['v1.2.3'])
|
||||
|
||||
def test_stable_same_content_rerun_has_no_writes(self) -> None:
|
||||
self.github.seed(self.publication(), self.assets, immutable=True)
|
||||
result: str = release.publish(self.github, self.config, self.publication(), self.assets)
|
||||
self.assertIn('already published', result)
|
||||
self.assertEqual([], self.github.writes)
|
||||
|
||||
def test_published_stable_artifacts_cannot_be_replaced(self) -> None:
|
||||
self.github.seed(self.publication(), self.assets, immutable=True)
|
||||
with self.assertRaises(ValueError):
|
||||
release.publish(self.github, self.config, self.publication(),
|
||||
{**self.assets, 'Example-1.2.3-bukkit.jar': b'different'})
|
||||
self.assertEqual([], self.github.writes)
|
||||
|
||||
def test_stable_release_identity_collision_has_no_writes(self) -> None:
|
||||
self.github.seed(self.publication(sha=OTHER_SHA), self.assets)
|
||||
self.github.tags['v1.2.3'] = SHA
|
||||
with self.assertRaises(ValueError):
|
||||
release.publish(self.github, self.config, self.publication(), self.assets)
|
||||
self.assertEqual([], self.github.writes)
|
||||
|
||||
def test_stable_release_cannot_reuse_published_prerelease(self) -> None:
|
||||
record: dict = self.github.seed(self.publication(), self.assets)
|
||||
record['prerelease'] = True
|
||||
with self.assertRaises(ValueError):
|
||||
release.publish(self.github, self.config, self.publication(), self.assets)
|
||||
self.assertEqual([], self.github.writes)
|
||||
|
||||
def test_partially_uploaded_stable_draft_resumes(self) -> None:
|
||||
record: dict = self.github.seed(self.publication(), {'Example-1.2.3-bukkit.jar': b'jar'}, draft=True)
|
||||
original_id: int = self.github.uploads[record['id']]['Example-1.2.3-bukkit.jar']['id']
|
||||
release.publish(self.github, self.config, self.publication(), self.assets)
|
||||
self.assertFalse(self.github.releases[record['id']]['draft'])
|
||||
self.assertEqual(original_id, self.github.uploads[record['id']]['Example-1.2.3-bukkit.jar']['id'])
|
||||
self.assertEqual(set(self.assets), set(self.github.uploads[record['id']]))
|
||||
self.assertEqual(1, len(self.github.releases))
|
||||
|
||||
def test_conflicting_draft_asset_fails_without_replacement(self) -> None:
|
||||
self.github.seed(self.publication(), {'Example-1.2.3-bukkit.jar': b'other'}, draft=True)
|
||||
with self.assertRaises(ValueError):
|
||||
release.publish(self.github, self.config, self.publication(), self.assets)
|
||||
self.assertEqual([], self.github.writes)
|
||||
self.assertTrue(self.github.releases[1]['draft'])
|
||||
|
||||
def test_newer_beta_run_or_attempt_is_preserved(self) -> None:
|
||||
for run, attempt in ((18, 1), (17, 3)):
|
||||
with self.subTest(run=run, attempt=attempt):
|
||||
github: FakeGitHub = FakeGitHub()
|
||||
github.seed(self.publication('beta', run=run, attempt=attempt, sha=OTHER_SHA), self.assets)
|
||||
result: str = release.publish(github, self.config, self.publication('beta'), self.assets)
|
||||
self.assertIn('Skipped', result)
|
||||
self.assertEqual([], github.writes)
|
||||
self.assertEqual(OTHER_SHA, github.tags['bukkit-beta'])
|
||||
|
||||
def test_successful_beta_replaces_assets_and_remains_prerelease(self) -> None:
|
||||
previous: dict = self.publication('beta', run=16, attempt=1, sha=OTHER_SHA, version='1.2.3-beta.16.1')
|
||||
record: dict = self.github.seed(previous, {'old.jar': b'old'})
|
||||
release.publish(self.github, self.config, self.publication('beta'), self.assets)
|
||||
current: dict = self.github.releases[record['id']]
|
||||
self.assertEqual(SHA, self.github.tags['bukkit-beta'])
|
||||
self.assertTrue(current['prerelease'])
|
||||
self.assertFalse(current['draft'])
|
||||
self.assertEqual('false', current['make_latest'])
|
||||
self.assertEqual(set(self.assets), set(self.github.uploads[record['id']]))
|
||||
|
||||
def test_failed_beta_upload_preserves_tag_and_assets_in_recoverable_draft(self) -> None:
|
||||
previous: dict = self.publication('beta', run=16, attempt=1, sha=OTHER_SHA, version='1.2.3-beta.16.1')
|
||||
record: dict = self.github.seed(previous, {'old.jar': b'old'})
|
||||
self.github.fail_upload = 'SHA256SUMS-1.2.3.txt'
|
||||
with self.assertRaises(RuntimeError):
|
||||
release.publish(self.github, self.config, self.publication('beta'), self.assets)
|
||||
self.assertEqual(OTHER_SHA, self.github.tags['bukkit-beta'])
|
||||
self.assertTrue(self.github.releases[record['id']]['draft'])
|
||||
self.assertEqual(17, release.release_metadata(self.github.releases[record['id']])['run'])
|
||||
self.assertIn('old.jar', self.github.uploads[record['id']])
|
||||
self.assertFalse(any(method == 'DELETE' or path.startswith('git/refs')
|
||||
for method, path in self.github.writes))
|
||||
|
||||
def test_failed_beta_cleanup_resumes_without_reuploading_verified_assets(self) -> None:
|
||||
previous: dict = self.publication('beta', run=16, attempt=1, sha=OTHER_SHA, version='1.2.3-beta.16.1')
|
||||
record: dict = self.github.seed(previous, {'old.jar': b'old'})
|
||||
self.github.fail_delete = self.github.uploads[record['id']]['old.jar']['id']
|
||||
with self.assertRaises(RuntimeError):
|
||||
release.publish(self.github, self.config, self.publication('beta'), self.assets)
|
||||
self.assertTrue(self.github.releases[record['id']]['draft'])
|
||||
self.assertEqual(OTHER_SHA, self.github.tags['bukkit-beta'])
|
||||
uploaded_ids: dict[str, int] = {name: self.github.uploads[record['id']][name]['id'] for name in self.assets}
|
||||
self.github.fail_delete = None
|
||||
release.publish(self.github, self.config, self.publication('beta'), self.assets)
|
||||
self.assertFalse(self.github.releases[record['id']]['draft'])
|
||||
self.assertEqual(SHA, self.github.tags['bukkit-beta'])
|
||||
self.assertEqual(uploaded_ids, {name: entry['id'] for name, entry in self.github.uploads[record['id']].items()})
|
||||
|
||||
def test_incomplete_newer_beta_draft_blocks_older_run(self) -> None:
|
||||
self.github.seed(self.publication('beta', run=18, sha=OTHER_SHA), {}, draft=True)
|
||||
result: str = release.publish(self.github, self.config, self.publication('beta'), self.assets)
|
||||
self.assertIn('Skipped', result)
|
||||
self.assertEqual([], self.github.writes)
|
||||
self.assertTrue(self.github.releases[1]['draft'])
|
||||
|
||||
def test_starter_upload_in_draft_is_replaced_before_publication(self) -> None:
|
||||
self.github.seed(self.publication(), {'Example-1.2.3-bukkit.jar': b''}, draft=True)
|
||||
starter: dict = self.github.uploads[1]['Example-1.2.3-bukkit.jar']
|
||||
identifier: int = starter['id']
|
||||
starter.update({'state': 'starter', 'digest': None})
|
||||
release.publish(self.github, self.config, self.publication(), self.assets)
|
||||
self.assertFalse(self.github.releases[1]['draft'])
|
||||
self.assertNotEqual(identifier, self.github.uploads[1]['Example-1.2.3-bukkit.jar']['id'])
|
||||
self.assertEqual(digest(b'jar'), self.github.uploads[1]['Example-1.2.3-bukkit.jar']['digest'])
|
||||
|
||||
def test_invalid_upload_digest_does_not_create_tag_or_publish(self) -> None:
|
||||
self.github.invalid_receipt = True
|
||||
with self.assertRaises(ValueError):
|
||||
release.publish(self.github, self.config, self.publication(), self.assets)
|
||||
self.assertEqual({}, self.github.tags)
|
||||
self.assertTrue(self.github.releases[1]['draft'])
|
||||
|
||||
def test_beta_will_not_replace_stable_or_immutable_release(self) -> None:
|
||||
for immutable, prerelease in ((True, True), (False, False)):
|
||||
with self.subTest(immutable=immutable, prerelease=prerelease):
|
||||
github: FakeGitHub = FakeGitHub()
|
||||
record: dict = github.seed(self.publication('beta', run=16, sha=OTHER_SHA), self.assets,
|
||||
immutable=immutable)
|
||||
record['prerelease'] = prerelease
|
||||
with self.assertRaises(ValueError):
|
||||
release.publish(github, self.config, self.publication('beta'), self.assets)
|
||||
self.assertEqual([], github.writes)
|
||||
|
||||
def test_unowned_existing_release_is_preserved(self) -> None:
|
||||
record: dict = self.github.seed(self.publication(), self.assets)
|
||||
record['body'] = 'Published manually'
|
||||
with self.assertRaises(ValueError):
|
||||
release.publish(self.github, self.config, self.publication(), self.assets)
|
||||
self.assertEqual([], self.github.writes)
|
||||
|
||||
def test_beta_tag_without_release_is_preserved(self) -> None:
|
||||
self.github.tags['bukkit-beta'] = OTHER_SHA
|
||||
with self.assertRaises(ValueError):
|
||||
release.publish(self.github, self.config, self.publication('beta'), self.assets)
|
||||
self.assertEqual([], self.github.writes)
|
||||
|
||||
def test_modded_stable_release_never_changes_latest(self) -> None:
|
||||
self.github.latest = {'tag_name': 'v1.0.0'}
|
||||
release.publish(self.github, {**self.config, 'platform': 'modded'}, self.publication(), self.assets)
|
||||
self.assertFalse(self.github.releases[1]['prerelease'])
|
||||
self.assertEqual('false', self.github.releases[1]['make_latest'])
|
||||
self.assertEqual('v1.0.0', self.github.latest['tag_name'])
|
||||
|
||||
def test_bukkit_backport_does_not_replace_newer_latest(self) -> None:
|
||||
for latest in ('v1.3.0', 'v2.0.0', 'v1.2.3'):
|
||||
with self.subTest(latest=latest):
|
||||
github: FakeGitHub = FakeGitHub()
|
||||
github.latest = {'tag_name': latest}
|
||||
release.publish(github, self.config, self.publication(), self.assets)
|
||||
self.assertEqual('false', github.releases[1]['make_latest'])
|
||||
self.assertEqual(latest, github.latest['tag_name'])
|
||||
|
||||
def test_newer_bukkit_release_becomes_latest(self) -> None:
|
||||
self.github.latest = {'tag_name': 'v1.2.2'}
|
||||
release.publish(self.github, self.config, self.publication(), self.assets)
|
||||
self.assertEqual('true', self.github.releases[1]['make_latest'])
|
||||
self.assertEqual('v1.2.3', self.github.latest['tag_name'])
|
||||
|
||||
|
||||
class GitHubReadTests(ReleaseTestCase):
|
||||
def test_annotated_tag_resolves_to_commit(self) -> None:
|
||||
github: release.GitHub = release.GitHub('example/product', 'test-token')
|
||||
responses: dict[str, dict] = {
|
||||
'git/ref/tags/v1.2.3': {'object': {'type': 'tag', 'sha': OTHER_SHA}},
|
||||
f'git/tags/{OTHER_SHA}': {'object': {'type': 'commit', 'sha': SHA}},
|
||||
}
|
||||
with patch.object(github, 'request', side_effect=lambda method, path, **options: responses[path]):
|
||||
self.assertEqual(SHA, github.tag_commit('v1.2.3'))
|
||||
|
||||
def test_noncommit_tag_cannot_be_published(self) -> None:
|
||||
github: release.GitHub = release.GitHub('example/product', 'test-token')
|
||||
with patch.object(github, 'request', return_value={'object': {'type': 'tree', 'sha': SHA}}):
|
||||
with self.assertRaises(ValueError):
|
||||
github.tag_commit('v1.2.3')
|
||||
|
||||
def test_assets_include_all_pages(self) -> None:
|
||||
github: release.GitHub = release.GitHub('example/product', 'test-token')
|
||||
first: list[dict] = [{'id': number, 'name': f'{number}.jar'} for number in range(100)]
|
||||
last: list[dict] = [{'id': 100, 'name': 'last.jar'}]
|
||||
with patch.object(github, 'request', side_effect=[first, last]):
|
||||
self.assertEqual(first + last, github.assets(1))
|
||||
|
||||
def test_invalid_repository_is_rejected(self) -> None:
|
||||
for repository in ('example', '../example/product', 'example/product/extra'):
|
||||
with self.subTest(repository=repository), self.assertRaises(ValueError):
|
||||
release.GitHub(repository, 'test-token')
|
||||
|
||||
|
||||
class DependencyResolutionTests(ReleaseTestCase):
|
||||
def test_branch_resolves_to_full_commit_sha(self) -> None:
|
||||
with patch.object(release.GitHub, 'request', return_value={'sha': SHA}) as request:
|
||||
self.assertEqual(SHA, release.resolve_dependency('example/product', 'master', 'test-token'))
|
||||
request.assert_called_once_with('GET', 'commits/master')
|
||||
|
||||
def test_full_commit_reference_is_verified(self) -> None:
|
||||
with patch.object(release.GitHub, 'request', return_value={'sha': SHA}) as request:
|
||||
self.assertEqual(SHA, release.resolve_dependency('example/product', SHA, 'test-token'))
|
||||
request.assert_called_once_with('GET', f'commits/{SHA}')
|
||||
|
||||
def test_incomplete_or_invalid_resolved_sha_fails(self) -> None:
|
||||
for sha in ('', 'a' * 39, 'a' * 41, 'A' * 40, 'g' * 40, 'master'):
|
||||
with self.subTest(sha=sha), patch.object(release.GitHub, 'request', return_value={'sha': sha}):
|
||||
with self.assertRaises(ValueError):
|
||||
release.resolve_dependency('example/product', 'master', 'test-token')
|
||||
|
||||
def test_reference_is_encoded_as_single_url_path_component(self) -> None:
|
||||
with patch.object(release.GitHub, 'request', return_value={'sha': SHA}) as request:
|
||||
self.assertEqual(SHA, release.resolve_dependency('example/product', 'feature/next+release #?', 'test-token'))
|
||||
request.assert_called_once_with('GET', 'commits/feature%2Fnext%2Brelease%20%23%3F')
|
||||
|
||||
|
||||
class MainTests(ReleaseTestCase):
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.config.update({'volmlibRef': 'master', 'wormholesRef': 'release/api'})
|
||||
(self.root / '.github').mkdir()
|
||||
(self.root / '.github/release.json').write_text(json.dumps(self.config))
|
||||
self.event['head_commit']['message'] = 'B+ Publish beta'
|
||||
(self.root / 'event.json').write_text(json.dumps(self.event))
|
||||
self.environment.update({
|
||||
'GITHUB_EVENT_PATH': str(self.root / 'event.json'),
|
||||
'GITHUB_OUTPUT': str(self.root / 'output.txt'),
|
||||
'GITHUB_REPOSITORY': 'example/product',
|
||||
'GH_TOKEN': 'test-token',
|
||||
})
|
||||
script = patch.object(release, '__file__', str(self.root / '.github/release.py'))
|
||||
script.start()
|
||||
self.addCleanup(script.stop)
|
||||
output = patch('sys.stdout', new=io.StringIO())
|
||||
output.start()
|
||||
self.addCleanup(output.stop)
|
||||
|
||||
def test_plan_freezes_each_dependency_branch_once(self) -> None:
|
||||
with patch.dict(release.os.environ, self.environment, clear=True), \
|
||||
patch.object(release.sys, 'argv', ['release.py', 'plan']), \
|
||||
patch.object(release.GitHub, 'request', side_effect=[{'sha': SHA}, {'sha': OTHER_SHA}]) as request:
|
||||
release.main()
|
||||
outputs: dict[str, str] = dict(line.split('=', 1) for line in (self.root / 'output.txt').read_text().splitlines())
|
||||
self.assertEqual(SHA, outputs['volmlib_ref'])
|
||||
self.assertEqual(OTHER_SHA, outputs['wormholes_ref'])
|
||||
self.assertEqual('1.2.3-beta.17.2', outputs['version'])
|
||||
self.assertEqual('2', outputs['attempt'])
|
||||
self.assertEqual(2, request.call_count)
|
||||
self.assertEqual(['commits/master', 'commits/release%2Fapi'], [call.args[1] for call in request.call_args_list])
|
||||
|
||||
def test_unmarked_push_never_resolves_dependencies(self) -> None:
|
||||
self.event['head_commit']['message'] = 'Fix a bug'
|
||||
(self.root / 'event.json').write_text(json.dumps(self.event))
|
||||
with patch.dict(release.os.environ, self.environment, clear=True), \
|
||||
patch.object(release.sys, 'argv', ['release.py', 'plan']), \
|
||||
patch.object(release, 'resolve_dependency', side_effect=AssertionError('Unexpected dependency lookup')):
|
||||
release.main()
|
||||
self.assertEqual('channel=none\n', (self.root / 'output.txt').read_text())
|
||||
|
||||
def test_publish_rerun_uses_original_planned_attempt_and_dependency_commits(self) -> None:
|
||||
publication: dict = self.publication('beta')
|
||||
artifacts: Path = self.artifact_tree(publication)
|
||||
github: FakeGitHub = FakeGitHub()
|
||||
self.environment.update({'GITHUB_RUN_ATTEMPT': '3', 'RELEASE_ATTEMPT': '2',
|
||||
'RELEASE_VOLMLIB_REF': SHA, 'RELEASE_WORMHOLES_REF': OTHER_SHA})
|
||||
with patch.dict(release.os.environ, self.environment, clear=True), \
|
||||
patch.object(release.sys, 'argv', ['release.py', 'publish', '--artifacts', str(artifacts)]), \
|
||||
patch.object(release, 'GitHub', return_value=github), \
|
||||
patch.object(release, 'resolve_dependency', side_effect=AssertionError('Dependencies must stay frozen')):
|
||||
release.main()
|
||||
metadata: dict = release.release_metadata(github.releases[1])
|
||||
self.assertEqual('1.2.3-beta.17.2', metadata['version'])
|
||||
self.assertEqual(2, metadata['attempt'])
|
||||
self.assertEqual(SHA, metadata['volmlib_ref'])
|
||||
self.assertEqual(OTHER_SHA, metadata['wormholes_ref'])
|
||||
self.assertFalse(github.releases[1]['draft'])
|
||||
self.assertEqual({'Example-1.2.3-beta.17.2-bukkit.jar', 'SHA256SUMS-1.2.3-beta.17.2.txt'},
|
||||
set(github.uploads[1]))
|
||||
writes: list[tuple[str, str]] = github.writes.copy()
|
||||
release.os.environ['GITHUB_RUN_ATTEMPT'] = '4'
|
||||
release.main()
|
||||
self.assertEqual(writes, github.writes)
|
||||
|
||||
def test_publish_rejects_unresolved_dependency_reference_before_writing(self) -> None:
|
||||
artifacts: Path = self.artifact_tree(self.publication('beta'))
|
||||
github: FakeGitHub = FakeGitHub()
|
||||
self.environment.update({'RELEASE_ATTEMPT': '2', 'RELEASE_VOLMLIB_REF': SHA,
|
||||
'RELEASE_WORMHOLES_REF': OTHER_SHA})
|
||||
for key in ('RELEASE_VOLMLIB_REF', 'RELEASE_WORMHOLES_REF'):
|
||||
with self.subTest(key=key), \
|
||||
patch.dict(release.os.environ, {**self.environment, key: 'master'}, clear=True), \
|
||||
patch.object(release.sys, 'argv', ['release.py', 'publish', '--artifacts', str(artifacts)]), \
|
||||
patch.object(release, 'GitHub', return_value=github):
|
||||
with self.assertRaises(ValueError):
|
||||
release.main()
|
||||
self.assertEqual([], github.writes)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,157 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/**'
|
||||
- '**/*.gradle'
|
||||
- '**/gradle.properties'
|
||||
- 'gradle/**/*.json'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PYTHONDONTWRITEBYTECODE: '1'
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Test release handling
|
||||
run: python3 -m unittest discover -s .github -p 'test_release.py' -v
|
||||
|
||||
plan:
|
||||
needs: validate
|
||||
if: github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
channel: ${{ steps.release.outputs.channel }}
|
||||
version: ${{ steps.release.outputs.version }}
|
||||
attempt: ${{ steps.release.outputs.attempt }}
|
||||
version_property: ${{ steps.release.outputs.version_property }}
|
||||
matrix: ${{ steps.release.outputs.matrix }}
|
||||
volmlib_ref: ${{ steps.release.outputs.volmlib_ref }}
|
||||
wormholes_ref: ${{ steps.release.outputs.wormholes_ref }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Select release channel and targets
|
||||
id: release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: python3 .github/release.py plan
|
||||
|
||||
build:
|
||||
needs: plan
|
||||
if: needs.plan.outputs.channel != 'none'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 3
|
||||
matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_localVolmLibDirectory: ${{ github.workspace }}/VolmLib
|
||||
RELEASE_VERSION: ${{ needs.plan.outputs.version }}
|
||||
VERSION_PROPERTY: ${{ needs.plan.outputs.version_property }}
|
||||
RELEASE_LOADER: ${{ matrix.loader }}
|
||||
MINECRAFT_VERSION: ${{ matrix.minecraft }}
|
||||
steps:
|
||||
- name: Check out release sources
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
path: project
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- name: Check out VolmLib
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
repository: VolmitSoftware/VolmLib
|
||||
ref: ${{ needs.plan.outputs.volmlib_ref }}
|
||||
path: VolmLib
|
||||
persist-credentials: false
|
||||
- name: Check out Wormholes API sources
|
||||
if: needs.plan.outputs.wormholes_ref != ''
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
repository: VolmitSoftware/Wormholes
|
||||
ref: ${{ needs.plan.outputs.wormholes_ref }}
|
||||
path: WormholesPlugin
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- name: Set up Java 21 toolchain
|
||||
uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '21'
|
||||
- name: Set up Java 25
|
||||
uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '25'
|
||||
- uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6
|
||||
- name: Build Wormholes API
|
||||
if: needs.plan.outputs.wormholes_ref != ''
|
||||
working-directory: WormholesPlugin
|
||||
run: ./gradlew apiJar --no-daemon --max-workers=2 '-Dorg.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8'
|
||||
- name: Locate Wormholes API
|
||||
if: needs.plan.outputs.wormholes_ref != ''
|
||||
shell: bash
|
||||
run: |
|
||||
api_jars=(WormholesPlugin/build/libs/*-api.jar)
|
||||
test "${#api_jars[@]}" -eq 1
|
||||
test -f "${api_jars[0]}"
|
||||
echo "ORG_GRADLE_PROJECT_wormholesApiJar=$PWD/${api_jars[0]}" >> "$GITHUB_ENV"
|
||||
- name: Build and verify release jar
|
||||
working-directory: project
|
||||
shell: bash
|
||||
run: |
|
||||
arguments=(prepareRelease --no-daemon --max-workers=2
|
||||
'-Dorg.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8'
|
||||
"-P$VERSION_PROPERTY=$RELEASE_VERSION")
|
||||
if [[ "$RELEASE_LOADER" != bukkit ]]; then
|
||||
arguments+=("-PreleaseLoader=$RELEASE_LOADER" "-PminecraftVersion=$MINECRAFT_VERSION"
|
||||
-PmoddedBuildParallelism=1 -PmoddedTestForks=1)
|
||||
fi
|
||||
./gradlew "${arguments[@]}"
|
||||
- name: Upload verified jar
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9
|
||||
with:
|
||||
name: release-${{ matrix.id }}
|
||||
path: project/build/release/*.jar
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
compression-level: 0
|
||||
|
||||
publish:
|
||||
needs: [plan, build]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
concurrency:
|
||||
group: release-publication
|
||||
cancel-in-progress: false
|
||||
queue: max
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Download complete release matrix
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333
|
||||
with:
|
||||
pattern: release-*
|
||||
path: build/release-artifacts
|
||||
- name: Publish verified release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_ATTEMPT: ${{ needs.plan.outputs.attempt }}
|
||||
RELEASE_VOLMLIB_REF: ${{ needs.plan.outputs.volmlib_ref }}
|
||||
RELEASE_WORMHOLES_REF: ${{ needs.plan.outputs.wormholes_ref }}
|
||||
run: python3 .github/release.py publish --artifacts build/release-artifacts
|
||||
+7
-1
@@ -497,7 +497,13 @@ public class NativeStructureVolumeIndexTest {
|
||||
private static Map<?, ?> cache(NativeStructureVolumeIndex index, String name) throws Exception {
|
||||
Field field = NativeStructureVolumeIndex.class.getDeclaredField(name);
|
||||
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) {
|
||||
|
||||
+7
-1
@@ -497,7 +497,13 @@ public class NativeStructureVolumeIndexTest {
|
||||
private static Map<?, ?> cache(NativeStructureVolumeIndex index, String name) throws Exception {
|
||||
Field field = NativeStructureVolumeIndex.class.getDeclaredField(name);
|
||||
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) {
|
||||
|
||||
@@ -58,6 +58,7 @@ IrisFileOperations irisFileOperations = objects.newInstance(IrisFileOperations)
|
||||
|
||||
apply from: file('../../gradle/modded-generation-build-revision.gradle')
|
||||
apply from: file('../../gradle/volmlib-stamp.gradle')
|
||||
apply from: file('../../gradle/java21-runtime-exclusions.gradle')
|
||||
|
||||
Properties rootProperties = new Properties()
|
||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||
@@ -322,8 +323,7 @@ tasks.named('shadowJar', ShadowJar).configure {
|
||||
configurations = [project.configurations.named('bundle').get()]
|
||||
mergeServiceFiles()
|
||||
if ((int) irisModTarget.java == 21) {
|
||||
exclude('art/arcane/volmlib/util/noise/NativeBillowLoader*.class')
|
||||
exclude('art/arcane/volmlib/util/noise/BillowBatch*.class')
|
||||
exclude(java21RuntimeExclusions)
|
||||
}
|
||||
exclude('META-INF/maven/**')
|
||||
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/volmlib-stamp.gradle')
|
||||
apply from: file('../../gradle/java21-runtime-exclusions.gradle')
|
||||
|
||||
Properties rootProperties = new Properties()
|
||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||
@@ -309,8 +310,7 @@ tasks.named('shadowJar', ShadowJar).configure {
|
||||
from(sourceSets.main.output)
|
||||
mergeServiceFiles()
|
||||
if ((int) irisModTarget.java == 21) {
|
||||
exclude('art/arcane/volmlib/util/noise/NativeBillowLoader*.class')
|
||||
exclude('art/arcane/volmlib/util/noise/BillowBatch*.class')
|
||||
exclude(java21RuntimeExclusions)
|
||||
}
|
||||
exclude('META-INF/maven/**')
|
||||
exclude('META-INF/proguard/**')
|
||||
|
||||
@@ -56,6 +56,7 @@ IrisFileOperations irisFileOperations = objects.newInstance(IrisFileOperations)
|
||||
|
||||
apply from: file('../../gradle/modded-generation-build-revision.gradle')
|
||||
apply from: file('../../gradle/volmlib-stamp.gradle')
|
||||
apply from: file('../../gradle/java21-runtime-exclusions.gradle')
|
||||
|
||||
Properties rootProperties = new Properties()
|
||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||
@@ -295,8 +296,7 @@ tasks.named('shadowJar', ShadowJar).configure {
|
||||
from(sourceSets.main.output)
|
||||
mergeServiceFiles()
|
||||
if ((int) irisModTarget.java == 21) {
|
||||
exclude('art/arcane/volmlib/util/noise/NativeBillowLoader*.class')
|
||||
exclude('art/arcane/volmlib/util/noise/BillowBatch*.class')
|
||||
exclude(java21RuntimeExclusions)
|
||||
}
|
||||
exclude('META-INF/maven/**')
|
||||
exclude('META-INF/proguard/**')
|
||||
|
||||
+18
-1
@@ -230,6 +230,7 @@ List<Object> moddedSharedInputs = []
|
||||
'gradle/libs.versions.toml',
|
||||
'gradle/volmlib-resolution.settings.gradle',
|
||||
'gradle/volmlib-stamp.gradle',
|
||||
'gradle/java21-runtime-exclusions.gradle',
|
||||
'gradle/generation-build-revision.gradle',
|
||||
'gradle/modded-generation-build-revision.gradle',
|
||||
'gradle/native-minecraft-sources.gradle',
|
||||
@@ -245,7 +246,8 @@ List<Object> moddedSharedInputs = []
|
||||
].each { String path -> moddedSharedInputs.add(layout.projectDirectory.file(path).asFile) }
|
||||
if (localVolmLibCheckout != null) {
|
||||
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/minecraft-common/src/**', 'packaging/src/main/**',
|
||||
'**/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")
|
||||
}
|
||||
|
||||
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') {
|
||||
group = 'iris'
|
||||
description = "Builds and verifies the Fabric ${minecraftVersion} jar."
|
||||
|
||||
+3
-2
@@ -39,6 +39,7 @@ plugins {
|
||||
}
|
||||
|
||||
apply from: rootProject.file('gradle/runtime-relocations.gradle')
|
||||
apply from: rootProject.file('gradle/java21-runtime-exclusions.gradle')
|
||||
|
||||
def lib = 'art.arcane.iris.util'
|
||||
|
||||
@@ -178,6 +179,7 @@ dependencies {
|
||||
tasks.named('test').configure {
|
||||
maxHeapSize = '1g'
|
||||
jvmArgs('-XX:+ExitOnOutOfMemoryError')
|
||||
systemProperty('iris.generation.counters', 'true')
|
||||
}
|
||||
|
||||
java {
|
||||
@@ -474,8 +476,7 @@ tasks.named('shadowJar', ShadowJar).configure {
|
||||
dependsOn(embeddedAgentJar)
|
||||
mergeServiceFiles()
|
||||
if (providers.gradleProperty('irisRuntimeJava').getOrElse(rootProject.irisModTarget.java.toString()) == '21') {
|
||||
exclude('art/arcane/volmlib/util/noise/NativeBillowLoader*.class')
|
||||
exclude('art/arcane/volmlib/util/noise/BillowBatch*.class')
|
||||
exclude(java21RuntimeExclusions)
|
||||
}
|
||||
// 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
|
||||
|
||||
@@ -16,8 +16,8 @@ art/arcane/iris/generation/block/IrisCustomData.java
|
||||
art/arcane/iris/generation/block/LegacyTileData.java
|
||||
art/arcane/iris/generation/block/TileData.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/ChunkDataOutput.java
|
||||
art/arcane/iris/generation/chunk/ChunkHunkView.java
|
||||
art/arcane/iris/generation/chunk/LinkedTerrainChunk.java
|
||||
art/arcane/iris/generation/chunk/TerrainChunk.java
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -515,7 +515,10 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
|
||||
}
|
||||
|
||||
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();
|
||||
Engine engine = data == null ? null : data.getEngine();
|
||||
CachedChildGenerator recent = recentChildGenerator;
|
||||
@@ -533,6 +536,10 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
|
||||
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) {
|
||||
return IrisBiomeLayerGenerator.generateLayers(this, dim, wx, wz, random, maxDepth, height, rdata, complex);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
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 Listener<T> listener;
|
||||
|
||||
@@ -35,6 +35,12 @@ public final class ColumnExtentListeningHunk<T> implements Hunk<T>, ColumnExtent
|
||||
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
|
||||
public T getRaw(int x, int y, int z) {
|
||||
return source.getRaw(x, y, z);
|
||||
@@ -63,5 +69,11 @@ public final class ColumnExtentListeningHunk<T> implements Hunk<T>, ColumnExtent
|
||||
@FunctionalInterface
|
||||
public interface Listener<T> {
|
||||
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();
|
||||
|
||||
public MultiBurst() {
|
||||
this("Iris");
|
||||
this("Iris", Thread.MIN_PRIORITY, MultiBurstSupport::performanceProcessors);
|
||||
}
|
||||
|
||||
public MultiBurst(String name) {
|
||||
@@ -140,8 +140,15 @@ public class MultiBurst extends MultiBurstSupport {
|
||||
|
||||
static final class HydrologyBurst extends MultiBurst {
|
||||
HydrologyBurst() {
|
||||
super("Iris Hydrology", () -> Math.max(2,
|
||||
IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism())));
|
||||
super("Iris Hydrology", HydrologyBurst::plannerParallelism);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -17,8 +17,6 @@ import art.arcane.volmlib.nativelib.terrain.NativeBiome;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class ChunkContext {
|
||||
@@ -95,38 +93,11 @@ public class ChunkContext {
|
||||
if (hydrologyRuntime != null && (resolvedPlan.height || resolvedPlan.biome)) {
|
||||
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();
|
||||
}
|
||||
if (shouldPrefillAsync(resolvedPlan, complex.isPlatformMainThread())) {
|
||||
prefillAsync(resolvedPlan);
|
||||
} else {
|
||||
List<CompletableFuture<Void>> futures = new ArrayList<>(fillTasks.size());
|
||||
for (Runnable fillTask : fillTasks) {
|
||||
futures.add(CompletableFuture.runAsync(fillTask, MultiBurst.burst));
|
||||
}
|
||||
for (CompletableFuture<Void> future : futures) {
|
||||
future.join();
|
||||
}
|
||||
prefillSurface(resolvedPlan);
|
||||
prefillPalettes(resolvedPlan);
|
||||
}
|
||||
if (capturePrefillMetric) {
|
||||
metrics.getContextPrefill().put((System.nanoTime() - totalStartNanos) / 1_000_000D);
|
||||
@@ -134,14 +105,46 @@ public class ChunkContext {
|
||||
}
|
||||
}
|
||||
|
||||
static boolean shouldPrefillAsync(PrefillPlan prefillPlan, int fillTaskCount) {
|
||||
if (prefillPlan == PrefillPlan.NATURAL_TERRAIN || fillTaskCount <= 1 || !IrisPlatforms.isBound()) {
|
||||
static boolean shouldPrefillAsync(PrefillPlan prefillPlan, boolean platformMainThread) {
|
||||
if (!platformMainThread || prefillPlan == PrefillPlan.NATURAL_TERRAIN || prefillPlan == PrefillPlan.NONE
|
||||
|| !IrisPlatforms.isBound()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !MultiBurst.burst.ownsCurrentThread();
|
||||
}
|
||||
|
||||
private void prefillSurface(PrefillPlan prefillPlan) {
|
||||
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) {
|
||||
this.terrainBiomes = terrainBiomes;
|
||||
}
|
||||
@@ -295,17 +298,4 @@ public class ChunkContext {
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+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.math.BlockPosition;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -57,6 +58,7 @@ import lombok.experimental.Accessors;
|
||||
public class IrisDepositGenerator {
|
||||
private static final int FOOTPRINT_MAXIMUM_SIZE = 64;
|
||||
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_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.
|
||||
*/
|
||||
public ClumpFootprint sampleClumpFootprint(RNG rng, IrisData rdata) {
|
||||
if (shape != IrisDepositShape.VANILLA_ELLIPSOID || minSize > FOOTPRINT_MAXIMUM_SIZE
|
||||
|| maxSize > FOOTPRINT_MAXIMUM_SIZE) {
|
||||
return null;
|
||||
}
|
||||
int paletteSize = getBlockData(rdata).size();
|
||||
if (paletteSize == 0) {
|
||||
if (!usesClumpFootprints(rdata)) {
|
||||
return null;
|
||||
}
|
||||
KList<NativeBlockState> resolvedPalette = getBlockData(rdata);
|
||||
int paletteSize = resolvedPalette.size();
|
||||
int size = rng.i(minSize, maxSize + 1);
|
||||
if (size <= 0) {
|
||||
return ClumpFootprint.EMPTY;
|
||||
}
|
||||
FootprintCells cells = FOOTPRINT_CELLS.get();
|
||||
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) {
|
||||
return ClumpFootprint.EMPTY;
|
||||
}
|
||||
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.
|
||||
*/
|
||||
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 reach = size / 8F;
|
||||
double startX = Math.sin(angle) * reach;
|
||||
double endX = -Math.sin(angle) * reach;
|
||||
double startZ = Math.cos(angle) * reach;
|
||||
double endZ = -Math.cos(angle) * reach;
|
||||
double sine = Math.sin(angle);
|
||||
double cosine = Math.cos(angle);
|
||||
double startX = sine * reach;
|
||||
double endX = -sine * reach;
|
||||
double startZ = cosine * reach;
|
||||
double endZ = -cosine * reach;
|
||||
double startY = 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++) {
|
||||
float progress = (float) i / size;
|
||||
@@ -328,7 +426,8 @@ public class IrisDepositGenerator {
|
||||
nodes[i * 4] = startX + (endX - startX) * progress;
|
||||
nodes[i * 4 + 1] = startY + (endY - startY) * 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++) {
|
||||
@@ -353,59 +452,20 @@ public class IrisDepositGenerator {
|
||||
}
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
double[] squaredY = new double[16];
|
||||
double[] squaredZ = new double[16];
|
||||
private static double[][] nodeSines() {
|
||||
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++) {
|
||||
double radius = nodes[i * 4 + 3];
|
||||
if (radius < 0D) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
float progress = (float) i / size;
|
||||
values[i] = Math.sin(Math.PI * progress);
|
||||
}
|
||||
sines[size] = values;
|
||||
}
|
||||
return sines;
|
||||
}
|
||||
|
||||
IrisObject generateVanillaScattered(RNG rng, IrisData rdata, int size) {
|
||||
@@ -630,8 +690,134 @@ public class IrisDepositGenerator {
|
||||
void add(int x, int y, int z);
|
||||
}
|
||||
|
||||
public record ClumpFootprint(int width, int minY, boolean empty) {
|
||||
static final ClumpFootprint EMPTY = new ClumpFootprint(1, 0, true);
|
||||
public static final class ClumpFootprint {
|
||||
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 {
|
||||
@@ -639,10 +825,16 @@ public class IrisDepositGenerator {
|
||||
private static final int AXIS = 64;
|
||||
private final long[] occupied = new long[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 count;
|
||||
private int extentX;
|
||||
private int minY;
|
||||
private boolean uniform;
|
||||
private long stamp;
|
||||
|
||||
private void reset() {
|
||||
for (int index = 0; index < touchedCount; index++) {
|
||||
@@ -652,6 +844,8 @@ public class IrisDepositGenerator {
|
||||
count = 0;
|
||||
extentX = 0;
|
||||
minY = Integer.MAX_VALUE;
|
||||
uniform = false;
|
||||
stamp++;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -667,9 +861,72 @@ public class IrisDepositGenerator {
|
||||
touched[touchedCount++] = word;
|
||||
}
|
||||
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));
|
||||
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;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.pack.value.IrisRange;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
@@ -79,6 +80,14 @@ public final class IrisOreBands {
|
||||
return minY <= maxY;
|
||||
}
|
||||
|
||||
public int lowestY() {
|
||||
return minY;
|
||||
}
|
||||
|
||||
public int highestY() {
|
||||
return maxY;
|
||||
}
|
||||
|
||||
public boolean contains(int y) {
|
||||
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.
|
||||
*/
|
||||
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];
|
||||
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) {
|
||||
return ore;
|
||||
}
|
||||
|
||||
@@ -65,6 +65,10 @@ public class IrisOreGenerator {
|
||||
}
|
||||
|
||||
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)) {
|
||||
return null;
|
||||
}
|
||||
@@ -73,7 +77,7 @@ public class IrisOreGenerator {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (chance(rng, data).sample(x, y, z) > threshold) {
|
||||
if (chance(rng, data, engine).sample(x, y, z) > threshold) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -81,8 +85,11 @@ public class IrisOreGenerator {
|
||||
}
|
||||
|
||||
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();
|
||||
Engine engine = data == null ? null : data.getEngine();
|
||||
CachedChance recent = recentChance;
|
||||
if (recent != null && recent.key.seed == seed && recent.key.data == data && recent.key.engine == engine) {
|
||||
return recent.chance;
|
||||
|
||||
+28
-7
@@ -20,14 +20,29 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.ForkJoinWorkerThread;
|
||||
|
||||
final class HydrologyCrossTileResolver {
|
||||
private static final int FALLBACK_TRIAL_PARALLELISM = 16;
|
||||
private static final int MINIMUM_FALLBACK_TRIAL_BATCH = 2;
|
||||
private final HydrologyPlanner planner;
|
||||
private final int trialLimit;
|
||||
|
||||
HydrologyCrossTileResolver(HydrologyPlanner planner) {
|
||||
HydrologyCrossTileResolver(HydrologyPlanner planner, int trialLimit) {
|
||||
if (trialLimit < 1) {
|
||||
throw new IllegalArgumentException("Hydrology fallback trial limit must be positive.");
|
||||
}
|
||||
this.planner = planner;
|
||||
this.trialLimit = trialLimit;
|
||||
}
|
||||
|
||||
static int fallbackTrialBatch(int parallelism, int idleWorkers, int trialLimit) {
|
||||
if (parallelism <= 2) {
|
||||
return 1;
|
||||
}
|
||||
int workers = Math.clamp(idleWorkers + 1L, MINIMUM_FALLBACK_TRIAL_BATCH, parallelism);
|
||||
return Math.max(1, Math.min(workers, Math.min(trialLimit, FALLBACK_TRIAL_PARALLELISM)));
|
||||
}
|
||||
|
||||
HydrologyOwnerDraft compileOwnerDraft(
|
||||
@@ -285,12 +300,18 @@ final class HydrologyCrossTileResolver {
|
||||
List<OutletCandidate> outlets,
|
||||
int start
|
||||
) {
|
||||
int parallelism = HydrologyPlanningAdmission.effectiveParallelism(
|
||||
Thread.currentThread() instanceof ForkJoinWorkerThread worker
|
||||
? worker.getPool().getParallelism()
|
||||
: IrisPlatforms.isBound() ? MultiBurst.hydrology.parallelism() : 1);
|
||||
int batchSize = parallelism <= 2 ? 1 : Math.max(1, Math.min(HydrologyPlanningAdmission.maximumTrials(),
|
||||
Math.min(FALLBACK_TRIAL_PARALLELISM, parallelism)));
|
||||
int parallelism;
|
||||
int idleWorkers;
|
||||
if (Thread.currentThread() instanceof ForkJoinWorkerThread worker) {
|
||||
ForkJoinPool pool = worker.getPool();
|
||||
parallelism = HydrologyPlanningAdmission.effectiveParallelism(pool.getParallelism());
|
||||
idleWorkers = pool.getParallelism() - pool.getActiveThreadCount();
|
||||
} else {
|
||||
parallelism = HydrologyPlanningAdmission.effectiveParallelism(
|
||||
IrisPlatforms.isBound() ? MultiBurst.hydrology.parallelism() : 1);
|
||||
idleWorkers = 0;
|
||||
}
|
||||
int batchSize = fallbackTrialBatch(parallelism, idleWorkers, trialLimit);
|
||||
int end = Math.min(outlets.size(), start + batchSize);
|
||||
ArrayList<Callable<FallbackTrial>> tasks = new ArrayList<>(end - start);
|
||||
HydrologyPlanner.PlanningSamples ownerSamples = planner.planningSamples.get();
|
||||
|
||||
@@ -197,7 +197,7 @@ public final class HydrologyPlanner {
|
||||
geometrySampler,
|
||||
settings.seaLevel()
|
||||
);
|
||||
this.crossTile = new HydrologyCrossTileResolver(this);
|
||||
this.crossTile = new HydrologyCrossTileResolver(this, HydrologyPlanningAdmission.maximumTrials());
|
||||
this.outletPlanner = new HydrologyOutletPlanner(this);
|
||||
this.sourcePlanner = new HydrologySourcePlanner(this);
|
||||
this.routeGeometry = new HydrologyRouteGeometry(this);
|
||||
|
||||
+3
-1
@@ -10,6 +10,8 @@ import java.util.function.BooleanSupplier;
|
||||
|
||||
final class HydrologyPlanningAdmission {
|
||||
private static final long BYTES_PER_ROOT = 2L * 1024L * 1024L * 1024L;
|
||||
private static final long BYTES_PER_TRIAL = 128L * 1024L * 1024L;
|
||||
private static final int MAXIMUM_TRIAL_LIMIT = 16;
|
||||
private static final int MAXIMUM_ROOTS = rootLimit(
|
||||
Runtime.getRuntime().maxMemory(), Runtime.getRuntime().availableProcessors());
|
||||
private static final int MAXIMUM_TRIALS = trialLimit(Runtime.getRuntime().maxMemory(), MAXIMUM_ROOTS);
|
||||
@@ -48,7 +50,7 @@ final class HydrologyPlanningAdmission {
|
||||
if (maximumRoots < 1) {
|
||||
throw new IllegalArgumentException("Maximum hydrology roots must be positive.");
|
||||
}
|
||||
return (int) Math.clamp(maximumHeap / maximumRoots / (1024L * 1024L * 1024L), 1L, 16L);
|
||||
return (int) Math.clamp(maximumHeap / maximumRoots / BYTES_PER_TRIAL, 1L, MAXIMUM_TRIAL_LIMIT);
|
||||
}
|
||||
|
||||
static int rootLimit(long maximumHeap, int processors) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.runtime.GenerationClosedException;
|
||||
import art.arcane.iris.generation.runtime.GenerationCounter;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||
@@ -43,7 +44,7 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
private static final byte AROUND_EMPTY = 1;
|
||||
private static final byte AROUND_PUBLISHED = 2;
|
||||
private static final int MAXIMUM_PREGENERATION_TILES = 4096;
|
||||
private static final int PREGENERATION_HALO_BLOCKS = 512;
|
||||
private static final int PREGENERATION_HALO_BLOCKS = 256;
|
||||
private static final HydrologyColumnSample[] NO_COLUMNS = new HydrologyColumnSample[CHUNK_COLUMN_COUNT];
|
||||
private static final Cache<SharedTileKey, HydrologyTile> SHARED_TILES = Caffeine.newBuilder()
|
||||
.maximumWeight(HydrologyCacheBudget.sharedStudioBytes())
|
||||
@@ -79,7 +80,6 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
private volatile SharedCacheScope sharedCacheScope;
|
||||
private volatile PreparedHydrologyTileStore persistentStore;
|
||||
private volatile boolean neighbourPrefetchEnabled;
|
||||
private volatile Runnable terrainPreparation;
|
||||
private PregenerationScope pregenerationScope;
|
||||
private int demandBatches;
|
||||
private int activePrefetches;
|
||||
@@ -267,7 +267,6 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
private List<HydrologyDiagnosticCandidate> planDiagnostics(HydrologyTileKey key, long epoch) {
|
||||
HydrologyTile tile = planInline(key, epoch);
|
||||
try (HydrologyPlanningAdmission.Permit ignored = HydrologyPlanningAdmission.acquireRoot(closed::get)) {
|
||||
prepareTerrain();
|
||||
return List.copyOf(planner.diagnosticCandidates(tile));
|
||||
}
|
||||
}
|
||||
@@ -458,7 +457,6 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
/** Plans the tile under a root permit; a persisted plan is restored before any permit is taken. */
|
||||
private HydrologyTile planAdmitted(HydrologyTileKey key, long planningEpoch,
|
||||
SharedTileKey sharedKey, PreparedHydrologyTileStore store) {
|
||||
prepareTerrain();
|
||||
HydrologyTile planned = planner.plan(key);
|
||||
synchronized (publicationLock) {
|
||||
if (sharedKey != null && !closed.get() && planningEpoch == cacheEpoch.get()) {
|
||||
@@ -499,6 +497,9 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
}
|
||||
|
||||
public Optional<HydrologyColumnSample> columnAt(int blockX, int blockZ) {
|
||||
if (GenerationCounter.ENABLED) {
|
||||
GenerationCounter.HYDROLOGY_LOOKUPS.increment();
|
||||
}
|
||||
return chunkColumns(blockX, blockZ).columnAt(blockX, blockZ);
|
||||
}
|
||||
|
||||
@@ -507,6 +508,9 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
if (!waitsForbidden()) {
|
||||
return HydrologyColumnSnapshot.ready(columnAt(blockX, blockZ).orElse(null));
|
||||
}
|
||||
if (GenerationCounter.ENABLED) {
|
||||
GenerationCounter.HYDROLOGY_LOOKUPS.increment();
|
||||
}
|
||||
int chunkX = Math.floorDiv(blockX, CHUNK_SIZE);
|
||||
int chunkZ = Math.floorDiv(blockZ, CHUNK_SIZE);
|
||||
ChunkColumns local = localChunks(cacheEpoch.get()).get(chunkX, chunkZ);
|
||||
@@ -770,17 +774,6 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
clear();
|
||||
}
|
||||
|
||||
public void setTerrainPreparation(Runnable preparation) {
|
||||
terrainPreparation = Objects.requireNonNull(preparation, "terrainPreparation");
|
||||
}
|
||||
|
||||
private void prepareTerrain() {
|
||||
Runnable preparation = terrainPreparation;
|
||||
if (preparation != null) {
|
||||
preparation.run();
|
||||
}
|
||||
}
|
||||
|
||||
private static void collectActiveLoad(List<CompletableFuture<?>> active, PendingLoad<?> load) {
|
||||
if (load.owner() == Thread.currentThread()) {
|
||||
throw new IllegalStateException("Hydrology tile cache cannot close from an active load.");
|
||||
|
||||
-1
@@ -123,7 +123,6 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
IrisPlatforms.isBound() ? MultiBurst.hydrology : null,
|
||||
context.waitingForbidden()
|
||||
);
|
||||
this.cache.setTerrainPreparation(context.terrainPreparation());
|
||||
IrisLogging.debug("Hydrology runtime: tileSize=%d publicationRadius=%d planningThreads=%d",
|
||||
settings.routing().tileSize(), settings.publicationRadius(), MultiBurst.hydrology.parallelism());
|
||||
}
|
||||
|
||||
+1
-3
@@ -17,8 +17,7 @@ public record IrisHydrologyRuntimeContext(
|
||||
IrisHydrologyNaturalHeightDescriber naturalHeightDescriber,
|
||||
IrisHydrologyNaturalOceanClassifier naturalOceanClassifier,
|
||||
HydrologyCaveVoxelViewFactory caveViewFactory,
|
||||
BooleanSupplier waitingForbidden,
|
||||
Runnable terrainPreparation
|
||||
BooleanSupplier waitingForbidden
|
||||
) {
|
||||
/**
|
||||
* {@code waitingForbidden} is true on a thread that must never wait for a cold hydrology plan,
|
||||
@@ -37,6 +36,5 @@ public record IrisHydrologyRuntimeContext(
|
||||
Objects.requireNonNull(naturalOceanClassifier);
|
||||
Objects.requireNonNull(caveViewFactory);
|
||||
Objects.requireNonNull(waitingForbidden);
|
||||
Objects.requireNonNull(terrainPreparation);
|
||||
}
|
||||
}
|
||||
+80
-38
@@ -18,6 +18,7 @@ import it.unimi.dsi.fastutil.longs.LongIterator;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class ErosionFieldCompiler {
|
||||
@@ -159,44 +160,7 @@ public final class ErosionFieldCompiler {
|
||||
IntArrayList stations = geometry.stations;
|
||||
DoubleArrayList distance = geometry.distance;
|
||||
if (!geometry.prepared) {
|
||||
for (int station = 0; station < count; station++) {
|
||||
int radius = prepared.radii()[station];
|
||||
int stationX = centerline.x()[station];
|
||||
int stationZ = centerline.z()[station];
|
||||
if (samplingBounds != null && !samplingBounds.intersects(stationX, stationZ, radius)) {
|
||||
continue;
|
||||
}
|
||||
int minimumZ = samplingBounds == null ? -radius : Math.max(-radius, samplingBounds.minimumZ() - stationZ);
|
||||
int maximumZ = samplingBounds == null ? radius : Math.min(radius, samplingBounds.maximumZ() - stationZ);
|
||||
int minimumX = samplingBounds == null ? -radius : Math.max(-radius, samplingBounds.minimumX() - stationX);
|
||||
int maximumX = samplingBounds == null ? radius : Math.min(radius, samplingBounds.maximumX() - stationX);
|
||||
for (int deltaZ = minimumZ; deltaZ <= maximumZ; deltaZ++) {
|
||||
for (int deltaX = minimumX; deltaX <= maximumX; deltaX++) {
|
||||
int cellX = stationX + deltaX;
|
||||
int cellZ = stationZ + deltaZ;
|
||||
double cellDistance = centerline.distanceToSegment(station, cellX, cellZ, SurfaceDistanceTable.SHARED);
|
||||
if (cellDistance > radius) {
|
||||
continue;
|
||||
}
|
||||
long key = RiverFootprint.pack(cellX, cellZ);
|
||||
int index = indices.putIfAbsent(key, stations.size());
|
||||
if (index < 0) {
|
||||
stations.add(station);
|
||||
distance.add(cellDistance);
|
||||
continue;
|
||||
}
|
||||
int existing = stations.getInt(index);
|
||||
double existingDistance = distance.getDouble(index);
|
||||
// A cell on a station's cross-section row ties between the two segments meeting
|
||||
// there; the later station owns it so the row uses the head its own ring bounded.
|
||||
if (cellDistance < existingDistance - EPSILON
|
||||
|| Math.abs(cellDistance - existingDistance) <= EPSILON && station > existing) {
|
||||
stations.set(index, station);
|
||||
distance.set(index, cellDistance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
rasterize(centerline, prepared.radii(), count, samplingBounds, indices, stations, distance);
|
||||
geometry.prepared = true;
|
||||
}
|
||||
int oceanStart = oceanStart(centerline, valley, terminal);
|
||||
@@ -912,12 +876,90 @@ public final class ErosionFieldCompiler {
|
||||
return 1D - t * t * (3D - 2D * t);
|
||||
}
|
||||
|
||||
static void rasterize(SurfaceCenterline centerline, int[] radii, int count, SurfaceBounds samplingBounds,
|
||||
Long2IntOpenHashMap indices, IntArrayList stations, DoubleArrayList distance) {
|
||||
CellPages pages = new CellPages();
|
||||
LongArrayList order = new LongArrayList();
|
||||
for (int station = 0; station < count; station++) {
|
||||
int radius = radii[station];
|
||||
int stationX = centerline.x()[station];
|
||||
int stationZ = centerline.z()[station];
|
||||
if (samplingBounds != null && !samplingBounds.intersects(stationX, stationZ, radius)) {
|
||||
continue;
|
||||
}
|
||||
int minimumZ = samplingBounds == null ? -radius : Math.max(-radius, samplingBounds.minimumZ() - stationZ);
|
||||
int maximumZ = samplingBounds == null ? radius : Math.min(radius, samplingBounds.maximumZ() - stationZ);
|
||||
int minimumX = samplingBounds == null ? -radius : Math.max(-radius, samplingBounds.minimumX() - stationX);
|
||||
int maximumX = samplingBounds == null ? radius : Math.min(radius, samplingBounds.maximumX() - stationX);
|
||||
for (int deltaZ = minimumZ; deltaZ <= maximumZ; deltaZ++) {
|
||||
for (int deltaX = minimumX; deltaX <= maximumX; deltaX++) {
|
||||
int cellX = stationX + deltaX;
|
||||
int cellZ = stationZ + deltaZ;
|
||||
double cellDistance = centerline.distanceToSegment(station, cellX, cellZ, SurfaceDistanceTable.SHARED);
|
||||
if (cellDistance > radius) {
|
||||
continue;
|
||||
}
|
||||
int[] page = pages.page(cellX, cellZ);
|
||||
int offset = CellPages.offset(cellX, cellZ);
|
||||
int index = page[offset];
|
||||
if (index < 0) {
|
||||
page[offset] = stations.size();
|
||||
stations.add(station);
|
||||
distance.add(cellDistance);
|
||||
order.add(RiverFootprint.pack(cellX, cellZ));
|
||||
continue;
|
||||
}
|
||||
int existing = stations.getInt(index);
|
||||
double existingDistance = distance.getDouble(index);
|
||||
// A cell on a station's cross-section row ties between the two segments meeting
|
||||
// there; the later station owns it so the row uses the head its own ring bounded.
|
||||
if (cellDistance < existingDistance - EPSILON
|
||||
|| Math.abs(cellDistance - existingDistance) <= EPSILON && station > existing) {
|
||||
stations.set(index, station);
|
||||
distance.set(index, cellDistance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < order.size(); index++) {
|
||||
indices.put(order.getLong(index), index);
|
||||
}
|
||||
}
|
||||
|
||||
private static double side(SurfaceCenterline centerline, int station, int cellX, int cellZ) {
|
||||
double deltaX = cellX - centerline.x()[station];
|
||||
double deltaZ = cellZ - centerline.z()[station];
|
||||
return deltaX * centerline.normalX(station) + deltaZ * centerline.normalZ(station);
|
||||
}
|
||||
|
||||
private static final class CellPages {
|
||||
private static final int SHIFT = 6;
|
||||
private static final int MASK = (1 << SHIFT) - 1;
|
||||
private final Long2ObjectOpenHashMap<int[]> pages = new Long2ObjectOpenHashMap<>();
|
||||
private long currentKey;
|
||||
private int[] current;
|
||||
|
||||
private int[] page(int x, int z) {
|
||||
long key = RiverFootprint.pack(x >> SHIFT, z >> SHIFT);
|
||||
if (current != null && key == currentKey) {
|
||||
return current;
|
||||
}
|
||||
int[] page = pages.get(key);
|
||||
if (page == null) {
|
||||
page = new int[1 << (SHIFT * 2)];
|
||||
Arrays.fill(page, -1);
|
||||
pages.put(key, page);
|
||||
}
|
||||
current = page;
|
||||
currentKey = key;
|
||||
return page;
|
||||
}
|
||||
|
||||
private static int offset(int x, int z) {
|
||||
return ((z & MASK) << SHIFT) | (x & MASK);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RasterGeometry {
|
||||
private final Long2IntOpenHashMap indices = new Long2IntOpenHashMap();
|
||||
private final IntArrayList stations = new IntArrayList();
|
||||
|
||||
@@ -66,6 +66,24 @@ final class CaveCarveScratch {
|
||||
final int[] tileLocalX = new int[4];
|
||||
final int[] tileLocalZ = new int[4];
|
||||
final int[] tileTopY = new int[4];
|
||||
final long[] rowCarveBits = new long[4];
|
||||
final long[] cellPassBits = new long[4];
|
||||
final CaveDensityLattice cellLattice = new CaveDensityLattice();
|
||||
final double[] cellThresholdMin = new double[4];
|
||||
final double[] cellThresholdMax = new double[4];
|
||||
final double[] cellThresholdMaxBoost = new double[4];
|
||||
final int[] cellBreakFloorMin = new int[4];
|
||||
final int[] cellCeilingFadeStart = new int[4];
|
||||
final int[] cellTopY = new int[4];
|
||||
final int[] cellMinTopY = new int[4];
|
||||
final boolean[] cellProtected = new boolean[4];
|
||||
final int[] columnProtectStartY = new int[256];
|
||||
final int[] columnProtectEndY = new int[256];
|
||||
int[] rowActiveCount = new int[0];
|
||||
long solidCells;
|
||||
long openCells;
|
||||
long mixedCells;
|
||||
long exactRows;
|
||||
CaveFieldModuleState[] activeModules = new CaveFieldModuleState[0];
|
||||
double[] activeModuleRemainingMin = new double[0];
|
||||
double[] activeModuleRemainingMax = new double[0];
|
||||
@@ -84,5 +102,6 @@ final class CaveCarveScratch {
|
||||
Arrays.fill(sectionMatter, null);
|
||||
Arrays.fill(sectionSlices, null);
|
||||
Arrays.fill(densityColumns, null);
|
||||
cellLattice.releaseReferences();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
final class CaveCellKernel {
|
||||
static final int NONE = 0;
|
||||
static final int ALL = 1;
|
||||
static final int MIXED = 2;
|
||||
private static final double GUARD_ULPS = 32D;
|
||||
private static final double MAXIMUM_MAGNITUDE = 0x1p500;
|
||||
|
||||
private CaveCellKernel() {
|
||||
}
|
||||
|
||||
static int decide(double d00, double d01, double d10, double d11, double thresholdLow, double thresholdHigh) {
|
||||
double low = Math.min(Math.min(d00, d01), Math.min(d10, d11));
|
||||
double high = Math.max(Math.max(d00, d01), Math.max(d10, d11));
|
||||
double magnitude = Math.max(Math.abs(low), Math.abs(high));
|
||||
if (!(magnitude < MAXIMUM_MAGNITUDE)) {
|
||||
return MIXED;
|
||||
}
|
||||
double guard = Math.ulp(magnitude) * GUARD_ULPS;
|
||||
if (high + guard <= thresholdLow) {
|
||||
return ALL;
|
||||
}
|
||||
if (low - guard > thresholdHigh) {
|
||||
return NONE;
|
||||
}
|
||||
return MIXED;
|
||||
}
|
||||
|
||||
static double threshold(double localThreshold, double normalizationFactor, double inverseNormalization) {
|
||||
return (localThreshold * normalizationFactor) * inverseNormalization;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import art.arcane.iris.generation.cache.ConcurrentClockCache;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.VarHandle;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||
|
||||
@@ -22,42 +23,63 @@ final class CaveDensityGrid {
|
||||
private static final AtomicInteger OWNERS = new AtomicInteger();
|
||||
|
||||
private final ConcurrentClockCache<Column> columns;
|
||||
private final ConcurrentClockCache<Column> warpColumns;
|
||||
private final ConcurrentHashMap<Object, Owner> owners = new ConcurrentHashMap<>();
|
||||
|
||||
CaveDensityGrid(int capacity) {
|
||||
columns = new ConcurrentClockCache<>(capacity);
|
||||
warpColumns = new ConcurrentClockCache<>(Math.max(1, capacity >> 2));
|
||||
}
|
||||
|
||||
static int nextOwner() {
|
||||
return OWNERS.getAndIncrement();
|
||||
Owner owner(Object fieldKey) {
|
||||
return owners.computeIfAbsent(fieldKey, ignored -> new Owner(OWNERS.getAndIncrement()));
|
||||
}
|
||||
|
||||
Column column(Object owner, int ownerId, int height, int latticeX, int latticeZ) {
|
||||
long key = (long) (ownerId & 0xFFFF) << 48 | (latticeX & COORDINATE_MASK) << 24 | (latticeZ & COORDINATE_MASK);
|
||||
Column cached = columns.get(key);
|
||||
if (cached == null) {
|
||||
cached = columns.putIfAbsent(key, new Column(owner, latticeX, latticeZ, height));
|
||||
Column column(Owner owner, int height, int latticeX, int latticeZ) {
|
||||
return column(columns, owner, height, latticeX, latticeZ);
|
||||
}
|
||||
return cached.matches(owner, latticeX, latticeZ) ? cached : null;
|
||||
|
||||
Column warpColumn(Owner owner, int height, int latticeX, int latticeZ) {
|
||||
return column(warpColumns, owner, height, latticeX, latticeZ);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
columns.clear();
|
||||
warpColumns.clear();
|
||||
owners.clear();
|
||||
}
|
||||
|
||||
private static Column column(ConcurrentClockCache<Column> cache, Owner owner, int height, int latticeX, int latticeZ) {
|
||||
long key = (long) (owner.id & 0xFFFF) << 48 | (latticeX & COORDINATE_MASK) << 24 | (latticeZ & COORDINATE_MASK);
|
||||
Column cached = cache.get(key);
|
||||
if (cached == null) {
|
||||
cached = cache.putIfAbsent(key, new Column(owner, latticeX, latticeZ, height));
|
||||
}
|
||||
return cached.matches(owner, latticeX, latticeZ) ? cached : null;
|
||||
}
|
||||
|
||||
static final class Owner {
|
||||
private final int id;
|
||||
|
||||
private Owner(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
static final class Column {
|
||||
private final Object owner;
|
||||
private final Owner owner;
|
||||
private final int latticeX;
|
||||
private final int latticeZ;
|
||||
private final AtomicReferenceArray<double[]> segments;
|
||||
|
||||
private Column(Object owner, int latticeX, int latticeZ, int height) {
|
||||
private Column(Owner owner, int latticeX, int latticeZ, int height) {
|
||||
this.owner = owner;
|
||||
this.latticeX = latticeX;
|
||||
this.latticeZ = latticeZ;
|
||||
segments = new AtomicReferenceArray<>(Math.ceilDiv(Math.max(1, height), SEGMENT_SIZE));
|
||||
}
|
||||
|
||||
boolean matches(Object owner, int latticeX, int latticeZ) {
|
||||
boolean matches(Owner owner, int latticeX, int latticeZ) {
|
||||
return this.owner == owner && this.latticeX == latticeX && this.latticeZ == latticeZ;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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.mantle;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
final class CaveDensityLattice {
|
||||
static final int CORNERS = 9;
|
||||
|
||||
final CaveDensityGrid.Column[] densityColumns = new CaveDensityGrid.Column[CORNERS];
|
||||
final CaveDensityGrid.Column[] warpColumns = new CaveDensityGrid.Column[CORNERS];
|
||||
private double[] values = new double[0];
|
||||
private byte[] rowCells = new byte[0];
|
||||
private short[] rowCorners = new short[0];
|
||||
private int minimumY;
|
||||
private int rows;
|
||||
|
||||
void reset(int minimumY, int rows) {
|
||||
this.minimumY = minimumY;
|
||||
this.rows = rows;
|
||||
if (rowCells.length < rows) {
|
||||
rowCells = new byte[rows];
|
||||
rowCorners = new short[rows];
|
||||
}
|
||||
if (values.length < rows * CORNERS) {
|
||||
values = new double[rows * CORNERS];
|
||||
}
|
||||
Arrays.fill(rowCells, 0, rows, (byte) 0);
|
||||
Arrays.fill(rowCorners, 0, rows, (short) 0);
|
||||
}
|
||||
|
||||
void markRow(int y, int cells, int corners) {
|
||||
int row = y - minimumY;
|
||||
rowCells[row] = (byte) cells;
|
||||
rowCorners[row] = (short) corners;
|
||||
}
|
||||
|
||||
int cells(int y) {
|
||||
return rowCells[y - minimumY];
|
||||
}
|
||||
|
||||
boolean needs(int corner, int y) {
|
||||
return (rowCorners[y - minimumY] & (1 << corner)) != 0;
|
||||
}
|
||||
|
||||
double density(int corner, int y) {
|
||||
return values[corner * rows + (y - minimumY)];
|
||||
}
|
||||
|
||||
void store(int corner, int y, double density) {
|
||||
values[corner * rows + (y - minimumY)] = density;
|
||||
}
|
||||
|
||||
void releaseReferences() {
|
||||
Arrays.fill(densityColumns, null);
|
||||
Arrays.fill(warpColumns, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.mantle;
|
||||
|
||||
import art.arcane.volmlib.util.noise.program.NoiseProgram;
|
||||
import art.arcane.volmlib.util.noise.program.PrimitiveKind;
|
||||
|
||||
record CaveFieldKey(long signature, long probe, NoiseProgram identity) {
|
||||
private static final double[][] PROBE_POINTS = {
|
||||
{0.5D, 3.25D, -7.75D},
|
||||
{-1031.125D, 77.5D, 2049.875D},
|
||||
{16D, 255D, -16D},
|
||||
{123_456.375D, -40.25D, -98_765.625D}
|
||||
};
|
||||
|
||||
static CaveFieldKey of(NoiseProgram program) {
|
||||
if (!shareable(program)) {
|
||||
return new CaveFieldKey(program.structuralSignature(), 0L, program);
|
||||
}
|
||||
long probe = 0xCBF29CE484222325L;
|
||||
for (double[] point : PROBE_POINTS) {
|
||||
probe = (probe ^ Double.doubleToRawLongBits(program.sampleSigned3D(point[0], point[1], point[2]))) * 0x100000001B3L;
|
||||
}
|
||||
return new CaveFieldKey(program.structuralSignature(), probe, null);
|
||||
}
|
||||
|
||||
private static boolean shareable(NoiseProgram program) {
|
||||
for (int node = 0; node < program.nodeCount(); node++) {
|
||||
if (program.nodeOpaque(node) || program.nodeKernel(node).kind() == PrimitiveKind.GENERIC) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,10 @@ package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.generation.cave.IrisCaveFieldModule;
|
||||
import art.arcane.iris.pack.value.IrisRange;
|
||||
import art.arcane.volmlib.util.noise.CNG;
|
||||
import art.arcane.volmlib.util.noise.program.NoiseProgram;
|
||||
|
||||
final class CaveFieldModuleState {
|
||||
final CNG density;
|
||||
final NoiseProgram density;
|
||||
final int minY;
|
||||
final int maxY;
|
||||
final double weight;
|
||||
@@ -32,7 +32,7 @@ final class CaveFieldModuleState {
|
||||
final double minContribution;
|
||||
final double maxContribution;
|
||||
|
||||
CaveFieldModuleState(IrisCaveFieldModule module, CNG density) {
|
||||
CaveFieldModuleState(IrisCaveFieldModule module, NoiseProgram density) {
|
||||
IrisRange range = module.getVerticalRange();
|
||||
this.density = density;
|
||||
this.minY = (int) Math.floor(range.getMin());
|
||||
@@ -47,12 +47,12 @@ final class CaveFieldModuleState {
|
||||
}
|
||||
|
||||
double sample(double x, int y, double z) {
|
||||
double sampled = density.noiseFastSigned3D(x, y, z);
|
||||
double sampled = density.sampleSigned3D(x, y, z);
|
||||
return invert ? (threshold - sampled) * weight : (sampled - threshold) * weight;
|
||||
}
|
||||
|
||||
double sample(double x, double y, double z) {
|
||||
double sampled = density.noiseFastSigned3D(x, y, z);
|
||||
double sampled = density.sampleSigned3D(x, y, z);
|
||||
return invert ? (threshold - sampled) * weight : (sampled - threshold) * weight;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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.mantle;
|
||||
|
||||
import art.arcane.volmlib.util.noise.program.NoiseScratch;
|
||||
|
||||
final class CaveLatticeBatch {
|
||||
private static final ThreadLocal<CaveLatticeBatch> CURRENT = ThreadLocal.withInitial(CaveLatticeBatch::new);
|
||||
|
||||
final NoiseScratch noise = new NoiseScratch();
|
||||
final int[] cornerStart = new int[CaveDensityLattice.CORNERS + 1];
|
||||
final int[] snapStart = new int[CaveDensityLattice.CORNERS + 1];
|
||||
int[] y = new int[0];
|
||||
int[] snapY = new int[0];
|
||||
boolean[] snapMissing = new boolean[0];
|
||||
double[] warpA = new double[0];
|
||||
double[] warpB = new double[0];
|
||||
double[] warpedX = new double[0];
|
||||
double[] warpedY = new double[0];
|
||||
double[] warpedZ = new double[0];
|
||||
double[] density = new double[0];
|
||||
double[] values = new double[0];
|
||||
double[] pointX = new double[0];
|
||||
double[] pointY = new double[0];
|
||||
double[] pointZ = new double[0];
|
||||
int[] pointIndex = new int[0];
|
||||
private CaveLatticeBatch nested;
|
||||
private boolean active;
|
||||
|
||||
static CaveLatticeBatch acquire() {
|
||||
CaveLatticeBatch batch = CURRENT.get();
|
||||
while (batch.active) {
|
||||
if (batch.nested == null) {
|
||||
batch.nested = new CaveLatticeBatch();
|
||||
}
|
||||
batch = batch.nested;
|
||||
}
|
||||
batch.active = true;
|
||||
return batch;
|
||||
}
|
||||
|
||||
void release() {
|
||||
active = false;
|
||||
}
|
||||
|
||||
void ensure(int size) {
|
||||
if (y.length >= size) {
|
||||
return;
|
||||
}
|
||||
y = new int[size];
|
||||
snapY = new int[size];
|
||||
snapMissing = new boolean[size];
|
||||
warpA = new double[size];
|
||||
warpB = new double[size];
|
||||
warpedX = new double[size];
|
||||
warpedY = new double[size];
|
||||
warpedZ = new double[size];
|
||||
density = new double[size];
|
||||
values = new double[size * 2];
|
||||
pointX = new double[size * 2];
|
||||
pointY = new double[size * 2];
|
||||
pointZ = new double[size * 2];
|
||||
pointIndex = new int[size * 2];
|
||||
}
|
||||
}
|
||||
+102
-71
@@ -30,27 +30,41 @@ import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.math.BlockPosition;
|
||||
import art.arcane.iris.generation.block.B;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Arrays;
|
||||
|
||||
final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
private static final int CACHE_MISS = Integer.MIN_VALUE;
|
||||
private static final int POSITION_BITS = 21;
|
||||
private static final int POSITION_LIMIT = 1 << (POSITION_BITS - 1);
|
||||
private static final long POSITION_MASK = (1L << POSITION_BITS) - 1L;
|
||||
|
||||
private final IObjectPlacer delegate;
|
||||
private final Engine engine;
|
||||
private final int anchorY;
|
||||
private final int minDepthBelowSurface;
|
||||
private final int worldHeight;
|
||||
private final KList<BufferedMutation> mutations;
|
||||
private final Map<PositionKey, NativeBlockState> bufferedBlocks;
|
||||
private static final byte BLOCK = 0;
|
||||
private static final byte TILE = 1;
|
||||
private static final byte DATA = 2;
|
||||
private int[] mutationX = new int[64];
|
||||
private int[] mutationY = new int[64];
|
||||
private int[] mutationZ = new int[64];
|
||||
private Object[] mutationValues = new Object[64];
|
||||
private byte[] mutationKinds = new byte[64];
|
||||
private boolean[] mutationCanopies = new boolean[64];
|
||||
private int mutationCount;
|
||||
private final Long2ObjectOpenHashMap<NativeBlockState> bufferedBlocks;
|
||||
private final Long2IntOpenHashMap surfaceHeights;
|
||||
private final Long2IntOpenHashMap caveCeilings;
|
||||
private int blockWrites;
|
||||
private BlockPosition canopyPosition;
|
||||
private boolean originKnown;
|
||||
private int originX;
|
||||
private int originZ;
|
||||
|
||||
CaveObjectPlacementTransaction(IObjectPlacer delegate, int anchorY, int minDepthBelowSurface) {
|
||||
this.delegate = delegate;
|
||||
@@ -58,8 +72,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
this.anchorY = anchorY;
|
||||
this.minDepthBelowSurface = Math.max(0, minDepthBelowSurface);
|
||||
this.worldHeight = engine == null ? 0 : engine.getHeight();
|
||||
this.mutations = new KList<>();
|
||||
this.bufferedBlocks = new HashMap<>();
|
||||
this.bufferedBlocks = new Long2ObjectOpenHashMap<>();
|
||||
this.surfaceHeights = new Long2IntOpenHashMap();
|
||||
this.caveCeilings = new Long2IntOpenHashMap();
|
||||
this.surfaceHeights.defaultReturnValue(CACHE_MISS);
|
||||
@@ -73,28 +86,41 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
}
|
||||
|
||||
IrisComplex complex = engine == null ? null : engine.getComplex();
|
||||
for (BufferedMutation mutation : mutations) {
|
||||
boolean previousChecked = false;
|
||||
int previousX = 0;
|
||||
int previousY = 0;
|
||||
int previousZ = 0;
|
||||
for (int index = 0; index < mutationCount; index++) {
|
||||
int x = mutationX[index];
|
||||
int y = mutationY[index];
|
||||
int z = mutationZ[index];
|
||||
if (previousChecked && x == previousX && y == previousY && z == previousZ) {
|
||||
continue;
|
||||
}
|
||||
if (complex != null
|
||||
&& !complex.allowsNewDiscreteContentAt(mutation.x(), mutation.z())) {
|
||||
&& !complex.allowsNewDiscreteContentAt(x, z)) {
|
||||
discard();
|
||||
return CommitResult.REJECTED_TRANSITION;
|
||||
}
|
||||
if (!isWithinBounds(mutation.x(), mutation.y(), mutation.z())) {
|
||||
if (!isWithinBounds(x, y, z)) {
|
||||
discard();
|
||||
return CommitResult.REJECTED_BOUNDS;
|
||||
}
|
||||
SubterrainCell feature = engine.getSubterrainCell(mutation.x(), mutation.y(), mutation.z());
|
||||
SubterrainCell feature = engine.getSubterrainCell(x, y, z);
|
||||
if (SubterrainRasterizer.protectsPlacement(feature)
|
||||
|| feature != null && feature.solid() && delegate.isSolid(mutation.x(), mutation.y(), mutation.z())) {
|
||||
|| feature != null && feature.solid() && delegate.isSolid(x, y, z)) {
|
||||
discard();
|
||||
return CommitResult.REJECTED_SUBTERRAIN;
|
||||
}
|
||||
HydrologyCaveCell hydrology = delegate.getData(
|
||||
mutation.x(), mutation.y(), mutation.z(), HydrologyCaveCell.class);
|
||||
HydrologyCaveCell hydrology = delegate.getData(x, y, z, HydrologyCaveCell.class);
|
||||
if (hydrology != null && hydrology.protectsPlacement()) {
|
||||
discard();
|
||||
return CommitResult.REJECTED_HYDROLOGY;
|
||||
}
|
||||
previousChecked = true;
|
||||
previousX = x;
|
||||
previousY = y;
|
||||
previousZ = z;
|
||||
}
|
||||
|
||||
applyMutations();
|
||||
@@ -106,18 +132,26 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
boolean canopyOpen = false;
|
||||
boolean skipCanopy = false;
|
||||
try {
|
||||
for (BufferedMutation mutation : mutations) {
|
||||
if (mutation.value() instanceof NativeBlockState) {
|
||||
for (int index = 0; index < mutationCount; index++) {
|
||||
int x = mutationX[index];
|
||||
int y = mutationY[index];
|
||||
int z = mutationZ[index];
|
||||
Object value = mutationValues[index];
|
||||
boolean canopy = mutationCanopies[index];
|
||||
if (value instanceof NativeBlockState) {
|
||||
if (canopyOpen) {
|
||||
delegate.endTreeCanopy();
|
||||
canopyOpen = false;
|
||||
}
|
||||
skipCanopy = mutation.canopy()
|
||||
&& !delegate.beginTreeCanopy(new BlockPosition(mutation.x(), mutation.y(), mutation.z()));
|
||||
canopyOpen = mutation.canopy() && !skipCanopy;
|
||||
skipCanopy = canopy && !delegate.beginTreeCanopy(new BlockPosition(x, y, z));
|
||||
canopyOpen = canopy && !skipCanopy;
|
||||
}
|
||||
if (!canopy || !skipCanopy) {
|
||||
switch (mutationKinds[index]) {
|
||||
case BLOCK -> delegate.set(x, y, z, (NativeBlockState) value);
|
||||
case TILE -> delegate.setTile(x, y, z, (TileData) value);
|
||||
default -> delegate.setData(x, y, z, value);
|
||||
}
|
||||
if (!mutation.canopy() || !skipCanopy) {
|
||||
mutation.apply(delegate);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@@ -128,11 +162,31 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
}
|
||||
|
||||
void discard() {
|
||||
mutations.clear();
|
||||
Arrays.fill(mutationValues, 0, mutationCount, null);
|
||||
mutationCount = 0;
|
||||
bufferedBlocks.clear();
|
||||
blockWrites = 0;
|
||||
}
|
||||
|
||||
private void buffer(byte kind, int x, int y, int z, Object value) {
|
||||
if (mutationCount == mutationValues.length) {
|
||||
int capacity = mutationCount * 2;
|
||||
mutationX = Arrays.copyOf(mutationX, capacity);
|
||||
mutationY = Arrays.copyOf(mutationY, capacity);
|
||||
mutationZ = Arrays.copyOf(mutationZ, capacity);
|
||||
mutationValues = Arrays.copyOf(mutationValues, capacity);
|
||||
mutationKinds = Arrays.copyOf(mutationKinds, capacity);
|
||||
mutationCanopies = Arrays.copyOf(mutationCanopies, capacity);
|
||||
}
|
||||
mutationX[mutationCount] = x;
|
||||
mutationY[mutationCount] = y;
|
||||
mutationZ[mutationCount] = z;
|
||||
mutationValues[mutationCount] = value;
|
||||
mutationKinds[mutationCount] = kind;
|
||||
mutationCanopies[mutationCount] = isCanopy(x, y, z);
|
||||
mutationCount++;
|
||||
}
|
||||
|
||||
int getCaveCeiling(int x, int z) {
|
||||
long key = Cache.key(x, z);
|
||||
int cached = caveCeilings.get(key);
|
||||
@@ -164,8 +218,8 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
mutations.add(new BlockMutation(x, y, z, state, isCanopy(x, y, z)));
|
||||
bufferedBlocks.put(new PositionKey(x, y, z), state);
|
||||
buffer(BLOCK, x, y, z, state);
|
||||
bufferedBlocks.put(bufferKey(x, y, z), state);
|
||||
blockWrites++;
|
||||
}
|
||||
|
||||
@@ -190,7 +244,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
|
||||
@Override
|
||||
public NativeBlockState get(int x, int y, int z) {
|
||||
NativeBlockState buffered = bufferedBlocks.get(new PositionKey(x, y, z));
|
||||
NativeBlockState buffered = bufferedBlocks.isEmpty() ? null : bufferedBlocks.get(bufferKey(x, y, z));
|
||||
return buffered == null ? delegate.get(x, y, z) : buffered;
|
||||
}
|
||||
|
||||
@@ -211,7 +265,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
|
||||
@Override
|
||||
public boolean isSolid(int x, int y, int z) {
|
||||
NativeBlockState buffered = bufferedBlocks.get(new PositionKey(x, y, z));
|
||||
NativeBlockState buffered = bufferedBlocks.isEmpty() ? null : bufferedBlocks.get(bufferKey(x, y, z));
|
||||
return buffered == null ? delegate.isSolid(x, y, z) : B.isSolid(buffered);
|
||||
}
|
||||
|
||||
@@ -242,7 +296,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
@Override
|
||||
public void setTile(int x, int y, int z, TileData tile) {
|
||||
if (tile != null) {
|
||||
mutations.add(new TileMutation(x, y, z, tile, isCanopy(x, y, z)));
|
||||
buffer(TILE, x, y, z, tile);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,21 +305,20 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
mutations.add(new DataMutation(x, y, z, data, isCanopy(x, y, z)));
|
||||
buffer(DATA, x, y, z, data);
|
||||
if (data instanceof NativeBlockState state) {
|
||||
bufferedBlocks.put(new PositionKey(x, y, z), state);
|
||||
bufferedBlocks.put(bufferKey(x, y, z), state);
|
||||
blockWrites++;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> @Nullable T getData(int x, int y, int z, Class<T> type) {
|
||||
for (int i = mutations.size() - 1; i >= 0; i--) {
|
||||
BufferedMutation mutation = mutations.get(i);
|
||||
if (mutation.x() != x || mutation.y() != y || mutation.z() != z) {
|
||||
for (int index = mutationCount - 1; index >= 0; index--) {
|
||||
if (mutationX[index] != x || mutationY[index] != y || mutationZ[index] != z) {
|
||||
continue;
|
||||
}
|
||||
Object value = mutation.value();
|
||||
Object value = mutationValues[index];
|
||||
if (type.isInstance(value)) {
|
||||
return type.cast(value);
|
||||
}
|
||||
@@ -278,6 +331,22 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
return engine;
|
||||
}
|
||||
|
||||
private long bufferKey(int x, int y, int z) {
|
||||
if (!originKnown) {
|
||||
originX = x;
|
||||
originZ = z;
|
||||
originKnown = true;
|
||||
}
|
||||
int dx = x - originX;
|
||||
int dz = z - originZ;
|
||||
if (dx < -POSITION_LIMIT || dx >= POSITION_LIMIT || dz < -POSITION_LIMIT || dz >= POSITION_LIMIT
|
||||
|| y < -POSITION_LIMIT || y >= POSITION_LIMIT) {
|
||||
throw new IllegalStateException("Cave object write at " + x + "," + y + "," + z
|
||||
+ " is too far from its first write at " + originX + "," + originZ);
|
||||
}
|
||||
return ((dx & POSITION_MASK) << (POSITION_BITS * 2)) | ((dz & POSITION_MASK) << POSITION_BITS) | (y & POSITION_MASK);
|
||||
}
|
||||
|
||||
private boolean isWithinBounds(int x, int y, int z) {
|
||||
if (engine == null || y < 0 || y >= worldHeight) {
|
||||
return false;
|
||||
@@ -325,42 +394,4 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
REJECTED_HYDROLOGY,
|
||||
REJECTED_SUBTERRAIN
|
||||
}
|
||||
|
||||
private interface BufferedMutation {
|
||||
int x();
|
||||
|
||||
int y();
|
||||
|
||||
int z();
|
||||
|
||||
Object value();
|
||||
|
||||
boolean canopy();
|
||||
|
||||
void apply(IObjectPlacer placer);
|
||||
}
|
||||
|
||||
private record BlockMutation(int x, int y, int z, NativeBlockState value, boolean canopy) implements BufferedMutation {
|
||||
@Override
|
||||
public void apply(IObjectPlacer placer) {
|
||||
placer.set(x, y, z, value);
|
||||
}
|
||||
}
|
||||
|
||||
private record TileMutation(int x, int y, int z, TileData value, boolean canopy) implements BufferedMutation {
|
||||
@Override
|
||||
public void apply(IObjectPlacer placer) {
|
||||
placer.setTile(x, y, z, value);
|
||||
}
|
||||
}
|
||||
|
||||
private record DataMutation(int x, int y, int z, Object value, boolean canopy) implements BufferedMutation {
|
||||
@Override
|
||||
public void apply(IObjectPlacer placer) {
|
||||
placer.setData(x, y, z, value);
|
||||
}
|
||||
}
|
||||
|
||||
private record PositionKey(int x, int y, int z) {
|
||||
}
|
||||
}
|
||||
@@ -144,20 +144,6 @@ public final class CaveTerrainSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
public MatterCavern[] composedFace(TerrainMatterView.Face face, int height) {
|
||||
Objects.requireNonNull(face, "Terrain face");
|
||||
if (height < 0) {
|
||||
throw new IllegalArgumentException("Terrain face height must be nonnegative");
|
||||
}
|
||||
MatterCavern[] result = new MatterCavern[Math.multiplyExact(height, 16)];
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int offset = 0; offset < 16; offset++) {
|
||||
result[y * 16 + offset] = composedCavern(face.x(offset), y, face.z(offset));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void captureSection(int section, Matter matter) {
|
||||
caverns[section] = copy(matter.getSlice(MatterCavern.class), MatterCavern[]::new);
|
||||
hydrology[section] = copy(matter.getSlice(HydrologyCaveCell.class), HydrologyCaveCell[]::new);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -34,6 +34,8 @@ import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.iris.generation.block.B;
|
||||
import art.arcane.iris.generation.context.ChunkContext;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import art.arcane.iris.generation.stream.BakedDoubleStream;
|
||||
import art.arcane.iris.generation.stream.BakedStream;
|
||||
import art.arcane.iris.generation.stream.ChunkFillableDoubleStream2D;
|
||||
import art.arcane.iris.generation.simd.SimdSupport;
|
||||
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
|
||||
@@ -64,6 +66,12 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
private static final int[] KERNEL_DX = new int[KERNEL_SIZE];
|
||||
private static final int[] KERNEL_DZ = new int[KERNEL_SIZE];
|
||||
private static final double[] KERNEL_WEIGHT = new double[KERNEL_SIZE];
|
||||
private static final int NEIGHBOURS = 9;
|
||||
private static final int[] NEIGHBOUR_DX = new int[NEIGHBOURS];
|
||||
private static final int[] NEIGHBOUR_DZ = new int[NEIGHBOURS];
|
||||
private static final long[][] NEIGHBOUR_COLUMNS = new long[NEIGHBOURS][4];
|
||||
private static final int[] FIELD_NEIGHBOUR = new int[FIELD_SIZE * FIELD_SIZE];
|
||||
private static final int[] FIELD_COLUMN = new int[FIELD_SIZE * FIELD_SIZE];
|
||||
private static final ThreadLocal<BlendScratch> BLEND_SCRATCH = ThreadLocal.withInitial(BlendScratch::new);
|
||||
|
||||
private final Object profileCarverLock = new Object();
|
||||
@@ -81,6 +89,22 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
kernelIndex++;
|
||||
}
|
||||
}
|
||||
for (int neighbour = 0; neighbour < NEIGHBOURS; neighbour++) {
|
||||
NEIGHBOUR_DX[neighbour] = (neighbour / 3) - 1;
|
||||
NEIGHBOUR_DZ[neighbour] = (neighbour % 3) - 1;
|
||||
}
|
||||
for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) {
|
||||
int offsetX = fieldX - BLEND_RADIUS;
|
||||
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
|
||||
int offsetZ = fieldZ - BLEND_RADIUS;
|
||||
int neighbour = ((Math.floorDiv(offsetX, CHUNK_SIZE) + 1) * 3) + Math.floorDiv(offsetZ, CHUNK_SIZE) + 1;
|
||||
int column = ((offsetZ & (CHUNK_SIZE - 1)) << 4) | (offsetX & (CHUNK_SIZE - 1));
|
||||
int fieldIndex = (fieldX * FIELD_SIZE) + fieldZ;
|
||||
FIELD_NEIGHBOUR[fieldIndex] = neighbour;
|
||||
FIELD_COLUMN[fieldIndex] = column;
|
||||
NEIGHBOUR_COLUMNS[neighbour][column >>> 6] |= 1L << (column & 63);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public MantleCarvingComponent(EngineMantle engineMantle) {
|
||||
@@ -128,7 +152,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
|
||||
private void generateLayer(MantleWriter writer, int x, int z, ChunkContext context, BlendScratch blendScratch) {
|
||||
IrisComplex complex = context.getComplex();
|
||||
IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State();
|
||||
IrisDimensionCarvingResolver.State resolverState = blendScratch.resolverState;
|
||||
int[] chunkSurfaceHeights = prepareChunkSurfaceHeights(x, z, context, blendScratch.chunkSurfaceHeights);
|
||||
PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start();
|
||||
List<WeightedProfile> weightedProfiles = resolveWeightedProfiles(x, z, complex, resolverState);
|
||||
@@ -422,7 +446,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
private void fillProfileField(IrisCaveProfile[] profileField, int chunkX, int chunkZ, IrisComplex complex, IrisDimensionCarvingResolver.State resolverState, BlendScratch blendScratch) {
|
||||
int startX = PowerOfTwoCoordinates.chunkToBlock(chunkX) - BLEND_RADIUS;
|
||||
int startZ = PowerOfTwoCoordinates.chunkToBlock(chunkZ) - BLEND_RADIUS;
|
||||
prefillProfileFieldSamples(startX, startZ, complex, blendScratch);
|
||||
prefillProfileFieldSamples(chunkX, chunkZ, startX, startZ, complex, blendScratch);
|
||||
|
||||
for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) {
|
||||
int worldX = startX + fieldX;
|
||||
@@ -519,17 +543,28 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private void prefillProfileFieldSamples(int startX, int startZ, IrisComplex complex, BlendScratch blendScratch) {
|
||||
fillFieldHeights(complex.getNaturalHeightStream(), startX, startZ, blendScratch.fieldSurfaceHeights);
|
||||
Arrays.fill(blendScratch.fieldFluidHeights, complex.getFluidHeight());
|
||||
fillFieldFluidPresence(complex, startX, startZ, blendScratch.fieldSurfaceHeights,
|
||||
blendScratch.fieldFluidHeights, blendScratch.fieldHasFluid);
|
||||
fillFieldObjects(complex.getRegionStream(), startX, startZ, blendScratch.fieldRegions);
|
||||
fillFieldObjects(complex.getNaturalTrueBiomeStream(), startX, startZ, blendScratch.fieldSurfaceBiomes);
|
||||
fillFieldObjects(complex.getCaveBiomeStream(), startX, startZ, blendScratch.fieldCaveBiomes);
|
||||
private void prefillProfileFieldSamples(int chunkX, int chunkZ, int startX, int startZ, IrisComplex complex, BlendScratch blendScratch) {
|
||||
fillFieldHeights(complex.getNaturalHeightStream(), chunkX, chunkZ, startX, startZ, blendScratch);
|
||||
double fluidHeight = complex.getFluidHeight();
|
||||
Arrays.fill(blendScratch.fieldFluidHeights, fluidHeight);
|
||||
fillFieldFluidPresence(complex.getFluidStream(), chunkX, chunkZ, startX, startZ, Math.round(fluidHeight), blendScratch);
|
||||
fillFieldObjects(complex.getRegionStream(), chunkX, chunkZ, startX, startZ, blendScratch.fieldRegions, blendScratch.neighbourObjects);
|
||||
fillFieldObjects(complex.getNaturalTrueBiomeStream(), chunkX, chunkZ, startX, startZ, blendScratch.fieldSurfaceBiomes, blendScratch.neighbourObjects);
|
||||
fillFieldObjects(complex.getCaveBiomeStream(), chunkX, chunkZ, startX, startZ, blendScratch.fieldCaveBiomes, blendScratch.neighbourObjects);
|
||||
}
|
||||
|
||||
private <T> void fillFieldObjects(ProceduralStream<T> stream, int startX, int startZ, T[] target) {
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> void fillFieldObjects(ProceduralStream<T> stream, int chunkX, int chunkZ, int startX, int startZ, T[] target, Object[][] neighbourObjects) {
|
||||
if (stream instanceof BakedStream<?> baked) {
|
||||
for (int neighbour = 0; neighbour < NEIGHBOURS; neighbour++) {
|
||||
baked.fillColumns(chunkX + NEIGHBOUR_DX[neighbour], chunkZ + NEIGHBOUR_DZ[neighbour],
|
||||
NEIGHBOUR_COLUMNS[neighbour], neighbourObjects[neighbour]);
|
||||
}
|
||||
for (int fieldIndex = 0; fieldIndex < target.length; fieldIndex++) {
|
||||
target[fieldIndex] = (T) neighbourObjects[FIELD_NEIGHBOUR[fieldIndex]][FIELD_COLUMN[fieldIndex]];
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) {
|
||||
int worldX = startX + fieldX;
|
||||
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
|
||||
@@ -538,7 +573,19 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private void fillFieldHeights(ProceduralStream<Double> stream, int startX, int startZ, double[] target) {
|
||||
private void fillFieldHeights(ProceduralStream<Double> stream, int chunkX, int chunkZ, int startX, int startZ, BlendScratch blendScratch) {
|
||||
double[] target = blendScratch.fieldSurfaceHeights;
|
||||
if (stream instanceof BakedDoubleStream baked) {
|
||||
double[][] neighbourHeights = blendScratch.neighbourHeights;
|
||||
for (int neighbour = 0; neighbour < NEIGHBOURS; neighbour++) {
|
||||
baked.fillColumns(chunkX + NEIGHBOUR_DX[neighbour], chunkZ + NEIGHBOUR_DZ[neighbour],
|
||||
NEIGHBOUR_COLUMNS[neighbour], neighbourHeights[neighbour]);
|
||||
}
|
||||
for (int fieldIndex = 0; fieldIndex < target.length; fieldIndex++) {
|
||||
target[fieldIndex] = neighbourHeights[FIELD_NEIGHBOUR[fieldIndex]][FIELD_COLUMN[fieldIndex]];
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) {
|
||||
int worldX = startX + fieldX;
|
||||
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
|
||||
@@ -547,20 +594,41 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private void fillFieldFluidPresence(
|
||||
IrisComplex complex,
|
||||
int startX,
|
||||
int startZ,
|
||||
double[] surfaceHeights,
|
||||
double[] fluidHeights,
|
||||
boolean[] target
|
||||
) {
|
||||
private void fillFieldFluidPresence(ProceduralStream<NativeBlockState> stream, int chunkX, int chunkZ, int startX, int startZ,
|
||||
long roundedFluidHeight, BlendScratch blendScratch) {
|
||||
double[] surfaceHeights = blendScratch.fieldSurfaceHeights;
|
||||
boolean[] target = blendScratch.fieldHasFluid;
|
||||
if (stream instanceof BakedStream<?> baked) {
|
||||
long[][] submerged = blendScratch.neighbourSubmerged;
|
||||
for (long[] columns : submerged) {
|
||||
Arrays.fill(columns, 0L);
|
||||
}
|
||||
for (int fieldIndex = 0; fieldIndex < target.length; fieldIndex++) {
|
||||
if (Math.round(surfaceHeights[fieldIndex]) < roundedFluidHeight) {
|
||||
int column = FIELD_COLUMN[fieldIndex];
|
||||
submerged[FIELD_NEIGHBOUR[fieldIndex]][column >>> 6] |= 1L << (column & 63);
|
||||
}
|
||||
}
|
||||
Object[][] neighbourObjects = blendScratch.neighbourObjects;
|
||||
for (int neighbour = 0; neighbour < NEIGHBOURS; neighbour++) {
|
||||
long[] columns = submerged[neighbour];
|
||||
if ((columns[0] | columns[1] | columns[2] | columns[3]) != 0L) {
|
||||
baked.fillColumns(chunkX + NEIGHBOUR_DX[neighbour], chunkZ + NEIGHBOUR_DZ[neighbour],
|
||||
columns, neighbourObjects[neighbour]);
|
||||
}
|
||||
}
|
||||
for (int fieldIndex = 0; fieldIndex < target.length; fieldIndex++) {
|
||||
target[fieldIndex] = Math.round(surfaceHeights[fieldIndex]) < roundedFluidHeight
|
||||
&& B.isFluid((NativeBlockState) neighbourObjects[FIELD_NEIGHBOUR[fieldIndex]][FIELD_COLUMN[fieldIndex]]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) {
|
||||
int worldX = startX + fieldX;
|
||||
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
|
||||
int fieldIndex = (fieldX * FIELD_SIZE) + fieldZ;
|
||||
target[fieldIndex] = B.isFluid(complex.getFluidStream().get(worldX, startZ + fieldZ))
|
||||
&& Math.round(surfaceHeights[fieldIndex]) < Math.round(fluidHeights[fieldIndex]);
|
||||
target[fieldIndex] = B.isFluid(stream.get(worldX, startZ + fieldZ))
|
||||
&& Math.round(surfaceHeights[fieldIndex]) < roundedFluidHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -682,6 +750,10 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
private final IrisBiome[] fieldCaveBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE];
|
||||
private final int[] chunkSurfaceHeights = new int[CHUNK_AREA];
|
||||
private final double[] chunkSurfaceHeightSamples = new double[CHUNK_AREA];
|
||||
private final double[][] neighbourHeights = new double[NEIGHBOURS][CHUNK_AREA];
|
||||
private final Object[][] neighbourObjects = new Object[NEIGHBOURS][CHUNK_AREA];
|
||||
private final long[][] neighbourSubmerged = new long[NEIGHBOURS][4];
|
||||
private final IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State();
|
||||
private int nextWeightBuffer;
|
||||
private boolean inUse;
|
||||
|
||||
@@ -705,6 +777,9 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
Arrays.fill(fieldRegions, null);
|
||||
Arrays.fill(fieldSurfaceBiomes, null);
|
||||
Arrays.fill(fieldCaveBiomes, null);
|
||||
for (Object[] objects : neighbourObjects) {
|
||||
Arrays.fill(objects, null);
|
||||
}
|
||||
columnProfileWeights.clear();
|
||||
profileOrder.clear();
|
||||
nextWeightBuffer = 0;
|
||||
|
||||
+42
-19
@@ -44,7 +44,6 @@ import art.arcane.volmlib.util.documentation.ChunkCoordinates;
|
||||
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
@@ -598,7 +597,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
@Override
|
||||
protected int computeRadius() {
|
||||
int maxObjectExtent = 0;
|
||||
Map<String, IrisBlockVector> sizeCache = new HashMap<>();
|
||||
Map<String, PlacementReach.Extent> extents = new HashMap<>();
|
||||
Set<String> warnedLargeObjects = new HashSet<>();
|
||||
try {
|
||||
IrisData data = getData();
|
||||
@@ -608,15 +607,17 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
continue;
|
||||
}
|
||||
for (IrisFloatingChildBiomes entry : entries) {
|
||||
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getFloatingObjects(), data, sizeCache, warnedLargeObjects));
|
||||
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getExtraObjects(), data, sizeCache, warnedLargeObjects));
|
||||
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getTopObjectOverrides(), data, sizeCache, warnedLargeObjects));
|
||||
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getBottomObjectOverrides(), data, sizeCache, warnedLargeObjects));
|
||||
PlacementScan scan = new PlacementScan(data, extents, warnedLargeObjects,
|
||||
entry.hasObjectShrink() ? entry.getShrinkScale().getMaxScale() : 1D);
|
||||
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.getFloatingObjects(), false));
|
||||
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.getExtraObjects(), true));
|
||||
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.getTopObjectOverrides(), true));
|
||||
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.getBottomObjectOverrides(), true));
|
||||
try {
|
||||
IrisBiome target = entry.getRealBiome(biome, data);
|
||||
if (target != null) {
|
||||
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.resolveTopObjects(target), data, sizeCache, warnedLargeObjects));
|
||||
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.resolveBottomObjects(target), data, sizeCache, warnedLargeObjects));
|
||||
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.resolveTopObjects(target), true));
|
||||
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.resolveBottomObjects(target), true));
|
||||
} else {
|
||||
maxObjectExtent = Math.max(maxObjectExtent, MINIMUM_PLACEMENT_RADIUS);
|
||||
}
|
||||
@@ -635,9 +636,21 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
return maxObjectExtent;
|
||||
}
|
||||
|
||||
private int computePlacementRadius(KList<IrisObjectPlacement> placements, IrisData data,
|
||||
Map<String, IrisBlockVector> sizeCache,
|
||||
Set<String> warnedLargeObjects) {
|
||||
private final class PlacementScan {
|
||||
private final IrisData data;
|
||||
private final Map<String, PlacementReach.Extent> extents;
|
||||
private final Set<String> warnedLargeObjects;
|
||||
private final double shrink;
|
||||
|
||||
private PlacementScan(IrisData data, Map<String, PlacementReach.Extent> extents, Set<String> warnedLargeObjects,
|
||||
double shrink) {
|
||||
this.data = data;
|
||||
this.extents = extents;
|
||||
this.warnedLargeObjects = warnedLargeObjects;
|
||||
this.shrink = shrink;
|
||||
}
|
||||
|
||||
private int radius(KList<IrisObjectPlacement> placements, boolean anchored) {
|
||||
if (placements == null) {
|
||||
return MINIMUM_PLACEMENT_RADIUS;
|
||||
}
|
||||
@@ -649,18 +662,16 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
if (placement == null || placement.getPlace() == null) {
|
||||
continue;
|
||||
}
|
||||
PlacementReach.Profile profile = PlacementReach.profile(placement);
|
||||
double scale = placement.getMaximumScale(getDimension());
|
||||
for (String objectKey : placement.getPlace()) {
|
||||
try {
|
||||
IrisBlockVector size = sizeCache.get(objectKey);
|
||||
if (size == null) {
|
||||
File file = data.getObjectLoader().findFile(objectKey);
|
||||
if (file == null) {
|
||||
PlacementReach.Extent extent = extent(objectKey);
|
||||
if (extent == null) {
|
||||
continue;
|
||||
}
|
||||
size = IrisObject.sampleSize(file);
|
||||
sizeCache.put(objectKey, size);
|
||||
}
|
||||
int reach = MantleObjectComponent.calculatePlacementReach(size, placement, placement.getMaximumScale(getDimension()));
|
||||
PlacementReach.Extent scaled = extent.scaled(scale).scaled(shrink);
|
||||
int reach = anchored ? profile.anchored(scaled).horizontal() : profile.of(scaled).horizontal();
|
||||
if (reach > 128 && warnedLargeObjects.add(objectKey)) {
|
||||
IrisLogging.warn("Floating object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!");
|
||||
}
|
||||
@@ -673,6 +684,18 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
return radius;
|
||||
}
|
||||
|
||||
private PlacementReach.Extent extent(String objectKey) {
|
||||
if (extents.containsKey(objectKey)) {
|
||||
return extents.get(objectKey);
|
||||
}
|
||||
IrisObject object = data.getObjectLoader().load(objectKey);
|
||||
PlacementReach.Extent extent = object == null ? null : PlacementReach.Extent.of(object);
|
||||
extents.put(objectKey, extent);
|
||||
return extent;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PlacementSequence {
|
||||
private int ordinal;
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@ import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
|
||||
import art.arcane.iris.generation.subterrain.SubterrainPlan;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.iris.generation.context.ChunkContext;
|
||||
import art.arcane.volmlib.util.documentation.BlockCoordinates;
|
||||
@@ -69,12 +68,12 @@ import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.iris.generation.block.DataProvider;
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@@ -89,11 +88,14 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
private static final Map<String, CaveRejectLogState> CAVE_REJECT_LOG_STATE = new ConcurrentHashMap<>();
|
||||
private static final Set<String> MISSING_LOAD_KEY_WARNED = ConcurrentHashMap.newKeySet();
|
||||
private static final long SOURCE_PLAN_STATS_INTERVAL_MS = 30_000L;
|
||||
private static final int MAXIMUM_TREE_MATERIALS = 8192;
|
||||
private static final Map<NativeBlockState, TreeBlockMaterial> TREE_MATERIALS = new ConcurrentHashMap<>();
|
||||
|
||||
private final Object collisionRuleLock;
|
||||
private final ObjectSourcePlanCache sourcePlans;
|
||||
private final AtomicLong lastSourcePlanStatsLog = new AtomicLong();
|
||||
private volatile int collisionRuleState;
|
||||
private volatile int inputReach;
|
||||
|
||||
public MantleObjectComponent(EngineMantle engineMantle) {
|
||||
super(engineMantle, ReservedFlag.OBJECT, 2);
|
||||
@@ -124,7 +126,14 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
|
||||
private static void writeTreeMaterial(IObjectPlacer placer, int x, int y, int z, NativeBlockState data) {
|
||||
placer.setData(x, y, z, TreeBlockMaterial.of(data));
|
||||
TreeBlockMaterial material = TREE_MATERIALS.get(data);
|
||||
if (material == null) {
|
||||
material = TreeBlockMaterial.of(data);
|
||||
if (TREE_MATERIALS.size() < MAXIMUM_TREE_MATERIALS) {
|
||||
TREE_MATERIALS.putIfAbsent(data, material);
|
||||
}
|
||||
}
|
||||
placer.setData(x, y, z, material);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -134,7 +143,8 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
|
||||
@Override
|
||||
public int getInputRadius() {
|
||||
return calculateInputRadius(getRadius(), hasCollisionRules());
|
||||
int radius = getRadius();
|
||||
return calculateInputRadius(radius, inputReach, hasCollisionRules());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -156,19 +166,23 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, x, z);
|
||||
int radius = getRadius();
|
||||
int sourceChunkRadius = sourceChunkRadius(radius);
|
||||
replaySourceChunks(
|
||||
x,
|
||||
z,
|
||||
radius,
|
||||
(sourceX, sourceZ) -> {
|
||||
ObjectSourcePlan plan = sourcePlans.acquire(sourceX, sourceZ, x, z, sourceChunkRadius,
|
||||
int side = (sourceChunkRadius * 2) + 1;
|
||||
ObjectSourcePlan[] plans = new ObjectSourcePlan[side * side];
|
||||
int[] slot = new int[1];
|
||||
replaySourceChunks(x, z, radius, (sourceX, sourceZ) -> plans[slot[0]++] = sourcePlans.tryAcquire(
|
||||
sourceX, sourceZ, x, z, sourceChunkRadius, () -> buildSourcePlan(writer, sourceX, sourceZ, context)));
|
||||
slot[0] = 0;
|
||||
replaySourceChunks(x, z, radius, (sourceX, sourceZ) -> {
|
||||
ObjectSourcePlan plan = plans[slot[0]++];
|
||||
if (plan == ObjectSourcePlanCache.BUSY) {
|
||||
plan = sourcePlans.acquire(sourceX, sourceZ, x, z, sourceChunkRadius,
|
||||
() -> buildSourcePlan(writer, sourceX, sourceZ, context));
|
||||
}
|
||||
if (plan != null) {
|
||||
transaction.apply(plan);
|
||||
plan.persistContinuations(writer, sourceX, sourceZ, x, z);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
transaction.commit();
|
||||
logSourcePlanStats();
|
||||
}
|
||||
@@ -297,11 +311,11 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
return inputRadius >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) inputRadius;
|
||||
}
|
||||
|
||||
static int calculateInputRadius(int radius, boolean sourceAnchoredCollisions) {
|
||||
static int calculateInputRadius(int radius, int inputReach, boolean sourceAnchoredCollisions) {
|
||||
int normalizedRadius = Math.max(0, radius);
|
||||
long sourceChunkRadius = sourceChunkRadius(normalizedRadius);
|
||||
long sourceLegs = sourceAnchoredCollisions ? 2L : 1L;
|
||||
long inputRadius = (sourceChunkRadius * 16L * sourceLegs) + normalizedRadius + 1L;
|
||||
long inputRadius = (sourceChunkRadius * 16L * sourceLegs) + Math.max(normalizedRadius, inputReach) + 1L;
|
||||
return inputRadius >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) inputRadius;
|
||||
}
|
||||
|
||||
@@ -1370,7 +1384,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
int id = rng.i(0, Integer.MAX_VALUE);
|
||||
IrisObjectPlacement placement = objectPlacement.toPlacement(v.getLoadKey());
|
||||
placement.setMode(ObjectPlaceMode.CENTER_HEIGHT);
|
||||
placement.setRotation(buildUpsideDownRotation());
|
||||
placement.setRotation(upsideDownRotation());
|
||||
placement.setCarvingSupport(CarvingMode.ANYWHERE);
|
||||
if (forcePlace) {
|
||||
placement.setForcePlace(true);
|
||||
@@ -1408,7 +1422,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private IrisObjectRotation buildUpsideDownRotation() {
|
||||
private static IrisObjectRotation upsideDownRotation() {
|
||||
IrisObjectRotation rt = new IrisObjectRotation();
|
||||
rt.setEnabled(true);
|
||||
rt.setXAxis(new IrisAxisRotationClamp(true, true, 180D, 180D, 90D));
|
||||
@@ -1785,135 +1799,13 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
|
||||
protected int computeRadius() {
|
||||
KSet<String> warnedLargeObjects = new KSet<>();
|
||||
int radius = computeDimensionRadius(getDimension(), this::getData, warnedLargeObjects);
|
||||
PlacementReach reach = new ReachScan(getDimension(), this::getData, warnedLargeObjects).dimension();
|
||||
UpperDimensionContext upperContext = activeUpperContext();
|
||||
if (upperContext != null) {
|
||||
radius = Math.max(radius, computeDimensionRadius(
|
||||
upperContext.getDimension(),
|
||||
upperContext,
|
||||
warnedLargeObjects
|
||||
));
|
||||
reach = reach.union(new ReachScan(upperContext.getDimension(), upperContext, warnedLargeObjects).upperDimension());
|
||||
}
|
||||
return radius;
|
||||
}
|
||||
|
||||
private int computeDimensionRadius(
|
||||
IrisDimension dimension,
|
||||
DataProvider dataProvider,
|
||||
KSet<String> warnedLargeObjects
|
||||
) {
|
||||
KMap<String, IrisBlockVector> sizeCache = new KMap<>();
|
||||
int radius = 0;
|
||||
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
|
||||
if (region == null) {
|
||||
continue;
|
||||
}
|
||||
radius = Math.max(radius, computePlacementRadius(
|
||||
region.getObjects(),
|
||||
dimension,
|
||||
dataProvider,
|
||||
sizeCache,
|
||||
warnedLargeObjects
|
||||
));
|
||||
radius = Math.max(radius, computeProceduralRadius(region.getProceduralObjects(), dimension, dataProvider));
|
||||
}
|
||||
for (IrisBiome biome : dimension.getReachableBiomes(dataProvider)) {
|
||||
if (biome == null) {
|
||||
continue;
|
||||
}
|
||||
radius = Math.max(radius, computePlacementRadius(
|
||||
biome.getObjects(),
|
||||
dimension,
|
||||
dataProvider,
|
||||
sizeCache,
|
||||
warnedLargeObjects
|
||||
));
|
||||
radius = Math.max(radius, computeProceduralRadius(biome.getProceduralObjects(), dimension, dataProvider));
|
||||
}
|
||||
return radius;
|
||||
}
|
||||
|
||||
private int computePlacementRadius(
|
||||
KList<IrisObjectPlacement> placements,
|
||||
IrisDimension dimension,
|
||||
DataProvider dataProvider,
|
||||
KMap<String, IrisBlockVector> sizeCache,
|
||||
KSet<String> warnedLargeObjects
|
||||
) {
|
||||
int radius = 0;
|
||||
for (IrisObjectPlacement placement : placements) {
|
||||
if (placement == null) {
|
||||
continue;
|
||||
}
|
||||
for (String objectKey : placement.getPlace()) {
|
||||
try {
|
||||
IrisBlockVector size = loadObjectSize(dataProvider, sizeCache, objectKey);
|
||||
if (size == null) {
|
||||
continue;
|
||||
}
|
||||
int reach = calculatePlacementReach(size, placement, placement.getMaximumScale(dimension));
|
||||
if (reach > 128 && warnedLargeObjects.add(objectKey)) {
|
||||
IrisLogging.warn("Object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!");
|
||||
}
|
||||
radius = Math.max(radius, reach);
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return radius;
|
||||
}
|
||||
|
||||
private int computeProceduralRadius(IrisProceduralObjects procedural, IrisDimension dimension, DataProvider dataProvider) {
|
||||
if (procedural == null || procedural.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
int radius = 0;
|
||||
for (IrisProceduralPlacement placement : procedural.getAllPlacements()) {
|
||||
if (placement == null) {
|
||||
continue;
|
||||
}
|
||||
KList<IrisObject> variants = placement.getVariantObjects(dataProvider.getData());
|
||||
if (variants == null) {
|
||||
continue;
|
||||
}
|
||||
IrisObjectPlacement objectPlacement = placement.asPlacement();
|
||||
for (IrisObject variant : variants) {
|
||||
if (variant == null) {
|
||||
continue;
|
||||
}
|
||||
IrisBlockVector size = new IrisBlockVector(variant.getW(), variant.getH(), variant.getD());
|
||||
radius = Math.max(radius, calculatePlacementReach(size, objectPlacement, objectPlacement.getMaximumScale(dimension)));
|
||||
}
|
||||
}
|
||||
return radius;
|
||||
}
|
||||
|
||||
static int calculatePlacementReach(IrisBlockVector size, IrisObjectPlacement placement, double scale) {
|
||||
if (size == null) {
|
||||
return 0;
|
||||
}
|
||||
if (placement == null) {
|
||||
return Math.max(Math.abs(size.getBlockX()), Math.abs(size.getBlockZ()));
|
||||
}
|
||||
|
||||
int width = scaledDimension(size.getBlockX(), scale);
|
||||
int height = scaledDimension(size.getBlockY(), scale);
|
||||
int depth = scaledDimension(size.getBlockZ(), scale);
|
||||
IrisObjectRotation rotation = placement.getRotation();
|
||||
boolean rotateX = rotation != null && rotation.isEnabled() && rotation.getXAxis() != null && rotation.getXAxis().isEnabled();
|
||||
boolean rotateZ = rotation != null && rotation.isEnabled() && rotation.getZAxis() != null && rotation.getZAxis().isEnabled();
|
||||
int footprint = rotateX || rotateZ ? Math.max(width, Math.max(height, depth)) : Math.max(width, depth);
|
||||
int translation = calculateTranslationReach(placement.getTranslate(), rotation, rotateX || rotateZ);
|
||||
int warp = placement.getWarp() != null && !placement.getWarp().isFlat()
|
||||
? (int) Math.ceil(Math.abs(placement.getWarp().getMultiplier()) / 2D)
|
||||
: 0;
|
||||
int vacuum = IrisObjectVacuum.isVacuumMode(placement.getMode())
|
||||
? 2 * IrisObjectVacuum.resolveRadius(placement.getMode(), placement.getVacuumSettings())
|
||||
: 0;
|
||||
long reach = (long) footprint + translation + warp + vacuum;
|
||||
return reach > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) reach;
|
||||
inputReach = reach.input();
|
||||
return reach.horizontal();
|
||||
}
|
||||
|
||||
static boolean allowsObjectPlacement(
|
||||
@@ -1926,9 +1818,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
if (complex == null || object == null) {
|
||||
return true;
|
||||
}
|
||||
int reach = calculatePlacementReach(
|
||||
new IrisBlockVector(object.getW(), object.getH(), object.getD()),
|
||||
placement, 1D);
|
||||
int reach = transitionFootprint(object, placement);
|
||||
return complex.allowsNewGenerationFootprint(
|
||||
saturatedOffset(anchorX, -reach),
|
||||
saturatedOffset(anchorZ, -reach),
|
||||
@@ -1936,20 +1826,34 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
saturatedOffset(anchorZ, reach));
|
||||
}
|
||||
|
||||
private static int transitionFootprint(IrisObject object, IrisObjectPlacement placement) {
|
||||
int width = Math.abs(object.getW());
|
||||
int height = Math.abs(object.getH());
|
||||
int depth = Math.abs(object.getD());
|
||||
if (placement == null) {
|
||||
return Math.max(width, depth);
|
||||
}
|
||||
IrisObjectRotation rotation = placement.getRotation();
|
||||
boolean rotateX = rotation != null && rotation.isEnabled() && rotation.getXAxis() != null && rotation.getXAxis().isEnabled();
|
||||
boolean rotateZ = rotation != null && rotation.isEnabled() && rotation.getZAxis() != null && rotation.getZAxis().isEnabled();
|
||||
int footprint = rotateX || rotateZ ? Math.max(width, Math.max(height, depth)) : Math.max(width, depth);
|
||||
int translation = transitionTranslation(placement.getTranslate(), rotation, rotateX || rotateZ);
|
||||
int warp = placement.getWarp() != null && !placement.getWarp().isFlat()
|
||||
? (int) Math.ceil(Math.abs(placement.getWarp().getMultiplier()) / 2D)
|
||||
: 0;
|
||||
int vacuum = IrisObjectVacuum.isVacuumMode(placement.getMode())
|
||||
? 2 * IrisObjectVacuum.resolveRadius(placement.getMode(), placement.getVacuumSettings())
|
||||
: 0;
|
||||
long reach = (long) footprint + translation + warp + vacuum;
|
||||
return reach > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) reach;
|
||||
}
|
||||
|
||||
private static int saturatedOffset(int coordinate, int offset) {
|
||||
long result = (long) coordinate + offset;
|
||||
return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, result));
|
||||
}
|
||||
|
||||
private static int scaledDimension(int dimension, double scale) {
|
||||
int absoluteDimension = Math.abs(dimension);
|
||||
if (scale == 1D) {
|
||||
return absoluteDimension;
|
||||
}
|
||||
return (int) Math.ceil(absoluteDimension * scale);
|
||||
}
|
||||
|
||||
private static int calculateTranslationReach(IrisObjectTranslate translate, IrisObjectRotation rotation, boolean rotateVertically) {
|
||||
private static int transitionTranslation(IrisObjectTranslate translate, IrisObjectRotation rotation, boolean rotateVertically) {
|
||||
if (translate == null || !translate.canTranslate()) {
|
||||
return 0;
|
||||
}
|
||||
@@ -1963,22 +1867,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
return rotateY ? (int) Math.ceil(Math.hypot(x, z)) : (int) Math.max(Math.abs(x), Math.abs(z));
|
||||
}
|
||||
|
||||
private IrisBlockVector loadObjectSize(
|
||||
DataProvider dataProvider,
|
||||
KMap<String, IrisBlockVector> sizeCache,
|
||||
String objectKey
|
||||
) {
|
||||
return sizeCache.computeIfAbsent(objectKey, k -> {
|
||||
try {
|
||||
return IrisObject.sampleSize(dataProvider.getData().getObjectLoader().findFile(objectKey));
|
||||
} catch (IOException e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private UpperDimensionContext activeUpperContext() {
|
||||
if (!getDimension().isUpperDimensionObjects()) {
|
||||
return null;
|
||||
@@ -2047,6 +1935,187 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ReachScan {
|
||||
private final IrisDimension dimension;
|
||||
private final DataProvider dataProvider;
|
||||
private final KSet<String> warnedLargeObjects;
|
||||
private final Map<String, PlacementReach.Extent> extents;
|
||||
private final Set<ObjectPlaceMode> caveVacuumModes;
|
||||
|
||||
private ReachScan(IrisDimension dimension, DataProvider dataProvider, KSet<String> warnedLargeObjects) {
|
||||
this.dimension = dimension;
|
||||
this.dataProvider = dataProvider;
|
||||
this.warnedLargeObjects = warnedLargeObjects;
|
||||
this.extents = new HashMap<>();
|
||||
this.caveVacuumModes = caveVacuumModes(dimension, dataProvider);
|
||||
}
|
||||
|
||||
private PlacementReach dimension() {
|
||||
PlacementReach reach = PlacementReach.NONE;
|
||||
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
|
||||
if (region != null) {
|
||||
reach = reach.union(placements(region.getObjects())).union(procedural(region.getProceduralObjects()));
|
||||
}
|
||||
}
|
||||
for (IrisBiome biome : dimension.getReachableBiomes(dataProvider)) {
|
||||
if (biome != null) {
|
||||
reach = reach.union(placements(biome.getObjects())).union(procedural(biome.getProceduralObjects()));
|
||||
}
|
||||
}
|
||||
return reach.withReadMargin(PlacementReach.supportReadMargin(dimension.getObjectSurfaceSupportBuffer()));
|
||||
}
|
||||
|
||||
private PlacementReach upperDimension() {
|
||||
PlacementReach reach = PlacementReach.NONE;
|
||||
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
|
||||
if (region != null) {
|
||||
reach = reach.union(upperPlacements(region.getSurfaceObjects()));
|
||||
}
|
||||
}
|
||||
for (IrisBiome biome : dimension.getReachableBiomes(dataProvider)) {
|
||||
if (biome != null) {
|
||||
reach = reach.union(upperPlacements(biome.getSurfaceObjects()));
|
||||
}
|
||||
}
|
||||
return reach;
|
||||
}
|
||||
|
||||
private PlacementReach placements(KList<IrisObjectPlacement> placements) {
|
||||
PlacementReach reach = PlacementReach.NONE;
|
||||
if (placements == null) {
|
||||
return reach;
|
||||
}
|
||||
for (IrisObjectPlacement placement : placements) {
|
||||
if (placement != null) {
|
||||
reach = reach.union(objects(placement, profiles(placement)));
|
||||
}
|
||||
}
|
||||
return reach;
|
||||
}
|
||||
|
||||
private PlacementReach upperPlacements(KList<IrisObjectPlacement> placements) {
|
||||
PlacementReach reach = PlacementReach.NONE;
|
||||
if (placements == null) {
|
||||
return reach;
|
||||
}
|
||||
for (IrisObjectPlacement placement : placements) {
|
||||
if (placement == null) {
|
||||
continue;
|
||||
}
|
||||
IrisObjectPlacement upper = placement.toPlacement(placement.getPlace().toArray(new String[0]));
|
||||
upper.setMode(ObjectPlaceMode.CENTER_HEIGHT);
|
||||
upper.setRotation(upsideDownRotation());
|
||||
upper.setCarvingSupport(CarvingMode.ANYWHERE);
|
||||
reach = reach.union(objects(placement, new KList<>(PlacementReach.profile(upper))));
|
||||
}
|
||||
return reach;
|
||||
}
|
||||
|
||||
private PlacementReach objects(IrisObjectPlacement placement, KList<PlacementReach.Profile> profiles) {
|
||||
PlacementReach reach = PlacementReach.NONE;
|
||||
double scale = placement.getMaximumScale(dimension);
|
||||
for (String objectKey : placement.getPlace()) {
|
||||
try {
|
||||
PlacementReach.Extent extent = extent(objectKey);
|
||||
if (extent == null) {
|
||||
continue;
|
||||
}
|
||||
PlacementReach.Extent scaled = extent.scaled(scale);
|
||||
for (PlacementReach.Profile profile : profiles) {
|
||||
PlacementReach objectReach = profile.of(scaled);
|
||||
if (objectReach.horizontal() > 128 && warnedLargeObjects.add(objectKey)) {
|
||||
IrisLogging.warn("Object " + objectKey + " has a large placement reach (" + objectReach.horizontal() + " blocks) and may increase memory usage!");
|
||||
}
|
||||
reach = reach.union(objectReach);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
return reach;
|
||||
}
|
||||
|
||||
private PlacementReach procedural(IrisProceduralObjects procedural) {
|
||||
PlacementReach reach = PlacementReach.NONE;
|
||||
if (procedural == null || procedural.isEmpty()) {
|
||||
return reach;
|
||||
}
|
||||
for (IrisProceduralPlacement placement : procedural.getAllPlacements()) {
|
||||
if (placement == null) {
|
||||
continue;
|
||||
}
|
||||
KList<IrisObject> variants = placement.getVariantObjects(dataProvider.getData());
|
||||
if (variants == null) {
|
||||
continue;
|
||||
}
|
||||
IrisObjectPlacement objectPlacement = placement.asPlacement();
|
||||
KList<PlacementReach.Profile> profiles = profiles(objectPlacement);
|
||||
double scale = objectPlacement.getMaximumScale(dimension);
|
||||
for (IrisObject variant : variants) {
|
||||
if (variant == null) {
|
||||
continue;
|
||||
}
|
||||
PlacementReach.Extent extent = PlacementReach.Extent.of(variant).scaled(scale);
|
||||
for (PlacementReach.Profile profile : profiles) {
|
||||
reach = reach.union(profile.of(extent));
|
||||
}
|
||||
}
|
||||
}
|
||||
return reach;
|
||||
}
|
||||
|
||||
private KList<PlacementReach.Profile> profiles(IrisObjectPlacement placement) {
|
||||
KList<PlacementReach.Profile> profiles = new KList<>(PlacementReach.profile(placement));
|
||||
if (placement.getMode() != ObjectPlaceMode.CENTER_HEIGHT
|
||||
|| placement.getCarvingSupport() == CarvingMode.SURFACE_ONLY) {
|
||||
return profiles;
|
||||
}
|
||||
for (ObjectPlaceMode mode : caveVacuumModes) {
|
||||
IrisObjectPlacement cave = placement.toPlacement(placement.getPlace().toArray(new String[0]));
|
||||
cave.setMode(mode);
|
||||
profiles.add(PlacementReach.profile(cave));
|
||||
}
|
||||
return profiles;
|
||||
}
|
||||
|
||||
private PlacementReach.Extent extent(String objectKey) {
|
||||
if (extents.containsKey(objectKey)) {
|
||||
return extents.get(objectKey);
|
||||
}
|
||||
IrisObject object = dataProvider.getData().getObjectLoader().load(objectKey);
|
||||
PlacementReach.Extent extent = object == null ? null : PlacementReach.Extent.of(object);
|
||||
extents.put(objectKey, extent);
|
||||
return extent;
|
||||
}
|
||||
|
||||
private static Set<ObjectPlaceMode> caveVacuumModes(IrisDimension dimension, DataProvider dataProvider) {
|
||||
Set<ObjectPlaceMode> modes = EnumSet.noneOf(ObjectPlaceMode.class);
|
||||
addCaveVacuumMode(modes, dimension.getCaveProfile());
|
||||
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
|
||||
if (region != null) {
|
||||
addCaveVacuumMode(modes, region.getCaveProfile());
|
||||
}
|
||||
}
|
||||
for (IrisBiome biome : dimension.getReachableBiomes(dataProvider)) {
|
||||
if (biome != null) {
|
||||
addCaveVacuumMode(modes, biome.getCaveProfile());
|
||||
}
|
||||
}
|
||||
return modes;
|
||||
}
|
||||
|
||||
private static void addCaveVacuumMode(Set<ObjectPlaceMode> modes, IrisCaveProfile profile) {
|
||||
if (profile == null) {
|
||||
return;
|
||||
}
|
||||
ObjectPlaceMode mode = profile.getDefaultObjectPlaceMode();
|
||||
if (mode != null && IrisObjectVacuum.isVacuumMode(mode)) {
|
||||
modes.add(mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface SourceChunkConsumer {
|
||||
void accept(int chunkX, int chunkZ);
|
||||
|
||||
@@ -244,7 +244,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
synchronized (chunk) {
|
||||
Matter matter = chunk.getOrCreate(y >> 4);
|
||||
if ((t instanceof NativeBlockState || t instanceof MatterCavern)
|
||||
&& (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z))) {
|
||||
&& hasProtectedHydrology(matter, x, y, z)) {
|
||||
return;
|
||||
}
|
||||
if (t instanceof NativeBlockState) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package art.arcane.iris.generation.mantle;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.EnginePlatformHooks;
|
||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.generation.context.ChunkContext;
|
||||
@@ -23,13 +24,14 @@ import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public interface MatterGenerator {
|
||||
MultiBurst DISPATCHER = MultiBurst.burst;
|
||||
ConcurrentHashMap<MatterTaskKey, MatterComponentTask> IN_FLIGHT_COMPONENTS = new ConcurrentHashMap<>();
|
||||
int COMPONENT_SUBMISSION_BATCH_SIZE = Math.max(1, Math.min(8, Runtime.getRuntime().availableProcessors()));
|
||||
int COMPONENT_SUBMISSION_LIMIT = Math.max(1, Math.min(8, Runtime.getRuntime().availableProcessors()));
|
||||
long COMPONENT_TASK_POLL_MS = 1000L;
|
||||
long COMPONENT_TASK_TIMEOUT_MS = Long.getLong("iris.mantle.componentTimeout", 120000L);
|
||||
|
||||
@@ -87,7 +89,7 @@ public interface MatterGenerator {
|
||||
if (passPlans.length == 0) {
|
||||
return;
|
||||
}
|
||||
int prefetchRadius = passPlans[0].passChunkRadius();
|
||||
int prefetchRadius = phasePrefetchRadius(passPlans, phase);
|
||||
LongOpenHashSet partialChunks = new LongOpenHashSet();
|
||||
List<MantleComponent> requiredComponents = enabledComponents();
|
||||
boolean terrainOnAccess = false;
|
||||
@@ -104,8 +106,10 @@ public interface MatterGenerator {
|
||||
// close() releases the cached chunks, even when a pass throws, or detached pool threads
|
||||
// write into released chunks.
|
||||
List<MatterComponentTask> outstandingTasks = null;
|
||||
List<MatterComponentTask> submittedTasks = multicore && !DISPATCHER.ownsCurrentThread()
|
||||
? new ArrayList<>(COMPONENT_SUBMISSION_BATCH_SIZE)
|
||||
List<DeferredComponent> deferredComponents = null;
|
||||
IrisEngine.GenerationRuntimeBinding generationRuntime = scopedGenerationRuntime();
|
||||
SubmissionWindow submissions = multicore && !DISPATCHER.ownsCurrentThread()
|
||||
? new SubmissionWindow()
|
||||
: null;
|
||||
|
||||
try {
|
||||
@@ -229,25 +233,28 @@ public interface MatterGenerator {
|
||||
for (int componentIndex = 0; componentIndex < eligibleComponentCount; componentIndex++) {
|
||||
MantleComponent component = eligibleComponents[componentIndex];
|
||||
outstandingTasks.add(runComponentAsync(
|
||||
chunk, component, writer, finalPassX, finalPassZ, context, submittedTasks));
|
||||
if (submittedTasks != null && submittedTasks.size() >= COMPONENT_SUBMISSION_BATCH_SIZE) {
|
||||
awaitComponentTasks(submittedTasks);
|
||||
}
|
||||
chunk, component, writer, finalPassX, finalPassZ, context, submissions,
|
||||
generationRuntime));
|
||||
}
|
||||
} else {
|
||||
for (int componentIndex = 0; componentIndex < eligibleComponentCount; componentIndex++) {
|
||||
MantleComponent component = eligibleComponents[componentIndex];
|
||||
runComponentInline(chunk, component, writer, finalPassX, finalPassZ, context);
|
||||
if (!tryRunComponentInline(chunk, component, writer, finalPassX, finalPassZ, context)) {
|
||||
if (deferredComponents == null) {
|
||||
deferredComponents = new ArrayList<>();
|
||||
}
|
||||
deferredComponents.add(new DeferredComponent(chunk, component, finalPassX, finalPassZ));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (asyncComponents) {
|
||||
runQueuedComponentTasks(outstandingTasks, generationRuntime);
|
||||
awaitComponentTasks(outstandingTasks);
|
||||
if (submittedTasks != null) {
|
||||
submittedTasks.clear();
|
||||
}
|
||||
} else if (deferredComponents != null) {
|
||||
runDeferredComponents(deferredComponents, writer, context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,6 +327,25 @@ public interface MatterGenerator {
|
||||
return new MatterGenerationPlan(plans, writerChunkRadius * 2, phase);
|
||||
}
|
||||
|
||||
private int phasePrefetchRadius(MatterPassPlan[] passPlans, MatterGenerationPhase phase) {
|
||||
int radius = 0;
|
||||
for (MatterPassPlan passPlan : passPlans) {
|
||||
if (passPlan.passChunkRadius() > radius && generatesInPhase(passPlan.pass(), phase)) {
|
||||
radius = passPlan.passChunkRadius();
|
||||
}
|
||||
}
|
||||
return radius;
|
||||
}
|
||||
|
||||
private boolean generatesInPhase(MantlePass pass, MatterGenerationPhase phase) {
|
||||
for (MantleComponent component : pass.components()) {
|
||||
if (phase.includes(component) && shouldGenerateComponent(component)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<MantleComponent> enabledComponents() {
|
||||
List<MantleComponent> components = new ArrayList<>();
|
||||
for (MantlePass pass : getComponents()) {
|
||||
@@ -360,6 +386,19 @@ public interface MatterGenerator {
|
||||
return true;
|
||||
}
|
||||
|
||||
private IrisEngine.GenerationRuntimeBinding scopedGenerationRuntime() {
|
||||
return getEngine() instanceof IrisEngine irisEngine && irisEngine.hasGenerationRuntimeScope()
|
||||
? irisEngine.captureGenerationRuntimeBinding()
|
||||
: null;
|
||||
}
|
||||
|
||||
private IrisEngine.GenerationRuntimeScope openGenerationRuntime(IrisEngine.GenerationRuntimeBinding binding) {
|
||||
if (binding == null || !(getEngine() instanceof IrisEngine irisEngine)) {
|
||||
return null;
|
||||
}
|
||||
return irisEngine.openGenerationRuntimeScope(binding);
|
||||
}
|
||||
|
||||
private boolean shouldGenerateComponent(MantleComponent component) {
|
||||
if (!component.isEnabled()) {
|
||||
return false;
|
||||
@@ -376,7 +415,8 @@ public interface MatterGenerator {
|
||||
int chunkX,
|
||||
int chunkZ,
|
||||
ChunkContext context,
|
||||
List<MatterComponentTask> submittedTasks
|
||||
SubmissionWindow submissions,
|
||||
IrisEngine.GenerationRuntimeBinding generationRuntime
|
||||
) {
|
||||
MantleFlag flag = component.getFlag();
|
||||
MatterTaskKey key = new MatterTaskKey(getMantle(), chunkX, chunkZ, flag);
|
||||
@@ -386,29 +426,45 @@ public interface MatterGenerator {
|
||||
return task;
|
||||
}
|
||||
|
||||
if (submissions == null) {
|
||||
MatterComponentTask existing = IN_FLIGHT_COMPONENTS.putIfAbsent(key, task);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
try {
|
||||
if (DISPATCHER.ownsCurrentThread()) {
|
||||
// A pool thread runs its claim inline: a task queued behind pool threads that block
|
||||
// on unmanaged waits (structure builds, cache locks) could starve the whole pool.
|
||||
completeComponentTask(task, chunk, component, writer, chunkX, chunkZ, context, IrisContext.get());
|
||||
return task;
|
||||
} catch (Throwable throwable) {
|
||||
task.cancel(throwable);
|
||||
throw throwable;
|
||||
}
|
||||
}
|
||||
|
||||
MatterComponentTask running = IN_FLIGHT_COMPONENTS.get(key);
|
||||
if (running != null) {
|
||||
return running;
|
||||
}
|
||||
boolean submit = submissions.reserve();
|
||||
MatterComponentTask existing = IN_FLIGHT_COMPONENTS.putIfAbsent(key, task);
|
||||
if (existing != null) {
|
||||
if (submit) {
|
||||
submissions.release();
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
try {
|
||||
if (!submit) {
|
||||
completeComponentTask(task, chunk, component, writer, chunkX, chunkZ, context, null);
|
||||
return task;
|
||||
}
|
||||
IrisContext callerContext = IrisContext.get();
|
||||
task.setSubmission(DISPATCHER.submit(() -> completeComponentTask(
|
||||
task,
|
||||
chunk,
|
||||
component,
|
||||
writer,
|
||||
chunkX,
|
||||
chunkZ,
|
||||
context,
|
||||
callerContext)));
|
||||
submittedTasks.add(task);
|
||||
submissions.track(task);
|
||||
Runnable work = () -> runComponentTask(
|
||||
task, chunk, component, writer, chunkX, chunkZ, context, callerContext, generationRuntime);
|
||||
task.queue(work, generationRuntime);
|
||||
task.setSubmission(DISPATCHER.submit(work));
|
||||
return task;
|
||||
} catch (Throwable throwable) {
|
||||
task.cancel(throwable);
|
||||
@@ -416,6 +472,56 @@ public interface MatterGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean tryRunComponentInline(
|
||||
MantleChunk<Matter> chunk,
|
||||
MantleComponent component,
|
||||
MantleWriter writer,
|
||||
int chunkX,
|
||||
int chunkZ,
|
||||
ChunkContext context
|
||||
) {
|
||||
MantleFlag flag = component.getFlag();
|
||||
if (!chunk.tryClaim(flag)) {
|
||||
return chunk.isFlagged(flag);
|
||||
}
|
||||
boolean generated = false;
|
||||
try {
|
||||
writer.withComponentPriority(component.getPriority(),
|
||||
() -> component.generateLayer(writer, chunkX, chunkZ, context));
|
||||
generated = true;
|
||||
} finally {
|
||||
if (generated) {
|
||||
chunk.completeClaim(flag);
|
||||
} else {
|
||||
chunk.abandonClaim(flag);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void runDeferredComponents(List<DeferredComponent> deferredComponents, MantleWriter writer,
|
||||
ChunkContext context) {
|
||||
for (DeferredComponent deferred : deferredComponents) {
|
||||
runComponentInline(deferred.chunk(), deferred.component(), writer, deferred.chunkX(), deferred.chunkZ(),
|
||||
context);
|
||||
}
|
||||
deferredComponents.clear();
|
||||
}
|
||||
|
||||
private static void runQueuedComponentTasks(List<MatterComponentTask> tasks,
|
||||
IrisEngine.GenerationRuntimeBinding helperRuntime) {
|
||||
boolean interrupted = Thread.interrupted();
|
||||
try {
|
||||
for (int index = tasks.size() - 1; index >= 0; index--) {
|
||||
tasks.get(index).runIfQueued(helperRuntime);
|
||||
}
|
||||
} finally {
|
||||
if (interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass barrier. Waits for every launched task, even if one of them failed, so that no task is
|
||||
* still writing through the writer once this returns. The first failure is rethrown.
|
||||
@@ -519,19 +625,42 @@ public interface MatterGenerator {
|
||||
int chunkZ,
|
||||
ChunkContext context,
|
||||
IrisContext callerContext
|
||||
) {
|
||||
Throwable failure = runComponentTask(task, chunk, component, writer, chunkX, chunkZ, context, callerContext, null);
|
||||
if (failure instanceof RuntimeException runtime) {
|
||||
throw runtime;
|
||||
}
|
||||
if (failure instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
if (failure != null) {
|
||||
throw new CompletionException(failure);
|
||||
}
|
||||
}
|
||||
|
||||
private Throwable runComponentTask(
|
||||
MatterComponentTask task,
|
||||
MantleChunk<Matter> chunk,
|
||||
MantleComponent component,
|
||||
MantleWriter writer,
|
||||
int chunkX,
|
||||
int chunkZ,
|
||||
ChunkContext context,
|
||||
IrisContext callerContext,
|
||||
IrisEngine.GenerationRuntimeBinding generationRuntime
|
||||
) {
|
||||
if (!task.start()) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
Throwable failure = null;
|
||||
try {
|
||||
runComponentWithContext(chunk, component, writer, chunkX, chunkZ, context, callerContext);
|
||||
runComponentWithContext(chunk, component, writer, chunkX, chunkZ, context, callerContext, generationRuntime);
|
||||
} catch (Throwable throwable) {
|
||||
failure = throwable;
|
||||
throw throwable;
|
||||
} finally {
|
||||
task.finish(failure);
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
|
||||
private void runComponentWithContext(
|
||||
@@ -541,14 +670,16 @@ public interface MatterGenerator {
|
||||
int chunkX,
|
||||
int chunkZ,
|
||||
ChunkContext context,
|
||||
IrisContext callerContext
|
||||
IrisContext callerContext,
|
||||
IrisEngine.GenerationRuntimeBinding generationRuntime
|
||||
) {
|
||||
if (callerContext == null) {
|
||||
if (callerContext == null && generationRuntime == null) {
|
||||
runComponentInline(chunk, component, writer, chunkX, chunkZ, context);
|
||||
return;
|
||||
}
|
||||
|
||||
try (IrisContext.Scope ignored = IrisContext.open(
|
||||
try (IrisEngine.GenerationRuntimeScope runtimeScope = openGenerationRuntime(generationRuntime);
|
||||
IrisContext.Scope contextScope = callerContext == null ? null : IrisContext.open(
|
||||
callerContext.getEngine(),
|
||||
callerContext.getGenerationSessionId(),
|
||||
callerContext.getChunkContext())) {
|
||||
@@ -574,6 +705,8 @@ public interface MatterGenerator {
|
||||
private final MatterTaskKey key;
|
||||
private final CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
private volatile Future<?> submission;
|
||||
private volatile Runnable queuedWork;
|
||||
private volatile IrisEngine.GenerationRuntimeBinding queuedRuntime;
|
||||
private volatile Throwable failure;
|
||||
private boolean started;
|
||||
private boolean slowWarningIssued;
|
||||
@@ -594,6 +727,22 @@ public interface MatterGenerator {
|
||||
return true;
|
||||
}
|
||||
|
||||
synchronized boolean isStarted() {
|
||||
return started;
|
||||
}
|
||||
|
||||
void queue(Runnable work, IrisEngine.GenerationRuntimeBinding runtime) {
|
||||
queuedRuntime = runtime;
|
||||
queuedWork = work;
|
||||
}
|
||||
|
||||
void runIfQueued(IrisEngine.GenerationRuntimeBinding helperRuntime) {
|
||||
Runnable work = queuedWork;
|
||||
if (work != null && !future.isDone() && (helperRuntime == null || helperRuntime == queuedRuntime)) {
|
||||
work.run();
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void setSubmission(Future<?> submission) {
|
||||
this.submission = submission;
|
||||
if (failure != null) {
|
||||
@@ -647,7 +796,7 @@ public interface MatterGenerator {
|
||||
continue;
|
||||
}
|
||||
Future<?> submitted = submission;
|
||||
boolean dropped = submitted != null && submitted.isDone();
|
||||
boolean dropped = submitted != null && submitted.isDone() && !isStarted();
|
||||
long waited = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
|
||||
if (dropped) {
|
||||
IllegalStateException dispatchFailure = new IllegalStateException(
|
||||
@@ -698,6 +847,43 @@ public interface MatterGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
final class SubmissionWindow {
|
||||
private final Semaphore slots = new Semaphore(COMPONENT_SUBMISSION_LIMIT);
|
||||
private volatile Throwable failure;
|
||||
|
||||
boolean reserve() {
|
||||
Throwable observed = failure;
|
||||
if (observed != null) {
|
||||
throw new CompletionException(observed);
|
||||
}
|
||||
if (!slots.tryAcquire()) {
|
||||
return false;
|
||||
}
|
||||
observed = failure;
|
||||
if (observed != null) {
|
||||
slots.release();
|
||||
throw new CompletionException(observed);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void release() {
|
||||
slots.release();
|
||||
}
|
||||
|
||||
void track(MatterComponentTask task) {
|
||||
task.future.whenComplete((ignored, throwable) -> {
|
||||
if (throwable != null && failure == null) {
|
||||
failure = throwable;
|
||||
}
|
||||
slots.release();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
record DeferredComponent(MantleChunk<Matter> chunk, MantleComponent component, int chunkX, int chunkZ) {
|
||||
}
|
||||
|
||||
record MatterPassPlan(MantlePass pass, int passChunkRadius, int downstreamBlockRadius) {
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
final class MutationLog {
|
||||
private static final byte CUSTOM = 1;
|
||||
private static final byte CANOPY = 2;
|
||||
|
||||
private int size;
|
||||
private int[] x = new int[32];
|
||||
private int[] y = new int[32];
|
||||
private int[] z = new int[32];
|
||||
private Object[] values = new Object[32];
|
||||
private byte[] flags = new byte[32];
|
||||
|
||||
static MutationLog of(List<ObjectDestinationTransaction.Mutation> mutations) {
|
||||
MutationLog log = new MutationLog();
|
||||
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
|
||||
Objects.requireNonNull(mutation, "mutation");
|
||||
ObjectDestinationTransaction.DataKey key = mutation.key();
|
||||
switch (mutation) {
|
||||
case ObjectDestinationTransaction.SetMutation set -> {
|
||||
Object value = Objects.requireNonNull(set.value(), "mutation value");
|
||||
if (ObjectSourcePlan.typeOf(value) != key.type()) {
|
||||
throw new IllegalArgumentException("Object mutation type " + key.type().getName()
|
||||
+ " does not match its value " + value.getClass().getName());
|
||||
}
|
||||
log.add(key.x(), key.y(), key.z(), value, false, set.canopy());
|
||||
}
|
||||
case ObjectDestinationTransaction.CustomBlockMutation block -> log.add(key.x(), key.y(), key.z(),
|
||||
Objects.requireNonNull(block.state(), "custom block state"), true, block.canopy());
|
||||
}
|
||||
}
|
||||
return log;
|
||||
}
|
||||
|
||||
void add(int blockX, int blockY, int blockZ, Object value, boolean custom, boolean canopy) {
|
||||
if (size == values.length) {
|
||||
int capacity = size * 2;
|
||||
x = Arrays.copyOf(x, capacity);
|
||||
y = Arrays.copyOf(y, capacity);
|
||||
z = Arrays.copyOf(z, capacity);
|
||||
values = Arrays.copyOf(values, capacity);
|
||||
flags = Arrays.copyOf(flags, capacity);
|
||||
}
|
||||
x[size] = blockX;
|
||||
y[size] = blockY;
|
||||
z[size] = blockZ;
|
||||
values[size] = value;
|
||||
flags[size] = (byte) ((custom ? CUSTOM : 0) | (canopy ? CANOPY : 0));
|
||||
size++;
|
||||
}
|
||||
|
||||
int size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
int x(int index) {
|
||||
return x[index];
|
||||
}
|
||||
|
||||
int y(int index) {
|
||||
return y[index];
|
||||
}
|
||||
|
||||
int z(int index) {
|
||||
return z[index];
|
||||
}
|
||||
|
||||
Object value(int index) {
|
||||
return values[index];
|
||||
}
|
||||
|
||||
boolean custom(int index) {
|
||||
return (flags[index] & CUSTOM) != 0;
|
||||
}
|
||||
|
||||
boolean canopy(int index) {
|
||||
return (flags[index] & CANOPY) != 0;
|
||||
}
|
||||
|
||||
Class<?> type(int index) {
|
||||
return custom(index) ? NativeBlockState.class : ObjectSourcePlan.typeOf(values[index]);
|
||||
}
|
||||
}
|
||||
+110
-75
@@ -8,7 +8,6 @@ import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.structure.object.IObjectPlacer;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.math.BlockPosition;
|
||||
import art.arcane.iris.generation.block.B;
|
||||
@@ -18,10 +17,11 @@ import org.jetbrains.annotations.Nullable;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||
import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -32,7 +32,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
private final int destinationChunkX;
|
||||
private final int destinationChunkZ;
|
||||
private final int worldHeight;
|
||||
private final List<Mutation> mutations;
|
||||
private final MutationLog log = new MutationLog();
|
||||
private final List<PlacementRange> placements = new ArrayList<>();
|
||||
private final Long2ObjectOpenHashMap<Int2ObjectOpenHashMap<OverlayCell>> overlay;
|
||||
private BlockPosition canopyPosition;
|
||||
@@ -40,13 +40,17 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
private int hydrologyGateChunkX;
|
||||
private int hydrologyGateChunkZ;
|
||||
private boolean hydrologyGateOpen;
|
||||
private boolean subterrainKnown;
|
||||
private int subterrainX;
|
||||
private int subterrainY;
|
||||
private int subterrainZ;
|
||||
private boolean subterrainProtects;
|
||||
|
||||
ObjectDestinationTransaction(MantleWriter writer, int destinationChunkX, int destinationChunkZ) {
|
||||
this.writer = writer;
|
||||
this.destinationChunkX = destinationChunkX;
|
||||
this.destinationChunkZ = destinationChunkZ;
|
||||
this.worldHeight = writer.getMantle().getWorldHeight();
|
||||
this.mutations = new ArrayList<>();
|
||||
this.overlay = new Long2ObjectOpenHashMap<>();
|
||||
}
|
||||
|
||||
@@ -55,11 +59,11 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
}
|
||||
|
||||
int mutationCheckpoint() {
|
||||
return mutations.size();
|
||||
return log.size();
|
||||
}
|
||||
|
||||
ObjectSourcePlan sourcePlanSince(int checkpoint) {
|
||||
if (checkpoint < 0 || checkpoint > mutations.size()) {
|
||||
if (checkpoint < 0 || checkpoint > log.size()) {
|
||||
throw new IllegalArgumentException("Mutation checkpoint is outside the transaction");
|
||||
}
|
||||
ArrayList<PlacementRange> accepted = new ArrayList<>();
|
||||
@@ -68,65 +72,91 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
accepted.add(new PlacementRange(placement.start() - checkpoint, placement.end() - checkpoint));
|
||||
}
|
||||
}
|
||||
return new ObjectSourcePlan(mutations.subList(checkpoint, mutations.size()), accepted);
|
||||
return new ObjectSourcePlan(log, checkpoint, accepted);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int beginObjectPlacement() {
|
||||
return mutations.size();
|
||||
return log.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endObjectPlacement(int checkpoint) {
|
||||
if (checkpoint >= 0 && checkpoint < mutations.size()) {
|
||||
placements.add(new PlacementRange(checkpoint, mutations.size()));
|
||||
if (checkpoint >= 0 && checkpoint < log.size()) {
|
||||
placements.add(new PlacementRange(checkpoint, log.size()));
|
||||
}
|
||||
}
|
||||
|
||||
void apply(ObjectSourcePlan plan) {
|
||||
int slot = plan.destinationSlot(destinationChunkX, destinationChunkZ);
|
||||
if (slot < 0) {
|
||||
return;
|
||||
}
|
||||
int baseX = destinationChunkX << 4;
|
||||
int baseZ = destinationChunkZ << 4;
|
||||
boolean skipCanopy = false;
|
||||
for (Mutation mutation : plan.mutationsFor(destinationChunkX, destinationChunkZ)) {
|
||||
if (mutation.key().type() == NativeBlockState.class) {
|
||||
NativeBlockState existing = mutation.canopy() ? get(mutation.key().x(), mutation.key().y(), mutation.key().z()) : null;
|
||||
int end = plan.slotEnd(slot);
|
||||
for (int index = plan.slotStart(slot); index < end; index++) {
|
||||
int cell = plan.cellAt(index);
|
||||
int x = baseX | ObjectSourcePlan.cellX(cell);
|
||||
int y = ObjectSourcePlan.cellY(cell);
|
||||
int z = baseZ | ObjectSourcePlan.cellZ(cell);
|
||||
Object value = plan.valueAt(index);
|
||||
boolean custom = ObjectSourcePlan.isCustom(cell);
|
||||
boolean canopy = ObjectSourcePlan.isCanopy(cell);
|
||||
if (custom || value instanceof NativeBlockState) {
|
||||
NativeBlockState existing = canopy ? get(x, y, z) : null;
|
||||
skipCanopy = existing != null && existing.isSolid();
|
||||
}
|
||||
if (mutation.canopy() && skipCanopy) {
|
||||
if (canopy && skipCanopy) {
|
||||
continue;
|
||||
}
|
||||
switch (mutation) {
|
||||
case SetMutation set -> setData(set.key().x(), set.key().y(), set.key().z(), set.value(), set);
|
||||
case CustomBlockMutation custom -> set(custom.key().x(), custom.key().y(), custom.key().z(), custom.state(), custom);
|
||||
if (custom) {
|
||||
set(x, y, z, (NativeBlockState) value, true, canopy);
|
||||
} else {
|
||||
setData(x, y, z, value, true, canopy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void commitFenced() {
|
||||
LinkedHashMap<DataKey, Object> originals = new LinkedHashMap<>();
|
||||
CommitJournal journal = new CommitJournal();
|
||||
try {
|
||||
for (Mutation mutation : mutations) {
|
||||
if (!isDestination(mutation.x(), mutation.z())) {
|
||||
for (int position = 0; position < log.size(); position++) {
|
||||
int x = log.x(position);
|
||||
int z = log.z(position);
|
||||
if (!isDestination(x, z)) {
|
||||
continue;
|
||||
}
|
||||
captureOriginal(originals, mutation.key());
|
||||
if (mutation.key().type() == NativeBlockState.class) {
|
||||
TileWrapper tile = writer.getDataIfPresent(
|
||||
mutation.key().x(), mutation.key().y(), mutation.key().z(), TileWrapper.class);
|
||||
int y = log.y(position);
|
||||
Object value = log.value(position);
|
||||
boolean custom = log.custom(position);
|
||||
Class<?> type = log.type(position);
|
||||
if (!journal.captured(x, y, z, type)) {
|
||||
journal.capture(x, y, z, type, publishedOrCleared(x, y, z, type));
|
||||
}
|
||||
if (type == NativeBlockState.class) {
|
||||
if (!journal.captured(x, y, z, TileWrapper.class)) {
|
||||
TileWrapper tile = writer.getDataIfPresent(x, y, z, TileWrapper.class);
|
||||
if (tile != null) {
|
||||
originals.putIfAbsent(new DataKey(
|
||||
mutation.key().x(), mutation.key().y(), mutation.key().z(), TileWrapper.class), tile);
|
||||
}
|
||||
Identifier identifier = writer.getDataIfPresent(
|
||||
mutation.key().x(), mutation.key().y(), mutation.key().z(), Identifier.class);
|
||||
if (identifier != null || mutation instanceof CustomBlockMutation) {
|
||||
originals.putIfAbsent(new DataKey(
|
||||
mutation.key().x(), mutation.key().y(), mutation.key().z(), Identifier.class),
|
||||
identifier == null ? CLEARED : identifier);
|
||||
journal.capture(x, y, z, TileWrapper.class, tile);
|
||||
}
|
||||
}
|
||||
mutation.apply(writer);
|
||||
if (!journal.captured(x, y, z, Identifier.class)) {
|
||||
Identifier identifier = writer.getDataIfPresent(x, y, z, Identifier.class);
|
||||
if (identifier != null || custom) {
|
||||
journal.capture(x, y, z, Identifier.class, identifier == null ? CLEARED : identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (custom) {
|
||||
writer.set(x, y, z, (NativeBlockState) value);
|
||||
} else {
|
||||
writer.setData(x, y, z, value);
|
||||
}
|
||||
}
|
||||
} catch (Throwable failure) {
|
||||
rollback(originals, failure);
|
||||
journal.rollback(failure);
|
||||
if (failure instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
@@ -149,10 +179,10 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
|
||||
@Override
|
||||
public void set(int x, int y, int z, NativeBlockState state) {
|
||||
set(x, y, z, state, null);
|
||||
set(x, y, z, state, false, false);
|
||||
}
|
||||
|
||||
private void set(int x, int y, int z, NativeBlockState state, CustomBlockMutation replayed) {
|
||||
private void set(int x, int y, int z, NativeBlockState state, boolean replayed, boolean replayedCanopy) {
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
@@ -167,8 +197,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
cell.block = baseState;
|
||||
cell.tile = CLEARED;
|
||||
cell.identifier = identifier;
|
||||
mutations.add(replayed != null
|
||||
? replayed : new CustomBlockMutation(new DataKey(x, y, z, NativeBlockState.class), state, isCanopy(x, y, z)));
|
||||
log.add(x, y, z, state, true, replayed ? replayedCanopy : isCanopy(x, y, z));
|
||||
return;
|
||||
}
|
||||
setData(x, y, z, state);
|
||||
@@ -262,10 +291,10 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
|
||||
@Override
|
||||
public <T> void setData(int x, int y, int z, T data) {
|
||||
setData(x, y, z, data, null);
|
||||
setData(x, y, z, data, false, false);
|
||||
}
|
||||
|
||||
private void setData(int x, int y, int z, Object data, SetMutation replayed) {
|
||||
private void setData(int x, int y, int z, Object data, boolean replayed, boolean replayedCanopy) {
|
||||
if (data == null || y < 0 || y >= worldHeight
|
||||
|| protectsSubterrain(x, y, z)) {
|
||||
return;
|
||||
@@ -285,8 +314,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
} else {
|
||||
cell.put(type, data);
|
||||
}
|
||||
mutations.add(replayed != null
|
||||
? replayed : new SetMutation(new DataKey(x, y, z, type), data, isCanopy(x, y, z)));
|
||||
log.add(x, y, z, data, false, replayed ? replayedCanopy : isCanopy(x, y, z));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -368,18 +396,6 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
return value == null ? CLEARED : value;
|
||||
}
|
||||
|
||||
private void captureOriginal(LinkedHashMap<DataKey, Object> originals, DataKey key) {
|
||||
originals.computeIfAbsent(
|
||||
key,
|
||||
candidate -> publishedOrCleared(
|
||||
candidate.x(),
|
||||
candidate.y(),
|
||||
candidate.z(),
|
||||
candidate.type()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private boolean isDestination(int x, int z) {
|
||||
return (x >> 4) == destinationChunkX && (z >> 4) == destinationChunkZ;
|
||||
}
|
||||
@@ -396,8 +412,17 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
}
|
||||
|
||||
private boolean protectsSubterrain(int x, int y, int z) {
|
||||
if (subterrainKnown && x == subterrainX && y == subterrainY && z == subterrainZ) {
|
||||
return subterrainProtects;
|
||||
}
|
||||
Engine engine = getEngine();
|
||||
return engine != null && SubterrainRasterizer.protectsPlacement(engine.getSubterrainCell(x, y, z));
|
||||
boolean protects = engine != null && SubterrainRasterizer.protectsPlacement(engine.getSubterrainCell(x, y, z));
|
||||
subterrainX = x;
|
||||
subterrainY = y;
|
||||
subterrainZ = z;
|
||||
subterrainProtects = protects;
|
||||
subterrainKnown = true;
|
||||
return protects;
|
||||
}
|
||||
|
||||
private boolean hasProtectedHydrology(int x, int y, int z) {
|
||||
@@ -419,14 +444,32 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
return hydrologyGateOpen;
|
||||
}
|
||||
|
||||
private void rollback(LinkedHashMap<DataKey, Object> originals, Throwable failure) {
|
||||
ArrayList<Map.Entry<DataKey, Object>> entries = new ArrayList<>(originals.entrySet());
|
||||
for (int index = entries.size() - 1; index >= 0; index--) {
|
||||
Map.Entry<DataKey, Object> entry = entries.get(index);
|
||||
DataKey key = entry.getKey();
|
||||
private final class CommitJournal {
|
||||
private final Reference2IntOpenHashMap<Class<?>> typeIds = new Reference2IntOpenHashMap<>();
|
||||
private final LongOpenHashSet keys = new LongOpenHashSet();
|
||||
private final ArrayList<Original> originals = new ArrayList<>();
|
||||
|
||||
private long key(int x, int y, int z, Class<?> type) {
|
||||
int typeId = typeIds.computeIfAbsent(type, (Class<?> ignored) -> typeIds.size());
|
||||
return ((long) typeId << 32) | ((long) y << 8) | ((long) (z & 15) << 4) | (x & 15);
|
||||
}
|
||||
|
||||
private boolean captured(int x, int y, int z, Class<?> type) {
|
||||
return keys.contains(key(x, y, z, type));
|
||||
}
|
||||
|
||||
private void capture(int x, int y, int z, Class<?> type, Object value) {
|
||||
if (keys.add(key(x, y, z, type))) {
|
||||
originals.add(new Original(x, y, z, type, value));
|
||||
}
|
||||
}
|
||||
|
||||
private void rollback(Throwable failure) {
|
||||
for (int index = originals.size() - 1; index >= 0; index--) {
|
||||
Original original = originals.get(index);
|
||||
try {
|
||||
writer.restoreData(key.x(), key.y(), key.z(), key.type(),
|
||||
entry.getValue() == CLEARED ? null : entry.getValue());
|
||||
writer.restoreData(original.x(), original.y(), original.z(), original.type(),
|
||||
original.value() == CLEARED ? null : original.value());
|
||||
} catch (Throwable rollbackFailure) {
|
||||
if (rollbackFailure != failure) {
|
||||
failure.addSuppressed(rollbackFailure);
|
||||
@@ -434,6 +477,10 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record Original(int x, int y, int z, Class<?> type, Object value) {
|
||||
}
|
||||
|
||||
private static final class OverlayCell {
|
||||
private Object block;
|
||||
@@ -506,8 +553,6 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
DataKey key();
|
||||
|
||||
boolean canopy();
|
||||
|
||||
void apply(IObjectPlacer placer);
|
||||
}
|
||||
|
||||
record SetMutation(DataKey key, Object value, boolean canopy) implements Mutation {
|
||||
@@ -520,11 +565,6 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
public int z() {
|
||||
return key.z();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(IObjectPlacer placer) {
|
||||
placer.setData(key.x(), key.y(), key.z(), value);
|
||||
}
|
||||
}
|
||||
|
||||
record CustomBlockMutation(DataKey key, NativeBlockState state, boolean canopy) implements Mutation {
|
||||
@@ -537,10 +577,5 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
public int z() {
|
||||
return key.z();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(IObjectPlacer placer) {
|
||||
placer.set(key.x(), key.y(), key.z(), state);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
final class ObjectSourcePlan {
|
||||
static final ObjectSourcePlan EMPTY = new ObjectSourcePlan(List.of());
|
||||
@@ -45,18 +44,21 @@ final class ObjectSourcePlan {
|
||||
private final int estimatedRetainedBytes;
|
||||
|
||||
ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations) {
|
||||
this(mutations, List.of());
|
||||
this(MutationLog.of(mutations), 0, List.of());
|
||||
}
|
||||
|
||||
ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations, List<ObjectDestinationTransaction.PlacementRange> accepted) {
|
||||
int count = mutations.size();
|
||||
this(MutationLog.of(mutations), 0, accepted);
|
||||
}
|
||||
|
||||
ObjectSourcePlan(MutationLog log, int from, List<ObjectDestinationTransaction.PlacementRange> accepted) {
|
||||
int count = log.size() - from;
|
||||
long[] mutationDestinations = new long[count];
|
||||
Long2IntOpenHashMap destinationSlots = new Long2IntOpenHashMap();
|
||||
destinationSlots.defaultReturnValue(-1);
|
||||
LongArrayList firstSeen = new LongArrayList();
|
||||
for (int index = 0; index < count; index++) {
|
||||
ObjectDestinationTransaction.Mutation mutation = Objects.requireNonNull(mutations.get(index), "mutation");
|
||||
long destination = destinationKey(mutation.x() >> 4, mutation.z() >> 4);
|
||||
long destination = destinationKey(log.x(from + index) >> 4, log.z(from + index) >> 4);
|
||||
mutationDestinations[index] = destination;
|
||||
if (destinationSlots.putIfAbsent(destination, firstSeen.size()) == -1) {
|
||||
firstSeen.add(destination);
|
||||
@@ -80,38 +82,23 @@ final class ObjectSourcePlan {
|
||||
valueIndexes = new int[count];
|
||||
ValueTable table = new ValueTable();
|
||||
for (int index = 0; index < count; index++) {
|
||||
ObjectDestinationTransaction.Mutation mutation = mutations.get(index);
|
||||
int logged = from + index;
|
||||
int position = cursor[destinationSlots.get(mutationDestinations[index])]++;
|
||||
positions[index] = position;
|
||||
ObjectDestinationTransaction.DataKey key = mutation.key();
|
||||
if (key.y() < 0 || key.y() > MAXIMUM_Y) {
|
||||
throw new IllegalArgumentException("Object mutation height is outside the plan range: " + key.y());
|
||||
int y = log.y(logged);
|
||||
if (y < 0 || y > MAXIMUM_Y) {
|
||||
throw new IllegalArgumentException("Object mutation height is outside the plan range: " + y);
|
||||
}
|
||||
Object value;
|
||||
int custom;
|
||||
switch (mutation) {
|
||||
case ObjectDestinationTransaction.SetMutation set -> {
|
||||
value = Objects.requireNonNull(set.value(), "mutation value");
|
||||
if (typeOf(value) != key.type()) {
|
||||
throw new IllegalArgumentException("Object mutation type " + key.type().getName()
|
||||
+ " does not match its value " + value.getClass().getName());
|
||||
}
|
||||
custom = 0;
|
||||
}
|
||||
case ObjectDestinationTransaction.CustomBlockMutation block -> {
|
||||
value = Objects.requireNonNull(block.state(), "custom block state");
|
||||
custom = CUSTOM_BIT;
|
||||
}
|
||||
}
|
||||
cells[position] = (key.y() << Y_SHIFT) | custom | (mutation.canopy() ? CANOPY_BIT : 0) | ((key.z() & 15) << 4) | (key.x() & 15);
|
||||
valueIndexes[position] = table.indexOf(value);
|
||||
cells[position] = (y << Y_SHIFT) | (log.custom(logged) ? CUSTOM_BIT : 0) | (log.canopy(logged) ? CANOPY_BIT : 0)
|
||||
| ((log.z(logged) & 15) << 4) | (log.x(logged) & 15);
|
||||
valueIndexes[position] = table.indexOf(log.value(logged));
|
||||
}
|
||||
values = table.values.toArray();
|
||||
|
||||
ArrayList<Placement> crossing = new ArrayList<>();
|
||||
for (int ordinal = 0; ordinal < accepted.size(); ordinal++) {
|
||||
ObjectDestinationTransaction.PlacementRange range = accepted.get(ordinal);
|
||||
Placement placement = crossingPlacement(ordinal, range.start(), range.end(), mutations, mutationDestinations, destinationSlots, positions);
|
||||
Placement placement = crossingPlacement(ordinal, range.start(), range.end(), log, from, mutationDestinations, destinationSlots, positions);
|
||||
if (placement != null) {
|
||||
crossing.add(placement);
|
||||
}
|
||||
@@ -147,12 +134,13 @@ final class ObjectSourcePlan {
|
||||
int ordinal,
|
||||
int start,
|
||||
int end,
|
||||
List<ObjectDestinationTransaction.Mutation> mutations,
|
||||
MutationLog log,
|
||||
int from,
|
||||
long[] mutationDestinations,
|
||||
Long2IntOpenHashMap destinationSlots,
|
||||
int[] positions
|
||||
) {
|
||||
if (start < 0 || end > mutations.size() || start >= end) {
|
||||
if (start < 0 || end > log.size() - from || start >= end) {
|
||||
throw new IllegalArgumentException("Object placement range is outside the plan");
|
||||
}
|
||||
long first = mutationDestinations[start];
|
||||
@@ -172,11 +160,12 @@ final class ObjectSourcePlan {
|
||||
int maximumX = Integer.MIN_VALUE;
|
||||
int maximumZ = Integer.MIN_VALUE;
|
||||
for (int mutationIndex = start; mutationIndex < end; mutationIndex++) {
|
||||
ObjectDestinationTransaction.Mutation mutation = mutations.get(mutationIndex);
|
||||
minimumX = Math.min(minimumX, mutation.x());
|
||||
minimumZ = Math.min(minimumZ, mutation.z());
|
||||
maximumX = Math.max(maximumX, mutation.x());
|
||||
maximumZ = Math.max(maximumZ, mutation.z());
|
||||
int mutationX = log.x(from + mutationIndex);
|
||||
int mutationZ = log.z(from + mutationIndex);
|
||||
minimumX = Math.min(minimumX, mutationX);
|
||||
minimumZ = Math.min(minimumZ, mutationZ);
|
||||
maximumX = Math.max(maximumX, mutationX);
|
||||
maximumZ = Math.max(maximumZ, mutationZ);
|
||||
long destination = mutationDestinations[mutationIndex];
|
||||
int touchedIndex = touchedIndexes.get(destination);
|
||||
if (touchedIndex < 0) {
|
||||
@@ -216,6 +205,55 @@ final class ObjectSourcePlan {
|
||||
return destinations.length == 0;
|
||||
}
|
||||
|
||||
int slotStart(int slot) {
|
||||
return offsets[slot];
|
||||
}
|
||||
|
||||
int slotEnd(int slot) {
|
||||
return offsets[slot + 1];
|
||||
}
|
||||
|
||||
int cellAt(int index) {
|
||||
return cells[index];
|
||||
}
|
||||
|
||||
Object valueAt(int index) {
|
||||
return values[valueIndexes[index]];
|
||||
}
|
||||
|
||||
static int cellX(int cell) {
|
||||
return cell & 15;
|
||||
}
|
||||
|
||||
static int cellZ(int cell) {
|
||||
return (cell >>> 4) & 15;
|
||||
}
|
||||
|
||||
static int cellY(int cell) {
|
||||
return cell >>> Y_SHIFT;
|
||||
}
|
||||
|
||||
static boolean isCustom(int cell) {
|
||||
return (cell & CUSTOM_BIT) != 0;
|
||||
}
|
||||
|
||||
static boolean isCanopy(int cell) {
|
||||
return (cell & CANOPY_BIT) != 0;
|
||||
}
|
||||
|
||||
ObjectDestinationTransaction.Mutation mutationAt(int chunkX, int chunkZ, int index) {
|
||||
int cell = cells[index];
|
||||
int x = (chunkX << 4) | cellX(cell);
|
||||
int z = (chunkZ << 4) | cellZ(cell);
|
||||
int y = cellY(cell);
|
||||
Object value = valueAt(index);
|
||||
return isCustom(cell)
|
||||
? new ObjectDestinationTransaction.CustomBlockMutation(
|
||||
new ObjectDestinationTransaction.DataKey(x, y, z, NativeBlockState.class), (NativeBlockState) value, isCanopy(cell))
|
||||
: new ObjectDestinationTransaction.SetMutation(
|
||||
new ObjectDestinationTransaction.DataKey(x, y, z, typeOf(value)), value, isCanopy(cell));
|
||||
}
|
||||
|
||||
List<ObjectDestinationTransaction.Mutation> mutationsFor(int chunkX, int chunkZ) {
|
||||
int slot = destinationSlot(chunkX, chunkZ);
|
||||
return slot < 0 ? List.of() : decode(chunkX, chunkZ, offsets[slot], offsets[slot + 1] - offsets[slot]);
|
||||
@@ -266,19 +304,8 @@ final class ObjectSourcePlan {
|
||||
|
||||
private List<ObjectDestinationTransaction.Mutation> decode(int chunkX, int chunkZ, int start, int length) {
|
||||
ObjectDestinationTransaction.Mutation[] decoded = new ObjectDestinationTransaction.Mutation[length];
|
||||
int baseX = chunkX << 4;
|
||||
int baseZ = chunkZ << 4;
|
||||
for (int index = 0; index < length; index++) {
|
||||
int cell = cells[start + index];
|
||||
int x = baseX | (cell & 15);
|
||||
int z = baseZ | ((cell >>> 4) & 15);
|
||||
int y = cell >>> Y_SHIFT;
|
||||
Object value = values[valueIndexes[start + index]];
|
||||
decoded[index] = (cell & CUSTOM_BIT) != 0
|
||||
? new ObjectDestinationTransaction.CustomBlockMutation(
|
||||
new ObjectDestinationTransaction.DataKey(x, y, z, NativeBlockState.class), (NativeBlockState) value, (cell & CANOPY_BIT) != 0)
|
||||
: new ObjectDestinationTransaction.SetMutation(
|
||||
new ObjectDestinationTransaction.DataKey(x, y, z, typeOf(value)), value, (cell & CANOPY_BIT) != 0);
|
||||
decoded[index] = mutationAt(chunkX, chunkZ, start + index);
|
||||
}
|
||||
return List.of(decoded);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import art.arcane.volmlib.util.cache.CacheKey;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
@@ -14,6 +15,7 @@ import java.util.function.Supplier;
|
||||
import static art.arcane.iris.generation.cache.Cache.key;
|
||||
|
||||
final class ObjectSourcePlanCache {
|
||||
static final ObjectSourcePlan BUSY = new ObjectSourcePlan(List.of());
|
||||
private static final long MAXIMUM_RETAINED_BYTES = 512L * 1024L * 1024L;
|
||||
private static final int ENTRY_BYTES = 64;
|
||||
|
||||
@@ -68,6 +70,19 @@ final class ObjectSourcePlanCache {
|
||||
return plan;
|
||||
}
|
||||
|
||||
ObjectSourcePlan tryAcquire(int sourceChunkX, int sourceChunkZ, int destinationChunkX, int destinationChunkZ,
|
||||
int sourceChunkRadius, Supplier<ObjectSourcePlan> builder) {
|
||||
State current = state;
|
||||
long source = CacheKey.mix(key(sourceChunkX, sourceChunkZ));
|
||||
if (current.plans.getIfPresent(source) == null) {
|
||||
Pending pending = current.pending.get(source);
|
||||
if (pending != null && pending.owner != Thread.currentThread()) {
|
||||
return BUSY;
|
||||
}
|
||||
}
|
||||
return acquire(sourceChunkX, sourceChunkZ, destinationChunkX, destinationChunkZ, sourceChunkRadius, builder);
|
||||
}
|
||||
|
||||
private ObjectSourcePlan claim(State current, long source, Entry entry, int destinationChunkX, int destinationChunkZ,
|
||||
Supplier<ObjectSourcePlan> builder) {
|
||||
int slot = entry.slot(destinationChunkX, destinationChunkZ);
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
/*
|
||||
* 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.mantle;
|
||||
|
||||
import art.arcane.iris.generation.block.VectorMap;
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||
import art.arcane.iris.structure.object.IrisObject;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.structure.object.IrisObjectRotation;
|
||||
import art.arcane.iris.structure.object.IrisObjectTranslate;
|
||||
import art.arcane.iris.structure.object.IrisObjectVacuum;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.math.Vector3i;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record PlacementReach(int horizontal, int readMargin) {
|
||||
public static final PlacementReach NONE = new PlacementReach(0, 0);
|
||||
private static final int SPIN_PERIOD = 360;
|
||||
private static final int MAXIMUM_ROTATION_KERNELS = 1024;
|
||||
private static final double ROUNDING_SLACK = 1.0E-9D;
|
||||
|
||||
public PlacementReach {
|
||||
if (horizontal < 0 || readMargin < 0) {
|
||||
throw new IllegalArgumentException("Placement reach must be non-negative: " + horizontal + "+" + readMargin);
|
||||
}
|
||||
}
|
||||
|
||||
public static PlacementReach of(IrisObject object, IrisObjectPlacement placement, double scale) {
|
||||
return of(Extent.of(object).scaled(scale), placement);
|
||||
}
|
||||
|
||||
public static PlacementReach of(Extent extent, IrisObjectPlacement placement) {
|
||||
return profile(placement).of(extent);
|
||||
}
|
||||
|
||||
public static PlacementReach anchored(Extent extent, IrisObjectPlacement placement) {
|
||||
return profile(placement).anchored(extent);
|
||||
}
|
||||
|
||||
public static Profile profile(IrisObjectPlacement placement) {
|
||||
return new Profile(Objects.requireNonNull(placement, "placement"));
|
||||
}
|
||||
|
||||
public static int supportReadMargin(int surfaceSupportBuffer) {
|
||||
return 1 + Math.max(0, surfaceSupportBuffer);
|
||||
}
|
||||
|
||||
public int input() {
|
||||
return saturate((long) horizontal + readMargin);
|
||||
}
|
||||
|
||||
public PlacementReach union(PlacementReach other) {
|
||||
int unionHorizontal = Math.max(horizontal, other.horizontal);
|
||||
int unionInput = Math.max(input(), other.input());
|
||||
return new PlacementReach(unionHorizontal, unionInput - unionHorizontal);
|
||||
}
|
||||
|
||||
public PlacementReach withReadMargin(int minimumReadMargin) {
|
||||
return minimumReadMargin <= readMargin ? this : new PlacementReach(horizontal, minimumReadMargin);
|
||||
}
|
||||
|
||||
private static int roundedBound(double maximumMagnitude) {
|
||||
return saturate((long) Math.floor(maximumMagnitude + 0.5D + ROUNDING_SLACK));
|
||||
}
|
||||
|
||||
private static int saturate(long value) {
|
||||
return value >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) Math.max(0L, value);
|
||||
}
|
||||
|
||||
private static IrisBlockVector corner(Extent extent, int corner) {
|
||||
return new IrisBlockVector(
|
||||
(corner & 1) == 0 ? extent.x().minimum() : extent.x().maximum(),
|
||||
(corner & 2) == 0 ? extent.y().minimum() : extent.y().maximum(),
|
||||
(corner & 4) == 0 ? extent.z().minimum() : extent.z().maximum());
|
||||
}
|
||||
|
||||
public static final class Profile {
|
||||
private final IrisObjectRotation rotation;
|
||||
private final IrisBlockVector translation;
|
||||
private final boolean rotating;
|
||||
private final boolean radial;
|
||||
private final boolean vertical;
|
||||
private final int[] spinsX;
|
||||
private final int[] spinsY;
|
||||
private final int[] spinsZ;
|
||||
private final int additive;
|
||||
private final int readMargin;
|
||||
|
||||
private Profile(IrisObjectPlacement placement) {
|
||||
IrisObjectRotation configuredRotation = placement.getRotation();
|
||||
IrisObjectTranslate translate = placement.getTranslate();
|
||||
this.rotation = configuredRotation;
|
||||
this.translation = translate != null && translate.canTranslate()
|
||||
? new IrisBlockVector(translate.getX(), translate.getY(), translate.getZ())
|
||||
: null;
|
||||
this.rotating = configuredRotation != null && configuredRotation.canRotate();
|
||||
this.spinsX = rotating ? distinctSpins(configuredRotation, Axis.X) : new int[]{0};
|
||||
this.spinsY = rotating ? distinctSpins(configuredRotation, Axis.Y) : new int[]{0};
|
||||
this.spinsZ = rotating ? distinctSpins(configuredRotation, Axis.Z) : new int[]{0};
|
||||
long kernels = (long) spinsX.length * spinsY.length * spinsZ.length;
|
||||
this.radial = placement.isRotateTowardsSlope() || (rotating && kernels > MAXIMUM_ROTATION_KERNELS);
|
||||
this.vertical = placement.isRotateTowardsSlope()
|
||||
|| (rotating && (configuredRotation.canRotateX() || configuredRotation.canRotateZ()));
|
||||
this.additive = saturate((long) warpReach(placement.getWarp()) + vacuumReach(placement));
|
||||
this.readMargin = supportReadMargin(placement.getSurfaceSupportBuffer());
|
||||
}
|
||||
|
||||
public PlacementReach of(Extent extent) {
|
||||
Objects.requireNonNull(extent, "extent");
|
||||
return new PlacementReach(saturate((long) rotatedReach(extent) + additive), readMargin);
|
||||
}
|
||||
|
||||
public PlacementReach anchored(Extent extent) {
|
||||
Objects.requireNonNull(extent, "extent");
|
||||
long span = Math.max(extent.x().span(), extent.z().span());
|
||||
return new PlacementReach(saturate(span + additive), readMargin);
|
||||
}
|
||||
|
||||
private int rotatedReach(Extent extent) {
|
||||
if (radial) {
|
||||
return radialReach(extent);
|
||||
}
|
||||
if (!rotating) {
|
||||
return unrotatedReach(extent);
|
||||
}
|
||||
double maximum = 0D;
|
||||
for (int spinX : spinsX) {
|
||||
for (int spinY : spinsY) {
|
||||
for (int spinZ : spinsZ) {
|
||||
maximum = Math.max(maximum, rotatedCornerMaximum(extent, spinX, spinY, spinZ));
|
||||
}
|
||||
}
|
||||
}
|
||||
return roundedBound(maximum);
|
||||
}
|
||||
|
||||
private double rotatedCornerMaximum(Extent extent, int spinX, int spinY, int spinZ) {
|
||||
IrisBlockVector offset = translation == null ? null : rotation.rotate(translation, spinX, spinY, spinZ);
|
||||
double maximum = 0D;
|
||||
for (int corner = 0; corner < 8; corner++) {
|
||||
IrisBlockVector rotated = rotation.rotate(corner(extent, corner), spinX, spinY, spinZ);
|
||||
double x = offset == null ? rotated.getX() : rotated.getX() + offset.getX();
|
||||
double z = offset == null ? rotated.getZ() : rotated.getZ() + offset.getZ();
|
||||
maximum = Math.max(maximum, Math.max(Math.abs(x), Math.abs(z)));
|
||||
}
|
||||
return maximum;
|
||||
}
|
||||
|
||||
private int unrotatedReach(Extent extent) {
|
||||
double translateX = translation == null ? 0D : translation.getX();
|
||||
double translateZ = translation == null ? 0D : translation.getZ();
|
||||
double x = Math.max(Math.abs(extent.x().minimum() + translateX), Math.abs(extent.x().maximum() + translateX));
|
||||
double z = Math.max(Math.abs(extent.z().minimum() + translateZ), Math.abs(extent.z().maximum() + translateZ));
|
||||
return roundedBound(Math.max(x, z));
|
||||
}
|
||||
|
||||
private int radialReach(Extent extent) {
|
||||
double translateX = translation == null ? 0D : translation.getX();
|
||||
double translateY = translation == null ? 0D : translation.getY();
|
||||
double translateZ = translation == null ? 0D : translation.getZ();
|
||||
double maximum = 0D;
|
||||
for (int corner = 0; corner < 8; corner++) {
|
||||
IrisBlockVector position = corner(extent, corner);
|
||||
double x = position.getX() + translateX;
|
||||
double y = vertical ? position.getY() + translateY : 0D;
|
||||
double z = position.getZ() + translateZ;
|
||||
maximum = Math.max(maximum, Math.sqrt((x * x) + (y * y) + (z * z)));
|
||||
}
|
||||
return roundedBound(maximum);
|
||||
}
|
||||
|
||||
private static int warpReach(IrisGeneratorStyle warp) {
|
||||
if (warp == null || warp.isFlat()) {
|
||||
return 0;
|
||||
}
|
||||
return saturate((long) Math.ceil(Math.abs(warp.getMultiplier()) / 2D));
|
||||
}
|
||||
|
||||
private static int vacuumReach(IrisObjectPlacement placement) {
|
||||
if (!IrisObjectVacuum.isVacuumMode(placement.getMode())) {
|
||||
return 0;
|
||||
}
|
||||
return IrisObjectVacuum.resolveRadius(placement.getMode(), placement.getVacuumSettings());
|
||||
}
|
||||
|
||||
private static int[] distinctSpins(IrisObjectRotation rotation, Axis axis) {
|
||||
boolean rotates = switch (axis) {
|
||||
case X -> rotation.canRotateX();
|
||||
case Y -> rotation.canRotateY();
|
||||
case Z -> rotation.canRotateZ();
|
||||
};
|
||||
if (!rotates) {
|
||||
return new int[]{0};
|
||||
}
|
||||
LongOpenHashSet angles = new LongOpenHashSet();
|
||||
IntArrayList spins = new IntArrayList();
|
||||
for (int spin = 0; spin < SPIN_PERIOD; spin++) {
|
||||
double angle = switch (axis) {
|
||||
case X -> rotation.getXRotation(spin);
|
||||
case Y -> rotation.getYRotation(spin);
|
||||
case Z -> rotation.getZRotation(spin);
|
||||
};
|
||||
if (angles.add(Double.doubleToLongBits(angle))) {
|
||||
spins.add(spin);
|
||||
}
|
||||
}
|
||||
return spins.toIntArray();
|
||||
}
|
||||
}
|
||||
|
||||
private enum Axis {
|
||||
X,
|
||||
Y,
|
||||
Z
|
||||
}
|
||||
|
||||
public record Extent(Span x, Span y, Span z) {
|
||||
public Extent {
|
||||
Objects.requireNonNull(x, "x");
|
||||
Objects.requireNonNull(y, "y");
|
||||
Objects.requireNonNull(z, "z");
|
||||
}
|
||||
|
||||
public static Extent of(IrisObject object) {
|
||||
Objects.requireNonNull(object, "object");
|
||||
Vector3i center = object.getCenter();
|
||||
int[] keys = keyBounds(object.getBlocks());
|
||||
return new Extent(
|
||||
Span.of(object.getW(), center.getBlockX(), keys[0], keys[3]),
|
||||
Span.of(object.getH(), center.getBlockY(), keys[1], keys[4]),
|
||||
Span.of(object.getD(), center.getBlockZ(), keys[2], keys[5]));
|
||||
}
|
||||
|
||||
public Extent scaled(double scale) {
|
||||
if (scale == 1D) {
|
||||
return this;
|
||||
}
|
||||
return new Extent(x.scaled(scale), y.scaled(scale), z.scaled(scale));
|
||||
}
|
||||
|
||||
private static int[] keyBounds(VectorMap<NativeBlockState> blocks) {
|
||||
int[] bounds = {Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE,
|
||||
Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE};
|
||||
VectorMap<NativeBlockState>.Cursor cursor = blocks.cursor();
|
||||
while (cursor.next()) {
|
||||
IrisBlockVector key = cursor.key();
|
||||
bounds[0] = Math.min(bounds[0], key.getBlockX());
|
||||
bounds[1] = Math.min(bounds[1], key.getBlockY());
|
||||
bounds[2] = Math.min(bounds[2], key.getBlockZ());
|
||||
bounds[3] = Math.max(bounds[3], key.getBlockX());
|
||||
bounds[4] = Math.max(bounds[4], key.getBlockY());
|
||||
bounds[5] = Math.max(bounds[5], key.getBlockZ());
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
}
|
||||
|
||||
public record Span(int size, int center, int minimum, int maximum, boolean framed) {
|
||||
public Span {
|
||||
if (minimum > -center || maximum < size - 1 - center) {
|
||||
throw new IllegalArgumentException("Span must cover the object frame: " + minimum + ".." + maximum
|
||||
+ " for size " + size + " centered at " + center);
|
||||
}
|
||||
}
|
||||
|
||||
public static Span of(int size, int center, int keyMinimum, int keyMaximum) {
|
||||
int frameMinimum = -center;
|
||||
int frameMaximum = size - 1 - center;
|
||||
boolean framed = keyMinimum >= frameMinimum && keyMaximum <= frameMaximum;
|
||||
return new Span(size, center, Math.min(keyMinimum, frameMinimum), Math.max(keyMaximum, frameMaximum), framed);
|
||||
}
|
||||
|
||||
public int span() {
|
||||
return maximum - minimum;
|
||||
}
|
||||
|
||||
public Span scaled(double scale) {
|
||||
int scaledSize = (int) Math.ceil(size * scale);
|
||||
int scaledCenter = scaledSize / 2;
|
||||
int frameMinimum = -scaledCenter;
|
||||
int frameMaximum = scaledSize - 1 - scaledCenter;
|
||||
if (framed) {
|
||||
return new Span(scaledSize, scaledCenter, frameMinimum, frameMaximum, true);
|
||||
}
|
||||
int scaledMinimum = (int) Math.floor((minimum + center) * scale) - scaledCenter;
|
||||
double centerExcess = Math.max(0D, center - (size / 2D));
|
||||
int scaledMaximum = (int) Math.ceil(((maximum + 1 + centerExcess) * scale) + 0.5D + ROUNDING_SLACK) - 1;
|
||||
return new Span(scaledSize, scaledCenter, Math.min(scaledMinimum, frameMinimum),
|
||||
Math.max(scaledMaximum, frameMaximum), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,12 +136,8 @@ public final class TerrainMatterView {
|
||||
SOUTH(false, 15);
|
||||
|
||||
private final int[] positions = new int[256];
|
||||
private final boolean fixedX;
|
||||
private final int coordinate;
|
||||
|
||||
Face(boolean fixedX, int coordinate) {
|
||||
this.fixedX = fixedX;
|
||||
this.coordinate = coordinate;
|
||||
for (int y = 0; y < 16; y++) {
|
||||
for (int offset = 0; offset < 16; offset++) {
|
||||
int x = fixedX ? coordinate : offset;
|
||||
@@ -150,14 +146,6 @@ public final class TerrainMatterView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int x(int offset) {
|
||||
return fixedX ? coordinate : offset;
|
||||
}
|
||||
|
||||
int z(int offset) {
|
||||
return fixedX ? offset : coordinate;
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> void iterate(MantleChunk<Matter> chunk, Class<T> type,
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
package art.arcane.iris.generation.noise;
|
||||
|
||||
import art.arcane.volmlib.util.interpolation.IrisInterpolation;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class GeneratorProgram {
|
||||
private static final int SURFACE_GRID = 6;
|
||||
|
||||
private final IrisGenerator generator;
|
||||
private final long superSeed;
|
||||
private final NoiseLayerProgram[] layers;
|
||||
private final double totalOpacity;
|
||||
private final NoiseLayerProgram cliffs;
|
||||
private final boolean multiplicitive;
|
||||
private final double opacity;
|
||||
private final double offsetX;
|
||||
private final double offsetZ;
|
||||
private final double zoom;
|
||||
private final double surfaceDetail;
|
||||
private final double cliffHeightMin;
|
||||
private final double cliffHeightMax;
|
||||
private final boolean cellCracks;
|
||||
|
||||
GeneratorProgram(IrisGenerator generator, long superSeed, NoiseLayerProgram[] layers, double totalOpacity, NoiseLayerProgram cliffs) {
|
||||
this.generator = generator;
|
||||
this.superSeed = superSeed;
|
||||
this.layers = layers;
|
||||
this.totalOpacity = totalOpacity;
|
||||
this.cliffs = cliffs;
|
||||
this.multiplicitive = generator.isMultiplicitive();
|
||||
this.opacity = generator.getOpacity();
|
||||
this.offsetX = generator.getOffsetX();
|
||||
this.offsetZ = generator.getOffsetZ();
|
||||
this.zoom = generator.getZoom();
|
||||
this.surfaceDetail = generator.getSurfaceDetail();
|
||||
this.cliffHeightMin = generator.getCliffHeightMin();
|
||||
this.cliffHeightMax = generator.getCliffHeightMax();
|
||||
this.cellCracks = generator.getCellFractureHeight() != 0;
|
||||
}
|
||||
|
||||
public IrisGenerator generator() {
|
||||
return generator;
|
||||
}
|
||||
|
||||
public void heights(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
if (layers.length == 0) {
|
||||
Arrays.fill(out, 0, count, 0D);
|
||||
return;
|
||||
}
|
||||
if (surfaceDetail == 1D) {
|
||||
sample(scratch, xs, zs, count, out);
|
||||
return;
|
||||
}
|
||||
surfaceHeights(scratch, xs, zs, count, out);
|
||||
}
|
||||
|
||||
public double height(TerrainBatchScratch scratch, double x, double z) {
|
||||
if (layers.length == 0) {
|
||||
return 0D;
|
||||
}
|
||||
if (surfaceDetail == 1D) {
|
||||
return sample(scratch, x, z);
|
||||
}
|
||||
double lowerX = Math.floor(x / SURFACE_GRID) * SURFACE_GRID;
|
||||
double lowerZ = Math.floor(z / SURFACE_GRID) * SURFACE_GRID;
|
||||
double dx = (x - lowerX) / SURFACE_GRID;
|
||||
double dz = (z - lowerZ) / SURFACE_GRID;
|
||||
IrisGenerator.SurfaceCacheStripe[] stripes = generator.surfaceStripes();
|
||||
double northWest = lattice(scratch, stripes, lowerX, lowerZ);
|
||||
if (dx == 0D && dz == 0D) {
|
||||
return northWest;
|
||||
}
|
||||
double north = dx == 0D ? northWest
|
||||
: IrisInterpolation.lerp(northWest, lattice(scratch, stripes, lowerX + SURFACE_GRID, lowerZ), dx);
|
||||
double smooth = north;
|
||||
if (dz != 0D) {
|
||||
double southWest = lattice(scratch, stripes, lowerX, lowerZ + SURFACE_GRID);
|
||||
double south = dx == 0D ? southWest
|
||||
: IrisInterpolation.lerp(southWest, lattice(scratch, stripes, lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID), dx);
|
||||
smooth = IrisInterpolation.lerp(north, south, dz);
|
||||
}
|
||||
return surfaceDetail == 0D ? smooth : smooth + (sample(scratch, x, z) - smooth) * surfaceDetail;
|
||||
}
|
||||
|
||||
private double lattice(TerrainBatchScratch scratch, IrisGenerator.SurfaceCacheStripe[] stripes, double x, double z) {
|
||||
if (x != (int) x || z != (int) z) {
|
||||
return sample(scratch, x, z);
|
||||
}
|
||||
int mark = scratch.mark();
|
||||
boolean hit;
|
||||
double value;
|
||||
try {
|
||||
double[] cached = scratch.buffer(1);
|
||||
hit = IrisGenerator.cachedSurface(stripes, x, z, superSeed, cached, 0);
|
||||
value = cached[0];
|
||||
} finally {
|
||||
scratch.reset(mark);
|
||||
}
|
||||
if (hit) {
|
||||
return value;
|
||||
}
|
||||
double height = sample(scratch, x, z);
|
||||
IrisGenerator.storeSurface(stripes, x, z, superSeed, height);
|
||||
return height;
|
||||
}
|
||||
|
||||
private double sample(TerrainBatchScratch scratch, double x, double z) {
|
||||
double sampleX = (x + offsetX) / zoom;
|
||||
double sampleZ = (z + offsetZ) / zoom;
|
||||
double value = multiplicitive ? 1D : 0D;
|
||||
for (NoiseLayerProgram layer : layers) {
|
||||
double noise = layer.sample(scratch, sampleX, sampleZ);
|
||||
value = multiplicitive ? value * noise : value + noise;
|
||||
}
|
||||
value = multiplicitive ? value * opacity : (value / totalOpacity) * opacity;
|
||||
if (Double.isNaN(value)) {
|
||||
value = 0D;
|
||||
}
|
||||
if (cliffs != null) {
|
||||
double height = IrisInterpolation.lerp(cliffHeightMin, cliffHeightMax, cliffs.sample(scratch, sampleX, sampleZ));
|
||||
value = (Math.round((value * 255D) / height) * height) / 255D;
|
||||
}
|
||||
return cellCracks ? generator.cell(x, z, value, 0D) : value;
|
||||
}
|
||||
|
||||
private void surfaceHeights(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
|
||||
int mark = scratch.mark();
|
||||
int indexMark = scratch.indexMark();
|
||||
try {
|
||||
fillSurfaceHeights(scratch, xs, zs, count, out);
|
||||
} finally {
|
||||
scratch.resetIndices(indexMark);
|
||||
scratch.reset(mark);
|
||||
}
|
||||
}
|
||||
|
||||
private void fillSurfaceHeights(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
|
||||
int[] northWest = scratch.indices(count);
|
||||
int[] northEast = scratch.indices(count);
|
||||
int[] southWest = scratch.indices(count);
|
||||
int[] southEast = scratch.indices(count);
|
||||
double[] fractionX = scratch.buffer(count);
|
||||
double[] fractionZ = scratch.buffer(count);
|
||||
scratch.clearLattice(count * 4);
|
||||
int detailed = 0;
|
||||
double previousX = Double.NaN;
|
||||
double previousZ = Double.NaN;
|
||||
int cellNorthWest = -1;
|
||||
int cellNorthEast = -1;
|
||||
int cellSouthWest = -1;
|
||||
int cellSouthEast = -1;
|
||||
for (int index = 0; index < count; index++) {
|
||||
double lowerX = Math.floor(xs[index] / SURFACE_GRID) * SURFACE_GRID;
|
||||
double lowerZ = Math.floor(zs[index] / SURFACE_GRID) * SURFACE_GRID;
|
||||
double dx = (xs[index] - lowerX) / SURFACE_GRID;
|
||||
double dz = (zs[index] - lowerZ) / SURFACE_GRID;
|
||||
fractionX[index] = dx;
|
||||
fractionZ[index] = dz;
|
||||
if (Double.doubleToRawLongBits(lowerX) != Double.doubleToRawLongBits(previousX)
|
||||
|| Double.doubleToRawLongBits(lowerZ) != Double.doubleToRawLongBits(previousZ)) {
|
||||
previousX = lowerX;
|
||||
previousZ = lowerZ;
|
||||
cellNorthWest = scratch.lattice(lowerX, lowerZ);
|
||||
cellNorthEast = -1;
|
||||
cellSouthWest = -1;
|
||||
cellSouthEast = -1;
|
||||
}
|
||||
if (dx != 0D && cellNorthEast < 0) {
|
||||
cellNorthEast = scratch.lattice(lowerX + SURFACE_GRID, lowerZ);
|
||||
}
|
||||
if (dz != 0D && cellSouthWest < 0) {
|
||||
cellSouthWest = scratch.lattice(lowerX, lowerZ + SURFACE_GRID);
|
||||
}
|
||||
if (dx != 0D && dz != 0D && cellSouthEast < 0) {
|
||||
cellSouthEast = scratch.lattice(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID);
|
||||
}
|
||||
northWest[index] = cellNorthWest;
|
||||
northEast[index] = dx == 0D ? -1 : cellNorthEast;
|
||||
southWest[index] = dz == 0D ? -1 : cellSouthWest;
|
||||
southEast[index] = dx == 0D || dz == 0D ? -1 : cellSouthEast;
|
||||
if (dx != 0D || dz != 0D) {
|
||||
detailed++;
|
||||
}
|
||||
}
|
||||
int nodes = scratch.latticeCount();
|
||||
double[] lattice = scratch.buffer(nodes);
|
||||
resolveLattice(scratch, nodes, lattice);
|
||||
double[] detail = null;
|
||||
int[] detailIndex = null;
|
||||
if (surfaceDetail != 0D && detailed > 0) {
|
||||
double[] detailX = scratch.buffer(detailed);
|
||||
double[] detailZ = scratch.buffer(detailed);
|
||||
detail = scratch.buffer(detailed);
|
||||
detailIndex = scratch.indices(count);
|
||||
int cursor = 0;
|
||||
for (int index = 0; index < count; index++) {
|
||||
if (fractionX[index] != 0D || fractionZ[index] != 0D) {
|
||||
detailX[cursor] = xs[index];
|
||||
detailZ[cursor] = zs[index];
|
||||
detailIndex[index] = cursor++;
|
||||
}
|
||||
}
|
||||
sample(scratch, detailX, detailZ, detailed, detail);
|
||||
}
|
||||
for (int index = 0; index < count; index++) {
|
||||
double dx = fractionX[index];
|
||||
double dz = fractionZ[index];
|
||||
double nw = lattice[northWest[index]];
|
||||
if (dx == 0D && dz == 0D) {
|
||||
out[index] = nw;
|
||||
continue;
|
||||
}
|
||||
double north = dx == 0D ? nw : IrisInterpolation.lerp(nw, lattice[northEast[index]], dx);
|
||||
double smooth = north;
|
||||
if (dz != 0D) {
|
||||
double sw = lattice[southWest[index]];
|
||||
double south = dx == 0D ? sw : IrisInterpolation.lerp(sw, lattice[southEast[index]], dx);
|
||||
smooth = IrisInterpolation.lerp(north, south, dz);
|
||||
}
|
||||
out[index] = surfaceDetail == 0D ? smooth : smooth + (detail[detailIndex[index]] - smooth) * surfaceDetail;
|
||||
}
|
||||
}
|
||||
|
||||
private void resolveLattice(TerrainBatchScratch scratch, int nodes, double[] lattice) {
|
||||
IrisGenerator.SurfaceCacheStripe[] stripes = generator.surfaceStripes();
|
||||
int indexMark = scratch.indexMark();
|
||||
int mark = scratch.mark();
|
||||
try {
|
||||
fillLattice(scratch, nodes, lattice, stripes);
|
||||
} finally {
|
||||
scratch.reset(mark);
|
||||
scratch.resetIndices(indexMark);
|
||||
}
|
||||
}
|
||||
|
||||
private void fillLattice(TerrainBatchScratch scratch, int nodes, double[] lattice, IrisGenerator.SurfaceCacheStripe[] stripes) {
|
||||
int[] missing = scratch.indices(nodes);
|
||||
double[] missingX = scratch.buffer(nodes);
|
||||
double[] missingZ = scratch.buffer(nodes);
|
||||
int missingCount = 0;
|
||||
for (int node = 0; node < nodes; node++) {
|
||||
double x = scratch.latticeX(node);
|
||||
double z = scratch.latticeZ(node);
|
||||
if (x == (int) x && z == (int) z && IrisGenerator.cachedSurface(stripes, x, z, superSeed, lattice, node)) {
|
||||
continue;
|
||||
}
|
||||
missing[missingCount] = node;
|
||||
missingX[missingCount] = x;
|
||||
missingZ[missingCount++] = z;
|
||||
}
|
||||
if (missingCount > 0) {
|
||||
double[] sampled = scratch.buffer(missingCount);
|
||||
sample(scratch, missingX, missingZ, missingCount, sampled);
|
||||
for (int index = 0; index < missingCount; index++) {
|
||||
int node = missing[index];
|
||||
lattice[node] = sampled[index];
|
||||
if (missingX[index] == (int) missingX[index] && missingZ[index] == (int) missingZ[index]) {
|
||||
IrisGenerator.storeSurface(stripes, missingX[index], missingZ[index], superSeed, sampled[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void sample(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
|
||||
int mark = scratch.mark();
|
||||
try {
|
||||
sampleBatch(scratch, xs, zs, count, out);
|
||||
} finally {
|
||||
scratch.reset(mark);
|
||||
}
|
||||
}
|
||||
|
||||
private void sampleBatch(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
|
||||
double[] sampleX = scratch.buffer(count);
|
||||
double[] sampleZ = scratch.buffer(count);
|
||||
double[] noise = scratch.buffer(count);
|
||||
for (int index = 0; index < count; index++) {
|
||||
sampleX[index] = (xs[index] + offsetX) / zoom;
|
||||
sampleZ[index] = (zs[index] + offsetZ) / zoom;
|
||||
}
|
||||
Arrays.fill(out, 0, count, multiplicitive ? 1D : 0D);
|
||||
for (NoiseLayerProgram layer : layers) {
|
||||
layer.evaluate(scratch, sampleX, sampleZ, count, noise);
|
||||
if (multiplicitive) {
|
||||
for (int index = 0; index < count; index++) {
|
||||
out[index] *= noise[index];
|
||||
}
|
||||
} else {
|
||||
for (int index = 0; index < count; index++) {
|
||||
out[index] += noise[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < count; index++) {
|
||||
double value = multiplicitive ? out[index] * opacity : (out[index] / totalOpacity) * opacity;
|
||||
out[index] = Double.isNaN(value) ? 0D : value;
|
||||
}
|
||||
if (cliffs != null) {
|
||||
cliffs.evaluate(scratch, sampleX, sampleZ, count, noise);
|
||||
for (int index = 0; index < count; index++) {
|
||||
double height = IrisInterpolation.lerp(cliffHeightMin, cliffHeightMax, noise[index]);
|
||||
out[index] = (Math.round((out[index] * 255D) / height) * height) / 255D;
|
||||
}
|
||||
}
|
||||
if (cellCracks) {
|
||||
for (int index = 0; index < count; index++) {
|
||||
out[index] = generator.cell(xs[index], zs[index], out[index], 0D);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,10 +41,7 @@ import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.DoubleBinaryOperator;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@@ -211,7 +208,7 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
if (surfaceDetail == 1D) {
|
||||
return sampleHeight(rx, rz, superSeed, data, engine);
|
||||
}
|
||||
return surfaceHeight(rx, rz, superSeed, data, engine, null);
|
||||
return surfaceHeight(rx, rz, superSeed, data, engine);
|
||||
}
|
||||
|
||||
public boolean supportsBatch() {
|
||||
@@ -227,90 +224,84 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
return !hasCliffs() || cliffHeightGenerator.supportsBatch();
|
||||
}
|
||||
|
||||
public void getHeights(double[] xs, double[] zs, long superSeed, double[] target, int count) {
|
||||
Objects.checkFromIndexSize(0, count, xs.length);
|
||||
Objects.checkFromIndexSize(0, count, zs.length);
|
||||
Objects.checkFromIndexSize(0, count, target.length);
|
||||
if (count > 4096) {
|
||||
throw new IllegalArgumentException("Generator batch exceeds 4096 samples");
|
||||
public GeneratorProgram compileProgram(long superSeed) {
|
||||
if (!Double.isFinite(surfaceDetail) || surfaceDetail < 0D || surfaceDetail > 1D) {
|
||||
throw new IllegalArgumentException("Generator surfaceDetail must be finite and between 0 and 1");
|
||||
}
|
||||
if (count < 8 || !supportsBatch()) {
|
||||
for (int index = 0; index < count; index++) {
|
||||
target[index] = getHeight(xs[index], zs[index], superSeed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (composite.isEmpty()) {
|
||||
Arrays.fill(target, 0, count, 0D);
|
||||
return;
|
||||
if (!supportsBatch()) {
|
||||
throw new IllegalStateException("Generator " + getLoadKey() + " cannot be compiled into a batch program");
|
||||
}
|
||||
IrisData data = getLoader();
|
||||
Engine engine = data == null ? null : data.getEngine();
|
||||
DoubleBinaryOperator prepared = prepareHeight(superSeed, data, engine);
|
||||
for (int index = 0; index < count; index++) {
|
||||
target[index] = surfaceDetail == 1D ? prepared.applyAsDouble(xs[index], zs[index])
|
||||
: surfaceHeight(xs[index], zs[index], superSeed, data, engine, prepared);
|
||||
}
|
||||
}
|
||||
|
||||
private DoubleBinaryOperator prepareHeight(long superSeed, IrisData data, Engine engine) {
|
||||
int hc = (int) ((cliffHeightMin * 10) + 10 + cliffHeightMax * getSeed() + offsetX + offsetZ);
|
||||
DoubleBinaryOperator[] layers = new DoubleBinaryOperator[composite.size()];
|
||||
NoiseLayerProgram[] layers = new NoiseLayerProgram[composite.size()];
|
||||
double totalOpacity = 0D;
|
||||
for (int index = 0; index < layers.length; index++) {
|
||||
IrisNoiseGenerator layer = composite.get(index);
|
||||
layers[index] = layer.prepareBatch(getSeed() + superSeed + hc, data, engine);
|
||||
layers[index] = layer.compileProgram(getSeed() + superSeed + hc, data, engine);
|
||||
if (!multiplicitive) {
|
||||
totalOpacity += layer.getOpacity();
|
||||
}
|
||||
}
|
||||
double sumOpacity = totalOpacity;
|
||||
double cliffSeed = (double) (superSeed + 294596 + hc) - 34857;
|
||||
DoubleBinaryOperator cliffs = hasCliffs() ? cliffHeightGenerator.prepareBatch(
|
||||
(long) (getSeed() + cliffSeed + hc), data, engine) : null;
|
||||
return (rx, rz) -> {
|
||||
double sampleX = (rx + offsetX) / zoom;
|
||||
double sampleZ = (rz + offsetZ) / zoom;
|
||||
double h = multiplicitive ? 1D : 0D;
|
||||
for (DoubleBinaryOperator layer : layers) {
|
||||
double sampled = layer.applyAsDouble(sampleX, sampleZ);
|
||||
h = multiplicitive ? h * sampled : h + sampled;
|
||||
}
|
||||
double value = multiplicitive ? h * opacity : (h / sumOpacity) * opacity;
|
||||
if (Double.isNaN(value)) {
|
||||
value = 0D;
|
||||
}
|
||||
if (cliffs != null) {
|
||||
double height = IrisInterpolation.lerp(cliffHeightMin, cliffHeightMax,
|
||||
cliffs.applyAsDouble(sampleX, sampleZ));
|
||||
value = (Math.round((value * 255D) / height) * height) / 255D;
|
||||
}
|
||||
return hasCellCracks() ? cell(rx, rz, value, superSeed + 48622 + hc) : value;
|
||||
};
|
||||
NoiseLayerProgram cliffs = hasCliffs()
|
||||
? cliffHeightGenerator.compileProgram((long) (getSeed() + cliffSeed + hc), data, engine)
|
||||
: null;
|
||||
return new GeneratorProgram(this, superSeed, layers, totalOpacity, cliffs);
|
||||
}
|
||||
|
||||
private double surfaceHeight(double rx, double rz, long superSeed, IrisData data, Engine engine,
|
||||
DoubleBinaryOperator prepared) {
|
||||
SurfaceCacheStripe[] surfaceStripes() {
|
||||
IrisData data = getLoader();
|
||||
return surfaceCache(data, data == null ? null : data.getEngine()).stripes();
|
||||
}
|
||||
|
||||
static boolean cachedSurface(SurfaceCacheStripe[] stripes, double x, double z, long superSeed, double[] target, int index) {
|
||||
long key = ((long) (int) x << 32) | ((int) z & 0xffffffffL);
|
||||
long mixed = surfaceMix(key);
|
||||
SurfaceSample cached = stripes[(int) mixed & (SURFACE_CACHE_STRIPES - 1)]
|
||||
.get(key, superSeed, (int) (mixed >>> 4) & (SURFACE_CACHE_STRIPE_SIZE - 1));
|
||||
if (cached == null) {
|
||||
return false;
|
||||
}
|
||||
target[index] = cached.height();
|
||||
return true;
|
||||
}
|
||||
|
||||
static void storeSurface(SurfaceCacheStripe[] stripes, double x, double z, long superSeed, double height) {
|
||||
long key = ((long) (int) x << 32) | ((int) z & 0xffffffffL);
|
||||
long mixed = surfaceMix(key);
|
||||
stripes[(int) mixed & (SURFACE_CACHE_STRIPES - 1)]
|
||||
.put((int) (mixed >>> 4) & (SURFACE_CACHE_STRIPE_SIZE - 1), new SurfaceSample(key, superSeed, height));
|
||||
}
|
||||
|
||||
private static long surfaceMix(long key) {
|
||||
long mixed = key ^ (key >>> 33);
|
||||
mixed *= 0xff51afd7ed558ccdL;
|
||||
mixed ^= mixed >>> 33;
|
||||
return mixed;
|
||||
}
|
||||
|
||||
private double surfaceHeight(double rx, double rz, long superSeed, IrisData data, Engine engine) {
|
||||
SurfaceCacheStripe[] stripes = surfaceCache(data, engine).stripes();
|
||||
double lowerX = Math.floor(rx / SURFACE_GRID) * SURFACE_GRID;
|
||||
double lowerZ = Math.floor(rz / SURFACE_GRID) * SURFACE_GRID;
|
||||
double dx = (rx - lowerX) / SURFACE_GRID;
|
||||
double dz = (rz - lowerZ) / SURFACE_GRID;
|
||||
double northWest = cachedSurfaceHeight(lowerX, lowerZ, superSeed, stripes, data, engine, prepared);
|
||||
double northWest = cachedSurfaceHeight(lowerX, lowerZ, superSeed, stripes, data, engine);
|
||||
if (dx == 0D && dz == 0D) {
|
||||
return northWest;
|
||||
}
|
||||
double north = dx == 0D ? northWest : IrisInterpolation.lerp(northWest,
|
||||
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ, superSeed, stripes, data, engine, prepared), dx);
|
||||
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ, superSeed, stripes, data, engine), dx);
|
||||
double smooth = north;
|
||||
if (dz != 0D) {
|
||||
double southWest = cachedSurfaceHeight(lowerX, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine, prepared);
|
||||
double southWest = cachedSurfaceHeight(lowerX, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine);
|
||||
double south = dx == 0D ? southWest : IrisInterpolation.lerp(southWest,
|
||||
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine, prepared), dx);
|
||||
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine), dx);
|
||||
smooth = IrisInterpolation.lerp(north, south, dz);
|
||||
}
|
||||
return surfaceDetail == 0D ? smooth
|
||||
: smooth + ((prepared == null ? sampleHeight(rx, rz, superSeed, data, engine) : prepared.applyAsDouble(rx, rz)) - smooth) * surfaceDetail;
|
||||
: smooth + (sampleHeight(rx, rz, superSeed, data, engine) - smooth) * surfaceDetail;
|
||||
}
|
||||
|
||||
private double sampleHeight(double rx, double rz, long superSeed, IrisData data, Engine engine) {
|
||||
@@ -350,21 +341,19 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
return v;
|
||||
}
|
||||
|
||||
private double cachedSurfaceHeight(double x, double z, long superSeed, SurfaceCacheStripe[] stripes, IrisData data, Engine engine, DoubleBinaryOperator prepared) {
|
||||
private double cachedSurfaceHeight(double x, double z, long superSeed, SurfaceCacheStripe[] stripes, IrisData data, Engine engine) {
|
||||
if (x != (int) x || z != (int) z) {
|
||||
return prepared == null ? sampleHeight(x, z, superSeed, data, engine) : prepared.applyAsDouble(x, z);
|
||||
return sampleHeight(x, z, superSeed, data, engine);
|
||||
}
|
||||
long key = ((long) (int) x << 32) | ((int) z & 0xffffffffL);
|
||||
long mixed = key ^ (key >>> 33);
|
||||
mixed *= 0xff51afd7ed558ccdL;
|
||||
mixed ^= mixed >>> 33;
|
||||
long mixed = surfaceMix(key);
|
||||
SurfaceCacheStripe stripe = stripes[(int) mixed & (SURFACE_CACHE_STRIPES - 1)];
|
||||
int slot = (int) (mixed >>> 4) & (SURFACE_CACHE_STRIPE_SIZE - 1);
|
||||
SurfaceSample cached = stripe.get(key, superSeed, slot);
|
||||
if (cached != null) {
|
||||
return cached.height();
|
||||
}
|
||||
double height = prepared == null ? sampleHeight(x, z, superSeed, data, engine) : prepared.applyAsDouble(x, z);
|
||||
double height = sampleHeight(x, z, superSeed, data, engine);
|
||||
stripe.put(slot, new SurfaceSample(key, superSeed, height));
|
||||
return height;
|
||||
}
|
||||
@@ -457,7 +446,7 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
private record SurfaceSample(long key, long seed, double height) {
|
||||
}
|
||||
|
||||
private static final class SurfaceCacheStripe {
|
||||
static final class SurfaceCacheStripe {
|
||||
private final AtomicReferenceArray<SurfaceSample> entries = new AtomicReferenceArray<>(SURFACE_CACHE_STRIPE_SIZE);
|
||||
|
||||
private SurfaceSample get(long key, long seed, int slot) {
|
||||
|
||||
@@ -39,6 +39,7 @@ import art.arcane.volmlib.util.noise.NoiseGenerator;
|
||||
import art.arcane.volmlib.util.noise.OffsetNoiseGenerator;
|
||||
import art.arcane.volmlib.util.noise.PerlinNoise;
|
||||
import art.arcane.volmlib.util.noise.SimplexNoise;
|
||||
import art.arcane.volmlib.util.noise.program.NoiseProgram;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
@@ -47,8 +48,8 @@ import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.DoubleBinaryOperator;
|
||||
|
||||
@Snippet("generator")
|
||||
@Accessors(chain = true)
|
||||
@@ -198,44 +199,29 @@ public class IrisNoiseGenerator {
|
||||
return true;
|
||||
}
|
||||
|
||||
DoubleBinaryOperator prepareBatch(long superSeed, IrisData data, Engine engine) {
|
||||
NoiseLayerProgram compileProgram(long superSeed, IrisData data, Engine engine) {
|
||||
if (!enabled) {
|
||||
double value = offsetY;
|
||||
return (x, z) -> value;
|
||||
return NoiseLayerProgram.disabled(this);
|
||||
}
|
||||
if (hasDeepFracture()) {
|
||||
FractureSamples samples = new FractureSamples();
|
||||
if (samples.prepare(this, superSeed, data, engine)) {
|
||||
return samples::sample;
|
||||
}
|
||||
}
|
||||
DoubleBinaryOperator[] children = new DoubleBinaryOperator[fracture.size()];
|
||||
double[] offsets = new double[fracture.size()];
|
||||
int salt = 33;
|
||||
for (int index = 0; index < children.length; index++) {
|
||||
IrisNoiseGenerator child = fracture.get(index);
|
||||
int enabledChildren = 0;
|
||||
for (IrisNoiseGenerator child : fracture) {
|
||||
if (child.isEnabled()) {
|
||||
children[index] = child.prepareBatch(superSeed + seed + salt, data, engine);
|
||||
offsets[index] = child.getOpacity() / 2D;
|
||||
enabledChildren++;
|
||||
}
|
||||
}
|
||||
NoiseLayerProgram[] children = new NoiseLayerProgram[enabledChildren];
|
||||
double[] offsets = new double[enabledChildren];
|
||||
int salt = 33;
|
||||
int compiled = 0;
|
||||
for (IrisNoiseGenerator child : fracture) {
|
||||
if (child.isEnabled()) {
|
||||
children[compiled] = child.compileProgram(superSeed + seed + salt, data, engine);
|
||||
offsets[compiled++] = child.getOpacity() / 2D;
|
||||
}
|
||||
salt += 819;
|
||||
}
|
||||
CNG generator = getGenerator(superSeed, data, engine);
|
||||
return (x, z) -> samplePrepared(generator, children, offsets, x, z);
|
||||
}
|
||||
|
||||
private double samplePrepared(CNG generator, DoubleBinaryOperator[] children, double[] offsets,
|
||||
double xv, double zv) {
|
||||
double x = xv;
|
||||
double z = zv;
|
||||
for (int index = 0; index < children.length; index++) {
|
||||
DoubleBinaryOperator child = children[index];
|
||||
if (child != null) {
|
||||
x += child.applyAsDouble(xv, zv) - offsets[index];
|
||||
z += child.applyAsDouble(zv, xv) - offsets[index];
|
||||
}
|
||||
}
|
||||
return sample(generator, x, z);
|
||||
CNG generator = Objects.requireNonNull(getGenerator(superSeed, data, engine), "Noise layer generator");
|
||||
return NoiseLayerProgram.of(this, NoiseProgram.compile2D(generator), children, offsets);
|
||||
}
|
||||
|
||||
public KList<IrisNoiseGenerator> getAllComposites() {
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
package art.arcane.iris.generation.noise;
|
||||
|
||||
import art.arcane.volmlib.util.interpolation.IrisInterpolation;
|
||||
import art.arcane.volmlib.util.noise.program.NoiseProgram;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class NoiseLayerProgram {
|
||||
private final boolean enabled;
|
||||
private final double offsetY;
|
||||
private final NoiseProgram program;
|
||||
private final double zoom;
|
||||
private final double opacity;
|
||||
private final double offsetX;
|
||||
private final double offsetZ;
|
||||
private final double exponent;
|
||||
private final boolean negative;
|
||||
private final boolean parametric;
|
||||
private final boolean bezier;
|
||||
private final boolean sinCentered;
|
||||
private final NoiseLayerProgram[] children;
|
||||
private final double[] childOffsets;
|
||||
|
||||
private NoiseLayerProgram(IrisNoiseGenerator layer, NoiseProgram program, NoiseLayerProgram[] children, double[] childOffsets) {
|
||||
this.enabled = program != null;
|
||||
this.offsetY = layer.getOffsetY();
|
||||
this.program = program;
|
||||
this.zoom = layer.getZoom();
|
||||
this.opacity = layer.getOpacity();
|
||||
this.offsetX = layer.getOffsetX();
|
||||
this.offsetZ = layer.getOffsetZ();
|
||||
this.exponent = layer.getExponent();
|
||||
this.negative = layer.isNegative();
|
||||
this.parametric = layer.isParametric();
|
||||
this.bezier = layer.isBezier();
|
||||
this.sinCentered = layer.isSinCentered();
|
||||
this.children = children;
|
||||
this.childOffsets = childOffsets;
|
||||
}
|
||||
|
||||
static NoiseLayerProgram disabled(IrisNoiseGenerator layer) {
|
||||
return new NoiseLayerProgram(layer, null, new NoiseLayerProgram[0], new double[0]);
|
||||
}
|
||||
|
||||
static NoiseLayerProgram of(IrisNoiseGenerator layer, NoiseProgram program, NoiseLayerProgram[] children, double[] childOffsets) {
|
||||
return new NoiseLayerProgram(layer, program, children, childOffsets);
|
||||
}
|
||||
|
||||
public void evaluate(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
|
||||
if (!enabled) {
|
||||
Arrays.fill(out, 0, count, offsetY);
|
||||
return;
|
||||
}
|
||||
int mark = scratch.mark();
|
||||
try {
|
||||
fill(scratch, xs, zs, count, out);
|
||||
} finally {
|
||||
scratch.reset(mark);
|
||||
}
|
||||
}
|
||||
|
||||
private void fill(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
|
||||
double[] sampleX = scratch.buffer(count);
|
||||
double[] sampleZ = scratch.buffer(count);
|
||||
if (children.length == 0) {
|
||||
for (int index = 0; index < count; index++) {
|
||||
sampleX[index] = (xs[index] / zoom) + offsetX;
|
||||
sampleZ[index] = (zs[index] / zoom) + offsetZ;
|
||||
}
|
||||
} else {
|
||||
System.arraycopy(xs, 0, sampleX, 0, count);
|
||||
System.arraycopy(zs, 0, sampleZ, 0, count);
|
||||
double[] forward = scratch.buffer(count);
|
||||
double[] reverse = scratch.buffer(count);
|
||||
for (int child = 0; child < children.length; child++) {
|
||||
children[child].evaluatePair(scratch, xs, zs, count, forward, reverse);
|
||||
double offset = childOffsets[child];
|
||||
for (int index = 0; index < count; index++) {
|
||||
sampleX[index] += forward[index] - offset;
|
||||
sampleZ[index] += reverse[index] - offset;
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < count; index++) {
|
||||
sampleX[index] = (sampleX[index] / zoom) + offsetX;
|
||||
sampleZ[index] = (sampleZ[index] / zoom) + offsetZ;
|
||||
}
|
||||
}
|
||||
program.fill2D(scratch.noise(), sampleX, sampleZ, count, out);
|
||||
for (int index = 0; index < count; index++) {
|
||||
out[index] = post(out[index]);
|
||||
}
|
||||
}
|
||||
|
||||
void evaluatePair(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] forward, double[] reverse) {
|
||||
if (!enabled) {
|
||||
Arrays.fill(forward, 0, count, offsetY);
|
||||
Arrays.fill(reverse, 0, count, offsetY);
|
||||
return;
|
||||
}
|
||||
int mark = scratch.mark();
|
||||
try {
|
||||
fillPair(scratch, xs, zs, count, forward, reverse);
|
||||
} finally {
|
||||
scratch.reset(mark);
|
||||
}
|
||||
}
|
||||
|
||||
private void fillPair(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] forward, double[] reverse) {
|
||||
int pairs = count * 2;
|
||||
double[] sampleX = scratch.buffer(pairs);
|
||||
double[] sampleZ = scratch.buffer(pairs);
|
||||
System.arraycopy(xs, 0, sampleX, 0, count);
|
||||
System.arraycopy(zs, 0, sampleZ, 0, count);
|
||||
System.arraycopy(zs, 0, sampleX, count, count);
|
||||
System.arraycopy(xs, 0, sampleZ, count, count);
|
||||
if (children.length > 0) {
|
||||
double[] childForward = scratch.buffer(count);
|
||||
double[] childReverse = scratch.buffer(count);
|
||||
for (int child = 0; child < children.length; child++) {
|
||||
children[child].evaluatePair(scratch, xs, zs, count, childForward, childReverse);
|
||||
double offset = childOffsets[child];
|
||||
for (int index = 0; index < count; index++) {
|
||||
sampleX[index] += childForward[index] - offset;
|
||||
sampleZ[index] += childReverse[index] - offset;
|
||||
sampleX[count + index] += childReverse[index] - offset;
|
||||
sampleZ[count + index] += childForward[index] - offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < pairs; index++) {
|
||||
sampleX[index] = (sampleX[index] / zoom) + offsetX;
|
||||
sampleZ[index] = (sampleZ[index] / zoom) + offsetZ;
|
||||
}
|
||||
double[] values = scratch.buffer(pairs);
|
||||
program.fill2D(scratch.noise(), sampleX, sampleZ, pairs, values);
|
||||
for (int index = 0; index < count; index++) {
|
||||
forward[index] = post(values[index]);
|
||||
reverse[index] = post(values[count + index]);
|
||||
}
|
||||
}
|
||||
|
||||
public double sample(TerrainBatchScratch scratch, double x, double z) {
|
||||
if (!enabled) {
|
||||
return offsetY;
|
||||
}
|
||||
double sampleX = x;
|
||||
double sampleZ = z;
|
||||
if (children.length > 0) {
|
||||
int mark = scratch.mark();
|
||||
try {
|
||||
double[] pair = scratch.buffer(2);
|
||||
for (int child = 0; child < children.length; child++) {
|
||||
children[child].samplePair(scratch, x, z, pair);
|
||||
double offset = childOffsets[child];
|
||||
sampleX += pair[0] - offset;
|
||||
sampleZ += pair[1] - offset;
|
||||
}
|
||||
} finally {
|
||||
scratch.reset(mark);
|
||||
}
|
||||
}
|
||||
return post(program.sample2D((sampleX / zoom) + offsetX, (sampleZ / zoom) + offsetZ));
|
||||
}
|
||||
|
||||
private void samplePair(TerrainBatchScratch scratch, double x, double z, double[] out) {
|
||||
if (!enabled) {
|
||||
out[0] = offsetY;
|
||||
out[1] = offsetY;
|
||||
return;
|
||||
}
|
||||
double forwardX = x;
|
||||
double forwardZ = z;
|
||||
double reverseX = z;
|
||||
double reverseZ = x;
|
||||
if (children.length > 0) {
|
||||
int mark = scratch.mark();
|
||||
try {
|
||||
double[] pair = scratch.buffer(2);
|
||||
for (int child = 0; child < children.length; child++) {
|
||||
children[child].samplePair(scratch, x, z, pair);
|
||||
double offset = childOffsets[child];
|
||||
forwardX += pair[0] - offset;
|
||||
forwardZ += pair[1] - offset;
|
||||
reverseX += pair[1] - offset;
|
||||
reverseZ += pair[0] - offset;
|
||||
}
|
||||
} finally {
|
||||
scratch.reset(mark);
|
||||
}
|
||||
}
|
||||
out[0] = post(program.sample2D((forwardX / zoom) + offsetX, (forwardZ / zoom) + offsetZ));
|
||||
out[1] = post(program.sample2D((reverseX / zoom) + offsetX, (reverseZ / zoom) + offsetZ));
|
||||
}
|
||||
|
||||
private double post(double raw) {
|
||||
double n = raw * opacity;
|
||||
n = negative ? (-n + opacity) : n;
|
||||
n = (exponent != 1 ? n < 0 ? -Math.pow(-n, exponent) : Math.pow(n, exponent) : n) + offsetY;
|
||||
n = parametric ? IrisInterpolation.parametric(n, 2D) : n;
|
||||
n = bezier ? IrisInterpolation.bezier(n) : n;
|
||||
n = sinCentered ? IrisInterpolation.sinCenter(n) : n;
|
||||
return n;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package art.arcane.iris.generation.noise;
|
||||
|
||||
import art.arcane.volmlib.util.noise.program.NoiseScratch;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class TerrainBatchScratch {
|
||||
private static final int MINIMUM_BUFFER = 256;
|
||||
|
||||
private final NoiseScratch noise = new NoiseScratch();
|
||||
private double[][] buffers = new double[32][];
|
||||
private int[][] indexBuffers = new int[8][];
|
||||
private int top;
|
||||
private int indexTop;
|
||||
private long[] latticeX = new long[64];
|
||||
private long[] latticeZ = new long[64];
|
||||
private int[] latticeSlots = new int[64];
|
||||
private int latticeCapacity = 64;
|
||||
private int latticeCount;
|
||||
|
||||
public NoiseScratch noise() {
|
||||
return noise;
|
||||
}
|
||||
|
||||
public int mark() {
|
||||
return top;
|
||||
}
|
||||
|
||||
public int indexMark() {
|
||||
return indexTop;
|
||||
}
|
||||
|
||||
public void reset(int mark) {
|
||||
top = mark;
|
||||
}
|
||||
|
||||
public void resetIndices(int mark) {
|
||||
indexTop = mark;
|
||||
}
|
||||
|
||||
public double[] buffer(int length) {
|
||||
if (top == buffers.length) {
|
||||
buffers = Arrays.copyOf(buffers, top * 2);
|
||||
}
|
||||
double[] buffer = buffers[top];
|
||||
if (buffer == null || buffer.length < length) {
|
||||
buffer = new double[Math.max(length, MINIMUM_BUFFER)];
|
||||
buffers[top] = buffer;
|
||||
}
|
||||
top++;
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public int[] indices(int length) {
|
||||
if (indexTop == indexBuffers.length) {
|
||||
indexBuffers = Arrays.copyOf(indexBuffers, indexTop * 2);
|
||||
}
|
||||
int[] buffer = indexBuffers[indexTop];
|
||||
if (buffer == null || buffer.length < length) {
|
||||
buffer = new int[Math.max(length, MINIMUM_BUFFER)];
|
||||
indexBuffers[indexTop] = buffer;
|
||||
}
|
||||
indexTop++;
|
||||
return buffer;
|
||||
}
|
||||
|
||||
void clearLattice(int maximumEntries) {
|
||||
int capacity = Integer.highestOneBit(Math.max(16, maximumEntries * 2) - 1) << 1;
|
||||
if (latticeSlots.length < capacity) {
|
||||
latticeSlots = new int[capacity];
|
||||
} else {
|
||||
Arrays.fill(latticeSlots, 0, capacity, 0);
|
||||
}
|
||||
latticeCapacity = capacity;
|
||||
latticeCount = 0;
|
||||
}
|
||||
|
||||
int lattice(double x, double z) {
|
||||
long xBits = Double.doubleToRawLongBits(x);
|
||||
long zBits = Double.doubleToRawLongBits(z);
|
||||
long mixed = xBits * 0x9E3779B97F4A7C15L ^ Long.rotateLeft(zBits * 0xC2B2AE3D27D4EB4FL, 31);
|
||||
mixed ^= mixed >>> 29;
|
||||
int mask = latticeCapacity - 1;
|
||||
int slot = (int) mixed & mask;
|
||||
while (true) {
|
||||
int stored = latticeSlots[slot];
|
||||
if (stored == 0) {
|
||||
if (latticeCount == latticeX.length) {
|
||||
latticeX = Arrays.copyOf(latticeX, latticeCount * 2);
|
||||
latticeZ = Arrays.copyOf(latticeZ, latticeCount * 2);
|
||||
}
|
||||
latticeX[latticeCount] = xBits;
|
||||
latticeZ[latticeCount] = zBits;
|
||||
latticeSlots[slot] = ++latticeCount;
|
||||
return latticeCount - 1;
|
||||
}
|
||||
if (latticeX[stored - 1] == xBits && latticeZ[stored - 1] == zBits) {
|
||||
return stored - 1;
|
||||
}
|
||||
slot = (slot + 1) & mask;
|
||||
}
|
||||
}
|
||||
|
||||
int latticeCount() {
|
||||
return latticeCount;
|
||||
}
|
||||
|
||||
double latticeX(int index) {
|
||||
return Double.longBitsToDouble(latticeX[index]);
|
||||
}
|
||||
|
||||
double latticeZ(int index) {
|
||||
return Double.longBitsToDouble(latticeZ[index]);
|
||||
}
|
||||
}
|
||||
@@ -50,4 +50,18 @@ final class ChunkBlockUpdateListener implements ColumnExtentListeningHunk.Listen
|
||||
}
|
||||
quiet = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun(int x, int z, int yFromInclusive, int yToInclusive, NativeBlockState state) {
|
||||
if (state == null || state == quiet) {
|
||||
return;
|
||||
}
|
||||
if (B.isUpdatable(state) || state.isCustom()) {
|
||||
for (int y = yToInclusive; y >= yFromInclusive; y--) {
|
||||
updater.catchBlockUpdates(originX + x, y, originZ + z, state);
|
||||
}
|
||||
return;
|
||||
}
|
||||
quiet = state;
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,6 @@ import java.awt.Color;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
@@ -224,10 +223,6 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
EngineWorldManager getWorldManager();
|
||||
|
||||
default UUID getBiomeID(int x, int z) {
|
||||
return getComplex().getBaseBiomeIDStream().get(x, z);
|
||||
}
|
||||
|
||||
int getParallelism();
|
||||
|
||||
void setParallelism(int parallelism);
|
||||
@@ -347,15 +342,18 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
@BlockCoordinates
|
||||
default SubterrainCell getSubterrainCell(int x, int y, int z) {
|
||||
if (getComplex() == null || getDimension() == null
|
||||
|| !getDimension().allowsSubterrainFeatures()
|
||||
|| getDimension().getSubterrainFeatures() == null || getDimension().getSubterrainFeatures().isEmpty()) {
|
||||
return SubterrainCell.OUTSIDE;
|
||||
}
|
||||
SubterrainPlanner planner = getComplex().getSubterrainPlanner();
|
||||
IrisComplex complex = getComplex();
|
||||
SubterrainPlanner planner = complex == null ? null : SubterrainChunkCells.activePlanner(complex, getDimension());
|
||||
return planner == null ? SubterrainCell.OUTSIDE : planner.sample(x, y + getWorld().minHeight(), z);
|
||||
}
|
||||
|
||||
@ChunkCoordinates
|
||||
default boolean subterrainAbsentInChunk(int chunkX, int chunkZ) {
|
||||
IrisComplex complex = getComplex();
|
||||
SubterrainPlanner planner = complex == null ? null : SubterrainChunkCells.activePlanner(complex, getDimension());
|
||||
return planner == null || !planner.hasCandidates(chunkX, chunkZ);
|
||||
}
|
||||
|
||||
@BlockCoordinates
|
||||
default IrisBiome getSubterrainBiome(int x, int y, int z) {
|
||||
SubterrainCell cell = getSubterrainCell(x, y, z);
|
||||
@@ -415,6 +413,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
return;
|
||||
}
|
||||
IrisComplex complex = getComplex();
|
||||
boolean subterrain = !subterrainAbsentInChunk(x >> 4, z >> 4);
|
||||
int caveTop = getHeight(x, z) - 2;
|
||||
IrisRegion region = getRegion(x, 0, z);
|
||||
IrisBiome surface = null;
|
||||
@@ -428,7 +427,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
for (int index = 0; index < biomes.length; index++) {
|
||||
int y = index * step;
|
||||
regions[index] = region;
|
||||
IrisBiome featureBiome = getSubterrainBiome(x, y, z);
|
||||
IrisBiome featureBiome = subterrain ? getSubterrainBiome(x, y, z) : null;
|
||||
if (featureBiome != null) {
|
||||
biomes[index] = featureBiome;
|
||||
continue;
|
||||
|
||||
@@ -24,63 +24,65 @@ import lombok.Data;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.DoubleAdder;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
|
||||
@Data
|
||||
public class EngineMetrics {
|
||||
private final AtomicRollingSequence total;
|
||||
private final AtomicRollingSequence updates;
|
||||
private final AtomicRollingSequence terrain;
|
||||
private final AtomicRollingSequence biome;
|
||||
private final AtomicRollingSequence parallax;
|
||||
private final AtomicRollingSequence parallaxInsert;
|
||||
private final AtomicRollingSequence post;
|
||||
private final AtomicRollingSequence perfection;
|
||||
private final AtomicRollingSequence api;
|
||||
private final AtomicRollingSequence decoration;
|
||||
private final AtomicRollingSequence cave;
|
||||
private final AtomicRollingSequence ravine;
|
||||
private final AtomicRollingSequence deposit;
|
||||
private final AtomicRollingSequence carveResolve;
|
||||
private final AtomicRollingSequence carveApply;
|
||||
private final AtomicRollingSequence noiseHeightInterpolate;
|
||||
private final AtomicRollingSequence noiseHeightGenerator;
|
||||
private final AtomicRollingSequence contextPrefill;
|
||||
private final AtomicRollingSequence contextPrefillHeight;
|
||||
private final AtomicRollingSequence contextPrefillBiome;
|
||||
private final AtomicRollingSequence contextPrefillRock;
|
||||
private final AtomicRollingSequence contextPrefillFluid;
|
||||
private final AtomicRollingSequence contextPrefillRegion;
|
||||
private final AtomicRollingSequence contextPrefillCave;
|
||||
private final AtomicRollingSequence pregenWaitPermit;
|
||||
private final AtomicRollingSequence pregenWaitAdaptive;
|
||||
private final StageTimer total;
|
||||
private final StageTimer updates;
|
||||
private final StageTimer terrain;
|
||||
private final StageTimer biome;
|
||||
private final StageTimer parallax;
|
||||
private final StageTimer parallaxInsert;
|
||||
private final StageTimer post;
|
||||
private final StageTimer perfection;
|
||||
private final StageTimer api;
|
||||
private final StageTimer decoration;
|
||||
private final StageTimer cave;
|
||||
private final StageTimer ravine;
|
||||
private final StageTimer deposit;
|
||||
private final StageTimer carveResolve;
|
||||
private final StageTimer carveApply;
|
||||
private final StageTimer noiseHeightInterpolate;
|
||||
private final StageTimer noiseHeightGenerator;
|
||||
private final StageTimer contextPrefill;
|
||||
private final StageTimer contextPrefillHeight;
|
||||
private final StageTimer contextPrefillBiome;
|
||||
private final StageTimer contextPrefillRock;
|
||||
private final StageTimer contextPrefillFluid;
|
||||
private final StageTimer contextPrefillRegion;
|
||||
private final StageTimer contextPrefillCave;
|
||||
private final StageTimer pregenWaitPermit;
|
||||
private final StageTimer pregenWaitAdaptive;
|
||||
|
||||
public EngineMetrics(int mem) {
|
||||
this.total = new AtomicRollingSequence(mem);
|
||||
this.terrain = new AtomicRollingSequence(mem);
|
||||
this.api = new AtomicRollingSequence(mem);
|
||||
this.biome = new AtomicRollingSequence(mem);
|
||||
this.perfection = new AtomicRollingSequence(mem);
|
||||
this.parallax = new AtomicRollingSequence(mem);
|
||||
this.parallaxInsert = new AtomicRollingSequence(mem);
|
||||
this.post = new AtomicRollingSequence(mem);
|
||||
this.decoration = new AtomicRollingSequence(mem);
|
||||
this.updates = new AtomicRollingSequence(mem);
|
||||
this.cave = new AtomicRollingSequence(mem);
|
||||
this.ravine = new AtomicRollingSequence(mem);
|
||||
this.deposit = new AtomicRollingSequence(mem);
|
||||
this.carveResolve = new AtomicRollingSequence(mem);
|
||||
this.carveApply = new AtomicRollingSequence(mem);
|
||||
this.noiseHeightInterpolate = new AtomicRollingSequence(mem);
|
||||
this.noiseHeightGenerator = new AtomicRollingSequence(mem);
|
||||
this.contextPrefill = new AtomicRollingSequence(mem);
|
||||
this.contextPrefillHeight = new AtomicRollingSequence(mem);
|
||||
this.contextPrefillBiome = new AtomicRollingSequence(mem);
|
||||
this.contextPrefillRock = new AtomicRollingSequence(mem);
|
||||
this.contextPrefillFluid = new AtomicRollingSequence(mem);
|
||||
this.contextPrefillRegion = new AtomicRollingSequence(mem);
|
||||
this.contextPrefillCave = new AtomicRollingSequence(mem);
|
||||
this.pregenWaitPermit = new AtomicRollingSequence(mem);
|
||||
this.pregenWaitAdaptive = new AtomicRollingSequence(mem);
|
||||
this.total = new StageTimer(mem);
|
||||
this.terrain = new StageTimer(mem);
|
||||
this.api = new StageTimer(mem);
|
||||
this.biome = new StageTimer(mem);
|
||||
this.perfection = new StageTimer(mem);
|
||||
this.parallax = new StageTimer(mem);
|
||||
this.parallaxInsert = new StageTimer(mem);
|
||||
this.post = new StageTimer(mem);
|
||||
this.decoration = new StageTimer(mem);
|
||||
this.updates = new StageTimer(mem);
|
||||
this.cave = new StageTimer(mem);
|
||||
this.ravine = new StageTimer(mem);
|
||||
this.deposit = new StageTimer(mem);
|
||||
this.carveResolve = new StageTimer(mem);
|
||||
this.carveApply = new StageTimer(mem);
|
||||
this.noiseHeightInterpolate = new StageTimer(mem);
|
||||
this.noiseHeightGenerator = new StageTimer(mem);
|
||||
this.contextPrefill = new StageTimer(mem);
|
||||
this.contextPrefillHeight = new StageTimer(mem);
|
||||
this.contextPrefillBiome = new StageTimer(mem);
|
||||
this.contextPrefillRock = new StageTimer(mem);
|
||||
this.contextPrefillFluid = new StageTimer(mem);
|
||||
this.contextPrefillRegion = new StageTimer(mem);
|
||||
this.contextPrefillCave = new StageTimer(mem);
|
||||
this.pregenWaitPermit = new StageTimer(mem);
|
||||
this.pregenWaitAdaptive = new StageTimer(mem);
|
||||
}
|
||||
|
||||
public KMap<String, Double> pull() {
|
||||
@@ -134,4 +136,61 @@ public class EngineMetrics {
|
||||
averages.entrySet().removeIf(entry -> !Double.isFinite(entry.getValue()) || entry.getValue() < 0D);
|
||||
return Map.copyOf(averages);
|
||||
}
|
||||
|
||||
public Map<String, StageTotal> stageTotals() {
|
||||
Map<String, StageTotal> totals = new LinkedHashMap<>();
|
||||
totals.put("total", total.snapshot());
|
||||
totals.put("terrain", terrain.snapshot());
|
||||
totals.put("biome", biome.snapshot());
|
||||
totals.put("parallax", parallax.snapshot());
|
||||
totals.put("parallax.insert", parallaxInsert.snapshot());
|
||||
totals.put("post", post.snapshot());
|
||||
totals.put("perfection", perfection.snapshot());
|
||||
totals.put("decoration", decoration.snapshot());
|
||||
totals.put("api", api.snapshot());
|
||||
totals.put("updates", updates.snapshot());
|
||||
totals.put("cave", cave.snapshot());
|
||||
totals.put("ravine", ravine.snapshot());
|
||||
totals.put("deposit", deposit.snapshot());
|
||||
totals.put("carve.resolve", carveResolve.snapshot());
|
||||
totals.put("carve.apply", carveApply.snapshot());
|
||||
totals.put("noise.height.interpolate", noiseHeightInterpolate.snapshot());
|
||||
totals.put("noise.height.generator", noiseHeightGenerator.snapshot());
|
||||
totals.put("context.prefill", contextPrefill.snapshot());
|
||||
totals.put("context.prefill.height", contextPrefillHeight.snapshot());
|
||||
totals.put("context.prefill.biome", contextPrefillBiome.snapshot());
|
||||
totals.put("context.prefill.rock", contextPrefillRock.snapshot());
|
||||
totals.put("context.prefill.fluid", contextPrefillFluid.snapshot());
|
||||
totals.put("context.prefill.region", contextPrefillRegion.snapshot());
|
||||
totals.put("context.prefill.cave", contextPrefillCave.snapshot());
|
||||
totals.put("pregen.wait.permit", pregenWaitPermit.snapshot());
|
||||
totals.put("pregen.wait.adaptive", pregenWaitAdaptive.snapshot());
|
||||
return totals;
|
||||
}
|
||||
|
||||
public static final class StageTimer extends AtomicRollingSequence {
|
||||
private final DoubleAdder totalMillis = new DoubleAdder();
|
||||
private final LongAdder samples = new LongAdder();
|
||||
|
||||
public StageTimer(int size) {
|
||||
super(size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(double millis) {
|
||||
super.put(millis);
|
||||
totalMillis.add(millis);
|
||||
samples.increment();
|
||||
}
|
||||
|
||||
public StageTotal snapshot() {
|
||||
return new StageTotal(totalMillis.sum(), samples.sum());
|
||||
}
|
||||
}
|
||||
|
||||
public record StageTotal(double millis, long samples) {
|
||||
public StageTotal since(StageTotal earlier) {
|
||||
return new StageTotal(millis - earlier.millis(), samples - earlier.samples());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
|
||||
public enum GenerationCounter {
|
||||
GENERATOR_EVALUATIONS("generator_evaluations"),
|
||||
CORNER_STARCASTS("corner_starcasts"),
|
||||
HYDROLOGY_LOOKUPS("hydrology_lookups"),
|
||||
CAVE_LATTICE_READS("cave_lattice_reads"),
|
||||
CAVE_LATTICE_SAMPLES("cave_lattice_samples"),
|
||||
CAVE_WARP_SAMPLES("cave_warp_samples");
|
||||
|
||||
public static final boolean ENABLED = Boolean.getBoolean("iris.generation.counters");
|
||||
|
||||
private final String key;
|
||||
private final LongAdder count = new LongAdder();
|
||||
|
||||
GenerationCounter(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String key() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void add(long amount) {
|
||||
count.add(amount);
|
||||
}
|
||||
|
||||
public void increment() {
|
||||
count.increment();
|
||||
}
|
||||
|
||||
public long sum() {
|
||||
return count.sum();
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
final class HydrologyNoiseCacheBudget {
|
||||
static final int MAXIMUM_CHUNKS = 32_768;
|
||||
static final int BYTES_PER_CHUNK = 4_096;
|
||||
static final HydrologyNoiseCacheBudget SHARED = new HydrologyNoiseCacheBudget(Runtime.getRuntime().maxMemory() / 8L);
|
||||
|
||||
private final long maximumBytes;
|
||||
private long reservedBytes;
|
||||
|
||||
HydrologyNoiseCacheBudget(long maximumBytes) {
|
||||
if (maximumBytes < 0L) {
|
||||
throw new IllegalArgumentException("Cache budget must not be negative.");
|
||||
}
|
||||
this.maximumBytes = maximumBytes;
|
||||
}
|
||||
|
||||
synchronized Reservation reserve(int cacheCount, int requestedAdditionalChunks) {
|
||||
if (cacheCount <= 0 || requestedAdditionalChunks < 0 || requestedAdditionalChunks > MAXIMUM_CHUNKS) {
|
||||
throw new IllegalArgumentException("Invalid hydrology cache reservation.");
|
||||
}
|
||||
long bytesPerIncrement = (long) BYTES_PER_CHUNK * cacheCount;
|
||||
int additionalChunks = (int) Math.min(requestedAdditionalChunks, (maximumBytes - reservedBytes) / bytesPerIncrement);
|
||||
long bytes = additionalChunks * bytesPerIncrement;
|
||||
reservedBytes += bytes;
|
||||
return new Reservation(this, additionalChunks, bytes);
|
||||
}
|
||||
|
||||
synchronized long reservedBytes() {
|
||||
return reservedBytes;
|
||||
}
|
||||
|
||||
private synchronized void release(Reservation reservation) {
|
||||
if (!reservation.released) {
|
||||
reservedBytes -= reservation.bytes;
|
||||
reservation.released = true;
|
||||
}
|
||||
}
|
||||
|
||||
static final class Reservation implements AutoCloseable {
|
||||
private final HydrologyNoiseCacheBudget budget;
|
||||
private final int additionalChunks;
|
||||
private final long bytes;
|
||||
private boolean released;
|
||||
|
||||
private Reservation(HydrologyNoiseCacheBudget budget, int additionalChunks, long bytes) {
|
||||
this.budget = budget;
|
||||
this.additionalChunks = additionalChunks;
|
||||
this.bytes = bytes;
|
||||
}
|
||||
|
||||
int additionalChunks() {
|
||||
return additionalChunks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
budget.release(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -19,6 +19,7 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.generation.subterrain.SubterrainCell;
|
||||
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
|
||||
import art.arcane.iris.generation.runtime.EngineBackgroundTasks.BackgroundTaskDrain;
|
||||
import art.arcane.iris.generation.runtime.EngineRuntimeBuilder.RuntimeAssembly;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
@@ -177,7 +178,10 @@ public class IrisEngine implements Engine {
|
||||
volatile GenerationHistoryRuntimeRouter generationHistoryRuntimeRouter;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private boolean generationHistoryRoutingRequired;
|
||||
private volatile boolean generationHistoryRoutingRequired;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private volatile SubterrainGate subterrainGate;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
volatile EngineTarget publishedTarget;
|
||||
@@ -613,14 +617,64 @@ public class IrisEngine implements Engine {
|
||||
@BlockCoordinates
|
||||
@Override
|
||||
public SubterrainCell getSubterrainCell(int x, int y, int z) {
|
||||
if (!hasGenerationRuntimeScope() && generationHistoryRuntimeRouter != null) {
|
||||
Optional<SubterrainCell> saved = generationHistoryRuntimeRouter.biomes()
|
||||
.subterrainCell(x, y + getWorld().minHeight(), z);
|
||||
EngineThreadState.Frame frame = threadState.current();
|
||||
GenerationHistoryRuntimeRouter router = generationHistoryRuntimeRouter;
|
||||
if ((frame == null || frame.binding == null) && router != null) {
|
||||
Optional<SubterrainCell> saved = router.biomes()
|
||||
.subterrainCell(x, y + targetIn(frame).getWorld().minHeight(), z);
|
||||
if (saved.isPresent()) {
|
||||
return saved.get();
|
||||
}
|
||||
}
|
||||
return Engine.super.getSubterrainCell(x, y, z);
|
||||
if (frame == null || frame.assembly == null && (frame.environment == null || frame.binding != null)) {
|
||||
GenerationRuntime current = selectedGenerationRuntime(frame);
|
||||
if (current == null) {
|
||||
return SubterrainCell.OUTSIDE;
|
||||
}
|
||||
SubterrainPlanner planner = admittedSubterrainPlanner(current.complex(), current.dimension());
|
||||
return planner == null ? SubterrainCell.OUTSIDE
|
||||
: planner.sample(x, y + current.target().getWorld().minHeight(), z);
|
||||
}
|
||||
SubterrainPlanner planner = subterrainPlannerIn(frame);
|
||||
return planner == null ? SubterrainCell.OUTSIDE
|
||||
: planner.sample(x, y + targetIn(frame).getWorld().minHeight(), z);
|
||||
}
|
||||
|
||||
@ChunkCoordinates
|
||||
public SubterrainChunkCells subterrainChunkCells(int chunkX, int chunkZ) {
|
||||
EngineThreadState.Frame frame = threadState.current();
|
||||
if ((frame == null || frame.binding == null) && generationHistoryRuntimeRouter != null) {
|
||||
return SubterrainChunkCells.delegated(this);
|
||||
}
|
||||
SubterrainPlanner planner = subterrainPlannerIn(frame);
|
||||
return SubterrainChunkCells.planned(this, planner, chunkX, chunkZ,
|
||||
planner == null ? 0 : targetIn(frame).getWorld().minHeight());
|
||||
}
|
||||
|
||||
private SubterrainPlanner subterrainPlannerIn(EngineThreadState.Frame frame) {
|
||||
IrisComplex complex = complexIn(frame);
|
||||
return complex == null ? null : admittedSubterrainPlanner(complex, dimensionIn(frame));
|
||||
}
|
||||
|
||||
private SubterrainPlanner admittedSubterrainPlanner(IrisComplex complex, IrisDimension dimension) {
|
||||
if (complex == null || dimension == null) {
|
||||
return null;
|
||||
}
|
||||
SubterrainGate gate = subterrainGate;
|
||||
if (gate == null || gate.dimension() != dimension) {
|
||||
gate = new SubterrainGate(dimension, SubterrainChunkCells.admits(dimension));
|
||||
subterrainGate = gate;
|
||||
}
|
||||
return gate.admitted() ? complex.getSubterrainPlanner() : null;
|
||||
}
|
||||
|
||||
@ChunkCoordinates
|
||||
@Override
|
||||
public boolean subterrainAbsentInChunk(int chunkX, int chunkZ) {
|
||||
if (!hasGenerationRuntimeScope() && generationHistoryRuntimeRouter != null) {
|
||||
return false;
|
||||
}
|
||||
return Engine.super.subterrainAbsentInChunk(chunkX, chunkZ);
|
||||
}
|
||||
|
||||
@BlockCoordinates
|
||||
@@ -1011,14 +1065,8 @@ public class IrisEngine implements Engine {
|
||||
}
|
||||
|
||||
public Optional<GenerationHistoryRuntimeRouter> getGenerationHistoryRuntimeRouter() {
|
||||
GenerationHistoryRuntimeRouter router = generationHistoryRuntimeRouter;
|
||||
if (router != null) {
|
||||
return Optional.of(router);
|
||||
}
|
||||
synchronized (generationHistoryRuntimeRouterLock) {
|
||||
return Optional.ofNullable(generationHistoryRuntimeRouter);
|
||||
}
|
||||
}
|
||||
|
||||
public GenerationHistoryRuntimeRouter.CoordinateScope openGenerationHistoryCoordinateScope(
|
||||
int blockX,
|
||||
@@ -1028,19 +1076,24 @@ public class IrisEngine implements Engine {
|
||||
if (router != null) {
|
||||
return router.openCoordinateScope(blockX, blockZ);
|
||||
}
|
||||
synchronized (generationHistoryRuntimeRouterLock) {
|
||||
if (!generationHistoryRoutingRequired) {
|
||||
return null;
|
||||
}
|
||||
router = generationHistoryRuntimeRouter;
|
||||
if (router == null && generationHistoryRoutingRequired) {
|
||||
if (router == null) {
|
||||
throw new IllegalStateException("Iris generation-history runtime router is detached.");
|
||||
}
|
||||
}
|
||||
return router == null ? null : router.openCoordinateScope(blockX, blockZ);
|
||||
return router.openCoordinateScope(blockX, blockZ);
|
||||
}
|
||||
|
||||
public boolean hasGenerationRuntimeScope() {
|
||||
return threadState.binding() != null;
|
||||
}
|
||||
|
||||
public boolean hasGenerationHistoryRouting() {
|
||||
return generationHistoryRuntimeRouter != null || generationHistoryRoutingRequired;
|
||||
}
|
||||
|
||||
private boolean usesScopedNaturalTerrain() {
|
||||
return hasGenerationRuntimeScope() && getComplex().isNaturalTerrainContext();
|
||||
}
|
||||
@@ -1338,7 +1391,10 @@ public class IrisEngine implements Engine {
|
||||
|
||||
@Override
|
||||
public IrisComplex getComplex() {
|
||||
EngineThreadState.Frame frame = threadState.current();
|
||||
return complexIn(threadState.current());
|
||||
}
|
||||
|
||||
private IrisComplex complexIn(EngineThreadState.Frame frame) {
|
||||
RuntimeAssembly assembly = frame == null ? null : frame.assembly;
|
||||
if (assembly != null && assembly.complex != null) {
|
||||
return assembly.complex;
|
||||
@@ -1349,7 +1405,10 @@ public class IrisEngine implements Engine {
|
||||
|
||||
@Override
|
||||
public EngineTarget getTarget() {
|
||||
EngineThreadState.Frame frame = threadState.current();
|
||||
return targetIn(threadState.current());
|
||||
}
|
||||
|
||||
private EngineTarget targetIn(EngineThreadState.Frame frame) {
|
||||
RuntimeAssembly assembly = frame == null ? null : frame.assembly;
|
||||
if (assembly != null) {
|
||||
return assembly.target;
|
||||
@@ -1377,7 +1436,10 @@ public class IrisEngine implements Engine {
|
||||
|
||||
@Override
|
||||
public IrisDimension getDimension() {
|
||||
EngineThreadState.Frame frame = threadState.current();
|
||||
return dimensionIn(threadState.current());
|
||||
}
|
||||
|
||||
private IrisDimension dimensionIn(EngineThreadState.Frame frame) {
|
||||
if (frame != null) {
|
||||
BiomeEnvironmentBinding environment = frame.environment;
|
||||
if (environment != null && frame.binding == null) {
|
||||
@@ -1897,4 +1959,6 @@ public class IrisEngine implements Engine {
|
||||
}
|
||||
}
|
||||
|
||||
private record SubterrainGate(IrisDimension dimension, boolean admitted) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.VarHandle;
|
||||
import java.util.Objects;
|
||||
|
||||
final class SharedBoundsBiomes {
|
||||
static final int MINIMUM_CAPACITY = 1 << 16;
|
||||
static final int MAXIMUM_CAPACITY = 1 << 21;
|
||||
static final long BYTES_PER_ENTRY = 16L;
|
||||
static final long HEAP_SHARE = 256L;
|
||||
private static final VarHandle INTS = MethodHandles.arrayElementVarHandle(int[].class);
|
||||
private static final VarHandle LONGS = MethodHandles.arrayElementVarHandle(long[].class);
|
||||
private static final VarHandle OBJECTS = MethodHandles.arrayElementVarHandle(Object[].class);
|
||||
|
||||
private final Object source;
|
||||
private final long[] keys;
|
||||
private final Object[] values;
|
||||
private final int[] stamps;
|
||||
private final int mask;
|
||||
|
||||
SharedBoundsBiomes(Object source, int capacity) {
|
||||
if (capacity < 2 || Integer.bitCount(capacity) != 1) {
|
||||
throw new IllegalArgumentException("capacity must be a power of two of at least 2");
|
||||
}
|
||||
this.source = Objects.requireNonNull(source, "source");
|
||||
this.keys = new long[capacity];
|
||||
this.values = new Object[capacity];
|
||||
this.stamps = new int[capacity];
|
||||
this.mask = capacity - 1;
|
||||
}
|
||||
|
||||
static int capacityFor(long maxMemory) {
|
||||
long entries = Math.max(0L, maxMemory) / HEAP_SHARE / BYTES_PER_ENTRY;
|
||||
long bounded = Math.max(MINIMUM_CAPACITY, Math.min(MAXIMUM_CAPACITY, entries));
|
||||
return Integer.highestOneBit((int) bounded);
|
||||
}
|
||||
|
||||
Object source() {
|
||||
return source;
|
||||
}
|
||||
|
||||
int slot(int x, int z) {
|
||||
long mixed = key(x, z);
|
||||
mixed = (mixed ^ (mixed >>> 30)) * 0xBF58476D1CE4E5B9L;
|
||||
mixed = (mixed ^ (mixed >>> 27)) * 0x94D049BB133111EBL;
|
||||
return (int) ((mixed ^ (mixed >>> 31)) & mask);
|
||||
}
|
||||
|
||||
IrisBiome get(int x, int z) {
|
||||
long key = key(x, z);
|
||||
int slot = slot(x, z);
|
||||
int before = (int) INTS.getAcquire(stamps, slot);
|
||||
if (before == 0 || (before & 1) != 0) {
|
||||
return null;
|
||||
}
|
||||
long storedKey = (long) LONGS.getAcquire(keys, slot);
|
||||
Object value = OBJECTS.getAcquire(values, slot);
|
||||
VarHandle.loadLoadFence();
|
||||
int after = (int) INTS.getAcquire(stamps, slot);
|
||||
if (before != after || storedKey != key) {
|
||||
return null;
|
||||
}
|
||||
return (IrisBiome) value;
|
||||
}
|
||||
|
||||
void put(int x, int z, IrisBiome biome) {
|
||||
if (biome == null) {
|
||||
return;
|
||||
}
|
||||
int slot = slot(x, z);
|
||||
int stamp = (int) INTS.getAcquire(stamps, slot);
|
||||
if ((stamp & 1) != 0 || !INTS.compareAndSet(stamps, slot, stamp, stamp + 1)) {
|
||||
return;
|
||||
}
|
||||
LONGS.setRelease(keys, slot, key(x, z));
|
||||
OBJECTS.setRelease(values, slot, biome);
|
||||
INTS.setRelease(stamps, slot, stamp + 2);
|
||||
}
|
||||
|
||||
private static long key(int x, int z) {
|
||||
return ((long) x << 32) | (z & 0xFFFFFFFFL);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.subterrain.SubterrainCell;
|
||||
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class SubterrainChunkCells {
|
||||
private final Engine engine;
|
||||
private final boolean delegated;
|
||||
private final SubterrainPlanner.ChunkSampler sampler;
|
||||
private final int minimumY;
|
||||
|
||||
private SubterrainChunkCells(Engine engine, boolean delegated, SubterrainPlanner.ChunkSampler sampler, int minimumY) {
|
||||
this.engine = Objects.requireNonNull(engine);
|
||||
this.delegated = delegated;
|
||||
this.sampler = sampler;
|
||||
this.minimumY = minimumY;
|
||||
}
|
||||
|
||||
public static SubterrainChunkCells of(Engine engine, int chunkX, int chunkZ) {
|
||||
return engine instanceof IrisEngine irisEngine
|
||||
? irisEngine.subterrainChunkCells(chunkX, chunkZ)
|
||||
: delegated(engine);
|
||||
}
|
||||
|
||||
public static SubterrainChunkCells delegated(Engine engine) {
|
||||
return new SubterrainChunkCells(engine, true, null, 0);
|
||||
}
|
||||
|
||||
static SubterrainChunkCells planned(Engine engine, SubterrainPlanner planner, int chunkX, int chunkZ, int minimumY) {
|
||||
return new SubterrainChunkCells(engine, false, planner == null ? null : planner.chunkSampler(chunkX, chunkZ), minimumY);
|
||||
}
|
||||
|
||||
static SubterrainPlanner activePlanner(IrisComplex complex, IrisDimension dimension) {
|
||||
if (complex == null || dimension == null || !admits(dimension)) {
|
||||
return null;
|
||||
}
|
||||
return complex.getSubterrainPlanner();
|
||||
}
|
||||
|
||||
static boolean admits(IrisDimension dimension) {
|
||||
return dimension.allowsSubterrainFeatures()
|
||||
&& dimension.getSubterrainFeatures() != null && !dimension.getSubterrainFeatures().isEmpty();
|
||||
}
|
||||
|
||||
public SubterrainCell cell(int x, int y, int z) {
|
||||
if (delegated) {
|
||||
SubterrainCell cell = engine.getSubterrainCell(x, y, z);
|
||||
return cell == null ? SubterrainCell.OUTSIDE : cell;
|
||||
}
|
||||
return sampler == null ? SubterrainCell.OUTSIDE : sampler.sample(x, y + minimumY, z);
|
||||
}
|
||||
|
||||
public IrisBiome biome(int x, int y, int z) {
|
||||
if (delegated) {
|
||||
return engine.getSubterrainBiome(x, y, z);
|
||||
}
|
||||
SubterrainCell cell = cell(x, y, z);
|
||||
if (!cell.occupied() || cell.room().biome().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return engine.getSubterrainBiome(x, y, z);
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,7 @@ package art.arcane.iris.generation.stage;
|
||||
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.subterrain.SubterrainCell;
|
||||
import art.arcane.iris.generation.subterrain.SubterrainPlan;
|
||||
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
|
||||
import java.util.List;
|
||||
import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
|
||||
import art.arcane.iris.generation.subterrain.SubterrainSurfaceMaterials;
|
||||
import art.arcane.iris.generation.runtime.DimensionStackContext;
|
||||
@@ -31,6 +29,7 @@ import art.arcane.iris.generation.runtime.UpperDimensionContext;
|
||||
import art.arcane.iris.generation.terrain.Terrain3DColumn;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.generation.runtime.SubterrainChunkCells;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
@@ -39,6 +38,7 @@ import art.arcane.iris.generation.mantle.CaveTerrainSnapshot;
|
||||
import art.arcane.iris.generation.mantle.TerrainMatterView;
|
||||
import art.arcane.iris.generation.terrain.InferredType;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.biome.IrisBiomePaletteLayer;
|
||||
import art.arcane.iris.generation.decoration.IrisDecorationPart;
|
||||
import art.arcane.iris.generation.decoration.IrisDecorator;
|
||||
import art.arcane.iris.generation.terrain.IrisDimensionCarvingResolver;
|
||||
@@ -79,15 +79,20 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
private static final int CAVE_BIOME_BLEND_CENTER_WEIGHT = 4;
|
||||
private static final int CAVE_BIOME_BLEND_TOTAL_WEIGHT = 8;
|
||||
private static final int SUBMERGED_FLOOR_SUBSTRATE_DEPTH = 8;
|
||||
private static final long RANDOM_MULTIPLIER = 0x5DEECE66DL;
|
||||
private static final long RANDOM_ADDEND = 0xBL;
|
||||
private static final long RANDOM_MASK = (1L << 48) - 1;
|
||||
private static final MatterCavern BASIC_CAVERN = new MatterCavern(true, "", (byte) 0);
|
||||
private final RNG rng;
|
||||
private final long caveBlendSeed;
|
||||
private final NativeBlockState AIR = B.getState("CAVE_AIR");
|
||||
private final NativeBlockState LAVA = B.getState("LAVA");
|
||||
private final IrisDecorantActuator decorant;
|
||||
|
||||
public IrisCarveModifier(Engine engine) {
|
||||
super(engine, "Carve");
|
||||
rng = new RNG(getEngine().getSeedManager().getCarve());
|
||||
caveBlendSeed = getEngine().getSeedManager().getCarve();
|
||||
rng = new RNG(caveBlendSeed);
|
||||
decorant = new IrisDecorantActuator(engine);
|
||||
}
|
||||
|
||||
@@ -96,10 +101,10 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
public void onModify(int x, int z, Hunk<NativeBlockState> output, boolean multicore, ChunkContext context) {
|
||||
PrecisionStopwatch caveStopwatch = PrecisionStopwatch.start();
|
||||
Mantle<Matter> mantle = getEngine().getMantle().getMantle();
|
||||
CaveInputs resolverState = new CaveInputs(getEngine());
|
||||
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache = new Long2ObjectOpenHashMap<>(2048);
|
||||
CaveInputs resolverState = CaveInputs.forTerrainChunk(getEngine(), x, z);
|
||||
IrisCarveScratch scratch = SCRATCH.get();
|
||||
scratch.reset();
|
||||
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache = scratch.caveBiomeCache;
|
||||
CarveWallBuffer walls = scratch.walls;
|
||||
CarveColumnMask[] columnMasks = scratch.columnMasks;
|
||||
CarveColumnMask[] boundaryMasks = scratch.boundaryMasks;
|
||||
@@ -139,6 +144,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
}
|
||||
int worldHeightSpan = getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight();
|
||||
int caveLavaHeight = getEngine().getDimension().getCaveLavaHeight();
|
||||
SubterrainPlanner subterrainPlanner = resolverState.complex.getSubterrainPlanner();
|
||||
CarveResolutionContext resolutionContext = new CarveResolutionContext(
|
||||
output,
|
||||
context,
|
||||
@@ -151,44 +157,28 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
caveLavaHeight,
|
||||
chunkBlockX,
|
||||
chunkBlockZ,
|
||||
getComplex().getSubterrainPlanner(),
|
||||
getComplex().getSubterrainPlanner() == null ? List.of() : getComplex().getSubterrainPlanner().plansForBounds(chunkBlockX - 1, chunkBlockZ - 1, chunkBlockX + 16, chunkBlockZ + 16)
|
||||
getEngine().getWorld().minHeight(),
|
||||
resolverState.complex,
|
||||
subterrainPlanner == null ? null : subterrainPlanner.chunkSampler(x, z)
|
||||
);
|
||||
CarveResolver carveResolver = new CarveResolver(resolutionContext);
|
||||
terrain.forEachCell(carveResolver::apply);
|
||||
if (scratch.customCaveBiomePresent) {
|
||||
addInternalWallsFromMantle(terrain, walls, columnMasks);
|
||||
} else {
|
||||
addInternalWallsFromMasks(walls, columnMasks);
|
||||
collectInternalWalls(scratch.wallMasks, columnMasks);
|
||||
}
|
||||
addCrossChunkBoundaryWalls(mantle, terrain, walls, boundaryMasks, x, z, surfaceHeights);
|
||||
getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds());
|
||||
|
||||
PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start();
|
||||
try {
|
||||
IrisData wallData = getData();
|
||||
walls.forEach((rx, yy, rz, cavern) -> {
|
||||
HydrologyCaveCell hydrology = terrain.hydrology(rx, yy, rz);
|
||||
if (hydrology != null && hydrology.protectsPlacement()
|
||||
|| subterrainCell(rx + chunkBlockX, yy, rz + chunkBlockZ).owned()) {
|
||||
return;
|
||||
WallPainter painter = new WallPainter(output, terrain, resolverState, caveBiomeCache, customBiomeCache,
|
||||
surfaceHeights, getData(), chunkBlockX, chunkBlockZ);
|
||||
if (!scratch.customCaveBiomePresent) {
|
||||
paintInternalWalls(painter, scratch.wallMasks, boundaryMasks);
|
||||
}
|
||||
int worldX = rx + chunkBlockX;
|
||||
int worldZ = rz + chunkBlockZ;
|
||||
String customBiome = cavern.getCustomBiome();
|
||||
IrisBiome biome = customBiome.isEmpty()
|
||||
? resolveCaveBiome(caveBiomeCache, worldX, yy, worldZ, resolverState)
|
||||
: resolveCustomBiome(customBiomeCache, customBiome);
|
||||
|
||||
if (biome != null) {
|
||||
NativeBlockState data = biome.getWall().get(rng, worldX, yy, worldZ, wallData);
|
||||
int columnIndex = PowerOfTwoCoordinates.packLocal16(rx, rz);
|
||||
|
||||
if (data != null && B.isSolid(output.getRaw(rx, yy, rz)) && yy < surfaceHeights[columnIndex]) {
|
||||
output.setRaw(rx, yy, rz, data);
|
||||
}
|
||||
}
|
||||
});
|
||||
walls.forEach(painter);
|
||||
|
||||
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
|
||||
processColumnFromMask(
|
||||
@@ -200,7 +190,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
z,
|
||||
resolverState,
|
||||
caveBiomeCache,
|
||||
customBiomeCache
|
||||
customBiomeCache,
|
||||
scratch.layers
|
||||
);
|
||||
}
|
||||
|
||||
@@ -218,7 +209,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
z,
|
||||
resolverState,
|
||||
caveBiomeCache,
|
||||
customBiomeCache
|
||||
customBiomeCache,
|
||||
scratch.layers
|
||||
);
|
||||
}
|
||||
|
||||
@@ -252,11 +244,6 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
}
|
||||
}
|
||||
|
||||
private SubterrainCell subterrainCell(int x, int y, int z) {
|
||||
SubterrainCell cell = getEngine().getSubterrainCell(x, y, z);
|
||||
return cell == null ? SubterrainCell.OUTSIDE : cell;
|
||||
}
|
||||
|
||||
public void preserveSubterrain(int blockX, int blockZ, Hunk<NativeBlockState> output) {
|
||||
SubterrainPlanner planner = getComplex().getSubterrainPlanner();
|
||||
if (planner == null || planner.isEmpty()) {
|
||||
@@ -432,8 +419,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
int localX = x & 15;
|
||||
int localZ = z & 15;
|
||||
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
|
||||
SubterrainCell feature = context.subterrainPlanner() == null ? SubterrainCell.OUTSIDE : context.subterrainPlanner().sample(context.subterrainPlans(),
|
||||
context.chunkBlockX() + localX, y + getEngine().getWorld().minHeight(), context.chunkBlockZ() + localZ);
|
||||
SubterrainCell feature = context.subterrainSampler() == null ? SubterrainCell.OUTSIDE
|
||||
: context.subterrainSampler().sampleCandidates(context.chunkBlockX() + localX, y + context.minimumY(),
|
||||
context.chunkBlockZ() + localZ);
|
||||
if (feature.owned() && (!feature.solid() || feature.room().reservedSolid())) {
|
||||
context.output().setRaw(localX, y, localZ, SubterrainRasterizer.state(feature));
|
||||
if (feature.carve()) {
|
||||
@@ -487,7 +475,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
if (isFluidIntent(cavern)) {
|
||||
fluid = hydrology == null
|
||||
? context.chunkContext().getFluid().get(localX, localZ)
|
||||
: getComplex().resolveHydrologyFluid(
|
||||
: context.complex().resolveHydrologyFluid(
|
||||
hydrology.fluidProfileKey(),
|
||||
context.chunkBlockX() + localX,
|
||||
context.chunkBlockZ() + localZ
|
||||
@@ -521,35 +509,87 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
int caveLavaHeight,
|
||||
int chunkBlockX,
|
||||
int chunkBlockZ,
|
||||
SubterrainPlanner subterrainPlanner,
|
||||
List<SubterrainPlan> subterrainPlans
|
||||
int minimumY,
|
||||
IrisComplex complex,
|
||||
SubterrainPlanner.ChunkSampler subterrainSampler
|
||||
) {
|
||||
}
|
||||
|
||||
private void addInternalWallsFromMasks(CarveWallBuffer walls, CarveColumnMask[] columnMasks) {
|
||||
static void collectInternalWalls(CarveColumnMask[] wallMasks, CarveColumnMask[] columnMasks) {
|
||||
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
|
||||
CarveColumnMask columnMask = columnMasks[columnIndex];
|
||||
if (columnMask.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int rx = columnIndex >> 4;
|
||||
int rz = columnIndex & 15;
|
||||
int yy = columnMask.nextSetBit(0);
|
||||
while (yy >= 0) {
|
||||
if (rz < 15 && !columnMasks[columnIndex + 1].contains(yy)) {
|
||||
walls.put(rx, yy, rz + 1, BASIC_CAVERN);
|
||||
wallMasks[columnIndex].collectWalls(
|
||||
columnMasks[columnIndex],
|
||||
rz > 0 ? columnMasks[columnIndex - 1] : null,
|
||||
rz < 15 ? columnMasks[columnIndex + 1] : null,
|
||||
rx > 0 ? columnMasks[columnIndex - 16] : null,
|
||||
rx < 15 ? columnMasks[columnIndex + 16] : null
|
||||
);
|
||||
}
|
||||
if (rx < 15 && !columnMasks[columnIndex + 16].contains(yy)) {
|
||||
walls.put(rx + 1, yy, rz, BASIC_CAVERN);
|
||||
}
|
||||
if (rz > 0 && !columnMasks[columnIndex - 1].contains(yy)) {
|
||||
walls.put(rx, yy, rz - 1, BASIC_CAVERN);
|
||||
|
||||
private static void paintInternalWalls(WallPainter painter, CarveColumnMask[] wallMasks, CarveColumnMask[] boundaryMasks) {
|
||||
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
|
||||
CarveColumnMask wallMask = wallMasks[columnIndex];
|
||||
if (wallMask.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (rx > 0 && !columnMasks[columnIndex - 16].contains(yy)) {
|
||||
walls.put(rx - 1, yy, rz, BASIC_CAVERN);
|
||||
int rx = columnIndex >> 4;
|
||||
int rz = columnIndex & 15;
|
||||
CarveColumnMask boundaryMask = boundaryMasks[columnIndex];
|
||||
for (int yy = wallMask.nextSetBit(0); yy >= 0; yy = wallMask.nextSetBit(yy + 1)) {
|
||||
if (!boundaryMask.contains(yy)) {
|
||||
painter.accept(rx, yy, rz, BASIC_CAVERN);
|
||||
}
|
||||
yy = columnMask.nextSetBit(yy + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class WallPainter implements CarveWallBuffer.Consumer {
|
||||
private final Hunk<NativeBlockState> output;
|
||||
private final CaveTerrainSnapshot terrain;
|
||||
private final CaveInputs inputs;
|
||||
private final Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache;
|
||||
private final Map<String, IrisBiome> customBiomeCache;
|
||||
private final int[] surfaceHeights;
|
||||
private final IrisData data;
|
||||
private final int chunkBlockX;
|
||||
private final int chunkBlockZ;
|
||||
|
||||
private WallPainter(Hunk<NativeBlockState> output, CaveTerrainSnapshot terrain, CaveInputs inputs,
|
||||
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache,
|
||||
int[] surfaceHeights, IrisData data, int chunkBlockX, int chunkBlockZ) {
|
||||
this.output = output;
|
||||
this.terrain = terrain;
|
||||
this.inputs = inputs;
|
||||
this.caveBiomeCache = caveBiomeCache;
|
||||
this.customBiomeCache = customBiomeCache;
|
||||
this.surfaceHeights = surfaceHeights;
|
||||
this.data = data;
|
||||
this.chunkBlockX = chunkBlockX;
|
||||
this.chunkBlockZ = chunkBlockZ;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(int rx, int yy, int rz, MatterCavern cavern) {
|
||||
HydrologyCaveCell hydrology = terrain.hydrology(rx, yy, rz);
|
||||
int worldX = rx + chunkBlockX;
|
||||
int worldZ = rz + chunkBlockZ;
|
||||
if (hydrology != null && hydrology.protectsPlacement() || inputs.cells.cell(worldX, yy, worldZ).owned()
|
||||
|| yy >= surfaceHeights[PowerOfTwoCoordinates.packLocal16(rx, rz)] || !B.isSolid(output.getRaw(rx, yy, rz))) {
|
||||
return;
|
||||
}
|
||||
String customBiome = cavern.getCustomBiome();
|
||||
IrisBiome biome = customBiome.isEmpty()
|
||||
? resolveCaveBiome(caveBiomeCache, worldX, yy, worldZ, inputs)
|
||||
: resolveCustomBiome(customBiomeCache, customBiome);
|
||||
if (biome == null) {
|
||||
return;
|
||||
}
|
||||
NativeBlockState wall = biome.getWall().get(rng, worldX, yy, worldZ, data);
|
||||
if (wall != null) {
|
||||
output.setRaw(rx, yy, rz, wall);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -616,10 +656,6 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
}
|
||||
|
||||
int height = maxY + 1;
|
||||
MatterCavern[] ownWest = west == null ? null : terrain.composedFace(TerrainMatterView.Face.WEST, height);
|
||||
MatterCavern[] ownEast = east == null ? null : terrain.composedFace(TerrainMatterView.Face.EAST, height);
|
||||
MatterCavern[] ownNorth = north == null ? null : terrain.composedFace(TerrainMatterView.Face.NORTH, height);
|
||||
MatterCavern[] ownSouth = south == null ? null : terrain.composedFace(TerrainMatterView.Face.SOUTH, height);
|
||||
MatterCavern[] fromWest = west == null ? null : TerrainMatterView.getComposedFace(west, TerrainMatterView.Face.EAST, height);
|
||||
MatterCavern[] fromEast = east == null ? null : TerrainMatterView.getComposedFace(east, TerrainMatterView.Face.WEST, height);
|
||||
MatterCavern[] fromNorth = north == null ? null : TerrainMatterView.getComposedFace(north, TerrainMatterView.Face.SOUTH, height);
|
||||
@@ -628,16 +664,16 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
for (int offset = 0; offset < 16; offset++) {
|
||||
int index = yy * 16 + offset;
|
||||
if (west != null) {
|
||||
tryAddBoundaryWall(ownWest[index], fromWest[index], walls, boundaryMasks, 0, yy, offset);
|
||||
tryAddBoundaryWall(terrain.composedCavern(0, yy, offset), fromWest[index], walls, boundaryMasks, 0, yy, offset);
|
||||
}
|
||||
if (east != null) {
|
||||
tryAddBoundaryWall(ownEast[index], fromEast[index], walls, boundaryMasks, 15, yy, offset);
|
||||
tryAddBoundaryWall(terrain.composedCavern(15, yy, offset), fromEast[index], walls, boundaryMasks, 15, yy, offset);
|
||||
}
|
||||
if (north != null) {
|
||||
tryAddBoundaryWall(ownNorth[index], fromNorth[index], walls, boundaryMasks, offset, yy, 0);
|
||||
tryAddBoundaryWall(terrain.composedCavern(offset, yy, 0), fromNorth[index], walls, boundaryMasks, offset, yy, 0);
|
||||
}
|
||||
if (south != null) {
|
||||
tryAddBoundaryWall(ownSouth[index], fromSouth[index], walls, boundaryMasks, offset, yy, 15);
|
||||
tryAddBoundaryWall(terrain.composedCavern(offset, yy, 15), fromSouth[index], walls, boundaryMasks, offset, yy, 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -677,7 +713,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
int chunkZ,
|
||||
CaveInputs resolverState,
|
||||
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
|
||||
Map<String, IrisBiome> customBiomeCache
|
||||
Map<String, IrisBiome> customBiomeCache,
|
||||
CaveLayerMemo layers
|
||||
) {
|
||||
if (columnMask == null || columnMask.isEmpty()) {
|
||||
return;
|
||||
@@ -692,33 +729,19 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
int rz = columnIndex & 15;
|
||||
int worldX = rx + PowerOfTwoCoordinates.chunkToBlock(chunkX);
|
||||
int worldZ = rz + PowerOfTwoCoordinates.chunkToBlock(chunkZ);
|
||||
int height = resolverState.height;
|
||||
layers.reset();
|
||||
int floor = firstHeight;
|
||||
while (floor >= 0 && floor <= height) {
|
||||
int end = columnMask.nextClearBit(floor) - 1;
|
||||
CaveZone zone = new CaveZone();
|
||||
zone.setFloor(firstHeight);
|
||||
int buf = firstHeight - 1;
|
||||
int y = firstHeight;
|
||||
|
||||
while (y >= 0) {
|
||||
if (y <= resolverState.height) {
|
||||
if (y == buf + 1) {
|
||||
buf = y;
|
||||
zone.ceiling = buf;
|
||||
} else {
|
||||
if (zone.isValid(resolverState.height)) {
|
||||
zone.setFloor(floor);
|
||||
zone.setCeiling(Math.min(end, height));
|
||||
if (zone.isValid(height)) {
|
||||
processZone(output, terrain, zone, rx, rz, worldX, worldZ, resolverState,
|
||||
caveBiomeCache, customBiomeCache);
|
||||
caveBiomeCache, customBiomeCache, layers);
|
||||
}
|
||||
zone = new CaveZone();
|
||||
zone.setFloor(y);
|
||||
buf = y;
|
||||
}
|
||||
}
|
||||
|
||||
y = columnMask.nextSetBit(y + 1);
|
||||
}
|
||||
|
||||
if (zone.isValid(resolverState.height)) {
|
||||
processZone(output, terrain, zone, rx, rz, worldX, worldZ, resolverState,
|
||||
caveBiomeCache, customBiomeCache);
|
||||
floor = columnMask.nextSetBit(end + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -732,7 +755,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
int chunkZ,
|
||||
CaveInputs resolverState,
|
||||
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
|
||||
Map<String, IrisBiome> customBiomeCache
|
||||
Map<String, IrisBiome> customBiomeCache,
|
||||
CaveLayerMemo layers
|
||||
) {
|
||||
int firstHeight = boundaryMask.nextSetBit(0);
|
||||
if (firstHeight < 0) {
|
||||
@@ -743,6 +767,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
int rz = columnIndex & 15;
|
||||
int worldX = rx + PowerOfTwoCoordinates.chunkToBlock(chunkX);
|
||||
int worldZ = rz + PowerOfTwoCoordinates.chunkToBlock(chunkZ);
|
||||
layers.reset();
|
||||
int zoneFloor = firstHeight;
|
||||
int zoneCeiling = firstHeight;
|
||||
int y = boundaryMask.nextSetBit(firstHeight + 1);
|
||||
@@ -752,7 +777,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
zoneCeiling = y;
|
||||
} else {
|
||||
paintBoundaryZone(output, terrain, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling,
|
||||
resolverState, caveBiomeCache, customBiomeCache);
|
||||
resolverState, caveBiomeCache, customBiomeCache, layers);
|
||||
zoneFloor = y;
|
||||
zoneCeiling = y;
|
||||
}
|
||||
@@ -760,7 +785,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
}
|
||||
|
||||
paintBoundaryZone(output, terrain, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling,
|
||||
resolverState, caveBiomeCache, customBiomeCache);
|
||||
resolverState, caveBiomeCache, customBiomeCache, layers);
|
||||
}
|
||||
|
||||
private void paintBoundaryZone(
|
||||
@@ -775,7 +800,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
int zoneCeiling,
|
||||
CaveInputs resolverState,
|
||||
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
|
||||
Map<String, IrisBiome> customBiomeCache
|
||||
Map<String, IrisBiome> customBiomeCache,
|
||||
CaveLayerMemo layers
|
||||
) {
|
||||
IrisBiome floorBiome = resolveCaveBoundaryBiome(
|
||||
walls.get(rx, zoneFloor, rz), worldX, zoneFloor, worldZ,
|
||||
@@ -789,9 +815,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
|
||||
if (floorBiome != null) {
|
||||
HydrologyCaveCell floorHydrology = terrain.hydrology(rx, zoneFloor, rz);
|
||||
IrisRiverMaterialConfig bedMaterial = undergroundBedMaterial();
|
||||
KList<NativeBlockState> floorLayers = floorBiome.generateLayers(
|
||||
resolverState.dimension, worldX, worldZ, rng, 3, zoneFloor, resolverState.data, resolverState.complex);
|
||||
IrisRiverMaterialConfig bedMaterial = undergroundBedMaterial(resolverState.dimension);
|
||||
KList<NativeBlockState> floorLayers = floorLayers(layers, floorBiome, worldX, worldZ, zoneFloor, resolverState);
|
||||
for (int i = 0; i < zoneFloor - 1; i++) {
|
||||
if (!floorLayers.hasIndex(i)) {
|
||||
break;
|
||||
@@ -828,8 +853,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
|
||||
if (ceilingBiome != null) {
|
||||
int worldMaxY = getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight();
|
||||
KList<NativeBlockState> ceilingLayers = ceilingBiome.generateCeilingLayers(
|
||||
resolverState.dimension, worldX, worldZ, rng, 3, zoneCeiling, resolverState.data, resolverState.complex);
|
||||
KList<NativeBlockState> ceilingLayers = ceilingLayers(layers, ceilingBiome, worldX, worldZ, zoneCeiling, resolverState);
|
||||
for (int i = 0; i < ceilingLayers.size(); i++) {
|
||||
int ceilingY = zoneCeiling + i + 1;
|
||||
if (ceilingY >= worldMaxY) {
|
||||
@@ -874,7 +898,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
CaveZone zone, int rx, int rz, int xx, int zz,
|
||||
CaveInputs resolverState,
|
||||
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
|
||||
Map<String, IrisBiome> customBiomeCache) {
|
||||
Map<String, IrisBiome> customBiomeCache,
|
||||
CaveLayerMemo layers) {
|
||||
int maxY = output.getHeight();
|
||||
|
||||
if (zone.ceiling + 1 < maxY && B.isDecorant(output.getRaw(rx, zone.ceiling + 1, rz))) {
|
||||
@@ -894,8 +919,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
|
||||
if (floorBiome != null) {
|
||||
HydrologyCaveCell floorHydrology = terrain.hydrology(rx, zone.floor, rz);
|
||||
IrisRiverMaterialConfig bedMaterial = undergroundBedMaterial();
|
||||
KList<NativeBlockState> floorBlocks = floorBiome.generateLayers(resolverState.dimension, xx, zz, rng, 3, zone.floor, resolverState.data, resolverState.complex);
|
||||
IrisRiverMaterialConfig bedMaterial = undergroundBedMaterial(resolverState.dimension);
|
||||
KList<NativeBlockState> floorBlocks = floorLayers(layers, floorBiome, xx, zz, zone.floor, resolverState);
|
||||
for (int i = 0; i < zone.floor - 1; i++) {
|
||||
if (!floorBlocks.hasIndex(i)) {
|
||||
break;
|
||||
@@ -928,7 +953,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
}
|
||||
|
||||
if (ceilingBiome != null) {
|
||||
KList<NativeBlockState> ceilingBlocks = ceilingBiome.generateCeilingLayers(resolverState.dimension, xx, zz, rng, 3, zone.ceiling, resolverState.data, resolverState.complex);
|
||||
KList<NativeBlockState> ceilingBlocks = ceilingLayers(layers, ceilingBiome, xx, zz, zone.ceiling, resolverState);
|
||||
for (int i = 0; i < ceilingBlocks.size(); i++) {
|
||||
int cy = zone.ceiling + i + 1;
|
||||
if (cy >= maxY) {
|
||||
@@ -956,6 +981,44 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
normalizeCaveZoneWaterlogging(output, terrain, zone, rx, rz, xx, zz);
|
||||
}
|
||||
|
||||
private KList<NativeBlockState> floorLayers(CaveLayerMemo layers, IrisBiome biome, int worldX, int worldZ, int floorY,
|
||||
CaveInputs inputs) {
|
||||
if (!hasHeightFreeFloorLayers(biome)) {
|
||||
return biome.generateLayers(inputs.dimension, worldX, worldZ, rng, 3, floorY, inputs.data, inputs.complex);
|
||||
}
|
||||
KList<NativeBlockState> remembered = layers.floor(biome);
|
||||
if (remembered != null) {
|
||||
return remembered;
|
||||
}
|
||||
KList<NativeBlockState> blocks = biome.generateLayers(inputs.dimension, worldX, worldZ, rng, 3, floorY, inputs.data, inputs.complex);
|
||||
layers.rememberFloor(biome, blocks);
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private KList<NativeBlockState> ceilingLayers(CaveLayerMemo layers, IrisBiome biome, int worldX, int worldZ, int ceilingY,
|
||||
CaveInputs inputs) {
|
||||
KList<NativeBlockState> remembered = layers.ceiling(biome);
|
||||
if (remembered != null) {
|
||||
return remembered;
|
||||
}
|
||||
KList<NativeBlockState> blocks = biome.generateCeilingLayers(inputs.dimension, worldX, worldZ, rng, 3, ceilingY, inputs.data, inputs.complex);
|
||||
layers.rememberCeiling(biome, blocks);
|
||||
return blocks;
|
||||
}
|
||||
|
||||
static boolean hasHeightFreeFloorLayers(IrisBiome biome) {
|
||||
if (biome.isLockLayers()) {
|
||||
return false;
|
||||
}
|
||||
KList<IrisBiomePaletteLayer> layers = biome.getLayers();
|
||||
for (int index = 0; index < layers.size(); index++) {
|
||||
if (!layers.get(index).getSlopeCondition().isDefault()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void decorateNaturalCaves(int blockX, int blockZ, Hunk<NativeBlockState> output, ChunkContext context) {
|
||||
Mantle<Matter> mantle = getEngine().getMantle().getMantle();
|
||||
CaveTerrainSnapshot terrain = context == null ? null : context.getCaveTerrain();
|
||||
@@ -971,35 +1034,38 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
}
|
||||
}
|
||||
CaveInputs resolver = CaveInputs.forContentChunk(getEngine(), blockX >> 4, blockZ >> 4);
|
||||
boolean caveFloorMarkers = getDimension().hasUndergroundSpawners(getEngine());
|
||||
Long2ObjectOpenHashMap<IrisBiome> caveBiomes = new Long2ObjectOpenHashMap<>(256);
|
||||
Map<String, IrisBiome> customBiomes = new HashMap<>();
|
||||
int width = output.getWidth();
|
||||
int depth = output.getDepth();
|
||||
int height = output.getHeight();
|
||||
ColumnExtent extent = output instanceof ColumnExtent columnExtent ? columnExtent : null;
|
||||
SolidMemo solids = new SolidMemo();
|
||||
for (int localX = 0; localX < width; localX++) {
|
||||
for (int localZ = 0; localZ < depth; localZ++) {
|
||||
int worldX = blockX + localX;
|
||||
int worldZ = blockZ + localZ;
|
||||
int floor = -1;
|
||||
int top = extent == null ? height - 1 : extent.highestStoredY(localX, localZ);
|
||||
boolean previousKnown = false;
|
||||
boolean previousSolid = false;
|
||||
for (int y = 1; y < height; y++) {
|
||||
if (y > top) {
|
||||
break;
|
||||
}
|
||||
NativeBlockState state = output.getRaw(localX, y, localZ);
|
||||
if (B.isSolid(state)) {
|
||||
boolean solid = solids.test(output.getRaw(localX, y, localZ));
|
||||
boolean decorated = false;
|
||||
if (solid) {
|
||||
if (floor >= 0) {
|
||||
CaveZone zone = new CaveZone();
|
||||
zone.setFloor(floor);
|
||||
zone.setCeiling(y - 1);
|
||||
if (zone.isValid(resolver.height)
|
||||
&& (subterrainCell(worldX, floor, worldZ).occupied()
|
||||
|| !isNaturalTerrainOpening(getComplex(), worldX, floor, worldZ, context))) {
|
||||
if (markerRoll(worldX, zone.ceiling, worldZ, 0x9E3779B97F4A7C15L)) {
|
||||
mantle.set(worldX, zone.ceiling, worldZ, MarkerMatter.CAVE_CEILING);
|
||||
}
|
||||
if (markerRoll(worldX, zone.floor, worldZ, 0xC2B2AE3D27D4EB4FL)) {
|
||||
SubterrainCell floorCell = zone.isValid(resolver.height)
|
||||
? resolver.cells.cell(worldX, floor, worldZ) : null;
|
||||
if (floorCell != null && (floorCell.occupied()
|
||||
|| !isNaturalTerrainOpening(resolver.complex, worldX, floor, worldZ, context))) {
|
||||
if (caveFloorMarkers && markerRoll(worldX, zone.floor, worldZ, 0xC2B2AE3D27D4EB4FL)) {
|
||||
mantle.set(worldX, zone.floor, worldZ, MarkerMatter.CAVE_FLOOR);
|
||||
}
|
||||
IrisBiome floorBiome = resolveCaveBoundaryBiome(terrain, localX, floor, localZ,
|
||||
@@ -1007,25 +1073,28 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
IrisBiome ceilingBiome = resolveCaveBoundaryBiome(terrain, localX, y - 1, localZ,
|
||||
worldX, worldZ, resolver, caveBiomes, customBiomes);
|
||||
decorateZone(output, zone, localX, localZ, worldX, worldZ,
|
||||
floorBiome, ceilingBiome);
|
||||
floorBiome, ceilingBiome, floorCell);
|
||||
decorated = true;
|
||||
if (extent != null) {
|
||||
top = extent.highestStoredY(localX, localZ);
|
||||
}
|
||||
}
|
||||
floor = -1;
|
||||
}
|
||||
} else if (floor < 0 && B.isSolid(output.getRaw(localX, y - 1, localZ))) {
|
||||
} else if (floor < 0 && (previousKnown ? previousSolid
|
||||
: solids.test(output.getRaw(localX, y - 1, localZ)))) {
|
||||
floor = y;
|
||||
}
|
||||
previousSolid = solid;
|
||||
previousKnown = !decorated;
|
||||
}
|
||||
}
|
||||
}
|
||||
restoreSubterrainSolids(output, blockX >> 4, blockZ >> 4, true);
|
||||
}
|
||||
|
||||
private void decorateZone(Hunk<NativeBlockState> output, CaveZone zone,
|
||||
int rx, int rz, int xx, int zz, IrisBiome floorBiome, IrisBiome ceilingBiome) {
|
||||
SubterrainCell feature = subterrainCell(xx, zone.getFloor(), zz);
|
||||
private void decorateZone(Hunk<NativeBlockState> output, CaveZone zone, int rx, int rz, int xx, int zz,
|
||||
IrisBiome floorBiome, IrisBiome ceilingBiome, SubterrainCell feature) {
|
||||
boolean reservedPassage = feature.owned() && feature.room().reservedPassage();
|
||||
int maxY = output.getHeight();
|
||||
IrisDecorator[] surfaceDecorators = floorBiome == null
|
||||
@@ -1054,6 +1123,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
int worldX,
|
||||
int worldZ
|
||||
) {
|
||||
if (!terrain.hasHydrology()) {
|
||||
return;
|
||||
}
|
||||
int minimumY = Math.max(0, zone.floor - 1);
|
||||
int maximumY = Math.min(output.getHeight() - 1, zone.ceiling + 1);
|
||||
for (int y = minimumY; y <= maximumY; y++) {
|
||||
@@ -1087,7 +1159,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
}
|
||||
|
||||
IrisBiome resolveCaveBoundaryBiome(MatterCavern cavern, int worldX, int y, int worldZ, CaveInputs resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache) {
|
||||
IrisBiome featureBiome = getEngine().getSubterrainBiome(worldX, y, worldZ);
|
||||
IrisBiome featureBiome = resolverState.cells.biome(worldX, y, worldZ);
|
||||
if (featureBiome != null) {
|
||||
return featureBiome;
|
||||
}
|
||||
@@ -1124,8 +1196,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
}
|
||||
|
||||
/** The dimension's underground river bed palette, or null when nothing paints. */
|
||||
private IrisRiverMaterialConfig undergroundBedMaterial() {
|
||||
IrisHydrology hydrology = getDimension().getHydrology();
|
||||
private static IrisRiverMaterialConfig undergroundBedMaterial(IrisDimension dimension) {
|
||||
IrisHydrology hydrology = dimension.getHydrology();
|
||||
if (hydrology == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -1233,7 +1305,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
return null;
|
||||
}
|
||||
|
||||
int roll = Math.floorMod(rng.nextParallelRNG(BlockPosition.toLong(x, y, z)).nextInt(), CAVE_BIOME_BLEND_TOTAL_WEIGHT);
|
||||
int roll = Math.floorMod(caveBlendRandom(caveBlendSeed, x, y, z), CAVE_BIOME_BLEND_TOTAL_WEIGHT);
|
||||
if (roll < CAVE_BIOME_BLEND_CENTER_WEIGHT) {
|
||||
return center;
|
||||
}
|
||||
@@ -1247,7 +1319,15 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
return neighbor != null ? neighbor : center;
|
||||
}
|
||||
|
||||
static int caveBlendRandom(long seed, int x, int y, int z) {
|
||||
long scrambled = ((seed + BlockPosition.toLong(x, y, z)) ^ RANDOM_MULTIPLIER) & RANDOM_MASK;
|
||||
return (int) (((scrambled * RANDOM_MULTIPLIER + RANDOM_ADDEND) & RANDOM_MASK) >>> 16);
|
||||
}
|
||||
|
||||
private IrisBiome sampleCaveBiome(Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, int x, int y, int z, CaveInputs resolverState) {
|
||||
if (resolverState.captures(x, z)) {
|
||||
return resolverState.resolve(x, y, z);
|
||||
}
|
||||
long key = BlockPosition.toLong(x, y, z);
|
||||
IrisBiome cachedBiome = caveBiomeCache.get(key);
|
||||
if (cachedBiome != null) {
|
||||
@@ -1279,6 +1359,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
private final int height;
|
||||
private final IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State();
|
||||
private final IrisDimensionCarvingResolver.Snapshot snapshot;
|
||||
private final SubterrainChunkCells cells;
|
||||
private final boolean subterrainFirst;
|
||||
private final int minimumY;
|
||||
private final DimensionStackContext stack;
|
||||
private final ProceduralStream<IrisBiome> surfaceBiomes;
|
||||
@@ -1293,6 +1375,10 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
this(engine, false, 0, 0);
|
||||
}
|
||||
|
||||
static CaveInputs forTerrainChunk(Engine engine, int chunkX, int chunkZ) {
|
||||
return new CaveInputs(engine, false, chunkX, chunkZ);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inputs for the content stage of one chunk. A coordinate inside that chunk resolves under the chunk's
|
||||
* own generation route, exactly the binding the natural terrain stage samples with, so it takes the
|
||||
@@ -1308,14 +1394,19 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
dimension = engine.getDimension();
|
||||
data = engine.getData();
|
||||
height = engine.getHeight();
|
||||
cells = SubterrainChunkCells.of(engine, chunkX, chunkZ);
|
||||
IrisContext context = IrisContext.get();
|
||||
boolean generating = engine instanceof IrisEngine irisEngine
|
||||
&& irisEngine.hasGenerationRuntimeScope() && !engine.getPlatformHooks().isMainThread()
|
||||
IrisEngine irisEngine = engine instanceof IrisEngine candidate ? candidate : null;
|
||||
boolean bound = irisEngine != null && irisEngine.hasGenerationRuntimeScope();
|
||||
boolean worker = irisEngine != null && !engine.getPlatformHooks().isMainThread();
|
||||
boolean generating = bound && worker
|
||||
&& context != null && context.getChunkContext() != null
|
||||
&& context.getChunkContext().getComplex() == complex;
|
||||
boolean natural = generating && context.getChunkContext().isNaturalTerrain();
|
||||
boolean scoped = natural || generating && contentChunk;
|
||||
this.chunkLimited = !natural;
|
||||
boolean unrouted = !bound && worker && !irisEngine.hasGenerationHistoryRouting();
|
||||
boolean scoped = natural || generating && contentChunk || unrouted;
|
||||
this.subterrainFirst = unrouted;
|
||||
this.chunkLimited = !natural && !unrouted;
|
||||
this.chunkX = chunkX;
|
||||
this.chunkZ = chunkZ;
|
||||
snapshot = scoped ? IrisDimensionCarvingResolver.snapshot(engine) : null;
|
||||
@@ -1327,10 +1418,20 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
columns = scoped ? new CaveColumn[1024] : null;
|
||||
}
|
||||
|
||||
boolean captures(int x, int z) {
|
||||
return snapshot != null && (!chunkLimited || (x >> 4) == chunkX && (z >> 4) == chunkZ);
|
||||
}
|
||||
|
||||
IrisBiome resolve(int x, int y, int z) {
|
||||
if (snapshot == null || chunkLimited && ((x >> 4) != chunkX || (z >> 4) != chunkZ)) {
|
||||
if (!captures(x, z)) {
|
||||
return engine.getCaveBiome(x, y, z, state);
|
||||
}
|
||||
if (subterrainFirst) {
|
||||
IrisBiome feature = cells.biome(x, y, z);
|
||||
if (feature != null) {
|
||||
return feature;
|
||||
}
|
||||
}
|
||||
IrisBiome configured = snapshot.resolveBiome(x, y + minimumY, z);
|
||||
if (configured != null) {
|
||||
return configured;
|
||||
@@ -1364,6 +1465,19 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
private record CaveColumn(long key, IrisBiome surface, IrisBiome cave, int surfaceY, int minimumDepth) {
|
||||
}
|
||||
|
||||
private static final class SolidMemo {
|
||||
private NativeBlockState state;
|
||||
private boolean solid;
|
||||
|
||||
private boolean test(NativeBlockState candidate) {
|
||||
if (candidate != state) {
|
||||
state = candidate;
|
||||
solid = B.isSolid(candidate);
|
||||
}
|
||||
return solid;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class CaveZone {
|
||||
private int ceiling = -1;
|
||||
|
||||
@@ -19,8 +19,11 @@
|
||||
package art.arcane.iris.generation.stage;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
@@ -29,9 +32,12 @@ import java.util.Map;
|
||||
final class IrisCarveScratch {
|
||||
final CarveColumnMask[] columnMasks = new CarveColumnMask[256];
|
||||
final CarveColumnMask[] boundaryMasks = new CarveColumnMask[256];
|
||||
final CarveColumnMask[] wallMasks = new CarveColumnMask[256];
|
||||
final int[] surfaceHeights = new int[256];
|
||||
final CarveWallBuffer walls = new CarveWallBuffer(512);
|
||||
final Map<String, IrisBiome> customBiomeCache = new HashMap<>();
|
||||
final Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache = new Long2ObjectOpenHashMap<>(4096);
|
||||
final CaveLayerMemo layers = new CaveLayerMemo();
|
||||
int[] upperSurfaceHeights;
|
||||
boolean customCaveBiomePresent;
|
||||
|
||||
@@ -39,6 +45,7 @@ final class IrisCarveScratch {
|
||||
for (int index = 0; index < columnMasks.length; index++) {
|
||||
columnMasks[index] = new CarveColumnMask();
|
||||
boundaryMasks[index] = new CarveColumnMask();
|
||||
wallMasks[index] = new CarveColumnMask();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,13 +60,61 @@ final class IrisCarveScratch {
|
||||
for (int index = 0; index < columnMasks.length; index++) {
|
||||
columnMasks[index].clear();
|
||||
boundaryMasks[index].clear();
|
||||
wallMasks[index].clear();
|
||||
}
|
||||
walls.clear();
|
||||
customBiomeCache.clear();
|
||||
caveBiomeCache.clear();
|
||||
customCaveBiomePresent = false;
|
||||
}
|
||||
}
|
||||
|
||||
final class CaveLayerMemo {
|
||||
private static final int SLOTS = 4;
|
||||
|
||||
private final Layers[] floor = new Layers[SLOTS];
|
||||
private final Layers[] ceiling = new Layers[SLOTS];
|
||||
private int floorCount;
|
||||
private int ceilingCount;
|
||||
|
||||
void reset() {
|
||||
floorCount = 0;
|
||||
ceilingCount = 0;
|
||||
}
|
||||
|
||||
KList<NativeBlockState> floor(IrisBiome biome) {
|
||||
return find(floor, floorCount, biome);
|
||||
}
|
||||
|
||||
KList<NativeBlockState> ceiling(IrisBiome biome) {
|
||||
return find(ceiling, ceilingCount, biome);
|
||||
}
|
||||
|
||||
void rememberFloor(IrisBiome biome, KList<NativeBlockState> blocks) {
|
||||
if (floorCount < SLOTS) {
|
||||
floor[floorCount++] = new Layers(biome, blocks);
|
||||
}
|
||||
}
|
||||
|
||||
void rememberCeiling(IrisBiome biome, KList<NativeBlockState> blocks) {
|
||||
if (ceilingCount < SLOTS) {
|
||||
ceiling[ceilingCount++] = new Layers(biome, blocks);
|
||||
}
|
||||
}
|
||||
|
||||
private static KList<NativeBlockState> find(Layers[] entries, int count, IrisBiome biome) {
|
||||
for (int index = 0; index < count; index++) {
|
||||
if (entries[index].biome() == biome) {
|
||||
return entries[index].blocks();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private record Layers(IrisBiome biome, KList<NativeBlockState> blocks) {
|
||||
}
|
||||
}
|
||||
|
||||
final class CarveColumnMask {
|
||||
private long[] words = new long[8];
|
||||
private int maxWord = -1;
|
||||
@@ -105,10 +160,59 @@ final class CarveColumnMask {
|
||||
}
|
||||
}
|
||||
|
||||
int nextClearBit(int fromBit) {
|
||||
int startBit = Math.max(0, fromBit);
|
||||
int wordIndex = startBit >> 6;
|
||||
if (wordIndex > maxWord) {
|
||||
return startBit;
|
||||
}
|
||||
|
||||
long word = ~words[wordIndex] & (-1L << (startBit & 63));
|
||||
while (true) {
|
||||
if (word != 0L) {
|
||||
return (wordIndex << 6) + Long.numberOfTrailingZeros(word);
|
||||
}
|
||||
|
||||
wordIndex++;
|
||||
if (wordIndex > maxWord) {
|
||||
return wordIndex << 6;
|
||||
}
|
||||
word = ~words[wordIndex];
|
||||
}
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return maxWord < 0;
|
||||
}
|
||||
|
||||
void collectWalls(CarveColumnMask own, CarveColumnMask first, CarveColumnMask second,
|
||||
CarveColumnMask third, CarveColumnMask fourth) {
|
||||
int top = Math.max(Math.max(topWord(first), topWord(second)), Math.max(topWord(third), topWord(fourth)));
|
||||
clear();
|
||||
if (top < 0) {
|
||||
return;
|
||||
}
|
||||
if (top >= words.length) {
|
||||
words = Arrays.copyOf(words, top + 1);
|
||||
}
|
||||
for (int index = 0; index <= top; index++) {
|
||||
long walls = (word(first, index) | word(second, index) | word(third, index) | word(fourth, index))
|
||||
& ~word(own, index);
|
||||
words[index] = walls;
|
||||
if (walls != 0L) {
|
||||
maxWord = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int topWord(CarveColumnMask mask) {
|
||||
return mask == null ? -1 : mask.maxWord;
|
||||
}
|
||||
|
||||
private static long word(CarveColumnMask mask, int index) {
|
||||
return mask == null || index > mask.maxWord ? 0L : mask.words[index];
|
||||
}
|
||||
|
||||
boolean contains(int y) {
|
||||
if (y < 0) {
|
||||
return false;
|
||||
|
||||
@@ -23,6 +23,8 @@ import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.generation.runtime.EngineAssignedModifier;
|
||||
import art.arcane.iris.generation.runtime.DimensionStackLayout;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.chunk.ColumnExtent;
|
||||
import art.arcane.iris.generation.decoration.DepositRandom;
|
||||
import art.arcane.iris.generation.decoration.IrisDepositBiomeScope;
|
||||
import art.arcane.iris.generation.decoration.IrisDepositGenerator;
|
||||
import art.arcane.iris.generation.decoration.IrisDepositHeightDistribution;
|
||||
@@ -52,14 +54,14 @@ import art.arcane.iris.pack.loading.ResourceLoader;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState> {
|
||||
private static final int CLUMPS_PER_BATCH = 8;
|
||||
private static final int PREPARATION_BATCH_COUNT = 4;
|
||||
private static final int PREPARATION_TASKS = 4;
|
||||
private static final int MINIMUM_TASK_ATTEMPTS = 96;
|
||||
|
||||
private final RNG rng;
|
||||
private volatile DepositVariantScan variantScan;
|
||||
private volatile PackScan packScan;
|
||||
|
||||
public IrisDepositModifier(Engine engine) {
|
||||
super(engine, "Deposit");
|
||||
@@ -87,49 +89,47 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
||||
|
||||
BurstExecutor burst = burst().burst(multicore);
|
||||
long seed = x * 341873128712L + z * 132897987541L;
|
||||
PreparedDeposit[] prepared = new PreparedDeposit[PREPARATION_BATCH_COUNT];
|
||||
int pending = 0;
|
||||
int highestSurface = highestTerrainSurface(x, z, null, context);
|
||||
IrisEngine generationEngine = getEngine() instanceof IrisEngine irisEngine
|
||||
&& irisEngine.hasGenerationRuntimeScope() ? irisEngine : null;
|
||||
PreparationContext preparation = new PreparationContext(context, generationEngine,
|
||||
generationEngine == null ? null : generationEngine.captureGenerationRuntimeBinding(), new Throwable[PREPARATION_BATCH_COUNT]);
|
||||
MantleChunk chunk = getEngine().getMantle().getMantle().useChunk(x, z);
|
||||
PreparationContext preparation = null;
|
||||
Throwable callerFailure = null;
|
||||
try {
|
||||
List<DepositPlan> plans = new ArrayList<>(generators.size());
|
||||
AboveTerrainHosts aboveTerrainHosts = new AboveTerrainHosts(terrain, context);
|
||||
int attempts = 0;
|
||||
for (int i = 0; i < generators.size(); i++) {
|
||||
DepositPlan plan = plan(generators.get(i), rng.nextParallelRNG(seed * (i + 1L)), context);
|
||||
for (int first = 0; first < plan.attempts(); first += CLUMPS_PER_BATCH) {
|
||||
int firstAttempt = first;
|
||||
int limit = Math.min(plan.attempts(), first + CLUMPS_PER_BATCH);
|
||||
int index = pending++;
|
||||
if (plan.attempts() == 0 || footprintsMissTerrain(plan, highestSurface)
|
||||
|| plan.generator().getPlacementScope() == IrisDepositPlacementScope.ABOVE_TERRAIN
|
||||
&& !aboveTerrainHosts.present(plan.generator().getSurfaceClearance())) {
|
||||
continue;
|
||||
}
|
||||
plans.add(plan);
|
||||
attempts += plan.attempts();
|
||||
}
|
||||
if (attempts == 0) {
|
||||
return;
|
||||
}
|
||||
int tasks = multicore
|
||||
? Math.max(1, Math.min(PREPARATION_TASKS, Math.ceilDiv(attempts, MINIMUM_TASK_ATTEMPTS)))
|
||||
: 1;
|
||||
int attemptsPerTask = Math.ceilDiv(attempts, tasks);
|
||||
preparation = new PreparationContext(context, generationEngine,
|
||||
generationEngine == null ? null : generationEngine.captureGenerationRuntimeBinding(), new Throwable[tasks]);
|
||||
PreparedDeposit[][] prepared = new PreparedDeposit[tasks][];
|
||||
for (int task = 0; task < tasks; task++) {
|
||||
int first = task * attemptsPerTask;
|
||||
int last = Math.min(attempts, first + attemptsPerTask);
|
||||
if (first >= last) {
|
||||
break;
|
||||
}
|
||||
int index = task;
|
||||
burst.queue(scopedDepositTask(
|
||||
() -> prepared[index] = prepare(plan, firstAttempt, limit, x, z, null, context), preparation, index));
|
||||
if (pending == prepared.length) {
|
||||
completeBatches(burst, prepared, chunk, terrain, x, z, preparation);
|
||||
pending = 0;
|
||||
() -> prepared[index] = prepareSlice(plans, first, last, x, z, context, highestSurface),
|
||||
preparation, index));
|
||||
}
|
||||
}
|
||||
}
|
||||
completeBatches(burst, prepared, chunk, terrain, x, z, preparation);
|
||||
} catch (Throwable failure) {
|
||||
callerFailure = failure;
|
||||
throw failure;
|
||||
} finally {
|
||||
// complete() must run before release() even when queueing throws — already
|
||||
// submitted burst tasks must never write into a released chunk.
|
||||
try {
|
||||
burst.complete();
|
||||
if (callerFailure != null) {
|
||||
preparation.takeFailure(callerFailure);
|
||||
}
|
||||
} finally {
|
||||
chunk.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void completeBatches(BurstExecutor burst, PreparedDeposit[] prepared, MantleChunk chunk,
|
||||
Hunk<NativeBlockState> terrain, int x, int z, PreparationContext preparation) {
|
||||
burst.complete();
|
||||
Throwable failure = preparation.takeFailure(null);
|
||||
if (failure instanceof Error error) {
|
||||
@@ -141,15 +141,49 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
||||
if (failure != null) {
|
||||
throw new IllegalStateException("Deposit preparation failed", failure);
|
||||
}
|
||||
try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), preparation.chunk().getGenerationSessionId(), preparation.chunk())) {
|
||||
for (int i = 0; i < prepared.length; i++) {
|
||||
PreparedDeposit deposit = prepared[i];
|
||||
prepared[i] = null;
|
||||
if (deposit != null) {
|
||||
place(deposit, chunk, terrain, x, z, null, preparation.chunk());
|
||||
try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), context.getGenerationSessionId(), context)) {
|
||||
for (PreparedDeposit[] slice : prepared) {
|
||||
if (slice == null) {
|
||||
continue;
|
||||
}
|
||||
for (PreparedDeposit deposit : slice) {
|
||||
place(deposit, chunk, terrain, x, z, null, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable failure) {
|
||||
callerFailure = failure;
|
||||
throw failure;
|
||||
} finally {
|
||||
// complete() must run before release() even when queueing throws — already
|
||||
// submitted burst tasks must never write into a released chunk.
|
||||
try {
|
||||
burst.complete();
|
||||
if (callerFailure != null && preparation != null) {
|
||||
preparation.takeFailure(callerFailure);
|
||||
}
|
||||
} finally {
|
||||
chunk.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PreparedDeposit[] prepareSlice(List<DepositPlan> plans, int first, int last, int cx, int cz,
|
||||
ChunkContext context, int highestSurface) {
|
||||
List<PreparedDeposit> deposits = new ArrayList<>();
|
||||
int offset = 0;
|
||||
for (DepositPlan plan : plans) {
|
||||
int start = Math.max(first, offset);
|
||||
int end = Math.min(last, offset + plan.attempts());
|
||||
if (start < end) {
|
||||
deposits.add(prepare(plan, start - offset, end - offset, cx, cz, null, context, highestSurface));
|
||||
}
|
||||
offset += plan.attempts();
|
||||
if (offset >= last) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return deposits.toArray(new PreparedDeposit[0]);
|
||||
}
|
||||
|
||||
private Runnable scopedDepositTask(Runnable task, PreparationContext preparation, int index) {
|
||||
@@ -170,10 +204,25 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
||||
|
||||
public void generate(IrisDepositGenerator k, MantleChunk chunk, Hunk<NativeBlockState> data, RNG rng, int cx, int cz, boolean safe, HeightMap he, ChunkContext context) {
|
||||
DepositPlan plan = plan(k, rng, context);
|
||||
for (int first = 0; first < plan.attempts(); first += CLUMPS_PER_BATCH) {
|
||||
int limit = Math.min(plan.attempts(), first + CLUMPS_PER_BATCH);
|
||||
place(prepare(plan, first, limit, cx, cz, he, context), chunk, data, cx, cz, he, context);
|
||||
if (plan.attempts() == 0) {
|
||||
return;
|
||||
}
|
||||
place(prepare(plan, 0, plan.attempts(), cx, cz, he, context, Integer.MIN_VALUE), chunk, data, cx, cz, he, context);
|
||||
}
|
||||
|
||||
private boolean footprintsMissTerrain(DepositPlan plan, int highestSurface) {
|
||||
IrisDepositGenerator generator = plan.generator();
|
||||
if (highestSurface == Integer.MIN_VALUE
|
||||
|| generator.getPlacementScope() != IrisDepositPlacementScope.TERRAIN
|
||||
|| !generator.usesClumpFootprints(getData())
|
||||
|| plan.ore() && packScan().oreScaling()) {
|
||||
return false;
|
||||
}
|
||||
int minimum = generator.getHeightDistribution() == IrisDepositHeightDistribution.CLIPPED_UNIFORM
|
||||
? Math.max(0, generator.getMinHeight())
|
||||
: generator.getMinHeight();
|
||||
long lowest = (long) minimum + IrisDepositGenerator.lowestFootprintOffset(generator.getMaxSize());
|
||||
return lowest > (long) highestSurface - Math.max(0, generator.getSurfaceClearance());
|
||||
}
|
||||
|
||||
private DepositPlan plan(IrisDepositGenerator generator, RNG rng, ChunkContext context) {
|
||||
@@ -213,21 +262,27 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
||||
return false;
|
||||
}
|
||||
|
||||
private PreparedDeposit prepare(DepositPlan plan, int first, int limit, int cx, int cz, HeightMap he, ChunkContext context) {
|
||||
private PreparedDeposit prepare(DepositPlan plan, int first, int limit, int cx, int cz, HeightMap he,
|
||||
ChunkContext context, int highestSurface) {
|
||||
IrisDepositGenerator k = plan.generator();
|
||||
boolean oreDeposit = plan.ore();
|
||||
boolean needsCaveBiome = oreDeposit || k.usesCaveBiomeFilter();
|
||||
boolean scalable = oreDeposit && packScan().oreScaling();
|
||||
boolean needsCaveBiome = scalable || k.usesCaveBiomeFilter();
|
||||
IrisDimensionCarvingResolver.State carvingState = needsCaveBiome ? new IrisDimensionCarvingResolver.State() : null;
|
||||
boolean terrainScoped = k.getPlacementScope() == IrisDepositPlacementScope.TERRAIN;
|
||||
int highest = highestSurface;
|
||||
int engineHeight = getEngine().getHeight();
|
||||
List<PreparedClump> clumps = new ArrayList<>(limit - first);
|
||||
int terrainLimit = Integer.MIN_VALUE;
|
||||
for (int l = first; l < limit; l++) {
|
||||
long clumpSeed = clumpSeed(plan.seed(), l);
|
||||
RNG clumpRng = new RNG(clumpSeed);
|
||||
DepositRandom clumpRng = new DepositRandom(clumpSeed(plan.seed(), l));
|
||||
if (k.getPerClumpSpawnChance() < clumpRng.d()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
IrisDepositGenerator.ClumpFootprint footprint = k.sampleClumpFootprint(clumpRng, getData());
|
||||
if (footprint != null && footprint.empty() && !scalable) {
|
||||
continue;
|
||||
}
|
||||
IrisObject clump = footprint == null ? k.getClump(getEngine(), clumpRng, getData()) : null;
|
||||
|
||||
int dim = footprint == null ? clump.getW() : footprint.width();
|
||||
@@ -242,9 +297,9 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
||||
int x = clumpRng.i(min, max + 1);
|
||||
int z = clumpRng.i(min, max + 1);
|
||||
int terrainSurface = getDepositTerrainSurface(cx, cz, x, z, he, context);
|
||||
int height = k.getPlacementScope() == IrisDepositPlacementScope.TERRAIN
|
||||
int height = terrainScoped
|
||||
? depositSurfaceLimit(terrainSurface, k.getSurfaceClearance())
|
||||
: getEngine().getHeight() - 1;
|
||||
: engineHeight - 1;
|
||||
|
||||
if (height < 0) {
|
||||
continue;
|
||||
@@ -252,7 +307,7 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
||||
|
||||
int y = sampleHeight(
|
||||
k.getHeightDistribution(), clumpRng, k.getMinHeight(), k.getMaxHeight(),
|
||||
Math.min(height, getEngine().getHeight() - 1));
|
||||
Math.min(height, engineHeight - 1));
|
||||
if (y == Integer.MIN_VALUE) {
|
||||
continue;
|
||||
}
|
||||
@@ -262,7 +317,16 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
||||
continue;
|
||||
}
|
||||
|
||||
int biomeY = Math.max(0, Math.min(getEngine().getHeight() - 1, y));
|
||||
boolean footprintLimited = footprint != null && terrainScoped && !footprint.empty();
|
||||
if (footprintLimited && highest == Integer.MIN_VALUE) {
|
||||
highest = highestTerrainSurface(cx, cz, he, context);
|
||||
}
|
||||
int terrainLimit = highest - Math.max(0, k.getSurfaceClearance());
|
||||
if (footprintLimited && !scalable && y + footprint.minY() > terrainLimit) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int biomeY = Math.max(0, Math.min(engineHeight - 1, y));
|
||||
IrisBiome surfaceBiome = context.getBiome().get(x, z);
|
||||
IrisBiome depositBiome = needsCaveBiome
|
||||
? getEngine().getCaveBiome(
|
||||
@@ -272,8 +336,7 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
||||
continue;
|
||||
}
|
||||
|
||||
if (oreDeposit) {
|
||||
if (depositBiome != null) {
|
||||
if (oreDeposit && depositBiome != null) {
|
||||
double frequencyMultiplier = depositBiome.getOreDepositFrequencyMultiplier();
|
||||
if (frequencyMultiplier < 1D
|
||||
&& !passesOreFrequency(frequencyMultiplier, clumpRng.d())) {
|
||||
@@ -286,29 +349,21 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
||||
int scaledDimension = scaledClump.getW();
|
||||
x = clampDepositCenter(x, scaledDimension, 16);
|
||||
if (clippedHeight) {
|
||||
y = clampDepositCenter(y, scaledDimension, getEngine().getHeight());
|
||||
y = clampDepositCenter(y, scaledDimension, engineHeight);
|
||||
}
|
||||
z = clampDepositCenter(z, scaledDimension, 16);
|
||||
clump = scaledClump;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (clump == null) {
|
||||
if (k.getPlacementScope() == IrisDepositPlacementScope.TERRAIN && !footprint.empty()) {
|
||||
if (terrainLimit == Integer.MIN_VALUE) {
|
||||
terrainLimit = chunkTerrainLimit(cx, cz, he, context, k.getSurfaceClearance());
|
||||
}
|
||||
if (y + footprint.minY() > terrainLimit) {
|
||||
if (clump != null) {
|
||||
clumps.add(new PreparedClump(clump, null, x, y, z, clumpRng));
|
||||
continue;
|
||||
}
|
||||
if (footprint.empty() || footprintLimited && y + footprint.minY() > terrainLimit) {
|
||||
continue;
|
||||
}
|
||||
RNG replay = new RNG(clumpSeed);
|
||||
replay.d();
|
||||
clump = k.getClump(getEngine(), replay, getData());
|
||||
}
|
||||
|
||||
clumps.add(new PreparedClump(clump, x, y, z, clumpRng));
|
||||
clumps.add(new PreparedClump(null, footprint.cells(), x, y, z, clumpRng));
|
||||
}
|
||||
return new PreparedDeposit(k, oreDeposit, clumps);
|
||||
}
|
||||
@@ -318,72 +373,24 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
||||
if (deposit.clumps().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
IrisDepositGenerator k = deposit.generator();
|
||||
boolean oreDeposit = deposit.ore();
|
||||
IrisDimensionCarvingResolver.State carvingState = new IrisDimensionCarvingResolver.State();
|
||||
IrisDimension dimension = getDimension();
|
||||
boolean variants = depositVariantsPossible();
|
||||
ClumpPlacement placement = new ClumpPlacement(deposit.generator(), deposit.ore(), chunk, data, cx, cz, he,
|
||||
context, depositVariantsPossible());
|
||||
for (PreparedClump prepared : deposit.clumps()) {
|
||||
IrisObject clump = prepared.object();
|
||||
int x = prepared.x();
|
||||
int y = prepared.y();
|
||||
int z = prepared.z();
|
||||
RNG rng = prepared.rng();
|
||||
|
||||
VectorMap<NativeBlockState>.Cursor cursor = clump.getBlocks().cursor();
|
||||
IrisDepositGenerator.ClumpCells cells = prepared.cells();
|
||||
if (cells != null) {
|
||||
for (int cell = 0; cell < cells.size(); cell++) {
|
||||
placement.place(cells.x(cell) + x, cells.y(cell) + y, cells.z(cell) + z, cells.block(cell), rng);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
VectorMap<NativeBlockState>.Cursor cursor = prepared.object().getBlocks().cursor();
|
||||
while (cursor.next()) {
|
||||
IrisBlockVector j = cursor.key();
|
||||
int nx = j.getBlockX() + x;
|
||||
int ny = j.getBlockY() + y;
|
||||
int nz = j.getBlockZ() + z;
|
||||
|
||||
if (nx > 15 || nx < 0 || ny >= getEngine().getHeight() || ny < 0 || nz < 0 || nz > 15) {
|
||||
continue;
|
||||
}
|
||||
int columnSurface = getDepositTerrainSurface(cx, cz, nx, nz, he, context);
|
||||
if (!placementSurfaceAllows(
|
||||
k.getPlacementScope(), ny, columnSurface, k.getSurfaceClearance())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DimensionStackLayout stackLayout = context.getDimensionStackLayout(nx, nz);
|
||||
if (stackLayout != null && stackLayout.isHostFeatureProtectedY(ny)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NativeBlockState current = data.get(nx, ny, nz);
|
||||
if (!canReplaceDepositTarget(current)) {
|
||||
continue;
|
||||
}
|
||||
if (!k.isReplaceBedrock() && IrisProceduralBlocks.materialKey(current).equals("minecraft:bedrock")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
IrisBiome candidateSurfaceBiome = null;
|
||||
boolean exteriorSurface = false;
|
||||
if (oreDeposit) {
|
||||
candidateSurfaceBiome = context.getBiome().get(nx, nz);
|
||||
exteriorSurface = k.hasSurfaceReplaceableBlocks(candidateSurfaceBiome)
|
||||
&& isTerrainSurface(data, nx, ny, nz, columnSurface);
|
||||
}
|
||||
if (!canReplaceDepositHost(k, current, candidateSurfaceBiome, exteriorSurface)) {
|
||||
continue;
|
||||
}
|
||||
boolean adjacentToAir = k.getDiscardChanceOnAirExposure() > 0D
|
||||
&& isAdjacentToAir(data, nx, ny, nz);
|
||||
if (shouldDiscardExposed(k.getDiscardChanceOnAirExposure(), rng.d(), adjacentToAir)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (chunk.get(nx, ny, nz, MatterCavern.class) == null) {
|
||||
NativeBlockState ore = cursor.value();
|
||||
NativeBlockState remapped = variants ? resolveDepositVariant(
|
||||
cx, cz, nx, ny, nz, ore, dimension, context, carvingState) : null;
|
||||
NativeBlockState finalBlock = remapped != null
|
||||
? remapped
|
||||
: B.toDeepSlateOre(current, ore);
|
||||
data.set(nx, ny, nz, finalBlock);
|
||||
}
|
||||
placement.place(j.getBlockX() + x, j.getBlockY() + y, j.getBlockZ() + z, cursor.value(), rng);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -397,17 +404,17 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
||||
}
|
||||
|
||||
/**
|
||||
* The highest block a TERRAIN-scoped deposit may place anywhere in the chunk; a clump whose lowest block sits
|
||||
* above it would have every block rejected by {@link #placementSurfaceAllows}.
|
||||
* The highest terrain surface in the chunk. A TERRAIN-scoped clump whose lowest block sits above it, less the
|
||||
* generator's clearance, would have every block rejected by {@link #placementSurfaceAllows}.
|
||||
*/
|
||||
private int chunkTerrainLimit(int cx, int cz, HeightMap he, ChunkContext context, int surfaceClearance) {
|
||||
private int highestTerrainSurface(int cx, int cz, HeightMap he, ChunkContext context) {
|
||||
int highest = Integer.MIN_VALUE;
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
highest = Math.max(highest, getDepositTerrainSurface(cx, cz, x, z, he, context));
|
||||
}
|
||||
}
|
||||
return highest - Math.max(0, surfaceClearance);
|
||||
return highest;
|
||||
}
|
||||
|
||||
static int depositSurfaceLimit(int surfaceY) {
|
||||
@@ -532,39 +539,48 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
||||
* enumerate (stacked dimensions, unreadable loaders) keeps the lookup.
|
||||
*/
|
||||
private boolean depositVariantsPossible() {
|
||||
return packScan().variants();
|
||||
}
|
||||
|
||||
private PackScan packScan() {
|
||||
IrisData data = getData();
|
||||
DepositVariantScan scan = variantScan;
|
||||
PackScan scan = packScan;
|
||||
if (scan == null || scan.data() != data) {
|
||||
scan = new DepositVariantScan(data, scanDepositVariants(data));
|
||||
variantScan = scan;
|
||||
scan = scanPack(data);
|
||||
packScan = scan;
|
||||
}
|
||||
return scan.possible();
|
||||
return scan;
|
||||
}
|
||||
|
||||
private boolean scanDepositVariants(IrisData data) {
|
||||
private PackScan scanPack(IrisData data) {
|
||||
IrisDimension dimension = getDimension();
|
||||
if (data == null || dimension == null || getEngine().getDimensionStackContext() != null
|
||||
|| dimension.getDepositVariants() == null || !dimension.getDepositVariants().isEmpty()) {
|
||||
return true;
|
||||
if (data == null || dimension == null || getEngine().getDimensionStackContext() != null) {
|
||||
return new PackScan(data, true, true);
|
||||
}
|
||||
Boolean regions = declaresVariants(data.getRegionLoader(), IrisRegion::getDepositVariants);
|
||||
Boolean biomes = declaresVariants(data.getBiomeLoader(), IrisBiome::getDepositVariants);
|
||||
return regions == null || regions || biomes == null || biomes;
|
||||
Boolean regionVariants = anyLoaded(data.getRegionLoader(), region -> declaresVariants(region.getDepositVariants()));
|
||||
Boolean biomeVariants = anyLoaded(data.getBiomeLoader(), biome -> declaresVariants(biome.getDepositVariants()));
|
||||
Boolean oreScaling = anyLoaded(data.getBiomeLoader(), IrisDepositModifier::scalesOreDeposits);
|
||||
boolean variants = declaresVariants(dimension.getDepositVariants())
|
||||
|| regionVariants == null || regionVariants || biomeVariants == null || biomeVariants;
|
||||
return new PackScan(data, variants, oreScaling == null || oreScaling);
|
||||
}
|
||||
|
||||
private static <T extends IrisRegistrant> Boolean declaresVariants(ResourceLoader<T> loader,
|
||||
Function<T, ? extends List<IrisDepositVariant>> variants) {
|
||||
private static boolean declaresVariants(List<IrisDepositVariant> declared) {
|
||||
return declared == null || !declared.isEmpty();
|
||||
}
|
||||
|
||||
private static boolean scalesOreDeposits(IrisBiome biome) {
|
||||
return biome.getOreDepositFrequencyMultiplier() < 1D || biome.getOreDepositSizeMultiplier() != 1D;
|
||||
}
|
||||
|
||||
private static <T extends IrisRegistrant> Boolean anyLoaded(ResourceLoader<T> loader, Predicate<T> test) {
|
||||
String[] keys = loader == null ? null : loader.getPossibleKeys();
|
||||
if (keys == null) {
|
||||
return null;
|
||||
}
|
||||
for (String key : keys) {
|
||||
T loaded = loader.load(key);
|
||||
if (loaded == null) {
|
||||
continue;
|
||||
}
|
||||
List<IrisDepositVariant> declared = variants.apply(loaded);
|
||||
if (declared == null || !declared.isEmpty()) {
|
||||
if (loaded != null && test.test(loaded)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -659,12 +675,206 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
||||
private record DepositPlan(IrisDepositGenerator generator, long seed, boolean ore, int attempts) {
|
||||
}
|
||||
|
||||
private record DepositVariantScan(IrisData data, boolean possible) {
|
||||
private static final class HostStates {
|
||||
private static final int SLOTS = 32;
|
||||
private static final byte BEDROCK_KNOWN = 1;
|
||||
private static final byte BEDROCK = 2;
|
||||
private static final byte REPLACEABLE_KNOWN = 4;
|
||||
private static final byte REPLACEABLE = 8;
|
||||
private static final byte TARGET_KNOWN = 16;
|
||||
private static final byte TARGET = 32;
|
||||
|
||||
private final IrisDepositGenerator generator;
|
||||
private final NativeBlockState[] states = new NativeBlockState[SLOTS];
|
||||
private final byte[] flags = new byte[SLOTS];
|
||||
|
||||
private HostStates(IrisDepositGenerator generator) {
|
||||
this.generator = generator;
|
||||
}
|
||||
|
||||
private boolean target(NativeBlockState state) {
|
||||
int slot = slot(state);
|
||||
byte known = flags[slot];
|
||||
if ((known & TARGET_KNOWN) == 0) {
|
||||
boolean target = canReplaceDepositTarget(state);
|
||||
known = (byte) (known | TARGET_KNOWN | (target ? TARGET : 0));
|
||||
flags[slot] = known;
|
||||
}
|
||||
return (known & TARGET) != 0;
|
||||
}
|
||||
|
||||
private boolean bedrock(NativeBlockState state) {
|
||||
int slot = slot(state);
|
||||
byte known = flags[slot];
|
||||
if ((known & BEDROCK_KNOWN) == 0) {
|
||||
boolean bedrock = IrisProceduralBlocks.materialKey(state).equals("minecraft:bedrock");
|
||||
known = (byte) (known | BEDROCK_KNOWN | (bedrock ? BEDROCK : 0));
|
||||
flags[slot] = known;
|
||||
}
|
||||
return (known & BEDROCK) != 0;
|
||||
}
|
||||
|
||||
private boolean replaceable(NativeBlockState state) {
|
||||
int slot = slot(state);
|
||||
byte known = flags[slot];
|
||||
if ((known & REPLACEABLE_KNOWN) == 0) {
|
||||
boolean replaceable = generator.canReplace(state);
|
||||
known = (byte) (known | REPLACEABLE_KNOWN | (replaceable ? REPLACEABLE : 0));
|
||||
flags[slot] = known;
|
||||
}
|
||||
return (known & REPLACEABLE) != 0;
|
||||
}
|
||||
|
||||
private int slot(NativeBlockState state) {
|
||||
int slot = System.identityHashCode(state) & (SLOTS - 1);
|
||||
if (states[slot] != state) {
|
||||
states[slot] = state;
|
||||
flags[slot] = 0;
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AboveTerrainHosts {
|
||||
private final Hunk<NativeBlockState> terrain;
|
||||
private final ChunkContext context;
|
||||
private int scannedClearance = -1;
|
||||
private boolean present;
|
||||
|
||||
private AboveTerrainHosts(Hunk<NativeBlockState> terrain, ChunkContext context) {
|
||||
this.terrain = terrain;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
private boolean present(int surfaceClearance) {
|
||||
int clearance = Math.max(0, surfaceClearance);
|
||||
if (clearance != scannedClearance) {
|
||||
present = scan(clearance);
|
||||
scannedClearance = clearance;
|
||||
}
|
||||
return present;
|
||||
}
|
||||
|
||||
private boolean scan(int clearance) {
|
||||
int height = terrain.getHeight();
|
||||
for (int z = 0; z < terrain.getDepth(); z++) {
|
||||
for (int x = 0; x < terrain.getWidth(); x++) {
|
||||
long floor = Math.max(-1L, (long) context.getRoundedHeight(x, z) + clearance);
|
||||
for (int y = Math.min(height - 1, ColumnExtent.highestStoredY(terrain, x, z)); y > floor; y--) {
|
||||
if (canReplaceDepositTarget(terrain.get(x, y, z))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private record PackScan(IrisData data, boolean variants, boolean oreScaling) {
|
||||
}
|
||||
|
||||
private record PreparedDeposit(IrisDepositGenerator generator, boolean ore, List<PreparedClump> clumps) {
|
||||
}
|
||||
|
||||
private record PreparedClump(IrisObject object, int x, int y, int z, RNG rng) {
|
||||
private record PreparedClump(IrisObject object, IrisDepositGenerator.ClumpCells cells, int x, int y, int z, RNG rng) {
|
||||
}
|
||||
|
||||
private final class ClumpPlacement {
|
||||
private final IrisDepositGenerator generator;
|
||||
private final boolean ore;
|
||||
private final MantleChunk chunk;
|
||||
private final Hunk<NativeBlockState> data;
|
||||
private final int chunkX;
|
||||
private final int chunkZ;
|
||||
private final HeightMap heightMap;
|
||||
private final ChunkContext context;
|
||||
private final boolean variants;
|
||||
private final IrisDimension dimension;
|
||||
private IrisDimensionCarvingResolver.State carvingState;
|
||||
private final IrisDepositPlacementScope scope;
|
||||
private final int surfaceClearance;
|
||||
private final double discardChance;
|
||||
private final int height;
|
||||
private final boolean replaceBedrock;
|
||||
private final HostStates hosts;
|
||||
|
||||
private ClumpPlacement(IrisDepositGenerator generator, boolean ore, MantleChunk chunk,
|
||||
Hunk<NativeBlockState> data, int chunkX, int chunkZ, HeightMap heightMap,
|
||||
ChunkContext context, boolean variants) {
|
||||
this.generator = generator;
|
||||
this.ore = ore;
|
||||
this.chunk = chunk;
|
||||
this.data = data;
|
||||
this.chunkX = chunkX;
|
||||
this.chunkZ = chunkZ;
|
||||
this.heightMap = heightMap;
|
||||
this.context = context;
|
||||
this.variants = variants;
|
||||
this.dimension = getDimension();
|
||||
this.scope = generator.getPlacementScope();
|
||||
this.surfaceClearance = generator.getSurfaceClearance();
|
||||
this.discardChance = generator.getDiscardChanceOnAirExposure();
|
||||
this.height = getEngine().getHeight();
|
||||
this.replaceBedrock = generator.isReplaceBedrock();
|
||||
this.hosts = new HostStates(generator);
|
||||
}
|
||||
|
||||
private IrisDimensionCarvingResolver.State carvingState() {
|
||||
IrisDimensionCarvingResolver.State state = carvingState;
|
||||
if (state == null) {
|
||||
state = new IrisDimensionCarvingResolver.State();
|
||||
carvingState = state;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
private void place(int nx, int ny, int nz, NativeBlockState block, RNG rng) {
|
||||
if (nx > 15 || nx < 0 || ny >= height || ny < 0 || nz < 0 || nz > 15) {
|
||||
return;
|
||||
}
|
||||
int columnSurface = getDepositTerrainSurface(chunkX, chunkZ, nx, nz, heightMap, context);
|
||||
if (!placementSurfaceAllows(scope, ny, columnSurface, surfaceClearance)) {
|
||||
return;
|
||||
}
|
||||
|
||||
DimensionStackLayout stackLayout = context.getDimensionStackLayout(nx, nz);
|
||||
if (stackLayout != null && stackLayout.isHostFeatureProtectedY(ny)) {
|
||||
return;
|
||||
}
|
||||
|
||||
NativeBlockState current = data.get(nx, ny, nz);
|
||||
if (current == null || !hosts.target(current)) {
|
||||
return;
|
||||
}
|
||||
if (!replaceBedrock && hosts.bedrock(current)) {
|
||||
return;
|
||||
}
|
||||
|
||||
IrisBiome candidateSurfaceBiome = null;
|
||||
boolean exteriorSurface = false;
|
||||
if (ore) {
|
||||
candidateSurfaceBiome = context.getBiome().get(nx, nz);
|
||||
exteriorSurface = generator.hasSurfaceReplaceableBlocks(candidateSurfaceBiome)
|
||||
&& isTerrainSurface(data, nx, ny, nz, columnSurface);
|
||||
}
|
||||
if (exteriorSurface
|
||||
? !canReplaceDepositHost(generator, current, candidateSurfaceBiome, true)
|
||||
: !hosts.replaceable(current)) {
|
||||
return;
|
||||
}
|
||||
if (discardChance > 0D) {
|
||||
boolean adjacentToAir = isAdjacentToAir(data, nx, ny, nz);
|
||||
if (shouldDiscardExposed(discardChance, rng.d(), adjacentToAir)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.get(nx, ny, nz, MatterCavern.class) == null) {
|
||||
NativeBlockState remapped = variants ? resolveDepositVariant(
|
||||
chunkX, chunkZ, nx, ny, nz, block, dimension, context, carvingState()) : null;
|
||||
data.set(nx, ny, nz, remapped != null ? remapped : B.toDeepSlateOre(current, block));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,9 @@ import java.util.Map;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
|
||||
public class IrisPerfectionModifier extends EngineAssignedModifier<NativeBlockState> {
|
||||
private static final int SOLID = 1;
|
||||
private static final int SPIKE = 2;
|
||||
private static final int SLICE_TASKS = 16;
|
||||
private static final BoundBlockState AIR = BoundBlockState.of("AIR");
|
||||
private static final BoundBlockState WATER = BoundBlockState.of("WATER");
|
||||
private static final Map<String, BoundBlockState> ORE_BASES = buildOreBases();
|
||||
@@ -85,31 +88,54 @@ public class IrisPerfectionModifier extends EngineAssignedModifier<NativeBlockSt
|
||||
if (getDimension().isHideOresForHiddenOre()) {
|
||||
hideOres(output, multicore);
|
||||
}
|
||||
int tasks = multicore ? SLICE_TASKS : 1;
|
||||
int slicesPerTask = 16 / tasks;
|
||||
BurstExecutor burst = burst().burst(multicore);
|
||||
for (int index = 0; index < 16; index++) {
|
||||
int columnX = index;
|
||||
burst.queue(() -> perfectSlice(output, columnX));
|
||||
for (int task = 0; task < tasks; task++) {
|
||||
int firstSlice = task * slicesPerTask;
|
||||
burst.queue(() -> perfectSlices(output, firstSlice, firstSlice + slicesPerTask));
|
||||
}
|
||||
burst.complete();
|
||||
|
||||
getEngine().getMetrics().getPerfection().put(p.getMilliseconds());
|
||||
}
|
||||
|
||||
private void perfectSlice(Hunk<NativeBlockState> output, int x) {
|
||||
private void perfectSlices(Hunk<NativeBlockState> output, int firstSlice, int lastSlice) {
|
||||
IntArrayList surfaces = new IntArrayList();
|
||||
StateFlags states = new StateFlags();
|
||||
NativeBlockState air = AIR.get();
|
||||
NativeBlockState water = WATER.get();
|
||||
for (int x = firstSlice; x < lastSlice; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
boolean changed;
|
||||
perfectColumn(output, x, z, surfaces, states, air, water);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void perfectColumn(Hunk<NativeBlockState> output, int x, int z, IntArrayList surfaces,
|
||||
StateFlags states, NativeBlockState air, NativeBlockState water) {
|
||||
do {
|
||||
surfaces.clear();
|
||||
int top = getHeight(output, x, z);
|
||||
int top = getHeight(output, x, z, states);
|
||||
boolean inside = true;
|
||||
surfaces.add(top);
|
||||
NativeBlockState classified = null;
|
||||
int classifiedFlags = 0;
|
||||
for (int y = top; y >= 0; y--) {
|
||||
NativeBlockState block = output.get(x, y, z);
|
||||
if (IrisSpeleothems.isSpike(block)) {
|
||||
block = normalizeSpike(block, output, x, z, y, AIR.get(), WATER.get());
|
||||
int flags = 0;
|
||||
if (block != null) {
|
||||
if (block != classified) {
|
||||
classified = block;
|
||||
classifiedFlags = states.of(block);
|
||||
}
|
||||
boolean now = block != null && !(B.isAir(block) || B.isFluid(block));
|
||||
flags = classifiedFlags;
|
||||
if ((flags & SPIKE) != 0) {
|
||||
block = normalizeSpike(block, output, x, z, y, air, water);
|
||||
flags = block == null ? 0 : states.of(block);
|
||||
}
|
||||
}
|
||||
boolean now = (flags & SOLID) != 0;
|
||||
if (now != inside) {
|
||||
inside = now;
|
||||
if (inside) {
|
||||
@@ -117,7 +143,12 @@ public class IrisPerfectionModifier extends EngineAssignedModifier<NativeBlockSt
|
||||
}
|
||||
}
|
||||
}
|
||||
changed = false;
|
||||
} while (removeUnsupportedDecorants(output, x, z, surfaces, air));
|
||||
}
|
||||
|
||||
private static boolean removeUnsupportedDecorants(Hunk<NativeBlockState> output, int x, int z,
|
||||
IntArrayList surfaces, NativeBlockState air) {
|
||||
boolean changed = false;
|
||||
for (int index = 0; index < surfaces.size(); index++) {
|
||||
int y = surfaces.getInt(index);
|
||||
NativeBlockState tip = output.get(x, y, z);
|
||||
@@ -136,14 +167,13 @@ public class IrisPerfectionModifier extends EngineAssignedModifier<NativeBlockSt
|
||||
}
|
||||
if (remove) {
|
||||
changed = true;
|
||||
output.set(x, y, z, AIR.get());
|
||||
output.set(x, y, z, air);
|
||||
if (removeBelow) {
|
||||
output.set(x, y - 1, z, AIR.get());
|
||||
output.set(x, y - 1, z, air);
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (changed);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
static NativeBlockState normalizeSpike(NativeBlockState state, Hunk<NativeBlockState> output,
|
||||
@@ -203,17 +233,41 @@ public class IrisPerfectionModifier extends EngineAssignedModifier<NativeBlockSt
|
||||
burst.complete();
|
||||
}
|
||||
|
||||
private int getHeight(Hunk<NativeBlockState> output, int x, int z) {
|
||||
private static int getHeight(Hunk<NativeBlockState> output, int x, int z, StateFlags states) {
|
||||
NativeBlockState classified = null;
|
||||
int classifiedFlags = 0;
|
||||
for (int i = Math.min(output.getHeight() - 1, ColumnExtent.highestStoredY(output, x, z)); i >= 0; i--) {
|
||||
NativeBlockState b = output.get(x, i, z);
|
||||
|
||||
if (b != null) {
|
||||
if (!B.isAir(b) && !B.isFluid(b)) {
|
||||
return i;
|
||||
NativeBlockState state = output.get(x, i, z);
|
||||
if (state == null) {
|
||||
continue;
|
||||
}
|
||||
if (state != classified) {
|
||||
classified = state;
|
||||
classifiedFlags = states.of(state);
|
||||
}
|
||||
if ((classifiedFlags & SOLID) != 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static final class StateFlags {
|
||||
private static final int SLOTS = 64;
|
||||
|
||||
private final NativeBlockState[] states = new NativeBlockState[SLOTS];
|
||||
private final byte[] flags = new byte[SLOTS];
|
||||
|
||||
private int of(NativeBlockState state) {
|
||||
int slot = System.identityHashCode(state) & (SLOTS - 1);
|
||||
if (states[slot] == state) {
|
||||
return flags[slot];
|
||||
}
|
||||
int computed = (state.isAir() || state.isFluid() ? 0 : SOLID) | (IrisSpeleothems.isSpike(state) ? SPIKE : 0);
|
||||
states[slot] = state;
|
||||
flags[slot] = (byte) computed;
|
||||
return computed;
|
||||
}
|
||||
}
|
||||
}
|
||||
+118
-33
@@ -36,6 +36,7 @@ import art.arcane.iris.generation.hydrology.IrisRiverMaterialConfig;
|
||||
import art.arcane.iris.generation.hydrology.IrisSurfaceRiverBankConfig;
|
||||
import art.arcane.iris.generation.terrain.Terrain3DColumn;
|
||||
import art.arcane.iris.generation.block.BoundBlockState;
|
||||
import art.arcane.iris.generation.chunk.ColumnRunHunk;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.generation.context.ChunkedDataCache;
|
||||
import art.arcane.iris.generation.context.ChunkContext;
|
||||
@@ -152,6 +153,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
private final int chunkHeight;
|
||||
private final IrisDimension dimension;
|
||||
private final IrisData data;
|
||||
private final Engine oreEngine;
|
||||
private final IrisComplex complex;
|
||||
private final ProceduralStream<Double> riverWaterSurfaceStream;
|
||||
private final boolean bedrockEnabled;
|
||||
@@ -160,6 +162,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
private final ChunkedDataCache<IrisBiome> biomeCache;
|
||||
private final ChunkedDataCache<IrisRegion> regionCache;
|
||||
private final ChunkedDataCache<NativeBlockState> rockCache;
|
||||
private final ChunkedDataCache<NativeBlockState> fluidCache;
|
||||
private final UpperDimensionContext upperContext;
|
||||
private final IrisOreBands dimensionSurfaceOres;
|
||||
private final IrisOreBands dimensionUndergroundOres;
|
||||
@@ -175,6 +178,15 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
private IrisOreBands regionSurfaceOres;
|
||||
private IrisOreBands biomeUndergroundOres;
|
||||
private IrisOreBands regionUndergroundOres;
|
||||
private int surfaceOreTop;
|
||||
private int surfaceOreFloor;
|
||||
private int undergroundOreTop;
|
||||
private int undergroundOreFloor;
|
||||
private int columnX;
|
||||
private int columnZ;
|
||||
private NativeBlockState runState;
|
||||
private int runTop = -1;
|
||||
private int runBottom;
|
||||
|
||||
private ColumnPainter(int x, int z, Hunk<NativeBlockState> h, ChunkContext context) {
|
||||
this.x = x;
|
||||
@@ -184,6 +196,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
chunkHeight = h.getHeight();
|
||||
dimension = getDimension();
|
||||
data = getData();
|
||||
oreEngine = data == null ? null : data.getEngine();
|
||||
complex = getComplex();
|
||||
riverWaterSurfaceStream = complex.getRiverWaterSurfaceStream();
|
||||
bedrockEnabled = dimension.isBedrock();
|
||||
@@ -192,6 +205,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
biomeCache = context.getBiome();
|
||||
regionCache = context.getRegion();
|
||||
rockCache = context.getRock();
|
||||
fluidCache = context.getFluid();
|
||||
upperContext = getEngine().getUpperContext();
|
||||
dimensionSurfaceOres = hideOres ? IrisOreBands.EMPTY : dimension.getSurfaceOreBands();
|
||||
dimensionUndergroundOres = hideOres ? IrisOreBands.EMPTY : dimension.getUndergroundOreBands();
|
||||
@@ -247,8 +261,10 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
IrisRiverMaterialConfig roleMaterial = hydrologyRoleMaterial(
|
||||
hydrologyTerrain, bedMaterial, shoreMaterial, bankMaterial);
|
||||
int paintDepth = roleMaterial == null ? 0 : roleMaterial.getDepth();
|
||||
NativeBlockState fluid = hydrologyFluid == null || transitionDisplaced
|
||||
NativeBlockState fluid = transitionDisplaced
|
||||
? complex.resolveSurfaceFluid(realX, realZ)
|
||||
: hydrologyFluid == null
|
||||
? fluidCache.get(xf, zf)
|
||||
: complex.resolveHydrologyFluid(hydrologyFluid.profileKey(), realX, realZ);
|
||||
NativeBlockState rock = rockCache.get(xf, zf);
|
||||
NativeBlockState mappedSurfaceBlock = complex.getImageMapRuntime().sampleSurfaceBlock(realX, realZ);
|
||||
@@ -256,8 +272,12 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
regionSurfaceOres = hideOres ? IrisOreBands.EMPTY : region.getSurfaceOreBands();
|
||||
biomeUndergroundOres = hideOres ? IrisOreBands.EMPTY : biome.getUndergroundOreBands();
|
||||
regionUndergroundOres = hideOres ? IrisOreBands.EMPTY : region.getUndergroundOreBands();
|
||||
boolean hasSurfaceOres = biomeSurfaceOres.hasOres() || regionSurfaceOres.hasOres() || dimensionSurfaceOres.hasOres();
|
||||
boolean hasUndergroundOres = biomeUndergroundOres.hasOres() || regionUndergroundOres.hasOres() || dimensionUndergroundOres.hasOres();
|
||||
surfaceOreTop = oreTop(biomeSurfaceOres, regionSurfaceOres, dimensionSurfaceOres);
|
||||
surfaceOreFloor = oreFloor(biomeSurfaceOres, regionSurfaceOres, dimensionSurfaceOres);
|
||||
undergroundOreTop = oreTop(biomeUndergroundOres, regionUndergroundOres, dimensionUndergroundOres);
|
||||
undergroundOreFloor = oreFloor(biomeUndergroundOres, regionUndergroundOres, dimensionUndergroundOres);
|
||||
columnX = xf;
|
||||
columnZ = zf;
|
||||
int bedrockFloor = bedrockEnabled ? 1 : 0;
|
||||
KList<NativeBlockState> blocks = null;
|
||||
KList<NativeBlockState> ceilingBlocks = null;
|
||||
@@ -265,7 +285,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
|
||||
for (int i = topY; i >= 0; i--) {
|
||||
if (i == 0 && bedrockEnabled) {
|
||||
h.setRaw(xf, i, zf, bedrock);
|
||||
put(i, bedrock);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -276,16 +296,16 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
ceilingBlocks = null;
|
||||
}
|
||||
if (terrainSpan < 0 || i > terrainColumn.floor(terrainSpan)) {
|
||||
h.setRaw(xf, i, zf, air);
|
||||
put(i, air);
|
||||
continue;
|
||||
}
|
||||
layerSurfaceY = terrainColumn.floor(terrainSpan);
|
||||
layerCeilingY = terrainColumn.ceiling(terrainSpan);
|
||||
}
|
||||
|
||||
NativeBlockState ore = hasSurfaceOres ? surfaceOre(realX, i, realZ) : null;
|
||||
NativeBlockState ore = i <= surfaceOreTop && i >= surfaceOreFloor ? surfaceOre(realX, i, realZ) : null;
|
||||
if (ore != null) {
|
||||
h.setRaw(xf, i, zf, ore);
|
||||
put(i, ore);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -294,7 +314,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
if (hydrologyFluid == null && fblocks == null) {
|
||||
fblocks = biome.generateSeaLayers(realX, realZ, rng, hf - he, data);
|
||||
}
|
||||
h.setRaw(xf, i, zf, HydrologyFluidLayerSelector.select(
|
||||
put(i, HydrologyFluidLayerSelector.select(
|
||||
fblocks,
|
||||
fdepth,
|
||||
fluid,
|
||||
@@ -306,7 +326,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
if (i <= he) {
|
||||
int depth = layerSurfaceY - i;
|
||||
if (depth == 0 && mappedSurfaceBlock != null) {
|
||||
h.setRaw(xf, i, zf, mappedSurfaceBlock);
|
||||
put(i, mappedSurfaceBlock);
|
||||
continue;
|
||||
}
|
||||
if (blocks == null) {
|
||||
@@ -320,7 +340,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
|
||||
int deepFloor = Math.max(bedrockFloor, layerCeilingY > 0 ? layerCeilingY + 2 : 0);
|
||||
if (i >= deepFloor && depth >= Math.max(blocks.size(), paintDepth)) {
|
||||
paintDeep(xf, zf, realX, realZ, i, deepFloor, rock, hasSurfaceOres, hasUndergroundOres);
|
||||
paintDeep(realX, realZ, i, deepFloor, rock);
|
||||
i = deepFloor;
|
||||
continue;
|
||||
}
|
||||
@@ -331,7 +351,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
2, layerCeilingY, data, complex);
|
||||
}
|
||||
int ceilingDepth = i - layerCeilingY;
|
||||
h.setRaw(xf, i, zf, ceilingBlocks.hasIndex(ceilingDepth)
|
||||
put(i, ceilingBlocks.hasIndex(ceilingDepth)
|
||||
? ceilingBlocks.get(ceilingDepth) : rock);
|
||||
continue;
|
||||
}
|
||||
@@ -344,17 +364,19 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
if (riverOwned && depth <= riverBed.getPadding() && IrisProceduralBlocks.isGravityAffected(layerBlock)) {
|
||||
layerBlock = riverBed.getPaddingPalette().get(rng, realX, i, realZ, data);
|
||||
}
|
||||
h.setRaw(xf, i, zf, layerBlock);
|
||||
put(i, layerBlock);
|
||||
continue;
|
||||
}
|
||||
|
||||
ore = hasUndergroundOres ? undergroundOre(realX, i, realZ) : null;
|
||||
h.setRaw(xf, i, zf, ore != null ? ore : rock);
|
||||
ore = i <= undergroundOreTop && i >= undergroundOreFloor ? undergroundOre(realX, i, realZ) : null;
|
||||
put(i, ore != null ? ore : rock);
|
||||
}
|
||||
}
|
||||
flush();
|
||||
|
||||
if (upperContext != null) {
|
||||
paintUpper(xf, zf, realX, realZ);
|
||||
paintUpper(realX, realZ);
|
||||
flush();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,27 +384,90 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
* Paints the solid run from {@code top} down to {@code floor}, which lies below every biome
|
||||
* layer, river material, mapped surface and cave ceiling of its span: only ores or rock land there.
|
||||
*/
|
||||
private void paintDeep(int xf, int zf, int realX, int realZ, int top, int floor, NativeBlockState rock,
|
||||
boolean hasSurfaceOres, boolean hasUndergroundOres) {
|
||||
for (int y = top; y >= floor; y--) {
|
||||
NativeBlockState ore = hasSurfaceOres ? surfaceOre(realX, y, realZ) : null;
|
||||
if (ore == null && hasUndergroundOres) {
|
||||
private void paintDeep(int realX, int realZ, int top, int floor, NativeBlockState rock) {
|
||||
int bandTop = Math.min(top, Math.max(surfaceOreTop, undergroundOreTop));
|
||||
int bandFloor = Math.max(floor, Math.min(surfaceOreFloor, undergroundOreFloor));
|
||||
if (bandTop < bandFloor) {
|
||||
putRun(top, floor, rock);
|
||||
return;
|
||||
}
|
||||
if (top > bandTop) {
|
||||
putRun(top, bandTop + 1, rock);
|
||||
}
|
||||
for (int y = bandTop; y >= bandFloor; y--) {
|
||||
NativeBlockState ore = y <= surfaceOreTop && y >= surfaceOreFloor ? surfaceOre(realX, y, realZ) : null;
|
||||
if (ore == null && y <= undergroundOreTop && y >= undergroundOreFloor) {
|
||||
ore = undergroundOre(realX, y, realZ);
|
||||
}
|
||||
h.setRaw(xf, y, zf, ore != null ? ore : rock);
|
||||
put(y, ore != null ? ore : rock);
|
||||
}
|
||||
if (bandFloor > floor) {
|
||||
putRun(bandFloor - 1, floor, rock);
|
||||
}
|
||||
}
|
||||
|
||||
private void put(int y, NativeBlockState state) {
|
||||
putRun(y, y, state);
|
||||
}
|
||||
|
||||
private void putRun(int top, int bottom, NativeBlockState state) {
|
||||
if (runTop >= 0 && state == runState && top == runBottom - 1) {
|
||||
runBottom = bottom;
|
||||
return;
|
||||
}
|
||||
flush();
|
||||
runState = state;
|
||||
runTop = top;
|
||||
runBottom = bottom;
|
||||
}
|
||||
|
||||
private void flush() {
|
||||
if (runTop < 0) {
|
||||
return;
|
||||
}
|
||||
ColumnRunHunk.fillColumnRun(h, columnX, columnZ, runBottom, runTop, runState);
|
||||
runTop = -1;
|
||||
runState = null;
|
||||
}
|
||||
|
||||
private static int oreTop(IrisOreBands first, IrisOreBands second, IrisOreBands third) {
|
||||
int top = Integer.MIN_VALUE;
|
||||
if (first.hasOres()) {
|
||||
top = Math.max(top, first.highestY());
|
||||
}
|
||||
if (second.hasOres()) {
|
||||
top = Math.max(top, second.highestY());
|
||||
}
|
||||
if (third.hasOres()) {
|
||||
top = Math.max(top, third.highestY());
|
||||
}
|
||||
return top;
|
||||
}
|
||||
|
||||
private static int oreFloor(IrisOreBands first, IrisOreBands second, IrisOreBands third) {
|
||||
int floor = Integer.MAX_VALUE;
|
||||
if (first.hasOres()) {
|
||||
floor = Math.min(floor, first.lowestY());
|
||||
}
|
||||
if (second.hasOres()) {
|
||||
floor = Math.min(floor, second.lowestY());
|
||||
}
|
||||
if (third.hasOres()) {
|
||||
floor = Math.min(floor, third.lowestY());
|
||||
}
|
||||
return floor;
|
||||
}
|
||||
|
||||
private NativeBlockState surfaceOre(int realX, int y, int realZ) {
|
||||
NativeBlockState ore = null;
|
||||
if (biomeSurfaceOres.contains(y)) {
|
||||
ore = biomeSurfaceOres.generate(realX, y, realZ, rng, data);
|
||||
ore = biomeSurfaceOres.generate(realX, y, realZ, rng, data, oreEngine);
|
||||
}
|
||||
if (ore == null && regionSurfaceOres.contains(y)) {
|
||||
ore = regionSurfaceOres.generate(realX, y, realZ, rng, data);
|
||||
ore = regionSurfaceOres.generate(realX, y, realZ, rng, data, oreEngine);
|
||||
}
|
||||
if (ore == null && dimensionSurfaceOres.contains(y)) {
|
||||
ore = dimensionSurfaceOres.generate(realX, y, realZ, rng, data);
|
||||
ore = dimensionSurfaceOres.generate(realX, y, realZ, rng, data, oreEngine);
|
||||
}
|
||||
return ore;
|
||||
}
|
||||
@@ -390,18 +475,18 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
private NativeBlockState undergroundOre(int realX, int y, int realZ) {
|
||||
NativeBlockState ore = null;
|
||||
if (biomeUndergroundOres.contains(y)) {
|
||||
ore = biomeUndergroundOres.generate(realX, y, realZ, rng, data);
|
||||
ore = biomeUndergroundOres.generate(realX, y, realZ, rng, data, oreEngine);
|
||||
}
|
||||
if (ore == null && regionUndergroundOres.contains(y)) {
|
||||
ore = regionUndergroundOres.generate(realX, y, realZ, rng, data);
|
||||
ore = regionUndergroundOres.generate(realX, y, realZ, rng, data, oreEngine);
|
||||
}
|
||||
if (ore == null && dimensionUndergroundOres.contains(y)) {
|
||||
ore = dimensionUndergroundOres.generate(realX, y, realZ, rng, data);
|
||||
ore = dimensionUndergroundOres.generate(realX, y, realZ, rng, data, oreEngine);
|
||||
}
|
||||
return ore;
|
||||
}
|
||||
|
||||
private void paintUpper(int xf, int zf, int realX, int realZ) {
|
||||
private void paintUpper(int realX, int realZ) {
|
||||
UpperDimensionContext.Column upperColumn = upperContext.sampleColumn(realX, realZ);
|
||||
int upperSurfaceY = upperColumn.surfaceY();
|
||||
if (upperSurfaceY >= chunkHeight - 1) {
|
||||
@@ -416,11 +501,11 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
|
||||
for (int y = chunkHeight - 1; y >= upperSurfaceY; y--) {
|
||||
if (y == chunkHeight - 1 && bedrockEnabled) {
|
||||
h.setRaw(xf, y, zf, bedrock);
|
||||
put(y, bedrock);
|
||||
continue;
|
||||
}
|
||||
if (!upperColumn.isSolid(y)) {
|
||||
h.setRaw(xf, y, zf, air);
|
||||
put(y, air);
|
||||
continue;
|
||||
}
|
||||
int faceY = upperColumn.faceY(y);
|
||||
@@ -430,7 +515,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
upperBlocks = null;
|
||||
}
|
||||
if (y == faceY && upperMappedSurface != null) {
|
||||
h.setRaw(xf, y, zf, upperMappedSurface);
|
||||
put(y, upperMappedSurface);
|
||||
continue;
|
||||
}
|
||||
int depthFromFace = y - faceY;
|
||||
@@ -440,9 +525,9 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
upperContext.getData(), upperContext.getSurfaceSlopeStream(sourceSurfaceY));
|
||||
}
|
||||
if (upperBlocks != null && upperBlocks.hasIndex(depthFromFace)) {
|
||||
h.setRaw(xf, y, zf, upperBlocks.get(depthFromFace));
|
||||
put(y, upperBlocks.get(depthFromFace));
|
||||
} else {
|
||||
h.setRaw(xf, y, zf, upperRock);
|
||||
put(y, upperRock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-3
@@ -120,13 +120,40 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
|
||||
|
||||
@Override
|
||||
public BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) {
|
||||
NativeBlockState state = blocks.getRaw(localX, worldY - minimumY, localZ);
|
||||
if (state == null) {
|
||||
return voxelOf(stateAt(localX, worldY - minimumY, localZ));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scanColumn(int localX, int localZ, int minimumY, int height, SavedTerrainChunk.VoxelRuns runs) {
|
||||
if (height <= 0) {
|
||||
return;
|
||||
}
|
||||
int base = minimumY - this.minimumY;
|
||||
NativeBlockState runState = stateAt(localX, base, localZ);
|
||||
int start = 0;
|
||||
for (int offset = 1; offset < height; offset++) {
|
||||
NativeBlockState state = stateAt(localX, base + offset, localZ);
|
||||
if (state != runState) {
|
||||
runs.add(start, offset, voxelOf(runState));
|
||||
start = offset;
|
||||
runState = state;
|
||||
}
|
||||
}
|
||||
runs.add(start, height, voxelOf(runState));
|
||||
}
|
||||
|
||||
private NativeBlockState stateAt(int localX, int offset, int localZ) {
|
||||
NativeBlockState state = blocks.getRaw(localX, offset, localZ);
|
||||
if (state != null) {
|
||||
return state;
|
||||
}
|
||||
if (air == null) {
|
||||
air = IrisPlatforms.get().registries().air();
|
||||
}
|
||||
state = air;
|
||||
return air;
|
||||
}
|
||||
|
||||
private BoundaryColumnGeometry.Voxel voxelOf(NativeBlockState state) {
|
||||
if (state != lastState) {
|
||||
lastVoxel = voxels.computeIfAbsent(state, GeometrySource::encode);
|
||||
lastState = state;
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package art.arcane.iris.generation.stream;
|
||||
|
||||
import art.arcane.iris.generation.bake.BakePlane;
|
||||
import art.arcane.iris.generation.bake.ChunkBakeCache;
|
||||
import art.arcane.iris.generation.bake.ChunkPlanes;
|
||||
import art.arcane.volmlib.util.stream.BasicStream;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class BakedDoubleStream extends BasicStream<Double> implements ChunkFillableStream2D, ChunkFillableDoubleStream2D {
|
||||
private final ChunkBakeCache cache;
|
||||
private final BakePlane plane;
|
||||
private final ProceduralStream<Double> scalar;
|
||||
|
||||
public BakedDoubleStream(ChunkBakeCache cache, BakePlane plane, ProceduralStream<Double> scalar) {
|
||||
super();
|
||||
this.cache = Objects.requireNonNull(cache, "Bake cache");
|
||||
this.plane = Objects.requireNonNull(plane, "Bake plane");
|
||||
this.scalar = Objects.requireNonNull(scalar, "Scalar source");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double get(double x, double z) {
|
||||
return getDouble(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getDouble(double x, double z) {
|
||||
int blockX = (int) x;
|
||||
int blockZ = (int) z;
|
||||
int index = ((blockZ & 15) << 4) | (blockX & 15);
|
||||
ChunkPlanes.Plane values = cache.planeIfPresent(plane, blockX >> 4, blockZ >> 4);
|
||||
if (values != null && values.ready(index)) {
|
||||
return values.doubleAt(index);
|
||||
}
|
||||
if (!cache.memoAllowed()) {
|
||||
return scalar.getDouble(blockX, blockZ);
|
||||
}
|
||||
long revision = ProvisionalSampling.revision();
|
||||
double value = scalar.getDouble(blockX, blockZ);
|
||||
cache.storeDouble(plane, blockX, blockZ, value, revision);
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double get(double x, double y, double z) {
|
||||
return scalar.get(x, y, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getDouble(double x, double y, double z) {
|
||||
return scalar.getDouble(x, y, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double toDouble(Double t) {
|
||||
return t;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double fromDouble(double d) {
|
||||
return d;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fillChunkDoubles(int worldX, int worldZ, double[] target) {
|
||||
if (target == null || target.length != 256) {
|
||||
throw new IllegalArgumentException("Expected a 16x16 target array.");
|
||||
}
|
||||
int chunkX = worldX >> 4;
|
||||
int chunkZ = worldZ >> 4;
|
||||
if (cache.memoAllowed()) {
|
||||
System.arraycopy(cache.complete(plane, chunkX, chunkZ), 0, target, 0, 256);
|
||||
return;
|
||||
}
|
||||
int originX = chunkX << 4;
|
||||
int originZ = chunkZ << 4;
|
||||
for (int index = 0; index < 256; index++) {
|
||||
target[index] = getDouble(originX + (index & 15), originZ + (index >> 4));
|
||||
}
|
||||
}
|
||||
|
||||
public void fillColumns(int chunkX, int chunkZ, long[] columns, double[] target) {
|
||||
if (cache.memoAllowed()) {
|
||||
cache.fillColumns(plane, chunkX, chunkZ, columns, target);
|
||||
return;
|
||||
}
|
||||
int originX = chunkX << 4;
|
||||
int originZ = chunkZ << 4;
|
||||
for (int word = 0; word < 4; word++) {
|
||||
for (long bits = columns[word]; bits != 0L; bits &= bits - 1L) {
|
||||
int index = (word << 6) | Long.numberOfTrailingZeros(bits);
|
||||
target[index] = getDouble(originX + (index & 15), originZ + (index >> 4));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
|
||||
double[] values = new double[256];
|
||||
fillChunkDoubles(worldX, worldZ, values);
|
||||
for (int index = 0; index < 256; index++) {
|
||||
target[index] = values[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package art.arcane.iris.generation.stream;
|
||||
|
||||
import art.arcane.iris.generation.bake.BakePlane;
|
||||
import art.arcane.iris.generation.bake.ChunkBakeCache;
|
||||
import art.arcane.iris.generation.bake.ChunkPlanes;
|
||||
import art.arcane.volmlib.util.function.NoiseProvider;
|
||||
import art.arcane.volmlib.util.stream.BasicStream;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
public final class BakedHeightStream extends BasicStream<Double> implements ChunkFillableStream2D, ChunkFillableDoubleStream2D {
|
||||
private final ChunkBakeCache cache;
|
||||
private final BooleanSupplier mainThread;
|
||||
private final NoiseProvider nonblocking;
|
||||
private final NoiseProvider resolved;
|
||||
|
||||
public BakedHeightStream(ChunkBakeCache cache, BooleanSupplier mainThread, NoiseProvider nonblocking, NoiseProvider resolved) {
|
||||
super();
|
||||
this.cache = Objects.requireNonNull(cache, "Bake cache");
|
||||
this.mainThread = Objects.requireNonNull(mainThread, "Main thread check");
|
||||
this.nonblocking = Objects.requireNonNull(nonblocking, "Nonblocking height");
|
||||
this.resolved = Objects.requireNonNull(resolved, "Resolved height");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double get(double x, double z) {
|
||||
return getDouble(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getDouble(double x, double z) {
|
||||
if (mainThread.getAsBoolean()) {
|
||||
return nonblocking.noise(x, z);
|
||||
}
|
||||
return column((int) x, (int) z, !ChunkBakeCache.inSparseScope());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double get(double x, double y, double z) {
|
||||
return getDouble(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getDouble(double x, double y, double z) {
|
||||
return getDouble(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double toDouble(Double t) {
|
||||
return t;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double fromDouble(double d) {
|
||||
return d;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fillChunkDoubles(int worldX, int worldZ, double[] target) {
|
||||
if (target == null || target.length != 256) {
|
||||
throw new IllegalArgumentException("Expected a 16x16 target array.");
|
||||
}
|
||||
boolean main = mainThread.getAsBoolean();
|
||||
boolean memo = !main && !ChunkBakeCache.inSparseScope();
|
||||
if (memo && (worldX & 15) == 0 && (worldZ & 15) == 0) {
|
||||
System.arraycopy(cache.complete(BakePlane.HEIGHT, worldX >> 4, worldZ >> 4), 0, target, 0, 256);
|
||||
return;
|
||||
}
|
||||
for (int index = 0; index < 256; index++) {
|
||||
int x = worldX + (index & 15);
|
||||
int z = worldZ + (index >> 4);
|
||||
target[index] = main ? nonblocking.noise(x, z) : column(x, z, memo);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
|
||||
double[] values = new double[256];
|
||||
fillChunkDoubles(worldX, worldZ, values);
|
||||
for (int index = 0; index < 256; index++) {
|
||||
target[index] = values[index];
|
||||
}
|
||||
}
|
||||
|
||||
private double column(int x, int z, boolean memo) {
|
||||
int index = ((z & 15) << 4) | (x & 15);
|
||||
ChunkPlanes.Plane values = cache.planeIfPresent(BakePlane.HEIGHT, x >> 4, z >> 4);
|
||||
if (values != null && values.ready(index)) {
|
||||
return values.doubleAt(index);
|
||||
}
|
||||
if (!memo) {
|
||||
return resolved.noise(x, z);
|
||||
}
|
||||
long revision = ProvisionalSampling.revision();
|
||||
double value = resolved.noise(x, z);
|
||||
cache.storeDouble(BakePlane.HEIGHT, x, z, value, revision);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package art.arcane.iris.generation.stream;
|
||||
|
||||
import art.arcane.iris.generation.bake.BakePlane;
|
||||
import art.arcane.iris.generation.bake.ChunkBakeCache;
|
||||
import art.arcane.iris.generation.bake.ChunkPlanes;
|
||||
import art.arcane.volmlib.util.stream.BasicStream;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class BakedStream<T> extends BasicStream<T> implements ChunkFillableStream2D {
|
||||
private final ChunkBakeCache cache;
|
||||
private final BakePlane plane;
|
||||
private final ProceduralStream<T> scalar;
|
||||
|
||||
public BakedStream(ChunkBakeCache cache, BakePlane plane, ProceduralStream<T> scalar) {
|
||||
super();
|
||||
this.cache = Objects.requireNonNull(cache, "Bake cache");
|
||||
this.plane = Objects.requireNonNull(plane, "Bake plane");
|
||||
this.scalar = Objects.requireNonNull(scalar, "Scalar source");
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public T get(double x, double z) {
|
||||
int blockX = (int) x;
|
||||
int blockZ = (int) z;
|
||||
int index = ((blockZ & 15) << 4) | (blockX & 15);
|
||||
ChunkPlanes.Plane values = cache.planeIfPresent(plane, blockX >> 4, blockZ >> 4);
|
||||
if (values != null && values.ready(index)) {
|
||||
return (T) values.objectAt(index);
|
||||
}
|
||||
if (!cache.memoAllowed()) {
|
||||
return scalar.get(blockX, blockZ);
|
||||
}
|
||||
long revision = ProvisionalSampling.revision();
|
||||
T value = scalar.get(blockX, blockZ);
|
||||
cache.storeObject(plane, blockX, blockZ, value, revision);
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T get(double x, double y, double z) {
|
||||
return scalar.get(x, y, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double toDouble(T t) {
|
||||
return scalar.toDouble(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T fromDouble(double d) {
|
||||
return scalar.fromDouble(d);
|
||||
}
|
||||
|
||||
public void fillColumns(int chunkX, int chunkZ, long[] columns, Object[] target) {
|
||||
if (cache.memoAllowed()) {
|
||||
cache.fillColumns(plane, chunkX, chunkZ, columns, target);
|
||||
return;
|
||||
}
|
||||
int originX = chunkX << 4;
|
||||
int originZ = chunkZ << 4;
|
||||
for (int word = 0; word < 4; word++) {
|
||||
for (long bits = columns[word]; bits != 0L; bits &= bits - 1L) {
|
||||
int index = (word << 6) | Long.numberOfTrailingZeros(bits);
|
||||
target[index] = get(originX + (index & 15), originZ + (index >> 4));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
|
||||
int chunkX = worldX >> 4;
|
||||
int chunkZ = worldZ >> 4;
|
||||
if (cache.memoAllowed()) {
|
||||
System.arraycopy(cache.complete(plane, chunkX, chunkZ), 0, target, 0, 256);
|
||||
return;
|
||||
}
|
||||
int originX = chunkX << 4;
|
||||
int originZ = chunkZ << 4;
|
||||
for (int index = 0; index < 256; index++) {
|
||||
target[index] = get(originX + (index & 15), originZ + (index >> 4));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package art.arcane.iris.generation.stream;
|
||||
|
||||
import art.arcane.volmlib.util.math.Rarity;
|
||||
import art.arcane.volmlib.util.stream.BasicStream;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class RarityStream<T extends Rarity> extends BasicStream<T> {
|
||||
private final ProceduralStream<?> noise;
|
||||
private final Object[] options;
|
||||
private final double[] cumulative;
|
||||
|
||||
public RarityStream(ProceduralStream<?> noise, List<T> options) {
|
||||
super();
|
||||
this.noise = Objects.requireNonNull(noise, "Rarity noise");
|
||||
this.options = options.toArray();
|
||||
this.cumulative = new double[this.options.length];
|
||||
double buffer = 0;
|
||||
for (int index = 0; index < this.options.length; index++) {
|
||||
buffer += 1d / Rarity.get(this.options[index]);
|
||||
cumulative[index] = buffer;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public T get(double x, double z) {
|
||||
return pick(noise.getDouble(x, z));
|
||||
}
|
||||
|
||||
@Override
|
||||
public T get(double x, double y, double z) {
|
||||
return pick(noise.getDouble(x, y, z));
|
||||
}
|
||||
|
||||
@Override
|
||||
public double toDouble(T t) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T fromDouble(double d) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public T pick(double noiseValue) {
|
||||
int count = options.length;
|
||||
if (count == 0) {
|
||||
return null;
|
||||
}
|
||||
if (count == 1) {
|
||||
return (T) options[0];
|
||||
}
|
||||
double threshold = cumulative[count - 1] * noiseValue;
|
||||
for (int index = 0; index < count; index++) {
|
||||
if (cumulative[index] >= threshold) {
|
||||
return (T) options[index];
|
||||
}
|
||||
}
|
||||
return (T) options[count - 1];
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,27 @@
|
||||
package art.arcane.iris.generation.subterrain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public final class SubterrainPlanner {
|
||||
private static final int PLAN_CACHE_SLOTS = 4096;
|
||||
private static final int TILE_CACHE_SLOTS = 1024;
|
||||
private static final Comparator<SubterrainPlan> PLAN_ORDER =
|
||||
Comparator.comparingInt(SubterrainPlan::priority).reversed().thenComparing(SubterrainPlan::id);
|
||||
|
||||
private final List<Definition> definitions;
|
||||
private final long seed;
|
||||
private final int minY;
|
||||
private final int maxYExclusive;
|
||||
private final ThreadLocal<CandidateTile> sampleTile = new ThreadLocal<>();
|
||||
private final AtomicReferenceArray<CachedPlan> planCache = new AtomicReferenceArray<>(PLAN_CACHE_SLOTS);
|
||||
private final AtomicReferenceArray<CandidateTile> tileCache = new AtomicReferenceArray<>(TILE_CACHE_SLOTS);
|
||||
private final Predicate<SubterrainBounds> allowedBounds;
|
||||
private final RegionResolver regionResolver;
|
||||
|
||||
@@ -70,8 +78,9 @@ public final class SubterrainPlanner {
|
||||
if (minX > maxX || minZ > maxZ) {
|
||||
throw new IllegalArgumentException("Subterrain query bounds are inverted");
|
||||
}
|
||||
List<SubterrainPlan> result = new ArrayList<>();
|
||||
for (Definition definition : definitions) {
|
||||
List<SubterrainPlan> result = null;
|
||||
for (int definitionIndex = 0; definitionIndex < definitions.size(); definitionIndex++) {
|
||||
Definition definition = definitions.get(definitionIndex);
|
||||
long firstX = Math.floorDiv(minX, definition.spacing()) - 1L;
|
||||
long lastX = Math.floorDiv(maxX, definition.spacing()) + 1L;
|
||||
long firstZ = Math.floorDiv(minZ, definition.spacing()) - 1L;
|
||||
@@ -81,35 +90,36 @@ public final class SubterrainPlanner {
|
||||
}
|
||||
for (long gridX = firstX; gridX <= lastX; gridX++) {
|
||||
for (long gridZ = firstZ; gridZ <= lastZ; gridZ++) {
|
||||
SubterrainPlan plan = create(definition, gridX, gridZ);
|
||||
SubterrainPlan plan = plan(definitionIndex, definition, gridX, gridZ);
|
||||
if (plan != null && plan.bounds().intersects(minX, minZ, maxX, maxZ)) {
|
||||
if (result == null) {
|
||||
result = new ArrayList<>();
|
||||
}
|
||||
result.add(plan);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result.sort(Comparator.comparingInt(SubterrainPlan::priority).reversed().thenComparing(SubterrainPlan::id));
|
||||
if (result == null) {
|
||||
return List.of();
|
||||
}
|
||||
result.sort(PLAN_ORDER);
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
public boolean hasCandidates(int chunkX, int chunkZ) {
|
||||
return !isEmpty() && !candidates(chunkX, chunkZ).isEmpty();
|
||||
}
|
||||
|
||||
public SubterrainCell sample(int x, int y, int z) {
|
||||
if (isEmpty() || y < minY || y >= maxYExclusive) {
|
||||
return SubterrainCell.OUTSIDE;
|
||||
}
|
||||
int chunkX = Math.floorDiv(x, 16);
|
||||
int chunkZ = Math.floorDiv(z, 16);
|
||||
CandidateTile tile = sampleTile.get();
|
||||
if (tile == null || tile.chunkX() != chunkX || tile.chunkZ() != chunkZ) {
|
||||
long baseX = (long) chunkX * 16;
|
||||
long baseZ = (long) chunkZ * 16;
|
||||
tile = new CandidateTile(chunkX, chunkZ, plansForBounds(
|
||||
(int) Math.max(Integer.MIN_VALUE, baseX - 1),
|
||||
(int) Math.max(Integer.MIN_VALUE, baseZ - 1),
|
||||
(int) Math.min(Integer.MAX_VALUE, baseX + 16),
|
||||
(int) Math.min(Integer.MAX_VALUE, baseZ + 16)));
|
||||
sampleTile.set(tile);
|
||||
return sample(candidates(Math.floorDiv(x, 16), Math.floorDiv(z, 16)), x, y, z);
|
||||
}
|
||||
return sample(tile.plans(), x, y, z);
|
||||
|
||||
public ChunkSampler chunkSampler(int chunkX, int chunkZ) {
|
||||
return new ChunkSampler(this, chunkX, chunkZ, isEmpty() ? List.of() : candidates(chunkX, chunkZ));
|
||||
}
|
||||
|
||||
public SubterrainCell sample(List<SubterrainPlan> candidates, int x, int y, int z) {
|
||||
@@ -127,10 +137,38 @@ public final class SubterrainPlanner {
|
||||
return selected;
|
||||
}
|
||||
|
||||
private List<SubterrainPlan> candidates(int chunkX, int chunkZ) {
|
||||
int slot = (int) mix(((long) chunkX << 32) ^ (chunkZ & 0xffffffffL)) & (TILE_CACHE_SLOTS - 1);
|
||||
CandidateTile tile = tileCache.getAcquire(slot);
|
||||
if (tile != null && tile.chunkX() == chunkX && tile.chunkZ() == chunkZ) {
|
||||
return tile.plans();
|
||||
}
|
||||
long baseX = (long) chunkX * 16;
|
||||
long baseZ = (long) chunkZ * 16;
|
||||
List<SubterrainPlan> plans = plansForBounds(
|
||||
(int) Math.max(Integer.MIN_VALUE, baseX - 1),
|
||||
(int) Math.max(Integer.MIN_VALUE, baseZ - 1),
|
||||
(int) Math.min(Integer.MAX_VALUE, baseX + 16),
|
||||
(int) Math.min(Integer.MAX_VALUE, baseZ + 16));
|
||||
tileCache.setRelease(slot, new CandidateTile(chunkX, chunkZ, plans));
|
||||
return plans;
|
||||
}
|
||||
|
||||
private SubterrainPlan plan(int definitionIndex, Definition definition, long gridX, long gridZ) {
|
||||
int slot = (int) mix(mix(gridX ^ definition.seedSalt()) ^ Long.rotateLeft(gridZ, 29)) & (PLAN_CACHE_SLOTS - 1);
|
||||
CachedPlan cached = planCache.getAcquire(slot);
|
||||
if (cached != null && cached.definition() == definitionIndex && cached.gridX() == gridX && cached.gridZ() == gridZ) {
|
||||
return cached.plan();
|
||||
}
|
||||
SubterrainPlan plan = create(definition, gridX, gridZ);
|
||||
planCache.setRelease(slot, new CachedPlan(definitionIndex, gridX, gridZ, plan));
|
||||
return plan;
|
||||
}
|
||||
|
||||
private SubterrainCell raw(List<SubterrainPlan> candidates, int x, int y, int z) {
|
||||
SubterrainCell selected = SubterrainCell.OUTSIDE;
|
||||
for (SubterrainPlan plan : candidates) {
|
||||
SubterrainCell cell = plan.sample(x, y, z);
|
||||
for (int index = 0; index < candidates.size(); index++) {
|
||||
SubterrainCell cell = candidates.get(index).sample(x, y, z);
|
||||
if (cell.solid() && cell.room().reservedSolid()) {
|
||||
return cell;
|
||||
}
|
||||
@@ -209,9 +247,85 @@ public final class SubterrainPlanner {
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ChunkSampler {
|
||||
private final SubterrainPlanner planner;
|
||||
private final int chunkX;
|
||||
private final int chunkZ;
|
||||
private final List<SubterrainPlan> plans;
|
||||
private final int[] columnMinY;
|
||||
private final int[] columnMaxY;
|
||||
|
||||
private ChunkSampler(SubterrainPlanner planner, int chunkX, int chunkZ, List<SubterrainPlan> plans) {
|
||||
this.planner = planner;
|
||||
this.chunkX = chunkX;
|
||||
this.chunkZ = chunkZ;
|
||||
this.plans = plans;
|
||||
if (plans.isEmpty()) {
|
||||
columnMinY = null;
|
||||
columnMaxY = null;
|
||||
return;
|
||||
}
|
||||
columnMinY = new int[256];
|
||||
columnMaxY = new int[256];
|
||||
Arrays.fill(columnMinY, Integer.MAX_VALUE);
|
||||
Arrays.fill(columnMaxY, Integer.MIN_VALUE);
|
||||
int baseX = chunkX << 4;
|
||||
int baseZ = chunkZ << 4;
|
||||
for (int index = 0; index < plans.size(); index++) {
|
||||
SubterrainBounds bounds = plans.get(index).bounds();
|
||||
int lowX = Math.max(bounds.minX(), baseX) - baseX;
|
||||
int highX = Math.min(bounds.maxX(), baseX + 15) - baseX;
|
||||
int lowZ = Math.max(bounds.minZ(), baseZ) - baseZ;
|
||||
int highZ = Math.min(bounds.maxZ(), baseZ + 15) - baseZ;
|
||||
for (int localX = lowX; localX <= highX; localX++) {
|
||||
for (int localZ = lowZ; localZ <= highZ; localZ++) {
|
||||
int column = (localX << 4) | localZ;
|
||||
columnMinY[column] = Math.min(columnMinY[column], bounds.minY());
|
||||
columnMaxY[column] = Math.max(columnMaxY[column], bounds.maxY());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return plans.isEmpty();
|
||||
}
|
||||
|
||||
public SubterrainCell sample(int x, int y, int z) {
|
||||
if ((x >> 4) != chunkX || (z >> 4) != chunkZ) {
|
||||
return planner.sample(x, y, z);
|
||||
}
|
||||
if (plans.isEmpty() || y < planner.minY || y >= planner.maxYExclusive) {
|
||||
return SubterrainCell.OUTSIDE;
|
||||
}
|
||||
return sampleColumn(x, y, z);
|
||||
}
|
||||
|
||||
public SubterrainCell sampleCandidates(int x, int y, int z) {
|
||||
if (plans.isEmpty()) {
|
||||
return SubterrainCell.OUTSIDE;
|
||||
}
|
||||
if ((x >> 4) != chunkX || (z >> 4) != chunkZ) {
|
||||
return planner.sample(plans, x, y, z);
|
||||
}
|
||||
return sampleColumn(x, y, z);
|
||||
}
|
||||
|
||||
private SubterrainCell sampleColumn(int x, int y, int z) {
|
||||
int column = ((x & 15) << 4) | (z & 15);
|
||||
if (y < columnMinY[column] || y > columnMaxY[column]) {
|
||||
return SubterrainCell.OUTSIDE;
|
||||
}
|
||||
return planner.sample(plans, x, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
private record CandidateTile(int chunkX, int chunkZ, List<SubterrainPlan> plans) {
|
||||
}
|
||||
|
||||
private record CachedPlan(int definition, long gridX, long gridZ, SubterrainPlan plan) {
|
||||
}
|
||||
|
||||
record Definition(String id, IrisSubterrainFamily family, IrisSubterrainFluid fluid, String biome, int spacing, double probability,
|
||||
int length, int radius, int height, double shapeWarp, int fluidDepth, int chimneyHeight, int terraceCount,
|
||||
int pillarSpacing, double formationFraction, String solid, int priority,
|
||||
|
||||
+21
-10
@@ -120,7 +120,7 @@ public final class IrisDimensionCarvingResolver {
|
||||
}
|
||||
|
||||
int remainingDepth = clampDepth(rootEntry.getChildRecursionDepth());
|
||||
if (remainingDepth <= 0) {
|
||||
if (remainingDepth <= 0 || terminal(engine, data, rootEntry, rootBiome, resolvedState)) {
|
||||
return rootEntry;
|
||||
}
|
||||
|
||||
@@ -131,13 +131,12 @@ public final class IrisDimensionCarvingResolver {
|
||||
return columns.entries[slot];
|
||||
}
|
||||
|
||||
Map<String, IrisDimensionCarvingEntry> entryIndex = resolveEntryIndex(engine, resolvedState);
|
||||
IrisDimensionCarvingEntry current = rootEntry;
|
||||
int depth = remainingDepth;
|
||||
boolean cacheable = true;
|
||||
while (depth > 0) {
|
||||
cacheable &= fixedChildStyle(current);
|
||||
IrisDimensionCarvingEntry selected = selectChild(engine, data, current, worldX, worldZ, entryIndex, resolvedState);
|
||||
IrisDimensionCarvingEntry selected = selectChild(engine, data, current, worldX, worldZ, resolvedState);
|
||||
if (selected == null || selected == current || selected.getWorldYRange() != null
|
||||
&& !selected.getWorldYRange().contains(worldY)) {
|
||||
break;
|
||||
@@ -164,6 +163,24 @@ public final class IrisDimensionCarvingResolver {
|
||||
return current;
|
||||
}
|
||||
|
||||
private static boolean terminal(Engine engine, IrisData data, IrisDimensionCarvingEntry entry, IrisBiome biome, State state) {
|
||||
KList<String> children = entry.getChildren();
|
||||
if (children == null || children.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return selectionPlan(engine, data, entry, biome, state).size() <= 1L;
|
||||
}
|
||||
|
||||
private static IrisRaritySelection<CarvingChoice> selectionPlan(Engine engine, IrisData data, IrisDimensionCarvingEntry parent,
|
||||
IrisBiome parentBiome, State state) {
|
||||
IrisRaritySelection<CarvingChoice> selectionPlan = state.selectionPlans.get(parent);
|
||||
if (selectionPlan == null) {
|
||||
selectionPlan = buildSelectionPlan(data, parent, parentBiome, resolveEntryIndex(engine, state), state);
|
||||
state.selectionPlans.put(parent, selectionPlan);
|
||||
}
|
||||
return selectionPlan;
|
||||
}
|
||||
|
||||
private static boolean fixedChildStyle(IrisDimensionCarvingEntry entry) {
|
||||
for (IrisGeneratorStyle style = entry.getChildStyle(); style != null; style = style.getFracture()) {
|
||||
if (style.getExpression() != null) {
|
||||
@@ -202,7 +219,6 @@ public final class IrisDimensionCarvingResolver {
|
||||
IrisDimensionCarvingEntry parent,
|
||||
int worldX,
|
||||
int worldZ,
|
||||
Map<String, IrisDimensionCarvingEntry> entryIndex,
|
||||
State state
|
||||
) {
|
||||
KList<String> children = parent.getChildren();
|
||||
@@ -215,12 +231,7 @@ public final class IrisDimensionCarvingResolver {
|
||||
return parent;
|
||||
}
|
||||
|
||||
IrisRaritySelection<CarvingChoice> selectionPlan = state.selectionPlans.get(parent);
|
||||
if (selectionPlan == null) {
|
||||
selectionPlan = buildSelectionPlan(data, parent, parentBiome, entryIndex, state);
|
||||
state.selectionPlans.put(parent, selectionPlan);
|
||||
}
|
||||
|
||||
IrisRaritySelection<CarvingChoice> selectionPlan = selectionPlan(engine, data, parent, parentBiome, state);
|
||||
if (selectionPlan.size() <= 1L) {
|
||||
return parent;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package art.arcane.iris.generation.terrain;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.image.IrisImageMapRuntime;
|
||||
import art.arcane.iris.generation.image.IrisImageMapApplication;
|
||||
import art.arcane.iris.generation.stream.RarityStream;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.studio.generation.BiomeBuffetLayout;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
@@ -24,7 +25,7 @@ public final class RegionSelection {
|
||||
regions.add(region);
|
||||
}
|
||||
}
|
||||
ProceduralStream<IrisRegion> selected = options.style().selectRarity(regions);
|
||||
ProceduralStream<IrisRegion> selected = new RarityStream<>(options.style(), regions);
|
||||
if (!options.imageMaps().has(IrisImageMapApplication.REGION)) {
|
||||
return selected;
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Data
|
||||
public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
private static final int CUSTOM_BIOME_KEY_CACHE_LIMIT = 4096;
|
||||
private static final Map<File, IrisData> dataLoaders = new ConcurrentHashMap<>();
|
||||
// Loaders (cached or detached) that currently have registered engines; see hasActiveEngines.
|
||||
// Identity-keyed on purpose: Lombok's @Data hashCode over this class's mutable loader state
|
||||
@@ -159,6 +160,10 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
@Setter(AccessLevel.NONE)
|
||||
@EqualsAndHashCode.Exclude
|
||||
private transient volatile GenerationRegistryContractFactory.CustomBiomeResourceResolver customBiomeResourceResolver;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
@EqualsAndHashCode.Exclude
|
||||
private transient volatile CustomBiomeKeyCache customBiomeKeyCache;
|
||||
private transient volatile Engine soleEngine;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
@@ -234,6 +239,32 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
|
||||
IrisBiomeCustom requiredBiome = Objects.requireNonNull(customBiome, "customBiome");
|
||||
GenerationRegistryContractFactory.CustomBiomeResourceResolver resolver = customBiomeResourceResolver;
|
||||
ContentGate gate = resolver == null ? getContentGate() : null;
|
||||
Object owner = resolver == null ? gate : resolver;
|
||||
CustomBiomeKeyCache cache = customBiomeKeyCache;
|
||||
if (cache == null || cache.owner() != owner) {
|
||||
cache = new CustomBiomeKeyCache(owner, new ConcurrentHashMap<>());
|
||||
customBiomeKeyCache = cache;
|
||||
}
|
||||
CustomBiomeKey key = new CustomBiomeKey(requiredDimension.getLoadKey(), requiredBiome);
|
||||
String cached = cache.keys().get(key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
String resolved = resolveCustomBiomeResourceKey(requiredDimension, requiredBiome, resolver, gate);
|
||||
if (cache.keys().size() >= CUSTOM_BIOME_KEY_CACHE_LIMIT) {
|
||||
cache.keys().clear();
|
||||
}
|
||||
cache.keys().put(key, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private String resolveCustomBiomeResourceKey(
|
||||
IrisDimension requiredDimension,
|
||||
IrisBiomeCustom requiredBiome,
|
||||
GenerationRegistryContractFactory.CustomBiomeResourceResolver resolver,
|
||||
ContentGate gate
|
||||
) {
|
||||
if (resolver != null) {
|
||||
try {
|
||||
return resolver.resolve(
|
||||
@@ -248,7 +279,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
return GenerationRegistryContractFactory.customBiomeResourceKey(
|
||||
requiredDimension.getLoadKey(),
|
||||
requiredBiome,
|
||||
getContentGate()
|
||||
gate
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1035,4 +1066,29 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
return immutable;
|
||||
}
|
||||
}
|
||||
|
||||
private record CustomBiomeKeyCache(Object owner, ConcurrentHashMap<CustomBiomeKey, String> keys) {
|
||||
}
|
||||
|
||||
private static final class CustomBiomeKey {
|
||||
private final String dimensionKey;
|
||||
private final IrisBiomeCustom biome;
|
||||
private final int hash;
|
||||
|
||||
private CustomBiomeKey(String dimensionKey, IrisBiomeCustom biome) {
|
||||
this.dimensionKey = dimensionKey;
|
||||
this.biome = biome;
|
||||
hash = 31 * Objects.hashCode(dimensionKey) + System.identityHashCode(biome);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return other instanceof CustomBiomeKey key && key.biome == biome && Objects.equals(key.dimensionKey, dimensionKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.generation.runtime.DimensionStackContext;
|
||||
import art.arcane.iris.generation.runtime.DimensionStackLayout;
|
||||
import art.arcane.iris.generation.runtime.DimensionTerrainContext;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||
@@ -27,6 +28,7 @@ import org.bukkit.block.Biome;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
@@ -46,7 +48,7 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
private static final int STRONGHOLD_RING_SEARCH_RADIUS = 112;
|
||||
private static final int STRONGHOLD_RING_SEARCH_QUART_STEP = 4;
|
||||
|
||||
private final long seed;
|
||||
private final RNG biomeRng;
|
||||
private final Engine engine;
|
||||
private final BukkitChunkGenerator platformGenerator;
|
||||
private final NativeBiomeRegistry<H, V> biomeCustomRegistry;
|
||||
@@ -64,7 +66,7 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
this.engine = engine;
|
||||
this.platformGenerator = options.platformGenerator();
|
||||
this.possibleBiomeRegistry = options.possibleBiomeRegistry();
|
||||
this.seed = options.seed();
|
||||
this.biomeRng = new RNG(options.seed());
|
||||
this.biomeCustomRegistry = registry;
|
||||
this.biomeRegistry = biomeCustomRegistry;
|
||||
this.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry);
|
||||
@@ -826,7 +828,7 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
int z,
|
||||
IrisDimensionCarvingResolver.State resolverState
|
||||
) {
|
||||
return resolveVisibleBiomeHolder(x, y, z, resolverState, null);
|
||||
return resolveVisibleBiomeHolder(x, y, z, resolverState, null, null);
|
||||
}
|
||||
|
||||
private H resolveVisibleBiomeHolder(
|
||||
@@ -835,32 +837,44 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
int z,
|
||||
IrisDimensionCarvingResolver.State resolverState,
|
||||
DimensionStackLayout stackLayout
|
||||
) {
|
||||
return resolveVisibleBiomeHolder(x, y, z, resolverState, stackLayout, null);
|
||||
}
|
||||
|
||||
private H resolveVisibleBiomeHolder(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
IrisDimensionCarvingResolver.State resolverState,
|
||||
DimensionStackLayout stackLayout,
|
||||
ChunkResolution chunk
|
||||
) {
|
||||
int blockX = x << 2;
|
||||
int blockY = y << 2;
|
||||
int blockZ = z << 2;
|
||||
Optional<String> historicalKey = engine.getComplex().historicalPhysicalBiomeKeyAt(
|
||||
IrisComplex complex = chunk == null ? engine.getComplex() : chunk.complex;
|
||||
Optional<String> historicalKey = complex.historicalPhysicalBiomeKeyAt(
|
||||
blockX, blockY, blockZ);
|
||||
if (historicalKey.isPresent()) {
|
||||
return resolvePhysicalBiomeHolder(historicalKey.get());
|
||||
}
|
||||
BiomeResolution resolution = resolveBiomeResolution(
|
||||
x, y, z, resolverState, true, stackLayout);
|
||||
x, y, z, resolverState, true, stackLayout, chunk);
|
||||
if (resolution == null) {
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
return resolveVisibleBiomeHolder(resolution);
|
||||
return resolveVisibleBiomeHolder(resolution, chunk);
|
||||
}
|
||||
|
||||
private H resolveVisibleBiomeHolder(BiomeResolution resolution) {
|
||||
private H resolveVisibleBiomeHolder(BiomeResolution resolution, ChunkResolution chunk) {
|
||||
if (resolution.irisBiome.isCustom()) {
|
||||
return resolveCustomHolder(resolution);
|
||||
return resolveCustomHolder(resolution, chunk);
|
||||
}
|
||||
|
||||
Biome vanillaBiome = resolution.underground
|
||||
? resolution.irisBiome.getGroundBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ)
|
||||
: resolution.irisBiome.getSkyBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
|
||||
? resolution.irisBiome.getGroundBiome(biomeRng, engine, resolution.blockX, resolution.blockY, resolution.blockZ)
|
||||
: resolution.irisBiome.getSkyBiome(biomeRng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
|
||||
H holder = biomeRegistry.lookup(vanillaBiome);
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
@@ -869,11 +883,12 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
private H resolveCustomHolder(BiomeResolution resolution) {
|
||||
IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
|
||||
private H resolveCustomHolder(BiomeResolution resolution, ChunkResolution chunk) {
|
||||
IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(biomeRng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
|
||||
if (customBiome != null) {
|
||||
H holder = runtimeBiomeState().customBiomes().get(customBiomeKey(
|
||||
resolution.dimension, customBiome.getId()));
|
||||
H holder = chunk == null
|
||||
? runtimeBiomeState().customBiomes().get(customBiomeKey(resolution.dimension, customBiome.getId()))
|
||||
: chunk.customHolder(resolution.dimension, customBiome);
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
}
|
||||
@@ -883,7 +898,7 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
}
|
||||
|
||||
private BiomeResolution resolveStructureBiomeResolution(int x, int y, int z) {
|
||||
return resolveBiomeResolution(x, y, z, null, false);
|
||||
return resolveBiomeResolution(x, y, z, null, false, null, null);
|
||||
}
|
||||
|
||||
private H resolvePhysicalBiomeHolder(String physicalKey) {
|
||||
@@ -898,65 +913,50 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
return holder;
|
||||
}
|
||||
|
||||
private BiomeResolution resolveBiomeResolution(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
IrisDimensionCarvingResolver.State resolverState
|
||||
) {
|
||||
return resolveBiomeResolution(x, y, z, resolverState, true);
|
||||
}
|
||||
|
||||
private BiomeResolution resolveBiomeResolution(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
IrisDimensionCarvingResolver.State resolverState,
|
||||
boolean includeDimensionStack
|
||||
) {
|
||||
return resolveBiomeResolution(
|
||||
x, y, z, resolverState, includeDimensionStack, null);
|
||||
}
|
||||
|
||||
private BiomeResolution resolveBiomeResolution(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
IrisDimensionCarvingResolver.State resolverState,
|
||||
boolean includeDimensionStack,
|
||||
DimensionStackLayout stackLayout
|
||||
DimensionStackLayout stackLayout,
|
||||
ChunkResolution chunk
|
||||
) {
|
||||
if (engine == null || engine.isClosed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (engine.getComplex() == null) {
|
||||
if ((chunk == null ? engine.getComplex() : chunk.complex) == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int blockX = x << 2;
|
||||
int blockZ = z << 2;
|
||||
int blockY = y << 2;
|
||||
int worldMinHeight = engine.getWorld().minHeight();
|
||||
int worldMinHeight = chunk == null ? engine.getWorld().minHeight() : chunk.worldMinHeight;
|
||||
int internalY = blockY - worldMinHeight;
|
||||
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
|
||||
IrisBiome featureBiome = engine.getSubterrainBiome(blockX, internalY, blockZ);
|
||||
IrisBiome featureBiome = chunk != null && !chunk.subterrain
|
||||
? null : engine.getSubterrainBiome(blockX, internalY, blockZ);
|
||||
if (featureBiome != null) {
|
||||
return createBiomeResolution(featureBiome, true, engine.getDimension(), blockX, blockY, blockZ);
|
||||
return new BiomeResolution(featureBiome, true, chunk == null ? engine.getDimension() : chunk.dimension,
|
||||
blockX, blockY, blockZ);
|
||||
}
|
||||
DimensionStackLayout.Layer stackLayer = includeDimensionStack
|
||||
? stackLayout == null
|
||||
? resolveDimensionStackLayer(engine, blockX, internalY, blockZ)
|
||||
? resolveDimensionStackLayer(
|
||||
chunk == null ? engine.getDimensionStackContext() : chunk.stackContext,
|
||||
blockX, internalY, blockZ)
|
||||
: stackLayout.layerAt(internalY)
|
||||
: null;
|
||||
IrisDimension owningDimension = stackLayer == null
|
||||
? engine.getDimension()
|
||||
? chunk == null ? engine.getDimension() : chunk.dimension
|
||||
: stackLayer.terrainContext().getDimension();
|
||||
if (stackLayer != null && !stackLayer.terrainContext().isSelfReferencing()) {
|
||||
IrisBiome stackedBiome = stackLayer.biome();
|
||||
return stackedBiome == null
|
||||
? null
|
||||
: createBiomeResolution(
|
||||
: new BiomeResolution(
|
||||
stackedBiome, false, owningDimension, blockX, blockY, blockZ);
|
||||
}
|
||||
boolean deepUnderground = internalY <= caveSwitchInternalY;
|
||||
@@ -976,41 +976,31 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
)
|
||||
: stackLayer.biome();
|
||||
} else if (deepUnderground) {
|
||||
int surfaceInternalY = Engine.hostHeight(engine, blockX, blockZ, true);
|
||||
int surfaceInternalY = chunk == null
|
||||
? Engine.hostHeight(engine, blockX, blockZ, true)
|
||||
: chunk.hostHeight(blockX, blockZ);
|
||||
underground = internalY <= surfaceInternalY - 8;
|
||||
irisBiome = underground
|
||||
? engine.getCaveBiome(blockX, internalY, blockZ, resolverState)
|
||||
: engine.getHostSurfaceBiome(blockX, blockZ);
|
||||
: hostSurfaceBiome(chunk, blockX, blockZ);
|
||||
} else {
|
||||
irisBiome = engine.getHostSurfaceBiome(blockX, blockZ);
|
||||
irisBiome = hostSurfaceBiome(chunk, blockX, blockZ);
|
||||
}
|
||||
if (irisBiome == null && underground) {
|
||||
irisBiome = stackLayer == null
|
||||
? engine.getHostSurfaceBiome(blockX, blockZ)
|
||||
? hostSurfaceBiome(chunk, blockX, blockZ)
|
||||
: stackLayer.biome();
|
||||
}
|
||||
if (irisBiome == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createBiomeResolution(
|
||||
return new BiomeResolution(
|
||||
irisBiome, underground, owningDimension, blockX, blockY, blockZ);
|
||||
}
|
||||
|
||||
private BiomeResolution createBiomeResolution(
|
||||
IrisBiome irisBiome,
|
||||
boolean underground,
|
||||
IrisDimension dimension,
|
||||
int blockX,
|
||||
int blockY,
|
||||
int blockZ
|
||||
) {
|
||||
RNG noiseRng = new RNG(seed
|
||||
^ (((long) blockX) * 341873128712L)
|
||||
^ (((long) blockY) * 132897987541L)
|
||||
^ (((long) blockZ) * 42317861L));
|
||||
return new BiomeResolution(
|
||||
irisBiome, underground, dimension, blockX, blockY, blockZ, noiseRng);
|
||||
private IrisBiome hostSurfaceBiome(ChunkResolution chunk, int blockX, int blockZ) {
|
||||
return chunk == null ? engine.getHostSurfaceBiome(blockX, blockZ) : chunk.hostSurfaceBiome(blockX, blockZ);
|
||||
}
|
||||
|
||||
private H getFallbackBiome() {
|
||||
@@ -1094,12 +1084,11 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
}
|
||||
|
||||
private static DimensionStackLayout.Layer resolveDimensionStackLayer(
|
||||
Engine engine,
|
||||
DimensionStackContext stackContext,
|
||||
int blockX,
|
||||
int internalY,
|
||||
int blockZ
|
||||
) {
|
||||
DimensionStackContext stackContext = engine.getDimensionStackContext();
|
||||
return stackContext == null ? null : stackContext.getLayerAt(blockX, internalY, blockZ);
|
||||
}
|
||||
|
||||
@@ -1136,7 +1125,6 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
private final int blockX;
|
||||
private final int blockY;
|
||||
private final int blockZ;
|
||||
private final RNG rng;
|
||||
|
||||
private BiomeResolution(
|
||||
IrisBiome irisBiome,
|
||||
@@ -1144,8 +1132,7 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
IrisDimension dimension,
|
||||
int blockX,
|
||||
int blockY,
|
||||
int blockZ,
|
||||
RNG rng
|
||||
int blockZ
|
||||
) {
|
||||
this.irisBiome = irisBiome;
|
||||
this.underground = underground;
|
||||
@@ -1153,7 +1140,100 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
this.blockX = blockX;
|
||||
this.blockY = blockY;
|
||||
this.blockZ = blockZ;
|
||||
this.rng = rng;
|
||||
}
|
||||
}
|
||||
|
||||
private final class ChunkVisibleResolver implements VisibleResolver<H> {
|
||||
private final IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State();
|
||||
private final boolean flat = platformGenerator != null && platformGenerator.usesFlatStudioTerrain();
|
||||
private ChunkResolution chunk;
|
||||
|
||||
@Override
|
||||
public H biome(int quartX, int quartY, int quartZ) {
|
||||
if (flat) {
|
||||
return fallbackBiome;
|
||||
}
|
||||
int chunkX = quartX >> 2;
|
||||
int chunkZ = quartZ >> 2;
|
||||
ChunkResolution current = chunk;
|
||||
if (current == null || current.chunkX != chunkX || current.chunkZ != chunkZ) {
|
||||
current = new ChunkResolution(chunkX, chunkZ);
|
||||
chunk = current;
|
||||
}
|
||||
return resolveVisibleBiomeHolder(quartX, quartY, quartZ, resolverState, null, current);
|
||||
}
|
||||
}
|
||||
|
||||
private final class ChunkResolution {
|
||||
private static final int COLUMNS = 16;
|
||||
|
||||
private final int chunkX;
|
||||
private final int chunkZ;
|
||||
private final IrisComplex complex;
|
||||
private final IrisDimension dimension;
|
||||
private final DimensionStackContext stackContext;
|
||||
private final int worldMinHeight;
|
||||
private final boolean subterrain;
|
||||
private final RuntimeBiomeState<H, V> runtimeState;
|
||||
private final int[] hostHeights = new int[COLUMNS];
|
||||
private final boolean[] hostHeightResolved = new boolean[COLUMNS];
|
||||
private final IrisBiome[] hostSurfaces = new IrisBiome[COLUMNS];
|
||||
private final boolean[] hostSurfaceResolved = new boolean[COLUMNS];
|
||||
private IrisDimension[] customDimensions = new IrisDimension[4];
|
||||
private IrisBiomeCustom[] customBiomes = new IrisBiomeCustom[4];
|
||||
private Object[] customHolders = new Object[4];
|
||||
private int customCount;
|
||||
|
||||
private ChunkResolution(int chunkX, int chunkZ) {
|
||||
this.chunkX = chunkX;
|
||||
this.chunkZ = chunkZ;
|
||||
this.complex = engine.getComplex();
|
||||
this.dimension = engine.getDimension();
|
||||
this.stackContext = engine.getDimensionStackContext();
|
||||
this.worldMinHeight = engine.getWorld().minHeight();
|
||||
this.subterrain = !engine.subterrainAbsentInChunk(chunkX, chunkZ);
|
||||
this.runtimeState = runtimeBiomeState();
|
||||
}
|
||||
|
||||
private int hostHeight(int blockX, int blockZ) {
|
||||
int column = column(blockX, blockZ);
|
||||
if (!hostHeightResolved[column]) {
|
||||
hostHeights[column] = Engine.hostHeight(engine, blockX, blockZ, true);
|
||||
hostHeightResolved[column] = true;
|
||||
}
|
||||
return hostHeights[column];
|
||||
}
|
||||
|
||||
private IrisBiome hostSurfaceBiome(int blockX, int blockZ) {
|
||||
int column = column(blockX, blockZ);
|
||||
if (!hostSurfaceResolved[column]) {
|
||||
hostSurfaces[column] = engine.getHostSurfaceBiome(blockX, blockZ);
|
||||
hostSurfaceResolved[column] = true;
|
||||
}
|
||||
return hostSurfaces[column];
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private H customHolder(IrisDimension owner, IrisBiomeCustom custom) {
|
||||
for (int index = 0; index < customCount; index++) {
|
||||
if (customBiomes[index] == custom && customDimensions[index] == owner) {
|
||||
return (H) customHolders[index];
|
||||
}
|
||||
}
|
||||
H holder = runtimeState.customBiomes().get(customBiomeKey(owner, custom.getId()));
|
||||
if (customCount == customBiomes.length) {
|
||||
customDimensions = Arrays.copyOf(customDimensions, customCount * 2);
|
||||
customBiomes = Arrays.copyOf(customBiomes, customCount * 2);
|
||||
customHolders = Arrays.copyOf(customHolders, customCount * 2);
|
||||
}
|
||||
customDimensions[customCount] = owner;
|
||||
customBiomes[customCount] = custom;
|
||||
customHolders[customCount++] = holder;
|
||||
return holder;
|
||||
}
|
||||
|
||||
private static int column(int blockX, int blockZ) {
|
||||
return ((blockX & 15) >> 2) << 2 | ((blockZ & 15) >> 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1181,8 +1261,7 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
}
|
||||
@Override
|
||||
public VisibleResolver<H> visibleResolver() {
|
||||
IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State();
|
||||
return (x, y, z) -> getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, state);
|
||||
return new ChunkVisibleResolver();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,7 +20,7 @@ package art.arcane.iris.platform.bukkit.nms;
|
||||
|
||||
import art.arcane.volmlib.nativelib.terrain.ServerShutdownBoundary;
|
||||
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockVolume;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeSectionVolume;
|
||||
|
||||
import art.arcane.volmlib.nativelib.terrain.BukkitTerrainBuffer;
|
||||
|
||||
@@ -217,7 +217,7 @@ public interface INMSBinding extends NativeTerrainAccess {
|
||||
return false;
|
||||
}
|
||||
|
||||
default boolean applyChunkDataBlocks(ChunkGenerator.ChunkData chunkData, NativeBlockVolume data) {
|
||||
default boolean applyChunkDataBlocks(ChunkGenerator.ChunkData chunkData, NativeSectionVolume data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,8 @@ import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
import art.arcane.iris.generation.chunk.ColumnBiomeHunk;
|
||||
import art.arcane.iris.platform.bukkit.BukkitBiome;
|
||||
import art.arcane.iris.generation.chunk.ChunkDataHunkHolder;
|
||||
import art.arcane.iris.generation.chunk.ChunkDataOutput;
|
||||
import art.arcane.iris.generation.chunk.SectionBuffer;
|
||||
import art.arcane.iris.generation.context.ChunkContext;
|
||||
import art.arcane.iris.generation.context.IrisContext;
|
||||
import art.arcane.volmlib.util.io.ReactiveFolder;
|
||||
@@ -1344,7 +1345,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
if (selected != null) {
|
||||
selected.generateChunk(engine, TerrainChunk.create(d), x, z);
|
||||
} else {
|
||||
ChunkDataHunkHolder blocks = new ChunkDataHunkHolder(d);
|
||||
SectionBuffer blocks = new SectionBuffer(d.getMaxHeight() - d.getMinHeight());
|
||||
Hunk<NativeBiome> biomes = new ColumnBiomeHunk(d.getMaxHeight() - d.getMinHeight(), () -> BukkitBiome.of(Biome.PLAINS));
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope =
|
||||
openGenerationHistoryCoordinateScope(engine, x << 4, z << 4);
|
||||
@@ -1352,7 +1353,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
engine.generate(x << 4, z << 4, blocks, biomes,
|
||||
initialEntryPending || studioEntryBootstrapActive.get() || engine.shouldGenerateMulticore());
|
||||
blocks.apply();
|
||||
ChunkDataOutput.apply(d, blocks);
|
||||
if (historyScope != null) {
|
||||
historyScope.claimGeneratedSemantics();
|
||||
}
|
||||
|
||||
+48
-31
@@ -13,12 +13,13 @@ import art.arcane.volmlib.util.collection.KList;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.IntConsumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -38,8 +39,8 @@ public final class NativeStructureVolumeIndex {
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
private final OriginResolver origins;
|
||||
private final Map<RuntimeChunkKey, KList<NativeStructureVolume>> originCache = lru(MAX_CACHED_ORIGIN_CHUNKS);
|
||||
private final Map<RuntimeChunkKey, KList<NativeStructureVolume>> queryCache = lru(MAX_CACHED_QUERY_CHUNKS);
|
||||
private final VolumeCache originCache = new VolumeCache(MAX_CACHED_ORIGIN_CHUNKS);
|
||||
private final VolumeCache queryCache = new VolumeCache(MAX_CACHED_QUERY_CHUNKS);
|
||||
private final ConcurrentHashMap<RuntimeChunkKey, CompletableFuture<KList<NativeStructureVolume>>> originBuilds =
|
||||
new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<RuntimeChunkKey, CompletableFuture<KList<NativeStructureVolume>>> queryBuilds =
|
||||
@@ -180,14 +181,12 @@ public final class NativeStructureVolumeIndex {
|
||||
Engine engine, int chunkX, int chunkZ, Integer pinnedRuntimeId
|
||||
) {
|
||||
if (pinnedRuntimeId != null) {
|
||||
synchronized (originCache) {
|
||||
KList<NativeStructureVolume> cached = originCache.get(
|
||||
new RuntimeChunkKey(pinnedRuntimeId, chunkKey(chunkX, chunkZ)));
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
}
|
||||
return originVolumes(engine, chunkX, chunkZ);
|
||||
}
|
||||
|
||||
@@ -210,28 +209,22 @@ public final class NativeStructureVolumeIndex {
|
||||
}
|
||||
|
||||
void evictRuntime(int runtimeId) {
|
||||
synchronized (originCache) {
|
||||
originBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||
originCache.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||
}
|
||||
synchronized (queryCache) {
|
||||
originCache.evictRuntime(runtimeId);
|
||||
queryBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||
queryCache.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||
}
|
||||
queryCache.evictRuntime(runtimeId);
|
||||
}
|
||||
|
||||
private KList<NativeStructureVolume> cached(
|
||||
Map<RuntimeChunkKey, KList<NativeStructureVolume>> cache,
|
||||
VolumeCache volumes,
|
||||
ConcurrentHashMap<RuntimeChunkKey, CompletableFuture<KList<NativeStructureVolume>>> builds,
|
||||
RuntimeChunkKey key,
|
||||
Supplier<KList<NativeStructureVolume>> loader
|
||||
) {
|
||||
synchronized (cache) {
|
||||
KList<NativeStructureVolume> hit = cache.get(key);
|
||||
KList<NativeStructureVolume> hit = volumes.get(key);
|
||||
if (hit != null) {
|
||||
return hit;
|
||||
}
|
||||
}
|
||||
|
||||
CompletableFuture<KList<NativeStructureVolume>> future = new CompletableFuture<>();
|
||||
CompletableFuture<KList<NativeStructureVolume>> existing = builds.putIfAbsent(key, future);
|
||||
@@ -241,22 +234,16 @@ public final class NativeStructureVolumeIndex {
|
||||
|
||||
// Close the check-then-claim window: a racer whose cache check missed before the
|
||||
// previous builder cached could claim the build slot after it retired and rebuild.
|
||||
synchronized (cache) {
|
||||
KList<NativeStructureVolume> published = cache.get(key);
|
||||
KList<NativeStructureVolume> published = volumes.get(key);
|
||||
if (published != null) {
|
||||
future.complete(published);
|
||||
builds.remove(key, future);
|
||||
return published;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
KList<NativeStructureVolume> built = loader.get();
|
||||
synchronized (cache) {
|
||||
if (builds.get(key) == future) {
|
||||
cache.put(key, built);
|
||||
}
|
||||
}
|
||||
volumes.publish(key, built, () -> builds.get(key) == future);
|
||||
future.complete(built);
|
||||
return built;
|
||||
} catch (RuntimeException | Error error) {
|
||||
@@ -303,15 +290,45 @@ public final class NativeStructureVolumeIndex {
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<RuntimeChunkKey, KList<NativeStructureVolume>> lru(int capacity) {
|
||||
return new LinkedHashMap<>(16, 0.75F, true) {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(
|
||||
Map.Entry<RuntimeChunkKey, KList<NativeStructureVolume>> eldest
|
||||
) {
|
||||
return size() > capacity;
|
||||
private static final class VolumeCache {
|
||||
private final int capacity;
|
||||
private final ConcurrentHashMap<RuntimeChunkKey, KList<NativeStructureVolume>> entries = new ConcurrentHashMap<>();
|
||||
private final ConcurrentLinkedQueue<RuntimeChunkKey> insertionOrder = new ConcurrentLinkedQueue<>();
|
||||
|
||||
private VolumeCache(int capacity) {
|
||||
this.capacity = capacity;
|
||||
}
|
||||
|
||||
private KList<NativeStructureVolume> get(RuntimeChunkKey key) {
|
||||
return entries.get(key);
|
||||
}
|
||||
|
||||
private void publish(RuntimeChunkKey key, KList<NativeStructureVolume> volumes, BooleanSupplier current) {
|
||||
boolean[] inserted = new boolean[1];
|
||||
entries.compute(key, (ignored, previous) -> {
|
||||
if (!current.getAsBoolean()) {
|
||||
return previous;
|
||||
}
|
||||
inserted[0] = previous == null;
|
||||
return volumes;
|
||||
});
|
||||
if (!inserted[0]) {
|
||||
return;
|
||||
}
|
||||
insertionOrder.add(key);
|
||||
while (entries.size() > capacity) {
|
||||
RuntimeChunkKey eldest = insertionOrder.poll();
|
||||
if (eldest == null) {
|
||||
return;
|
||||
}
|
||||
entries.remove(eldest);
|
||||
}
|
||||
}
|
||||
|
||||
private void evictRuntime(int runtimeId) {
|
||||
entries.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||
insertionOrder.removeIf(key -> key.runtimeId() == runtimeId);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static int runtimeChunkHash(int runtimeId, long chunkKey) {
|
||||
|
||||
@@ -61,6 +61,7 @@ import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.function.BiConsumer;
|
||||
@@ -144,6 +145,8 @@ public class IrisObject extends IrisRegistrant {
|
||||
protected transient AtomicCache<AxisAlignedBB> aabb = new AtomicCache<>();
|
||||
transient final AtomicCache<KList<IrisBlockVector>> surfaceSupportOffsets = new AtomicCache<>();
|
||||
transient final AtomicCache<FloatingObjectFootprint> floatingFootprint = new AtomicCache<>();
|
||||
transient final ConcurrentHashMap<ObjectPlacementBake.Key, ObjectPlacementBake> placementBakes = new ConcurrentHashMap<>();
|
||||
transient volatile ObjectPlacementBake.Layout placementLayout;
|
||||
@Getter
|
||||
@Setter
|
||||
private transient TreeGrounding treeGrounding;
|
||||
|
||||
+6
-1455
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,221 @@
|
||||
package art.arcane.iris.structure.object;
|
||||
|
||||
import art.arcane.iris.generation.block.BlockDataMergeSupport;
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.generation.decoration.IrisProceduralBlocks;
|
||||
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.iris.generation.block.B;
|
||||
import art.arcane.volmlib.util.noise.CNG;
|
||||
import art.arcane.volmlib.util.math.BlockPosition;
|
||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
final class ObjectBlockPass {
|
||||
|
||||
private final BiConsumer<BlockPosition, NativeBlockState> listener;
|
||||
|
||||
private final PlacementContext context;
|
||||
private final PlacementState state;
|
||||
private final ObjectPlacementBake bake;
|
||||
private final PlacementPalette palette;
|
||||
private final String[] markers;
|
||||
private final PlacementLifecycle lifecycle;
|
||||
|
||||
ObjectBlockPass(
|
||||
PlacementContext context,
|
||||
PlacementState state,
|
||||
PlacementLifecycle lifecycle,
|
||||
BiConsumer<BlockPosition, NativeBlockState> listener
|
||||
) {
|
||||
this.context = context;
|
||||
this.state = state;
|
||||
this.lifecycle = lifecycle;
|
||||
this.listener = listener;
|
||||
this.bake = state.bake;
|
||||
this.palette = state.palette;
|
||||
this.markers = state.config.getMarkers().isNotEmpty() && state.placer.getEngine() != null
|
||||
? ObjectMarkerPlacement.resolve(context, state.config, bake) : null;
|
||||
}
|
||||
|
||||
void process() {
|
||||
boolean warped = !state.config.getWarp().isFlat();
|
||||
boolean hasEdits = !state.config.getEdit().isEmpty();
|
||||
CNG surfaceWarp = warped ? state.config.getSurfaceWarp(context.rng, context.self.getLoader()) : null;
|
||||
double warpHalf = warped ? state.config.getWarp().getMultiplier() / 2D : 0D;
|
||||
boolean waterlogCandidate = state.config.isWaterloggable() || state.config.isUnderwater();
|
||||
double[] exact = warped || hasEdits ? bake.exact(context.self, state.spin) : null;
|
||||
int[] tileBlocks = bake.tileBlocks();
|
||||
TileData[] tileValues = bake.tileValues();
|
||||
int tileCursor = 0;
|
||||
int count = bake.count();
|
||||
|
||||
for (int index = 0; index < count; index++) {
|
||||
int entry = bake.paletteIndex(index);
|
||||
TileData tile = null;
|
||||
if (tileBlocks != null && tileCursor < tileBlocks.length && tileBlocks[tileCursor] == index) {
|
||||
tile = tileValues[tileCursor++];
|
||||
}
|
||||
|
||||
NativeBlockState data = palette.decayed(entry);
|
||||
boolean edited = false;
|
||||
if (hasEdits) {
|
||||
EditResult result = applyEdits(exact, index, data, tile);
|
||||
data = result.data();
|
||||
tile = result.tile();
|
||||
edited = result.edited();
|
||||
}
|
||||
|
||||
PlacementPalette.Facts facts = edited ? palette.factsOf(data) : palette.facts(entry);
|
||||
NativeBlockState rotated = facts.rotated();
|
||||
if (rotated == null) {
|
||||
continue;
|
||||
}
|
||||
int xx = context.x + bake.offsetX(index);
|
||||
int yy = state.y + bake.offsetY(index);
|
||||
int zz = context.z + bake.offsetZ(index);
|
||||
|
||||
if (warped) {
|
||||
double exactX = exact[index * 3];
|
||||
double exactY = exact[index * 3 + 1];
|
||||
double exactZ = exact[index * 3 + 2];
|
||||
xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, exactX + context.x, exactY + state.y, exactZ + context.z);
|
||||
zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, exactZ + context.z, exactY + state.y, exactX + context.x);
|
||||
}
|
||||
|
||||
if (state.paintProjection != null) {
|
||||
int surface = state.paintProjection.surfaceY(xx, zz);
|
||||
if (surface == IrisPaintSurfaceProjection.MISSING) {
|
||||
continue;
|
||||
}
|
||||
if (!B.isVineBlock(rotated)) {
|
||||
yy = bake.offsetY(index) + Math.floorDiv(context.self.h, 2) + surface + state.yrand;
|
||||
}
|
||||
}
|
||||
|
||||
if (state.config.isMeld() && !is(ObjectPlaceMode.STRUCTURE_PIECE) && !state.placer.isSolid(xx, yy, zz)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean canopy = bake.canopy(index);
|
||||
if (canopy && !state.placer.beginTreeCanopy(new BlockPosition(xx, yy, zz))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
data = rotated;
|
||||
if (facts.waterloggable()) {
|
||||
data = waterlogCandidate || facts.authoredWaterlogged()
|
||||
? IrisProceduralBlocks.normalizeWaterlogging(data, state.placer.get(xx, yy, zz), waterlogCandidate)
|
||||
: facts.dry();
|
||||
}
|
||||
|
||||
if (!is(ObjectPlaceMode.STRUCTURE_PIECE) && B.isVineBlock(data)) {
|
||||
data = attachVineFaces(state.placer, data, xx, yy, zz);
|
||||
}
|
||||
|
||||
String marker = markers == null ? null : markers[index];
|
||||
if (handleCustomData(data, facts.air(), tile, marker, xx, yy, zz)) {
|
||||
lifecycle.run(PlacementStage.BLOCK_WRITTEN, context, state, new PlacementBlockWrite(xx, yy, zz, data));
|
||||
if (state.isRejected()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (canopy) {
|
||||
state.placer.endTreeCanopy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean is(ObjectPlaceMode mode) {
|
||||
return state.config.getMode() == mode;
|
||||
}
|
||||
|
||||
static boolean shouldPlaceObjectBlock(boolean rawStructurePiece, boolean air, boolean wouldReplace) {
|
||||
return !wouldReplace && (rawStructurePiece || !air);
|
||||
}
|
||||
|
||||
private boolean handleCustomData(NativeBlockState data, boolean air, TileData tile, String marker, int xx, int yy, int zz) {
|
||||
// Short-circuit order matters for cost only: the mantle read is paid solely
|
||||
// for vine blocks. Both operands are pure, so the value is unchanged.
|
||||
boolean wouldReplace = !is(ObjectPlaceMode.STRUCTURE_PIECE) && B.isVineBlock(data) && B.isSolid(state.placer.get(xx, yy, zz));
|
||||
boolean place = shouldPlaceObjectBlock(is(ObjectPlaceMode.STRUCTURE_PIECE), air, wouldReplace);
|
||||
if (data.isCustom() || place) {
|
||||
state.placer.set(xx, yy, zz, data);
|
||||
if (tile != null) {
|
||||
state.placer.setTile(xx, yy, zz, tile);
|
||||
}
|
||||
if (marker != null) {
|
||||
state.placer.setData(xx, yy, zz, new MatterMarker(marker));
|
||||
}
|
||||
if (listener != null) {
|
||||
listener.accept(new BlockPosition(xx, yy, zz), data);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private EditResult applyEdits(double[] exact, int index, NativeBlockState data, TileData tile) {
|
||||
double exactX = exact[index * 3];
|
||||
double exactY = exact[index * 3 + 1];
|
||||
double exactZ = exact[index * 3 + 2];
|
||||
boolean edited = false;
|
||||
for (IrisObjectReplace j : state.config.getEdit()) {
|
||||
if (context.rng.chance(j.getChance())) {
|
||||
for (NativeBlockState k : j.getFind(context.rdata)) {
|
||||
if (j.isExact() ? k.matches(data) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(data))) {
|
||||
NativeBlockState newData = j.getReplace(context.rng, exactX + context.x, exactY + state.y, exactZ + context.z, context.rdata);
|
||||
|
||||
boolean sameMaterial = IrisObjectShaping.materialKey(newData).equals(IrisObjectShaping.materialKey(data));
|
||||
if (sameMaterial && !(newData.isCustom() || data.isCustom()))
|
||||
data = BlockDataMergeSupport.merge(data, newData);
|
||||
else
|
||||
data = newData;
|
||||
edited = true;
|
||||
|
||||
Optional<TileData> t = j.getReplace().getTile(context.rng, context.x, state.y, context.z, context.rdata);
|
||||
if (t.isPresent()) {
|
||||
tile = t.get();
|
||||
} else if (!sameMaterial) {
|
||||
tile = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new EditResult(data, tile, edited);
|
||||
}
|
||||
|
||||
static NativeBlockState normalizePlacedWaterlogging(NativeBlockState state, IObjectPlacer placer,
|
||||
int x, int y, int z, boolean enabled) {
|
||||
if (state == null || !IrisProceduralBlocks.hasProperty(state, "waterlogged")) {
|
||||
return state;
|
||||
}
|
||||
if (!enabled && !"true".equals(IrisProceduralBlocks.propertyValue(state, "waterlogged"))) {
|
||||
return IrisProceduralBlocks.normalizeWaterlogging(state, null, false);
|
||||
}
|
||||
return IrisProceduralBlocks.normalizeWaterlogging(state, placer.get(x, y, z), enabled);
|
||||
}
|
||||
|
||||
static NativeBlockState attachVineFaces(IObjectPlacer placer, NativeBlockState data, int x, int y, int z) {
|
||||
NativeBlockState result = data;
|
||||
for (String face : IrisProceduralBlocks.FACE_PROPERTIES) {
|
||||
if (!IrisProceduralBlocks.hasProperty(data, face)) {
|
||||
continue;
|
||||
}
|
||||
int[] mod = IrisProceduralBlocks.faceOffset(face);
|
||||
NativeBlockState facing = placer.get(x + mod[0], y + mod[1], z + mod[2]);
|
||||
if (B.isSolid(facing) && !B.isVineBlock(facing)) {
|
||||
result = result.withProperty(face, "true");
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private record EditResult(NativeBlockState data, TileData tile, boolean edited) {}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package art.arcane.iris.structure.object;
|
||||
|
||||
import art.arcane.iris.world.entity.IrisMarker;
|
||||
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.iris.generation.block.VectorMap;
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
/** Chooses marker positions from the object's own blocks, indexed like the placement bake. */
|
||||
final class ObjectMarkerPlacement {
|
||||
private ObjectMarkerPlacement() {
|
||||
}
|
||||
|
||||
/** Callers hold the object read lock, so the source cursor matches the bake order. */
|
||||
static String[] resolve(PlacementContext context, IrisObjectPlacement config, ObjectPlacementBake bake) {
|
||||
IrisObject self = context.self;
|
||||
VectorMap<NativeBlockState> blocks = self.blocks;
|
||||
KMap<IrisBlockVector, String> markers = new KMap<>();
|
||||
KList<IrisBlockVector> list = StreamSupport.stream(blocks.keys().spliterator(), false)
|
||||
.collect(KList.collector());
|
||||
// Marker selection persists into the mantle, so it must be seed-deterministic.
|
||||
// Derive a side stream keyed on the placement position instead of consuming the
|
||||
// caller's rng: consuming draws there would shift every later placement in the
|
||||
// chunk and invalidate existing worlds/goldenhashes.
|
||||
RNG markerRng = context.rng.nextParallelRNG(((((long) context.x) << 32) | (context.z & 0xFFFFFFFFL)) * 31L + context.yv);
|
||||
|
||||
int markerIndex = 0;
|
||||
for (IrisObjectMarker j : config.getMarkers()) {
|
||||
IrisMarker marker = self.getLoader().getMarkerLoader().load(j.getMarker());
|
||||
int markerSalt = markerIndex++;
|
||||
|
||||
if (marker == null || marker.isCompatExcluded()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int max = j.getMaximumMarkers();
|
||||
for (IrisBlockVector i : list.shuffleCopy(markerRng.nextParallelRNG(markerSalt))) {
|
||||
if (max <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
NativeBlockState data = blocks.get(i);
|
||||
if (data == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (NativeBlockState k : j.getMark(context.rdata)) {
|
||||
if (max <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (j.isExact() ? k.matches(data) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(data))) {
|
||||
boolean firstAboveEmpty = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 1, 0)));
|
||||
boolean secondAboveEmpty = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 2, 0)));
|
||||
|
||||
if (!marker.isEmptyAbove() || (firstAboveEmpty && secondAboveEmpty)) {
|
||||
markers.put(i, j.getMarker());
|
||||
max--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String[] resolved = new String[bake.count()];
|
||||
VectorMap<NativeBlockState>.Cursor cursor = blocks.cursor();
|
||||
int index = 0;
|
||||
while (index < resolved.length && cursor.next()) {
|
||||
resolved[index++] = markers.get(cursor.key());
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
@@ -18,91 +18,135 @@
|
||||
|
||||
package art.arcane.iris.structure.object;
|
||||
|
||||
|
||||
import art.arcane.volmlib.util.documentation.Description;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@Description("Object Place modes are useful for positioning objects just right. The default value is CENTER_HEIGHT.")
|
||||
public enum ObjectPlaceMode {
|
||||
@Description("The default place mode. This mode picks a center point (where the center of the object will be) and takes the height. That height is used for the whole object.")
|
||||
|
||||
CENTER_HEIGHT,
|
||||
CENTER_HEIGHT(PlacementModuleCenterHeight::new),
|
||||
|
||||
@Description("Samples a lot of points where the object will cover (horizontally) and picks the highest height, that height is then used to place the object. This mode is useful for preventing any part of your object from being buried though it will float off of cliffs.")
|
||||
|
||||
MAX_HEIGHT,
|
||||
MAX_HEIGHT(() -> new PlacementModuleHeight(PlacementModuleHeight.Mode.MAX, false)),
|
||||
|
||||
@Description("Samples only 4 points where the object will cover (horizontally) and picks the highest height, that height is then used to place the object. This mode is useful for preventing any part of your object from being buried though it will float off of cliffs.\"")
|
||||
|
||||
FAST_MAX_HEIGHT,
|
||||
FAST_MAX_HEIGHT(() -> new PlacementModuleHeight(PlacementModuleHeight.Mode.MAX, true)),
|
||||
|
||||
@Description("Samples a lot of points where the object will cover (horizontally) and picks the lowest height, that height is then used to place the object. This mode is useful for preventing any part of your object from overhanging a cliff though it gets buried a lot")
|
||||
|
||||
MIN_HEIGHT,
|
||||
MIN_HEIGHT(() -> new PlacementModuleHeight(PlacementModuleHeight.Mode.MIN, false)),
|
||||
|
||||
@Description("Samples only 4 points where the object will cover (horizontally) and picks the lowest height, that height is then used to place the object. This mode is useful for preventing any part of your object from overhanging a cliff though it gets buried a lot")
|
||||
|
||||
FAST_MIN_HEIGHT,
|
||||
FAST_MIN_HEIGHT(() -> new PlacementModuleHeight(PlacementModuleHeight.Mode.MIN, true)),
|
||||
|
||||
@Description("Stilting is MAX_HEIGHT but it repeats the bottom most block of your object until it hits the surface. This is expensive because it has to first sample every height value for each x,z position of your object. Avoid using this unless its structures for performance reasons.")
|
||||
|
||||
STILT,
|
||||
STILT(() -> new PlacementModuleStilt(PlacementModuleStilt.Mode.DEFAULT, false)),
|
||||
|
||||
@Description("Just like stilting but very inaccurate. Useful for stilting a lot of objects without too much care on accuracy (you can use the over-stilt value to force stilts under ground further)")
|
||||
|
||||
FAST_STILT,
|
||||
FAST_STILT(() -> new PlacementModuleStilt(PlacementModuleStilt.Mode.DEFAULT, true)),
|
||||
|
||||
@Description("Stilting is MIN_HEIGHT but it repeats the bottom most block of your object until it hits the surface. This is expensive because it has to first sample every height value for each x,z position of your object. Avoid using this unless its structures for performance reasons.")
|
||||
|
||||
MIN_STILT,
|
||||
MIN_STILT(() -> new PlacementModuleStilt(PlacementModuleStilt.Mode.MIN, false)),
|
||||
|
||||
@Description("Just like MIN_STILT but very inaccurate. Useful for stilting a lot of objects without too much care on accuracy (you can use the over-stilt value to force stilts under ground further)")
|
||||
|
||||
FAST_MIN_STILT,
|
||||
FAST_MIN_STILT(() -> new PlacementModuleStilt(PlacementModuleStilt.Mode.MIN, true)),
|
||||
|
||||
@Description("Stilting is CENTER_HEIGHT but it repeats the bottom most block of your object until it hits the surface. This is expensive because it has to first sample every height value for each x,z position of your object. Avoid using this unless its structures for performance reasons.")
|
||||
|
||||
CENTER_STILT,
|
||||
CENTER_STILT(() -> new PlacementModuleStilt(PlacementModuleStilt.Mode.CENTER, false)),
|
||||
|
||||
@Description("Erode stilting tapers columns downward like an ice cream cone. Blocks near the center extend deepest while edge blocks drop off first. Blocks are randomly skipped in the lower portion for a rough organic eroded texture.")
|
||||
|
||||
ERODE_STILT,
|
||||
ERODE_STILT(() -> new PlacementModuleStilt(PlacementModuleStilt.Mode.ERODE, false)),
|
||||
|
||||
@Description("Organic stilting scans straight down from the bottom of the object to the first solid block (the cave floor, or terrain) and fills the gap with the object's own bottom block. Each column stops at a slightly different, noise-varied depth and the lower portion is randomly broken up, so the underside reads as rough organic roots rather than a flat disc. Designed for objects placed inside caves so they connect to the floor instead of floating. Tune with stilt-settings (organicMaxScan, organicJitter, organicScratch).")
|
||||
|
||||
ORGANIC_STILT,
|
||||
ORGANIC_STILT(() -> new PlacementModuleStilt(PlacementModuleStilt.Mode.ORGANIC, false)),
|
||||
|
||||
@Description("Hangs the object upside-down from a cave ceiling. The object is flipped vertically (its tip points down like a stalactite) and its top is anchored to the ceiling, while an organic, noise-varied stilt grows up into the ceiling so it never floats below the roof. Use inside caves; tune with stilt-settings (organicMaxScan, organicJitter, organicScratch).")
|
||||
|
||||
CEILING_HANG,
|
||||
CEILING_HANG(() -> new PlacementModuleStilt(PlacementModuleStilt.Mode.CEILING, false)),
|
||||
|
||||
@Description("Anchors the object at the terrain surface (plus translate.y) like CENTER_HEIGHT, then bends the surrounding terrain to be flush with the object's base. Columns inside the object footprint are raised (or lowered) to meet the base, and the deformation falls off parabolically out to a radius so the terrain smoothly blends back to its natural height. Use this to seat a floating/translated object on the ground without a flat stilt disc, e.g. a cube translated up 10 on a mountain pulls the terrain up to meet it. Tune with vacuum-settings (radius, falloff).")
|
||||
|
||||
VACUUM,
|
||||
VACUUM(PlacementModuleCenterHeight::new),
|
||||
|
||||
@Description("VACUUM with a larger radius and finer per-column sampling for the smoothest terrain blend. More expensive.")
|
||||
|
||||
VACUUM_HIGH,
|
||||
VACUUM_HIGH(PlacementModuleCenterHeight::new),
|
||||
|
||||
@Description("VACUUM with a smaller radius and coarser sampling for performance. Use when seating many objects.")
|
||||
|
||||
VACUUM_FAST,
|
||||
VACUUM_FAST(PlacementModuleCenterHeight::new),
|
||||
|
||||
@Description("VACUUM whose falloff radius is perturbed by noise per column, so the terrain meets the object with an irregular organic edge rather than a clean parabolic bowl.")
|
||||
|
||||
VACUUM_ORGANIC,
|
||||
VACUUM_ORGANIC(PlacementModuleCenterHeight::new),
|
||||
|
||||
@Description("VACUUM whose terrain bend is bent by smooth coherent simplex noise so the surface meets the object with a gently rolling, wavy slope instead of the ragged white-noise edge of VACUUM_ORGANIC or a clean parabolic bowl. The object itself still seats flush; the waves fade to zero under the object and at the outer rim, peaking across the mid-slope. Tune with vacuum-settings (waveAmplitude, waveScale).")
|
||||
|
||||
VACUUM_WAVY,
|
||||
VACUUM_WAVY(PlacementModuleCenterHeight::new),
|
||||
|
||||
@Description("Samples the height of the terrain at every x,z position of your object and pushes it down to the surface. It's pretty much like a melt function over the terrain.")
|
||||
|
||||
PAINT,
|
||||
PAINT(PlacementModulePaint::new),
|
||||
|
||||
@Description("Places the object in pure air at an absolute Y driven entirely by translate.y (plus optional translate.yRandom). Terrain height, underwater, and carving anchor checks are skipped. Use this for floating islands, sky structures, clouds, or blimps where the object must not be translated to the ground.")
|
||||
|
||||
FLOATING,
|
||||
FLOATING(PlacementModuleFloating::new),
|
||||
|
||||
@Description("Raw stamp at the caller-supplied (x, y, z). No terrain sampling, no stilting, no Y recomputation, no underwater or carving anchor guards. Used internally to route native Minecraft structure pieces (villages etc.) through the Iris object placer.")
|
||||
|
||||
STRUCTURE_PIECE
|
||||
STRUCTURE_PIECE(() -> lifecycle -> PlacementModuleAnchor.install(lifecycle,
|
||||
(context, state, ignored) -> state.y = context.yv));
|
||||
|
||||
|
||||
private final Supplier<? extends PlacementModule> factory;
|
||||
|
||||
ObjectPlaceMode(Supplier<? extends PlacementModule> factory) {
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
PlacementLifecycle createLifecycle() {
|
||||
PlacementLifecycleBuilder lifecycle = new PlacementLifecycleBuilder();
|
||||
install(lifecycle);
|
||||
return lifecycle.build();
|
||||
}
|
||||
|
||||
void install(PlacementLifecycleBuilder lifecycle) {
|
||||
new PlacementModulePreparation().install(lifecycle);
|
||||
new PlacementModuleSlope().install(lifecycle);
|
||||
factory.get().install(lifecycle);
|
||||
new PlacementModuleAnchor().install(lifecycle);
|
||||
PlacementModulePaint.installProjection(lifecycle);
|
||||
new PlacementModuleSurfaceSupport().install(lifecycle);
|
||||
new PlacementModuleLimits().install(lifecycle);
|
||||
new PlacementModuleNativeStructureProtection().install(lifecycle);
|
||||
new PlacementModuleBounds().install(lifecycle);
|
||||
new PlacementModuleCollision().install(lifecycle);
|
||||
new PlacementModuleTreeGrounding().install(lifecycle);
|
||||
new PlacementModuleBore().install(lifecycle);
|
||||
new PlacementModuleBlocks().install(lifecycle);
|
||||
PlacementLifecycleDependency finish = switch (this) {
|
||||
case STILT, FAST_STILT, MIN_STILT, FAST_MIN_STILT, CENTER_STILT, ERODE_STILT, ORGANIC_STILT, CEILING_HANG ->
|
||||
PlacementModuleStilt.FILL;
|
||||
case VACUUM, VACUUM_HIGH, VACUUM_FAST, VACUUM_ORGANIC, VACUUM_WAVY -> {
|
||||
new PlacementModuleVacuum().install(lifecycle);
|
||||
yield PlacementModuleVacuum.FINISH;
|
||||
}
|
||||
default -> PlacementModuleBlocks.COMPLETE;
|
||||
};
|
||||
new PlacementModuleSnow(finish).install(lifecycle);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
package art.arcane.iris.structure.object;
|
||||
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.generation.block.VectorMap;
|
||||
import art.arcane.iris.generation.decoration.tree.TreeGrounding;
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
final class ObjectPlacementBake {
|
||||
private static final int MAXIMUM_VARIANTS_PER_OBJECT = 32;
|
||||
|
||||
private final Layout layout;
|
||||
private final Key key;
|
||||
private final int[] offsetX;
|
||||
private final int[] offsetY;
|
||||
private final int[] offsetZ;
|
||||
private final int[] layerY;
|
||||
private volatile double[] exact;
|
||||
|
||||
private ObjectPlacementBake(Layout layout, Key key, int[] offsetX, int[] offsetY, int[] offsetZ, int[] layerY) {
|
||||
this.layout = layout;
|
||||
this.key = key;
|
||||
this.offsetX = offsetX;
|
||||
this.offsetY = offsetY;
|
||||
this.offsetZ = offsetZ;
|
||||
this.layerY = layerY;
|
||||
}
|
||||
|
||||
static ObjectPlacementBake of(IrisObject object, SpinKernel spin, boolean ceilingHang, IrisBlockVector translateOffset) {
|
||||
Key key = Key.of(spin, ceilingHang, translateOffset);
|
||||
object.readLock.lock();
|
||||
try {
|
||||
Layout layout = Layout.of(object);
|
||||
ConcurrentHashMap<Key, ObjectPlacementBake> variants = object.placementBakes;
|
||||
ObjectPlacementBake cached = variants.get(key);
|
||||
if (cached != null && cached.layout == layout) {
|
||||
return cached;
|
||||
}
|
||||
ObjectPlacementBake built = bake(object, layout, key, spin, ceilingHang, translateOffset);
|
||||
if (cached != null) {
|
||||
variants.replace(key, cached, built);
|
||||
} else if (variants.size() < MAXIMUM_VARIANTS_PER_OBJECT) {
|
||||
variants.putIfAbsent(key, built);
|
||||
}
|
||||
return built;
|
||||
} finally {
|
||||
object.readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private static ObjectPlacementBake bake(IrisObject object, Layout layout, Key key, SpinKernel spin, boolean ceilingHang,
|
||||
IrisBlockVector translateOffset) {
|
||||
int count = layout.count;
|
||||
int[] x = new int[count];
|
||||
int[] y = new int[count];
|
||||
int[] z = new int[count];
|
||||
int[] layer = new int[count];
|
||||
boolean layerDiffers = false;
|
||||
IrisBlockVector rotated = new IrisBlockVector(0, 0, 0);
|
||||
VectorMap<NativeBlockState>.Cursor cursor = object.blocks.cursor();
|
||||
int index = 0;
|
||||
while (cursor.next() && index < count) {
|
||||
transform(cursor.key(), rotated, spin, ceilingHang, translateOffset);
|
||||
int roundedY = (int) Math.round(rotated.getY());
|
||||
int floorY = rotated.getBlockY();
|
||||
x[index] = (int) Math.round(rotated.getX());
|
||||
y[index] = roundedY;
|
||||
z[index] = (int) Math.round(rotated.getZ());
|
||||
layer[index] = floorY;
|
||||
layerDiffers |= floorY != roundedY;
|
||||
index++;
|
||||
}
|
||||
if (index != count) {
|
||||
throw new IllegalStateException("Object " + object.getLoadKey() + " changed while its placement bake was built");
|
||||
}
|
||||
return new ObjectPlacementBake(layout, key, x, y, z, layerDiffers ? layer : null);
|
||||
}
|
||||
|
||||
private static void transform(IrisBlockVector source, IrisBlockVector target, SpinKernel spin, boolean ceilingHang,
|
||||
IrisBlockVector translateOffset) {
|
||||
target.setX(source.getX());
|
||||
target.setY(source.getY());
|
||||
target.setZ(source.getZ());
|
||||
spin.rotate(target);
|
||||
if (ceilingHang) {
|
||||
target.setY(-target.getBlockY());
|
||||
}
|
||||
if (translateOffset != null) {
|
||||
target.add(translateOffset);
|
||||
}
|
||||
}
|
||||
|
||||
int count() {
|
||||
return layout.count;
|
||||
}
|
||||
|
||||
int offsetX(int index) {
|
||||
return offsetX[index];
|
||||
}
|
||||
|
||||
int offsetY(int index) {
|
||||
return offsetY[index];
|
||||
}
|
||||
|
||||
int offsetZ(int index) {
|
||||
return offsetZ[index];
|
||||
}
|
||||
|
||||
int layerY(int index) {
|
||||
return layerY == null ? offsetY[index] : layerY[index];
|
||||
}
|
||||
|
||||
int paletteIndex(int index) {
|
||||
return layout.paletteIndexes[index];
|
||||
}
|
||||
|
||||
NativeBlockState[] palette() {
|
||||
return layout.palette;
|
||||
}
|
||||
|
||||
int[] tileBlocks() {
|
||||
return layout.tileBlocks;
|
||||
}
|
||||
|
||||
TileData[] tileValues() {
|
||||
return layout.tileValues;
|
||||
}
|
||||
|
||||
boolean canopy(int index) {
|
||||
long[] canopy = layout.canopy;
|
||||
return canopy != null && (canopy[index >>> 6] & (1L << (index & 63))) != 0L;
|
||||
}
|
||||
|
||||
double[] exact(IrisObject object, SpinKernel spin) {
|
||||
double[] resolved = exact;
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
}
|
||||
int count = layout.count;
|
||||
object.readLock.lock();
|
||||
try {
|
||||
resolved = new double[count * 3];
|
||||
IrisBlockVector rotated = new IrisBlockVector(0, 0, 0);
|
||||
VectorMap<NativeBlockState>.Cursor cursor = object.blocks.cursor();
|
||||
int index = 0;
|
||||
while (cursor.next() && index < count) {
|
||||
transform(cursor.key(), rotated, spin, key.ceilingHang(), key.translateOffset());
|
||||
resolved[index * 3] = rotated.getX();
|
||||
resolved[index * 3 + 1] = rotated.getY();
|
||||
resolved[index * 3 + 2] = rotated.getZ();
|
||||
index++;
|
||||
}
|
||||
if (index != count) {
|
||||
throw new IllegalStateException("Object " + object.getLoadKey() + " changed while its placement bake was read");
|
||||
}
|
||||
} finally {
|
||||
object.readLock.unlock();
|
||||
}
|
||||
exact = resolved;
|
||||
return resolved;
|
||||
}
|
||||
|
||||
static final class Layout {
|
||||
private final long blocksRevision;
|
||||
private final long statesRevision;
|
||||
private final TreeGrounding grounding;
|
||||
private final int count;
|
||||
private final short[] paletteIndexes;
|
||||
private final NativeBlockState[] palette;
|
||||
private final int[] tileBlocks;
|
||||
private final TileData[] tileValues;
|
||||
private final long[] canopy;
|
||||
|
||||
private Layout(long blocksRevision, long statesRevision, TreeGrounding grounding, int count, short[] paletteIndexes,
|
||||
NativeBlockState[] palette, int[] tileBlocks, TileData[] tileValues, long[] canopy) {
|
||||
this.blocksRevision = blocksRevision;
|
||||
this.statesRevision = statesRevision;
|
||||
this.grounding = grounding;
|
||||
this.count = count;
|
||||
this.paletteIndexes = paletteIndexes;
|
||||
this.palette = palette;
|
||||
this.tileBlocks = tileBlocks;
|
||||
this.tileValues = tileValues;
|
||||
this.canopy = canopy;
|
||||
}
|
||||
|
||||
private static Layout of(IrisObject object) {
|
||||
Layout current = object.placementLayout;
|
||||
if (current != null && current.matches(object)) {
|
||||
return current;
|
||||
}
|
||||
Layout built = build(object);
|
||||
if (!built.matches(object)) {
|
||||
return built;
|
||||
}
|
||||
object.placementLayout = built;
|
||||
if (current != null) {
|
||||
object.placementBakes.clear();
|
||||
}
|
||||
return built;
|
||||
}
|
||||
|
||||
private static Layout build(IrisObject object) {
|
||||
VectorMap<NativeBlockState> blocks = object.blocks;
|
||||
VectorMap<TileData> states = object.states;
|
||||
long blocksRevision = blocks.modificationRevision();
|
||||
long statesRevision = states.modificationRevision();
|
||||
TreeGrounding grounding = object.getTreeGrounding();
|
||||
boolean hasStates = !states.isEmpty();
|
||||
int size = blocks.size();
|
||||
short[] paletteIndexes = new short[size];
|
||||
IntArrayList tileBlocks = new IntArrayList();
|
||||
List<TileData> tileValues = new ArrayList<>();
|
||||
List<NativeBlockState> palette = new ArrayList<>();
|
||||
IdentityHashMap<NativeBlockState, Integer> paletteSlots = new IdentityHashMap<>();
|
||||
long[] canopy = null;
|
||||
VectorMap<NativeBlockState>.Cursor cursor = blocks.cursor();
|
||||
int index = 0;
|
||||
while (cursor.next()) {
|
||||
IrisBlockVector source = cursor.key();
|
||||
NativeBlockState state = cursor.value();
|
||||
if (state == null) {
|
||||
state = IrisObject.States.air();
|
||||
}
|
||||
Integer slot = paletteSlots.get(state);
|
||||
if (slot == null) {
|
||||
slot = palette.size();
|
||||
if (slot >= Short.MAX_VALUE) {
|
||||
throw new IllegalStateException("Object " + object.getLoadKey() + " has more than "
|
||||
+ Short.MAX_VALUE + " distinct block states");
|
||||
}
|
||||
palette.add(state);
|
||||
paletteSlots.put(state, slot);
|
||||
}
|
||||
if (index == paletteIndexes.length) {
|
||||
throw new IllegalStateException("Object " + object.getLoadKey() + " changed while its placement layout was built");
|
||||
}
|
||||
paletteIndexes[index] = (short) slot.intValue();
|
||||
if (hasStates) {
|
||||
TileData tile = states.get(source);
|
||||
if (tile != null) {
|
||||
tileBlocks.add(index);
|
||||
tileValues.add(tile);
|
||||
}
|
||||
}
|
||||
if (grounding != null && grounding.isCanopy(source.getBlockX(), source.getBlockY(), source.getBlockZ())) {
|
||||
if (canopy == null) {
|
||||
canopy = new long[(size + 63) >>> 6];
|
||||
}
|
||||
canopy[index >>> 6] |= 1L << (index & 63);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
if (index != size) {
|
||||
throw new IllegalStateException("Object " + object.getLoadKey() + " changed while its placement layout was built");
|
||||
}
|
||||
return new Layout(blocksRevision, statesRevision, grounding, index, paletteIndexes,
|
||||
palette.toArray(new NativeBlockState[0]), tileBlocks.isEmpty() ? null : tileBlocks.toIntArray(),
|
||||
tileValues.isEmpty() ? null : tileValues.toArray(new TileData[0]), canopy);
|
||||
}
|
||||
|
||||
private boolean matches(IrisObject object) {
|
||||
return object.blocks.modificationRevision() == blocksRevision
|
||||
&& object.states.modificationRevision() == statesRevision
|
||||
&& object.getTreeGrounding() == grounding;
|
||||
}
|
||||
}
|
||||
|
||||
record Key(boolean rotates, int xMode, int yMode, int zMode, long xCos, long xSin, long yCos, long ySin,
|
||||
long zCos, long zSin, boolean ceilingHang, boolean translating, long translateX, long translateY,
|
||||
long translateZ) {
|
||||
static Key of(SpinKernel spin, boolean ceilingHang, IrisBlockVector translateOffset) {
|
||||
boolean translating = translateOffset != null;
|
||||
return new Key(spin.rotates, spin.xMode, spin.yMode, spin.zMode,
|
||||
Double.doubleToLongBits(spin.xCos), Double.doubleToLongBits(spin.xSin),
|
||||
Double.doubleToLongBits(spin.yCos), Double.doubleToLongBits(spin.ySin),
|
||||
Double.doubleToLongBits(spin.zCos), Double.doubleToLongBits(spin.zSin), ceilingHang, translating,
|
||||
translating ? Double.doubleToLongBits(translateOffset.getX()) : 0L,
|
||||
translating ? Double.doubleToLongBits(translateOffset.getY()) : 0L,
|
||||
translating ? Double.doubleToLongBits(translateOffset.getZ()) : 0L);
|
||||
}
|
||||
|
||||
IrisBlockVector translateOffset() {
|
||||
return translating ? new IrisBlockVector(Double.longBitsToDouble(translateX),
|
||||
Double.longBitsToDouble(translateY), Double.longBitsToDouble(translateZ)) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package art.arcane.iris.structure.object;
|
||||
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
|
||||
/** A completed object write, after its tile data and listener have been handled. */
|
||||
public record PlacementBlockWrite(int x, int y, int z, NativeBlockState data) {}
|
||||
@@ -0,0 +1,55 @@
|
||||
package art.arcane.iris.structure.object;
|
||||
|
||||
import art.arcane.iris.generation.cave.CarveResult;
|
||||
import art.arcane.iris.structure.object.PlacementContext.TransformedBounds;
|
||||
import art.arcane.iris.structure.object.PlacementContext.WorldBounds;
|
||||
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.math.BlockPosition;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/** Caller inputs for one placement. Modules keep derived values in PlacementState. */
|
||||
@Getter
|
||||
public final class PlacementContext {
|
||||
public record TransformedBounds(int minX, int maxX, int minY, int maxY, int minZ, int maxZ) {}
|
||||
public record WorldBounds(int minX, int maxX, int minY, int maxY, int minZ, int maxZ) {}
|
||||
|
||||
final IrisObjectPlacement config;
|
||||
final IrisData rdata;
|
||||
final IObjectPlacer placer;
|
||||
final CarveResult caveResult;
|
||||
final RNG rng;
|
||||
final IrisObject self;
|
||||
final IrisObjectPlacementAlignment alignment;
|
||||
final int x;
|
||||
final int z;
|
||||
final int yv;
|
||||
final BiConsumer<BlockPosition, NativeBlockState> listener;
|
||||
|
||||
public PlacementContext(IrisObjectPlacement config, IrisData rdata, IObjectPlacer placer,
|
||||
CarveResult caveResult, RNG rng, IrisObject self, int x, int z, int yv,
|
||||
BiConsumer<BlockPosition, NativeBlockState> listener) {
|
||||
this(config, rdata, placer, caveResult, rng, self, IrisObjectPlacementAlignment.CENTER, x, z, yv, listener);
|
||||
}
|
||||
|
||||
public PlacementContext(IrisObjectPlacement config, IrisData rdata, IObjectPlacer placer,
|
||||
CarveResult caveResult, RNG rng, IrisObject self, IrisObjectPlacementAlignment alignment,
|
||||
int x, int z, int yv, BiConsumer<BlockPosition, NativeBlockState> listener) {
|
||||
this.placer = Objects.requireNonNull(placer, "Object placer is required.");
|
||||
this.config = Objects.requireNonNull(config, "Object placement config is required.");
|
||||
this.rng = Objects.requireNonNull(rng, "Object placement RNG is required.");
|
||||
this.rdata = Objects.requireNonNull(rdata, "Object placement data is required.");
|
||||
this.self = Objects.requireNonNull(self, "Object is required.");
|
||||
this.alignment = Objects.requireNonNull(alignment, "Object placement alignment is required.");
|
||||
this.caveResult = caveResult;
|
||||
this.x = x;
|
||||
this.z = z;
|
||||
this.yv = yv;
|
||||
this.listener = listener;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package art.arcane.iris.structure.object;
|
||||
|
||||
|
||||
@FunctionalInterface
|
||||
public interface PlacementFunction<C> {
|
||||
void execute(PlacementContext context, PlacementState state, C input);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package art.arcane.iris.structure.object;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Immutable plan. Placement performs no registration, graph traversal, or sorting. */
|
||||
public final class PlacementLifecycle {
|
||||
private final List<List<PlacementLifecycleBuilder.Step<?>>> stages;
|
||||
|
||||
PlacementLifecycle(List<PlacementLifecycleBuilder.Step<?>> ordered) {
|
||||
stages = PlacementStage.ALL.stream()
|
||||
.map(stage -> ordered.stream().filter(step -> step.stage() == stage).toList()).toList();
|
||||
}
|
||||
|
||||
/** The block pass and everything after it read one consistent object snapshot. */
|
||||
public PlacementState execute(PlacementContext context) {
|
||||
PlacementState state = new PlacementState(context);
|
||||
for (PlacementStage<Void> stage : PlacementStage.BEFORE_PASS) {
|
||||
run(stage, context, state, null);
|
||||
}
|
||||
if (state.isRejected()) {
|
||||
return state;
|
||||
}
|
||||
context.self.readLock.lock();
|
||||
try {
|
||||
run(PlacementStage.BLOCKS, context, state, this);
|
||||
run(PlacementStage.AFTER_BLOCKS, context, state, null);
|
||||
} finally {
|
||||
context.self.readLock.unlock();
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
public <C> void run(PlacementStage<C> stage, PlacementContext context, PlacementState state, C input) {
|
||||
for (PlacementLifecycleBuilder.Step<?> step : stages.get(stage.order)) {
|
||||
if (state.isRejected()) {
|
||||
return;
|
||||
}
|
||||
// Registration pairs each function with its typed stage.
|
||||
@SuppressWarnings("unchecked")
|
||||
PlacementFunction<C> function = (PlacementFunction<C>) step.function();
|
||||
function.execute(context, state, input);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 231 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user