mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-23 16:44:06 +00:00
Compare commits
98
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;
|
||||
}
|
||||
}
|
||||
+328
-71
@@ -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 (cells.count == 0) {
|
||||
return ClumpFootprint.EMPTY;
|
||||
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++) {
|
||||
cells.picks[cell] = rng.i(0, paletteSize);
|
||||
}
|
||||
}
|
||||
for (int cell = 0; cell < cells.count; 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];
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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++) {
|
||||
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));
|
||||
}
|
||||
return cached.matches(owner, latticeX, latticeZ) ? cached : null;
|
||||
Column column(Owner owner, int height, int latticeX, int latticeZ) {
|
||||
return column(columns, owner, height, latticeX, latticeZ);
|
||||
}
|
||||
|
||||
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 (!mutation.canopy() || !skipCanopy) {
|
||||
mutation.apply(delegate);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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;
|
||||
|
||||
+60
-37
@@ -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,44 +636,66 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
return maxObjectExtent;
|
||||
}
|
||||
|
||||
private int computePlacementRadius(KList<IrisObjectPlacement> placements, IrisData data,
|
||||
Map<String, IrisBlockVector> sizeCache,
|
||||
Set<String> warnedLargeObjects) {
|
||||
if (placements == null) {
|
||||
return MINIMUM_PLACEMENT_RADIUS;
|
||||
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;
|
||||
}
|
||||
if (placements.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
int radius = MINIMUM_PLACEMENT_RADIUS;
|
||||
for (IrisObjectPlacement placement : placements) {
|
||||
if (placement == null || placement.getPlace() == null) {
|
||||
continue;
|
||||
|
||||
private int radius(KList<IrisObjectPlacement> placements, boolean anchored) {
|
||||
if (placements == null) {
|
||||
return MINIMUM_PLACEMENT_RADIUS;
|
||||
}
|
||||
for (String objectKey : placement.getPlace()) {
|
||||
try {
|
||||
IrisBlockVector size = sizeCache.get(objectKey);
|
||||
if (size == null) {
|
||||
File file = data.getObjectLoader().findFile(objectKey);
|
||||
if (file == null) {
|
||||
if (placements.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
int radius = MINIMUM_PLACEMENT_RADIUS;
|
||||
for (IrisObjectPlacement placement : placements) {
|
||||
if (placement == null || placement.getPlace() == null) {
|
||||
continue;
|
||||
}
|
||||
PlacementReach.Profile profile = PlacementReach.profile(placement);
|
||||
double scale = placement.getMaximumScale(getDimension());
|
||||
for (String objectKey : placement.getPlace()) {
|
||||
try {
|
||||
PlacementReach.Extent extent = extent(objectKey);
|
||||
if (extent == null) {
|
||||
continue;
|
||||
}
|
||||
size = IrisObject.sampleSize(file);
|
||||
sizeCache.put(objectKey, size);
|
||||
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!");
|
||||
}
|
||||
radius = Math.max(radius, reach);
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
int reach = MantleObjectComponent.calculatePlacementReach(size, placement, placement.getMaximumScale(getDimension()));
|
||||
if (reach > 128 && warnedLargeObjects.add(objectKey)) {
|
||||
IrisLogging.warn("Floating 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 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;
|
||||
}
|
||||
return radius;
|
||||
}
|
||||
|
||||
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,
|
||||
() -> buildSourcePlan(writer, sourceX, sourceZ, context));
|
||||
if (plan != null) {
|
||||
transaction.apply(plan);
|
||||
plan.persistContinuations(writer, sourceX, sourceZ, x, z);
|
||||
}
|
||||
}
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
MatterComponentTask existing = IN_FLIGHT_COMPONENTS.putIfAbsent(key, task);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
try {
|
||||
if (DISPATCHER.ownsCurrentThread()) {
|
||||
if (submissions == null) {
|
||||
MatterComponentTask existing = IN_FLIGHT_COMPONENTS.putIfAbsent(key, task);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
try {
|
||||
// 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,17 +670,19 @@ 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(
|
||||
callerContext.getEngine(),
|
||||
callerContext.getGenerationSessionId(),
|
||||
callerContext.getChunkContext())) {
|
||||
try (IrisEngine.GenerationRuntimeScope runtimeScope = openGenerationRuntime(generationRuntime);
|
||||
IrisContext.Scope contextScope = callerContext == null ? null : IrisContext.open(
|
||||
callerContext.getEngine(),
|
||||
callerContext.getGenerationSessionId(),
|
||||
callerContext.getChunkContext())) {
|
||||
runComponentInline(chunk, component, writer, chunkX, chunkZ, context);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
+114
-79
@@ -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);
|
||||
if (tile != null) {
|
||||
originals.putIfAbsent(new DataKey(
|
||||
mutation.key().x(), mutation.key().y(), mutation.key().z(), TileWrapper.class), tile);
|
||||
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) {
|
||||
journal.capture(x, y, 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
mutation.apply(writer);
|
||||
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,22 +444,44 @@ 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();
|
||||
try {
|
||||
writer.restoreData(key.x(), key.y(), key.z(), key.type(),
|
||||
entry.getValue() == CLEARED ? null : entry.getValue());
|
||||
} catch (Throwable rollbackFailure) {
|
||||
if (rollbackFailure != failure) {
|
||||
failure.addSuppressed(rollbackFailure);
|
||||
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(original.x(), original.y(), original.z(), original.type(),
|
||||
original.value() == CLEARED ? null : original.value());
|
||||
} catch (Throwable rollbackFailure) {
|
||||
if (rollbackFailure != failure) {
|
||||
failure.addSuppressed(rollbackFailure);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record Original(int x, int y, int z, Class<?> type, Object value) {
|
||||
}
|
||||
|
||||
private static final class OverlayCell {
|
||||
private Object block;
|
||||
private Object hydrology;
|
||||
@@ -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;
|
||||
int enabledChildren = 0;
|
||||
for (IrisNoiseGenerator child : fracture) {
|
||||
if (child.isEnabled()) {
|
||||
enabledChildren++;
|
||||
}
|
||||
}
|
||||
DoubleBinaryOperator[] children = new DoubleBinaryOperator[fracture.size()];
|
||||
double[] offsets = new double[fracture.size()];
|
||||
NoiseLayerProgram[] children = new NoiseLayerProgram[enabledChildren];
|
||||
double[] offsets = new double[enabledChildren];
|
||||
int salt = 33;
|
||||
for (int index = 0; index < children.length; index++) {
|
||||
IrisNoiseGenerator child = fracture.get(index);
|
||||
int compiled = 0;
|
||||
for (IrisNoiseGenerator child : fracture) {
|
||||
if (child.isEnabled()) {
|
||||
children[index] = child.prepareBatch(superSeed + seed + salt, data, engine);
|
||||
offsets[index] = child.getOpacity() / 2D;
|
||||
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,13 +1065,7 @@ 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);
|
||||
}
|
||||
return Optional.ofNullable(generationHistoryRuntimeRouter);
|
||||
}
|
||||
|
||||
public GenerationHistoryRuntimeRouter.CoordinateScope openGenerationHistoryCoordinateScope(
|
||||
@@ -1028,19 +1076,24 @@ public class IrisEngine implements Engine {
|
||||
if (router != null) {
|
||||
return router.openCoordinateScope(blockX, blockZ);
|
||||
}
|
||||
synchronized (generationHistoryRuntimeRouterLock) {
|
||||
router = generationHistoryRuntimeRouter;
|
||||
if (router == null && generationHistoryRoutingRequired) {
|
||||
throw new IllegalStateException("Iris generation-history runtime router is detached.");
|
||||
}
|
||||
if (!generationHistoryRoutingRequired) {
|
||||
return null;
|
||||
}
|
||||
return router == null ? null : router.openCoordinateScope(blockX, blockZ);
|
||||
router = generationHistoryRuntimeRouter;
|
||||
if (router == null) {
|
||||
throw new IllegalStateException("Iris generation-history runtime router is detached.");
|
||||
}
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
WallPainter painter = new WallPainter(output, terrain, resolverState, caveBiomeCache, customBiomeCache,
|
||||
surfaceHeights, getData(), chunkBlockX, chunkBlockZ);
|
||||
if (!scratch.customCaveBiomePresent) {
|
||||
paintInternalWalls(painter, scratch.wallMasks, boundaryMasks);
|
||||
}
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
if (rx > 0 && !columnMasks[columnIndex - 16].contains(yy)) {
|
||||
walls.put(rx - 1, 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);
|
||||
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)) {
|
||||
processZone(output, terrain, zone, rx, rz, worldX, worldZ, resolverState,
|
||||
caveBiomeCache, customBiomeCache);
|
||||
}
|
||||
zone = new CaveZone();
|
||||
zone.setFloor(y);
|
||||
buf = y;
|
||||
}
|
||||
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(floor);
|
||||
zone.setCeiling(Math.min(end, height));
|
||||
if (zone.isValid(height)) {
|
||||
processZone(output, terrain, zone, rx, rz, worldX, worldZ, resolverState,
|
||||
caveBiomeCache, customBiomeCache, layers);
|
||||
}
|
||||
|
||||
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,30 +89,68 @@ 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++;
|
||||
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;
|
||||
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] = prepareSlice(plans, first, last, x, z, context, highestSurface),
|
||||
preparation, index));
|
||||
}
|
||||
burst.complete();
|
||||
Throwable failure = preparation.takeFailure(null);
|
||||
if (failure instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
if (failure instanceof RuntimeException exception) {
|
||||
throw exception;
|
||||
}
|
||||
if (failure != null) {
|
||||
throw new IllegalStateException("Deposit preparation failed", failure);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
completeBatches(burst, prepared, chunk, terrain, x, z, preparation);
|
||||
} catch (Throwable failure) {
|
||||
callerFailure = failure;
|
||||
throw failure;
|
||||
@@ -119,7 +159,7 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
||||
// submitted burst tasks must never write into a released chunk.
|
||||
try {
|
||||
burst.complete();
|
||||
if (callerFailure != null) {
|
||||
if (callerFailure != null && preparation != null) {
|
||||
preparation.takeFailure(callerFailure);
|
||||
}
|
||||
} finally {
|
||||
@@ -128,28 +168,22 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
throw error;
|
||||
}
|
||||
if (failure instanceof RuntimeException exception) {
|
||||
throw exception;
|
||||
}
|
||||
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());
|
||||
}
|
||||
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,43 +336,34 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
|
||||
continue;
|
||||
}
|
||||
|
||||
if (oreDeposit) {
|
||||
if (depositBiome != null) {
|
||||
double frequencyMultiplier = depositBiome.getOreDepositFrequencyMultiplier();
|
||||
if (frequencyMultiplier < 1D
|
||||
&& !passesOreFrequency(frequencyMultiplier, clumpRng.d())) {
|
||||
continue;
|
||||
}
|
||||
if (oreDeposit && depositBiome != null) {
|
||||
double frequencyMultiplier = depositBiome.getOreDepositFrequencyMultiplier();
|
||||
if (frequencyMultiplier < 1D
|
||||
&& !passesOreFrequency(frequencyMultiplier, clumpRng.d())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
double sizeMultiplier = depositBiome.getOreDepositSizeMultiplier();
|
||||
if (sizeMultiplier != 1D) {
|
||||
IrisObject scaledClump = k.getClump(getEngine(), clumpRng, getData(), sizeMultiplier);
|
||||
int scaledDimension = scaledClump.getW();
|
||||
x = clampDepositCenter(x, scaledDimension, 16);
|
||||
if (clippedHeight) {
|
||||
y = clampDepositCenter(y, scaledDimension, getEngine().getHeight());
|
||||
}
|
||||
z = clampDepositCenter(z, scaledDimension, 16);
|
||||
clump = scaledClump;
|
||||
double sizeMultiplier = depositBiome.getOreDepositSizeMultiplier();
|
||||
if (sizeMultiplier != 1D) {
|
||||
IrisObject scaledClump = k.getClump(getEngine(), clumpRng, getData(), sizeMultiplier);
|
||||
int scaledDimension = scaledClump.getW();
|
||||
x = clampDepositCenter(x, scaledDimension, 16);
|
||||
if (clippedHeight) {
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
RNG replay = new RNG(clumpSeed);
|
||||
replay.d();
|
||||
clump = k.getClump(getEngine(), replay, getData());
|
||||
if (clump != null) {
|
||||
clumps.add(new PreparedClump(clump, null, x, y, z, clumpRng));
|
||||
continue;
|
||||
}
|
||||
|
||||
clumps.add(new PreparedClump(clump, x, y, z, clumpRng));
|
||||
if (footprint.empty() || footprintLimited && y + footprint.minY() > terrainLimit) {
|
||||
continue;
|
||||
}
|
||||
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,67 +88,94 @@ 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();
|
||||
for (int z = 0; z < 16; z++) {
|
||||
boolean changed;
|
||||
do {
|
||||
surfaces.clear();
|
||||
int top = getHeight(output, x, z);
|
||||
boolean inside = true;
|
||||
surfaces.add(top);
|
||||
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());
|
||||
}
|
||||
boolean now = block != null && !(B.isAir(block) || B.isFluid(block));
|
||||
if (now != inside) {
|
||||
inside = now;
|
||||
if (inside) {
|
||||
surfaces.add(y);
|
||||
}
|
||||
}
|
||||
}
|
||||
changed = false;
|
||||
for (int index = 0; index < surfaces.size(); index++) {
|
||||
int y = surfaces.getInt(index);
|
||||
NativeBlockState tip = output.get(x, y, z);
|
||||
if (!B.isDecorant(tip)) {
|
||||
continue;
|
||||
}
|
||||
NativeBlockState below = output.get(x, y - 1, z);
|
||||
boolean remove = below == null || !B.canPlaceOnto(tip, below);
|
||||
boolean removeBelow = false;
|
||||
if (!remove && IrisProceduralBlocks.hasProperty(below, "half")) {
|
||||
NativeBlockState support = output.get(x, y - 2, z);
|
||||
if (support == null || !B.canPlaceOnto(below, support)) {
|
||||
remove = true;
|
||||
removeBelow = true;
|
||||
}
|
||||
}
|
||||
if (remove) {
|
||||
changed = true;
|
||||
output.set(x, y, z, AIR.get());
|
||||
if (removeBelow) {
|
||||
output.set(x, y - 1, z, AIR.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (changed);
|
||||
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++) {
|
||||
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, 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);
|
||||
int flags = 0;
|
||||
if (block != null) {
|
||||
if (block != classified) {
|
||||
classified = block;
|
||||
classifiedFlags = states.of(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) {
|
||||
surfaces.add(y);
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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);
|
||||
if (!B.isDecorant(tip)) {
|
||||
continue;
|
||||
}
|
||||
NativeBlockState below = output.get(x, y - 1, z);
|
||||
boolean remove = below == null || !B.canPlaceOnto(tip, below);
|
||||
boolean removeBelow = false;
|
||||
if (!remove && IrisProceduralBlocks.hasProperty(below, "half")) {
|
||||
NativeBlockState support = output.get(x, y - 2, z);
|
||||
if (support == null || !B.canPlaceOnto(below, support)) {
|
||||
remove = true;
|
||||
removeBelow = true;
|
||||
}
|
||||
}
|
||||
if (remove) {
|
||||
changed = true;
|
||||
output.set(x, y, z, air);
|
||||
if (removeBelow) {
|
||||
output.set(x, y - 1, z, air);
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
static NativeBlockState normalizeSpike(NativeBlockState state, Hunk<NativeBlockState> output,
|
||||
int x, int z, int y, NativeBlockState air, NativeBlockState water) {
|
||||
if (IrisSpeleothems.isSupported(state, output, x, z, y)) {
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+33
-6
@@ -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) {
|
||||
if (air == null) {
|
||||
air = IrisPlatforms.get().registries().air();
|
||||
}
|
||||
state = air;
|
||||
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();
|
||||
}
|
||||
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(tile.plans(), x, y, z);
|
||||
return sample(candidates(Math.floorDiv(x, 16), Math.floorDiv(z, 16)), 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();
|
||||
}
|
||||
|
||||
+60
-43
@@ -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,12 +181,10 @@ 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;
|
||||
}
|
||||
KList<NativeStructureVolume> cached = originCache.get(
|
||||
new RuntimeChunkKey(pinnedRuntimeId, chunkKey(chunkX, chunkZ)));
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
return originVolumes(engine, chunkX, chunkZ);
|
||||
@@ -210,27 +209,21 @@ 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) {
|
||||
queryBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||
queryCache.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||
}
|
||||
originBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||
originCache.evictRuntime(runtimeId);
|
||||
queryBuilds.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);
|
||||
if (hit != null) {
|
||||
return hit;
|
||||
}
|
||||
KList<NativeStructureVolume> hit = volumes.get(key);
|
||||
if (hit != null) {
|
||||
return hit;
|
||||
}
|
||||
|
||||
CompletableFuture<KList<NativeStructureVolume>> future = new CompletableFuture<>();
|
||||
@@ -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);
|
||||
if (published != null) {
|
||||
future.complete(published);
|
||||
builds.remove(key, future);
|
||||
return published;
|
||||
}
|
||||
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;
|
||||
|
||||
+304
-346
@@ -30,7 +30,6 @@ import art.arcane.iris.generation.decoration.IrisVacuumSettings;
|
||||
import art.arcane.iris.world.entity.IrisMarker;
|
||||
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.runtime.GenerationFailures;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
||||
@@ -52,6 +51,7 @@ import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
@@ -475,127 +475,53 @@ final class IrisObjectPlacementRunner {
|
||||
int vacuumLowest = Integer.MAX_VALUE;
|
||||
int vacuumHighest = Integer.MIN_VALUE;
|
||||
self.readLock.lock();
|
||||
|
||||
KMap<IrisBlockVector, String> markers = null;
|
||||
|
||||
try {
|
||||
VectorMap<NativeBlockState> blocks = self.blocks;
|
||||
VectorMap<TileData> states = self.states;
|
||||
TreeGrounding tree = self.getTreeGrounding();
|
||||
// Zero-tile objects are the common case: skip the two Key allocations VectorMap#get needs to prove null.
|
||||
boolean hasStates = !states.isEmpty();
|
||||
ObjectPlacementBake bake = ObjectPlacementBake.of(self, spin, ceilingHang, translating ? translateOffset : null);
|
||||
PlacementStates palette = new PlacementStates(bake.palette(), config.getRotation(), spinx, spiny, spinz,
|
||||
preventingDecay, placer.isDebugSmartBore(), excludedStiltMaterials);
|
||||
double[] exact = warped || hasEdits || eroding ? bake.exact(self, spin) : null;
|
||||
String[] markers = config.getMarkers().isNotEmpty() && placer.getEngine() != null
|
||||
? resolveMarkers(config, rng, x, yv, z, rdata, bake) : null;
|
||||
int[] tileBlocks = bake.tileBlocks();
|
||||
TileData[] tileValues = bake.tileValues();
|
||||
int tileCursor = 0;
|
||||
int count = bake.count();
|
||||
|
||||
if (config.getMarkers().isNotEmpty() && placer.getEngine() != null) {
|
||||
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 = rng.nextParallelRNG(((((long) x) << 32) | (z & 0xFFFFFFFFL)) * 31L + 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(rdata)) {
|
||||
if (max <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (j.isExact() ? k.matches(data) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(data))) {
|
||||
boolean a = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 1, 0)));
|
||||
boolean fff = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 2, 0)));
|
||||
|
||||
if (!marker.isEmptyAbove() || (a && fff)) {
|
||||
markers.put(i, j.getMarker());
|
||||
max--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VectorMap<NativeBlockState>.Cursor cursor = blocks.cursor();
|
||||
while (cursor.next()) {
|
||||
IrisBlockVector g = cursor.key();
|
||||
NativeBlockState d;
|
||||
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++];
|
||||
}
|
||||
|
||||
try {
|
||||
d = cursor.value();
|
||||
if (hasStates) {
|
||||
tile = states.get(g);
|
||||
if (stilting && palette.stiltLayer(entry)) {
|
||||
int layer = bake.layerY(index);
|
||||
if (layer < lowest) {
|
||||
lowest = layer;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (cme)");
|
||||
d = IrisObject.States.air();
|
||||
}
|
||||
|
||||
if (d == null) {
|
||||
IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (null)");
|
||||
d = IrisObject.States.air();
|
||||
}
|
||||
|
||||
if (placer.isDebugSmartBore() && IrisObject.States.vair().equals(d)) {
|
||||
d = IrisObject.States.vairDebug();
|
||||
}
|
||||
|
||||
NativeBlockState data = d;
|
||||
IrisBlockVector i = g.clone();
|
||||
spin.rotate(i);
|
||||
if (ceilingHang) {
|
||||
i.setY(-i.getBlockY());
|
||||
}
|
||||
if (translating) {
|
||||
i.add(translateOffset);
|
||||
}
|
||||
|
||||
if (stilting && IrisObjectShaping.isStiltLayerBlock(data)) {
|
||||
if (i.getBlockY() < lowest) {
|
||||
lowest = i.getBlockY();
|
||||
}
|
||||
if (i.getBlockY() > topLayer) {
|
||||
topLayer = i.getBlockY();
|
||||
if (layer > topLayer) {
|
||||
topLayer = layer;
|
||||
}
|
||||
}
|
||||
|
||||
if (preventingDecay && IrisProceduralBlocks.hasProperty(data, "distance") && "false".equals(IrisProceduralBlocks.propertyValue(data, "persistent"))) {
|
||||
data = data.withProperty("persistent", "true");
|
||||
}
|
||||
|
||||
NativeBlockState data = palette.decayed(entry);
|
||||
boolean edited = false;
|
||||
if (hasEdits) {
|
||||
double exactX = exact[index * 3];
|
||||
double exactY = exact[index * 3 + 1];
|
||||
double exactZ = exact[index * 3 + 2];
|
||||
for (IrisObjectReplace j : config.getEdit()) {
|
||||
if (rng.chance(j.getChance())) {
|
||||
for (NativeBlockState k : j.getFind(rdata)) {
|
||||
if (j.isExact() ? k.matches(data) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(data))) {
|
||||
NativeBlockState newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata);
|
||||
NativeBlockState newData = j.getReplace(rng, exactX + x, exactY + y, exactZ + z, 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(rng, x, y, z, rdata);
|
||||
if (t.isPresent()) {
|
||||
@@ -609,18 +535,21 @@ final class IrisObjectPlacementRunner {
|
||||
}
|
||||
}
|
||||
|
||||
data = config.getRotation().rotate(data, spinx, spiny, spinz);
|
||||
if (data == null) {
|
||||
StateFacts facts = edited ? palette.factsOf(data) : palette.facts(entry);
|
||||
NativeBlockState rotated = facts.rotated();
|
||||
if (rotated == null) {
|
||||
continue;
|
||||
}
|
||||
xx = x + (int) Math.round(i.getX());
|
||||
|
||||
int yy = y + (int) Math.round(i.getY());
|
||||
zz = z + (int) Math.round(i.getZ());
|
||||
xx = x + bake.offsetX(index);
|
||||
int yy = y + bake.offsetY(index);
|
||||
zz = z + bake.offsetZ(index);
|
||||
|
||||
if (warped) {
|
||||
xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getX() + x, i.getY() + y, i.getZ() + z);
|
||||
zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getZ() + z, i.getY() + y, i.getX() + x);
|
||||
double exactX = exact[index * 3];
|
||||
double exactY = exact[index * 3 + 1];
|
||||
double exactZ = exact[index * 3 + 2];
|
||||
xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, exactX + x, exactY + y, exactZ + z);
|
||||
zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, exactZ + z, exactY + y, exactX + x);
|
||||
}
|
||||
|
||||
if (paint) {
|
||||
@@ -628,8 +557,8 @@ final class IrisObjectPlacementRunner {
|
||||
if (surface == IrisPaintSurfaceProjection.MISSING) {
|
||||
continue;
|
||||
}
|
||||
if (!B.isVineBlock(data)) {
|
||||
yy = (int) Math.round(i.getY()) + Math.floorDiv(self.h, 2) + surface + yrand;
|
||||
if (!B.isVineBlock(rotated)) {
|
||||
yy = bake.offsetY(index) + Math.floorDiv(self.h, 2) + surface + yrand;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -637,13 +566,18 @@ final class IrisObjectPlacementRunner {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean canopy = tree != null && tree.isCanopy(g.getBlockX(), g.getBlockY(), g.getBlockZ());
|
||||
boolean canopy = bake.canopy(index);
|
||||
if (canopy && !placer.beginTreeCanopy(new BlockPosition(xx, yy, zz))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
data = normalizePlacedWaterlogging(data, placer, xx, yy, zz, waterlogCandidate);
|
||||
data = rotated;
|
||||
if (facts.waterloggable()) {
|
||||
data = waterlogCandidate || facts.authoredWaterlogged()
|
||||
? IrisProceduralBlocks.normalizeWaterlogging(data, placer.get(xx, yy, zz), waterlogCandidate)
|
||||
: facts.dry();
|
||||
}
|
||||
|
||||
if (!rawStructurePiece && B.isVineBlock(data)) {
|
||||
data = attachVineFaces(placer, data, xx, yy, zz);
|
||||
@@ -652,9 +586,7 @@ final class IrisObjectPlacementRunner {
|
||||
// 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 = !rawStructurePiece && B.isVineBlock(data) && B.isSolid(placer.get(xx, yy, zz));
|
||||
String material = IrisObjectShaping.materialKey(data);
|
||||
boolean air = material.equals("minecraft:air") || material.equals("minecraft:cave_air");
|
||||
boolean place = shouldPlaceObjectBlock(rawStructurePiece, air, wouldReplace);
|
||||
boolean place = shouldPlaceObjectBlock(rawStructurePiece, facts.air(), wouldReplace);
|
||||
|
||||
if (data.isCustom() || place) {
|
||||
placer.set(xx, yy, zz, data);
|
||||
@@ -668,8 +600,8 @@ final class IrisObjectPlacementRunner {
|
||||
if (tile != null) {
|
||||
placer.setTile(xx, yy, zz, tile);
|
||||
}
|
||||
if (markers != null && markers.containsKey(g)) {
|
||||
placer.setData(xx, yy, zz, new MatterMarker(markers.get(g)));
|
||||
if (markers != null && markers[index] != null) {
|
||||
placer.setData(xx, yy, zz, new MatterMarker(markers[index]));
|
||||
}
|
||||
if (listener != null) {
|
||||
listener.accept(new BlockPosition(xx, yy, zz), data);
|
||||
@@ -687,27 +619,21 @@ final class IrisObjectPlacementRunner {
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
self.readLock.unlock();
|
||||
}
|
||||
|
||||
if (grounding != null) {
|
||||
for (TreeGrounding.Extension extension : grounding.extensions()) {
|
||||
for (int rootY = extension.topY(); rootY >= extension.bottomY(); rootY--) {
|
||||
NativeBlockState rootState = normalizePlacedWaterlogging(extension.state(), placer,
|
||||
extension.x(), rootY, extension.z(), waterlogCandidate);
|
||||
placer.set(extension.x(), rootY, extension.z(), rootState);
|
||||
if (listener != null) {
|
||||
listener.accept(new BlockPosition(extension.x(), rootY, extension.z()), rootState);
|
||||
if (grounding != null) {
|
||||
for (TreeGrounding.Extension extension : grounding.extensions()) {
|
||||
for (int rootY = extension.topY(); rootY >= extension.bottomY(); rootY--) {
|
||||
NativeBlockState rootState = normalizePlacedWaterlogging(extension.state(), placer,
|
||||
extension.x(), rootY, extension.z(), waterlogCandidate);
|
||||
placer.set(extension.x(), rootY, extension.z(), rootState);
|
||||
if (listener != null) {
|
||||
listener.accept(new BlockPosition(extension.x(), rootY, extension.z()), rootState);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stilting) {
|
||||
self.readLock.lock();
|
||||
try {
|
||||
VectorMap<NativeBlockState> blocks = self.blocks;
|
||||
if (stilting) {
|
||||
IrisStiltSettings settings = stiltSettings;
|
||||
|
||||
double erodeCentroidX = 0;
|
||||
@@ -715,93 +641,51 @@ final class IrisObjectPlacementRunner {
|
||||
double erodeMaxDist = 1;
|
||||
if (eroding) {
|
||||
int centroidCount = 0;
|
||||
VectorMap<NativeBlockState>.Cursor centroidCursor = blocks.cursor();
|
||||
while (centroidCursor.next()) {
|
||||
IrisBlockVector rot = centroidCursor.key().clone();
|
||||
spin.rotate(rot);
|
||||
if (translating) {
|
||||
rot.add(translateOffset);
|
||||
}
|
||||
if (rot.getBlockY() == lowest) {
|
||||
NativeBlockState bd = centroidCursor.value();
|
||||
if (bd != null && IrisObjectShaping.shouldStilt(bd, excludedStiltMaterials)) {
|
||||
erodeCentroidX += rot.getX();
|
||||
erodeCentroidZ += rot.getZ();
|
||||
centroidCount++;
|
||||
}
|
||||
for (int index = 0; index < count; index++) {
|
||||
if (bake.layerY(index) == lowest && palette.shouldStilt(bake.paletteIndex(index))) {
|
||||
erodeCentroidX += exact[index * 3];
|
||||
erodeCentroidZ += exact[index * 3 + 2];
|
||||
centroidCount++;
|
||||
}
|
||||
}
|
||||
if (centroidCount > 0) {
|
||||
erodeCentroidX /= centroidCount;
|
||||
erodeCentroidZ /= centroidCount;
|
||||
}
|
||||
VectorMap<NativeBlockState>.Cursor spreadCursor = blocks.cursor();
|
||||
while (spreadCursor.next()) {
|
||||
IrisBlockVector rot = spreadCursor.key().clone();
|
||||
spin.rotate(rot);
|
||||
if (translating) {
|
||||
rot.add(translateOffset);
|
||||
}
|
||||
if (rot.getBlockY() == lowest) {
|
||||
NativeBlockState bd = spreadCursor.value();
|
||||
if (bd != null && IrisObjectShaping.shouldStilt(bd, excludedStiltMaterials)) {
|
||||
double dx = rot.getX() - erodeCentroidX;
|
||||
double dz = rot.getZ() - erodeCentroidZ;
|
||||
double dist = Math.sqrt(dx * dx + dz * dz);
|
||||
if (dist > erodeMaxDist) {
|
||||
erodeMaxDist = dist;
|
||||
}
|
||||
for (int index = 0; index < count; index++) {
|
||||
if (bake.layerY(index) == lowest && palette.shouldStilt(bake.paletteIndex(index))) {
|
||||
double dx = exact[index * 3] - erodeCentroidX;
|
||||
double dz = exact[index * 3 + 2] - erodeCentroidZ;
|
||||
double dist = Math.sqrt(dx * dx + dz * dz);
|
||||
if (dist > erodeMaxDist) {
|
||||
erodeMaxDist = dist;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VectorMap<NativeBlockState>.Cursor stiltCursor = blocks.cursor();
|
||||
while (stiltCursor.next()) {
|
||||
IrisBlockVector g = stiltCursor.key();
|
||||
NativeBlockState sourceData;
|
||||
try {
|
||||
sourceData = stiltCursor.value();
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (stilt cme)");
|
||||
sourceData = IrisObject.States.air();
|
||||
}
|
||||
|
||||
if (sourceData == null) {
|
||||
IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (stilt null)");
|
||||
sourceData = IrisObject.States.air();
|
||||
}
|
||||
|
||||
if (!IrisObjectShaping.shouldStilt(sourceData, excludedStiltMaterials)) {
|
||||
int targetLayer = ceilingHang ? topLayer : lowest;
|
||||
for (int index = 0; index < count; index++) {
|
||||
int entry = bake.paletteIndex(index);
|
||||
if (!palette.shouldStilt(entry)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NativeBlockState d = sourceData;
|
||||
NativeBlockState d;
|
||||
if (settings != null && settings.getPalette() != null) {
|
||||
d = config.getStiltSettings().getPalette().get(rng, x, y, z, rdata);
|
||||
} else {
|
||||
String mat = IrisObjectShaping.materialKey(d);
|
||||
if (mat.equals("minecraft:grass_block") || mat.equals("minecraft:mycelium") || mat.equals("minecraft:podzol") || mat.equals("minecraft:dirt_path")) {
|
||||
d = IrisObject.States.dirt();
|
||||
}
|
||||
d = palette.stiltDefault(entry);
|
||||
}
|
||||
|
||||
IrisBlockVector i = g.clone();
|
||||
spin.rotate(i);
|
||||
if (ceilingHang) {
|
||||
i.setY(-i.getBlockY());
|
||||
}
|
||||
if (translating) {
|
||||
i.add(translateOffset);
|
||||
}
|
||||
int targetLayer = ceilingHang ? topLayer : lowest;
|
||||
if (i.getBlockY() != targetLayer)
|
||||
if (bake.layerY(index) != targetLayer)
|
||||
continue;
|
||||
d = config.getRotation().rotate(d, spinx, spiny, spinz);
|
||||
d = palette.rotate(d);
|
||||
|
||||
if (hasEdits) {
|
||||
double exactX = exact[index * 3];
|
||||
double exactY = exact[index * 3 + 1];
|
||||
double exactZ = exact[index * 3 + 2];
|
||||
for (IrisObjectReplace j : config.getEdit()) {
|
||||
if (rng.chance(j.getChance())) {
|
||||
for (NativeBlockState k : j.getFind(rdata)) {
|
||||
@@ -809,7 +693,7 @@ final class IrisObjectPlacementRunner {
|
||||
continue;
|
||||
}
|
||||
if (j.isExact() ? k.matches(d) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(d))) {
|
||||
NativeBlockState newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata);
|
||||
NativeBlockState newData = j.getReplace(rng, exactX + x, exactY + y, exactZ + z, rdata);
|
||||
|
||||
if (IrisObjectShaping.materialKey(newData).equals(IrisObjectShaping.materialKey(d))) {
|
||||
d = BlockDataMergeSupport.merge(d, newData);
|
||||
@@ -822,15 +706,18 @@ final class IrisObjectPlacementRunner {
|
||||
}
|
||||
}
|
||||
|
||||
if (d == null || !IrisObjectShaping.shouldStilt(d, excludedStiltMaterials))
|
||||
if (d == null || !palette.shouldStiltPlaced(d))
|
||||
continue;
|
||||
|
||||
xx = x + (int) Math.round(i.getX());
|
||||
zz = z + (int) Math.round(i.getZ());
|
||||
xx = x + bake.offsetX(index);
|
||||
zz = z + bake.offsetZ(index);
|
||||
|
||||
if (warped) {
|
||||
xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getX() + x, i.getY() + y, i.getZ() + z);
|
||||
zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getZ() + z, i.getY() + y, i.getX() + x);
|
||||
double exactX = exact[index * 3];
|
||||
double exactY = exact[index * 3 + 1];
|
||||
double exactZ = exact[index * 3 + 2];
|
||||
xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, exactX + x, exactY + y, exactZ + z);
|
||||
zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, exactZ + z, exactY + y, exactX + x);
|
||||
}
|
||||
|
||||
if (organic) {
|
||||
@@ -891,8 +778,6 @@ final class IrisObjectPlacementRunner {
|
||||
|
||||
int highest = placer.getHighest(xx, zz, self.getLoader(), true);
|
||||
|
||||
|
||||
|
||||
int lowerBound = highest - 1;
|
||||
if (settings != null) {
|
||||
lowerBound -= config.getStiltSettings().getOverStilt() - rng.i(0, config.getStiltSettings().getYRand());
|
||||
@@ -901,8 +786,8 @@ final class IrisObjectPlacementRunner {
|
||||
}
|
||||
|
||||
if (eroding) {
|
||||
double dx = i.getX() - erodeCentroidX;
|
||||
double dz = i.getZ() - erodeCentroidZ;
|
||||
double dx = exact[index * 3] - erodeCentroidX;
|
||||
double dz = exact[index * 3 + 2] - erodeCentroidZ;
|
||||
double normalizedDist = Math.sqrt(dx * dx + dz * dz) / erodeMaxDist;
|
||||
normalizedDist = Math.min(normalizedDist, 1.0);
|
||||
int totalDepth = (lowest + y) - lowerBound;
|
||||
@@ -933,11 +818,10 @@ final class IrisObjectPlacementRunner {
|
||||
}
|
||||
placer.set(xx, j, zz, normalizePlacedWaterlogging(d, placer, xx, j, zz, waterlogCandidate));
|
||||
}
|
||||
|
||||
}
|
||||
} finally {
|
||||
self.readLock.unlock();
|
||||
}
|
||||
} finally {
|
||||
self.readLock.unlock();
|
||||
}
|
||||
|
||||
if (vacuuming && vacuumLowest != Integer.MAX_VALUE && placer.getEngine() != null) {
|
||||
@@ -974,9 +858,76 @@ final class IrisObjectPlacementRunner {
|
||||
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);
|
||||
}
|
||||
|
||||
private String[] resolveMarkers(IrisObjectPlacement config, RNG rng, int x, int yv, int z, IrisData rdata,
|
||||
ObjectPlacementBake bake) {
|
||||
self.readLock.lock();
|
||||
try {
|
||||
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 = rng.nextParallelRNG(((((long) x) << 32) | (z & 0xFFFFFFFFL)) * 31L + 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(rdata)) {
|
||||
if (max <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (j.isExact() ? k.matches(data) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(data))) {
|
||||
boolean a = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 1, 0)));
|
||||
boolean fff = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 2, 0)));
|
||||
|
||||
if (!marker.isEmptyAbove() || (a && fff)) {
|
||||
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;
|
||||
} finally {
|
||||
self.readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private KList<IrisBlockVector> transformedPaintSupport(SpinKernel spin, IrisBlockVector translateOffset,
|
||||
CNG warp, double half, int x, int y, int z) {
|
||||
KList<IrisBlockVector> offsets = new KList<>();
|
||||
@@ -1337,152 +1288,159 @@ final class IrisObjectPlacementRunner {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Precomputed vector rotation for a single placement.
|
||||
* <p>
|
||||
* This mirrors {@link IrisObjectRotation#rotate(IrisBlockVector, int, int, int)} branch for branch, with two
|
||||
* differences: it mutates the vector handed to it instead of cloning, and the per-axis angle plus its cosine
|
||||
* and sine are resolved once instead of once per block. The angle is a pure function of the axis clamp and the
|
||||
* spin, both fixed for the placement, and Math.cos/Math.sin are pure, so every produced double is identical to
|
||||
* the value the per-call form would have produced.
|
||||
*/
|
||||
private static final class SpinKernel {
|
||||
private static final int MODE_NONE = 0;
|
||||
private static final int MODE_FLIP = 1;
|
||||
private static final int MODE_QUARTER = 2;
|
||||
private static final int MODE_THREE_QUARTER = 3;
|
||||
private static final int MODE_ANGLE = 4;
|
||||
private record StateFacts(NativeBlockState rotated, boolean waterloggable, boolean authoredWaterlogged,
|
||||
NativeBlockState dry, boolean air) {
|
||||
private static final StateFacts ROTATED_AWAY = new StateFacts(null, false, false, null, false);
|
||||
}
|
||||
|
||||
private final boolean rotates;
|
||||
private final int xMode;
|
||||
private final int yMode;
|
||||
private final int zMode;
|
||||
private final double xCos;
|
||||
private final double xSin;
|
||||
private final double yCos;
|
||||
private final double ySin;
|
||||
private final double zCos;
|
||||
private final double zSin;
|
||||
private static final class PlacementStates {
|
||||
private static final Object ROTATED_AWAY = new Object();
|
||||
private static final byte UNKNOWN = 0;
|
||||
private static final byte NO = 1;
|
||||
private static final byte YES = 2;
|
||||
|
||||
private SpinKernel(IrisObjectRotation rotation, int spinx, int spiny, int spinz) {
|
||||
rotates = rotation.canRotate();
|
||||
xMode = rotation.canRotateX() ? modeOf(rotation.getXAxis()) : MODE_NONE;
|
||||
zMode = rotation.canRotateZ() ? modeOf(rotation.getZAxis()) : MODE_NONE;
|
||||
yMode = rotation.canRotateY() ? modeOf(rotation.getYAxis()) : MODE_NONE;
|
||||
private final NativeBlockState[] palette;
|
||||
private final IrisObjectRotation rotation;
|
||||
private final int spinx;
|
||||
private final int spiny;
|
||||
private final int spinz;
|
||||
private final boolean preventingDecay;
|
||||
private final boolean debugSmartBore;
|
||||
private final Set<String> excludedStiltMaterials;
|
||||
private final NativeBlockState[] decayed;
|
||||
private final StateFacts[] facts;
|
||||
private final byte[] stiltLayers;
|
||||
private final byte[] stilts;
|
||||
private final NativeBlockState[] stiltDefaults;
|
||||
private IdentityHashMap<NativeBlockState, Object> rotations;
|
||||
private IdentityHashMap<NativeBlockState, StateFacts> editedFacts;
|
||||
private IdentityHashMap<NativeBlockState, Boolean> placedStilts;
|
||||
|
||||
double xAngle = xMode == MODE_ANGLE ? rotation.getXRotation(spinx) : 0;
|
||||
double zAngle = zMode == MODE_ANGLE ? rotation.getZRotation(spinz) : 0;
|
||||
double yAngle = yMode == MODE_ANGLE ? rotation.getYRotation(spiny) : 0;
|
||||
xCos = Math.cos(xAngle);
|
||||
xSin = Math.sin(xAngle);
|
||||
zCos = Math.cos(zAngle);
|
||||
zSin = Math.sin(zAngle);
|
||||
yCos = Math.cos(yAngle);
|
||||
ySin = Math.sin(yAngle);
|
||||
private PlacementStates(NativeBlockState[] palette, IrisObjectRotation rotation, int spinx, int spiny, int spinz,
|
||||
boolean preventingDecay, boolean debugSmartBore, Set<String> excludedStiltMaterials) {
|
||||
this.palette = palette;
|
||||
this.rotation = rotation;
|
||||
this.spinx = spinx;
|
||||
this.spiny = spiny;
|
||||
this.spinz = spinz;
|
||||
this.preventingDecay = preventingDecay;
|
||||
this.debugSmartBore = debugSmartBore;
|
||||
this.excludedStiltMaterials = excludedStiltMaterials;
|
||||
this.decayed = new NativeBlockState[palette.length];
|
||||
this.facts = new StateFacts[palette.length];
|
||||
this.stiltLayers = new byte[palette.length];
|
||||
this.stilts = new byte[palette.length];
|
||||
this.stiltDefaults = new NativeBlockState[palette.length];
|
||||
}
|
||||
|
||||
private static int modeOf(IrisAxisRotationClamp clamp) {
|
||||
if (!clamp.isLocked()) {
|
||||
return MODE_ANGLE;
|
||||
}
|
||||
|
||||
if (Math.abs(clamp.getMax()) % 360D == 180D) {
|
||||
return MODE_FLIP;
|
||||
}
|
||||
|
||||
if (clamp.getMax() % 360D == 90D || clamp.getMax() % 360D == -270D) {
|
||||
return MODE_QUARTER;
|
||||
}
|
||||
|
||||
if (clamp.getMax() == -90D || clamp.getMax() % 360D == 270D) {
|
||||
return MODE_THREE_QUARTER;
|
||||
}
|
||||
|
||||
return MODE_ANGLE;
|
||||
private NativeBlockState source(int entry) {
|
||||
NativeBlockState state = palette[entry];
|
||||
return debugSmartBore && IrisObject.States.vair().equals(state) ? IrisObject.States.vairDebug() : state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates in place. Axis order (X, then Z, then Y) is load bearing.
|
||||
*/
|
||||
private void rotate(IrisBlockVector v) {
|
||||
if (!rotates) {
|
||||
return;
|
||||
private boolean stiltLayer(int entry) {
|
||||
byte known = stiltLayers[entry];
|
||||
if (known == UNKNOWN) {
|
||||
known = IrisObjectShaping.isStiltLayerBlock(source(entry)) ? YES : NO;
|
||||
stiltLayers[entry] = known;
|
||||
}
|
||||
return known == YES;
|
||||
}
|
||||
|
||||
switch (xMode) {
|
||||
case MODE_FLIP -> {
|
||||
v.setZ(-v.getZ());
|
||||
v.setY(-v.getY());
|
||||
}
|
||||
case MODE_QUARTER -> {
|
||||
double z = v.getZ();
|
||||
v.setZ(v.getY());
|
||||
v.setY(-z);
|
||||
}
|
||||
case MODE_THREE_QUARTER -> {
|
||||
double z = v.getZ();
|
||||
v.setZ(-v.getY());
|
||||
v.setY(z);
|
||||
}
|
||||
case MODE_ANGLE -> {
|
||||
double y = xCos * v.getY() - xSin * v.getZ();
|
||||
double z = xSin * v.getY() + xCos * v.getZ();
|
||||
v.setY(y);
|
||||
v.setZ(z);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
private NativeBlockState decayed(int entry) {
|
||||
NativeBlockState resolved = decayed[entry];
|
||||
if (resolved == null) {
|
||||
NativeBlockState data = source(entry);
|
||||
resolved = preventingDecay && IrisProceduralBlocks.hasProperty(data, "distance")
|
||||
&& "false".equals(IrisProceduralBlocks.propertyValue(data, "persistent"))
|
||||
? data.withProperty("persistent", "true") : data;
|
||||
decayed[entry] = resolved;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
switch (zMode) {
|
||||
case MODE_FLIP -> {
|
||||
v.setY(-v.getY());
|
||||
v.setX(-v.getX());
|
||||
}
|
||||
case MODE_QUARTER -> {
|
||||
double y = v.getY();
|
||||
v.setY(v.getX());
|
||||
v.setX(-y);
|
||||
}
|
||||
case MODE_THREE_QUARTER -> {
|
||||
double y = v.getY();
|
||||
v.setY(-v.getX());
|
||||
v.setX(y);
|
||||
}
|
||||
case MODE_ANGLE -> {
|
||||
double x = zCos * v.getX() - zSin * v.getY();
|
||||
double y = zSin * v.getX() + zCos * v.getY();
|
||||
v.setX(x);
|
||||
v.setY(y);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
private StateFacts facts(int entry) {
|
||||
StateFacts resolved = facts[entry];
|
||||
if (resolved == null) {
|
||||
resolved = computeFacts(decayed(entry));
|
||||
facts[entry] = resolved;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
switch (yMode) {
|
||||
case MODE_FLIP -> {
|
||||
v.setX(-v.getX());
|
||||
v.setZ(-v.getZ());
|
||||
}
|
||||
case MODE_QUARTER -> {
|
||||
double x = v.getX();
|
||||
v.setX(v.getZ());
|
||||
v.setZ(-x);
|
||||
}
|
||||
case MODE_THREE_QUARTER -> {
|
||||
double x = v.getX();
|
||||
v.setX(-v.getZ());
|
||||
v.setZ(x);
|
||||
}
|
||||
case MODE_ANGLE -> {
|
||||
double x = yCos * v.getX() + ySin * v.getZ();
|
||||
double z = -ySin * v.getX() + yCos * v.getZ();
|
||||
v.setX(x);
|
||||
v.setZ(z);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
private StateFacts factsOf(NativeBlockState state) {
|
||||
if (editedFacts == null) {
|
||||
editedFacts = new IdentityHashMap<>();
|
||||
}
|
||||
StateFacts resolved = editedFacts.get(state);
|
||||
if (resolved == null) {
|
||||
resolved = computeFacts(state);
|
||||
editedFacts.put(state, resolved);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private StateFacts computeFacts(NativeBlockState state) {
|
||||
NativeBlockState rotated = rotate(state);
|
||||
if (rotated == null) {
|
||||
return StateFacts.ROTATED_AWAY;
|
||||
}
|
||||
boolean waterloggable = IrisProceduralBlocks.hasProperty(rotated, "waterlogged");
|
||||
boolean authored = waterloggable && "true".equals(IrisProceduralBlocks.propertyValue(rotated, "waterlogged"));
|
||||
NativeBlockState dry = waterloggable ? IrisProceduralBlocks.normalizeWaterlogging(rotated, null, false) : rotated;
|
||||
String material = IrisObjectShaping.materialKey(rotated);
|
||||
boolean air = material.equals("minecraft:air") || material.equals("minecraft:cave_air");
|
||||
return new StateFacts(rotated, waterloggable, authored, dry, air);
|
||||
}
|
||||
|
||||
private NativeBlockState rotate(NativeBlockState state) {
|
||||
if (state == null) {
|
||||
return null;
|
||||
}
|
||||
if (rotations == null) {
|
||||
rotations = new IdentityHashMap<>();
|
||||
}
|
||||
Object memo = rotations.get(state);
|
||||
if (memo == null) {
|
||||
NativeBlockState rotated = rotation.rotate(state, spinx, spiny, spinz);
|
||||
memo = rotated == null ? ROTATED_AWAY : rotated;
|
||||
rotations.put(state, memo);
|
||||
}
|
||||
return memo == ROTATED_AWAY ? null : (NativeBlockState) memo;
|
||||
}
|
||||
|
||||
private boolean shouldStilt(int entry) {
|
||||
byte known = stilts[entry];
|
||||
if (known == UNKNOWN) {
|
||||
known = IrisObjectShaping.shouldStilt(palette[entry], excludedStiltMaterials) ? YES : NO;
|
||||
stilts[entry] = known;
|
||||
}
|
||||
return known == YES;
|
||||
}
|
||||
|
||||
private NativeBlockState stiltDefault(int entry) {
|
||||
NativeBlockState resolved = stiltDefaults[entry];
|
||||
if (resolved == null) {
|
||||
NativeBlockState state = palette[entry];
|
||||
String material = IrisObjectShaping.materialKey(state);
|
||||
resolved = material.equals("minecraft:grass_block") || material.equals("minecraft:mycelium")
|
||||
|| material.equals("minecraft:podzol") || material.equals("minecraft:dirt_path")
|
||||
? IrisObject.States.dirt() : state;
|
||||
stiltDefaults[entry] = resolved;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private boolean shouldStiltPlaced(NativeBlockState state) {
|
||||
if (placedStilts == null) {
|
||||
placedStilts = new IdentityHashMap<>();
|
||||
}
|
||||
Boolean known = placedStilts.get(state);
|
||||
if (known == null) {
|
||||
known = IrisObjectShaping.shouldStilt(state, excludedStiltMaterials);
|
||||
placedStilts.put(state, known);
|
||||
}
|
||||
return known;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,152 @@
|
||||
package art.arcane.iris.structure.object;
|
||||
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
|
||||
/**
|
||||
* Precomputed vector rotation for a single placement.
|
||||
* <p>
|
||||
* This mirrors {@link IrisObjectRotation#rotate(IrisBlockVector, int, int, int)} branch for branch, with two
|
||||
* differences: it mutates the vector handed to it instead of cloning, and the per-axis angle plus its cosine
|
||||
* and sine are resolved once instead of once per block. The angle is a pure function of the axis clamp and the
|
||||
* spin, both fixed for the placement, and Math.cos/Math.sin are pure, so every produced double is identical to
|
||||
* the value the per-call form would have produced.
|
||||
*/
|
||||
final class SpinKernel {
|
||||
static final int MODE_NONE = 0;
|
||||
static final int MODE_FLIP = 1;
|
||||
static final int MODE_QUARTER = 2;
|
||||
static final int MODE_THREE_QUARTER = 3;
|
||||
static final int MODE_ANGLE = 4;
|
||||
|
||||
final boolean rotates;
|
||||
final int xMode;
|
||||
final int yMode;
|
||||
final int zMode;
|
||||
final double xCos;
|
||||
final double xSin;
|
||||
final double yCos;
|
||||
final double ySin;
|
||||
final double zCos;
|
||||
final double zSin;
|
||||
|
||||
SpinKernel(IrisObjectRotation rotation, int spinx, int spiny, int spinz) {
|
||||
rotates = rotation.canRotate();
|
||||
xMode = rotation.canRotateX() ? modeOf(rotation.getXAxis()) : MODE_NONE;
|
||||
zMode = rotation.canRotateZ() ? modeOf(rotation.getZAxis()) : MODE_NONE;
|
||||
yMode = rotation.canRotateY() ? modeOf(rotation.getYAxis()) : MODE_NONE;
|
||||
|
||||
double xAngle = xMode == MODE_ANGLE ? rotation.getXRotation(spinx) : 0;
|
||||
double zAngle = zMode == MODE_ANGLE ? rotation.getZRotation(spinz) : 0;
|
||||
double yAngle = yMode == MODE_ANGLE ? rotation.getYRotation(spiny) : 0;
|
||||
xCos = Math.cos(xAngle);
|
||||
xSin = Math.sin(xAngle);
|
||||
zCos = Math.cos(zAngle);
|
||||
zSin = Math.sin(zAngle);
|
||||
yCos = Math.cos(yAngle);
|
||||
ySin = Math.sin(yAngle);
|
||||
}
|
||||
|
||||
private static int modeOf(IrisAxisRotationClamp clamp) {
|
||||
if (!clamp.isLocked()) {
|
||||
return MODE_ANGLE;
|
||||
}
|
||||
|
||||
if (Math.abs(clamp.getMax()) % 360D == 180D) {
|
||||
return MODE_FLIP;
|
||||
}
|
||||
|
||||
if (clamp.getMax() % 360D == 90D || clamp.getMax() % 360D == -270D) {
|
||||
return MODE_QUARTER;
|
||||
}
|
||||
|
||||
if (clamp.getMax() == -90D || clamp.getMax() % 360D == 270D) {
|
||||
return MODE_THREE_QUARTER;
|
||||
}
|
||||
|
||||
return MODE_ANGLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates in place. Axis order (X, then Z, then Y) is load bearing.
|
||||
*/
|
||||
void rotate(IrisBlockVector v) {
|
||||
if (!rotates) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (xMode) {
|
||||
case MODE_FLIP -> {
|
||||
v.setZ(-v.getZ());
|
||||
v.setY(-v.getY());
|
||||
}
|
||||
case MODE_QUARTER -> {
|
||||
double z = v.getZ();
|
||||
v.setZ(v.getY());
|
||||
v.setY(-z);
|
||||
}
|
||||
case MODE_THREE_QUARTER -> {
|
||||
double z = v.getZ();
|
||||
v.setZ(-v.getY());
|
||||
v.setY(z);
|
||||
}
|
||||
case MODE_ANGLE -> {
|
||||
double y = xCos * v.getY() - xSin * v.getZ();
|
||||
double z = xSin * v.getY() + xCos * v.getZ();
|
||||
v.setY(y);
|
||||
v.setZ(z);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
|
||||
switch (zMode) {
|
||||
case MODE_FLIP -> {
|
||||
v.setY(-v.getY());
|
||||
v.setX(-v.getX());
|
||||
}
|
||||
case MODE_QUARTER -> {
|
||||
double y = v.getY();
|
||||
v.setY(v.getX());
|
||||
v.setX(-y);
|
||||
}
|
||||
case MODE_THREE_QUARTER -> {
|
||||
double y = v.getY();
|
||||
v.setY(-v.getX());
|
||||
v.setX(y);
|
||||
}
|
||||
case MODE_ANGLE -> {
|
||||
double x = zCos * v.getX() - zSin * v.getY();
|
||||
double y = zSin * v.getX() + zCos * v.getY();
|
||||
v.setX(x);
|
||||
v.setY(y);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
|
||||
switch (yMode) {
|
||||
case MODE_FLIP -> {
|
||||
v.setX(-v.getX());
|
||||
v.setZ(-v.getZ());
|
||||
}
|
||||
case MODE_QUARTER -> {
|
||||
double x = v.getX();
|
||||
v.setX(v.getZ());
|
||||
v.setZ(-x);
|
||||
}
|
||||
case MODE_THREE_QUARTER -> {
|
||||
double x = v.getX();
|
||||
v.setX(-v.getZ());
|
||||
v.setZ(x);
|
||||
}
|
||||
case MODE_ANGLE -> {
|
||||
double x = yCos * v.getX() + ySin * v.getZ();
|
||||
double z = -ySin * v.getX() + yCos * v.getZ();
|
||||
v.setX(x);
|
||||
v.setZ(z);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,11 +257,16 @@ public final class BoundaryColumnGeometry implements NativeBlockColumn {
|
||||
}
|
||||
|
||||
void add(Voxel voxel) {
|
||||
add(voxel, 1);
|
||||
}
|
||||
|
||||
void add(Voxel voxel, int run) {
|
||||
Objects.requireNonNull(voxel, "Geometry voxel");
|
||||
if (count == 0 || !sameVoxel(voxels[count - 1], voxel)) {
|
||||
voxels[count++] = voxel;
|
||||
}
|
||||
ends[count - 1] = ++length;
|
||||
length += run;
|
||||
ends[count - 1] = length;
|
||||
}
|
||||
|
||||
BoundaryColumnGeometry build(int minimumY) {
|
||||
|
||||
@@ -16,6 +16,9 @@ import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.generation.subterrain.SubterrainCell;
|
||||
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
|
||||
import art.arcane.iris.generation.terrain.IrisDimensionCarvingResolver;
|
||||
import art.arcane.iris.structure.placement.IrisStructurePlacement;
|
||||
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
|
||||
@@ -170,13 +173,14 @@ public final class GenerationSemanticCapture {
|
||||
cavern == null ? null : cavern.getCustomBiome()
|
||||
);
|
||||
});
|
||||
IrisComplex complex = engine.getComplex();
|
||||
mantle.iterateChunk(chunkX, chunkZ, HydrologyCaveCell.class, (localX, y, localZ, cell) -> {
|
||||
if (!caveSpace.test(localX, y, localZ)) {
|
||||
return;
|
||||
}
|
||||
int blockX = minimumX + localX;
|
||||
int blockZ = minimumZ + localZ;
|
||||
if (!hasFullHydrologyWeight(engine.getComplex(), blockX, blockZ)) {
|
||||
if (!hasFullHydrologyWeight(complex, blockX, blockZ)) {
|
||||
return;
|
||||
}
|
||||
captureCave(
|
||||
@@ -199,12 +203,8 @@ public final class GenerationSemanticCapture {
|
||||
Math.addExact(minimumZ, localZ))));
|
||||
}
|
||||
});
|
||||
mantle.iterateChunk(chunkX, chunkZ, String.class, (localX, y, localZ, marker) -> {
|
||||
StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker);
|
||||
if (decoded != null) {
|
||||
semantics.addObject(decoded.objectKey());
|
||||
}
|
||||
});
|
||||
ObjectMarkers objects = new ObjectMarkers(semantics);
|
||||
mantle.iterateChunk(chunkX, chunkZ, String.class, (localX, y, localZ, marker) -> objects.add(marker));
|
||||
}
|
||||
|
||||
private static boolean hasFullHydrologyWeight(IrisComplex complex, int blockX, int blockZ) {
|
||||
@@ -273,10 +273,76 @@ public final class GenerationSemanticCapture {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ObjectMarkers {
|
||||
private final ChunkGenerationSemantics.Builder semantics;
|
||||
private String lastMarker;
|
||||
|
||||
private ObjectMarkers(ChunkGenerationSemantics.Builder semantics) {
|
||||
this.semantics = semantics;
|
||||
}
|
||||
|
||||
private void add(String marker) {
|
||||
if (marker != null && marker.equals(lastMarker)) {
|
||||
return;
|
||||
}
|
||||
StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker);
|
||||
if (decoded != null) {
|
||||
semantics.addObject(decoded.objectKey());
|
||||
}
|
||||
lastMarker = marker;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SubterrainBiomes {
|
||||
private final Engine engine;
|
||||
private final boolean live;
|
||||
private final SubterrainPlanner planner;
|
||||
private final int minimumY;
|
||||
|
||||
private SubterrainBiomes(Engine engine, boolean live, SubterrainPlanner planner, int minimumY) {
|
||||
this.engine = engine;
|
||||
this.live = live;
|
||||
this.planner = planner;
|
||||
this.minimumY = minimumY;
|
||||
}
|
||||
|
||||
private static SubterrainBiomes of(Engine engine, int minimumY) {
|
||||
if (engine.getClass() != IrisEngine.class) {
|
||||
return new SubterrainBiomes(engine, false, null, 0);
|
||||
}
|
||||
IrisEngine irisEngine = (IrisEngine) engine;
|
||||
if (!irisEngine.hasGenerationRuntimeScope() && irisEngine.getGenerationHistoryRuntimeRouter().isPresent()) {
|
||||
return new SubterrainBiomes(engine, false, null, 0);
|
||||
}
|
||||
IrisComplex complex = engine.getComplex();
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
SubterrainPlanner planner = complex == null || dimension == null || !dimension.allowsSubterrainFeatures()
|
||||
|| dimension.getSubterrainFeatures() == null || dimension.getSubterrainFeatures().isEmpty()
|
||||
? null
|
||||
: complex.getSubterrainPlanner();
|
||||
return new SubterrainBiomes(engine, true, planner, minimumY);
|
||||
}
|
||||
|
||||
private IrisBiome resolve(int x, int y, int z) {
|
||||
if (!live) {
|
||||
return engine.getSubterrainBiome(x, y, z);
|
||||
}
|
||||
if (planner == null) {
|
||||
return null;
|
||||
}
|
||||
SubterrainCell cell = planner.sample(x, y + minimumY, z);
|
||||
if (!cell.occupied() || cell.room().biome().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return engine.getSubterrainBiome(x, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CaveBiomes {
|
||||
private final Engine engine;
|
||||
private final boolean scoped;
|
||||
private final CaveFallback fallback;
|
||||
private SubterrainBiomes subterrain;
|
||||
private final IrisDimensionCarvingResolver.State fallbackState = new IrisDimensionCarvingResolver.State();
|
||||
private IrisDimensionCarvingResolver.Snapshot snapshot;
|
||||
private int minimumY;
|
||||
@@ -305,8 +371,9 @@ public final class GenerationSemanticCapture {
|
||||
if (snapshot == null) {
|
||||
snapshot = IrisDimensionCarvingResolver.snapshot(engine);
|
||||
minimumY = engine.getWorld().minHeight();
|
||||
subterrain = SubterrainBiomes.of(engine, minimumY);
|
||||
}
|
||||
IrisBiome featureBiome = engine.getSubterrainBiome(x, y, z);
|
||||
IrisBiome featureBiome = subterrain.resolve(x, y, z);
|
||||
if (featureBiome != null) {
|
||||
return featureBiome;
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ public final class GenerationSemanticIndex {
|
||||
private volatile IOException journalFailure;
|
||||
private long appendingRegionKey;
|
||||
private RegionShard appendingBase;
|
||||
private boolean journalStorageReady;
|
||||
|
||||
private GenerationSemanticIndex(
|
||||
Path dimensionRoot,
|
||||
@@ -264,12 +265,13 @@ public final class GenerationSemanticIndex {
|
||||
if (updates.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ensureStorageDirectory();
|
||||
ensureJournalStorage();
|
||||
boolean existingJournal = journalEntries.containsKey(regionKey);
|
||||
appendingRegionKey = regionKey;
|
||||
appendingBase = baseRegion;
|
||||
publicationLock.unlock();
|
||||
try {
|
||||
SemanticJournal.append(directory, regionX, regionZ, updates, unpublished, durability);
|
||||
SemanticJournal.append(directory, regionX, regionZ, updates, unpublished, durability, existingJournal);
|
||||
} catch (JournalAppendFailure failure) {
|
||||
journalFailure = failure;
|
||||
throw failure;
|
||||
@@ -1193,6 +1195,14 @@ public final class GenerationSemanticIndex {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ensureJournalStorage() throws IOException {
|
||||
if (journalStorageReady) {
|
||||
return;
|
||||
}
|
||||
ensureStorageDirectory();
|
||||
journalStorageReady = true;
|
||||
}
|
||||
|
||||
private void ensureStorageDirectory() throws IOException {
|
||||
validateDimensionRoot();
|
||||
Path irisDirectory = ensureChildDirectory(dimensionRoot, "iris");
|
||||
@@ -1958,18 +1968,17 @@ public final class GenerationSemanticIndex {
|
||||
int regionZ,
|
||||
List<ChunkGenerationSemantics> claims,
|
||||
boolean unpublished,
|
||||
DeferredDurability durability
|
||||
DeferredDurability durability,
|
||||
boolean existing
|
||||
) throws IOException {
|
||||
Path file = directory.resolve(journalFileName(regionX, regionZ));
|
||||
boolean created = !Files.exists(file, LinkOption.NOFOLLOW_LINKS);
|
||||
if (!created && !Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) {
|
||||
boolean created = !existing && !Files.exists(file, LinkOption.NOFOLLOW_LINKS);
|
||||
if (!existing && !created && !Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) {
|
||||
throw invalid(file, "path is not a regular file");
|
||||
}
|
||||
FileChannel opened = FileChannel.open(
|
||||
file,
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.WRITE
|
||||
);
|
||||
FileChannel opened = existing
|
||||
? FileChannel.open(file, StandardOpenOption.WRITE)
|
||||
: FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
|
||||
boolean uncertain = created;
|
||||
boolean restored = false;
|
||||
try (FileChannel channel = opened) {
|
||||
|
||||
@@ -6,11 +6,16 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.zip.CRC32;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.DeflaterOutputStream;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
@@ -18,6 +23,8 @@ public final class NativeTerrainReceipt {
|
||||
private static final int MAGIC = 0x4952544E;
|
||||
private static final int VERSION = 1;
|
||||
private static final int MAXIMUM_BYTES = 64 * 1024 * 1024;
|
||||
private static final ThreadLocal<Deflater> DEFLATERS =
|
||||
ThreadLocal.withInitial(() -> new Deflater(Deflater.DEFAULT_COMPRESSION, true));
|
||||
|
||||
private NativeTerrainReceipt() {
|
||||
}
|
||||
@@ -25,7 +32,9 @@ public final class NativeTerrainReceipt {
|
||||
public static byte[] encode(SavedTerrainChunk chunk, long activationId, String epochId) throws IOException {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
UtfCache strings = new UtfCache();
|
||||
try (DataOutputStream output = new DataOutputStream(new BufferedOutputStream(new GZIPOutputStream(bytes)))) {
|
||||
Deflater deflater = DEFLATERS.get();
|
||||
deflater.reset();
|
||||
try (DataOutputStream output = new DataOutputStream(new BufferedOutputStream(new GzipStream(bytes, deflater)))) {
|
||||
output.writeInt(MAGIC);
|
||||
output.writeInt(VERSION);
|
||||
output.writeInt(chunk.chunkX());
|
||||
@@ -196,6 +205,60 @@ public final class NativeTerrainReceipt {
|
||||
public record Decoded(SavedTerrainChunk terrain, long activationId, String epochId) {
|
||||
}
|
||||
|
||||
private static final class GzipStream extends DeflaterOutputStream {
|
||||
private static final int BUFFER_BYTES = 512;
|
||||
private static final int TRAILER_BYTES = 8;
|
||||
private static final byte[] HEADER = gzipHeader();
|
||||
|
||||
private final CRC32 checksum = new CRC32();
|
||||
|
||||
private GzipStream(OutputStream out, Deflater deflater) throws IOException {
|
||||
super(out, deflater, BUFFER_BYTES);
|
||||
out.write(HEADER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer, int offset, int length) throws IOException {
|
||||
super.write(buffer, offset, length);
|
||||
checksum.update(buffer, offset, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() throws IOException {
|
||||
if (def.finished()) {
|
||||
return;
|
||||
}
|
||||
def.finish();
|
||||
while (!def.finished()) {
|
||||
int length = def.deflate(buf, 0, buf.length);
|
||||
if (length > 0) {
|
||||
out.write(buf, 0, length);
|
||||
}
|
||||
}
|
||||
byte[] trailer = new byte[TRAILER_BYTES];
|
||||
writeLittleEndian(trailer, 0, (int) checksum.getValue());
|
||||
writeLittleEndian(trailer, 4, (int) def.getBytesRead());
|
||||
out.write(trailer);
|
||||
}
|
||||
|
||||
private static void writeLittleEndian(byte[] target, int offset, int value) {
|
||||
target[offset] = (byte) value;
|
||||
target[offset + 1] = (byte) (value >>> 8);
|
||||
target[offset + 2] = (byte) (value >>> 16);
|
||||
target[offset + 3] = (byte) (value >>> 24);
|
||||
}
|
||||
|
||||
private static byte[] gzipHeader() {
|
||||
ByteArrayOutputStream empty = new ByteArrayOutputStream();
|
||||
try (GZIPOutputStream gzip = new GZIPOutputStream(empty)) {
|
||||
gzip.finish();
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Unable to resolve the platform gzip header", failure);
|
||||
}
|
||||
return Arrays.copyOf(empty.toByteArray(), 10);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class UtfCache {
|
||||
private static final int MAXIMUM_ENTRIES = 256;
|
||||
private static final int MAXIMUM_ENCODED_BYTES = 64 * 1024;
|
||||
|
||||
@@ -36,7 +36,8 @@ public final class SavedBiomeCapture {
|
||||
Math.addExact(startX, 15), Math.addExact(startZ, 15)).isEmpty();
|
||||
Map<Integer, List<SavedBiomeChunk.Span>> vertical = new HashMap<>(exactVolume ? 256 : 16);
|
||||
Cells cells = new Cells(activationId);
|
||||
Column column = new Column(engine, minimumY, height, exactVolume ? 1 : 4);
|
||||
Column column = new Column(engine, minimumY, height, exactVolume ? 1 : 4,
|
||||
!engine.subterrainAbsentInChunk(stage.chunkX(), stage.chunkZ()));
|
||||
for (int localX = 0; localX < 16; localX++) {
|
||||
for (int localZ = 0; localZ < 16; localZ++) {
|
||||
int blockX = startX + localX;
|
||||
@@ -82,7 +83,8 @@ public final class SavedBiomeCapture {
|
||||
for (int index = 0; index < column.biomes().length; index++) {
|
||||
int worldY = minimumY + index * column.step();
|
||||
SavedBiomeChunk.Cell current = cells.cell(column.biomes()[index], column.regions()[index]);
|
||||
if (floating != null && !column.engine().getSubterrainCell(blockX, index * column.step(), blockZ).occupied()) {
|
||||
if (floating != null && (!column.subterrain()
|
||||
|| !column.engine().getSubterrainCell(blockX, index * column.step(), blockZ).occupied())) {
|
||||
current = overlay(current, floating.volumeAt(blockX & 15, worldY - minimumY, blockZ & 15));
|
||||
}
|
||||
if (transition != null) {
|
||||
@@ -139,9 +141,10 @@ public final class SavedBiomeCapture {
|
||||
Objects.requireNonNull(region, "region").getLoadKey());
|
||||
}
|
||||
|
||||
private record Column(Engine engine, int minimumY, int height, int step, IrisBiome[] biomes, IrisRegion[] regions) {
|
||||
private Column(Engine engine, int minimumY, int height, int step) {
|
||||
this(engine, minimumY, height, step, new IrisBiome[(height + step - 1) / step],
|
||||
private record Column(Engine engine, int minimumY, int height, int step, boolean subterrain, IrisBiome[] biomes,
|
||||
IrisRegion[] regions) {
|
||||
private Column(Engine engine, int minimumY, int height, int step, boolean subterrain) {
|
||||
this(engine, minimumY, height, step, subterrain, new IrisBiome[(height + step - 1) / step],
|
||||
new IrisRegion[(height + step - 1) / step]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ public final class SavedBiomeStore {
|
||||
private static final int MAXIMUM_CACHED_CHUNKS = 128;
|
||||
private static final long MAXIMUM_CACHED_BYTES = 64L * 1024L * 1024L;
|
||||
private static final int MAXIMUM_WRITE_BATCH = 64;
|
||||
private static final ThreadLocal<Deflater> DEFLATERS = ThreadLocal.withInitial(() -> new Deflater(Deflater.BEST_SPEED));
|
||||
|
||||
private final Path dimensionRoot;
|
||||
private final Path directory;
|
||||
@@ -464,12 +465,16 @@ public final class SavedBiomeStore {
|
||||
}
|
||||
|
||||
private void ensureRegionFile(RegionIndex region) throws IOException {
|
||||
if (region.fileReady) {
|
||||
return;
|
||||
}
|
||||
requireSafeAncestors();
|
||||
Files.createDirectories(directory);
|
||||
if (Files.exists(region.path, LinkOption.NOFOLLOW_LINKS)) {
|
||||
if (!Files.isRegularFile(region.path, LinkOption.NOFOLLOW_LINKS)) {
|
||||
throw new IOException("Saved biome region is not a regular file: " + region.path);
|
||||
}
|
||||
region.fileReady = true;
|
||||
return;
|
||||
}
|
||||
ByteBuffer header = ByteBuffer.allocate(HEADER_BYTES);
|
||||
@@ -486,6 +491,7 @@ public final class SavedBiomeStore {
|
||||
}
|
||||
Files.move(temporary, region.path, StandardCopyOption.ATOMIC_MOVE);
|
||||
forceDirectory(directory);
|
||||
region.fileReady = true;
|
||||
} finally {
|
||||
Files.deleteIfExists(temporary);
|
||||
}
|
||||
@@ -622,10 +628,16 @@ public final class SavedBiomeStore {
|
||||
private static byte[] encode(SavedBiomeChunk chunk) throws IOException {
|
||||
LinkedHashMap<SavedBiomeChunk.Cell, Integer> palette = new LinkedHashMap<>();
|
||||
LinkedHashMap<SavedBiomeChunk.Column, Integer> columns = new LinkedHashMap<>();
|
||||
byte[] columnIndices = new byte[256];
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
SavedBiomeChunk.Column column = chunk.column(x, z);
|
||||
columns.computeIfAbsent(column, ignored -> columns.size());
|
||||
int known = columns.size();
|
||||
int index = columns.computeIfAbsent(column, ignored -> columns.size());
|
||||
columnIndices[z * 16 + x] = (byte) index;
|
||||
if (index < known) {
|
||||
continue;
|
||||
}
|
||||
palette.computeIfAbsent(column.surface(), ignored -> palette.size());
|
||||
palette.computeIfAbsent(column.caveBase(), ignored -> palette.size());
|
||||
for (SavedBiomeChunk.Span span : column.vertical()) {
|
||||
@@ -660,11 +672,7 @@ public final class SavedBiomeStore {
|
||||
writeUnsigned(output, palette.get(span.cell()));
|
||||
}
|
||||
}
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
output.writeByte(columns.get(chunk.column(x, z)));
|
||||
}
|
||||
}
|
||||
output.write(columnIndices);
|
||||
}
|
||||
if (raw.size() > MAXIMUM_DECODED_BYTES) {
|
||||
throw new IOException("Saved biome chunk exceeds its decoded size limit");
|
||||
@@ -674,11 +682,10 @@ public final class SavedBiomeStore {
|
||||
output.writeInt(chunk.chunkX());
|
||||
output.writeInt(chunk.chunkZ());
|
||||
output.writeInt(raw.size());
|
||||
Deflater deflater = new Deflater(Deflater.BEST_SPEED);
|
||||
Deflater deflater = DEFLATERS.get();
|
||||
deflater.reset();
|
||||
try (DeflaterOutputStream compressed = new DeflaterOutputStream(output, deflater)) {
|
||||
raw.writeTo(compressed);
|
||||
} finally {
|
||||
deflater.end();
|
||||
}
|
||||
if (encoded.size() > MAXIMUM_RECORD_BYTES) {
|
||||
throw new IOException("Saved biome chunk exceeds its encoded size limit");
|
||||
@@ -847,6 +854,7 @@ public final class SavedBiomeStore {
|
||||
private final int regionZ;
|
||||
private final long[] offsets = new long[1024];
|
||||
private final int[] lengths = new int[1024];
|
||||
private boolean fileReady;
|
||||
|
||||
private RegionIndex(Path path, int regionX, int regionZ) {
|
||||
this.path = path;
|
||||
|
||||
Loaded 100 of 196 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user