Compare commits
No commits in common. "neoforge-1.21.1" and "main" have entirely different histories.
neoforge-1
...
main
4687 changed files with 449565 additions and 490 deletions
40
.forgejo/workflows/gitleaks.yml
Normal file
40
.forgejo/workflows/gitleaks.yml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# .forgejo/workflows/gitleaks.yml
|
||||
#
|
||||
# Sulkta canonical gitleaks workflow. Drop a copy into every public repo at
|
||||
# `.forgejo/workflows/gitleaks.yml` after the Forgejo act_runner is registered
|
||||
# (task #295).
|
||||
#
|
||||
# Pairs with the pre-receive hook installed on every bare repo — that one is
|
||||
# the strict enforcement layer (rejects the push); this one provides the
|
||||
# per-PR red ✗ that branch-protection rules can require before merge.
|
||||
#
|
||||
# Layer 1 (this workflow): visible per-PR status, can be a required check.
|
||||
# Layer 2 (pre-receive hook): strict enforcement at the server.
|
||||
# Layer 3 (johnny5 cron sweep): nightly full-history sweep across all repos.
|
||||
|
||||
name: gitleaks
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
scan:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Full history — gitleaks needs depth to scan a commit range.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: install gitleaks
|
||||
run: |
|
||||
curl -sSL -o gl.tar.gz \
|
||||
https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz
|
||||
tar xzf gl.tar.gz gitleaks
|
||||
chmod +x gitleaks
|
||||
./gitleaks version
|
||||
|
||||
- name: scan
|
||||
run: |
|
||||
./gitleaks detect --source . --no-banner --redact --verbose
|
||||
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: Bug Report
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
> template is **bold**
|
||||
> sample data is *italicized*
|
||||
|
||||
**Issue Description:** *Dynmap sample issue description. This description would include as much and as little detail necessary for us to understand the issue in its entirety.*
|
||||
|
||||
* **Dynmap Version:** *dynmap-version*
|
||||
* **Server Version:** *forge 1.16 build x, or paper 1.15.2 build x*
|
||||
* **Pastebin of Configuration.txt:** *https://pastebin.com/5SETL3z2*
|
||||
* **Server Host (if applicable):** *McProHosting, Shockbyte, Selfhosted, etc.*
|
||||
* **Pastebin of crashlogs or other relevant logs:** *https://pastebin.com/crashcausedbydynmap*
|
||||
* **Other Relevant Data/Screenshots:** *I'm using this texture pack and these plugins and my map is from alpha .8*
|
||||
* **Steps to Replicate:** *Please be clear in this section so we can replicate your issue*
|
||||
|
||||
[ ] *I have looked at all other issues and this is not a duplicate*
|
||||
[ ] *I have been able to replicate this*
|
||||
15
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
15
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: Feature Request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
> template is **bold**
|
||||
> sample data is *italicized*
|
||||
|
||||
**Feature Description:** *Dynmap sample feature request description. This description would include as much and as little detail necessary for us to understand the feature in its entirety.*
|
||||
|
||||
* **Additional context:** *Please add any additional information you feel will help us understand the feature*
|
||||
62
.github/workflows/gibberish.txt
vendored
Normal file
62
.github/workflows/gibberish.txt
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
Subreddit
|
||||
blockscanner
|
||||
dynmap
|
||||
Dynmap's
|
||||
APIs
|
||||
APL
|
||||
Dynmap
|
||||
Dynmap's
|
||||
DynmapCore
|
||||
DynmapCoreAPI
|
||||
JRuby
|
||||
macOS
|
||||
Minecraft
|
||||
PRs
|
||||
PaperMC
|
||||
WorldGuard
|
||||
www
|
||||
reflow
|
||||
runtime
|
||||
theose
|
||||
DynampCore
|
||||
DynmapCore
|
||||
DynmapCoreAPI
|
||||
Reddit
|
||||
api
|
||||
Bukkit
|
||||
mikeprimm
|
||||
michaelprimm
|
||||
repo
|
||||
https
|
||||
Fi
|
||||
Ki
|
||||
Patreon
|
||||
fi
|
||||
ko
|
||||
patreon
|
||||
README
|
||||
Gradle
|
||||
gradlew
|
||||
gradle
|
||||
oldgradle
|
||||
Curseforge
|
||||
SpigotMC
|
||||
PaperMC
|
||||
backends
|
||||
Minecraft
|
||||
WorldGuard
|
||||
reflow
|
||||
PRs
|
||||
JRuby
|
||||
Primm
|
||||
reddit
|
||||
subreddit
|
||||
gg
|
||||
pqBpw
|
||||
JDBC
|
||||
JDK
|
||||
ForgeGradle
|
||||
Kosma
|
||||
Kosma's
|
||||
DEV
|
||||
Modrinth
|
||||
14
.github/workflows/spellcheck.yaml
vendored
Normal file
14
.github/workflows/spellcheck.yaml
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
name: Checking for spelling errors
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
spellcheck:
|
||||
name: rojopolis/spellcheck
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: actions/checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: rojopolis/spellcheck - actually doing something
|
||||
uses: rojopolis/spellcheck-github-actions@0.29.0
|
||||
41
.gitignore
vendored
41
.gitignore
vendored
|
|
@ -1,3 +1,40 @@
|
|||
build/
|
||||
.gradle/
|
||||
# Eclipse stuff
|
||||
.classpath
|
||||
.project
|
||||
.settings
|
||||
|
||||
# netbeans
|
||||
/nbproject
|
||||
|
||||
# idea
|
||||
.idea/
|
||||
|
||||
*.launch
|
||||
|
||||
# we use maven!
|
||||
/build.xml
|
||||
|
||||
# maven
|
||||
/target
|
||||
|
||||
# vim
|
||||
.*.sw[a-p]
|
||||
|
||||
# various other potential build files
|
||||
/build
|
||||
/bin
|
||||
/dist
|
||||
/manifest.mf
|
||||
/run
|
||||
|
||||
# Mac filesystem dust
|
||||
/.DS_Store
|
||||
/dependency-reduced-pom.xml
|
||||
|
||||
*.log
|
||||
/.gradle
|
||||
/fabric-1.16.1_client.launch
|
||||
/fabric-1.16.1_server.launch
|
||||
/fabric-1.16.2_client.launch
|
||||
/fabric-1.16.2_server.launch
|
||||
|
||||
|
|
|
|||
25
.spellcheck.yaml
Normal file
25
.spellcheck.yaml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
spellchecker: aspell
|
||||
matrix:
|
||||
- name: Markdown
|
||||
aspell:
|
||||
lang: en
|
||||
dictionary:
|
||||
wordlists:
|
||||
- .github/workflows/gibberish.txt
|
||||
encoding: utf-8
|
||||
pipeline:
|
||||
- pyspelling.filters.markdown:
|
||||
markdown_extensions:
|
||||
- pymdownx.extra:
|
||||
- pyspelling.filters.html:
|
||||
comments: true
|
||||
attributes:
|
||||
- title
|
||||
- alt
|
||||
ignores:
|
||||
- ':matches(code, pre)'
|
||||
- 'code'
|
||||
- 'pre'
|
||||
sources:
|
||||
- '**/*.md'
|
||||
default_encoding: utf-8
|
||||
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"java.configuration.updateBuildConfiguration": "automatic",
|
||||
"java.compile.nullAnalysis.mode": "automatic"
|
||||
}
|
||||
87
CHANGES.md
87
CHANGES.md
|
|
@ -1,87 +0,0 @@
|
|||
# Changelog — Dynmap NeoForge 1.21.1 Port
|
||||
|
||||
All changes relative to the upstream `neoforge-1.20.6` module.
|
||||
|
||||
---
|
||||
|
||||
## [2026-03-07] — Final clean state (pre-PR)
|
||||
|
||||
### Reverted
|
||||
- `touchChunk()` helper method extraction — inlined back to match 1.20.6 structure
|
||||
- Was purely cosmetic; upstream CONTRIBUTING.md prohibits refactoring in PRs
|
||||
- No behavior change, no runtime impact
|
||||
|
||||
### Fixed — whitespace
|
||||
- `ForgeMapChunkCache.java` — formatting-only diffs corrected to match 1.20.6 style
|
||||
|
||||
---
|
||||
|
||||
## [2026-03-07] — canOcclude() deadlock fix
|
||||
|
||||
### Changed
|
||||
- `DynmapPlugin.initializeBlockStates()` (lines ~248, ~259):
|
||||
- `isSolidRender(EmptyBlockGetter.INSTANCE, BlockPos.ZERO)` → `canOcclude()`
|
||||
- `propagatesSkylightDown(EmptyBlockGetter.INSTANCE, BlockPos.ZERO)` → `isAir()`
|
||||
- `isSolid()` → `canOcclude()`
|
||||
|
||||
### Why
|
||||
`isSolidRender()` / `propagatesSkylightDown()` / `isSolid()` all trigger a Guava
|
||||
`LoadingCache` lookup in ModernFix's `reduce_blockstate_cache_rebuilds` mixin. During
|
||||
world load with modernfix + ferritecore (present in ATM10), this causes a
|
||||
`BlockState$Cache.<init>` deadlock — lazy/deferred block state initialization enters
|
||||
an infinite loop in VoxelShape calculation for certain complex mod block states.
|
||||
The watchdog kills the server after ~60 seconds.
|
||||
|
||||
`canOcclude()` and `isAir()` read precomputed booleans directly from the block state
|
||||
with no cache involvement. Functionally equivalent for Dynmap's purposes, safe with
|
||||
all mods including modernfix and ferritecore.
|
||||
|
||||
### Test result
|
||||
- ✅ ATM10 5.5, 445 mods, NeoForge 21.1.219 — 3/3 clean boots
|
||||
- ✅ `[Dynmap] version 3.7-SNAPSHOT-Dev is enabled`
|
||||
- ✅ 17 worlds loaded, web server started on 0.0.0.0:8123
|
||||
|
||||
---
|
||||
|
||||
## [2026-03-07] — Runtime fixes + build improvements
|
||||
|
||||
### Fixed — API changes (1.21.1)
|
||||
- `DynmapPlugin`: `ServerTickEvent` listener updated to `ServerTickEvent.Post`
|
||||
- NeoForge 21.x made `ServerTickEvent` abstract; registering the base class no longer works
|
||||
- `DynmapPlugin.initializeBlockStates()`: replaced `isSolidRender(null, ...)` with `isSolidRender(EmptyBlockGetter.INSTANCE, BlockPos.ZERO)`
|
||||
- 1.21.1 actually uses the BlockGetter parameter; passing null causes NPE
|
||||
- (Superseded by canOcclude() fix above — kept here for history)
|
||||
- `DynmapPlugin`: `getLastAvailable()` → `getLatestChunk()` — API renamed
|
||||
- `DynmapPlugin`: `getStatus()` → `getPersistedStatus()` — API renamed
|
||||
- `DynmapPlugin`: `ChatHandler` inner class → direct method with `addListener()`
|
||||
- NeoForge 1.21.1 changed event registration; inner class pattern no longer supported
|
||||
- `NBT.java`: `contains()` reimplemented with manual type checking (behavior changed in 1.21.1)
|
||||
- `NBT.java`: `getAsString()` null safety added (behavior changed in 1.21.1)
|
||||
|
||||
### Fixed — Build
|
||||
- `build.gradle`: userdev plugin 7.0.133 → 7.1.20 (NeoForge 21.1.x requirement)
|
||||
- `build.gradle`: NeoForge dep 20.6.62-beta → 21.1.219
|
||||
- `gradle-wrapper.properties`: 8.7 → 8.14 (required for userdev 7.1.20)
|
||||
- All `fabric-*/build.gradle`: loom 1.6.11 → 1.8.13 (required for Gradle 8.14 compat)
|
||||
- `neoforge.mods.toml`: loader/version ranges updated for 1.21.1
|
||||
- Access transformer: SRG names → mojmap names
|
||||
- Heap caps: Gradle daemon `-Xmx2g`, forked javac `-Xmx3g`
|
||||
|
||||
### ⚠️ Known discrepancy (from earlier commit message)
|
||||
- An early commit message claimed `visibleChunkMap` was replaced with `getChunks()`
|
||||
- **This was inaccurate.** `visibleChunkMap` is still in the code and works fine in NeoForge 1.21.1
|
||||
|
||||
---
|
||||
|
||||
## [2026-03-07] — Initial 1.21.1 compile fix pass
|
||||
|
||||
### Fixed — Compile errors (43 total)
|
||||
- All compile errors resolved from `neoforge-1.20.6` → `neoforge-1.21.1` baseline:
|
||||
- `net.minecraftforge.*` → `net.neoforged.*` (full package migration)
|
||||
- `MinecraftForge.EVENT_BUS` → `NeoForge.EVENT_BUS`
|
||||
- Access transformer updated for 1.21.1 private field patterns
|
||||
|
||||
---
|
||||
|
||||
## Format note
|
||||
Entries are dated by session (not release), since this is a port-in-progress.
|
||||
107
CLAUDE.md
Normal file
107
CLAUDE.md
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Dynmap is a dynamic web mapping plugin/mod for Minecraft servers. It's a multi-platform project supporting Spigot/PaperMC, Forge, and Fabric across multiple Minecraft versions (1.12.2 - 1.21.x).
|
||||
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
# Build all platforms (requires JDK 21 as default)
|
||||
./gradlew setup build
|
||||
|
||||
# Build outputs go to /target directory
|
||||
|
||||
# Build specific module (for faster iteration, but NOT for PR submissions)
|
||||
./gradlew :DynmapCore:build
|
||||
|
||||
# Run unit tests (DynmapCore only — JUnit 4)
|
||||
./gradlew :DynmapCore:test
|
||||
|
||||
# Forge 1.12.2 (requires JDK 8 - set JAVA_HOME accordingly)
|
||||
cd oldgradle
|
||||
./gradlew setup build
|
||||
```
|
||||
|
||||
**JDK Requirements:**
|
||||
- Default: JDK 21
|
||||
- Forge 1.12.2 (oldgradle): JDK 8 strictly required
|
||||
- Runtime targets: JDK 8 (1.16-), JDK 16 (1.17.x), JDK 17 (1.18-1.20.4), JDK 21 (1.20.5+)
|
||||
|
||||
**Build notes:**
|
||||
- `gradle.properties` sets `org.gradle.parallel=false` and `org.gradle.daemon=false` — do not change these
|
||||
- `snakeyaml` is pinned at 1.23 intentionally — newer versions break on Windows-encoded config files
|
||||
|
||||
## Architecture
|
||||
|
||||
### Module Structure
|
||||
|
||||
**Core Shared Modules:**
|
||||
- `DynmapCoreAPI/` - Stable public API for external plugins/mods (markers, mod support, rendering). Published to `repo.mikeprimm.com`. The `org.dynmap.renderer` package here defines `DynmapBlockState` — the central block state abstraction used everywhere.
|
||||
- `DynmapCore/` - Internal shared implementation (NOT stable - subject to breaking changes)
|
||||
- `dynmap-api/` - Bukkit-specific public API
|
||||
|
||||
**Platform Implementations:**
|
||||
- `spigot/` - Bukkit/PaperMC implementation (`DynmapPlugin.java`)
|
||||
- `bukkit-helper-*` - Version-specific NMS code (one per MC version: 1.13-1.21)
|
||||
- `fabric-*` - Fabric mod implementations (1.14.4-1.21.x)
|
||||
- `forge-*` - Forge mod implementations (1.14.4-1.21.x); `forge-1.12.2` lives in `oldgradle/`
|
||||
|
||||
### Dependency Flow
|
||||
```
|
||||
External Plugins/Mods
|
||||
↓
|
||||
DynmapCoreAPI (stable, published to repo.mikeprimm.com)
|
||||
↓
|
||||
DynmapCore (internal, unstable)
|
||||
↓
|
||||
Platform-specific modules (Spigot, Fabric, Forge)
|
||||
```
|
||||
|
||||
### Key Components in DynmapCore
|
||||
|
||||
- `DynmapCore.java` — Main coordination hub (~3,100 lines); bootstrapped by each platform
|
||||
- `MapManager.java` — Tile rendering orchestration; owns the render thread pool and `FullWorldRenderState` queue
|
||||
- `hdmap/` — HD map rendering pipeline:
|
||||
- `IsoHDPerspective` — Isometric raytrace engine (the hot rendering path)
|
||||
- `HDBlockModels` / `HDScaledBlockModels` — Block geometry (patch/volumetric/scaled models)
|
||||
- `TexturePack` / `TexturePackLoader` — Texture resolution from resource packs
|
||||
- `hdmap/renderer/` — Custom block renderers (stairs, fences, doors, etc.) implementing `CustomRenderer`
|
||||
- Shaders (`DefaultHDShader`, `CaveHDShader`, `TopoHDShader`, etc.) — post-process pixel color
|
||||
- Lighting (`DefaultHDLighting`, `ShadowHDLighting`, etc.) — light level calculation
|
||||
- `storage/` — Storage backends (FileTree, MySQL, MariaDB, PostgreSQL, SQLite, MSSQL, AWS S3)
|
||||
- `web/` — Embedded Jetty 9 server with custom HTTP routing (no standard servlet container)
|
||||
- `markers/impl/` — Full marker system implementation; public interface is in `DynmapCoreAPI`
|
||||
- `utils/MapChunkCache` + `utils/MapIterator` — Abstract interfaces that each platform implements to feed world data into the renderer
|
||||
|
||||
### Platform Integration Pattern
|
||||
|
||||
Each platform module (Spigot `bukkit-helper-*`, Fabric, Forge) must implement:
|
||||
- `MapChunkCache` — Loads and caches chunk data for a tile's required chunks
|
||||
- `MapIterator` — Block-by-block iteration over the loaded chunk cache
|
||||
- A platform entry point (e.g., `DynmapPlugin` for Spigot) that bootstraps `DynmapCore`
|
||||
|
||||
The `bukkit-helper-*` modules contain version-specific NMS code; `spigot/` delegates to the appropriate helper at runtime via reflection.
|
||||
|
||||
## Testing
|
||||
|
||||
Unit tests exist in `DynmapCore/src/test/` (JUnit 4) covering `Matrix3D`, `Vector3D`, `IpAddressMatcher`, `DynIntHashMap`, and `BufferInputStream`. Run with `./gradlew :DynmapCore:test`.
|
||||
|
||||
Full verification requires:
|
||||
1. Building all platforms: `./gradlew setup build` AND `cd oldgradle && ./gradlew setup build`
|
||||
2. Manual testing on target Minecraft server platforms
|
||||
|
||||
## Critical Contribution Rules
|
||||
|
||||
**PRs must build and test on ALL platforms including oldgradle. Changes to DynmapCore/DynmapCoreAPI require testing on all platforms.**
|
||||
|
||||
- **Java 8 compatibility required** — Code must compile and run on Java 8
|
||||
- **Java only** — No Kotlin, Scala, or other JVM languages
|
||||
- **No dependency updates** — Library versions are tied to platform compatibility
|
||||
- **No platform-specific code** — Must work on Windows, Linux (x86/ARM), macOS, Docker
|
||||
- **Small PRs only** — One feature per PR, no style/formatting changes
|
||||
- **No mod-specific code** — Use Dynmap APIs instead; external mods should depend on DynmapCoreAPI
|
||||
- **Apache License v2** — All code must be compatible
|
||||
- **DynmapCoreAPI is the only stable API** — Do not add external dependencies on DynmapCore internals
|
||||
76
CONTRIBUTING.md
Normal file
76
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
## Do you need support?
|
||||
Details about support for the project can be found here on the Wiki. You may also wish to use the [Subreddit](https://reddit.com/r/Dynmap/) or the [Discord](https://discord.gg/52pqBpw) to get support. Templates for asking support can be found in each location.
|
||||
|
||||
## Have you found a bug?
|
||||
Before reporting a bug or issue, please make sure you can replicate the issue and that the issue is directly related to the main dynmap branch (not one of the forks like dynmap-essentials, dynmap-blockscanner, etc) If so, please submit bug reports **ONLY TO THIS GITHUB** with the title `[BUG REPORT] Bug Report Title`
|
||||
|
||||
## Contributing to Dynmap's Code?
|
||||
The Dynmap team welcomes Pull Requests with fixes, new features, and new platform support. That said, the following rules apply:
|
||||
- Ultimately, we reserve the right to accept or deny a PR for any reason: fact is, by accepting it, we're also accepting any of the problems with supporting it,
|
||||
explaining it to users, and fixing current and future problems - if we don't think the PR is of value consistent with that cost, we'll probably not accept it.
|
||||
- All PRs should be as small as they can be to accomplish the feature or fix being supplied. To that end:
|
||||
- Do not lump multiple features into one PR - you'll be asked to split them up before they will be reviewed or accepted
|
||||
- Do not make style changes, reflow code, pretty printing, or otherwise make formatting-only code changes. This makes the PR excessively large,
|
||||
creating changes to be reviewed that don't actually do anything (but we have to review them to be sure they aren't being used to disguise security
|
||||
compromises or other malicious code), and they create problems with the MANY people who fork Dynmap for the sake of doing PRs or their own private
|
||||
custom builds - since all theose modified lines create merge conflicts - once again, with no actual function having been accomplished. If we decide
|
||||
the code needs to be 'prettied up', it'll be done by the Dynmap team.
|
||||
- Do not make changes to core code (anything in DynmapCore or DynmapCoreAPI) unless you're ready to build and test it on all supported platforms. Code that
|
||||
breaks building of ANY supported platform will be rejected.
|
||||
- Likewise, any Spigot related changes are expected to function correctly on all supported Spigot and PaperMC versions (currently 1.10.2 through 1.16.1).
|
||||
- Do not include any code that involves platform specific native libraries or command line behaviors. Dynmap supports 32-bit and 64-bit, Windows, lots of
|
||||
Linux versions (both x86 and ARM), MacOS, being used in Docker environments, and more - this is all about staying as 'pure Java' as the Minecraft server itself
|
||||
is. If your PR includes platform specific dependencies that are not coded to handle working on all the above platforms properly, the PR will be rejected.
|
||||
- Dynmap's code is Apache Public License v2 - do not include any code that is not compatible with this license. By contributing code, you are agreeing to
|
||||
that code being subject to the APL v2.
|
||||
- Do not include any code that unconditionally adds to Dynmap's hosting requirements - for example, support for a database can be added, but the use of the
|
||||
database (which likely depends on a database server being deployed and configured by the user) cannot become an unconditional requirement in order to run
|
||||
Dynmap. Features can add the option to exploit new or additional technologies, but cannot add unconditionally to the minimum requirements on the supported
|
||||
platforms (which is what is needed to run the corresponding MC server, plus the Dynmap plugin or mod)
|
||||
- Dynmap is built and supports running on Java 8 - it can run on newer versions, but any contributed code and dependencies MUST support being compiled and run
|
||||
using just Java 8.
|
||||
- Don't introduce other language dependencies - Java only: no Kotlin, Scala, JRuby, whatever. They just add runtime dependencies that most of the platforms lack,
|
||||
and language skills above and beyond the Java language requirements the code base already mandates, which just creates obstacles to other people contributing.
|
||||
- Similarly, do not update existing libraries and dependencies - these are often tied to the versions on various platforms, and updates will likely break runtime
|
||||
- Do not include code specific to other plugins or mods. Dynmap has APIs for the purpose of avoiding the problem of working with other mods - there are many
|
||||
'Dynmap-XXX' mods and plugins which use the APIs to provide support for other mods and plugins (WorldGuard, Nucleus, Citizens, dozens of others). Maintaining
|
||||
interfaces in Dynmap particular to dozens of mods on multiple versions of multiple platforms is unmanageable, so we don't do it. The ONLY exception currently
|
||||
are security mods - although, even for those, leverage of platform-standard security interfaces is always preferred (e.g. Sponge or Bukkit standard permissions)
|
||||
|
||||
## Porting, Supporting Other Platforms, Customized Dynmap Builds
|
||||
While Dynmap is open source, subject to the Apache Public License, v2, the Dynmap team does have specific policies and requirements for anyone that would
|
||||
use the code here for anything except building contributions submitted back to this code base as Pull Requests (which is the only process by which code is accepted and can become part of a release supported by the Dynmap team). Other authorized uses include:
|
||||
|
||||
- Building custom version of Dynmap for use on a personal or on a specific server, so long as this version is NOT distributed to other parties.
|
||||
The modifying team agrees to not pursue support from the Dynmap team for this modified private version, but is otherwise not required to share the
|
||||
modified source code - though doing so is encouraged.
|
||||
- Building a modified version of Dynmap for otherwise unsupported platforms: in this event, the modified version MUST be for a platform or version
|
||||
not yet (or no longer) supported by the Dynmap team. If the Dynmap team comes to support this platform or version, the modifying team must agree to
|
||||
cease distribution of the unofficial version, unless otherwise authorized to continue doing so. Further:
|
||||
- The team distributing the modified version must cite the origin of the Dynmap code, but must also clearly indicate that the version is NOT supported by
|
||||
nor endorsed by the Dynmap team, and that ALL support should be directed through the team providing the modified version.
|
||||
- Any modified version CANNOT be monetized or otherwise charged for, under any circumstances, nor can redistribution of it be limited or restricted.
|
||||
- The modified code must continue to be Apache Public License v2, with no additional conditions or restrictions, including full public availability of the
|
||||
modified source code.
|
||||
- Any code from Dynmap used in such versions should be built from an appropriate fork, as DynampCore and other components (other than DynmapCoreAPI and
|
||||
dynmap-api) are subject to breaking changes at any time, and the support messages in DynmapCore MUST be modified to refer to the supporting team (or, at
|
||||
least, removed). The modified version should NOT refer to the Dynmap Discord nor to /r/Dynmap on Reddit for support. in any case.
|
||||
- Any bugs or issues opened in conjunction with the use of the modified version on this repository will be closed without comment.
|
||||
|
||||
Additions of new functions, including new platform support, in this official Dynmap code base MUST be fully contained within the PRs submitted to this
|
||||
repository. Further, it is always expected than any updates will be built and tested across all relevant platforms - meaning any chances to shared code
|
||||
components (DynmapCore, DynmapCoreAPI) MUST be successfully built and tested on ALL supported platforms (Forge, Spigot, etc). Changes which break
|
||||
supported platforms will be rejected.
|
||||
|
||||
The only interfaces published and maintained as 'stable' are the interfaces of the DynmapCoreAPI (cross platform) and dynmap-api (Bukkit/spigot specific)
|
||||
libraries. All other components are NOT libraries - DynmapCore, in particular, is a shared code component across the various platforms, but is subject to
|
||||
breaking changes without warning or consideration - any use of DynmapCore interfaces by code outside this repository is NOT supported, and will likely
|
||||
result in breaking of such consuming code without warning and without apology. DynmapCore is an internal shared code component, not a library - please
|
||||
treat it accordingly.
|
||||
|
||||
Plugins or mods using the published APIs - DynmapCoreAPI (for all platforms) or dynmap-api (only for Spigot/Bukkit) - may access these components as
|
||||
'compile' dependencies: DO NOT INTEGRATE THEM INTO YOUR PLUGIN - this will break Dynmap and/or other plugins when these interfaces are updated or
|
||||
expanded. These libraries are published at https://repo.mikeprimm.com and will be updated each official release.
|
||||
|
||||
## Want to support the dynmap team?
|
||||
I've set up a coffee-fund jar (I believe in the theory that software developers are machines that turn caffeine into code), for anyone who wants to throw in some tips! I've got a Patreon here - https://www.patreon.com/dynmap, and for folks just looking to for a one-time coffee buy, hit my Ko-Fi at https://ko-fi.com/michaelprimm !
|
||||
2
DynmapCore/.gitignore
vendored
Normal file
2
DynmapCore/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/build/
|
||||
/bin/
|
||||
100
DynmapCore/build.gradle
Normal file
100
DynmapCore/build.gradle
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
description = "DynmapCore"
|
||||
|
||||
apply plugin: 'eclipse'
|
||||
|
||||
eclipse {
|
||||
project {
|
||||
name = "Dynmap(DynmapCore)"
|
||||
}
|
||||
}
|
||||
|
||||
sourceCompatibility = targetCompatibility = compileJava.sourceCompatibility = compileJava.targetCompatibility = '1.8' // Need this here so eclipse task generates correctly.
|
||||
|
||||
dependencies {
|
||||
implementation project(':DynmapCoreAPI')
|
||||
implementation 'javax.servlet:javax.servlet-api:3.1'
|
||||
implementation'org.eclipse.jetty:jetty-server:9.4.26.v20200117'
|
||||
implementation 'org.eclipse.jetty:jetty-servlet:9.4.26.v20200117'
|
||||
implementation 'com.googlecode.json-simple:json-simple:1.1.1'
|
||||
implementation 'org.yaml:snakeyaml:1.23' // DON'T UPDATE - NEWER ONE TRIPS ON WINDOWS ENCODED FILES
|
||||
implementation 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20180219.1'
|
||||
implementation 'org.postgresql:postgresql:42.2.18'
|
||||
implementation 'io.github.linktosriram.s3lite:core:0.0.2-SNAPSHOT'
|
||||
implementation 'io.github.linktosriram.s3lite:api:0.0.2-SNAPSHOT'
|
||||
implementation 'io.github.linktosriram.s3lite:http-client-url-connection:0.0.2-SNAPSHOT'
|
||||
implementation 'io.github.linktosriram.s3lite:http-client-spi:0.0.2-SNAPSHOT'
|
||||
implementation 'io.github.linktosriram.s3lite:util:0.0.2-SNAPSHOT'
|
||||
implementation 'jakarta.xml.bind:jakarta.xml.bind-api:3.0.1'
|
||||
implementation 'com.sun.xml.bind:jaxb-impl:3.0.0'
|
||||
// Test dependencies (Java 8 compatible versions)
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.mockito:mockito-core:4.11.0'
|
||||
testImplementation 'org.assertj:assertj-core:3.24.2'
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnit()
|
||||
testLogging {
|
||||
events "passed", "skipped", "failed"
|
||||
exceptionFormat "full"
|
||||
}
|
||||
}
|
||||
|
||||
processResources {
|
||||
// replace stuff in mcmod.info, nothing else
|
||||
filesMatching([
|
||||
'core.yml',
|
||||
'lightings.txt',
|
||||
'perspectives.txt',
|
||||
'shaders.txt',
|
||||
'extracted/web/version.js',
|
||||
'extracted/web/index.html',
|
||||
'extracted/web/login.html',
|
||||
]) {
|
||||
// replace version and mcversion
|
||||
expand(
|
||||
buildnumber: project.parent.ext.globals.buildNumber,
|
||||
version: project.version
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
archiveClassifier = 'unshaded'
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
dependencies {
|
||||
include(dependency('com.googlecode.json-simple:json-simple:'))
|
||||
include(dependency('org.yaml:snakeyaml:'))
|
||||
include(dependency('com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:'))
|
||||
include(dependency('javax.servlet::'))
|
||||
include(dependency('org.eclipse.jetty::'))
|
||||
include(dependency('org.eclipse.jetty.orbit:javax.servlet:'))
|
||||
include(dependency('org.postgresql:postgresql:'))
|
||||
include(dependency('io.github.linktosriram.s3lite:core:'))
|
||||
include(dependency('io.github.linktosriram.s3lite:api:'))
|
||||
include(dependency('io.github.linktosriram.s3lite:http-client-url-connection:'))
|
||||
include(dependency('io.github.linktosriram.s3lite:http-client-spi:'))
|
||||
include(dependency('io.github.linktosriram.s3lite:util:'))
|
||||
include(dependency('jakarta.xml.bind::'))
|
||||
include(dependency('com.sun.xml.bind::'))
|
||||
include(dependency(':DynmapCoreAPI'))
|
||||
exclude("META-INF/maven/**")
|
||||
exclude("META-INF/services/**")
|
||||
}
|
||||
relocate('org.json.simple', 'org.dynmap.json.simple')
|
||||
relocate('org.yaml.snakeyaml', 'org.dynmap.snakeyaml')
|
||||
relocate('org.eclipse.jetty', 'org.dynmap.jetty')
|
||||
relocate('org.owasp.html', 'org.dynmap.org.owasp.html')
|
||||
relocate('javax.servlet', 'org.dynmap.javax.servlet' )
|
||||
relocate('org.postgresql', 'org.dynmap.org.postgresql')
|
||||
relocate('io.github.linktosriram.s3lite', 'org.dynmap.s3lite')
|
||||
|
||||
destinationDirectory = file '../target'
|
||||
archiveClassifier = ''
|
||||
}
|
||||
|
||||
artifacts {
|
||||
archives shadowJar
|
||||
}
|
||||
1
DynmapCore/src/.gitignore
vendored
Normal file
1
DynmapCore/src/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
.DS_Store
|
||||
162
DynmapCore/src/main/java/org/dynmap/AsynchronousQueue.java
Normal file
162
DynmapCore/src/main/java/org/dynmap/AsynchronousQueue.java
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
package org.dynmap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
public class AsynchronousQueue<T> {
|
||||
private Object lock = new Object();
|
||||
private Thread thread;
|
||||
private LinkedBlockingQueue<T> queue = new LinkedBlockingQueue<T>();
|
||||
private Set<T> set = new HashSet<T>();
|
||||
private Handler<T> handler;
|
||||
private int dequeueTime;
|
||||
private int accelDequeueTime;
|
||||
public int accelDequeueThresh;
|
||||
private int pendingcnt;
|
||||
private int pendinglimit;
|
||||
private boolean normalprio;
|
||||
|
||||
public AsynchronousQueue(Handler<T> handler, int dequeueTime, int accelDequeueThresh, int accelDequeueTime, int pendinglimit, boolean normalprio) {
|
||||
this.handler = handler;
|
||||
this.dequeueTime = dequeueTime;
|
||||
this.accelDequeueTime = accelDequeueTime;
|
||||
this.accelDequeueThresh = accelDequeueThresh;
|
||||
if(pendinglimit < 1) pendinglimit = 1;
|
||||
this.pendinglimit = pendinglimit;
|
||||
this.normalprio = normalprio;
|
||||
}
|
||||
|
||||
public boolean push(T t) {
|
||||
synchronized (lock) {
|
||||
if (!set.add(t)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
queue.offer(t);
|
||||
return true;
|
||||
}
|
||||
|
||||
private T pop() {
|
||||
try {
|
||||
T t = queue.take();
|
||||
synchronized (lock) {
|
||||
set.remove(t);
|
||||
}
|
||||
return t;
|
||||
} catch (InterruptedException ix) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean remove(T t) {
|
||||
synchronized (lock) {
|
||||
if (set.remove(t)) {
|
||||
queue.remove(t);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return set.size();
|
||||
}
|
||||
|
||||
public List<T> popAll() {
|
||||
List<T> s;
|
||||
synchronized(lock) {
|
||||
s = new ArrayList<T>(queue);
|
||||
queue.clear();
|
||||
set.clear();
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
synchronized (lock) {
|
||||
thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
running();
|
||||
}
|
||||
});
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
try {
|
||||
if(!normalprio)
|
||||
thread.setPriority(Thread.MIN_PRIORITY);
|
||||
} catch (SecurityException e) {
|
||||
Log.info("Failed to set minimum priority for worker thread!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
synchronized (lock) {
|
||||
if (thread == null)
|
||||
return;
|
||||
Thread oldThread = thread;
|
||||
thread = null;
|
||||
|
||||
Log.info("Stopping map renderer...");
|
||||
|
||||
oldThread.interrupt();
|
||||
try {
|
||||
oldThread.join(1000);
|
||||
} catch (InterruptedException e) {
|
||||
Log.info("Waiting for map renderer to stop is interrupted");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void running() {
|
||||
try {
|
||||
while (Thread.currentThread() == thread) {
|
||||
synchronized(lock) {
|
||||
while(pendingcnt >= pendinglimit) {
|
||||
try {
|
||||
lock.wait(accelDequeueTime);
|
||||
} catch (InterruptedException ix) {
|
||||
if(Thread.currentThread() != thread)
|
||||
return;
|
||||
throw ix;
|
||||
}
|
||||
}
|
||||
}
|
||||
T t = pop();
|
||||
if (t != null) {
|
||||
synchronized(lock) {
|
||||
pendingcnt++;
|
||||
}
|
||||
handler.handle(t);
|
||||
}
|
||||
if(set.size() >= accelDequeueThresh)
|
||||
sleep(accelDequeueTime);
|
||||
else
|
||||
sleep(dequeueTime);
|
||||
}
|
||||
|
||||
} catch (Exception ex) {
|
||||
Log.severe("Exception on rendering-thread", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean sleep(int time) {
|
||||
try {
|
||||
Thread.sleep(time);
|
||||
} catch (InterruptedException e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void done(T t) {
|
||||
synchronized (lock) {
|
||||
if(pendingcnt > 0) pendingcnt--;
|
||||
lock.notifyAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
12
DynmapCore/src/main/java/org/dynmap/ChatEvent.java
Normal file
12
DynmapCore/src/main/java/org/dynmap/ChatEvent.java
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
package org.dynmap;
|
||||
|
||||
public class ChatEvent {
|
||||
public String source;
|
||||
public String name;
|
||||
public String message;
|
||||
public ChatEvent(String source, String name, String message) {
|
||||
this.source = source;
|
||||
this.name = name;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
339
DynmapCore/src/main/java/org/dynmap/Client.java
Normal file
339
DynmapCore/src/main/java/org/dynmap/Client.java
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
package org.dynmap;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.util.Random;
|
||||
|
||||
import org.json.simple.JSONAware;
|
||||
import org.json.simple.JSONStreamAware;
|
||||
import org.owasp.html.HtmlPolicyBuilder;
|
||||
import org.owasp.html.PolicyFactory;
|
||||
import org.owasp.html.Sanitizers;
|
||||
import org.dynmap.common.DynmapChatColor;
|
||||
|
||||
public class Client {
|
||||
|
||||
public static class Update implements JSONAware, JSONStreamAware {
|
||||
public long timestamp = System.currentTimeMillis();
|
||||
|
||||
@Override
|
||||
public String toJSONString() {
|
||||
return org.dynmap.web.Json.stringifyJson(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeJSONString(Writer w) throws IOException {
|
||||
w.write(toJSONString());
|
||||
}
|
||||
}
|
||||
|
||||
public static class ChatMessage extends Update {
|
||||
public String type = "chat";
|
||||
public String source;
|
||||
public String playerName; // Note: this needs to be client-safe HTML text (can include tags, but only sanitized ones)
|
||||
public String message;
|
||||
public String account;
|
||||
public String channel;
|
||||
public ChatMessage(String source, String channel, String playerName, String message, String playeraccount) {
|
||||
this.source = source;
|
||||
if (ClientUpdateComponent.hideNames)
|
||||
this.playerName = "";
|
||||
else if (ClientUpdateComponent.usePlayerColors)
|
||||
this.playerName = Client.encodeColorInHTML(playerName);
|
||||
else
|
||||
this.playerName = Client.stripColor(playerName);
|
||||
this.message = DynmapChatColor.stripColor(message);
|
||||
this.account = playeraccount;
|
||||
this.channel = channel;
|
||||
}
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if(o instanceof ChatMessage) {
|
||||
ChatMessage m = (ChatMessage)o;
|
||||
return m.source.equals(source) && m.playerName.equals(playerName) && m.message.equals(message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return source.hashCode() ^ playerName.hashCode() ^ message.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
public static class PlayerJoinMessage extends Update {
|
||||
public String type = "playerjoin";
|
||||
public String playerName; // Note: this needs to be client-safe HTML text (can include tags, but only sanitized ones)
|
||||
public String account;
|
||||
public PlayerJoinMessage(String playerName, String playeraccount) {
|
||||
if (ClientUpdateComponent.hideNames)
|
||||
this.playerName = "";
|
||||
else if (ClientUpdateComponent.usePlayerColors)
|
||||
this.playerName = Client.encodeColorInHTML(playerName);
|
||||
else
|
||||
this.playerName = Client.stripColor(playerName);
|
||||
this.account = playeraccount;
|
||||
}
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if(o instanceof PlayerJoinMessage) {
|
||||
PlayerJoinMessage m = (PlayerJoinMessage)o;
|
||||
return m.playerName.equals(playerName);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return account.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
public static class PlayerQuitMessage extends Update {
|
||||
public String type = "playerquit";
|
||||
public String playerName; // Note: this needs to be client-safe HTML text (can include tags, but only sanitized ones)
|
||||
public String account;
|
||||
public PlayerQuitMessage(String playerName, String playeraccount) {
|
||||
if (ClientUpdateComponent.hideNames)
|
||||
this.playerName = "";
|
||||
else if (ClientUpdateComponent.usePlayerColors)
|
||||
this.playerName = Client.encodeColorInHTML(playerName);
|
||||
else
|
||||
this.playerName = Client.stripColor(playerName);
|
||||
this.account = playeraccount;
|
||||
}
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if(o instanceof PlayerQuitMessage) {
|
||||
PlayerQuitMessage m = (PlayerQuitMessage)o;
|
||||
return m.playerName.equals(playerName);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return account.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
public static class Tile extends Update {
|
||||
public String type = "tile";
|
||||
public String name;
|
||||
|
||||
public Tile(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if(o instanceof Tile) {
|
||||
Tile m = (Tile)o;
|
||||
return m.name.equals(name);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return name.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
public static class DayNight extends Update {
|
||||
public String type = "daynight";
|
||||
public boolean isday;
|
||||
|
||||
public DayNight(boolean isday) {
|
||||
this.isday = isday;
|
||||
}
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if(o instanceof DayNight) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 12345;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ComponentMessage extends Update {
|
||||
public String type = "component";
|
||||
/* Each subclass must provide 'ctype' string for component 'type' */
|
||||
}
|
||||
|
||||
// Strip color - assume we're returning safe html text
|
||||
public static String stripColor(String s) {
|
||||
s = DynmapChatColor.stripColor(s); /* Strip standard color encoding */
|
||||
/* Handle Essentials nickname encoding too */
|
||||
int idx = 0;
|
||||
while((idx = s.indexOf('&', idx)) >= 0) {
|
||||
char c = s.charAt(idx+1); /* Get next character */
|
||||
if(c == '&') { /* Another ampersand */
|
||||
s = s.substring(0, idx) + s.substring(idx+1);
|
||||
}
|
||||
else {
|
||||
s = s.substring(0, idx) + s.substring(idx+2);
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
// Apply sanitize policy before returning
|
||||
return sanitizeHTML(s);
|
||||
}
|
||||
private static String[][] codes = {
|
||||
{ "0", "<span style=\'color:#000000\'>" },
|
||||
{ "1", "<span style=\'color:#0000AA\'>" },
|
||||
{ "2", "<span style=\'color:#00AA00\'>" },
|
||||
{ "3", "<span style=\'color:#00AAAA\'>" },
|
||||
{ "4", "<span style=\'color:#AA0000\'>" },
|
||||
{ "5", "<span style=\'color:#AA00AA\'>" },
|
||||
{ "6", "<span style=\'color:#FFAA00\'>" },
|
||||
{ "7", "<span style=\'color:#AAAAAA\'>" },
|
||||
{ "8", "<span style=\'color:#555555\'>" },
|
||||
{ "9", "<span style=\'color:#5555FF\'>" },
|
||||
{ "a", "<span style=\'color:#55FF55\'>" },
|
||||
{ "b", "<span style=\'color:#55FFFF\'>" },
|
||||
{ "c", "<span style=\'color:#FF5555\'>" },
|
||||
{ "d", "<span style=\'color:#FF55FF\'>" },
|
||||
{ "e", "<span style=\'color:#FFFF55\'>" },
|
||||
{ "f", "<span style=\'color:#FFFFFF\'>" },
|
||||
{ "l", "<span style=\'font-weight:bold\'>" },
|
||||
{ "m", "<span style=\'text-decoration:line-through\'>" },
|
||||
{ "n", "<span style=\'text-decoration:underline\'>" },
|
||||
{ "o", "<span style=\'font-style:italic\'>" },
|
||||
{ "r", "<span style=\'font-style:normal,text-decoration:none,font-weight:normal\'>" }
|
||||
};
|
||||
private static Random rnd = new Random();
|
||||
private static String rndchars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
// Replace color codes with corresponding <span - assume we're returning safe HTML text
|
||||
public static String encodeColorInHTML(String s) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int cnt = s.length();
|
||||
int spancnt = 0;
|
||||
boolean magic = false;
|
||||
for (int i = 0; i < cnt; i++) {
|
||||
char c = s.charAt(i);
|
||||
if (c == '\u00A7') { // Escape?
|
||||
i++; // Move past it
|
||||
c = s.charAt(i);
|
||||
if (c == 'k') { // Magic text?
|
||||
magic = true;
|
||||
}
|
||||
else if (c == 'r') { // reset
|
||||
magic = false;
|
||||
}
|
||||
for (int j = 0; j < codes.length; j++) {
|
||||
if (codes[j][0].charAt(0) == c) { // Matching code?
|
||||
sb.append(codes[j][1]); // Substitute
|
||||
spancnt++;
|
||||
break;
|
||||
}
|
||||
else if (c == 'x') { // Essentials nickname hexcode format
|
||||
if (i + 12 <= cnt){ // Check if string is at least long enough to be valid hexcode
|
||||
if (s.charAt(i+1) == s.charAt(i+3) &&
|
||||
s.charAt(i+1) == s.charAt(i+5) &&
|
||||
s.charAt(i+1) == s.charAt(i+7) &&
|
||||
s.charAt(i+1) == s.charAt(i+9) &&
|
||||
s.charAt(i+1) == s.charAt(i+11) && // Check if there are enough \u00A7 in a row
|
||||
s.charAt(i+1) == '\u00A7'){
|
||||
StringBuilder hex = new StringBuilder().append(s.charAt(i+2))
|
||||
.append(s.charAt(i+4))
|
||||
.append(s.charAt(i+6))
|
||||
.append(s.charAt(i+8))
|
||||
.append(s.charAt(i+10))
|
||||
.append(s.charAt(i+12)); // Build hexcode string
|
||||
sb.append("<span style=\'color:#" + hex + "\'>"); // Substitute with hexcode
|
||||
i = i + 12; //move past hex codes
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (c == '&') { // Essentials color code?
|
||||
i++; // Move past it
|
||||
c = s.charAt(i);
|
||||
if (c == '&') { // Amp?
|
||||
sb.append(c);
|
||||
}
|
||||
else {
|
||||
if (c == 'k') { // Magic text?
|
||||
magic = true;
|
||||
}
|
||||
else if (c == 'r') { // reset
|
||||
magic = false;
|
||||
}
|
||||
for (int j = 0; j < codes.length; j++) {
|
||||
if (codes[j][0].charAt(0) == c) { // Matching code?
|
||||
sb.append(codes[j][1]); // Substitute
|
||||
spancnt++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (magic) {
|
||||
sb.append(rndchars.charAt(rnd.nextInt(rndchars.length())));
|
||||
}
|
||||
else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < spancnt; i++) {
|
||||
sb.append("</span>");
|
||||
}
|
||||
return sanitizeHTML(sb.toString());
|
||||
}
|
||||
|
||||
private static PolicyFactory sanitizer = null;
|
||||
private static PolicyFactory OLDTAGS = new HtmlPolicyBuilder().allowElements("center", "basefont", "hr").toFactory();
|
||||
public static String sanitizeHTML(String html) {
|
||||
// Don't sanitize if null or no html markup
|
||||
if ((html == null) || (html.indexOf('<') < 0)) return html;
|
||||
PolicyFactory s = sanitizer;
|
||||
if (s == null) {
|
||||
// Generous but safe html formatting allowances
|
||||
s = Sanitizers.FORMATTING.and(Sanitizers.BLOCKS).and(Sanitizers.IMAGES).and(Sanitizers.LINKS).and(Sanitizers.STYLES).and(Sanitizers.TABLES).and(OLDTAGS);
|
||||
sanitizer = s;
|
||||
}
|
||||
return s.sanitize(html);
|
||||
}
|
||||
private static PolicyFactory stripper = null;
|
||||
public static String stripHTML(String html) {
|
||||
PolicyFactory s = stripper;
|
||||
if (s == null) {
|
||||
// Strip all taks
|
||||
s = new HtmlPolicyBuilder().toFactory();
|
||||
stripper = s;
|
||||
}
|
||||
return s.sanitize(html);
|
||||
}
|
||||
// Encode plain text string for HTML presentation
|
||||
public static String encodeForHTML(String text) {
|
||||
String s = text != null ? text : "";
|
||||
StringBuilder str = new StringBuilder();
|
||||
|
||||
for (int j = 0; j < s.length(); j++) {
|
||||
char c = s.charAt(j);
|
||||
switch (c) {
|
||||
case '"':
|
||||
str.append(""");
|
||||
break;
|
||||
case '&':
|
||||
str.append("&");
|
||||
break;
|
||||
case '<':
|
||||
str.append("<");
|
||||
break;
|
||||
case '>':
|
||||
str.append(">");
|
||||
break;
|
||||
case '\'':
|
||||
str.append("'");
|
||||
break;
|
||||
default:
|
||||
str.append(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return str.toString();
|
||||
}
|
||||
}
|
||||
67
DynmapCore/src/main/java/org/dynmap/ClientComponent.java
Normal file
67
DynmapCore/src/main/java/org/dynmap/ClientComponent.java
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
package org.dynmap;
|
||||
|
||||
import static org.dynmap.JSONUtils.a;
|
||||
import static org.dynmap.JSONUtils.s;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
public class ClientComponent extends Component {
|
||||
private boolean disabled;
|
||||
|
||||
public ClientComponent(final DynmapCore plugin, final ConfigurationNode configuration) {
|
||||
super(plugin, configuration);
|
||||
plugin.events.addListener("buildclientconfiguration", new Event.Listener<JSONObject>() {
|
||||
@Override
|
||||
public void triggered(JSONObject root) {
|
||||
if(!disabled)
|
||||
buildClientConfiguration(root);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void disableComponent() {
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
protected void buildClientConfiguration(JSONObject root) {
|
||||
JSONObject o = createClientConfiguration();
|
||||
a(root, "components", o);
|
||||
}
|
||||
|
||||
protected JSONObject createClientConfiguration() {
|
||||
JSONObject o = convertMap(configuration);
|
||||
o.remove("class");
|
||||
return o;
|
||||
}
|
||||
|
||||
protected static final JSONObject convertMap(Map<String, ?> m) {
|
||||
JSONObject o = new JSONObject();
|
||||
for(Map.Entry<String, ?> entry : m.entrySet()) {
|
||||
s(o, entry.getKey(), convert(entry.getValue()));
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected static final JSONArray convertList(List<?> l) {
|
||||
JSONArray o = new JSONArray();
|
||||
for(Object entry : l) {
|
||||
o.add(convert(entry));
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected static final Object convert(Object o) {
|
||||
if (o instanceof Map<?, ?>) {
|
||||
return convertMap((Map<String, ?>)o);
|
||||
} else if (o instanceof List<?>) {
|
||||
return convertList((List<?>)o);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package org.dynmap;
|
||||
|
||||
import static org.dynmap.JSONUtils.a;
|
||||
import static org.dynmap.JSONUtils.s;
|
||||
import org.dynmap.Event.Listener;
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
public class ClientConfigurationComponent extends Component {
|
||||
public ClientConfigurationComponent(final DynmapCore core, ConfigurationNode configuration) {
|
||||
super(core, configuration);
|
||||
core.events.<JSONObject>addListener("buildclientconfiguration", new Listener<JSONObject>() {
|
||||
@Override
|
||||
public void triggered(JSONObject t) {
|
||||
ConfigurationNode c = core.configuration;
|
||||
s(t, "confighash", core.getConfigHashcode());
|
||||
s(t, "updaterate", c.getFloat("updaterate", 1.0f));
|
||||
s(t, "showplayerfacesinmenu", c.getBoolean("showplayerfacesinmenu", true));
|
||||
s(t, "joinmessage", c.getString("joinmessage", "%playername% joined"));
|
||||
s(t, "quitmessage", c.getString("quitmessage", "%playername% quit"));
|
||||
s(t, "spammessage", c.getString("spammessage", "You may only chat once every %interval% seconds."));
|
||||
s(t, "webprefix", unescapeString(c.getString("webprefix", "[WEB] ")));
|
||||
s(t, "defaultzoom", c.getInteger("defaultzoom", 0));
|
||||
s(t, "sidebaropened", c.getString("sidebaropened", "false"));
|
||||
s(t, "dynmapversion", core.getDynmapPluginVersion());
|
||||
s(t, "coreversion", core.getDynmapCoreVersion());
|
||||
s(t, "cyrillic", c.getBoolean("cyrillic-support", false));
|
||||
s(t, "showlayercontrol", c.getString("showlayercontrol", "true"));
|
||||
s(t, "grayplayerswhenhidden", c.getBoolean("grayplayerswhenhidden", true));
|
||||
s(t, "login-enabled", core.isLoginSupportEnabled());
|
||||
String sn = core.getServer().getServerName();
|
||||
if(sn.equals("Unknown Server"))
|
||||
sn = "Minecraft Dynamic Map";
|
||||
s(t, "title", c.getString("webpage-title", sn));
|
||||
s(t, "msg-maptypes", c.getString("msg/maptypes", "Map Types"));
|
||||
s(t, "msg-players", c.getString("msg/players", "Players"));
|
||||
s(t, "msg-chatrequireslogin", c.getString("msg/chatrequireslogin", "Chat Requires Login"));
|
||||
s(t, "msg-chatnotallowed", c.getString("msg/chatnotallowed", "You are not permitted to send chat messages"));
|
||||
s(t, "msg-hiddennamejoin", c.getString("msg/hiddennamejoin", "Player joined"));
|
||||
s(t, "msg-hiddennamequit", c.getString("msg/hiddennamequit", "Player quit"));
|
||||
s(t, "maxcount", core.getMaxPlayers());
|
||||
|
||||
DynmapWorld defaultWorld = null;
|
||||
String defmap = null;
|
||||
a(t, "worlds", null);
|
||||
for(DynmapWorld world : core.mapManager.getWorlds()) {
|
||||
if (world.maps.size() == 0) continue;
|
||||
if (defaultWorld == null) defaultWorld = world;
|
||||
JSONObject wo = new JSONObject();
|
||||
s(wo, "name", world.getName());
|
||||
s(wo, "title", world.getTitle());
|
||||
s(wo, "protected", world.isProtected());
|
||||
DynmapLocation center = world.getCenterLocation();
|
||||
s(wo, "center/x", center.x);
|
||||
s(wo, "center/y", center.y);
|
||||
s(wo, "center/z", center.z);
|
||||
s(wo, "extrazoomout", world.getExtraZoomOutLevels());
|
||||
s(wo, "sealevel", world.sealevel);
|
||||
s(wo, "worldheight", world.worldheight);
|
||||
a(t, "worlds", wo);
|
||||
|
||||
for(MapType mt : world.maps) {
|
||||
mt.buildClientConfiguration(wo, world);
|
||||
if(defmap == null) defmap = mt.getName();
|
||||
}
|
||||
}
|
||||
s(t, "defaultworld", c.getString("defaultworld", defaultWorld == null ? "world" : defaultWorld.getName()));
|
||||
s(t, "defaultmap", c.getString("defaultmap", defmap == null ? "surface" : defmap));
|
||||
if(c.getString("followmap", null) != null)
|
||||
s(t, "followmap", c.getString("followmap"));
|
||||
if(c.getInteger("followzoom",-1) >= 0)
|
||||
s(t, "followzoom", c.getInteger("followzoom", 0));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
181
DynmapCore/src/main/java/org/dynmap/ClientUpdateComponent.java
Normal file
181
DynmapCore/src/main/java/org/dynmap/ClientUpdateComponent.java
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
package org.dynmap;
|
||||
|
||||
import static org.dynmap.JSONUtils.a;
|
||||
import static org.dynmap.JSONUtils.s;
|
||||
|
||||
import java.util.List;
|
||||
import org.dynmap.common.DynmapPlayer;
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
public class ClientUpdateComponent extends Component {
|
||||
private int hideifshadow;
|
||||
private int hideifunder;
|
||||
private boolean hideifsneaking;
|
||||
private boolean hideifspectator;
|
||||
private boolean hideifinvisiblepotion;
|
||||
private boolean is_protected;
|
||||
public static boolean usePlayerColors;
|
||||
public static boolean hideNames;
|
||||
|
||||
public ClientUpdateComponent(final DynmapCore core, ConfigurationNode configuration) {
|
||||
super(core, configuration);
|
||||
|
||||
hideNames = configuration.getBoolean("hidenames", false);
|
||||
hideifshadow = configuration.getInteger("hideifshadow", 15);
|
||||
hideifunder = configuration.getInteger("hideifundercover", 15);
|
||||
hideifsneaking = configuration.getBoolean("hideifsneaking", false);
|
||||
hideifspectator = configuration.getBoolean("hideifspectator", false);
|
||||
hideifinvisiblepotion = configuration.getBoolean("hide-if-invisiblity-potion", true);
|
||||
is_protected = configuration.getBoolean("protected-player-info", false);
|
||||
usePlayerColors = configuration.getBoolean("use-name-colors", false);
|
||||
if(is_protected)
|
||||
core.player_info_protected = true;
|
||||
|
||||
core.events.addListener("buildclientupdate", new Event.Listener<ClientUpdateEvent>() {
|
||||
@Override
|
||||
public void triggered(ClientUpdateEvent e) {
|
||||
buildClientUpdate(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void buildClientUpdate(ClientUpdateEvent e) {
|
||||
DynmapWorld world = e.world;
|
||||
JSONObject u = e.update;
|
||||
long since = e.timestamp;
|
||||
String worldName = world.getName();
|
||||
boolean see_all = true;
|
||||
|
||||
if(is_protected && (!e.include_all_users)) {
|
||||
if(e.user != null)
|
||||
see_all = core.getServer().checkPlayerPermission(e.user, "playermarkers.seeall");
|
||||
else
|
||||
see_all = false;
|
||||
}
|
||||
if((e.include_all_users) && is_protected) { /* If JSON request AND protected, leave mark for script */
|
||||
s(u, "protected", true);
|
||||
}
|
||||
|
||||
s(u, "confighash", core.getConfigHashcode());
|
||||
|
||||
s(u, "servertime", world.getTime() % 24000);
|
||||
s(u, "hasStorm", world.hasStorm());
|
||||
s(u, "isThundering", world.isThundering());
|
||||
|
||||
s(u, "players", new JSONArray());
|
||||
List<DynmapPlayer> players = core.playerList.getVisiblePlayers();
|
||||
for(DynmapPlayer p : players) {
|
||||
boolean hide = false;
|
||||
DynmapLocation pl = p.getLocation();
|
||||
DynmapWorld pw = core.getWorld(pl.world);
|
||||
if(pw == null) {
|
||||
hide = true;
|
||||
}
|
||||
JSONObject jp = new JSONObject();
|
||||
|
||||
s(jp, "type", "player");
|
||||
if (hideNames)
|
||||
s(jp, "name", "");
|
||||
else if (usePlayerColors)
|
||||
s(jp, "name", Client.encodeColorInHTML(p.getDisplayName()));
|
||||
else
|
||||
s(jp, "name", Client.stripColor(p.getDisplayName()));
|
||||
s(jp, "account", p.getName());
|
||||
if((!hide) && (hideifshadow < 15)) {
|
||||
if(pw.getLightLevel((int)pl.x, (int)pl.y, (int)pl.z) <= hideifshadow) {
|
||||
hide = true;
|
||||
}
|
||||
}
|
||||
if((!hide) && (hideifunder < 15)) {
|
||||
if(pw.canGetSkyLightLevel()) { /* If we can get real sky level */
|
||||
if(pw.getSkyLightLevel((int)pl.x, (int)pl.y, (int)pl.z) <= hideifunder) {
|
||||
hide = true;
|
||||
}
|
||||
}
|
||||
else if(pw.isNether() == false) { /* Not nether */
|
||||
if(pw.getHighestBlockYAt((int)pl.x, (int)pl.z) > pl.y) {
|
||||
hide = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if((!hide) && hideifsneaking && p.isSneaking()) {
|
||||
hide = true;
|
||||
}
|
||||
if((!hide) && hideifspectator && p.isSpectator()) {
|
||||
hide = true;
|
||||
}
|
||||
if((!hide) && is_protected && (!see_all)) {
|
||||
if(e.user != null) {
|
||||
hide = !core.testIfPlayerVisibleToPlayer(e.user, p.getName());
|
||||
}
|
||||
else {
|
||||
hide = true;
|
||||
}
|
||||
}
|
||||
if((!hide) && hideifinvisiblepotion && p.isInvisible()) {
|
||||
hide = true;
|
||||
}
|
||||
|
||||
/* Don't leak player location for world not visible on maps, or if sendposition disbaled */
|
||||
DynmapWorld pworld = MapManager.mapman.worldsLookup.get(pl.world);
|
||||
/* Fix typo on 'sendpositon' to 'sendposition', keep bad one in case someone used it */
|
||||
if(configuration.getBoolean("sendposition", true) && configuration.getBoolean("sendpositon", true) &&
|
||||
(pworld != null) && pworld.sendposition && (!hide)) {
|
||||
s(jp, "world", pl.world);
|
||||
s(jp, "x", pl.x);
|
||||
s(jp, "y", pl.y);
|
||||
s(jp, "z", pl.z);
|
||||
}
|
||||
else {
|
||||
s(jp, "world", "-some-other-bogus-world-");
|
||||
s(jp, "x", 0.0);
|
||||
s(jp, "y", 64.0);
|
||||
s(jp, "z", 0.0);
|
||||
}
|
||||
/* Only send health if enabled AND we're on visible world */
|
||||
if (configuration.getBoolean("sendhealth", false) && (pworld != null) && pworld.sendhealth && (!hide)) {
|
||||
s(jp, "health", p.getHealth());
|
||||
s(jp, "armor", p.getArmorPoints());
|
||||
}
|
||||
else {
|
||||
s(jp, "health", 0);
|
||||
s(jp, "armor", 0);
|
||||
}
|
||||
s(jp, "sort", p.getSortWeight());
|
||||
a(u, "players", jp);
|
||||
}
|
||||
List<DynmapPlayer> hidden = core.playerList.getHiddenPlayers();
|
||||
if(configuration.getBoolean("includehiddenplayers", false)) {
|
||||
for(DynmapPlayer p : hidden) {
|
||||
JSONObject jp = new JSONObject();
|
||||
s(jp, "type", "player");
|
||||
if (hideNames)
|
||||
s(jp, "name", "");
|
||||
else if (usePlayerColors)
|
||||
s(jp, "name", Client.encodeColorInHTML(p.getDisplayName()));
|
||||
else
|
||||
s(jp, "name", Client.stripColor(p.getDisplayName()));
|
||||
s(jp, "account", p.getName());
|
||||
s(jp, "world", "-hidden-player-");
|
||||
s(jp, "x", 0.0);
|
||||
s(jp, "y", 64.0);
|
||||
s(jp, "z", 0.0);
|
||||
s(jp, "health", 0);
|
||||
s(jp, "armor", 0);
|
||||
s(jp, "sort", p.getSortWeight());
|
||||
a(u, "players", jp);
|
||||
}
|
||||
s(u, "currentcount", core.getCurrentPlayers());
|
||||
}
|
||||
else {
|
||||
s(u, "currentcount", core.getCurrentPlayers() - hidden.size());
|
||||
}
|
||||
|
||||
s(u, "updates", new JSONArray());
|
||||
for(Object update : core.mapManager.getWorldUpdates(worldName, since)) {
|
||||
a(u, "updates", (Client.Update)update);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
17
DynmapCore/src/main/java/org/dynmap/ClientUpdateEvent.java
Normal file
17
DynmapCore/src/main/java/org/dynmap/ClientUpdateEvent.java
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package org.dynmap;
|
||||
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
public class ClientUpdateEvent {
|
||||
public long timestamp;
|
||||
public DynmapWorld world;
|
||||
public JSONObject update;
|
||||
public String user;
|
||||
public boolean include_all_users;
|
||||
|
||||
public ClientUpdateEvent(long timestamp, DynmapWorld world, JSONObject update) {
|
||||
this.timestamp = timestamp;
|
||||
this.world = world;
|
||||
this.update = update;
|
||||
}
|
||||
}
|
||||
127
DynmapCore/src/main/java/org/dynmap/Color.java
Normal file
127
DynmapCore/src/main/java/org/dynmap/Color.java
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
package org.dynmap;
|
||||
|
||||
/**
|
||||
* Simple replacement for java.awt.Color for dynmap - it's not an invariant, so we don't make millions
|
||||
* of them during rendering
|
||||
*/
|
||||
public class Color {
|
||||
/* ARGB value */
|
||||
private int val;
|
||||
|
||||
public static final int TRANSPARENT = 0;
|
||||
|
||||
public Color(int red, int green, int blue, int alpha) {
|
||||
setRGBA(red, green, blue, alpha);
|
||||
}
|
||||
public Color(int red, int green, int blue) {
|
||||
setRGBA(red, green, blue, 0xFF);
|
||||
}
|
||||
public Color() {
|
||||
setTransparent();
|
||||
}
|
||||
public final int getRed() {
|
||||
return (val >> 16) & 0xFF;
|
||||
}
|
||||
public final int getGreen() {
|
||||
return (val >> 8) & 0xFF;
|
||||
}
|
||||
public final int getBlue() {
|
||||
return val & 0xFF;
|
||||
}
|
||||
public final int getAlpha() {
|
||||
return ((val >> 24) & 0xFF);
|
||||
}
|
||||
public final boolean isTransparent() {
|
||||
return ((val & 0xFF000000) == TRANSPARENT);
|
||||
}
|
||||
public final void setTransparent() {
|
||||
val = TRANSPARENT;
|
||||
}
|
||||
public final void setGrayscale() {
|
||||
int alpha = val & 0xFF000000;
|
||||
int num = (((val >> 16) & 0xFF) * 76)
|
||||
+ (((val >> 8) & 0xFF) * 151)
|
||||
+ (( val & 0xFF) * 28);
|
||||
// weights sum to 255, so num ∈ [0, 65025]; fast /255 via shift
|
||||
int gray = (num + (num >> 8) + 1) >> 8;
|
||||
val = alpha | (gray << 16) | (gray << 8) | gray;
|
||||
}
|
||||
public final void setColor(Color c) {
|
||||
val = c.val;
|
||||
}
|
||||
public final void setRGBA(int red, int green, int blue, int alpha) {
|
||||
val = ((alpha & 0xFF) << 24) | ((red & 0xFF) << 16) | ((green & 0xFF) << 8) | (blue & 0xFF);
|
||||
}
|
||||
public final int getARGB() {
|
||||
return val;
|
||||
}
|
||||
public final void setARGB(int c) {
|
||||
val = c;
|
||||
}
|
||||
public final int getComponent(int idx) {
|
||||
return 0xFF & (val >> ((3-idx)*8));
|
||||
}
|
||||
public final void setAlpha(int v) {
|
||||
val = (val & 0x00FFFFFF) | (v << 24);
|
||||
}
|
||||
public final void scaleColor(Color minimum, Color maximum) {
|
||||
int alpha = (val >> 24) & 0xFF;
|
||||
int red = (val >> 16) & 0xFF;
|
||||
int green = (val >> 8) & 0xFF;
|
||||
int blue = val & 0xFF;
|
||||
red = minimum.getRed() + ((maximum.getRed() - minimum.getRed()) * red) / 256;
|
||||
green = minimum.getGreen() + ((maximum.getGreen() - minimum.getGreen()) * green) / 256;
|
||||
blue = minimum.getBlue() + ((maximum.getBlue() - minimum.getBlue()) * blue) / 256;
|
||||
setRGBA(red, green, blue, alpha);
|
||||
}
|
||||
/**
|
||||
* Scale each color component, based on the corresponding component
|
||||
* @param c - color to blend
|
||||
*/
|
||||
public final void blendColor(Color c) {
|
||||
blendColor(c.val);
|
||||
}
|
||||
/**
|
||||
* Scale each color component, based on the corresponding component
|
||||
* @param argb - ARGB to blend
|
||||
*/
|
||||
public final void blendColor(int argb) {
|
||||
val = (mulDiv255(val >>> 24, argb >>> 24 ) << 24)
|
||||
| (mulDiv255((val >> 16) & 0xFF, (argb >> 16) & 0xFF) << 16)
|
||||
| (mulDiv255((val >> 8) & 0xFF, (argb >> 8) & 0xFF) << 8)
|
||||
| mulDiv255( val & 0xFF, argb & 0xFF);
|
||||
}
|
||||
/**
|
||||
* Scale each color component, based on the corresponding component
|
||||
* @param argb0 - first color
|
||||
* @param argb1 second color
|
||||
* @return blended color
|
||||
*/
|
||||
public static final int blendColor(int argb0, int argb1) {
|
||||
return (mulDiv255(argb0 >>> 24, argb1 >>> 24 ) << 24)
|
||||
| (mulDiv255((argb0 >> 16) & 0xFF, (argb1 >> 16) & 0xFF) << 16)
|
||||
| (mulDiv255((argb0 >> 8) & 0xFF, (argb1 >> 8) & 0xFF) << 8)
|
||||
| mulDiv255( argb0 & 0xFF, argb1 & 0xFF);
|
||||
}
|
||||
/**
|
||||
* Scale the RGB channels by scale/256, leaving alpha unchanged.
|
||||
* Equivalent to setRGBA(getRed()*scale>>8, getGreen()*scale>>8, getBlue()*scale>>8, getAlpha())
|
||||
* but avoids redundant unpack/repack of the alpha channel.
|
||||
* @param scale - scale factor 0..256 (256 = full brightness)
|
||||
*/
|
||||
public final void scaleRGB(int scale) {
|
||||
val = (val & 0xFF000000)
|
||||
| ((((val >> 16) & 0xFF) * scale >> 8) << 16)
|
||||
| ((((val >> 8) & 0xFF) * scale >> 8) << 8)
|
||||
| ((val & 0xFF) * scale >> 8);
|
||||
}
|
||||
/**
|
||||
* Fast multiply-then-divide-by-255 for two values a, b each in [0, 255].
|
||||
* Returns floor(a*b/255), equivalent to the standard integer division but
|
||||
* computed with shifts only: (x + (x >> 8) + 1) >> 8 where x = a * b.
|
||||
*/
|
||||
private static int mulDiv255(int a, int b) {
|
||||
int x = a * b;
|
||||
return (x + (x >> 8) + 1) >> 8;
|
||||
}
|
||||
}
|
||||
268
DynmapCore/src/main/java/org/dynmap/ColorScheme.java
Normal file
268
DynmapCore/src/main/java/org/dynmap/ColorScheme.java
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
package org.dynmap;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Scanner;
|
||||
|
||||
import org.dynmap.common.BiomeMap;
|
||||
import org.dynmap.debug.Debug;
|
||||
import org.dynmap.renderer.DynmapBlockState;
|
||||
|
||||
public class ColorScheme {
|
||||
private static final HashMap<String, ColorScheme> cache = new HashMap<String, ColorScheme>();
|
||||
|
||||
public String name;
|
||||
/* Switch to arrays - faster than map */
|
||||
public final Color[][] colors; /* [global-state-idx][step] */
|
||||
public final Color[][] biomecolors; /* [Biome.ordinal][step] */
|
||||
public final Color[][] raincolors; /* [rain * 63][step] */
|
||||
public final Color[][] tempcolors; /* [temp * 63][step] */
|
||||
|
||||
public ColorScheme(String name, Color[][] colors, Color[][] biomecolors, Color[][] raincolors,
|
||||
Color[][] tempcolors) {
|
||||
this.name = name;
|
||||
this.colors = colors;
|
||||
this.biomecolors = biomecolors;
|
||||
this.raincolors = raincolors;
|
||||
this.tempcolors = tempcolors;
|
||||
}
|
||||
|
||||
private static File getColorSchemeDirectory(DynmapCore core) {
|
||||
return new File(core.getDataFolder(), "colorschemes");
|
||||
}
|
||||
|
||||
public static ColorScheme getScheme(DynmapCore core, String name) {
|
||||
if (name == null)
|
||||
name = "default";
|
||||
ColorScheme scheme = cache.get(name);
|
||||
if (scheme == null) {
|
||||
scheme = loadScheme(core, name);
|
||||
cache.put(name, scheme);
|
||||
}
|
||||
return scheme;
|
||||
}
|
||||
|
||||
public static ColorScheme loadScheme(DynmapCore core, String name) {
|
||||
File colorSchemeFile = new File(getColorSchemeDirectory(core), name + ".txt");
|
||||
Color[][] colors = new Color[DynmapBlockState.getGlobalIndexMax()][];
|
||||
Color[][] biomecolors = new Color[BiomeMap.values().length][];
|
||||
Color[][] raincolors = new Color[64][];
|
||||
Color[][] tempcolors = new Color[64][];
|
||||
|
||||
/* Default the biome color */
|
||||
for (int i = 0; i < biomecolors.length; i++) {
|
||||
Color[] c = new Color[5];
|
||||
int red = 0x80 | (0x40 * ((i >> 0) & 1)) | (0x20 * ((i >> 3) & 1)) | (0x10 * ((i >> 6) & 1));
|
||||
int green = 0x80 | (0x40 * ((i >> 1) & 1)) | (0x20 * ((i >> 4) & 1)) | (0x10 * ((i >> 7) & 1));
|
||||
int blue = 0x80 | (0x40 * ((i >> 2) & 1)) | (0x20 * ((i >> 5) & 1));
|
||||
c[0] = new Color(red, green, blue);
|
||||
c[3] = new Color(red * 4 / 5, green * 4 / 5, blue * 4 / 5);
|
||||
c[1] = new Color(red / 2, green / 2, blue / 2);
|
||||
c[2] = new Color(red * 2 / 5, green * 2 / 5, blue * 2 / 5);
|
||||
c[4] = new Color((c[1].getRed() + c[3].getRed()) / 2, (c[1].getGreen() + c[3].getGreen()) / 2,
|
||||
(c[1].getBlue() + c[3].getBlue()) / 2, (c[1].getAlpha() + c[3].getAlpha()) / 2);
|
||||
|
||||
biomecolors[i] = c;
|
||||
}
|
||||
|
||||
InputStream stream;
|
||||
// Get defaults from biome_rainfall_temp.txt - let custom file override after
|
||||
File files[] = {
|
||||
new File(getColorSchemeDirectory(core), "biome_rainfall_temp.txt"),
|
||||
new File(getColorSchemeDirectory(core), "default.txt"),
|
||||
colorSchemeFile };
|
||||
try {
|
||||
for (int fidx = 0; fidx < files.length; fidx++) {
|
||||
Debug.debug("Loading colors from '" + files[fidx] + "' for " + name + "...");
|
||||
stream = new FileInputStream(files[fidx]);
|
||||
|
||||
Scanner scanner = new Scanner(stream);
|
||||
while (scanner.hasNextLine()) {
|
||||
String line = scanner.nextLine();
|
||||
if (line.startsWith("#") || line.equals("")) {
|
||||
continue;
|
||||
}
|
||||
/* Make parser less pedantic - tabs or spaces should be fine */
|
||||
String[] split = line.split("[\t ]");
|
||||
int cnt = 0;
|
||||
for (String s : split) {
|
||||
if (s.length() > 0)
|
||||
cnt++;
|
||||
}
|
||||
String[] nsplit = new String[cnt];
|
||||
cnt = 0;
|
||||
for (String s : split) {
|
||||
if (s.length() > 0) {
|
||||
nsplit[cnt] = s;
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
split = nsplit;
|
||||
if (split.length < 17) {
|
||||
continue;
|
||||
}
|
||||
Integer id = null;
|
||||
boolean isbiome = false;
|
||||
boolean istemp = false;
|
||||
boolean israin = false;
|
||||
DynmapBlockState state = null;
|
||||
int idx = split[0].indexOf(':');
|
||||
if (idx > 0) { /* ID:data - data color OR blockstate - data color */
|
||||
String[] vsplit = split[0].split("[\\[\\]]");
|
||||
// Log.info(String.format("split[0]=%s,vsplit[0]=%s,vsplit[1]=%s", split[0],
|
||||
// vsplit[0], vsplit.length > 1 ? vsplit[1] : ""));
|
||||
if (vsplit.length > 1) {
|
||||
state = DynmapBlockState.getStateByNameAndState(vsplit[0], vsplit[1]);
|
||||
} else {
|
||||
state = DynmapBlockState.getBaseStateByName(vsplit[0]);
|
||||
}
|
||||
} else if (split[0].charAt(0) == '[') { /* Biome color data */
|
||||
String bio = split[0].substring(1);
|
||||
idx = bio.indexOf(']');
|
||||
if (idx >= 0)
|
||||
bio = bio.substring(0, idx);
|
||||
isbiome = true;
|
||||
id = -1;
|
||||
BiomeMap[] bm = BiomeMap.values();
|
||||
for (int i = 0; i < bm.length; i++) {
|
||||
if (bm[i].getId().equalsIgnoreCase(bio)) {
|
||||
id = i;
|
||||
break;
|
||||
} else if (bio.equalsIgnoreCase("BIOME_" + i)) {
|
||||
id = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (id < 0) { /* Not biome - check for rain or temp */
|
||||
if (bio.startsWith("RAINFALL-")) {
|
||||
try {
|
||||
double v = Double.parseDouble(bio.substring(9));
|
||||
if ((v >= 0) && (v <= 1.00)) {
|
||||
id = (int) (v * 63.0);
|
||||
israin = true;
|
||||
}
|
||||
} catch (NumberFormatException nfx) {
|
||||
}
|
||||
} else if (bio.startsWith("TEMPERATURE-")) {
|
||||
try {
|
||||
double v = Double.parseDouble(bio.substring(12));
|
||||
if ((v >= 0) && (v <= 1.00)) {
|
||||
id = (int) (v * 63.0);
|
||||
istemp = true;
|
||||
}
|
||||
} catch (NumberFormatException nfx) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
id = Integer.parseInt(split[0]);
|
||||
state = DynmapBlockState.getStateByLegacyBlockID(id);
|
||||
}
|
||||
|
||||
Color[] c = new Color[5];
|
||||
|
||||
/* store colors by raycast sequence number */
|
||||
c[0] = new Color(Integer.parseInt(split[1]), Integer.parseInt(split[2]), Integer.parseInt(split[3]),
|
||||
Integer.parseInt(split[4]));
|
||||
c[3] = new Color(Integer.parseInt(split[5]), Integer.parseInt(split[6]), Integer.parseInt(split[7]),
|
||||
Integer.parseInt(split[8]));
|
||||
c[1] = new Color(Integer.parseInt(split[9]), Integer.parseInt(split[10]),
|
||||
Integer.parseInt(split[11]), Integer.parseInt(split[12]));
|
||||
c[2] = new Color(Integer.parseInt(split[13]), Integer.parseInt(split[14]),
|
||||
Integer.parseInt(split[15]), Integer.parseInt(split[16]));
|
||||
/* Blended color - for 'smooth' option on flat map */
|
||||
c[4] = new Color((c[1].getRed() + c[3].getRed()) / 2, (c[1].getGreen() + c[3].getGreen()) / 2,
|
||||
(c[1].getBlue() + c[3].getBlue()) / 2, (c[1].getAlpha() + c[3].getAlpha()) / 2);
|
||||
|
||||
if (isbiome) {
|
||||
if (istemp) {
|
||||
tempcolors[id] = c;
|
||||
} else if (israin) {
|
||||
raincolors[id] = c;
|
||||
} else if ((id >= 0) && (id < biomecolors.length))
|
||||
biomecolors[id] = c;
|
||||
} else if (state != null) {
|
||||
int stateid = state.globalStateIndex;
|
||||
colors[stateid] = c;
|
||||
}
|
||||
}
|
||||
scanner.close();
|
||||
}
|
||||
/* Last, push base color into any open slots in data colors list */
|
||||
for (int i = 0; i < colors.length; i++) {
|
||||
if (colors[i] == null) {
|
||||
DynmapBlockState bs = DynmapBlockState.getStateByGlobalIndex(i); // Get state
|
||||
DynmapBlockState bsbase = bs.baseState;
|
||||
if ((bsbase != null) && (colors[bsbase.globalStateIndex] != null)) {
|
||||
colors[i] = colors[bsbase.globalStateIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
/* And interpolate any missing rain and temperature colors */
|
||||
interpolateColorTable(tempcolors);
|
||||
interpolateColorTable(raincolors);
|
||||
} catch (RuntimeException e) {
|
||||
Log.severe("Could not load colors '" + name + "' ('" + colorSchemeFile + "').", e);
|
||||
return null;
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.severe("Could not load colors '" + name + "' ('" + colorSchemeFile + "'): File not found.", e);
|
||||
}
|
||||
return new ColorScheme(name, colors, biomecolors, raincolors, tempcolors);
|
||||
}
|
||||
|
||||
public static void interpolateColorTable(Color[][] c) {
|
||||
int idx = -1;
|
||||
for (int k = 0; k < c.length; k++) {
|
||||
if (c[k] == null) { /* Missing? */
|
||||
if ((idx >= 0) && (k == (c.length - 1))) { /* We're last - so fill forward from last color */
|
||||
for (int kk = idx + 1; kk <= k; kk++) {
|
||||
c[kk] = c[idx];
|
||||
}
|
||||
}
|
||||
/* Skip - will backfill when we find next color */
|
||||
} else if (idx == -1) { /* No previous color, just backfill this color */
|
||||
for (int kk = 0; kk < k; kk++) {
|
||||
c[kk] = c[k];
|
||||
}
|
||||
idx = k; /* This is now last defined color */
|
||||
} else { /* Else, interpolate between last idx and this one */
|
||||
int cnt = c[k].length;
|
||||
for (int kk = idx + 1; kk < k; kk++) {
|
||||
double interp = (double) (kk - idx) / (double) (k - idx);
|
||||
Color[] cc = new Color[cnt];
|
||||
for (int jj = 0; jj < cnt; jj++) {
|
||||
cc[jj] = new Color((int) ((1.0 - interp) * c[idx][jj].getRed() + interp * c[k][jj].getRed()),
|
||||
(int) ((1.0 - interp) * c[idx][jj].getGreen() + interp * c[k][jj].getGreen()),
|
||||
(int) ((1.0 - interp) * c[idx][jj].getBlue() + interp * c[k][jj].getBlue()),
|
||||
(int) ((1.0 - interp) * c[idx][jj].getAlpha() + interp * c[k][jj].getAlpha()));
|
||||
}
|
||||
c[kk] = cc;
|
||||
}
|
||||
idx = k;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Color[] getRainColor(double rain) {
|
||||
int idx = (int) (rain * 63.0);
|
||||
if ((idx >= 0) && (idx < raincolors.length))
|
||||
return raincolors[idx];
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
public Color[] getTempColor(double temp) {
|
||||
int idx = (int) (temp * 63.0);
|
||||
if ((idx >= 0) && (idx < tempcolors.length))
|
||||
return tempcolors[idx];
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
cache.clear();
|
||||
}
|
||||
}
|
||||
21
DynmapCore/src/main/java/org/dynmap/Component.java
Normal file
21
DynmapCore/src/main/java/org/dynmap/Component.java
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package org.dynmap;
|
||||
|
||||
public abstract class Component {
|
||||
protected DynmapCore core;
|
||||
protected ConfigurationNode configuration;
|
||||
public Component(DynmapCore core, ConfigurationNode configuration) {
|
||||
this.core = core;
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
}
|
||||
|
||||
/* Substitute proper values for escape sequences */
|
||||
public static String unescapeString(String v) {
|
||||
/* Replace color code &color; */
|
||||
v = v.replace("&color;", "\u00A7");
|
||||
|
||||
return v;
|
||||
}
|
||||
}
|
||||
52
DynmapCore/src/main/java/org/dynmap/ComponentManager.java
Normal file
52
DynmapCore/src/main/java/org/dynmap/ComponentManager.java
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package org.dynmap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
public class ComponentManager {
|
||||
public Set<Component> components = new HashSet<Component>();
|
||||
public Map<String, List<Component>> componentLookup = new HashMap<String, List<Component>>();
|
||||
|
||||
public void add(Component c) {
|
||||
if (components.add(c)) {
|
||||
String key = c.getClass().toString();
|
||||
List<Component> clist = componentLookup.get(key);
|
||||
if (clist == null) {
|
||||
clist = new ArrayList<Component>();
|
||||
componentLookup.put(key, clist);
|
||||
}
|
||||
clist.add(c);
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(Component c) {
|
||||
if (components.remove(c)) {
|
||||
String key = c.getClass().toString();
|
||||
List<Component> clist = componentLookup.get(key);
|
||||
if (clist != null) {
|
||||
clist.remove(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
componentLookup.clear();
|
||||
components.clear();
|
||||
}
|
||||
|
||||
public Iterable<Component> getComponents(Class<? extends Component> c) {
|
||||
List<Component> list = componentLookup.get(c.toString());
|
||||
if (list == null)
|
||||
return new ArrayList<Component>();
|
||||
return list;
|
||||
}
|
||||
|
||||
public Boolean isLoaded(Class<? extends Component> c){
|
||||
return StreamSupport.stream(getComponents(c).spliterator(), false).count() > 0;
|
||||
}
|
||||
}
|
||||
468
DynmapCore/src/main/java/org/dynmap/ConfigurationNode.java
Normal file
468
DynmapCore/src/main/java/org/dynmap/ConfigurationNode.java
Normal file
|
|
@ -0,0 +1,468 @@
|
|||
package org.dynmap;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Reader;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.yaml.snakeyaml.DumperOptions;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||
import org.yaml.snakeyaml.error.YAMLException;
|
||||
import org.yaml.snakeyaml.introspector.Property;
|
||||
import org.yaml.snakeyaml.nodes.CollectionNode;
|
||||
import org.yaml.snakeyaml.nodes.MappingNode;
|
||||
import org.yaml.snakeyaml.nodes.Node;
|
||||
import org.yaml.snakeyaml.nodes.NodeTuple;
|
||||
import org.yaml.snakeyaml.nodes.SequenceNode;
|
||||
import org.yaml.snakeyaml.nodes.Tag;
|
||||
import org.yaml.snakeyaml.reader.UnicodeReader;
|
||||
import org.yaml.snakeyaml.representer.Represent;
|
||||
import org.yaml.snakeyaml.representer.Representer;
|
||||
|
||||
public class ConfigurationNode implements Map<String, Object> {
|
||||
public Map<String, Object> entries;
|
||||
private File f;
|
||||
private Yaml yaml;
|
||||
|
||||
public ConfigurationNode() {
|
||||
entries = new LinkedHashMap<String, Object>();
|
||||
}
|
||||
|
||||
private void initparse() {
|
||||
if(yaml == null) {
|
||||
DumperOptions options = new DumperOptions();
|
||||
|
||||
options.setIndent(4);
|
||||
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
|
||||
options.setPrettyFlow(true);
|
||||
options.setVersion(DumperOptions.Version.V1_1);
|
||||
|
||||
yaml = new Yaml(new SafeConstructor(), new EmptyNullRepresenter(), options);
|
||||
}
|
||||
}
|
||||
|
||||
public ConfigurationNode(File f) {
|
||||
this.f = f;
|
||||
entries = new LinkedHashMap<String, Object>();
|
||||
}
|
||||
|
||||
public ConfigurationNode(Map<String, Object> map) {
|
||||
if (map == null) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
entries = map;
|
||||
}
|
||||
|
||||
public ConfigurationNode(InputStream in) {
|
||||
load(in);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public boolean load(InputStream in) {
|
||||
initparse();
|
||||
|
||||
Object o = yaml.load(new UnicodeReader(in));
|
||||
if((o != null) && (o instanceof Map))
|
||||
entries = (Map<String, Object>)o;
|
||||
return (entries != null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public boolean load() {
|
||||
initparse();
|
||||
// If no file to read, just return false
|
||||
if (!f.canRead()) { return false; }
|
||||
Reader fr = null;
|
||||
try {
|
||||
fr = new UnicodeReader(new BufferedInputStream(new FileInputStream(f)));
|
||||
Object o = yaml.load(fr);
|
||||
if((o != null) && (o instanceof Map))
|
||||
entries = (Map<String, Object>)o;
|
||||
fr.close();
|
||||
}
|
||||
catch (YAMLException e) {
|
||||
Log.severe("Error parsing " + f.getPath() + ". Use http://yamllint.com to debug the YAML syntax." );
|
||||
throw e;
|
||||
} catch(IOException iox) {
|
||||
Log.severe("Error reading " + f.getPath());
|
||||
return false;
|
||||
} finally {
|
||||
if(fr != null) {
|
||||
try { fr.close(); } catch (IOException x) {}
|
||||
}
|
||||
}
|
||||
return (entries != null);
|
||||
}
|
||||
|
||||
public boolean save() {
|
||||
return save(f);
|
||||
}
|
||||
|
||||
public boolean save(File file) {
|
||||
initparse();
|
||||
|
||||
OutputStream stream = null;
|
||||
|
||||
File parent = file.getParentFile();
|
||||
|
||||
if (parent != null) {
|
||||
parent.mkdirs();
|
||||
}
|
||||
|
||||
try {
|
||||
stream = new BufferedOutputStream(new FileOutputStream(file));
|
||||
OutputStreamWriter writer = new OutputStreamWriter(stream, "UTF-8");
|
||||
yaml.dump(entries, writer);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
} finally {
|
||||
try {
|
||||
if (stream != null) {
|
||||
stream.close();
|
||||
}
|
||||
} catch (IOException e) {}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Object getObject(String path) {
|
||||
if (path.isEmpty())
|
||||
return entries;
|
||||
// Try get first (in case '/' is legit part
|
||||
Object v = get(path);
|
||||
int separator = path.indexOf('/');
|
||||
if ((v != null) || (separator < 0)) return v;
|
||||
|
||||
String localKey = path.substring(0, separator);
|
||||
Object subvalue = get(localKey);
|
||||
if (subvalue == null)
|
||||
return null;
|
||||
if (!(subvalue instanceof Map<?, ?>))
|
||||
return null;
|
||||
Map<String, Object> submap;
|
||||
try {
|
||||
submap = (Map<String, Object>)subvalue;
|
||||
} catch (ClassCastException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String subpath = path.substring(separator + 1);
|
||||
return new ConfigurationNode(submap).getObject(subpath);
|
||||
|
||||
}
|
||||
|
||||
public Object getObject(String path, Object def) {
|
||||
Object o = getObject(path);
|
||||
if (o == null)
|
||||
return def;
|
||||
return o;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T getGeneric(String path, T def) {
|
||||
Object o = getObject(path, def);
|
||||
try {
|
||||
return (T)o;
|
||||
} catch(ClassCastException e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
public int getInteger(String path, int def) {
|
||||
return Integer.parseInt(getObject(path, def).toString());
|
||||
}
|
||||
|
||||
public double getLong(String path, long def) {
|
||||
return Long.parseLong(getObject(path, def).toString());
|
||||
}
|
||||
|
||||
public float getFloat(String path, float def) {
|
||||
return Float.parseFloat(getObject(path, def).toString());
|
||||
}
|
||||
|
||||
public double getDouble(String path, double def) {
|
||||
return Double.parseDouble(getObject(path, def).toString());
|
||||
}
|
||||
|
||||
public boolean getBoolean(String path, boolean def) {
|
||||
return Boolean.parseBoolean(getObject(path, def).toString());
|
||||
}
|
||||
|
||||
public String getString(String path) {
|
||||
return getString(path, null);
|
||||
}
|
||||
|
||||
public List<String> getStrings(String path, List<String> def) {
|
||||
Object o = getObject(path);
|
||||
if (!(o instanceof List<?>)) {
|
||||
return def;
|
||||
}
|
||||
ArrayList<String> strings = new ArrayList<String>();
|
||||
for(Object i : (List<?>)o) {
|
||||
strings.add(i.toString());
|
||||
}
|
||||
return strings;
|
||||
}
|
||||
|
||||
public String getString(String path, String def) {
|
||||
Object o = getObject(path, def);
|
||||
if (o == null)
|
||||
return null;
|
||||
return o.toString();
|
||||
}
|
||||
|
||||
public Color getColor(String path, String def) {
|
||||
String lclr = this.getString(path, def);
|
||||
if((lclr != null) && (lclr.startsWith("#"))) {
|
||||
try {
|
||||
int c = Integer.parseInt(lclr.substring(1), 16);
|
||||
return new Color((c>>16)&0xFF, (c>>8)&0xFF, c&0xFF);
|
||||
} catch (NumberFormatException nfx) {
|
||||
Log.severe("Invalid color value: " + lclr + " for '" + path + "'");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> List<T> getList(String path) {
|
||||
try {
|
||||
List<T> list = (List<T>)getObject(path, null);
|
||||
return list;
|
||||
} catch (ClassCastException e) {
|
||||
try {
|
||||
T o = (T)getObject(path, null);
|
||||
if (o == null) {
|
||||
return new ArrayList<T>();
|
||||
}
|
||||
ArrayList<T> al = new ArrayList<T>();
|
||||
al.add(o);
|
||||
return al;
|
||||
} catch (ClassCastException e2) {
|
||||
return new ArrayList<T>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<Map<String,Object>> getMapList(String path) {
|
||||
return getList(path);
|
||||
}
|
||||
|
||||
public ConfigurationNode getNode(String path) {
|
||||
Map<String, Object> v = null;
|
||||
v = getGeneric(path, v);
|
||||
if (v == null)
|
||||
return null;
|
||||
return new ConfigurationNode(v);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<ConfigurationNode> getNodes(String path) {
|
||||
List<Object> o = getList(path);
|
||||
|
||||
if(o == null)
|
||||
return new ArrayList<ConfigurationNode>();
|
||||
|
||||
ArrayList<ConfigurationNode> nodes = new ArrayList<ConfigurationNode>();
|
||||
for(Object i : (List<?>)o) {
|
||||
if (i instanceof Map<?, ?>) {
|
||||
Map<String, Object> map;
|
||||
try {
|
||||
map = (Map<String, Object>)i;
|
||||
} catch(ClassCastException e) {
|
||||
continue;
|
||||
}
|
||||
nodes.add(new ConfigurationNode(map));
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
public void extend(Map<String, Object> other) {
|
||||
if (other != null)
|
||||
extendMap(this, other);
|
||||
}
|
||||
|
||||
private final static Object copyValue(Object v) {
|
||||
if(v instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> mv = (Map<String, Object>)v;
|
||||
LinkedHashMap<String, Object> newv = new LinkedHashMap<String,Object>();
|
||||
for(Map.Entry<String, Object> me : mv.entrySet()) {
|
||||
newv.put(me.getKey(), copyValue(me.getValue()));
|
||||
}
|
||||
return newv;
|
||||
}
|
||||
else if(v instanceof List) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Object> lv = (List<Object>)v;
|
||||
ArrayList<Object> newv = new ArrayList<Object>();
|
||||
for(int i = 0; i < lv.size(); i++) {
|
||||
newv.add(copyValue(lv.get(i)));
|
||||
}
|
||||
return newv;
|
||||
}
|
||||
else {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
private final static void extendMap(Map<String, Object> left, Map<String, Object> right) {
|
||||
ConfigurationNode original = new ConfigurationNode(left);
|
||||
for(Map.Entry<String, Object> entry : right.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
original.put(key, copyValue(value));
|
||||
}
|
||||
}
|
||||
|
||||
public <T> T createInstance(Class<?>[] constructorParameters, Object[] constructorArguments) {
|
||||
String typeName = getString("class");
|
||||
try {
|
||||
Class<?> mapTypeClass = Class.forName(typeName);
|
||||
|
||||
Class<?>[] constructorParameterWithConfiguration = new Class<?>[constructorParameters.length+1];
|
||||
for(int i = 0; i < constructorParameters.length; i++) { constructorParameterWithConfiguration[i] = constructorParameters[i]; }
|
||||
constructorParameterWithConfiguration[constructorParameterWithConfiguration.length-1] = ConfigurationNode.class;
|
||||
|
||||
Object[] constructorArgumentsWithConfiguration = new Object[constructorArguments.length+1];
|
||||
for(int i = 0; i < constructorArguments.length; i++) { constructorArgumentsWithConfiguration[i] = constructorArguments[i]; }
|
||||
constructorArgumentsWithConfiguration[constructorArgumentsWithConfiguration.length-1] = this;
|
||||
Constructor<?> constructor = mapTypeClass.getConstructor(constructorParameterWithConfiguration);
|
||||
@SuppressWarnings("unchecked")
|
||||
T t = (T)constructor.newInstance(constructorArgumentsWithConfiguration);
|
||||
return t;
|
||||
} catch (Exception e) {
|
||||
// TODO: Remove reference to MapManager.
|
||||
Log.severe("Error loading maptype", e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public <T> List<T> createInstances(String path, Class<?>[] constructorParameters, Object[] constructorArguments) {
|
||||
List<ConfigurationNode> nodes = getNodes(path);
|
||||
List<T> instances = new ArrayList<T>();
|
||||
for(ConfigurationNode node : nodes) {
|
||||
T instance = node.<T>createInstance(constructorParameters, constructorArguments);
|
||||
if (instance != null) {
|
||||
instances.add(instance);
|
||||
}
|
||||
}
|
||||
return instances;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return entries.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return entries.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
return entries.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsValue(Object value) {
|
||||
return entries.containsValue(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get(Object key) {
|
||||
return entries.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object put(String key, Object value) {
|
||||
return entries.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object remove(Object key) {
|
||||
return entries.remove(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putAll(Map<? extends String, ? extends Object> m) {
|
||||
entries.putAll(m);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
entries.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> keySet() {
|
||||
return entries.keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Object> values() {
|
||||
return entries.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<java.util.Map.Entry<String, Object>> entrySet() {
|
||||
return entries.entrySet();
|
||||
}
|
||||
|
||||
private class EmptyNullRepresenter extends Representer {
|
||||
|
||||
public EmptyNullRepresenter() {
|
||||
super();
|
||||
this.nullRepresenter = new EmptyRepresentNull();
|
||||
}
|
||||
|
||||
protected class EmptyRepresentNull implements Represent {
|
||||
public Node representData(Object data) {
|
||||
return representScalar(Tag.NULL, ""); // Changed "null" to "" so as to avoid writing nulls
|
||||
}
|
||||
}
|
||||
|
||||
// Code borrowed from snakeyaml (http://code.google.com/p/snakeyaml/source/browse/src/test/java/org/yaml/snakeyaml/issues/issue60/SkipBeanTest.java)
|
||||
@Override
|
||||
protected NodeTuple representJavaBeanProperty(Object javaBean, Property property, Object propertyValue, Tag customTag) {
|
||||
NodeTuple tuple = super.representJavaBeanProperty(javaBean, property, propertyValue, customTag);
|
||||
Node valueNode = tuple.getValueNode();
|
||||
if (valueNode instanceof CollectionNode) {
|
||||
// Removed null check
|
||||
if (Tag.SEQ.equals(valueNode.getTag())) {
|
||||
SequenceNode seq = (SequenceNode) valueNode;
|
||||
if (seq.getValue().isEmpty()) {
|
||||
return null; // skip empty lists
|
||||
}
|
||||
}
|
||||
if (Tag.MAP.equals(valueNode.getTag())) {
|
||||
MappingNode seq = (MappingNode) valueNode;
|
||||
if (seq.getValue().isEmpty()) {
|
||||
return null; // skip empty maps
|
||||
}
|
||||
}
|
||||
}
|
||||
return tuple;
|
||||
}
|
||||
// End of borrowed code
|
||||
}
|
||||
|
||||
}
|
||||
22
DynmapCore/src/main/java/org/dynmap/DynmapChunk.java
Normal file
22
DynmapCore/src/main/java/org/dynmap/DynmapChunk.java
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package org.dynmap;
|
||||
|
||||
public class DynmapChunk {
|
||||
public int x, z;
|
||||
|
||||
public DynmapChunk(int x, int z) {
|
||||
this.x = x;
|
||||
this.z = z;
|
||||
}
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if(o instanceof DynmapChunk) {
|
||||
DynmapChunk dc = (DynmapChunk)o;
|
||||
return (dc.x == this.x) && (dc.z == this.z);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return x ^ (z << 5);
|
||||
}
|
||||
}
|
||||
3132
DynmapCore/src/main/java/org/dynmap/DynmapCore.java
Normal file
3132
DynmapCore/src/main/java/org/dynmap/DynmapCore.java
Normal file
File diff suppressed because it is too large
Load diff
19
DynmapCore/src/main/java/org/dynmap/DynmapLocation.java
Normal file
19
DynmapCore/src/main/java/org/dynmap/DynmapLocation.java
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package org.dynmap;
|
||||
|
||||
/**
|
||||
* Generic block location
|
||||
*/
|
||||
public class DynmapLocation {
|
||||
public double x, y, z;
|
||||
public String world;
|
||||
|
||||
public DynmapLocation() {}
|
||||
|
||||
public DynmapLocation(String w, double x, double y, double z) {
|
||||
world = w;
|
||||
this.x = x; this.y = y; this.z = z;
|
||||
}
|
||||
public String toString() {
|
||||
return String.format("{%s,%f,%f,%f}", world, x, y, z);
|
||||
}
|
||||
}
|
||||
1063
DynmapCore/src/main/java/org/dynmap/DynmapMapCommands.java
Normal file
1063
DynmapCore/src/main/java/org/dynmap/DynmapMapCommands.java
Normal file
File diff suppressed because it is too large
Load diff
571
DynmapCore/src/main/java/org/dynmap/DynmapWorld.java
Normal file
571
DynmapCore/src/main/java/org/dynmap/DynmapWorld.java
Normal file
|
|
@ -0,0 +1,571 @@
|
|||
package org.dynmap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.dynmap.MapType.ImageEncoding;
|
||||
import org.dynmap.hdmap.TexturePack;
|
||||
import org.dynmap.storage.MapStorage;
|
||||
import org.dynmap.storage.MapStorageTile;
|
||||
import org.dynmap.utils.DynmapBufferedImage;
|
||||
import org.dynmap.utils.ImageIOManager;
|
||||
import org.dynmap.utils.MapChunkCache;
|
||||
import org.dynmap.utils.RectangleVisibilityLimit;
|
||||
import org.dynmap.utils.RoundVisibilityLimit;
|
||||
import org.dynmap.utils.TileFlags;
|
||||
import org.dynmap.utils.VisibilityLimit;
|
||||
import org.dynmap.utils.Polygon;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
|
||||
public abstract class DynmapWorld {
|
||||
public List<MapType> maps = new ArrayList<MapType>();
|
||||
public List<MapTypeState> mapstate = new ArrayList<MapTypeState>();
|
||||
|
||||
public UpdateQueue updates = new UpdateQueue();
|
||||
public DynmapLocation center;
|
||||
public List<DynmapLocation> seedloc; /* All seed location - both direct and based on visibility limits */
|
||||
private List<DynmapLocation> seedloccfg; /* Configured full render seeds only */
|
||||
|
||||
public List<VisibilityLimit> visibility_limits;
|
||||
public List<VisibilityLimit> hidden_limits;
|
||||
public MapChunkCache.HiddenChunkStyle hiddenchunkstyle;
|
||||
public int servertime;
|
||||
public boolean sendposition;
|
||||
public boolean sendhealth;
|
||||
public boolean showborder;
|
||||
private int extrazoomoutlevels; /* Number of additional zoom out levels to generate */
|
||||
private boolean cancelled;
|
||||
private final String wname;
|
||||
private final int hashcode;
|
||||
private final String raw_wname;
|
||||
private String title;
|
||||
public int tileupdatedelay;
|
||||
private boolean is_enabled;
|
||||
boolean is_protected; /* If true, user needs 'dynmap.world.<worldid>' privilege to see world */
|
||||
protected int[] brightnessTable = new int[16]; // 0-256 scaled brightness table
|
||||
|
||||
private MapStorage storage; // Storage handler for this world's maps
|
||||
|
||||
/* World height data */
|
||||
public int worldheight; // really maxY+1
|
||||
public int minY;
|
||||
public int sealevel;
|
||||
|
||||
protected void updateWorldHeights(int worldheight, int minY, int sealevel) {
|
||||
this.worldheight = worldheight;
|
||||
this.minY = minY;
|
||||
this.sealevel = sealevel;
|
||||
}
|
||||
|
||||
protected DynmapWorld(String wname, int worldheight, int sealevel) {
|
||||
this(wname, worldheight, sealevel, 0);
|
||||
}
|
||||
protected DynmapWorld(String wname, int worldheight, int sealevel, int miny) {
|
||||
this.raw_wname = wname;
|
||||
this.wname = normalizeWorldName(wname);
|
||||
this.hashcode = this.wname.hashCode();
|
||||
this.title = wname;
|
||||
this.worldheight = worldheight;
|
||||
this.minY = miny;
|
||||
this.sealevel = sealevel;
|
||||
/* Generate default brightness table for surface world */
|
||||
for (int i = 0; i <= 15; ++i) {
|
||||
float f1 = 1.0F - (float)i / 15.0F;
|
||||
setBrightnessTableEntry(i, ((1.0F - f1) / (f1 * 3.0F + 1.0F)));
|
||||
}
|
||||
}
|
||||
protected void setBrightnessTableEntry(int level, float value) {
|
||||
if ((level < 0) || (level > 15)) return;
|
||||
this.brightnessTable[level] = (int)(256.0 * value);
|
||||
if (this.brightnessTable[level] > 256) this.brightnessTable[level] = 256;
|
||||
if (this.brightnessTable[level] < 0) this.brightnessTable[level] = 0;
|
||||
}
|
||||
/**
|
||||
* Get world's brightness table
|
||||
* @return table
|
||||
*/
|
||||
public int[] getBrightnessTable() {
|
||||
return brightnessTable;
|
||||
}
|
||||
|
||||
public void setExtraZoomOutLevels(int lvl) {
|
||||
extrazoomoutlevels = lvl;
|
||||
}
|
||||
public int getExtraZoomOutLevels() { return extrazoomoutlevels; }
|
||||
|
||||
public void enqueueZoomOutUpdate(MapStorageTile tile) {
|
||||
MapTypeState mts = getMapState(tile.map);
|
||||
if (mts != null) {
|
||||
mts.setZoomOutInv(tile.x, tile.y, tile.zoom);
|
||||
}
|
||||
}
|
||||
|
||||
public void freshenZoomOutFiles() {
|
||||
MapTypeState.ZoomOutCoord c = new MapTypeState.ZoomOutCoord();
|
||||
for (MapTypeState mts : mapstate) {
|
||||
if (cancelled) return;
|
||||
MapType mt = mts.type;
|
||||
MapType.ImageVariant var[] = mt.getVariants();
|
||||
mts.startZoomOutIter(); // Start iterator
|
||||
while (mts.nextZoomOutInv(c)) {
|
||||
if(cancelled) return;
|
||||
for (int varIdx = 0; varIdx < var.length; varIdx++) {
|
||||
MapStorageTile tile = storage.getTile(this, mt, c.x, c.y, c.zoomlevel, var[varIdx]);
|
||||
processZoomFile(mts, tile, varIdx == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void cancelZoomOutFreshen() {
|
||||
cancelled = true;
|
||||
}
|
||||
|
||||
public void activateZoomOutFreshen() {
|
||||
cancelled = false;
|
||||
}
|
||||
|
||||
private static final int[] stepseq = { 3, 1, 2, 0 };
|
||||
|
||||
private void processZoomFile(MapTypeState mts, MapStorageTile tile, boolean firstVariant) {
|
||||
long mostRecentTimestamp = 0;
|
||||
int step = 1 << tile.zoom;
|
||||
MapStorageTile ztile = tile.getZoomOutTile();
|
||||
int width = mts.tileSize, height = mts.tileSize;
|
||||
BufferedImage zIm = null;
|
||||
DynmapBufferedImage kzIm = null;
|
||||
boolean blank = true;
|
||||
int[] argb = new int[width*height];
|
||||
int tx = ztile.x;
|
||||
int ty = ztile.y;
|
||||
ty = ty - step; /* Adjust for negative step */
|
||||
|
||||
/* create image buffer */
|
||||
kzIm = DynmapBufferedImage.allocateBufferedImage(width, height);
|
||||
zIm = kzIm.buf_img;
|
||||
for(int i = 0; i < 4; i++) {
|
||||
boolean doblit = true;
|
||||
int tx1 = tx + step * (1 & stepseq[i]);
|
||||
int ty1 = ty + step * (stepseq[i] >> 1);
|
||||
MapStorageTile tile1 = storage.getTile(this, tile.map, tx1, ty1, tile.zoom, tile.var);
|
||||
if (tile1 == null) continue;
|
||||
tile1.getReadLock();
|
||||
if (firstVariant) { // We're handling this one - but only clear on first variant (so that we don't miss updates later)
|
||||
mts.clearZoomOutInv(tile1.x, tile1.y, tile1.zoom);
|
||||
}
|
||||
try {
|
||||
MapStorageTile.TileRead tr = tile1.read();
|
||||
if (tr != null) {
|
||||
BufferedImage im = null;
|
||||
try {
|
||||
im = ImageIOManager.imageIODecode(tr);
|
||||
// Only consider the timestamp when the tile exists and isn't broken
|
||||
mostRecentTimestamp = Math.max(mostRecentTimestamp, tr.lastModified);
|
||||
} catch (IOException iox) {
|
||||
// Broken file - zap it
|
||||
tile1.delete();
|
||||
}
|
||||
if((im != null) && (im.getWidth() >= width) && (im.getHeight() >= height)) {
|
||||
int iwidth = im.getWidth();
|
||||
int iheight = im.getHeight();
|
||||
if(iwidth > iheight) iwidth = iheight;
|
||||
|
||||
if ((iwidth == width) && (iheight == height)) {
|
||||
im.getRGB(0, 0, width, height, argb, 0, width); /* Read data */
|
||||
im.flush();
|
||||
/* Do binlinear scale to width/2 x height/2 */
|
||||
int off = 0;
|
||||
for(int y = 0; y < height; y += 2) {
|
||||
off = y*width;
|
||||
for(int x = 0; x < width; x += 2, off += 2) {
|
||||
int p0 = argb[off];
|
||||
int p1 = argb[off+1];
|
||||
int p2 = argb[off+width];
|
||||
int p3 = argb[off+width+1];
|
||||
int alpha = ((p0 >> 24) & 0xFF) + ((p1 >> 24) & 0xFF) + ((p2 >> 24) & 0xFF) + ((p3 >> 24) & 0xFF);
|
||||
int red = ((p0 >> 16) & 0xFF) + ((p1 >> 16) & 0xFF) + ((p2 >> 16) & 0xFF) + ((p3 >> 16) & 0xFF);
|
||||
int green = ((p0 >> 8) & 0xFF) + ((p1 >> 8) & 0xFF) + ((p2 >> 8) & 0xFF) + ((p3 >> 8) & 0xFF);
|
||||
int blue = (p0 & 0xFF) + (p1 & 0xFF) + (p2 & 0xFF) + (p3 & 0xFF);
|
||||
argb[off>>1] = (((alpha>>2)&0xFF)<<24) | (((red>>2)&0xFF)<<16) | (((green>>2)&0xFF)<<8) | ((blue>>2)&0xFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
int[] buf = new int[iwidth * iwidth];
|
||||
im.getRGB(0, 0, iwidth, iwidth, buf, 0, iwidth);
|
||||
im.flush();
|
||||
TexturePack.scaleTerrainPNGSubImage(iwidth, width/2, buf, argb);
|
||||
/* blit scaled rendered tile onto zoom-out tile */
|
||||
zIm.setRGB(((i>>1) != 0)?0:width/2, (i & 1) * height/2, width/2, height/2, argb, 0, width/2);
|
||||
doblit = false;
|
||||
}
|
||||
blank = false;
|
||||
}
|
||||
else {
|
||||
if (tile1.map.getImageFormat().getEncoding() == ImageEncoding.JPG) {
|
||||
Arrays.fill(argb, tile1.map.getBackgroundARGB(tile1.var));
|
||||
}
|
||||
else {
|
||||
Arrays.fill(argb, 0);
|
||||
}
|
||||
tile1.delete(); // Delete unusable tile
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (tile1.map.getImageFormat().getEncoding() == ImageEncoding.JPG) {
|
||||
Arrays.fill(argb, tile1.map.getBackgroundARGB(tile1.var));
|
||||
}
|
||||
else {
|
||||
Arrays.fill(argb, 0);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
tile1.releaseReadLock();
|
||||
}
|
||||
/* blit scaled rendered tile onto zoom-out tile */
|
||||
if(doblit) {
|
||||
zIm.setRGB(((i>>1) != 0)?0:width/2, (i & 1) * height/2, width/2, height/2, argb, 0, width);
|
||||
}
|
||||
}
|
||||
ztile.getWriteLock();
|
||||
try {
|
||||
MapManager mm = MapManager.mapman;
|
||||
if(mm == null)
|
||||
return;
|
||||
long crc = MapStorage.calculateImageHashCode(kzIm.argb_buf, 0, kzIm.argb_buf.length); /* Get hash of tile */
|
||||
if(blank) {
|
||||
if (ztile.exists()) {
|
||||
ztile.delete();
|
||||
MapManager.mapman.pushUpdate(this, new Client.Tile(ztile.getURI()));
|
||||
enqueueZoomOutUpdate(ztile);
|
||||
}
|
||||
}
|
||||
else /* if (!ztile.matchesHashCode(crc)) */ {
|
||||
ztile.write(crc, zIm, (mostRecentTimestamp == 0)? System.currentTimeMillis() : mostRecentTimestamp);
|
||||
MapManager.mapman.pushUpdate(this, new Client.Tile(ztile.getURI()));
|
||||
enqueueZoomOutUpdate(ztile);
|
||||
}
|
||||
} finally {
|
||||
ztile.releaseWriteLock();
|
||||
DynmapBufferedImage.freeBufferedImage(kzIm);
|
||||
}
|
||||
}
|
||||
/* Get world name */
|
||||
public String getName() {
|
||||
return wname;
|
||||
}
|
||||
/* Test if world is nether */
|
||||
public abstract boolean isNether();
|
||||
|
||||
/* Get world spawn location */
|
||||
public abstract DynmapLocation getSpawnLocation();
|
||||
|
||||
public int hashCode() {
|
||||
return this.hashcode;
|
||||
}
|
||||
/* Get world time */
|
||||
public abstract long getTime();
|
||||
/* World is storming */
|
||||
public abstract boolean hasStorm();
|
||||
/* World is thundering */
|
||||
public abstract boolean isThundering();
|
||||
/* World is loaded */
|
||||
public abstract boolean isLoaded();
|
||||
/* Set world unloaded */
|
||||
public abstract void setWorldUnloaded();
|
||||
/* Get light level of block */
|
||||
public abstract int getLightLevel(int x, int y, int z);
|
||||
/* Get highest Y coord of given location */
|
||||
public abstract int getHighestBlockYAt(int x, int z);
|
||||
/* Test if sky light level is requestable */
|
||||
public abstract boolean canGetSkyLightLevel();
|
||||
/* Return sky light level */
|
||||
public abstract int getSkyLightLevel(int x, int y, int z);
|
||||
/**
|
||||
* Get world environment ID (lower case - normal, the_end, nether)
|
||||
* @return environment ID
|
||||
*/
|
||||
public abstract String getEnvironment();
|
||||
/**
|
||||
* Get map chunk cache for world
|
||||
* @param chunks - list of chunks to load
|
||||
* @return cache
|
||||
*/
|
||||
public abstract MapChunkCache getChunkCache(List<DynmapChunk> chunks);
|
||||
|
||||
/**
|
||||
* Get title for world
|
||||
* @return title
|
||||
*/
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
/**
|
||||
* Get center location
|
||||
* @return center
|
||||
*/
|
||||
public DynmapLocation getCenterLocation() {
|
||||
if(center != null)
|
||||
return center;
|
||||
else
|
||||
return getSpawnLocation();
|
||||
}
|
||||
|
||||
/* Load world configuration from configuration node */
|
||||
public boolean loadConfiguration(DynmapCore core, ConfigurationNode worldconfig) {
|
||||
is_enabled = worldconfig.getBoolean("enabled", false);
|
||||
if (!is_enabled) {
|
||||
return false;
|
||||
}
|
||||
title = worldconfig.getString("title", title);
|
||||
ConfigurationNode ctr = worldconfig.getNode("center");
|
||||
int mid_y = (worldheight + minY)/2;
|
||||
if(ctr != null)
|
||||
center = new DynmapLocation(wname, ctr.getDouble("x", 0.0), ctr.getDouble("y", mid_y), ctr.getDouble("z", 0));
|
||||
else
|
||||
center = null;
|
||||
List<ConfigurationNode> loclist = worldconfig.getNodes("fullrenderlocations");
|
||||
seedloc = new ArrayList<DynmapLocation>();
|
||||
seedloccfg = new ArrayList<DynmapLocation>();
|
||||
servertime = (int)(getTime() % 24000);
|
||||
sendposition = worldconfig.getBoolean("sendposition", true);
|
||||
sendhealth = worldconfig.getBoolean("sendhealth", true);
|
||||
showborder = worldconfig.getBoolean("showborder", true);
|
||||
is_protected = worldconfig.getBoolean("protected", false);
|
||||
setExtraZoomOutLevels(worldconfig.getInteger("extrazoomout", 0));
|
||||
setTileUpdateDelay(worldconfig.getInteger("tileupdatedelay", -1));
|
||||
storage = core.getDefaultMapStorage();
|
||||
if(loclist != null) {
|
||||
for(ConfigurationNode loc : loclist) {
|
||||
DynmapLocation lx = new DynmapLocation(wname, loc.getDouble("x", 0), loc.getDouble("y", mid_y), loc.getDouble("z", 0));
|
||||
seedloc.add(lx); /* Add to both combined and configured seed list */
|
||||
seedloccfg.add(lx);
|
||||
}
|
||||
}
|
||||
/* Build maps */
|
||||
maps.clear();
|
||||
Log.verboseinfo("Loading maps of world '" + wname + "'...");
|
||||
for(MapType map : worldconfig.<MapType>createInstances("maps", new Class<?>[] { DynmapCore.class }, new Object[] { core })) {
|
||||
if(map.getName() != null) {
|
||||
maps.add(map);
|
||||
}
|
||||
}
|
||||
/* Rebuild map state list - match on indexes */
|
||||
mapstate.clear();
|
||||
for(MapType map : maps) {
|
||||
MapTypeState ms = new MapTypeState(this, map);
|
||||
ms.setInvalidatePeriod(map.getTileUpdateDelay(this));
|
||||
mapstate.add(ms);
|
||||
}
|
||||
Log.info("Loaded " + maps.size() + " maps of world '" + wname + "'.");
|
||||
/* Load visibility limits, if any are defined */
|
||||
List<ConfigurationNode> vislimits = worldconfig.getNodes("visibilitylimits");
|
||||
if(vislimits != null) {
|
||||
visibility_limits = new ArrayList<VisibilityLimit>();
|
||||
for(ConfigurationNode vis : vislimits) {
|
||||
VisibilityLimit lim;
|
||||
if (vis.containsKey("r")) { /* It is round visibility limit */
|
||||
int x_center = vis.getInteger("x", 0);
|
||||
int z_center = vis.getInteger("z", 0);
|
||||
int radius = vis.getInteger("r", 0);
|
||||
lim = new RoundVisibilityLimit(x_center, z_center, radius);
|
||||
}
|
||||
else { /* Rectangle visibility limit */
|
||||
int x0 = vis.getInteger("x0", 0);
|
||||
int x1 = vis.getInteger("x1", 0);
|
||||
int z0 = vis.getInteger("z0", 0);
|
||||
int z1 = vis.getInteger("z1", 0);
|
||||
lim = new RectangleVisibilityLimit(x0, z0, x1, z1);
|
||||
}
|
||||
visibility_limits.add(lim);
|
||||
/* Also, add a seed location for the middle of each visible area */
|
||||
seedloc.add(new DynmapLocation(wname, lim.xCenter(), 64, lim.zCenter()));
|
||||
}
|
||||
}
|
||||
/* Load hidden limits, if any are defined */
|
||||
List<ConfigurationNode> hidelimits = worldconfig.getNodes("hiddenlimits");
|
||||
if(hidelimits != null) {
|
||||
hidden_limits = new ArrayList<VisibilityLimit>();
|
||||
for(ConfigurationNode vis : hidelimits) {
|
||||
VisibilityLimit lim;
|
||||
if (vis.containsKey("r")) { /* It is round visibility limit */
|
||||
int x_center = vis.getInteger("x", 0);
|
||||
int z_center = vis.getInteger("z", 0);
|
||||
int radius = vis.getInteger("r", 0);
|
||||
lim = new RoundVisibilityLimit(x_center, z_center, radius);
|
||||
}
|
||||
else { /* Rectangle visibility limit */
|
||||
int x0 = vis.getInteger("x0", 0);
|
||||
int x1 = vis.getInteger("x1", 0);
|
||||
int z0 = vis.getInteger("z0", 0);
|
||||
int z1 = vis.getInteger("z1", 0);
|
||||
lim = new RectangleVisibilityLimit(x0, z0, x1, z1);
|
||||
}
|
||||
hidden_limits.add(lim);
|
||||
}
|
||||
}
|
||||
String hiddenchunkstyle = worldconfig.getString("hidestyle", "stone");
|
||||
this.hiddenchunkstyle = MapChunkCache.HiddenChunkStyle.fromValue(hiddenchunkstyle);
|
||||
if (this.hiddenchunkstyle == null) this.hiddenchunkstyle = MapChunkCache.HiddenChunkStyle.FILL_STONE_PLAIN;
|
||||
|
||||
return true;
|
||||
}
|
||||
/*
|
||||
* Make configuration node for saving world
|
||||
*/
|
||||
public ConfigurationNode saveConfiguration() {
|
||||
ConfigurationNode node = new ConfigurationNode();
|
||||
/* Add name and title */
|
||||
node.put("name", wname);
|
||||
node.put("title", getTitle());
|
||||
node.put("enabled", is_enabled);
|
||||
node.put("protected", is_protected);
|
||||
node.put("showborder", showborder);
|
||||
if(tileupdatedelay > 0) {
|
||||
node.put("tileupdatedelay", tileupdatedelay);
|
||||
}
|
||||
/* Add center */
|
||||
if(center != null) {
|
||||
ConfigurationNode c = new ConfigurationNode();
|
||||
c.put("x", center.x);
|
||||
c.put("y", center.y);
|
||||
c.put("z", center.z);
|
||||
node.put("center", c.entries);
|
||||
}
|
||||
/* Add seed locations, if any */
|
||||
if(seedloccfg.size() > 0) {
|
||||
ArrayList<Map<String,Object>> locs = new ArrayList<Map<String,Object>>();
|
||||
for(int i = 0; i < seedloccfg.size(); i++) {
|
||||
DynmapLocation dl = seedloccfg.get(i);
|
||||
ConfigurationNode ll = new ConfigurationNode();
|
||||
ll.put("x", dl.x);
|
||||
ll.put("y", dl.y);
|
||||
ll.put("z", dl.z);
|
||||
locs.add(ll.entries);
|
||||
}
|
||||
node.put("fullrenderlocations", locs);
|
||||
}
|
||||
/* Add flags */
|
||||
node.put("sendposition", sendposition);
|
||||
node.put("sendhealth", sendhealth);
|
||||
node.put("extrazoomout", extrazoomoutlevels);
|
||||
/* Save visibility limits, if defined */
|
||||
if(visibility_limits != null) {
|
||||
ArrayList<Map<String,Object>> lims = new ArrayList<Map<String,Object>>();
|
||||
for(int i = 0; i < visibility_limits.size(); i++) {
|
||||
VisibilityLimit lim = visibility_limits.get(i);
|
||||
LinkedHashMap<String, Object> lv = new LinkedHashMap<String,Object>();
|
||||
if (lim instanceof RectangleVisibilityLimit) {
|
||||
RectangleVisibilityLimit rect_lim = (RectangleVisibilityLimit) lim;
|
||||
lv.put("x0", rect_lim.x_min);
|
||||
lv.put("z0", rect_lim.z_min);
|
||||
lv.put("x1", rect_lim.x_max);
|
||||
lv.put("z1", rect_lim.z_max);
|
||||
}
|
||||
else {
|
||||
RoundVisibilityLimit round_lim = (RoundVisibilityLimit) lim;
|
||||
lv.put("x", round_lim.x_center);
|
||||
lv.put("z", round_lim.z_center);
|
||||
lv.put("r", round_lim.radius);
|
||||
}
|
||||
lims.add(lv);
|
||||
}
|
||||
node.put("visibilitylimits", lims);
|
||||
}
|
||||
/* Save hidden limits, if defined */
|
||||
if(hidden_limits != null) {
|
||||
ArrayList<Map<String,Object>> lims = new ArrayList<Map<String,Object>>();
|
||||
for(int i = 0; i < hidden_limits.size(); i++) {
|
||||
VisibilityLimit lim = hidden_limits.get(i);
|
||||
LinkedHashMap<String, Object> lv = new LinkedHashMap<String,Object>();
|
||||
if (lim instanceof RectangleVisibilityLimit) {
|
||||
RectangleVisibilityLimit rect_lim = (RectangleVisibilityLimit) lim;
|
||||
lv.put("x0", rect_lim.x_min);
|
||||
lv.put("z0", rect_lim.z_min);
|
||||
lv.put("x1", rect_lim.x_max);
|
||||
lv.put("z1", rect_lim.z_max);
|
||||
}
|
||||
else {
|
||||
RoundVisibilityLimit round_lim = (RoundVisibilityLimit) lim;
|
||||
lv.put("x", round_lim.x_center);
|
||||
lv.put("z", round_lim.z_center);
|
||||
lv.put("r", round_lim.radius);
|
||||
}
|
||||
lims.add(lv);
|
||||
}
|
||||
node.put("hiddenlimits", lims);
|
||||
}
|
||||
/* Handle hide style */
|
||||
node.put("hidestyle", hiddenchunkstyle.getValue());
|
||||
/* Handle map settings */
|
||||
ArrayList<Map<String,Object>> mapinfo = new ArrayList<Map<String,Object>>();
|
||||
for(MapType mt : maps) {
|
||||
ConfigurationNode mnode = mt.saveConfiguration();
|
||||
mapinfo.add(mnode);
|
||||
}
|
||||
node.put("maps", mapinfo);
|
||||
|
||||
return node;
|
||||
}
|
||||
public boolean isEnabled() {
|
||||
return is_enabled;
|
||||
}
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
public static String normalizeWorldName(String n) {
|
||||
return (n != null)?n.replace('/', '-').replace('[', '_').replace(']', '_'):null;
|
||||
}
|
||||
public String getRawName() {
|
||||
return raw_wname;
|
||||
}
|
||||
public boolean isProtected() {
|
||||
return is_protected;
|
||||
}
|
||||
public int getTileUpdateDelay() {
|
||||
if(tileupdatedelay > 0)
|
||||
return tileupdatedelay;
|
||||
else
|
||||
return MapManager.mapman.getDefTileUpdateDelay();
|
||||
}
|
||||
public void setTileUpdateDelay(int time_sec) {
|
||||
tileupdatedelay = time_sec;
|
||||
}
|
||||
public static void doInitialScan(boolean doscan) {
|
||||
}
|
||||
// Return number of chunks found (-1 if not implemented)
|
||||
public int getChunkMap(TileFlags map) {
|
||||
return -1;
|
||||
}
|
||||
// Get map state for given map
|
||||
public MapTypeState getMapState(MapType m) {
|
||||
for (int i = 0; i < this.maps.size(); i++) {
|
||||
MapType mt = this.maps.get(i);
|
||||
if (mt == m) {
|
||||
return this.mapstate.get(i);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void purgeTree() {
|
||||
storage.purgeMapTiles(this, null);
|
||||
}
|
||||
|
||||
public void purgeMap(MapType mt) {
|
||||
storage.purgeMapTiles(this, mt);
|
||||
}
|
||||
|
||||
public MapStorage getMapStorage() {
|
||||
return storage;
|
||||
}
|
||||
|
||||
public Polygon getWorldBorder() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
62
DynmapCore/src/main/java/org/dynmap/Event.java
Normal file
62
DynmapCore/src/main/java/org/dynmap/Event.java
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
package org.dynmap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
public class Event<T> {
|
||||
private List<Listener<T>> listeners = new LinkedList<Listener<T>>();
|
||||
private Object lock = new Object();
|
||||
|
||||
public void addListener(Listener<T> l) {
|
||||
synchronized(lock) {
|
||||
listeners.add(l);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeListener(Listener<T> l) {
|
||||
synchronized(lock) {
|
||||
listeners.remove(l);
|
||||
}
|
||||
}
|
||||
|
||||
/* Only use from main thread */
|
||||
public void trigger(T t) {
|
||||
ArrayList<Listener<T>> iterlist;
|
||||
synchronized(lock) {
|
||||
iterlist = new ArrayList<Listener<T>>(listeners);
|
||||
}
|
||||
for (Listener<T> l : iterlist) {
|
||||
l.triggered(t);
|
||||
}
|
||||
}
|
||||
|
||||
/* Trigger on main thread */
|
||||
public boolean triggerSync(DynmapCore core, final T t) {
|
||||
Future<T> future = core.getServer().callSyncMethod(new Callable<T>() {
|
||||
@Override
|
||||
public T call() throws Exception {
|
||||
trigger(t);
|
||||
return t;
|
||||
}
|
||||
});
|
||||
boolean success = false;
|
||||
try {
|
||||
if(future != null) {
|
||||
future.get();
|
||||
success = true;
|
||||
}
|
||||
} catch (ExecutionException ix) {
|
||||
Log.severe("Exception in triggerSync", ix.getCause());
|
||||
} catch (InterruptedException ix) {
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
public interface Listener<T> {
|
||||
void triggered(T t);
|
||||
}
|
||||
}
|
||||
44
DynmapCore/src/main/java/org/dynmap/Events.java
Normal file
44
DynmapCore/src/main/java/org/dynmap/Events.java
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package org.dynmap;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
public class Events {
|
||||
public Map<String, Event<?>> events = new HashMap<String, Event<?>>();
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> void addListener(String eventName, Event.Listener<T> listener) {
|
||||
Event<?> genericEvent = events.get(eventName);
|
||||
Event<T> event = null;
|
||||
if (genericEvent != null) {
|
||||
event = (Event<T>)genericEvent;
|
||||
} else {
|
||||
events.put(eventName, event = new Event<T>());
|
||||
}
|
||||
event.addListener(listener);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> void removeListener(String eventName, Event.Listener<T> listener) {
|
||||
Event<?> genericEvent = events.get(eventName);
|
||||
Event<T> event = null;
|
||||
if (genericEvent != null) {
|
||||
event = (Event<T>)genericEvent;
|
||||
event.removeListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> void trigger(String eventName, T argument) {
|
||||
Event<?> genericEvent = events.get(eventName);
|
||||
if (genericEvent == null)
|
||||
return;
|
||||
((Event<T>)genericEvent).trigger(argument);
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> void triggerSync(DynmapCore core, String eventName, T argument) {
|
||||
Event<?> genericEvent = events.get(eventName);
|
||||
if (genericEvent == null)
|
||||
return;
|
||||
((Event<T>)genericEvent).triggerSync(core, argument);
|
||||
}
|
||||
}
|
||||
5
DynmapCore/src/main/java/org/dynmap/Handler.java
Normal file
5
DynmapCore/src/main/java/org/dynmap/Handler.java
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package org.dynmap;
|
||||
|
||||
public interface Handler<T> {
|
||||
void handle(T t);
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
package org.dynmap;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.dynmap.servlet.ClientUpdateServlet;
|
||||
import org.dynmap.servlet.SendMessageServlet;
|
||||
import org.dynmap.utils.IpAddressMatcher;
|
||||
import org.json.simple.JSONObject;
|
||||
import static org.dynmap.JSONUtils.*;
|
||||
|
||||
public class InternalClientUpdateComponent extends ClientUpdateComponent {
|
||||
protected long jsonInterval;
|
||||
protected long currentTimestamp = 0;
|
||||
protected long lastTimestamp = 0;
|
||||
protected long lastChatTimestamp = 0;
|
||||
private long last_confighash;
|
||||
private ConcurrentHashMap<String, JSONObject> updates = new ConcurrentHashMap<String, JSONObject>();
|
||||
private JSONObject clientConfiguration = null;
|
||||
private static InternalClientUpdateComponent singleton;
|
||||
|
||||
public InternalClientUpdateComponent(final DynmapCore dcore, final ConfigurationNode configuration) {
|
||||
super(dcore, configuration);
|
||||
dcore.addServlet("/up/world/*", new ClientUpdateServlet(dcore));
|
||||
|
||||
if (dcore.isInternalWebServerDisabled) {
|
||||
Log.severe("Using InternalClientUpdateComponent with disable-webserver=true is not supported: there will likely be problems");
|
||||
}
|
||||
jsonInterval = (long)(configuration.getFloat("writeinterval", 1) * 1000);
|
||||
final Boolean allowwebchat = configuration.getBoolean("allowwebchat", false);
|
||||
final Boolean hidewebchatip = configuration.getBoolean("hidewebchatip", false);
|
||||
final Boolean trust_client_name = configuration.getBoolean("trustclientname", false);
|
||||
final float webchatInterval = configuration.getFloat("webchat-interval", 1);
|
||||
final String spammessage = dcore.configuration.getString("spammessage", "You may only chat once every %interval% seconds.");
|
||||
final Boolean use_player_ip = configuration.getBoolean("use-player-login-ip", true);
|
||||
final Boolean req_player_ip = configuration.getBoolean("require-player-login-ip", false);
|
||||
final Boolean block_banned_player_chat = configuration.getBoolean("block-banned-player-chat", false);
|
||||
final Boolean req_login = configuration.getBoolean("webchat-requires-login", false);
|
||||
final Boolean chat_perm = configuration.getBoolean("webchat-permissions", false);
|
||||
final int length_limit = configuration.getInteger("chatlengthlimit", 256);
|
||||
final List<String> trustedproxy = dcore.configuration.getStrings("trusted-proxies", null);
|
||||
|
||||
dcore.events.addListener("buildclientconfiguration", new Event.Listener<JSONObject>() {
|
||||
@Override
|
||||
public void triggered(JSONObject t) {
|
||||
s(t, "allowwebchat", allowwebchat);
|
||||
s(t, "webchat-interval", webchatInterval);
|
||||
s(t, "webchat-requires-login", req_login);
|
||||
s(t, "chatlengthlimit", length_limit);
|
||||
}
|
||||
});
|
||||
|
||||
if (allowwebchat) {
|
||||
@SuppressWarnings("serial")
|
||||
SendMessageServlet messageHandler = new SendMessageServlet() {{
|
||||
maximumMessageInterval = (int)(webchatInterval * 1000);
|
||||
spamMessage = "\""+spammessage+"\"";
|
||||
hideip = hidewebchatip;
|
||||
this.trustclientname = trust_client_name;
|
||||
this.use_player_login_ip = use_player_ip;
|
||||
this.require_player_login_ip = req_player_ip;
|
||||
this.check_user_ban = block_banned_player_chat;
|
||||
this.require_login = req_login;
|
||||
this.chat_perms = chat_perm;
|
||||
this.lengthlimit = length_limit;
|
||||
this.core = dcore;
|
||||
if(trustedproxy != null) {
|
||||
for(String s : trustedproxy) {
|
||||
this.proxyaddress.add(new IpAddressMatcher(s.trim()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.proxyaddress.add(new IpAddressMatcher("127.0.0.1"));
|
||||
this.proxyaddress.add(new IpAddressMatcher("0:0:0:0:0:0:0:1"));
|
||||
}
|
||||
onMessageReceived.addListener(new Event.Listener<Message> () {
|
||||
@Override
|
||||
public void triggered(Message t) {
|
||||
core.webChat(t.name, t.message);
|
||||
}
|
||||
});
|
||||
}};
|
||||
dcore.addServlet("/up/sendmessage", messageHandler);
|
||||
}
|
||||
core.getServer().scheduleServerTask(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
currentTimestamp = System.currentTimeMillis();
|
||||
if(last_confighash != core.getConfigHashcode()) {
|
||||
writeConfiguration();
|
||||
}
|
||||
writeUpdates();
|
||||
// if (allowwebchat) {
|
||||
// handleWebChat();
|
||||
// }
|
||||
// if(core.isLoginSupportEnabled())
|
||||
// handleRegister();
|
||||
lastTimestamp = currentTimestamp;
|
||||
core.getServer().scheduleServerTask(this, jsonInterval/50);
|
||||
}}, jsonInterval/50);
|
||||
|
||||
core.events.addListener("initialized", new Event.Listener<Object>() {
|
||||
@Override
|
||||
public void triggered(Object t) {
|
||||
writeConfiguration();
|
||||
writeUpdates(); /* Make sure we stay in sync */
|
||||
}
|
||||
});
|
||||
core.events.addListener("worldactivated", new Event.Listener<DynmapWorld>() {
|
||||
@Override
|
||||
public void triggered(DynmapWorld t) {
|
||||
writeConfiguration();
|
||||
writeUpdates(); /* Make sure we stay in sync */
|
||||
}
|
||||
});
|
||||
|
||||
/* Initialize */
|
||||
writeConfiguration();
|
||||
writeUpdates();
|
||||
|
||||
singleton = this;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
protected void writeUpdates() {
|
||||
if(core.mapManager == null) return;
|
||||
//Handles Updates
|
||||
for (DynmapWorld dynmapWorld : core.mapManager.getWorlds()) {
|
||||
JSONObject update = new JSONObject();
|
||||
update.put("timestamp", currentTimestamp);
|
||||
ClientUpdateEvent clientUpdate = new ClientUpdateEvent(currentTimestamp - 30000, dynmapWorld, update);
|
||||
clientUpdate.include_all_users = true;
|
||||
core.events.trigger("buildclientupdate", clientUpdate);
|
||||
|
||||
updates.put(dynmapWorld.getName(), update);
|
||||
}
|
||||
}
|
||||
protected void writeConfiguration() {
|
||||
JSONObject clientConfiguration = new JSONObject();
|
||||
core.events.trigger("buildclientconfiguration", clientConfiguration);
|
||||
this.clientConfiguration = clientConfiguration;
|
||||
last_confighash = core.getConfigHashcode();
|
||||
}
|
||||
public static JSONObject getWorldUpdate(String wname) {
|
||||
if(singleton != null) {
|
||||
return singleton.updates.get(wname);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public static JSONObject getClientConfig() {
|
||||
if(singleton != null)
|
||||
return singleton.clientConfiguration;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
72
DynmapCore/src/main/java/org/dynmap/JSONUtils.java
Normal file
72
DynmapCore/src/main/java/org/dynmap/JSONUtils.java
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
package org.dynmap;
|
||||
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
public class JSONUtils {
|
||||
|
||||
// Gets a value at the specified path.
|
||||
public static Object g(JSONObject o, String path) {
|
||||
int index = path.indexOf('/');
|
||||
if (index == -1) {
|
||||
return o.get(path);
|
||||
} else {
|
||||
String key = path.substring(0, index);
|
||||
String subpath = path.substring(index+1);
|
||||
Object oo = o.get(key);
|
||||
JSONObject subobject;
|
||||
if (oo == null) {
|
||||
return null;
|
||||
} else /*if (oo instanceof JSONObject)*/ {
|
||||
subobject = (JSONObject)o;
|
||||
}
|
||||
return g(subobject, subpath);
|
||||
}
|
||||
}
|
||||
|
||||
// Sets a value on the specified path. If JSONObjects inside the path are missing, they'll be created.
|
||||
@SuppressWarnings("unchecked")
|
||||
public static void s(JSONObject o, String path, Object value) {
|
||||
int index = path.indexOf('/');
|
||||
if (index == -1) {
|
||||
o.put(path, value);
|
||||
} else {
|
||||
String key = path.substring(0, index);
|
||||
String subpath = path.substring(index+1);
|
||||
Object oo = o.get(key);
|
||||
JSONObject subobject;
|
||||
if (oo == null) {
|
||||
subobject = new JSONObject();
|
||||
o.put(key, subobject);
|
||||
} else /*if (oo instanceof JSONObject)*/ {
|
||||
subobject = (JSONObject)oo;
|
||||
}
|
||||
s(subobject, subpath, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a value to the list at the specified path. If the list does not exist, it will be created.
|
||||
@SuppressWarnings("unchecked")
|
||||
public static void a(JSONObject o, String path, Object value) {
|
||||
Object oo = g(o, path);
|
||||
JSONArray array;
|
||||
if (oo == null) {
|
||||
array =new JSONArray();
|
||||
s(o, path, array);
|
||||
} else {
|
||||
array = (JSONArray)oo;
|
||||
}
|
||||
if(value != null)
|
||||
array.add(value);
|
||||
}
|
||||
|
||||
// Simply creates a JSONArray.
|
||||
@SuppressWarnings("unchecked")
|
||||
public static JSONArray l(Object... items) {
|
||||
JSONArray arr = new JSONArray();
|
||||
for(Object item : items) {
|
||||
arr.add(item);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,559 @@
|
|||
package org.dynmap;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.io.IOException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import org.dynmap.storage.MapStorage;
|
||||
import org.dynmap.utils.BufferInputStream;
|
||||
import org.dynmap.utils.BufferOutputStream;
|
||||
import org.dynmap.web.Json;
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.parser.JSONParser;
|
||||
import org.json.simple.parser.ParseException;
|
||||
|
||||
import static org.dynmap.JSONUtils.*;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class JsonFileClientUpdateComponent extends ClientUpdateComponent {
|
||||
protected long jsonInterval;
|
||||
protected long currentTimestamp = 0;
|
||||
protected long lastTimestamp = 0;
|
||||
protected long lastChatTimestamp = 0;
|
||||
protected JSONParser parser = new JSONParser();
|
||||
private boolean hidewebchatip;
|
||||
private boolean useplayerloginip;
|
||||
private boolean requireplayerloginip;
|
||||
private boolean trust_client_name;
|
||||
private boolean checkuserban;
|
||||
private boolean req_login;
|
||||
private boolean chat_perms;
|
||||
private int lengthlimit;
|
||||
private HashMap<String,String> useralias = new HashMap<String,String>();
|
||||
private int aliasindex = 1;
|
||||
private long last_confighash;
|
||||
private MessageDigest md;
|
||||
private MapStorage storage;
|
||||
private File baseStandaloneDir;
|
||||
|
||||
private static class FileToWrite {
|
||||
String filename;
|
||||
byte[] content;
|
||||
boolean phpwrapper;
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if(o instanceof FileToWrite) {
|
||||
return ((FileToWrite)o).filename.equals(this.filename);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
private class FileProcessor implements Runnable {
|
||||
public void run() {
|
||||
while(true) {
|
||||
FileToWrite f = null;
|
||||
synchronized(lock) {
|
||||
if(files_to_write.isEmpty() == false) {
|
||||
f = files_to_write.removeFirst();
|
||||
}
|
||||
else {
|
||||
pending = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
BufferOutputStream buf = null;
|
||||
if (f.content != null) {
|
||||
buf = new BufferOutputStream();
|
||||
if(f.phpwrapper) {
|
||||
buf.write("<?php /*\n".getBytes(cs_utf8));
|
||||
}
|
||||
buf.write(f.content);
|
||||
if(f.phpwrapper) {
|
||||
buf.write("\n*/ ?>\n".getBytes(cs_utf8));
|
||||
}
|
||||
}
|
||||
if (!storage.setStandaloneFile(f.filename, buf)) {
|
||||
Log.severe("Exception while writing JSON-file - " + f.filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private Object lock = new Object();
|
||||
private FileProcessor pending;
|
||||
private LinkedList<FileToWrite> files_to_write = new LinkedList<FileToWrite>();
|
||||
|
||||
private void enqueueFileWrite(String filename, byte[] content, boolean phpwrap) {
|
||||
FileToWrite ftw = new FileToWrite();
|
||||
ftw.filename = filename;
|
||||
ftw.content = content;
|
||||
ftw.phpwrapper = phpwrap;
|
||||
synchronized(lock) {
|
||||
boolean didadd = false;
|
||||
if(pending == null) {
|
||||
didadd = true;
|
||||
pending = new FileProcessor();
|
||||
}
|
||||
files_to_write.remove(ftw);
|
||||
files_to_write.add(ftw);
|
||||
if(didadd) {
|
||||
MapManager.scheduleDelayedJob(new FileProcessor(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Charset cs_utf8 = Charset.forName("UTF-8");
|
||||
public JsonFileClientUpdateComponent(final DynmapCore core, final ConfigurationNode configuration) {
|
||||
super(core, configuration);
|
||||
|
||||
if (!core.isInternalWebServerDisabled) {
|
||||
Log.severe("Using JsonFileClientUpdateComponent with disable-webserver=false is not supported: there will likely be problems");
|
||||
}
|
||||
|
||||
final boolean allowwebchat = configuration.getBoolean("allowwebchat", false);
|
||||
jsonInterval = (long)(configuration.getFloat("writeinterval", 1) * 1000);
|
||||
hidewebchatip = configuration.getBoolean("hidewebchatip", false);
|
||||
useplayerloginip = configuration.getBoolean("use-player-login-ip", true);
|
||||
requireplayerloginip = configuration.getBoolean("require-player-login-ip", false);
|
||||
trust_client_name = configuration.getBoolean("trustclientname", false);
|
||||
checkuserban = configuration.getBoolean("block-banned-player-chat", true);
|
||||
req_login = configuration.getBoolean("webchat-requires-login", false);
|
||||
chat_perms = configuration.getBoolean("webchat-permissions", false);
|
||||
lengthlimit = configuration.getInteger("chatlengthlimit", 256);
|
||||
storage = core.getDefaultMapStorage();
|
||||
baseStandaloneDir = new File(core.configuration.getString("webpath", "web"), "standalone");
|
||||
if (!baseStandaloneDir.isAbsolute()) {
|
||||
baseStandaloneDir = new File(core.getDataFolder(), baseStandaloneDir.toString());
|
||||
}
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA-1");
|
||||
} catch (NoSuchAlgorithmException nsax) {
|
||||
Log.severe("Unable to get message digest SHA-1");
|
||||
}
|
||||
/* Generate our config.js file */
|
||||
generateConfigJS(core);
|
||||
|
||||
core.getServer().scheduleServerTask(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
currentTimestamp = System.currentTimeMillis();
|
||||
if(last_confighash != core.getConfigHashcode()) {
|
||||
writeConfiguration();
|
||||
}
|
||||
writeUpdates();
|
||||
if (allowwebchat) {
|
||||
handleWebChat();
|
||||
}
|
||||
if(core.isLoginSupportEnabled())
|
||||
handleRegister();
|
||||
lastTimestamp = currentTimestamp;
|
||||
core.getServer().scheduleServerTask(this, jsonInterval/50);
|
||||
}}, jsonInterval/50);
|
||||
|
||||
core.events.addListener("buildclientconfiguration", new Event.Listener<JSONObject>() {
|
||||
@Override
|
||||
public void triggered(JSONObject t) {
|
||||
s(t, "jsonfile", true);
|
||||
s(t, "allowwebchat", allowwebchat);
|
||||
s(t, "webchat-requires-login", req_login);
|
||||
s(t, "loginrequired", core.isLoginRequired());
|
||||
// For 'sendmessage.php'
|
||||
s(t, "webchat-interval", configuration.getFloat("webchat-interval", 5.0f));
|
||||
s(t, "chatlengthlimit", lengthlimit);
|
||||
}
|
||||
});
|
||||
core.events.addListener("initialized", new Event.Listener<Object>() {
|
||||
@Override
|
||||
public void triggered(Object t) {
|
||||
writeConfiguration();
|
||||
writeUpdates(); /* Make sure we stay in sync */
|
||||
writeLogins();
|
||||
writeAccess();
|
||||
}
|
||||
});
|
||||
core.events.addListener("server-started", new Event.Listener<Object>() {
|
||||
@Override
|
||||
public void triggered(Object t) {
|
||||
writeConfiguration();
|
||||
writeUpdates(); /* Make sure we stay in sync */
|
||||
writeLogins();
|
||||
writeAccess();
|
||||
}
|
||||
});
|
||||
core.events.addListener("worldactivated", new Event.Listener<DynmapWorld>() {
|
||||
@Override
|
||||
public void triggered(DynmapWorld t) {
|
||||
writeConfiguration();
|
||||
writeUpdates(); /* Make sure we stay in sync */
|
||||
writeAccess();
|
||||
}
|
||||
});
|
||||
core.events.addListener("loginupdated", new Event.Listener<Object>() {
|
||||
@Override
|
||||
public void triggered(Object t) {
|
||||
writeLogins();
|
||||
writeAccess();
|
||||
}
|
||||
});
|
||||
core.events.addListener("playersetupdated", new Event.Listener<Object>() {
|
||||
@Override
|
||||
public void triggered(Object t) {
|
||||
writeAccess();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void generateConfigJS(DynmapCore core) {
|
||||
/* Test if login support is enabled */
|
||||
boolean login_enabled = core.isLoginSupportEnabled();
|
||||
|
||||
// configuration: 'standalone/dynmap_config.json?_={timestamp}',
|
||||
// update: 'standalone/dynmap_{world}.json?_={timestamp}',
|
||||
// sendmessage: 'standalone/sendmessage.php',
|
||||
// login: 'standalone/login.php',
|
||||
// register: 'standalone/register.php',
|
||||
// tiles : 'tiles/',
|
||||
// markers : 'tiles/'
|
||||
|
||||
// configuration: 'standalone/configuration.php',
|
||||
// update: 'standalone/update.php?world={world}&ts={timestamp}',
|
||||
// sendmessage: 'standalone/sendmessage.php',
|
||||
// login: 'standalone/login.php',
|
||||
// register: 'standalone/register.php',
|
||||
// tiles : 'standalone/tiles.php?tile=',
|
||||
// markers : 'standalone/markers.php?marker='
|
||||
|
||||
MapStorage store = core.getDefaultMapStorage();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("var config = {\n");
|
||||
sb.append(" url : {\n");
|
||||
/* Get configuration URL */
|
||||
sb.append(" configuration: '");
|
||||
sb.append(core.configuration.getString("url/configuration", store.getConfigurationJSONURI(login_enabled)));
|
||||
sb.append("',\n");
|
||||
/* Get update URL */
|
||||
sb.append(" update: '");
|
||||
sb.append(core.configuration.getString("url/update", store.getUpdateJSONURI(login_enabled)));
|
||||
sb.append("',\n");
|
||||
/* Get sendmessage URL */
|
||||
sb.append(" sendmessage: '");
|
||||
sb.append(core.configuration.getString("url/sendmessage", store.getSendMessageURI()));
|
||||
sb.append("',\n");
|
||||
/* Get login URL */
|
||||
sb.append(" login: '");
|
||||
sb.append(core.configuration.getString("url/login", store.getStandaloneLoginURI()));
|
||||
sb.append("',\n");
|
||||
/* Get register URL */
|
||||
sb.append(" register: '");
|
||||
sb.append(core.configuration.getString("url/register", store.getStandaloneRegisterURI()));
|
||||
sb.append("',\n");
|
||||
/* Get tiles URL */
|
||||
sb.append(" tiles: '");
|
||||
sb.append(core.configuration.getString("url/tiles", store.getTilesURI(login_enabled)));
|
||||
sb.append("',\n");
|
||||
/* Get markers URL */
|
||||
sb.append(" markers: '");
|
||||
sb.append(core.configuration.getString("url/markers", store.getMarkersURI(login_enabled)));
|
||||
sb.append("'\n }\n};\n");
|
||||
|
||||
byte[] outputBytes = sb.toString().getBytes(cs_utf8);
|
||||
MapManager.scheduleDelayedJob(new Runnable() {
|
||||
public void run() {
|
||||
if (core.getDefaultMapStorage().needsStaticWebFiles()) {
|
||||
BufferOutputStream os = new BufferOutputStream();
|
||||
os.write(outputBytes);
|
||||
core.getDefaultMapStorage().setStaticWebFile("standalone/config.js", os);
|
||||
}
|
||||
else {
|
||||
File f = new File(baseStandaloneDir, "config.js");
|
||||
FileOutputStream fos = null;
|
||||
try {
|
||||
fos = new FileOutputStream(f);
|
||||
fos.write(outputBytes);
|
||||
} catch (IOException iox) {
|
||||
Log.severe("Exception while writing " + f.getPath(), iox);
|
||||
} finally {
|
||||
if(fos != null) {
|
||||
try {
|
||||
fos.close();
|
||||
} catch (IOException x) {}
|
||||
fos = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
protected void writeConfiguration() {
|
||||
JSONObject clientConfiguration = new JSONObject();
|
||||
core.events.trigger("buildclientconfiguration", clientConfiguration);
|
||||
last_confighash = core.getConfigHashcode();
|
||||
|
||||
byte[] content = clientConfiguration.toJSONString().getBytes(cs_utf8);
|
||||
|
||||
String outputFile;
|
||||
boolean dowrap = storage.wrapStandaloneJSON(core.isLoginSupportEnabled());
|
||||
if(dowrap) {
|
||||
outputFile = "dynmap_config.php";
|
||||
}
|
||||
else {
|
||||
outputFile = "dynmap_config.json";
|
||||
}
|
||||
|
||||
enqueueFileWrite(outputFile, content, dowrap);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected void writeUpdates() {
|
||||
if(core.mapManager == null) return;
|
||||
//Handles Updates
|
||||
ArrayList<DynmapWorld> wlist = new ArrayList<DynmapWorld>(core.mapManager.getWorlds()); // Grab copy of world list
|
||||
for (int windx = 0; windx < wlist.size(); windx++) {
|
||||
DynmapWorld dynmapWorld = wlist.get(windx);
|
||||
JSONObject update = new JSONObject();
|
||||
update.put("timestamp", currentTimestamp);
|
||||
ClientUpdateEvent clientUpdate = new ClientUpdateEvent(currentTimestamp - 30000, dynmapWorld, update);
|
||||
clientUpdate.include_all_users = true;
|
||||
core.events.trigger("buildclientupdate", clientUpdate);
|
||||
|
||||
String outputFile;
|
||||
boolean dowrap = storage.wrapStandaloneJSON(core.isLoginSupportEnabled());
|
||||
if(dowrap) {
|
||||
outputFile = "updates_" + dynmapWorld.getName() + ".php";
|
||||
}
|
||||
else {
|
||||
outputFile = "dynmap_" + dynmapWorld.getName() + ".json";
|
||||
}
|
||||
|
||||
CompletableFuture.runAsync(() -> {
|
||||
byte[] content = Json.stringifyJson(update).getBytes(cs_utf8);
|
||||
|
||||
enqueueFileWrite(outputFile, content, dowrap);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] loginhash = new byte[16];
|
||||
|
||||
protected void writeLogins() {
|
||||
String loginFile = "dynmap_login.php";
|
||||
|
||||
if(core.isLoginSupportEnabled()) {
|
||||
String s = core.getLoginPHP(storage.wrapStandalonePHP());
|
||||
if(s != null) {
|
||||
byte[] bytes = s.getBytes(cs_utf8);
|
||||
md.reset();
|
||||
byte[] hash = md.digest(bytes);
|
||||
if(Arrays.equals(hash, loginhash)) {
|
||||
return;
|
||||
}
|
||||
enqueueFileWrite(loginFile, bytes, false);
|
||||
loginhash = hash;
|
||||
}
|
||||
}
|
||||
else {
|
||||
enqueueFileWrite(loginFile, null, false);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] accesshash = new byte[16];
|
||||
|
||||
protected void writeAccess() {
|
||||
String accessFile = "dynmap_access.php";
|
||||
|
||||
String s = core.getAccessPHP(storage.wrapStandalonePHP());
|
||||
if(s != null) {
|
||||
byte[] bytes = s.getBytes(cs_utf8);
|
||||
md.reset();
|
||||
byte[] hash = md.digest(bytes);
|
||||
if(Arrays.equals(hash, accesshash)) {
|
||||
return;
|
||||
}
|
||||
enqueueFileWrite(accessFile, bytes, false);
|
||||
accesshash = hash;
|
||||
}
|
||||
}
|
||||
|
||||
private void processWebChat(JSONArray jsonMsgs) {
|
||||
Iterator<?> iter = jsonMsgs.iterator();
|
||||
boolean init_skip = (lastChatTimestamp == 0);
|
||||
while (iter.hasNext()) {
|
||||
boolean ok = true;
|
||||
JSONObject o = (JSONObject) iter.next();
|
||||
String ts = String.valueOf(o.get("timestamp"));
|
||||
if(ts.equals("null")) ts = "0";
|
||||
long cts;
|
||||
try {
|
||||
cts = Long.parseLong(ts);
|
||||
} catch (NumberFormatException nfx) {
|
||||
try {
|
||||
cts = (long) Double.parseDouble(ts);
|
||||
} catch (NumberFormatException nfx2) {
|
||||
cts = 0;
|
||||
}
|
||||
}
|
||||
if (cts > lastChatTimestamp) {
|
||||
String name = String.valueOf(o.get("name"));
|
||||
String ip = String.valueOf(o.get("ip"));
|
||||
String uid = null;
|
||||
Object usr = o.get("userid");
|
||||
if(usr != null) {
|
||||
uid = String.valueOf(usr);
|
||||
}
|
||||
boolean isip = true;
|
||||
lastChatTimestamp = cts;
|
||||
if(init_skip)
|
||||
continue;
|
||||
if(uid == null) {
|
||||
if((!trust_client_name) || (name == null) || (name.equals(""))) {
|
||||
if(ip != null)
|
||||
name = ip;
|
||||
}
|
||||
if(useplayerloginip) { /* Try to match using IPs of player logins */
|
||||
List<String> ids = core.getIDsForIP(name);
|
||||
if(ids != null && !ids.isEmpty()) {
|
||||
name = ids.get(0);
|
||||
isip = false;
|
||||
if(checkuserban) {
|
||||
if(core.getServer().isPlayerBanned(name)) {
|
||||
Log.info("Ignore message from '" + ip + "' - banned player (" + name + ")");
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if(chat_perms && !core.getServer().checkPlayerPermission(name, "webchat")) {
|
||||
Log.info("Rejected web chat from " + ip + ": not permitted (" + name + ")");
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
else if(requireplayerloginip) {
|
||||
Log.info("Ignore message from '" + name + "' - no matching player login recorded");
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if(hidewebchatip && isip) {
|
||||
String n = useralias.get(name);
|
||||
if(n == null) { /* Make ID */
|
||||
n = String.format("web-%03d", aliasindex);
|
||||
aliasindex++;
|
||||
useralias.put(name, n);
|
||||
}
|
||||
name = n;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(core.getServer().isPlayerBanned(uid)) {
|
||||
Log.info("Ignore message from '" + uid + "' - banned user");
|
||||
ok = false;
|
||||
}
|
||||
if(chat_perms && !core.getServer().checkPlayerPermission(uid, "webchat")) {
|
||||
Log.info("Rejected web chat from " + uid + ": not permitted");
|
||||
ok = false;
|
||||
}
|
||||
name = uid;
|
||||
}
|
||||
if(ok) {
|
||||
String message = String.valueOf(o.get("message"));
|
||||
if((lengthlimit > 0) && (message.length() > lengthlimit))
|
||||
message = message.substring(0, lengthlimit);
|
||||
core.webChat(name, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleWebChat() {
|
||||
MapManager.scheduleDelayedJob(new Runnable() {
|
||||
public void run() {
|
||||
BufferInputStream bis = storage.getStandaloneFile("dynmap_webchat.json");
|
||||
if (bis != null && lastTimestamp != 0) {
|
||||
JSONArray jsonMsgs = null;
|
||||
Reader inputFileReader = null;
|
||||
try {
|
||||
inputFileReader = new InputStreamReader(bis, cs_utf8);
|
||||
jsonMsgs = (JSONArray) parser.parse(inputFileReader);
|
||||
} catch (IOException ex) {
|
||||
Log.severe("Exception while reading JSON-file.", ex);
|
||||
storage.setStandaloneFile("dynmap_webchat.json", null); // Delete it
|
||||
} catch (ParseException ex) {
|
||||
Log.severe("Exception while parsing JSON-file.", ex);
|
||||
storage.setStandaloneFile("dynmap_webchat.json", null); // Delete it
|
||||
} finally {
|
||||
if(inputFileReader != null) {
|
||||
try {
|
||||
inputFileReader.close();
|
||||
} catch (IOException iox) {
|
||||
|
||||
}
|
||||
inputFileReader = null;
|
||||
}
|
||||
}
|
||||
if (jsonMsgs != null) {
|
||||
final JSONArray json = jsonMsgs;
|
||||
// Process content on server thread
|
||||
core.getServer().scheduleServerTask(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
processWebChat(json);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
protected void handleRegister() {
|
||||
if(core.pendingRegisters() == false)
|
||||
return;
|
||||
BufferInputStream bis = storage.getStandaloneFile("dynmap_reg.php");
|
||||
if (bis != null) {
|
||||
BufferedReader br = null;
|
||||
ArrayList<String> lines = new ArrayList<String>();
|
||||
try {
|
||||
br = new BufferedReader(new InputStreamReader(bis));
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
if(line.startsWith("<?") || line.startsWith("*/")) {
|
||||
continue;
|
||||
}
|
||||
lines.add(line);
|
||||
}
|
||||
} catch (IOException iox) {
|
||||
Log.severe("Exception while reading dynmap_reg.php", iox);
|
||||
} finally {
|
||||
if (br != null) {
|
||||
try {
|
||||
br.close();
|
||||
} catch (IOException x) {
|
||||
}
|
||||
br = null;
|
||||
}
|
||||
}
|
||||
for(int i = 0; i < lines.size(); i++) {
|
||||
String[] vals = lines.get(i).split("=");
|
||||
if(vals.length == 3) {
|
||||
core.processCompletedRegister(vals[0].trim(), vals[1].trim(), vals[2].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
93
DynmapCore/src/main/java/org/dynmap/Log.java
Normal file
93
DynmapCore/src/main/java/org/dynmap/Log.java
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
package org.dynmap;
|
||||
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.dynmap.utils.DynmapLogger;
|
||||
|
||||
public class Log {
|
||||
private static Logger log = Logger.getLogger("Dynmap");
|
||||
private static String prefix = "";
|
||||
private static DynmapLogger dlog = null;
|
||||
public static boolean verbose = false;
|
||||
|
||||
public static String safeString(String s) { return s.replaceAll("[\\${}]", "_"); }
|
||||
|
||||
public static void setLogger(Logger logger, String pre) {
|
||||
log = logger;
|
||||
if((pre != null) && (pre.length() > 0))
|
||||
prefix = pre + " ";
|
||||
else
|
||||
prefix = "";
|
||||
}
|
||||
public static void setLogger(DynmapLogger logger) {
|
||||
dlog = logger;
|
||||
}
|
||||
public static void setLoggerParent(Logger parent) {
|
||||
log.setParent(parent);
|
||||
}
|
||||
public static void info(String msg) {
|
||||
msg = safeString(msg);
|
||||
if (dlog != null) {
|
||||
dlog.info(msg);
|
||||
}
|
||||
else {
|
||||
log.log(Level.INFO, prefix + msg);
|
||||
}
|
||||
}
|
||||
public static void verboseinfo(String msg) {
|
||||
if(verbose) {
|
||||
msg = safeString(msg);
|
||||
if (dlog != null) {
|
||||
dlog.info(msg);
|
||||
}
|
||||
else {
|
||||
log.log(Level.INFO, prefix + msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
public static void severe(Throwable e) {
|
||||
if (dlog != null) {
|
||||
dlog.severe(e);
|
||||
}
|
||||
else {
|
||||
log.log(Level.SEVERE, prefix + "Exception occured: ", e);
|
||||
}
|
||||
}
|
||||
public static void severe(String msg) {
|
||||
msg = safeString(msg);
|
||||
if (dlog != null) {
|
||||
dlog.severe(msg);
|
||||
}
|
||||
else {
|
||||
log.log(Level.SEVERE, prefix + msg);
|
||||
}
|
||||
}
|
||||
public static void severe(String msg, Throwable e) {
|
||||
msg = safeString(msg);
|
||||
if (dlog != null) {
|
||||
dlog.severe(msg, e);
|
||||
}
|
||||
else {
|
||||
log.log(Level.SEVERE, prefix + msg, e);
|
||||
}
|
||||
}
|
||||
public static void warning(String msg) {
|
||||
msg = safeString(msg);
|
||||
if (dlog != null) {
|
||||
dlog.warning(msg);
|
||||
}
|
||||
else {
|
||||
log.log(Level.WARNING, prefix + msg);
|
||||
}
|
||||
}
|
||||
public static void warning(String msg, Throwable e) {
|
||||
msg = safeString(msg);
|
||||
if (dlog != null) {
|
||||
dlog.warning(msg, e);
|
||||
}
|
||||
else {
|
||||
log.log(Level.WARNING, prefix + msg, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
2019
DynmapCore/src/main/java/org/dynmap/MapManager.java
Normal file
2019
DynmapCore/src/main/java/org/dynmap/MapManager.java
Normal file
File diff suppressed because it is too large
Load diff
64
DynmapCore/src/main/java/org/dynmap/MapTile.java
Normal file
64
DynmapCore/src/main/java/org/dynmap/MapTile.java
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
package org.dynmap;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.List;
|
||||
|
||||
import org.dynmap.utils.MapChunkCache;
|
||||
|
||||
public abstract class MapTile {
|
||||
protected DynmapWorld world;
|
||||
|
||||
public abstract boolean render(MapChunkCache cache, String mapname);
|
||||
public abstract List<DynmapChunk> getRequiredChunks();
|
||||
public abstract MapTile[] getAdjecentTiles();
|
||||
public abstract int getTileSize();
|
||||
|
||||
public DynmapWorld getDynmapWorld() {
|
||||
return world;
|
||||
}
|
||||
|
||||
public MapTile(DynmapWorld world) {
|
||||
this.world = world;
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract int hashCode();
|
||||
|
||||
@Override
|
||||
public abstract boolean equals(Object obj);
|
||||
|
||||
public abstract boolean isBiomeDataNeeded();
|
||||
public abstract boolean isHightestBlockYDataNeeded();
|
||||
public abstract boolean isRawBiomeDataNeeded();
|
||||
public abstract boolean isBlockTypeDataNeeded();
|
||||
|
||||
public abstract int tileOrdinalX();
|
||||
public abstract int tileOrdinalY();
|
||||
|
||||
public ConfigurationNode saveTile() {
|
||||
ConfigurationNode cn = new ConfigurationNode();
|
||||
cn.put("class", this.getClass().getName());
|
||||
cn.put("data", saveTileData());
|
||||
return cn;
|
||||
}
|
||||
|
||||
protected abstract String saveTileData();
|
||||
|
||||
public static MapTile restoreTile(DynmapWorld w, ConfigurationNode node) {
|
||||
String cn = node.getString("class");
|
||||
String dat = node.getString("data");
|
||||
if((cn == null) || (dat == null)) return null;
|
||||
try {
|
||||
Class<?> cls = Class.forName(cn);
|
||||
Constructor<?> con = cls.getConstructor(DynmapWorld.class, String.class);
|
||||
return (MapTile)con.newInstance(w, dat);
|
||||
} catch (ClassNotFoundException cnfx) {
|
||||
} catch (NoSuchMethodException nsmx) {
|
||||
} catch (InvocationTargetException itx) {
|
||||
} catch (IllegalAccessException iax) {
|
||||
} catch (InstantiationException ix) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
254
DynmapCore/src/main/java/org/dynmap/MapType.java
Normal file
254
DynmapCore/src/main/java/org/dynmap/MapType.java
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
package org.dynmap;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import org.dynmap.utils.TileFlags;
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
public abstract class MapType {
|
||||
private boolean is_protected;
|
||||
/**
|
||||
* Is the map type read-only? (i.e. should not be updated by renderer)
|
||||
*/
|
||||
private boolean is_readonly;
|
||||
protected int tileupdatedelay;
|
||||
|
||||
public enum ImageVariant {
|
||||
STANDARD(""), // Typical image
|
||||
DAY("day"); // Day (no shadow) image
|
||||
public final String variantSuffix;
|
||||
public final String variantID;
|
||||
|
||||
ImageVariant(String varid) {
|
||||
if (varid.length() > 0) {
|
||||
variantSuffix = "_" + varid;
|
||||
}
|
||||
else {
|
||||
variantSuffix = "";
|
||||
}
|
||||
variantID = varid;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ImageEncoding {
|
||||
PNG("png", "image/png", true), JPG("jpg", "image/jpeg", false), WEBP("webp", "image/webp", true);
|
||||
public final String ext;
|
||||
public final String mimetype;
|
||||
public final boolean hasAlpha;
|
||||
|
||||
ImageEncoding(String ext, String mime, boolean has_alpha) {
|
||||
this.ext = ext;
|
||||
this.mimetype = mime;
|
||||
this.hasAlpha = has_alpha;
|
||||
}
|
||||
public String getFileExt() { return ext; }
|
||||
public String getContentType() { return mimetype; }
|
||||
|
||||
public static ImageEncoding fromOrd(int ix) {
|
||||
ImageEncoding[] v = values();
|
||||
if ((ix >= 0) && (ix < v.length))
|
||||
return v[ix];
|
||||
return null;
|
||||
}
|
||||
public static ImageEncoding fromContentType(String ct) {
|
||||
ImageEncoding[] v = values();
|
||||
for (int i = 0; i < v.length; i++) {
|
||||
if (v[i].mimetype.equalsIgnoreCase(ct)) {
|
||||
return v[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public static ImageEncoding fromExt(String x) {
|
||||
ImageEncoding[] v = values();
|
||||
for (int i = 0; i < v.length; i++) {
|
||||
if (v[i].ext.equalsIgnoreCase(x)) {
|
||||
return v[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ImageFormat {
|
||||
FORMAT_PNG("png", 0.0f, ImageEncoding.PNG),
|
||||
FORMAT_JPG75("jpg-q75", 0.75f, ImageEncoding.JPG),
|
||||
FORMAT_JPG80("jpg-q80", 0.80f, ImageEncoding.JPG),
|
||||
FORMAT_JPG85("jpg-q85", 0.85f, ImageEncoding.JPG),
|
||||
FORMAT_JPG("jpg", 0.85f, ImageEncoding.JPG),
|
||||
FORMAT_JPG90("jpg-q90", 0.90f, ImageEncoding.JPG),
|
||||
FORMAT_JPG95("jpg-q95", 0.95f, ImageEncoding.JPG),
|
||||
FORMAT_JPG100("jpg-q100", 1.00f, ImageEncoding.JPG),
|
||||
FORMAT_WEBP75("webp-q75", 75, ImageEncoding.WEBP),
|
||||
FORMAT_WEBP80("webp-q80", 80, ImageEncoding.WEBP),
|
||||
FORMAT_WEBP85("webp-q85", 85, ImageEncoding.WEBP),
|
||||
FORMAT_WEBP("webp", 85, ImageEncoding.WEBP),
|
||||
FORMAT_WEBP90("webp-q90", 90, ImageEncoding.WEBP),
|
||||
FORMAT_WEBP95("webp-q95", 95, ImageEncoding.WEBP),
|
||||
FORMAT_WEBP100("webp-q100", 100, ImageEncoding.WEBP),
|
||||
FORMAT_WEBPL("webp-l", 85, ImageEncoding.WEBP);
|
||||
String id;
|
||||
float qual;
|
||||
ImageEncoding enc;
|
||||
|
||||
ImageFormat(String id, float quality, ImageEncoding enc) {
|
||||
this.id = id;
|
||||
this.qual = quality;
|
||||
this.enc = enc;
|
||||
}
|
||||
public String getID() { return id; }
|
||||
public String getFileExt() { return enc.getFileExt(); }
|
||||
public float getQuality() { return qual; }
|
||||
public ImageEncoding getEncoding() { return enc; }
|
||||
|
||||
public static ImageFormat fromID(String imgfmt) {
|
||||
for(ImageFormat i_f : MapType.ImageFormat.values()) {
|
||||
if(i_f.getID().equals(imgfmt)) {
|
||||
return i_f;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
public static class ZoomInfo {
|
||||
public String prefix;
|
||||
public int background_argb;
|
||||
public ZoomInfo(String pre, int bg) { prefix = pre; background_argb = bg; }
|
||||
}
|
||||
|
||||
public abstract void addMapTiles(List<MapTile> list, DynmapWorld w, int tx, int ty);
|
||||
|
||||
public abstract List<TileFlags.TileCoord> getTileCoords(DynmapWorld w, int x, int y, int z);
|
||||
|
||||
public abstract List<TileFlags.TileCoord> getTileCoords(DynmapWorld w, int minx, int miny, int minz, int maxx, int maxy, int maxz);
|
||||
|
||||
public abstract MapTile[] getAdjecentTiles(MapTile tile);
|
||||
|
||||
public abstract List<DynmapChunk> getRequiredChunks(MapTile tile);
|
||||
|
||||
public abstract int getTileSize();
|
||||
|
||||
public void buildClientConfiguration(JSONObject worldObject, DynmapWorld w) {
|
||||
}
|
||||
|
||||
public List<MapTile> getTiles(DynmapWorld w, int x, int y, int z) {
|
||||
List<TileFlags.TileCoord> coords = this.getTileCoords(w, x, y, z);
|
||||
ArrayList<MapTile> tiles = new ArrayList<MapTile>();
|
||||
for(TileFlags.TileCoord c : coords) {
|
||||
this.addMapTiles(tiles, w, c.x, c.y);
|
||||
}
|
||||
return tiles;
|
||||
}
|
||||
|
||||
public abstract String getName();
|
||||
|
||||
/* Get maps rendered concurrently with this map in this world */
|
||||
public abstract List<MapType> getMapsSharingRender(DynmapWorld w);
|
||||
/* Get names of maps rendered concurrently with this map type in this world */
|
||||
public abstract List<String> getMapNamesSharingRender(DynmapWorld w);
|
||||
|
||||
/* Return number of zoom levels needed by this map (before extra levels from extrazoomout) */
|
||||
public int getMapZoomOutLevels() { return 0; }
|
||||
|
||||
public ImageFormat getImageFormat() { return ImageFormat.FORMAT_PNG; }
|
||||
|
||||
public int getBackgroundARGBNight() { return 0; }
|
||||
|
||||
public int getBackgroundARGBDay() { return 0; }
|
||||
|
||||
public int getBackgroundARGB(ImageVariant var) {
|
||||
if (var == ImageVariant.DAY)
|
||||
return getBackgroundARGBDay();
|
||||
else
|
||||
return getBackgroundARGBNight();
|
||||
}
|
||||
|
||||
public void purgeOldTiles(DynmapWorld world, TileFlags rendered) { }
|
||||
|
||||
public interface FileCallback {
|
||||
public void fileFound(File f, File parent, boolean day);
|
||||
}
|
||||
|
||||
protected void walkMapTree(File root, FileCallback cb, boolean day) {
|
||||
LinkedList<File> dirs = new LinkedList<File>();
|
||||
String ext = "." + getImageFormat().getFileExt();
|
||||
dirs.add(root);
|
||||
while(dirs.isEmpty() == false) {
|
||||
File dir = dirs.pop();
|
||||
String[] lst = dir.list();
|
||||
if(lst == null) continue;
|
||||
for(String fn : lst) {
|
||||
if(fn.equals(".") || fn.equals(".."))
|
||||
continue;
|
||||
File f = new File(dir, fn);
|
||||
if(f.isDirectory()) { /* If directory, add to list to process */
|
||||
dirs.add(f);
|
||||
}
|
||||
else if(fn.endsWith(ext)) { /* Else, if matches suffix */
|
||||
cb.fileFound(f, dir, day);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ConfigurationNode saveConfiguration() {
|
||||
ConfigurationNode cn = new ConfigurationNode();
|
||||
cn.put("class", this.getClass().getName()); /* Add class */
|
||||
cn.put("name", getName()); /* Get map name */
|
||||
return cn;
|
||||
}
|
||||
public boolean isProtected() {
|
||||
return is_protected;
|
||||
}
|
||||
public boolean setProtected(boolean p) {
|
||||
if(is_protected != p) {
|
||||
is_protected = p;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Is the map type read-only? (i.e. should not be updated by renderer)
|
||||
* @return true if read-only
|
||||
*/
|
||||
public boolean isReadOnly() {
|
||||
return is_readonly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set read-only state of map type
|
||||
* @param r - true if read-only
|
||||
* @return true if state changed
|
||||
*/
|
||||
public boolean setReadOnly(boolean r) {
|
||||
if(is_readonly != r) {
|
||||
is_readonly = r;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public abstract String getPrefix();
|
||||
|
||||
public int getTileUpdateDelay(DynmapWorld w) {
|
||||
if(tileupdatedelay > 0)
|
||||
return tileupdatedelay;
|
||||
else
|
||||
return w.getTileUpdateDelay();
|
||||
}
|
||||
public boolean setTileUpdateDelay(int delay) {
|
||||
if(tileupdatedelay != delay) {
|
||||
tileupdatedelay = delay;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private static final ImageVariant[] defVariant = { ImageVariant.STANDARD };
|
||||
|
||||
public ImageVariant[] getVariants() {
|
||||
return defVariant;
|
||||
}
|
||||
}
|
||||
277
DynmapCore/src/main/java/org/dynmap/MapTypeState.java
Normal file
277
DynmapCore/src/main/java/org/dynmap/MapTypeState.java
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
package org.dynmap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.dynmap.utils.TileFlags;
|
||||
|
||||
public class MapTypeState {
|
||||
public static final long DEF_INV_PERIOD = 30;
|
||||
public static final long NANOS_PER_SECOND = 1000000000L;
|
||||
public MapType type;
|
||||
private Object invTileLock = new Object();
|
||||
private TileFlags pendingInvTiles = new TileFlags();
|
||||
private TileFlags pendingInvTilesAlt = new TileFlags();
|
||||
private TileFlags invTiles = new TileFlags();
|
||||
private TileFlags.Iterator invTilesIter = invTiles.getIterator();
|
||||
private long nextInvTS;
|
||||
private long invTSPeriod;
|
||||
private ArrayList<TileFlags> zoomOutInvAccum = new ArrayList<TileFlags>();
|
||||
private ArrayList<TileFlags> zoomOutInv = new ArrayList<TileFlags>();
|
||||
private TileFlags.Iterator zoomOutInvIter = null;
|
||||
private int zoomOutInvIterLevel = -1;
|
||||
private final int zoomOutLevels;
|
||||
public final int tileSize;
|
||||
|
||||
public MapTypeState(DynmapWorld world, MapType mt) {
|
||||
type = mt;
|
||||
invTSPeriod = DEF_INV_PERIOD * NANOS_PER_SECOND;
|
||||
nextInvTS = System.nanoTime() + invTSPeriod;
|
||||
zoomOutLevels = world.getExtraZoomOutLevels() + mt.getMapZoomOutLevels();
|
||||
for (int i = 0; i < zoomOutLevels; i++) {
|
||||
zoomOutInv.add(null);
|
||||
zoomOutInvAccum.add(null);
|
||||
}
|
||||
tileSize = mt.getTileSize();
|
||||
}
|
||||
public void setInvalidatePeriod(long inv_per_in_secs) {
|
||||
invTSPeriod = inv_per_in_secs * NANOS_PER_SECOND;
|
||||
}
|
||||
|
||||
public boolean invalidateTile(int tx, int ty) {
|
||||
boolean done;
|
||||
synchronized(invTileLock) {
|
||||
done = !pendingInvTiles.setFlag(tx, ty, true);
|
||||
}
|
||||
return done;
|
||||
}
|
||||
|
||||
public int invalidateTiles(List<TileFlags.TileCoord> coords) {
|
||||
int cnt = 0;
|
||||
synchronized(invTileLock) {
|
||||
for(TileFlags.TileCoord c : coords) {
|
||||
if(!pendingInvTiles.setFlag(c.x, c.y, true)) {
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cnt;
|
||||
}
|
||||
|
||||
public void tickMapTypeState(long now_nano) {
|
||||
if(nextInvTS < now_nano) {
|
||||
synchronized(invTileLock) {
|
||||
TileFlags tmp = pendingInvTilesAlt;
|
||||
pendingInvTilesAlt = pendingInvTiles;
|
||||
pendingInvTiles = tmp;
|
||||
invTiles.union(tmp);
|
||||
tmp.clear();
|
||||
nextInvTS = now_nano + invTSPeriod;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean getNextInvalidTileCoord(TileFlags.TileCoord coord) {
|
||||
boolean match;
|
||||
synchronized(invTileLock) {
|
||||
match = invTilesIter.next(coord);
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
public void validateTile(int tx, int ty) {
|
||||
synchronized(invTileLock) {
|
||||
invTiles.setFlag(tx, ty, false);
|
||||
pendingInvTiles.setFlag(tx, ty, false);
|
||||
pendingInvTilesAlt.setFlag(tx, ty, false);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isInvalidTile(int tx, int ty) {
|
||||
synchronized(invTileLock) {
|
||||
return invTiles.getFlag(tx, ty);
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> save() {
|
||||
synchronized(invTileLock) {
|
||||
invTiles.union(pendingInvTiles);
|
||||
invTiles.union(pendingInvTilesAlt);
|
||||
pendingInvTiles.clear();
|
||||
pendingInvTilesAlt.clear();
|
||||
return invTiles.save();
|
||||
}
|
||||
}
|
||||
public void restore(List<String> saved) {
|
||||
synchronized(invTileLock) {
|
||||
TileFlags tf = new TileFlags();
|
||||
tf.load(saved);
|
||||
invTiles.union(tf);
|
||||
}
|
||||
}
|
||||
|
||||
public List<List<String>> saveZoomOut() {
|
||||
ArrayList<List<String>> rslt = new ArrayList<List<String>>();
|
||||
synchronized(invTileLock) {
|
||||
boolean empty = true;
|
||||
for (TileFlags tf : zoomOutInv) {
|
||||
List<String> val;
|
||||
if (tf == null) {
|
||||
val = Collections.emptyList();
|
||||
}
|
||||
else {
|
||||
val = tf.save();
|
||||
if (val == null) {
|
||||
val = Collections.emptyList();
|
||||
}
|
||||
else {
|
||||
empty = false;
|
||||
}
|
||||
}
|
||||
rslt.add(val);
|
||||
}
|
||||
for (TileFlags tf : zoomOutInvAccum) {
|
||||
List<String> val;
|
||||
if (tf == null) {
|
||||
val = Collections.emptyList();
|
||||
}
|
||||
else {
|
||||
val = tf.save();
|
||||
if (val == null) {
|
||||
val = Collections.emptyList();
|
||||
}
|
||||
else {
|
||||
empty = false;
|
||||
}
|
||||
}
|
||||
rslt.add(val);
|
||||
}
|
||||
if (empty) {
|
||||
rslt = null;
|
||||
}
|
||||
}
|
||||
return rslt;
|
||||
}
|
||||
|
||||
public void restoreZoomOut(List<List<String>> dat) {
|
||||
synchronized(invTileLock) {
|
||||
int cnt = dat.size();
|
||||
int cntaccum = 0;
|
||||
if (cnt > zoomOutInv.size()) {
|
||||
if (cnt == (2*zoomOutInv.size())) {
|
||||
cntaccum = cnt / 2;
|
||||
}
|
||||
cnt = zoomOutInv.size();
|
||||
}
|
||||
for (int i = 0; i < cnt; i++) {
|
||||
List<String> lst = dat.get(i);
|
||||
TileFlags tf = null;
|
||||
if ((lst != null) && (lst.size() > 0)) {
|
||||
tf = new TileFlags();
|
||||
tf.load(lst);
|
||||
}
|
||||
zoomOutInv.set(i, tf);
|
||||
}
|
||||
for (int i = 0; i < cntaccum; i++) {
|
||||
List<String> lst = dat.get(i + cnt);
|
||||
TileFlags tf = null;
|
||||
if ((lst != null) && (lst.size() > 0)) {
|
||||
tf = new TileFlags();
|
||||
tf.load(lst);
|
||||
}
|
||||
zoomOutInvAccum.set(i, tf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getInvCount() {
|
||||
synchronized(invTileLock) {
|
||||
return invTiles.countFlags();
|
||||
}
|
||||
}
|
||||
public void clear() {
|
||||
synchronized(invTileLock) {
|
||||
invTiles.clear();
|
||||
}
|
||||
}
|
||||
// Set to zoom out accum
|
||||
public void setZoomOutInv(int x, int y, int zoomlevel) {
|
||||
if (zoomlevel >= zoomOutLevels) {
|
||||
return;
|
||||
}
|
||||
synchronized(invTileLock) {
|
||||
TileFlags tf = zoomOutInvAccum.get(zoomlevel);
|
||||
if (tf == null) {
|
||||
tf = new TileFlags();
|
||||
zoomOutInvAccum.set(zoomlevel, tf);
|
||||
}
|
||||
if ((((x >> zoomlevel) << zoomlevel) != x) ||
|
||||
(((y >> zoomlevel) << zoomlevel) != y)) {
|
||||
Log.info("setZoomOutInv(" + x + "," + y + "," + zoomlevel + ")");
|
||||
}
|
||||
tf.setFlag(x >> zoomlevel, y >> zoomlevel, true);
|
||||
}
|
||||
}
|
||||
// Clear flag in active zoom out flags
|
||||
public boolean clearZoomOutInv(int x, int y, int zoomlevel) {
|
||||
if (zoomlevel >= zoomOutLevels) {
|
||||
return false;
|
||||
}
|
||||
synchronized(invTileLock) {
|
||||
TileFlags tf = zoomOutInv.get(zoomlevel);
|
||||
if (tf == null) {
|
||||
return false;
|
||||
}
|
||||
return tf.setFlag(x >> zoomlevel, y >> zoomlevel, false);
|
||||
}
|
||||
}
|
||||
public static class ZoomOutCoord extends TileFlags.TileCoord {
|
||||
public int zoomlevel;
|
||||
}
|
||||
// Start zoom out iteration (stash and reset accumulator)
|
||||
public void startZoomOutIter() {
|
||||
synchronized(invTileLock) {
|
||||
ArrayList<TileFlags> tmplist = zoomOutInv;
|
||||
zoomOutInv = zoomOutInvAccum;
|
||||
for (int i = 0; i < tmplist.size(); i++) {
|
||||
tmplist.set(i, null);
|
||||
}
|
||||
zoomOutInvAccum = tmplist;
|
||||
zoomOutInvIter = null;
|
||||
zoomOutInvIterLevel = 0;
|
||||
}
|
||||
}
|
||||
public boolean nextZoomOutInv(ZoomOutCoord coord) {
|
||||
synchronized(invTileLock) {
|
||||
// Try existing iterator
|
||||
if (zoomOutInvIter != null) {
|
||||
if (zoomOutInvIter.hasNext()) {
|
||||
zoomOutInvIter.next(coord);
|
||||
coord.zoomlevel = zoomOutInvIterLevel;
|
||||
coord.x = coord.x << zoomOutInvIterLevel;
|
||||
coord.y = coord.y << zoomOutInvIterLevel;
|
||||
return true;
|
||||
}
|
||||
zoomOutInvIter = null;
|
||||
}
|
||||
for (; zoomOutInvIterLevel < zoomOutInv.size(); zoomOutInvIterLevel++) {
|
||||
TileFlags tf = zoomOutInv.get(zoomOutInvIterLevel);
|
||||
if (tf != null) {
|
||||
zoomOutInvIter = tf.getIterator();
|
||||
if (zoomOutInvIter.hasNext()) {
|
||||
zoomOutInvIter.next(coord);
|
||||
coord.zoomlevel = zoomOutInvIterLevel;
|
||||
coord.x = coord.x << zoomOutInvIterLevel;
|
||||
coord.y = coord.y << zoomOutInvIterLevel;
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
zoomOutInvIter = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
313
DynmapCore/src/main/java/org/dynmap/MarkersComponent.java
Normal file
313
DynmapCore/src/main/java/org/dynmap/MarkersComponent.java
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
package org.dynmap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.dynmap.common.DynmapListenerManager.EventType;
|
||||
import org.dynmap.common.DynmapListenerManager.WorldEventListener;
|
||||
import org.dynmap.common.DynmapListenerManager.PlayerEventListener;
|
||||
import org.dynmap.common.DynmapPlayer;
|
||||
import org.dynmap.markers.AreaMarker;
|
||||
import org.dynmap.markers.Marker;
|
||||
import org.dynmap.markers.MarkerAPI;
|
||||
import org.dynmap.markers.MarkerIcon;
|
||||
import org.dynmap.markers.MarkerSet;
|
||||
import org.dynmap.markers.impl.MarkerSignManager;
|
||||
import org.dynmap.utils.Polygon;
|
||||
|
||||
/**
|
||||
* Markers component - ties in the component system, both on the server and client
|
||||
*/
|
||||
public class MarkersComponent extends ClientComponent {
|
||||
private MarkerAPI api;
|
||||
private MarkerSignManager signmgr;
|
||||
private MarkerIcon spawnicon;
|
||||
private String spawnlbl;
|
||||
private String worldborderlbl;
|
||||
private MarkerSet offlineset;
|
||||
private MarkerIcon offlineicon;
|
||||
private MarkerSet spawnbedset;
|
||||
private MarkerIcon spawnbedicon;
|
||||
private String spawnbedformat;
|
||||
private boolean removebedonplayerleave;
|
||||
private long maxofflineage;
|
||||
private boolean showSpawn;
|
||||
private boolean showBorder;
|
||||
private HashMap<String, Long> offline_times = new HashMap<String, Long>();
|
||||
private static final String OFFLINE_PLAYERS_SETID = "offline_players";
|
||||
private static final String PLAYER_SPAWN_BED_SETID = "spawn_beds";
|
||||
|
||||
public MarkersComponent(final DynmapCore core, ConfigurationNode configuration) {
|
||||
super(core, configuration);
|
||||
|
||||
api = core.getMarkerAPI();
|
||||
|
||||
/* If configuration has enabled sign support, prime it too */
|
||||
if(configuration.getBoolean("enablesigns", false)) {
|
||||
signmgr = MarkerSignManager.initializeSignManager(core, configuration.getString("default-sign-set", MarkerSet.DEFAULT));
|
||||
}
|
||||
showBorder = configuration.getBoolean("showworldborder", false);
|
||||
showSpawn = configuration.getBoolean("showspawn", false);
|
||||
/* If we're posting spawn point markers, initialize and add world listener */
|
||||
if(showSpawn) {
|
||||
String ico = configuration.getString("spawnicon", MarkerIcon.WORLD);
|
||||
spawnlbl = configuration.getString("spawnlabel", "Spawn");
|
||||
spawnicon = api.getMarkerIcon(ico); /* Load it */
|
||||
if(spawnicon == null) {
|
||||
spawnicon = api.getMarkerIcon(MarkerIcon.WORLD);
|
||||
}
|
||||
}
|
||||
if (showBorder) {
|
||||
worldborderlbl = configuration.getString("worldborderlabel", "Border");
|
||||
}
|
||||
if (showSpawn || showBorder) {
|
||||
/* Add listener for world loads */
|
||||
WorldEventListener wel = new WorldEventListener() {
|
||||
@Override
|
||||
public void worldEvent(DynmapWorld w) {
|
||||
DynmapLocation loc = w.getSpawnLocation(); /* Get location of spawn */
|
||||
if(loc != null)
|
||||
addUpdateWorld(w, loc);
|
||||
}
|
||||
};
|
||||
core.listenerManager.addListener(EventType.WORLD_LOAD, wel);
|
||||
/* Add listener for spawn changes */
|
||||
core.listenerManager.addListener(EventType.WORLD_SPAWN_CHANGE, wel);
|
||||
|
||||
/* Initialize already loaded worlds */
|
||||
for(DynmapWorld w : core.getMapManager().getWorlds()) {
|
||||
DynmapLocation loc = w.getSpawnLocation();
|
||||
if(loc != null)
|
||||
addUpdateWorld(w, loc);
|
||||
}
|
||||
}
|
||||
/* If showing offline players as markers */
|
||||
if(configuration.getBoolean("showofflineplayers", false)) {
|
||||
/* Make set, if needed */
|
||||
offlineset = api.getMarkerSet(OFFLINE_PLAYERS_SETID);
|
||||
if(offlineset == null) {
|
||||
offlineset = api.createMarkerSet(OFFLINE_PLAYERS_SETID, configuration.getString("offlinelabel", "Offline"), null, true);
|
||||
}
|
||||
offlineset.setHideByDefault(configuration.getBoolean("offlinehidebydefault", true));
|
||||
offlineset.setMinZoom(configuration.getInteger("offlineminzoom", 0));
|
||||
maxofflineage = 60000L * configuration.getInteger("maxofflinetime", 30); /* 30 minutes */
|
||||
/* Now, see if existing offline markers - check for last login on their users */
|
||||
if(maxofflineage > 0) {
|
||||
Set<Marker> prev_m = offlineset.getMarkers();
|
||||
for(Marker m : prev_m) {
|
||||
DynmapPlayer p = core.getServer().getOfflinePlayer(m.getMarkerID());
|
||||
if(p != null) {
|
||||
long ageout = p.getLastLoginTime() + maxofflineage;
|
||||
if(ageout < System.currentTimeMillis()) {
|
||||
m.deleteMarker();
|
||||
}
|
||||
else {
|
||||
offline_times.put(p.getName(), ageout);
|
||||
}
|
||||
}
|
||||
else {
|
||||
m.deleteMarker();
|
||||
}
|
||||
}
|
||||
}
|
||||
offlineicon = api.getMarkerIcon(configuration.getString("offlineicon", "offlineuser"));
|
||||
if(maxofflineage > 0) {
|
||||
core.getServer().scheduleServerTask(new Runnable() {
|
||||
public void run() {
|
||||
long ts = System.currentTimeMillis();
|
||||
ArrayList<String> deleted = new ArrayList<String>();
|
||||
for(Map.Entry<String,Long> me : offline_times.entrySet()) {
|
||||
if(ts > me.getValue()) {
|
||||
deleted.add(me.getKey());
|
||||
}
|
||||
}
|
||||
for(String id : deleted) {
|
||||
Marker m = offlineset.findMarker(id);
|
||||
if(m != null)
|
||||
m.deleteMarker();
|
||||
}
|
||||
core.getServer().scheduleServerTask(this, 30 * 20);
|
||||
}
|
||||
}, 30 * 20); /* Check every 30 seconds */
|
||||
}
|
||||
/* Add listener for players coming and going */
|
||||
core.listenerManager.addListener(EventType.PLAYER_JOIN, new PlayerEventListener() {
|
||||
@Override
|
||||
public void playerEvent(DynmapPlayer p) {
|
||||
Marker m = offlineset.findMarker(p.getName());
|
||||
if(m != null) {
|
||||
m.deleteMarker();
|
||||
offline_times.remove(p.getName());
|
||||
}
|
||||
}
|
||||
});
|
||||
core.listenerManager.addListener(EventType.PLAYER_QUIT, new PlayerEventListener() {
|
||||
@Override
|
||||
public void playerEvent(DynmapPlayer p) {
|
||||
String pname = p.getName();
|
||||
Marker m = offlineset.findMarker(pname);
|
||||
if(m != null) {
|
||||
m.deleteMarker();
|
||||
offline_times.remove(p.getName());
|
||||
}
|
||||
if(core.playerList.isVisiblePlayer(pname)) {
|
||||
DynmapLocation loc = p.getLocation();
|
||||
m = offlineset.createMarker(p.getName(), core.getServer().stripChatColor(p.getDisplayName()), false,
|
||||
loc.world, loc.x, loc.y, loc.z, offlineicon, true);
|
||||
if(maxofflineage > 0)
|
||||
offline_times.put(p.getName(), System.currentTimeMillis() + maxofflineage);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
/* Make set, if needed */
|
||||
offlineset = api.getMarkerSet(OFFLINE_PLAYERS_SETID);
|
||||
if(offlineset != null) {
|
||||
offlineset.deleteMarkerSet();
|
||||
}
|
||||
}
|
||||
/* If showing player spawn bed locations as markers */
|
||||
if(configuration.getBoolean("showspawnbeds", false)) {
|
||||
/* Make set, if needed */
|
||||
spawnbedset = api.getMarkerSet(PLAYER_SPAWN_BED_SETID);
|
||||
if(spawnbedset == null) {
|
||||
spawnbedset = api.createMarkerSet(PLAYER_SPAWN_BED_SETID, configuration.getString("spawnbedlabel", "Spawn Beds"), null, true);
|
||||
}
|
||||
spawnbedset.setHideByDefault(configuration.getBoolean("spawnbedhidebydefault", true));
|
||||
spawnbedset.setMinZoom(configuration.getInteger("spawnbedminzoom", 0));
|
||||
|
||||
spawnbedicon = api.getMarkerIcon(configuration.getString("spawnbedicon", "bed"));
|
||||
spawnbedformat = configuration.getString("spawnbedformat", "%name%'s bed");
|
||||
removebedonplayerleave = configuration.getBoolean("spawnbedremoveonplayerleave", true);
|
||||
/* Add listener for players coming and going */
|
||||
core.listenerManager.addListener(EventType.PLAYER_JOIN, new PlayerEventListener() {
|
||||
@Override
|
||||
public void playerEvent(DynmapPlayer p) {
|
||||
updatePlayer(p);
|
||||
}
|
||||
});
|
||||
if (removebedonplayerleave) {
|
||||
core.listenerManager.addListener(EventType.PLAYER_QUIT, new PlayerEventListener() {
|
||||
@Override
|
||||
public void playerEvent(DynmapPlayer p) {
|
||||
Marker m = spawnbedset.findMarker(p.getName() + "_bed");
|
||||
if (m != null) {
|
||||
m.deleteMarker();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
core.listenerManager.addListener(EventType.PLAYER_BED_LEAVE, new PlayerEventListener() {
|
||||
@Override
|
||||
public void playerEvent(final DynmapPlayer p) {
|
||||
core.getServer().scheduleServerTask(new Runnable() {
|
||||
public void run() {
|
||||
updatePlayer(p);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
/* Make set, if needed */
|
||||
spawnbedset = api.getMarkerSet(PLAYER_SPAWN_BED_SETID);
|
||||
if(spawnbedset != null) {
|
||||
spawnbedset.deleteMarkerSet();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePlayer(DynmapPlayer p) {
|
||||
DynmapLocation bl = p.getBedSpawnLocation();
|
||||
Marker m = spawnbedset.findMarker(p.getName()+"_bed");
|
||||
if(bl == null) { /* No bed location */
|
||||
if(m != null) {
|
||||
m.deleteMarker();
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(m != null)
|
||||
m.setLocation(bl.world, bl.x, bl.y, bl.z);
|
||||
else
|
||||
m = spawnbedset.createMarker(p.getName()+"_bed", spawnbedformat.replace("%name%", core.getServer().stripChatColor(p.getDisplayName())), false,
|
||||
bl.world, bl.x, bl.y, bl.z,
|
||||
spawnbedicon, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void addUpdateWorld(DynmapWorld w, DynmapLocation loc) {
|
||||
MarkerSet ms = api.getMarkerSet(MarkerSet.DEFAULT);
|
||||
if(ms != null) {
|
||||
String spawnid = "_spawn_" + w.getName();
|
||||
Marker m = ms.findMarker(spawnid); /* See if defined */
|
||||
if (showSpawn) {
|
||||
if(m == null) { /* Not defined yet, add it */
|
||||
ms.createMarker(spawnid, spawnlbl, w.getName(), loc.x, loc.y, loc.z,
|
||||
spawnicon, false);
|
||||
}
|
||||
else {
|
||||
m.setLocation(w.getName(), loc.x, loc.y, loc.z);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (m != null) {
|
||||
m.deleteMarker();
|
||||
}
|
||||
}
|
||||
String borderid = "_worldborder_" + w.getName();
|
||||
AreaMarker am = ms.findAreaMarker(borderid);
|
||||
Polygon p = null;
|
||||
if (showBorder && w.showborder) {
|
||||
p = w.getWorldBorder();
|
||||
}
|
||||
if ((p != null) && (p.size() > 1)) {
|
||||
double[] x;
|
||||
double[] z;
|
||||
if (p.size() == 2) {
|
||||
x = new double[4];
|
||||
z = new double[4];
|
||||
Polygon.Point2D p0 = p.getVertex(0);
|
||||
Polygon.Point2D p1 = p.getVertex(1);
|
||||
x[0] = p0.x; z[0] = p0.y;
|
||||
x[1] = p0.x; z[1] = p1.y;
|
||||
x[2] = p1.x; z[2] = p1.y;
|
||||
x[3] = p1.x; z[3] = p0.y;
|
||||
}
|
||||
else {
|
||||
int sz = p.size();
|
||||
x = new double[sz];
|
||||
z = new double[sz];
|
||||
for (int i = 0; i < sz; i++) {
|
||||
Polygon.Point2D pi = p.getVertex(i);
|
||||
x[i] = pi.x; z[i] = pi.y;
|
||||
}
|
||||
}
|
||||
if (am == null) {
|
||||
am = ms.createAreaMarker(borderid, worldborderlbl, false, w.getName(), x, z, false);
|
||||
}
|
||||
else {
|
||||
am.setCornerLocations(x, z);
|
||||
}
|
||||
am.setFillStyle(0.0, 0);
|
||||
}
|
||||
else {
|
||||
if (am != null) {
|
||||
am.deleteMarker();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
if(signmgr != null) {
|
||||
MarkerSignManager.terminateSignManager(this.core);
|
||||
signmgr = null;
|
||||
}
|
||||
/* Don't unregister API - other plugins might be using it, and we want to keep non-persistent markers */
|
||||
}
|
||||
}
|
||||
311
DynmapCore/src/main/java/org/dynmap/PlayerFaces.java
Normal file
311
DynmapCore/src/main/java/org/dynmap/PlayerFaces.java
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
package org.dynmap;
|
||||
|
||||
import org.dynmap.MapType.ImageFormat;
|
||||
import org.dynmap.common.DynmapListenerManager.EventType;
|
||||
import org.dynmap.common.DynmapListenerManager.PlayerEventListener;
|
||||
import org.dynmap.common.DynmapPlayer;
|
||||
import org.dynmap.debug.Debug;
|
||||
import org.dynmap.storage.MapStorage;
|
||||
import org.dynmap.utils.BufferOutputStream;
|
||||
import org.dynmap.utils.DynmapBufferedImage;
|
||||
import org.dynmap.utils.ImageIOManager;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Listen for player logins, and process player faces by fetching skins *
|
||||
*/
|
||||
public class PlayerFaces {
|
||||
private boolean fetchskins;
|
||||
private boolean refreshskins;
|
||||
private String skinurl;
|
||||
public MapStorage storage;
|
||||
|
||||
public enum FaceType {
|
||||
FACE_8X8("8x8", 0),
|
||||
FACE_16X16("16x16", 1),
|
||||
FACE_32X32("32x32", 2),
|
||||
BODY_32X32("body", 3);
|
||||
|
||||
public final String id;
|
||||
public final int typeID;
|
||||
|
||||
FaceType(String id, int typeid) {
|
||||
this.id = id;
|
||||
this.typeID = typeid;
|
||||
}
|
||||
public static FaceType byID(String i_d) {
|
||||
for (FaceType ft : values()) {
|
||||
if (ft.id.equals(i_d)) {
|
||||
return ft;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public static FaceType byTypeID(int tid) {
|
||||
for (FaceType ft : values()) {
|
||||
if (ft.typeID == tid) {
|
||||
return ft;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void copyLayersToTarget(BufferedImage srcimg, int layer1x, int layer1y, int layer2x, int layer2y, int w, int h, int[] dest, int destoff, int destscansize)
|
||||
{
|
||||
int[] l1 = new int[w * h];
|
||||
int[] l2 = new int[w * h];
|
||||
int imgh = srcimg.getHeight();
|
||||
// Read layer 1
|
||||
if (imgh >= (layer1y+h))
|
||||
srcimg.getRGB(layer1x, layer1y, w, h, l1, 0, w);
|
||||
// Read layer 2
|
||||
if (imgh >= (layer2y+h))
|
||||
srcimg.getRGB(layer2x, layer2y, w, h, l2, 0, w);
|
||||
// Apply layer1 to layer 1
|
||||
boolean transp = false;
|
||||
int v = l2[0];
|
||||
for (int i = 0; i < (w*h); i++) {
|
||||
if ((l2[i] & 0xFF000000) == 0) {
|
||||
transp = true;
|
||||
break;
|
||||
}
|
||||
/* If any different values, render face too */
|
||||
else if (l2[i] != v) {
|
||||
transp = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(transp) {
|
||||
for (int i = 0; i < (w*h); i++) {
|
||||
if ((l2[i] & 0xFF000000) != 0)
|
||||
l1[i] = l2[i];
|
||||
}
|
||||
}
|
||||
// Write to dest
|
||||
for (int y = 0; y < h; y++) {
|
||||
for (int x = 0; x < w; x++) {
|
||||
dest[destoff + (y*destscansize + x)] = l1[(y*w)+x];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void copyLayersToTarget(BufferedImage srcimg, int layer1x, int layer1y, int layer2x, int layer2y, int w, int h, BufferedImage dest, int destoff, int destscansize)
|
||||
{
|
||||
int[] tmp = new int[w*h];
|
||||
copyLayersToTarget(srcimg,layer1x,layer1y,layer2x,layer2y,w,h,tmp,0,w);
|
||||
dest.setRGB(0,0,w,h,tmp,destoff,destscansize);
|
||||
}
|
||||
|
||||
private class LoadPlayerImages implements Runnable {
|
||||
private SkinUrlProvider mSkinUrlProvider;
|
||||
public final String playername;
|
||||
public final UUID playeruuid;
|
||||
public final String playerskinurl;
|
||||
|
||||
public LoadPlayerImages(String playername, String playerskinurl, UUID playeruuid, SkinUrlProvider skinUrlProvider) {
|
||||
this.playername = playername;
|
||||
this.playeruuid = playeruuid;
|
||||
this.playerskinurl = playerskinurl;
|
||||
mSkinUrlProvider = skinUrlProvider;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
boolean has_8x8 = storage.hasPlayerFaceImage(playername, FaceType.FACE_8X8);
|
||||
boolean has_16x16 = storage.hasPlayerFaceImage(playername, FaceType.FACE_16X16);
|
||||
boolean has_32x32 = storage.hasPlayerFaceImage(playername, FaceType.FACE_32X32);
|
||||
boolean has_body = storage.hasPlayerFaceImage(playername, FaceType.BODY_32X32);
|
||||
boolean missing_any = !(has_8x8 && has_16x16 && has_32x32 && has_body);
|
||||
boolean is_64x32_skin = false;
|
||||
|
||||
BufferedImage img = null;
|
||||
try {
|
||||
if(fetchskins && (refreshskins || missing_any)) {
|
||||
URL url = null;
|
||||
|
||||
if (mSkinUrlProvider == null) {
|
||||
if (!skinurl.equals("")) {
|
||||
url = new URL(skinurl.replace("%player%", URLEncoder.encode(playername, "UTF-8"))
|
||||
.replace("%uuid%", playeruuid.toString()));
|
||||
} else if (playerskinurl != null) {
|
||||
url = new URL(playerskinurl);
|
||||
}
|
||||
} else {
|
||||
url = mSkinUrlProvider.getSkinUrl(playername);
|
||||
}
|
||||
|
||||
if (url != null)
|
||||
img = ImageIO.read(url); /* Load skin for player */
|
||||
}
|
||||
} catch (IOException iox) {
|
||||
Debug.debug("Error loading skin for '" + playername + "' - " + iox);
|
||||
}
|
||||
if(img == null) {
|
||||
try {
|
||||
InputStream in = getClass().getResourceAsStream("/char.png");
|
||||
img = ImageIO.read(in); /* Load generic skin for player */
|
||||
in.close();
|
||||
} catch (IOException iox) {
|
||||
Debug.debug("Error loading default skin for '" + playername + "' - " + iox);
|
||||
}
|
||||
}
|
||||
if(img == null) { /* No image to process? Quit */
|
||||
return;
|
||||
}
|
||||
if((img.getWidth() < 64) || (img.getHeight() < 32)) {
|
||||
img.flush();
|
||||
return;
|
||||
}
|
||||
else if( (img.getWidth() / img.getHeight()) == 2 ) { /* Is single layer skin? */
|
||||
is_64x32_skin = true;
|
||||
}
|
||||
|
||||
/* Get buffered image for face at original size */
|
||||
int scale = img.getWidth()/8 /8;
|
||||
BufferedImage faceOriginal = new BufferedImage(8*scale, 8*scale, BufferedImage.TYPE_INT_ARGB);
|
||||
// Copy face and overlay to icon
|
||||
copyLayersToTarget(img, 8*scale, 8*scale, 40*scale, 8*scale, 8*scale, 8*scale, faceOriginal, 0, 8*scale);
|
||||
|
||||
/* Get buffered image for face at 8x8 */
|
||||
DynmapBufferedImage face8x8 = DynmapBufferedImage.allocateBufferedImage(8, 8);
|
||||
Image face8x8_image = faceOriginal.getScaledInstance(8,8,BufferedImage.SCALE_SMOOTH);
|
||||
BufferedImage face8x8_buff = new BufferedImage(8, 8, BufferedImage.TYPE_INT_ARGB);
|
||||
|
||||
face8x8_buff.getGraphics().drawImage(face8x8_image,0,0,null);
|
||||
face8x8_buff.getRGB(0,0,8,8,face8x8.argb_buf,0,8);
|
||||
/* Write 8x8 file */
|
||||
if(refreshskins || (!has_8x8)) {
|
||||
BufferOutputStream bos = ImageIOManager.imageIOEncode(face8x8.buf_img, ImageFormat.FORMAT_PNG);
|
||||
if (bos != null) {
|
||||
storage.setPlayerFaceImage(playername, FaceType.FACE_8X8, bos);
|
||||
}
|
||||
}
|
||||
/* Write 16x16 file */
|
||||
if(refreshskins || (!has_16x16)) {
|
||||
/* Make 16x16 version */
|
||||
DynmapBufferedImage face16x16 = DynmapBufferedImage.allocateBufferedImage(16, 16);
|
||||
Image face16x16_image = faceOriginal.getScaledInstance(16,16,BufferedImage.SCALE_SMOOTH);
|
||||
BufferedImage face16x16_buff = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
|
||||
|
||||
face16x16_buff.getGraphics().drawImage(face16x16_image,0,0,null);
|
||||
face16x16_buff.getRGB(0,0,16,16,face16x16.argb_buf,0,16);
|
||||
|
||||
BufferOutputStream bos = ImageIOManager.imageIOEncode(face16x16.buf_img, ImageFormat.FORMAT_PNG);
|
||||
if (bos != null) {
|
||||
storage.setPlayerFaceImage(playername, FaceType.FACE_16X16, bos);
|
||||
}
|
||||
DynmapBufferedImage.freeBufferedImage(face16x16);
|
||||
face16x16_buff.flush();
|
||||
}
|
||||
|
||||
/* Write 32x32 file */
|
||||
if(refreshskins || (!has_32x32)) {
|
||||
/* Make 32x32 version */
|
||||
DynmapBufferedImage face32x32 = DynmapBufferedImage.allocateBufferedImage(32, 32);
|
||||
Image face32x32_image = faceOriginal.getScaledInstance(32,32,BufferedImage.SCALE_SMOOTH);
|
||||
BufferedImage face32x32_buff = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
|
||||
|
||||
face32x32_buff.getGraphics().drawImage(face32x32_image,0,0,null);
|
||||
face32x32_buff.getRGB(0,0,32,32,face32x32.argb_buf,0,32);
|
||||
|
||||
BufferOutputStream bos = ImageIOManager.imageIOEncode(face32x32.buf_img, ImageFormat.FORMAT_PNG);
|
||||
if (bos != null) {
|
||||
storage.setPlayerFaceImage(playername, FaceType.FACE_32X32, bos);
|
||||
}
|
||||
DynmapBufferedImage.freeBufferedImage(face32x32);
|
||||
face32x32_buff.flush();
|
||||
}
|
||||
|
||||
/* Write body file */
|
||||
if(refreshskins || (!has_body)) {
|
||||
|
||||
Image skin_image = null;
|
||||
BufferedImage skin_buff = null;
|
||||
|
||||
if (is_64x32_skin){
|
||||
|
||||
skin_image = img.getScaledInstance(64,32,BufferedImage.SCALE_SMOOTH);
|
||||
skin_buff = new BufferedImage(64, 32, BufferedImage.TYPE_INT_ARGB);
|
||||
|
||||
skin_buff.getGraphics().drawImage(skin_image,0,0,null);
|
||||
|
||||
} else {
|
||||
|
||||
skin_image = img.getScaledInstance(64,64,BufferedImage.SCALE_SMOOTH);
|
||||
skin_buff = new BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB);
|
||||
|
||||
skin_buff.getGraphics().drawImage(skin_image,0,0,null);
|
||||
|
||||
}
|
||||
|
||||
/* Make 32x32 version */
|
||||
DynmapBufferedImage body32x32 = DynmapBufferedImage.allocateBufferedImage(32, 32);
|
||||
/* Copy face at 12,0 to 20,8 (already handled accessory) */
|
||||
for(int i = 0; i < 8; i++) {
|
||||
for(int j = 0; j < 8; j++) {
|
||||
body32x32.argb_buf[i*32+j+12] = face8x8.argb_buf[i*8 + j];
|
||||
}
|
||||
}
|
||||
/* Copy body at 20,20 and chest at 20,36 to 8,12 */
|
||||
copyLayersToTarget(skin_buff, 20, 20, 20, 36, 8, 12, body32x32.argb_buf, 8*32+12, 32);
|
||||
/* Copy right leg at 4,20 and 4,36 to 20,12 */
|
||||
copyLayersToTarget(skin_buff, 4, 20, 4, 36, 4, 12, body32x32.argb_buf, 20*32+12, 32);
|
||||
/* Copy left leg at 4,20 if old format or 20,52 and 4,53 to 20,16 */
|
||||
if(is_64x32_skin) {
|
||||
skin_buff.getRGB(4, 20, 4, 12, body32x32.argb_buf, 20*32+16, 32);
|
||||
}
|
||||
else {
|
||||
copyLayersToTarget(skin_buff, 20, 52, 4, 52, 4, 12, body32x32.argb_buf, 20 * 32 + 16, 32);
|
||||
}
|
||||
/* Copy right arm at 44,20 and 44,36 to 8,8 */
|
||||
copyLayersToTarget(skin_buff, 44, 20, 44, 36, 4, 12, body32x32.argb_buf, 8*32+8, 32);
|
||||
/* Copy left arm at 44,20 if old format or 36,52 and 52,52 to 8,20 */
|
||||
if(is_64x32_skin) {
|
||||
skin_buff.getRGB(44, 20, 4, 12, body32x32.argb_buf, 8*32+20, 32);
|
||||
}
|
||||
else {
|
||||
copyLayersToTarget(skin_buff, 36, 52, 52, 52, 4, 12, body32x32.argb_buf, 8 * 32 + 20, 32);
|
||||
}
|
||||
|
||||
BufferOutputStream bos = ImageIOManager.imageIOEncode(body32x32.buf_img, ImageFormat.FORMAT_PNG);
|
||||
if (bos != null) {
|
||||
storage.setPlayerFaceImage(playername, FaceType.BODY_32X32, bos);
|
||||
}
|
||||
|
||||
DynmapBufferedImage.freeBufferedImage(body32x32);
|
||||
skin_buff.flush();
|
||||
}
|
||||
|
||||
DynmapBufferedImage.freeBufferedImage(face8x8);
|
||||
face8x8_buff.flush();
|
||||
faceOriginal.flush();
|
||||
img.flush();
|
||||
}
|
||||
}
|
||||
public PlayerFaces(DynmapCore core) {
|
||||
fetchskins = core.configuration.getBoolean("fetchskins", true); /* Control whether to fetch skins */
|
||||
refreshskins = core.configuration.getBoolean("refreshskins", true); /* Control whether to update existing fetched skins or faces */
|
||||
skinurl = core.configuration.getString("skin-url", "");
|
||||
// These don't work anymore - Mojang retired them
|
||||
if (skinurl.equals("http://s3.amazonaws.com/MinecraftSkins/%player%.png") ||
|
||||
skinurl.equals("http://skins.minecraft.net/MinecraftSkins/%player%.png")) {
|
||||
skinurl = "";
|
||||
}
|
||||
core.listenerManager.addListener(EventType.PLAYER_JOIN, new PlayerEventListener() {
|
||||
@Override
|
||||
public void playerEvent(DynmapPlayer p) {
|
||||
Runnable job = new LoadPlayerImages(p.getName(), p.getSkinURL(), p.getUUID(), core.skinUrlProvider);
|
||||
MapManager.scheduleDelayedJob(job, 0);
|
||||
}
|
||||
});
|
||||
storage = core.getDefaultMapStorage();
|
||||
}
|
||||
}
|
||||
193
DynmapCore/src/main/java/org/dynmap/PlayerList.java
Normal file
193
DynmapCore/src/main/java/org/dynmap/PlayerList.java
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
package org.dynmap;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
import java.util.Set;
|
||||
|
||||
import org.dynmap.common.DynmapPlayer;
|
||||
import org.dynmap.common.DynmapServerInterface;
|
||||
|
||||
public class PlayerList {
|
||||
private DynmapServerInterface server;
|
||||
private HashSet<String> hiddenPlayerNames = new HashSet<String>();
|
||||
private File hiddenPlayersFile;
|
||||
private ConfigurationNode configuration;
|
||||
private DynmapPlayer[] online;
|
||||
private HashMap<String, Set<String>> invisibility_asserts = new HashMap<String, Set<String>>();
|
||||
private HashMap<String, Set<String>> visibility_asserts = new HashMap<String, Set<String>>();
|
||||
|
||||
public PlayerList(DynmapServerInterface server, File hiddenPlayersFile, ConfigurationNode configuration) {
|
||||
this.server = server;
|
||||
this.hiddenPlayersFile = hiddenPlayersFile;
|
||||
this.configuration = configuration;
|
||||
updateOnlinePlayers(null);
|
||||
}
|
||||
|
||||
public void save() {
|
||||
OutputStream stream;
|
||||
try {
|
||||
stream = new FileOutputStream(hiddenPlayersFile);
|
||||
OutputStreamWriter writer = new OutputStreamWriter(stream);
|
||||
for (String player : hiddenPlayerNames) {
|
||||
writer.write(player);
|
||||
writer.write("\n");
|
||||
}
|
||||
writer.close();
|
||||
stream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void load() {
|
||||
try {
|
||||
Scanner scanner = new Scanner(hiddenPlayersFile);
|
||||
while (scanner.hasNextLine()) {
|
||||
String line = scanner.nextLine();
|
||||
hiddenPlayerNames.add(line);
|
||||
}
|
||||
scanner.close();
|
||||
} catch (FileNotFoundException e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void hide(String playerName) {
|
||||
hiddenPlayerNames.add(playerName.toLowerCase());
|
||||
save();
|
||||
}
|
||||
|
||||
public void show(String playerName) {
|
||||
hiddenPlayerNames.remove(playerName.toLowerCase());
|
||||
save();
|
||||
}
|
||||
|
||||
public void setVisible(String playerName, boolean visible) {
|
||||
if (visible ^ configuration.getBoolean("display-whitelist", false))
|
||||
show(playerName);
|
||||
else
|
||||
hide(playerName);
|
||||
}
|
||||
|
||||
public void assertVisiblilty(String playerName, boolean visible, String plugin_id) {
|
||||
playerName = playerName.toLowerCase();
|
||||
if(visible) {
|
||||
Set<String> ids = visibility_asserts.get(playerName);
|
||||
if(ids == null) {
|
||||
ids = new HashSet<String>();
|
||||
visibility_asserts.put(playerName, ids);
|
||||
}
|
||||
ids.add(plugin_id);
|
||||
}
|
||||
else {
|
||||
Set<String> ids = visibility_asserts.get(playerName);
|
||||
if(ids != null) {
|
||||
ids.remove(plugin_id);
|
||||
if(ids.isEmpty()) {
|
||||
visibility_asserts.remove(playerName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void assertInvisiblilty(String playerName, boolean invisible, String plugin_id) {
|
||||
playerName = playerName.toLowerCase();
|
||||
if(invisible) {
|
||||
Set<String> ids = invisibility_asserts.get(playerName);
|
||||
if(ids == null) {
|
||||
ids = new HashSet<String>();
|
||||
invisibility_asserts.put(playerName, ids);
|
||||
}
|
||||
ids.add(plugin_id);
|
||||
}
|
||||
else {
|
||||
Set<String> ids = invisibility_asserts.get(playerName);
|
||||
if(ids != null) {
|
||||
ids.remove(plugin_id);
|
||||
if(ids.isEmpty()) {
|
||||
invisibility_asserts.remove(playerName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DynmapPlayer[] getOnlinePlayers() {
|
||||
return Arrays.copyOf(online, online.length);
|
||||
}
|
||||
|
||||
public List<DynmapPlayer> getVisiblePlayers(String worldName) {
|
||||
ArrayList<DynmapPlayer> visiblePlayers = new ArrayList<DynmapPlayer>();
|
||||
DynmapPlayer[] onlinePlayers = online; /* Use copied list - we don't call from server thread */
|
||||
boolean useWhitelist = configuration.getBoolean("display-whitelist", false);
|
||||
for (int i = 0; i < onlinePlayers.length; i++) {
|
||||
DynmapPlayer p = onlinePlayers[i];
|
||||
if(p == null) continue;
|
||||
if((worldName != null) && (p.getWorld().equals(worldName) == false)) continue;
|
||||
String pname = p.getName().toLowerCase();
|
||||
if (!(useWhitelist ^ hiddenPlayerNames.contains(pname))) {
|
||||
if(!invisibility_asserts.containsKey(pname)) {
|
||||
visiblePlayers.add(p);
|
||||
}
|
||||
}
|
||||
else if(visibility_asserts.containsKey(pname)) {
|
||||
visiblePlayers.add(p);
|
||||
}
|
||||
}
|
||||
return visiblePlayers;
|
||||
}
|
||||
|
||||
public List<DynmapPlayer> getVisiblePlayers() {
|
||||
return getVisiblePlayers(null);
|
||||
}
|
||||
|
||||
public List<DynmapPlayer> getHiddenPlayers() {
|
||||
ArrayList<DynmapPlayer> hidden = new ArrayList<DynmapPlayer>();
|
||||
DynmapPlayer[] onlinePlayers = online; /* Use copied list - we don't call from server thread */
|
||||
boolean useWhitelist = configuration.getBoolean("display-whitelist", false);
|
||||
for (int i = 0; i < onlinePlayers.length; i++) {
|
||||
DynmapPlayer p = onlinePlayers[i];
|
||||
if(p == null) continue;
|
||||
String pname = p.getName().toLowerCase();
|
||||
if (!(useWhitelist ^ hiddenPlayerNames.contains(pname))) {
|
||||
if(invisibility_asserts.containsKey(pname)) {
|
||||
hidden.add(p);
|
||||
}
|
||||
}
|
||||
else if(!visibility_asserts.containsKey(pname)) {
|
||||
hidden.add(p);
|
||||
}
|
||||
}
|
||||
return hidden;
|
||||
}
|
||||
|
||||
public boolean isVisiblePlayer(String p) {
|
||||
p = p.toLowerCase();
|
||||
boolean useWhitelist = configuration.getBoolean("display-whitelist", false);
|
||||
return (!(useWhitelist ^ hiddenPlayerNames.contains(p))) && (!invisibility_asserts.containsKey(p));
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this from server thread to update player list safely
|
||||
*/
|
||||
void updateOnlinePlayers(String skipone) {
|
||||
DynmapPlayer[] players = server.getOnlinePlayers();
|
||||
DynmapPlayer[] pl = new DynmapPlayer[players.length];
|
||||
System.arraycopy(players, 0, pl, 0, pl.length);
|
||||
if(skipone != null) {
|
||||
for(int i = 0; i < pl.length; i++)
|
||||
if(pl[i].getName().equals(skipone))
|
||||
pl[i] = null;
|
||||
}
|
||||
online = pl;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package org.dynmap;
|
||||
|
||||
import static org.dynmap.JSONUtils.s;
|
||||
|
||||
import org.dynmap.common.DynmapListenerManager;
|
||||
import org.dynmap.common.DynmapListenerManager.ChatEventListener;
|
||||
import org.dynmap.common.DynmapListenerManager.EventType;
|
||||
import org.dynmap.common.DynmapPlayer;
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
public class SimpleWebChatComponent extends Component {
|
||||
|
||||
public SimpleWebChatComponent(final DynmapCore plugin, final ConfigurationNode configuration) {
|
||||
super(plugin, configuration);
|
||||
plugin.events.addListener("webchat", new Event.Listener<ChatEvent>() {
|
||||
@Override
|
||||
public void triggered(ChatEvent t) {
|
||||
if(plugin.getServer().sendWebChatEvent(t.source, t.name, t.message)) {
|
||||
String msg;
|
||||
String msgfmt = plugin.configuration.getString("webmsgformat", null);
|
||||
if(msgfmt != null) {
|
||||
msgfmt = unescapeString(msgfmt);
|
||||
msg = msgfmt.replace("%playername%", t.name).replace("%message%", t.message);
|
||||
}
|
||||
else {
|
||||
msg = unescapeString(plugin.configuration.getString("webprefix", "\u00A72[WEB] ")) + t.name + ": " + unescapeString(plugin.configuration.getString("websuffix", "\u00A7f")) + t.message;
|
||||
}
|
||||
plugin.getServer().broadcastMessage(msg);
|
||||
if (core.mapManager != null) {
|
||||
core.mapManager.pushUpdate(new Client.ChatMessage("web", null, t.name, t.message, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
plugin.events.addListener("buildclientconfiguration", new Event.Listener<JSONObject>() {
|
||||
@Override
|
||||
public void triggered(JSONObject t) {
|
||||
s(t, "allowchat", configuration.getBoolean("allowchat", false));
|
||||
}
|
||||
});
|
||||
|
||||
if (configuration.getBoolean("allowchat", false)) {
|
||||
plugin.listenerManager.addListener(EventType.PLAYER_CHAT, new ChatEventListener() {
|
||||
@Override
|
||||
public void chatEvent(DynmapPlayer p, String msg) {
|
||||
if(core.disable_chat_to_web) return;
|
||||
msg = core.scanAndReplaceLog4JMacro(msg);
|
||||
if(core.mapManager != null)
|
||||
core.mapManager.pushUpdate(new Client.ChatMessage("player", "", p.getDisplayName(), msg, p.getName()));
|
||||
}
|
||||
});
|
||||
plugin.listenerManager.addListener(EventType.PLAYER_JOIN, new DynmapListenerManager.PlayerEventListener() {
|
||||
@Override
|
||||
public void playerEvent(DynmapPlayer p) {
|
||||
if(core.disable_chat_to_web) return;
|
||||
if((core.mapManager != null) && (core.playerList != null) && (core.playerList.isVisiblePlayer(p.getName()))) {
|
||||
core.mapManager.pushUpdate(new Client.PlayerJoinMessage(p.getDisplayName(), p.getName()));
|
||||
}
|
||||
}
|
||||
});
|
||||
plugin.listenerManager.addListener(EventType.PLAYER_QUIT, new DynmapListenerManager.PlayerEventListener() {
|
||||
@Override
|
||||
public void playerEvent(DynmapPlayer p) {
|
||||
if(core.disable_chat_to_web) return;
|
||||
if((core.mapManager != null) && (core.playerList != null) && (core.playerList.isVisiblePlayer(p.getName()))) {
|
||||
core.mapManager.pushUpdate(new Client.PlayerQuitMessage(p.getDisplayName(), p.getName()));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
7
DynmapCore/src/main/java/org/dynmap/SkinUrlProvider.java
Normal file
7
DynmapCore/src/main/java/org/dynmap/SkinUrlProvider.java
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package org.dynmap;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
public interface SkinUrlProvider {
|
||||
URL getSkinUrl(String playerName);
|
||||
}
|
||||
10
DynmapCore/src/main/java/org/dynmap/TestComponent.java
Normal file
10
DynmapCore/src/main/java/org/dynmap/TestComponent.java
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package org.dynmap;
|
||||
|
||||
public class TestComponent extends Component {
|
||||
|
||||
public TestComponent(DynmapCore plugin, ConfigurationNode configuration) {
|
||||
super(plugin, configuration);
|
||||
Log.info("Hello! I'm a component that does stuff! Like saying what is in my configuration: " + configuration.getString("stuff"));
|
||||
}
|
||||
|
||||
}
|
||||
121
DynmapCore/src/main/java/org/dynmap/UpdateQueue.java
Normal file
121
DynmapCore/src/main/java/org/dynmap/UpdateQueue.java
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
package org.dynmap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
public class UpdateQueue {
|
||||
public Object lock = new Object();
|
||||
private HashMap<UpdateRec,UpdateRec> updateSet = new HashMap<UpdateRec,UpdateRec>();
|
||||
private UpdateRec orderedlist = null; /* Oldest to youngest */
|
||||
private static final long maxUpdateAge = 120000;
|
||||
private static final long ageOutPeriod = 5000;
|
||||
private long lastageout = 0;
|
||||
|
||||
private static class UpdateRec {
|
||||
Client.Update u;
|
||||
UpdateRec next;
|
||||
UpdateRec prev;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if(o instanceof UpdateRec)
|
||||
return u.equals(((UpdateRec)o).u);
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return u.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
private void doAgeOut(long now) {
|
||||
/* If we're due */
|
||||
if((now < lastageout) || (now > (lastageout + ageOutPeriod))) {
|
||||
lastageout = now;
|
||||
long deadline = now - maxUpdateAge;
|
||||
while((orderedlist != null) && (orderedlist.u.timestamp < deadline)) {
|
||||
UpdateRec r = orderedlist;
|
||||
|
||||
updateSet.remove(r); /* Remove record from set */
|
||||
if(r.next == r) {
|
||||
orderedlist = null;
|
||||
}
|
||||
else {
|
||||
orderedlist = r.next;
|
||||
r.next.prev = r.prev;
|
||||
r.prev.next = r.next;
|
||||
}
|
||||
r.next = r.prev = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void pushUpdate(Client.Update obj) {
|
||||
synchronized (lock) {
|
||||
/* Do inside lock - prevent delay between time and actual work */
|
||||
long now = System.currentTimeMillis();
|
||||
doAgeOut(now); /* Consider age out */
|
||||
UpdateRec r = new UpdateRec();
|
||||
r.u = obj;
|
||||
r.u.timestamp = now; // Use our timestamp: makes sure order is preserved
|
||||
UpdateRec oldr = updateSet.remove(r); /* Try to remove redundant event */
|
||||
if(oldr != null) { /* If found, remove from ordered list too */
|
||||
if(oldr.next == oldr) { /* Only one? */
|
||||
orderedlist = null;
|
||||
}
|
||||
else {
|
||||
if(orderedlist == oldr) { /* We're oldest? */
|
||||
orderedlist = oldr.next;
|
||||
}
|
||||
oldr.next.prev = oldr.prev;
|
||||
oldr.prev.next = oldr.next;
|
||||
}
|
||||
oldr.next = oldr.prev = null;
|
||||
}
|
||||
updateSet.put(r, r);
|
||||
/* Add to end of ordered list */
|
||||
if(orderedlist == null) {
|
||||
orderedlist = r;
|
||||
r.next = r.prev = r;
|
||||
}
|
||||
else {
|
||||
r.next = orderedlist;
|
||||
r.prev = orderedlist.prev;
|
||||
r.next.prev = r.prev.next = r;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ArrayList<Client.Update> tmpupdates = new ArrayList<Client.Update>();
|
||||
|
||||
public Client.Update[] getUpdatedObjects(long since) {
|
||||
Client.Update[] updates;
|
||||
synchronized (lock) {
|
||||
long now = System.currentTimeMillis();
|
||||
doAgeOut(now); /* Consider age out */
|
||||
|
||||
tmpupdates.clear();
|
||||
if(orderedlist != null) {
|
||||
UpdateRec r = orderedlist.prev; /* Get newest */
|
||||
while(r != null) {
|
||||
if(r.u.timestamp >= since) {
|
||||
tmpupdates.add(r.u);
|
||||
if(r == orderedlist)
|
||||
r = null;
|
||||
else
|
||||
r = r.prev;
|
||||
}
|
||||
else {
|
||||
r = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reverse output.
|
||||
updates = new Client.Update[tmpupdates.size()];
|
||||
for (int i = 0; i < updates.length; i++) {
|
||||
updates[i] = tmpupdates.get(updates.length-1-i);
|
||||
}
|
||||
}
|
||||
return updates;
|
||||
}
|
||||
}
|
||||
360
DynmapCore/src/main/java/org/dynmap/WebAuthManager.java
Normal file
360
DynmapCore/src/main/java/org/dynmap/WebAuthManager.java
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
package org.dynmap;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
import org.dynmap.common.DynmapCommandSender;
|
||||
import org.dynmap.common.DynmapPlayer;
|
||||
import org.dynmap.servlet.LoginServlet;
|
||||
|
||||
public class WebAuthManager {
|
||||
private HashMap<String, String> pwdhash_by_userid = new HashMap<String, String>();
|
||||
private HashMap<String, String> pending_registrations = new HashMap<String, String>();
|
||||
private String hashsalt;
|
||||
private File pfile;
|
||||
public static final String WEBAUTHFILE = "webauth.txt";
|
||||
private static final String HASHSALT = "$HASH_SALT$";
|
||||
private static final String PWDHASH_PREFIX = "hash.";
|
||||
private SecureRandom rnd = new SecureRandom();
|
||||
private DynmapCore core;
|
||||
private String publicRegistrationURL;
|
||||
|
||||
public WebAuthManager(DynmapCore core) {
|
||||
this.core = core;
|
||||
pfile = new File(core.getDataFolder(), WEBAUTHFILE);
|
||||
if(pfile.canRead()) {
|
||||
FileReader rf = null;
|
||||
try {
|
||||
rf = new FileReader(pfile);
|
||||
Properties p = new Properties();
|
||||
p.load(rf);
|
||||
hashsalt = p.getProperty(HASHSALT);
|
||||
for(String k : p.stringPropertyNames()) {
|
||||
if(k.equals(HASHSALT)) {
|
||||
hashsalt = p.getProperty(k);
|
||||
}
|
||||
else if(k.startsWith(PWDHASH_PREFIX)) { /* Load password hashes */
|
||||
pwdhash_by_userid.put(k.substring(PWDHASH_PREFIX.length()).toLowerCase(), p.getProperty(k));
|
||||
}
|
||||
}
|
||||
|
||||
} catch (IOException iox) {
|
||||
Log.severe("Cannot read " + WEBAUTHFILE);
|
||||
} finally {
|
||||
if(rf != null) { try { rf.close(); } catch (IOException iox) {} }
|
||||
}
|
||||
}
|
||||
if(hashsalt == null) { /* No hashsalt */
|
||||
hashsalt = Long.toHexString(rnd.nextLong());
|
||||
}
|
||||
}
|
||||
public boolean save() {
|
||||
boolean success = false;
|
||||
FileWriter fw = null;
|
||||
try {
|
||||
fw = new FileWriter(pfile);
|
||||
Properties p = new Properties();
|
||||
p.setProperty(HASHSALT, hashsalt); /* Save salt */
|
||||
for(String k : pwdhash_by_userid.keySet()) {
|
||||
p.setProperty(PWDHASH_PREFIX + k, pwdhash_by_userid.get(k));
|
||||
}
|
||||
p.store(fw, "DO NOT EDIT THIS FILE");
|
||||
success = true;
|
||||
} catch (IOException iox) {
|
||||
Log.severe("Error writing " + WEBAUTHFILE);
|
||||
} finally {
|
||||
if(fw != null) { try { fw.close(); } catch (IOException iox) {} }
|
||||
}
|
||||
if(success)
|
||||
core.events.trigger("loginupdated", null);
|
||||
return success;
|
||||
}
|
||||
private String makeHash(String pwd) {
|
||||
String check = hashsalt + pwd;
|
||||
try {
|
||||
byte[] checkbytes = check.getBytes("UTF-8");
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
byte[] rslt = md.digest(checkbytes);
|
||||
String rslthash = "";
|
||||
for(int i = 0; i < rslt.length; i++) {
|
||||
rslthash += String.format("%02X", 0xFF & (int)rslt[i]);
|
||||
}
|
||||
return rslthash;
|
||||
} catch (NoSuchAlgorithmException nsax) {
|
||||
} catch (UnsupportedEncodingException uex) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public boolean checkLogin(String uid, String pwd) {
|
||||
uid = uid.toLowerCase();
|
||||
if(uid.equals(LoginServlet.USERID_GUEST)) {
|
||||
return true;
|
||||
}
|
||||
String hash = pwdhash_by_userid.get(uid);
|
||||
if(hash == null) {
|
||||
return false;
|
||||
}
|
||||
if(core.getServer().isPlayerBanned(uid)) {
|
||||
return false;
|
||||
}
|
||||
String checkhash = makeHash(pwd);
|
||||
return hash.equals(checkhash);
|
||||
}
|
||||
public boolean registerLogin(String uid, String pwd, String passcode) {
|
||||
uid = uid.toLowerCase();
|
||||
if(uid.equals(LoginServlet.USERID_GUEST)) {
|
||||
return false;
|
||||
}
|
||||
if(core.getServer().isPlayerBanned(uid)) {
|
||||
return false;
|
||||
}
|
||||
passcode = passcode.toLowerCase();
|
||||
String kcode = pending_registrations.remove(uid);
|
||||
if(kcode == null) {
|
||||
return false;
|
||||
}
|
||||
if(!kcode.equals(passcode)) {
|
||||
return false;
|
||||
}
|
||||
String hash = makeHash(pwd);
|
||||
pwdhash_by_userid.put(uid, hash);
|
||||
return save();
|
||||
}
|
||||
public boolean unregisterLogin(String uid) {
|
||||
if(uid.equals(LoginServlet.USERID_GUEST)) {
|
||||
return true;
|
||||
}
|
||||
uid = uid.toLowerCase();
|
||||
pwdhash_by_userid.remove(uid);
|
||||
return save();
|
||||
}
|
||||
public boolean isRegistered(String uid) {
|
||||
if(uid.equals(LoginServlet.USERID_GUEST)) {
|
||||
return false;
|
||||
}
|
||||
uid = uid.toLowerCase();
|
||||
return pwdhash_by_userid.containsKey(uid);
|
||||
}
|
||||
boolean processCompletedRegister(String uid, String pc, String hash) {
|
||||
uid = uid.toLowerCase();
|
||||
if(uid.equals(LoginServlet.USERID_GUEST)) {
|
||||
return false;
|
||||
}
|
||||
if(core.getServer().isPlayerBanned(uid)) {
|
||||
return false;
|
||||
}
|
||||
String kcode = pending_registrations.remove(uid);
|
||||
if(kcode == null) {
|
||||
return false;
|
||||
}
|
||||
pc = pc.toLowerCase();
|
||||
if(!kcode.equals(pc)) {
|
||||
return false;
|
||||
}
|
||||
pwdhash_by_userid.put(uid, hash);
|
||||
return save();
|
||||
}
|
||||
public static final boolean checkUserName(String name) {
|
||||
int nlen = name.length();
|
||||
if ((nlen > 0) && (nlen <= 16)) {
|
||||
for (int i = 0; i < nlen; i++) {
|
||||
if ("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_".indexOf(name.charAt(i)) < 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public boolean processWebRegisterCommand(DynmapCore core, DynmapCommandSender sender, DynmapPlayer player, String[] args) {
|
||||
String uid = null;
|
||||
boolean other = false;
|
||||
if(args.length > 1) {
|
||||
if(!core.checkPlayerPermission(sender, "webregister.other")) {
|
||||
sender.sendMessage("You're not authorised to access web registration info for other players");
|
||||
return true;
|
||||
}
|
||||
uid = args[1];
|
||||
other = true;
|
||||
}
|
||||
else if (player == null) { /* Console? */
|
||||
sender.sendMessage("Must provide username to access web registration info");
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
uid = player.getName();
|
||||
}
|
||||
if (checkUserName(uid) == false) {
|
||||
sender.sendMessage("Invalid username. Did you type it correctly?");
|
||||
return true;
|
||||
}
|
||||
String regkey = String.format("%04d-%04d", rnd.nextInt(10000), rnd.nextInt(10000));
|
||||
pending_registrations.put(uid.toLowerCase(), regkey.toLowerCase());
|
||||
sender.sendMessage("Registration pending for username: " + uid);
|
||||
sender.sendMessage("Registration code: " + regkey);
|
||||
publicRegistrationURL = core.configuration.getString("publicURL", "index.html");
|
||||
sender.sendMessage("Enter username and registration code when prompted on web page (" + publicRegistrationURL.toString() + ") to complete registration");
|
||||
if(other) {
|
||||
DynmapPlayer p = core.getServer().getPlayer(uid);
|
||||
if(p != null && sender != p) {
|
||||
p.sendMessage("The registration of your account for web access has been started.");
|
||||
p.sendMessage("To complete the process, access the Login page on the Dynmap map");
|
||||
p.sendMessage("Registration code: " + regkey);
|
||||
p.sendMessage("Enter your username and registration code when prompted on web page (" + publicRegistrationURL.toString() + ") to complete registration");
|
||||
}
|
||||
}
|
||||
core.events.trigger("loginupdated", null);
|
||||
|
||||
return true;
|
||||
}
|
||||
String getLoginPHP(boolean wrap) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (wrap) {
|
||||
sb.append("<?php\n");
|
||||
}
|
||||
sb.append("$pwdsalt = '").append(hashsalt).append("';\n");
|
||||
/* Create password hash */
|
||||
sb.append("$pwdhash = array(\n");
|
||||
for(String uid : pwdhash_by_userid.keySet()) {
|
||||
sb.append(" \'").append(esc(uid)).append("\' => \'").append(esc(pwdhash_by_userid.get(uid))).append("\',\n");
|
||||
}
|
||||
sb.append(");\n");
|
||||
/* Create registration table */
|
||||
sb.append("$pendingreg = array(\n");
|
||||
for(String uid : pending_registrations.keySet()) {
|
||||
sb.append(" \'").append(esc(uid)).append("\' => \'").append(esc(pending_registrations.get(uid))).append("\',\n");
|
||||
}
|
||||
sb.append(");\n");
|
||||
if (wrap) {
|
||||
sb.append("?>\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String esc(String s) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for(int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
if(c == '\\')
|
||||
sb.append("\\\\");
|
||||
else if(c == '\'')
|
||||
sb.append("\\\'");
|
||||
else
|
||||
sb.append(c);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
String getAccessPHP(boolean wrap) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (wrap) {
|
||||
sb.append("<?php\n");
|
||||
}
|
||||
ArrayList<String> mid = new ArrayList<String>();
|
||||
/* Create world access list */
|
||||
sb.append("$worldaccess = array(\n");
|
||||
for(DynmapWorld w : core.getMapManager().getWorlds()) {
|
||||
if(w.isProtected()) {
|
||||
String perm = "world." + w.getName();
|
||||
sb.append(" \'").append(esc(w.getName())).append("\' => \'");
|
||||
for(String uid : pwdhash_by_userid.keySet()) {
|
||||
if(core.getServer().checkPlayerPermission(uid, perm)) {
|
||||
sb.append("[").append(esc(uid)).append("]");
|
||||
}
|
||||
}
|
||||
sb.append("\',\n");
|
||||
}
|
||||
for(MapType mt : w.maps) {
|
||||
if(mt.isProtected()) {
|
||||
mid.add(w.getName() + "." + mt.getPrefix());
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.append(");\n");
|
||||
|
||||
/* Create map access list */
|
||||
sb.append("$mapaccess = array(\n");
|
||||
for(String id : mid) {
|
||||
String perm = "map." + id;
|
||||
sb.append(" \'").append(esc(id)).append("\' => \'");
|
||||
for(String uid : pwdhash_by_userid.keySet()) {
|
||||
if(core.getServer().checkPlayerPermission(uid, perm)) {
|
||||
sb.append("[").append(esc(uid)).append("]");
|
||||
}
|
||||
}
|
||||
sb.append("\',\n");
|
||||
}
|
||||
sb.append(");\n");
|
||||
|
||||
HashSet<String> cantseeall = new HashSet<String>();
|
||||
String perm = "playermarkers.seeall";
|
||||
sb.append("$seeallmarkers = \'");
|
||||
for(String uid : pwdhash_by_userid.keySet()) {
|
||||
if(core.getServer().checkPlayerPermission(uid, perm)) {
|
||||
sb.append("[").append(esc(uid)).append("]");
|
||||
}
|
||||
else {
|
||||
cantseeall.add(uid);
|
||||
}
|
||||
}
|
||||
sb.append("\';\n");
|
||||
/* Add visibility lists for each player that doesn't see everything */
|
||||
sb.append("$playervisible = array(\n");
|
||||
for(String id : cantseeall) {
|
||||
id = id.toLowerCase();
|
||||
Set<String> vis = core.getPlayersVisibleToPlayer(id);
|
||||
if((vis.size() == 1) && vis.contains(id)) continue;
|
||||
sb.append(" \'").append(esc(id)).append("\' => \'");
|
||||
for(String uid : vis) {
|
||||
sb.append("[").append(esc(uid)).append("]");
|
||||
}
|
||||
sb.append("\',\n");
|
||||
}
|
||||
sb.append(");\n");
|
||||
|
||||
core.getDefaultMapStorage().addPaths(sb, core);
|
||||
|
||||
if (wrap) {
|
||||
sb.append("?>\n");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
static String getDisabledAccessPHP(DynmapCore core, boolean wrap) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (wrap) {
|
||||
sb.append("<?php\n");
|
||||
}
|
||||
|
||||
core.getDefaultMapStorage().addPaths(sb, core);
|
||||
|
||||
if (wrap) {
|
||||
sb.append("?>\n");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
boolean pendingRegisters() {
|
||||
return (pending_registrations.size() > 0);
|
||||
}
|
||||
Set<String> getUserIDs() {
|
||||
HashSet<String> lst = new HashSet<String>();
|
||||
lst.addAll(pwdhash_by_userid.keySet());
|
||||
lst.addAll(pending_registrations.keySet());
|
||||
return lst;
|
||||
}
|
||||
}
|
||||
322
DynmapCore/src/main/java/org/dynmap/common/BiomeMap.java
Normal file
322
DynmapCore/src/main/java/org/dynmap/common/BiomeMap.java
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
package org.dynmap.common;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.dynmap.hdmap.HDBlockModels;
|
||||
|
||||
/* Generic biome mapping */
|
||||
public class BiomeMap {
|
||||
public static final int NO_INDEX = -2;
|
||||
private static BiomeMap[] biome_by_index = new BiomeMap[256];
|
||||
private static Map<String, BiomeMap> biome_by_rl = new HashMap<String, BiomeMap>(256);
|
||||
// Tracks registered IDs for O(1) uniqueness checks during initialization
|
||||
private static final HashSet<String> biome_ids = new HashSet<String>(256);
|
||||
public static final BiomeMap NULL = new BiomeMap(-1, "NULL", 0.5, 0.5, 0xFFFFFF, 0, 0, null);
|
||||
|
||||
public static final BiomeMap OCEAN = new BiomeMap(0, "OCEAN", "minecraft:ocean");
|
||||
public static final BiomeMap PLAINS = new BiomeMap(1, "PLAINS", 0.8, 0.4, "minecraft:plains");
|
||||
public static final BiomeMap DESERT = new BiomeMap(2, "DESERT", 2.0, 0.0, "minecraft:desert");
|
||||
public static final BiomeMap EXTREME_HILLS = new BiomeMap(3, "EXTREME_HILLS", 0.2, 0.3, "minecraft:mountains");
|
||||
public static final BiomeMap FOREST = new BiomeMap(4, "FOREST", 0.7, 0.8, "minecraft:forest");
|
||||
public static final BiomeMap TAIGA = new BiomeMap(5, "TAIGA", 0.05, 0.8, "minecraft:taiga");
|
||||
public static final BiomeMap SWAMPLAND = new BiomeMap(6, "SWAMPLAND", 0.8, 0.9, 0xE0FFAE, 0x2e282a, 0x902c52, "minecraft:swamp");
|
||||
public static final BiomeMap RIVER = new BiomeMap(7, "RIVER", "minecraft:river");
|
||||
public static final BiomeMap HELL = new BiomeMap(8, "HELL", 2.0, 0.0, "minecraft:nether");
|
||||
public static final BiomeMap SKY = new BiomeMap(9, "SKY", "minecraft:the_end");
|
||||
public static final BiomeMap FROZEN_OCEAN = new BiomeMap(10, "FROZEN_OCEAN", 0.0, 0.5, "minecraft:frozen_ocean");
|
||||
public static final BiomeMap FROZEN_RIVER = new BiomeMap(11, "FROZEN_RIVER", 0.0, 0.5, "minecraft:frozen_river");
|
||||
public static final BiomeMap ICE_PLAINS = new BiomeMap(12, "ICE_PLAINS", 0.0, 0.5, "minecraft:snowy_tundra");
|
||||
public static final BiomeMap ICE_MOUNTAINS = new BiomeMap(13, "ICE_MOUNTAINS", 0.0, 0.5, "minecraft:snowy_mountains");
|
||||
public static final BiomeMap MUSHROOM_ISLAND = new BiomeMap(14, "MUSHROOM_ISLAND", 0.9, 1.0, "minecraft:mushroom_fields");
|
||||
public static final BiomeMap MUSHROOM_SHORE = new BiomeMap(15, "MUSHROOM_SHORE", 0.9, 1.0, "minecraft:mushroom_field_shore");
|
||||
public static final BiomeMap BEACH = new BiomeMap(16, "BEACH", 0.8, 0.4, "minecraft:beach");
|
||||
public static final BiomeMap DESERT_HILLS = new BiomeMap(17, "DESERT_HILLS", 2.0, 0.0, "minecraft:desert_hills");
|
||||
public static final BiomeMap FOREST_HILLS = new BiomeMap(18, "FOREST_HILLS", 0.7, 0.8, "minecraft:wooded_hills");
|
||||
public static final BiomeMap TAIGA_HILLS = new BiomeMap(19, "TAIGA_HILLS", 0.05, 0.8, "minecraft:taiga_hills");
|
||||
public static final BiomeMap SMALL_MOUNTAINS = new BiomeMap(20, "SMALL_MOUNTAINS", 0.2, 0.8, "minecraft:mountain_edge");
|
||||
public static final BiomeMap JUNGLE = new BiomeMap(21, "JUNGLE", 1.2, 0.9, "minecraft:jungle");
|
||||
public static final BiomeMap JUNGLE_HILLS = new BiomeMap(22, "JUNGLE_HILLS", 1.2, 0.9, "minecraft:jungle_hills");
|
||||
|
||||
public static final int LAST_WELL_KNOWN = 175;
|
||||
|
||||
private double tmp;
|
||||
private double rain;
|
||||
private int watercolormult;
|
||||
private int grassmult;
|
||||
private int foliagemult;
|
||||
private Optional<?> biomeObj = Optional.empty();
|
||||
private final String id;
|
||||
private final String resourcelocation;
|
||||
private final int index;
|
||||
private int biomeindex256; // Standard biome mapping index (for 256 x 256)
|
||||
private boolean isDef;
|
||||
|
||||
private static boolean loadDone = false;
|
||||
|
||||
public static void loadWellKnownByVersion(String mcver) {
|
||||
if (loadDone) return;
|
||||
if (HDBlockModels.checkVersionRange(mcver, "1.7.0-")) {
|
||||
new BiomeMap(23, "JUNGLE_EDGE", 0.95, 0.8, "minecraft:jungle_edge");
|
||||
new BiomeMap(24, "DEEP_OCEAN", "minecraft:deep_ocean");
|
||||
new BiomeMap(25, "STONE_BEACH", 0.2, 0.3, "minecraft:stone_shore");
|
||||
new BiomeMap(26, "COLD_BEACH", 0.05, 0.3, "minecraft:snowy_beach");
|
||||
new BiomeMap(27, "BIRCH_FOREST", 0.6, 0.6, "minecraft:birch_forest");
|
||||
new BiomeMap(28, "BIRCH_FOREST_HILLS", 0.6, 0.6, "minecraft:birch_forest_hills");
|
||||
new BiomeMap(29, "ROOFED_FOREST", 0.7, 0.8, 0xFFFFFF, 0x28340A, 0, "minecraft:dark_forest");
|
||||
new BiomeMap(30, "COLD_TAIGA", -0.5, 0.4, "minecraft:snowy_taiga");
|
||||
new BiomeMap(31, "COLD_TAIGA_HILLS", -0.5, 0.4, "minecraft:snowy_taiga_hills");
|
||||
new BiomeMap(32, "MEGA_TAIGA", 0.3, 0.8, "minecraft:giant_tree_taiga");
|
||||
new BiomeMap(33, "MEGA_TAIGA_HILLS", 0.3, 0.8, "minecraft:giant_tree_taiga_hills");
|
||||
new BiomeMap(34, "EXTREME_HILLS_PLUS", 0.2, 0.3, "minecraft:wooded_mountains");
|
||||
new BiomeMap(35, "SAVANNA", 1.2, 0.0, "minecraft:savanna");
|
||||
new BiomeMap(36, "SAVANNA_PLATEAU", 1.0, 0.0, "minecraft:savanna_plateau");
|
||||
new BiomeMap(37, "MESA", 2.0, 0.0, 0xFFFFFF, 0x624c46, 0x8e5e70, "minecraft:badlands");
|
||||
new BiomeMap(129, "SUNFLOWER_PLAINS", 0.8, 0.4, "minecraft:sunflower_plains");
|
||||
new BiomeMap(130, "DESERT_MOUNTAINS", 2.0, 0.0, "minecraft:desert_lakes");
|
||||
new BiomeMap(131, "EXTREME_HILLS_MOUNTAINS", 0.2, 0.3, "minecraft:gravelly_mountains");
|
||||
new BiomeMap(132, "FLOWER_FOREST", 0.7, 0.8, "minecraft:flower_forest");
|
||||
new BiomeMap(133, "TAIGA_MOUNTAINS", 0.05, 0.8, "minecraft:taiga_mountains");
|
||||
new BiomeMap(140, "ICE_PLAINS_SPIKES", 0.0, 0.5, "minecraft:ice_spikes");
|
||||
new BiomeMap(149, "JUNGLE_MOUNTAINS", 1.2, 0.9, "minecraft:modified_jungle");
|
||||
new BiomeMap(151, "JUNGLE_EDGE_MOUNTAINS", 0.95, 0.8, "minecraft:modified_jungle_edge");
|
||||
new BiomeMap(155, "BIRCH_FOREST_MOUNTAINS", 0.6, 0.6, "minecraft:tall_birch_forest");
|
||||
new BiomeMap(156, "BIRCH_FOREST_HILLS_MOUNTAINS", 0.6, 0.6, "minecraft:tall_birch_hills");
|
||||
new BiomeMap(157, "ROOFED_FOREST_MOUNTAINS", 0.7, 0.8, 0xFFFFFF, 0x28340A, 0, "minecraft:dark_forest_hills");
|
||||
new BiomeMap(158, "COLD_TAIGA_MOUNTAINS", -0.5, 0.4, "minecraft:snowy_taiga_mountains");
|
||||
new BiomeMap(160, "MEGA_SPRUCE_TAIGA", 0.25, 0.8, "minecraft:giant_spruce_taiga");
|
||||
new BiomeMap(161, "MEGA_SPRUCE_TAIGA_HILLS", 0.3, 0.8, "minecraft:giant_spruce_taiga_hills");
|
||||
new BiomeMap(162, "EXTREME_HILLS_PLUS_MOUNTAINS", 0.2, 0.3, "minecraft:modified_gravelly_mountains");
|
||||
new BiomeMap(163, "SAVANNA_MOUNTAINS", 1.2, 0.0, "minecraft:shattered_savanna");
|
||||
new BiomeMap(164, "SAVANNA_PLATEAU_MOUNTAINS", 1.0, 0.0, "minecraft:shattered_savanna_plateau");
|
||||
new BiomeMap(165, "MESA_BRYCE", 2.0, 0.0,0xFFFFFF, 0x624c46, 0x8e5e70, "minecraft:eroded_badlands");
|
||||
}
|
||||
if (HDBlockModels.checkVersionRange(mcver, "1.7.0-1.17.1")) {
|
||||
new BiomeMap(38, "MESA_PLATEAU_FOREST", 2.0, 0.0, 0xFFFFFF, 0x624c46, 0x8e5e70, "minecraft:wooded_badlands_plateau");
|
||||
new BiomeMap(39, "MESA_PLATEAU", 2.0, 0.0, 0xFFFFFF, 0x624c46, 0x8e5e70, "minecraft:badlands_plateau");
|
||||
new BiomeMap(134, "SWAMPLAND_MOUNTAINS", 0.8, 0.9, 0xE0FFAE, 0x2e282a, 0x902c52, "minecraft:swamp_hills");
|
||||
new BiomeMap(166, "MESA_PLATEAU_FOREST_MOUNTAINS", 2.0, 0.0,0xFFFFFF, 0x624c46, 0x8e5e70, "minecraft:modified_wooded_badlands_plateau");
|
||||
new BiomeMap(167, "MESA_PLATEAU_MOUNTAINS", 2.0, 0.0,0xFFFFFF, 0x624c46, 0x8e5e70, "minecraft:modified_badlands_plateau");
|
||||
}
|
||||
if (HDBlockModels.checkVersionRange(mcver, "1.9.0-")) {
|
||||
new BiomeMap(127, "THE_VOID", "minecraft:the_void");
|
||||
}
|
||||
if (HDBlockModels.checkVersionRange(mcver, "1.13.0-")) {
|
||||
new BiomeMap(40, "SMALL_END_ISLANDS", "minecraft:small_end_islands");
|
||||
new BiomeMap(41, "END_MIDLANDS", "minecraft:end_midlands");
|
||||
new BiomeMap(42, "END_HIGHLANDS", "minecraft:end_highlands");
|
||||
new BiomeMap(43, "END_BARRENS", "minecraft:end_barrens");
|
||||
new BiomeMap(44, "WARM_OCEAN", "minecraft:warm_ocean");
|
||||
new BiomeMap(45, "LUKEWARM_OCEAN", "minecraft:lukewarm_ocean");
|
||||
new BiomeMap(46, "COLD_OCEAN", "minecraft:cold_ocean");
|
||||
new BiomeMap(47, "DEEP_WARM_OCEAN", "minecraft:deep_warm_ocean");
|
||||
new BiomeMap(48, "DEEP_LUKEWARM_OCEAN", "minecraft:deep_lukewarm_ocean");
|
||||
new BiomeMap(49, "DEEP_COLD_OCEAN", "minecraft:deep_cold_ocean");
|
||||
new BiomeMap(50, "DEEP_FROZEN_OCEAN", "minecraft:deep_frozen_ocean");
|
||||
}
|
||||
if (HDBlockModels.checkVersionRange(mcver, "1.14.0-")) {
|
||||
new BiomeMap(168, "BAMBOO_JUNGLE", "minecraft:bamboo_jungle");
|
||||
new BiomeMap(169, "BAMBOO_JUNGLE_HILLS", "minecraft:bamboo_jungle_hills");
|
||||
}
|
||||
if (HDBlockModels.checkVersionRange(mcver, "1.16.0-")) {
|
||||
new BiomeMap(170, "SOUL_SAND_VALLEY", "minecraft:soul_sand_valley");
|
||||
new BiomeMap(171, "CRIMSON_FOREST", "minecraft:crimson_forest");
|
||||
new BiomeMap(172, "WARPED_FOREST", "minecraft:warped_forest");
|
||||
new BiomeMap(173, "BASALT_DELTAS", "minecraft:basalt_deltas");
|
||||
}
|
||||
if (HDBlockModels.checkVersionRange(mcver, "1.17.0-")) {
|
||||
new BiomeMap(174, "DRIPSTONE_CAVES", "minecraft:dripstone_caves");
|
||||
new BiomeMap(175, "LUSH_CAVES", "minecraft:lush_caves");
|
||||
}
|
||||
if (HDBlockModels.checkVersionRange(mcver, "1.18.0-")) {
|
||||
new BiomeMap(38, "MESA_FOREST", 2.0, 0.0, 0xFFFFFF, 0x624c46, 0x8e5e70, "minecraft:wooded_badlands");
|
||||
}
|
||||
loadDone = true;
|
||||
}
|
||||
|
||||
static {
|
||||
for (int i = 0; i < biome_by_index.length; i++) {
|
||||
BiomeMap bm = BiomeMap.byBiomeID(i-1);
|
||||
if (bm == null) {
|
||||
bm = new BiomeMap(i-1, "BIOME_" + (i-1));
|
||||
bm.isDef = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isUniqueID(String id) {
|
||||
return !biome_ids.contains(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a grass/foliage color multiplier for efficient hot-path dispatch:
|
||||
* == 0 → 0 (passthrough: return raw value unchanged)
|
||||
* 0 < val ≤ 0xFFFFFF → positive (blend: average with raw)
|
||||
* val > 0xFFFFFF → -(val & 0xFFFFFF) (negative sentinel: fixed override color)
|
||||
*/
|
||||
private static int encodeColorMult(int val) {
|
||||
return (val > 0xFFFFFF) ? -(val & 0xFFFFFF) : val;
|
||||
}
|
||||
|
||||
private static void resizeIfNeeded(int idx) {
|
||||
if ((idx >= biome_by_index.length) ) {
|
||||
int oldlen = biome_by_index.length;
|
||||
biome_by_index = Arrays.copyOf(biome_by_index, idx * 3 / 2);
|
||||
for (int i = oldlen; i < biome_by_index.length; i++) {
|
||||
if (biome_by_index[i] == null) {
|
||||
BiomeMap bm = new BiomeMap(i-1, "BIOME_" + (i-1));
|
||||
bm.isDef = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private BiomeMap(int idx, String id, double tmp, double rain, int waterColorMultiplier, int grassmult, int foliagemult, String rl) {
|
||||
/* Clamp values : we use raw values from MC code, which are clamped during color mapping only */
|
||||
setTemperature(tmp);
|
||||
setRainfall(rain);
|
||||
this.watercolormult = waterColorMultiplier;
|
||||
this.grassmult = encodeColorMult(grassmult);
|
||||
this.foliagemult = encodeColorMult(foliagemult);
|
||||
// Handle null biome
|
||||
if (id == null) { id = "biome_" + idx; }
|
||||
id = id.toUpperCase().replace(' ', '_');
|
||||
if (isUniqueID(id) == false) {
|
||||
id = id + "_" + idx;
|
||||
}
|
||||
this.id = id;
|
||||
biome_ids.add(this.id);
|
||||
// If index is NO_INDEX, find one after the well known ones
|
||||
if (idx == NO_INDEX) {
|
||||
idx = LAST_WELL_KNOWN;
|
||||
while (true) {
|
||||
idx++;
|
||||
resizeIfNeeded(idx);
|
||||
if (biome_by_index[idx].isDef) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
idx++; /* Insert one after ID value - null is zero index */
|
||||
}
|
||||
this.index = idx;
|
||||
if (idx >= 0) {
|
||||
resizeIfNeeded(idx);
|
||||
biome_by_index[idx] = this;
|
||||
}
|
||||
this.resourcelocation = rl;
|
||||
if (rl != null) {
|
||||
biome_by_rl.put(rl, this);
|
||||
}
|
||||
}
|
||||
public BiomeMap(int idx, String id) {
|
||||
this(idx, id, 0.5, 0.5, 0xFFFFFF, 0, 0, null);
|
||||
}
|
||||
public BiomeMap(int idx, String id, String rl) {
|
||||
this(idx, id, 0.5, 0.5, 0xFFFFFF, 0, 0, rl);
|
||||
}
|
||||
|
||||
public BiomeMap(int idx, String id, double tmp, double rain) {
|
||||
this(idx, id, tmp, rain, 0xFFFFFF, 0, 0, null);
|
||||
}
|
||||
|
||||
public BiomeMap(int idx, String id, double tmp, double rain, String rl) {
|
||||
this(idx, id, tmp, rain, 0xFFFFFF, 0, 0, rl);
|
||||
}
|
||||
|
||||
public BiomeMap(String id, double tmp, double rain, String rl) {
|
||||
this(NO_INDEX, id, tmp, rain, 0xFFFFFF, 0, 0, rl); // No index
|
||||
}
|
||||
|
||||
private final int biomeLookup(int width) {
|
||||
int w = width-1;
|
||||
int t = (int)((1.0-tmp)*w);
|
||||
int h = (int)((1.0 - (tmp*rain))*w);
|
||||
return width*h + t;
|
||||
}
|
||||
|
||||
public final int biomeLookup() {
|
||||
return this.biomeindex256;
|
||||
}
|
||||
|
||||
public final int getModifiedGrassMultiplier(int rawgrassmult) {
|
||||
if (grassmult == 0) return rawgrassmult; // common case: no override
|
||||
if (grassmult < 0) return -grassmult; // fixed color (pre-masked at set-time)
|
||||
return ((rawgrassmult & 0xfefefe) + grassmult) >> 1; // blend
|
||||
}
|
||||
|
||||
public final int getModifiedFoliageMultiplier(int rawfoliagemult) {
|
||||
if (foliagemult == 0) return rawfoliagemult; // common case: no override
|
||||
if (foliagemult < 0) return -foliagemult; // fixed color (pre-masked at set-time)
|
||||
return ((rawfoliagemult & 0xfefefe) + foliagemult) >> 1; // blend
|
||||
}
|
||||
public final int getWaterColorMult() {
|
||||
return watercolormult;
|
||||
}
|
||||
public final int ordinal() {
|
||||
return index;
|
||||
}
|
||||
public static final BiomeMap byBiomeID(int idx) {
|
||||
idx++;
|
||||
if((idx >= 0) && (idx < biome_by_index.length))
|
||||
return biome_by_index[idx];
|
||||
else
|
||||
return NULL;
|
||||
}
|
||||
public static final BiomeMap byBiomeResourceLocation(String resloc) {
|
||||
BiomeMap b = biome_by_rl.get(resloc);
|
||||
return (b != null) ? b : NULL;
|
||||
}
|
||||
public int getBiomeID() {
|
||||
return index - 1; // Index of biome in MC biome table
|
||||
}
|
||||
public static final BiomeMap[] values() {
|
||||
return biome_by_index;
|
||||
}
|
||||
public void setWaterColorMultiplier(int watercolormult) {
|
||||
this.watercolormult = watercolormult;
|
||||
}
|
||||
public void setGrassColorMultiplier(int grassmult) {
|
||||
this.grassmult = encodeColorMult(grassmult);
|
||||
}
|
||||
public void setFoliageColorMultiplier(int foliagemult) {
|
||||
this.foliagemult = encodeColorMult(foliagemult);
|
||||
}
|
||||
public void setTemperature(double tmp) {
|
||||
if(tmp < 0.0) tmp = 0.0;
|
||||
if(tmp > 1.0) tmp = 1.0;
|
||||
this.tmp = tmp;
|
||||
this.biomeindex256 = this.biomeLookup(256);
|
||||
}
|
||||
public void setRainfall(double rain) {
|
||||
if(rain < 0.0) rain = 0.0;
|
||||
if(rain > 1.0) rain = 1.0;
|
||||
this.rain = rain;
|
||||
this.biomeindex256 = this.biomeLookup(256);
|
||||
}
|
||||
public final double getTemperature() {
|
||||
return this.tmp;
|
||||
}
|
||||
public final double getRainfall() {
|
||||
return this.rain;
|
||||
}
|
||||
public boolean isDefault() {
|
||||
return isDef;
|
||||
}
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
public String toString() {
|
||||
return String.format("%s(%s)", id, resourcelocation);
|
||||
}
|
||||
public @SuppressWarnings("unchecked") <T> Optional<T> getBiomeObject() {
|
||||
return (Optional<T>) biomeObj;
|
||||
}
|
||||
public void setBiomeObject(Object biomeObj) {
|
||||
this.biomeObj = Optional.of(biomeObj);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package org.dynmap.common;
|
||||
|
||||
|
||||
public enum DynmapChatColor {
|
||||
BLACK(0x0),
|
||||
DARK_BLUE(0x1),
|
||||
DARK_GREEN(0x2),
|
||||
DARK_AQUA(0x3),
|
||||
DARK_RED(0x4),
|
||||
DARK_PURPLE(0x5),
|
||||
GOLD(0x6),
|
||||
GRAY(0x7),
|
||||
DARK_GRAY(0x8),
|
||||
BLUE(0x9),
|
||||
GREEN(0xA),
|
||||
AQUA(0xB),
|
||||
RED(0xC),
|
||||
LIGHT_PURPLE(0xD),
|
||||
YELLOW(0xE),
|
||||
WHITE(0xF);
|
||||
|
||||
private final String str;
|
||||
|
||||
private DynmapChatColor(final int code) {
|
||||
this.str = String.format("\u00A7%x", code);
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
return str;
|
||||
}
|
||||
public static String stripColor(final String input) {
|
||||
if (input == null) {
|
||||
return null;
|
||||
}
|
||||
return input.replaceAll("(?i)\u00A7[0-9A-Za-z]", "");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package org.dynmap.common;
|
||||
|
||||
public interface DynmapCommandSender {
|
||||
/**
|
||||
* Does command sender have given security privilege
|
||||
* @param privid - privilege ID
|
||||
* @return true if it does, false if it doesn't
|
||||
*/
|
||||
public boolean hasPrivilege(String privid);
|
||||
/**
|
||||
* Send given message to command sender
|
||||
* @param msg - message to be sent (with color codes marked &0 to &F)
|
||||
*/
|
||||
public void sendMessage(String msg);
|
||||
/**
|
||||
* Test if command sender is still connected/online
|
||||
* @return true if connected, false if not
|
||||
*/
|
||||
public boolean isConnected();
|
||||
/**
|
||||
* Is operator privilege
|
||||
* @return true if operator
|
||||
*/
|
||||
public boolean isOp();
|
||||
/**
|
||||
* Test for permission node (no dynmap. prefix assumed)
|
||||
* @param node - permission ID
|
||||
* @return true if allowed
|
||||
*/
|
||||
public boolean hasPermissionNode(String node);
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
package org.dynmap.common;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.dynmap.DynmapCore;
|
||||
import org.dynmap.DynmapWorld;
|
||||
import org.dynmap.Log;
|
||||
|
||||
/**
|
||||
* Simple handler for managing event listeners and dispatch in a neutral fashion
|
||||
*
|
||||
*/
|
||||
public class DynmapListenerManager {
|
||||
private DynmapCore core;
|
||||
|
||||
public DynmapListenerManager(DynmapCore core) {
|
||||
this.core = core;
|
||||
}
|
||||
public interface EventListener {
|
||||
}
|
||||
public interface WorldEventListener extends EventListener {
|
||||
public void worldEvent(DynmapWorld w);
|
||||
}
|
||||
public interface PlayerEventListener extends EventListener {
|
||||
public void playerEvent(DynmapPlayer p);
|
||||
}
|
||||
public interface ChatEventListener extends EventListener {
|
||||
public void chatEvent(DynmapPlayer p, String msg);
|
||||
}
|
||||
public interface BlockEventListener extends EventListener {
|
||||
public void blockEvent(String material, String w, int x, int y, int z);
|
||||
}
|
||||
public interface SignChangeEventListener extends EventListener {
|
||||
public void signChangeEvent(String material, String w, int x, int y, int z, String[] lines, DynmapPlayer p);
|
||||
}
|
||||
public enum EventType {
|
||||
WORLD_LOAD,
|
||||
WORLD_UNLOAD,
|
||||
WORLD_SPAWN_CHANGE,
|
||||
PLAYER_JOIN,
|
||||
PLAYER_QUIT,
|
||||
PLAYER_BED_LEAVE,
|
||||
PLAYER_CHAT,
|
||||
BLOCK_BREAK,
|
||||
SIGN_CHANGE
|
||||
}
|
||||
private Map<EventType, ArrayList<EventListener>> listeners = new EnumMap<EventType, ArrayList<EventListener>>(EventType.class);
|
||||
|
||||
public void addListener(EventType type, EventListener listener) {
|
||||
synchronized(listeners) {
|
||||
ArrayList<EventListener> lst = listeners.get(type);
|
||||
if(lst == null) {
|
||||
lst = new ArrayList<EventListener>();
|
||||
listeners.put(type, lst);
|
||||
core.getServer().requestEventNotification(type);
|
||||
}
|
||||
lst.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void processWorldEvent(EventType type, DynmapWorld w) {
|
||||
ArrayList<EventListener> lst = listeners.get(type);
|
||||
if(lst == null) return;
|
||||
int sz = lst.size();
|
||||
for(int i = 0; i < sz; i++) {
|
||||
EventListener el = lst.get(i);
|
||||
if(el instanceof WorldEventListener) {
|
||||
try {
|
||||
((WorldEventListener)el).worldEvent(w);
|
||||
} catch (Throwable t) {
|
||||
Log.warning("processWorldEvent(" + type + "," + w + ") - exception", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public void processPlayerEvent(EventType type, DynmapPlayer p) {
|
||||
ArrayList<EventListener> lst = listeners.get(type);
|
||||
if(lst == null) return;
|
||||
int sz = lst.size();
|
||||
for(int i = 0; i < sz; i++) {
|
||||
EventListener el = lst.get(i);
|
||||
if(el instanceof PlayerEventListener) {
|
||||
try {
|
||||
((PlayerEventListener)el).playerEvent(p);
|
||||
} catch (Throwable t) {
|
||||
Log.warning("processPlayerEvent(" + type + "," + p + ") - exception", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public void processChatEvent(EventType type, DynmapPlayer p, String msg) {
|
||||
ArrayList<EventListener> lst = listeners.get(type);
|
||||
if(lst == null) return;
|
||||
int sz = lst.size();
|
||||
for(int i = 0; i < sz; i++) {
|
||||
EventListener el = lst.get(i);
|
||||
if(el instanceof ChatEventListener) {
|
||||
try {
|
||||
((ChatEventListener)el).chatEvent(p, msg);
|
||||
} catch (Throwable t) {
|
||||
Log.warning("processChatEvent(" + type + "," + msg + ") - exception", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public void processBlockEvent(EventType type, String material, String world, int x, int y, int z)
|
||||
{
|
||||
ArrayList<EventListener> lst = listeners.get(type);
|
||||
if(lst == null) return;
|
||||
int sz = lst.size();
|
||||
for(int i = 0; i < sz; i++) {
|
||||
EventListener el = lst.get(i);
|
||||
if(el instanceof BlockEventListener) {
|
||||
try {
|
||||
((BlockEventListener)el).blockEvent(material, world, x, y, z);
|
||||
} catch (Throwable t) {
|
||||
Log.warning("processBlockEvent(" + type + "," + material + "," + world + "," + x + "," + y + "," + z + ") - exception", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public void processSignChangeEvent(EventType type, String material, String world, int x, int y, int z, String[] lines, DynmapPlayer p)
|
||||
{
|
||||
ArrayList<EventListener> lst = listeners.get(type);
|
||||
if(lst == null) return;
|
||||
int sz = lst.size();
|
||||
for(int i = 0; i < sz; i++) {
|
||||
EventListener el = lst.get(i);
|
||||
if(el instanceof SignChangeEventListener) {
|
||||
try {
|
||||
((SignChangeEventListener)el).signChangeEvent(material, world, x, y, z, lines, p);
|
||||
} catch (Throwable t) {
|
||||
Log.warning("processSignChangeEvent(" + type + "," + material + "," + world + "," + x + "," + y + "," + z + ") - exception", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Clean up registered listeners */
|
||||
public void cleanup() {
|
||||
for(ArrayList<EventListener> l : listeners.values())
|
||||
l.clear();
|
||||
listeners.clear();
|
||||
}
|
||||
}
|
||||
111
DynmapCore/src/main/java/org/dynmap/common/DynmapPlayer.java
Normal file
111
DynmapCore/src/main/java/org/dynmap/common/DynmapPlayer.java
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
package org.dynmap.common;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.dynmap.DynmapLocation;
|
||||
|
||||
/**
|
||||
* Player (server neutral) - represents online or offline player
|
||||
*/
|
||||
public interface DynmapPlayer extends DynmapCommandSender {
|
||||
/**
|
||||
* Get player ID
|
||||
* @return ID (case insensitive)
|
||||
*/
|
||||
public String getName();
|
||||
/**
|
||||
* Get player display name
|
||||
* @return display name
|
||||
*/
|
||||
public String getDisplayName();
|
||||
/**
|
||||
* Is player online?
|
||||
* @return true if online
|
||||
*/
|
||||
public boolean isOnline();
|
||||
/**
|
||||
* Get current location of player
|
||||
* @return location
|
||||
*/
|
||||
public DynmapLocation getLocation();
|
||||
/**
|
||||
* Get world ID of player
|
||||
* @return id
|
||||
*/
|
||||
public String getWorld();
|
||||
/**
|
||||
* Get connected address for player
|
||||
* @return connection address, or null if unknown
|
||||
*/
|
||||
public InetSocketAddress getAddress();
|
||||
/**
|
||||
* Check if player is sneaking
|
||||
* @return true if sneaking
|
||||
*/
|
||||
public boolean isSneaking();
|
||||
|
||||
/**
|
||||
* get spectator gamemode
|
||||
* @return true if gamemode spectator
|
||||
*/
|
||||
public boolean isSpectator();
|
||||
/**
|
||||
* Get health
|
||||
* @return health points
|
||||
*/
|
||||
public double getHealth();
|
||||
/**
|
||||
* Get armor points
|
||||
* @return armor points
|
||||
*/
|
||||
public int getArmorPoints();
|
||||
/**
|
||||
* Get spawn bed location
|
||||
* @return bed location, or null if none
|
||||
*/
|
||||
public DynmapLocation getBedSpawnLocation();
|
||||
/**
|
||||
* Get last login time
|
||||
* @return UTC time (msec) of last login
|
||||
*/
|
||||
public long getLastLoginTime();
|
||||
/**
|
||||
* Get first login time
|
||||
* @return UTC time (msec) of first login
|
||||
*/
|
||||
public long getFirstLoginTime();
|
||||
/**
|
||||
* Is invisible
|
||||
* @return true if invisible
|
||||
*/
|
||||
public boolean isInvisible();
|
||||
/**
|
||||
* Get sort weight (ordered lowest to highest in player list: 0=default)
|
||||
* @return sort weight
|
||||
*/
|
||||
public int getSortWeight();
|
||||
/**
|
||||
* Set sort weight (ordered lowest to highest in player list: 0=default)
|
||||
* @param wt - sort weight
|
||||
*/
|
||||
public void setSortWeight(int wt);
|
||||
/**
|
||||
* Get skin URL for player
|
||||
* @return URL, or null if not available
|
||||
*/
|
||||
public default String getSkinURL() { return null; }
|
||||
/**
|
||||
* Get player UUID
|
||||
* Return UUID, or null if not available
|
||||
*/
|
||||
public default UUID getUUID() { return null; }
|
||||
/**
|
||||
* Send title and subtitle text (called from server thread)
|
||||
*/
|
||||
public default void sendTitleText(String title, String subtitle, int fadeInTicks, int stayTicks, int fadeOutTIcks) {
|
||||
// Fallback if not implemented
|
||||
if (title != null) this.sendMessage(title);;
|
||||
if (subtitle != null) this.sendMessage(subtitle);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
package org.dynmap.common;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import org.dynmap.DynmapChunk;
|
||||
import org.dynmap.DynmapWorld;
|
||||
import org.dynmap.common.DynmapListenerManager.EventType;
|
||||
import org.dynmap.utils.MapChunkCache;
|
||||
|
||||
/**
|
||||
* This interface defines a server-neutral interface for the DynmapCore and other neutral components to use to access server provided
|
||||
* services. Platform-specific plugin must supply DynmapCore with an instance of an object implementing this interface.
|
||||
*/
|
||||
public abstract class DynmapServerInterface {
|
||||
/**
|
||||
* Schedule task to run on server-safe thread (one suitable for other server API calls)
|
||||
* @param run - runnable method
|
||||
* @param delay - delay in server ticks (50msec)
|
||||
*/
|
||||
public abstract void scheduleServerTask(Runnable run, long delay);
|
||||
/**
|
||||
* Call method on server-safe thread
|
||||
* @param task - Callable method
|
||||
* @param <T> - return value type for method called
|
||||
* @return future for completion of call
|
||||
*/
|
||||
public abstract <T> Future<T> callSyncMethod(Callable<T> task);
|
||||
/**
|
||||
* Get list of online players
|
||||
* @return list of online players
|
||||
*/
|
||||
public abstract DynmapPlayer[] getOnlinePlayers();
|
||||
/**
|
||||
* Request reload of plugin
|
||||
*/
|
||||
public abstract void reload();
|
||||
/**
|
||||
* Get active player
|
||||
* @param name - player name
|
||||
* @return player
|
||||
*/
|
||||
public abstract DynmapPlayer getPlayer(String name);
|
||||
/**
|
||||
* Get offline player
|
||||
* @param name - player name
|
||||
* @return player (offline or not)
|
||||
*/
|
||||
public abstract DynmapPlayer getOfflinePlayer(String name);
|
||||
|
||||
/**
|
||||
* Get banned IPs
|
||||
* @return set of banned IPs
|
||||
*/
|
||||
public abstract Set<String> getIPBans();
|
||||
/**
|
||||
* Get server name
|
||||
* @return server name
|
||||
*/
|
||||
public abstract String getServerName();
|
||||
/**
|
||||
* Test if player ID is banned
|
||||
* @param pid - player ID
|
||||
* @return true if banned
|
||||
*/
|
||||
public abstract boolean isPlayerBanned(String pid);
|
||||
/**
|
||||
* Strip out chat color
|
||||
* @param s - string to strip
|
||||
* @return string stripped of color codes
|
||||
*/
|
||||
public abstract String stripChatColor(String s);
|
||||
/**
|
||||
* Request notificiation for given events (used by DynmapListenerManager)
|
||||
* @param type - event type
|
||||
* @return true if successful
|
||||
*/
|
||||
public abstract boolean requestEventNotification(EventType type);
|
||||
/**
|
||||
* Send notification of web chat message
|
||||
* @param source - source
|
||||
* @param name - name
|
||||
* @param msg - message text
|
||||
* @return true if not cancelled
|
||||
*/
|
||||
public abstract boolean sendWebChatEvent(String source, String name, String msg);
|
||||
/**
|
||||
* Broadcast message to players
|
||||
* @param msg - message
|
||||
*/
|
||||
public abstract void broadcastMessage(String msg);
|
||||
/**
|
||||
* Get Biome ID lis
|
||||
* @return list of biome IDs
|
||||
*/
|
||||
public abstract String[] getBiomeIDs();
|
||||
/**
|
||||
* Get snapshot cache hit rate
|
||||
* @return hit rate
|
||||
*/
|
||||
public abstract double getCacheHitRate();
|
||||
/**
|
||||
* Reset cache stats
|
||||
*/
|
||||
public abstract void resetCacheStats();
|
||||
/**
|
||||
* Get world by name
|
||||
* @param wname - world name
|
||||
* @return world object, or null if not found
|
||||
*/
|
||||
public abstract DynmapWorld getWorldByName(String wname);
|
||||
/**
|
||||
* Test which of given set of permisssions a possibly offline user has
|
||||
* @param player - player
|
||||
* @param perms - set of permission IDs
|
||||
* @return set of permission IDs allowed to player
|
||||
*/
|
||||
public abstract Set<String> checkPlayerPermissions(String player, Set<String> perms);
|
||||
/**
|
||||
* Test single permission attribute
|
||||
* @param player - player
|
||||
* @param perm - permission ID
|
||||
* @return true if permitted
|
||||
*/
|
||||
public abstract boolean checkPlayerPermission(String player, String perm);
|
||||
/**
|
||||
* Render processor helper - used by code running on render threads to request chunk snapshot cache
|
||||
* @param w - world
|
||||
* @param chunks - list of chunks
|
||||
* @param blockdata - include block data, if true
|
||||
* @param highesty - include highest Y, if true
|
||||
* @param biome - include biome data, if true
|
||||
* @param rawbiome - include raw biome data, if true
|
||||
* @return chunk map
|
||||
*/
|
||||
public abstract MapChunkCache createMapChunkCache(DynmapWorld w, List<DynmapChunk> chunks,
|
||||
boolean blockdata, boolean highesty, boolean biome, boolean rawbiome);
|
||||
/**
|
||||
* Get maximum player count
|
||||
* @return maximum online players
|
||||
*/
|
||||
public abstract int getMaxPlayers();
|
||||
/**
|
||||
* Get current player count
|
||||
* @return number of online players
|
||||
*/
|
||||
public abstract int getCurrentPlayers();
|
||||
/**
|
||||
* Test if given mod is loaded (Forge)
|
||||
* @param name - mod name
|
||||
* @return true if mod loaded
|
||||
*/
|
||||
public boolean isModLoaded(String name) {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Get version of mod with given name
|
||||
*
|
||||
* @param name - name of mod
|
||||
* @return version, or null of not found
|
||||
*/
|
||||
public String getModVersion(String name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get block ID at given coordinate in given world (if chunk is loaded)
|
||||
* @param wname - world name
|
||||
* @param x - X coordinate
|
||||
* @param y - Y coordinate
|
||||
* @param z - Z coordinate
|
||||
* @return block ID, or -1 if chunk at given coordinate isn't loaded
|
||||
*/
|
||||
public abstract int getBlockIDAt(String wname, int x, int y, int z);
|
||||
/**
|
||||
* Checks if a sign is at a given coordinate in a given world (if chunk is loaded)
|
||||
* @param wname - world name
|
||||
* @param x - X coordinate
|
||||
* @param y - Y coordinate
|
||||
* @param z - Z coordinate
|
||||
* @return 1 if a sign is at the location, 0 if it's not, -1 if the chunk isn't loaded
|
||||
*/
|
||||
public abstract int isSignAt(String wname, int x, int y, int z);
|
||||
/**
|
||||
* Get current TPS for server (20.0 is nominal)
|
||||
* @return ticks per second
|
||||
*/
|
||||
public abstract double getServerTPS();
|
||||
/**
|
||||
* Get address configured for server
|
||||
*
|
||||
* @return "" or null if none configured
|
||||
*/
|
||||
public abstract String getServerIP();
|
||||
/**
|
||||
* Get file/directory for given mod (for loading mod resources)
|
||||
* @param mod - mod name
|
||||
* @return file or directory, or null if not loaded
|
||||
*/
|
||||
public File getModContainerFile(String mod) {
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Get mod list
|
||||
* @return list of mods
|
||||
*/
|
||||
public List<String> getModList() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
/**
|
||||
* Get block ID map (modID:blockname, keyed by block ID)
|
||||
* @return block ID map
|
||||
*/
|
||||
public Map<Integer, String> getBlockIDMap() {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
/**
|
||||
* Open resource (check all mods)
|
||||
* @param modid - mod id
|
||||
* @param rname - resource namep
|
||||
* @return stream, or null
|
||||
*/
|
||||
public InputStream openResource(String modid, String rname) {
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Get block unique ID map (module:blockid)
|
||||
* @return block unique ID map
|
||||
*/
|
||||
public Map<String, Integer> getBlockUniqueIDMap() {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
/**
|
||||
* Get item unique ID map (module:itemid)
|
||||
* @return item unique ID map
|
||||
*/
|
||||
public Map<String, Integer> getItemUniqueIDMap() {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
/**
|
||||
* Test if current thread is server thread
|
||||
* @return true if server thread
|
||||
*/
|
||||
public boolean isServerThread() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package org.dynmap.common.chunk;
|
||||
|
||||
public interface GenericBitStorage {
|
||||
public int get(int idx);
|
||||
}
|
||||
|
|
@ -0,0 +1,234 @@
|
|||
package org.dynmap.common.chunk;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.dynmap.renderer.DynmapBlockState;
|
||||
import org.dynmap.common.BiomeMap;
|
||||
|
||||
// Generic chunk representation
|
||||
public class GenericChunk {
|
||||
public final int cx, cz; // Chunk coord (world coord / 16)
|
||||
public final GenericChunkSection[] sections;
|
||||
public final int sectionCnt;
|
||||
public final int cy_min; // CY value of first section in sections list (index = (Y >> 4) - cy_min
|
||||
public final long inhabitedTicks;
|
||||
public final int dataVersion; // Version of chunk data loaded
|
||||
public final String chunkStatus; // Chunk status of loaded chunk
|
||||
public final boolean isEmpty; // All sections are empty
|
||||
|
||||
private GenericChunk(int cx, int cz, int cy_min, GenericChunkSection[] sections, long inhabTicks, int dataversion, String chunkstatus) {
|
||||
this.cx = cx;
|
||||
this.cz = cz;
|
||||
this.inhabitedTicks = inhabTicks;
|
||||
this.dataVersion = dataversion;
|
||||
this.chunkStatus = chunkstatus;
|
||||
this.sections = new GenericChunkSection[sections.length + 2]; // Add one empty at top and bottom
|
||||
this.cy_min = cy_min - 1; // Include empty at bottom
|
||||
Arrays.fill(this.sections, GenericChunkSection.EMPTY); // Fill all spots with empty, including pad on bottom/top
|
||||
boolean empty = true;
|
||||
for (int off = 0; off < sections.length; off++) {
|
||||
if (sections[off] != null) { // If defined, set the section
|
||||
this.sections[off+1] = sections[off];
|
||||
empty = empty && sections[off].isEmpty;
|
||||
}
|
||||
}
|
||||
this.sectionCnt = sections.length;
|
||||
this.isEmpty = empty;
|
||||
}
|
||||
// Get section for given block Y coord
|
||||
public final GenericChunkSection getSection(int y) {
|
||||
int idx = (y >> 4) - this.cy_min;
|
||||
if ((idx < 0) || (idx >= sections.length)) {
|
||||
return GenericChunkSection.EMPTY;
|
||||
}
|
||||
return this.sections[idx];
|
||||
}
|
||||
|
||||
public final DynmapBlockState getBlockType(int x, int y, int z) {
|
||||
return getSection(y).blocks.getBlock(x, y, z);
|
||||
}
|
||||
public final DynmapBlockState getBlockType(GenericChunkPos pos) {
|
||||
return getSection(pos.y).blocks.getBlock(pos);
|
||||
}
|
||||
public final int getBlockSkyLight(int x, int y, int z) {
|
||||
return getSection(y).sky.getLight(x, y, z);
|
||||
}
|
||||
public final int getBlockSkyLight(GenericChunkPos pos) {
|
||||
return getSection(pos.y).sky.getLight(pos);
|
||||
}
|
||||
public final int getBlockEmittedLight(int x, int y, int z) {
|
||||
return getSection(y).emitted.getLight(x, y, z);
|
||||
}
|
||||
public final int getBlockEmittedLight(GenericChunkPos pos) {
|
||||
return getSection(pos.y).emitted.getLight(pos);
|
||||
}
|
||||
public final BiomeMap getBiome(int x, int y, int z) {
|
||||
return getSection(y).biomes.getBiome(x, y, z);
|
||||
}
|
||||
public final BiomeMap getBiome(GenericChunkPos pos) {
|
||||
return getSection(pos.y).biomes.getBiome(pos);
|
||||
}
|
||||
public final boolean isSectionEmpty(int cy) {
|
||||
return getSection(cy << 4).isEmpty;
|
||||
}
|
||||
public final long getInhabitedTicks() {
|
||||
return inhabitedTicks;
|
||||
}
|
||||
public String toString() {
|
||||
return String.format("chunk(%d,%d:%s,off=%d", cx, cz, Arrays.deepToString((sections)), cy_min);
|
||||
}
|
||||
|
||||
// Builder for fabricating finalized chunk
|
||||
public static class Builder {
|
||||
int x;
|
||||
int z;
|
||||
int y_min;
|
||||
GenericChunkSection[] sections;
|
||||
long inhabTicks;
|
||||
String chunkstatus;
|
||||
int dataversion;
|
||||
|
||||
public Builder(int world_ymin, int world_ymax) {
|
||||
reset(world_ymin, world_ymax);
|
||||
}
|
||||
public void reset(int world_ymin, int world_ymax) {
|
||||
x = 0; z = 0;
|
||||
y_min = world_ymin >> 4;
|
||||
dataversion = 0;
|
||||
chunkstatus = null;
|
||||
int y_max = (world_ymax + 15) >> 4; // Round up
|
||||
sections = new GenericChunkSection[y_max - y_min + 1]; // Range for all potential sections
|
||||
}
|
||||
// Set inhabited ticks
|
||||
public Builder inhabitedTicks(long inh) {
|
||||
this.inhabTicks = inh;
|
||||
return this;
|
||||
}
|
||||
// Set section
|
||||
public Builder addSection(int sy, GenericChunkSection sect) {
|
||||
if ((sy >= y_min) && ((sy - y_min) < sections.length)) {
|
||||
this.sections[sy - y_min] = sect;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
// Set coordinates
|
||||
public Builder coords(int sx, int sz) {
|
||||
this.x = sx;
|
||||
this.z = sz;
|
||||
return this;
|
||||
}
|
||||
// Generate simple sky lighting (must be after all sections have been added)
|
||||
public Builder generateSky() {
|
||||
int sky[] = new int[256]; // ZX array
|
||||
boolean nonOpaque[] = new boolean[256]; // ZX array of non opaque blocks (atten < 15)
|
||||
Arrays.fill(sky, 15); // Start fully lit at top
|
||||
GenericChunkSection.Builder bld = new GenericChunkSection.Builder();
|
||||
boolean allzero = false;
|
||||
// Make light array for each section, start from top
|
||||
for (int i = (sections.length - 1); i >= 0; i--) {
|
||||
GenericChunkSection sect = sections[i];
|
||||
if (sect == null) continue;
|
||||
if (allzero) { // Start section with all zero already, just zero it and move on
|
||||
// Replace section with new all zero light
|
||||
sections[i] = bld.buildFrom(sect, 0);
|
||||
continue;
|
||||
}
|
||||
byte[] ssky = new byte[2048];
|
||||
int allfullcnt = 0;
|
||||
// Top to bottom
|
||||
for (int y = 15; (y >= 0) && (!allzero); y--) {
|
||||
int totalval = 0; // Use for allzero or allfull
|
||||
int yidx = y << 7;
|
||||
// Light next layer down
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
int idx = (z << 4) + x;
|
||||
int val = sky[idx];
|
||||
DynmapBlockState bs = sect.blocks.getBlock(x, y, z); // Get block
|
||||
int atten = bs.getLightAttenuation();
|
||||
if ((atten > 0) && (val > 0)) {
|
||||
val = (val >= atten) ? (val - atten) : 0;
|
||||
sky[idx] = val;
|
||||
}
|
||||
nonOpaque[idx] = atten < 15;
|
||||
totalval += val;
|
||||
}
|
||||
}
|
||||
allzero = (totalval == 0);
|
||||
boolean allfull = (totalval == (15 * 256));
|
||||
if (allfull) allfullcnt++;
|
||||
// If not all fully lit nor all zero, handle horizontal spread
|
||||
if (! (allfull || allzero)) {
|
||||
// Now do horizontal spread
|
||||
boolean changed;
|
||||
do {
|
||||
changed = false;
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
int idx = (z << 4) + x;
|
||||
int cur = sky[idx];
|
||||
boolean curnonopaq = nonOpaque[idx];
|
||||
if (x < 15) { // If not right edge, check X spread
|
||||
int right = sky[idx+1];
|
||||
boolean rightnonopaq = nonOpaque[idx+1];
|
||||
// If spread right
|
||||
if (rightnonopaq && ((cur - 1) > right)) {
|
||||
sky[idx+1] = cur - 1; changed = true;
|
||||
}
|
||||
// If spread left
|
||||
else if (curnonopaq && (cur < (right - 1))) {
|
||||
sky[idx] = cur = right - 1; changed = true;
|
||||
}
|
||||
}
|
||||
if (z < 15) { // If not bottom edge, check Z spread
|
||||
int down = sky[idx+16];
|
||||
boolean downnonopaq = nonOpaque[idx+16];
|
||||
// If spread down
|
||||
if (downnonopaq && ((cur - 1) > down)) {
|
||||
sky[idx+16] = cur - 1; changed = true;
|
||||
}
|
||||
// If spread up
|
||||
else if (curnonopaq && (cur < (down - 1))) {
|
||||
sky[idx] = down - 1; changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (changed);
|
||||
// Save values
|
||||
for (int v = 0; v < 128; v++) {
|
||||
ssky[yidx | v] = (byte)(sky[v << 1] | (sky[(v << 1) + 1] << 4));
|
||||
}
|
||||
}
|
||||
else if (allfull) { // All light, just fill it
|
||||
for (int v = 0; v < 128; v++) {
|
||||
ssky[yidx | v] = (byte) 0xFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Replace section with new one with new lighting
|
||||
if (allfullcnt == 16) { // Just full?
|
||||
sections[i] = bld.buildFrom(sect, 15);
|
||||
}
|
||||
else {
|
||||
sections[i] = bld.buildFrom(sect, ssky);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
// Set chunk status
|
||||
public Builder chunkStatus(String chunkstat) {
|
||||
this.chunkstatus = chunkstat;
|
||||
return this;
|
||||
}
|
||||
// Set data version
|
||||
public Builder dataVersion(int dataver) {
|
||||
this.dataversion = dataver;
|
||||
return this;
|
||||
}
|
||||
// Build chunk
|
||||
public GenericChunk build() {
|
||||
return new GenericChunk(x, z, y_min, sections, inhabTicks, dataversion, chunkstatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
package org.dynmap.common.chunk;
|
||||
|
||||
import java.lang.ref.Reference;
|
||||
import java.lang.ref.ReferenceQueue;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.HashMap;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.dynmap.utils.DynIntHashMap;
|
||||
|
||||
// Generic chunk cache
|
||||
public class GenericChunkCache {
|
||||
public static class ChunkCacheRec {
|
||||
public GenericChunk ss;
|
||||
public DynIntHashMap tileData;
|
||||
};
|
||||
|
||||
private CacheHashMap snapcache;
|
||||
private final Object snapcachelock;
|
||||
private ReferenceQueue<ChunkCacheRec> refqueue;
|
||||
private long cache_attempts;
|
||||
private long cache_success;
|
||||
private boolean softref;
|
||||
// World name -> small integer ID, used to build long cache keys without String concatenation.
|
||||
// Accessed only while holding snapcachelock.
|
||||
private final HashMap<String, Integer> worldIds = new HashMap<String, Integer>();
|
||||
private int nextWorldId = 0;
|
||||
|
||||
private static class CacheRec {
|
||||
Reference<ChunkCacheRec> ref;
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public class CacheHashMap extends LinkedHashMap<Long, CacheRec> {
|
||||
private int limit;
|
||||
private IdentityHashMap<Reference<ChunkCacheRec>, Long> reverselookup;
|
||||
|
||||
public CacheHashMap(int lim) {
|
||||
super(16, (float)0.75, true);
|
||||
limit = lim;
|
||||
reverselookup = new IdentityHashMap<Reference<ChunkCacheRec>, Long>();
|
||||
}
|
||||
protected boolean removeEldestEntry(Map.Entry<Long, CacheRec> last) {
|
||||
boolean remove = (size() >= limit);
|
||||
if(remove && (last != null) && (last.getValue() != null)) {
|
||||
reverselookup.remove(last.getValue().ref);
|
||||
}
|
||||
return remove;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create snapshot cache
|
||||
*/
|
||||
public GenericChunkCache(int max_size, boolean softref) {
|
||||
snapcachelock = new Object();
|
||||
snapcache = new CacheHashMap(max_size);
|
||||
refqueue = new ReferenceQueue<ChunkCacheRec>();
|
||||
this.softref = softref;
|
||||
}
|
||||
/**
|
||||
* Encode world name + chunk coords as a single long key.
|
||||
* Worlds are assigned small integer IDs (10 bits) on first use.
|
||||
* cx and cz are each encoded in 27 bits (signed, supports ±8M chunks / ±128M blocks).
|
||||
* Must be called while holding snapcachelock.
|
||||
*/
|
||||
private long getKey(String w, int cx, int cz) {
|
||||
Integer wid = worldIds.get(w);
|
||||
if (wid == null) {
|
||||
wid = nextWorldId++;
|
||||
worldIds.put(w, wid);
|
||||
}
|
||||
return ((long)(wid & 0x3FF) << 54) | ((long)(cx & 0x7FFFFFF) << 27) | (long)(cz & 0x7FFFFFF);
|
||||
}
|
||||
/**
|
||||
* Invalidate cached snapshot, if in cache
|
||||
*/
|
||||
public void invalidateSnapshot(String w, int x, int y, int z) {
|
||||
synchronized(snapcachelock) {
|
||||
long key = getKey(w, x>>4, z>>4);
|
||||
CacheRec rec = (snapcache != null) ? snapcache.remove(key) : null;
|
||||
if(rec != null) {
|
||||
snapcache.reverselookup.remove(rec.ref);
|
||||
rec.ref.clear();
|
||||
}
|
||||
}
|
||||
//processRefQueue();
|
||||
}
|
||||
/**
|
||||
* Invalidate cached snapshot, if in cache
|
||||
*/
|
||||
public void invalidateSnapshot(String w, int x0, int y0, int z0, int x1, int y1, int z1) {
|
||||
synchronized(snapcachelock) {
|
||||
for(int xx = (x0>>4); xx <= (x1>>4); xx++) {
|
||||
for(int zz = (z0>>4); zz <= (z1>>4); zz++) {
|
||||
long key = getKey(w, xx, zz);
|
||||
CacheRec rec = (snapcache != null) ? snapcache.remove(key) : null;
|
||||
if(rec != null) {
|
||||
snapcache.reverselookup.remove(rec.ref);
|
||||
rec.ref.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//processRefQueue();
|
||||
}
|
||||
/**
|
||||
* Look for chunk snapshot in cache
|
||||
*/
|
||||
public ChunkCacheRec getSnapshot(String w, int chunkx, int chunkz) {
|
||||
processRefQueue();
|
||||
ChunkCacheRec ss = null;
|
||||
CacheRec rec;
|
||||
synchronized(snapcachelock) {
|
||||
long key = getKey(w, chunkx, chunkz);
|
||||
rec = (snapcache != null) ? snapcache.get(key) : null;
|
||||
if(rec != null) {
|
||||
ss = rec.ref.get();
|
||||
if(ss == null) {
|
||||
snapcache.reverselookup.remove(rec.ref);
|
||||
snapcache.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
cache_attempts++;
|
||||
if(ss != null) cache_success++;
|
||||
|
||||
return ss;
|
||||
}
|
||||
/**
|
||||
* Add chunk snapshot to cache
|
||||
*/
|
||||
public void putSnapshot(String w, int chunkx, int chunkz, ChunkCacheRec ss) {
|
||||
processRefQueue();
|
||||
CacheRec rec = new CacheRec();
|
||||
if (softref)
|
||||
rec.ref = new SoftReference<ChunkCacheRec>(ss, refqueue);
|
||||
else
|
||||
rec.ref = new WeakReference<ChunkCacheRec>(ss, refqueue);
|
||||
synchronized(snapcachelock) {
|
||||
long key = getKey(w, chunkx, chunkz);
|
||||
CacheRec prevrec = (snapcache != null) ? snapcache.put(key, rec) : null;
|
||||
if(prevrec != null) {
|
||||
snapcache.reverselookup.remove(prevrec.ref);
|
||||
}
|
||||
snapcache.reverselookup.put(rec.ref, key);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Process reference queue
|
||||
*/
|
||||
private void processRefQueue() {
|
||||
Reference<? extends ChunkCacheRec> ref;
|
||||
while((ref = refqueue.poll()) != null) {
|
||||
synchronized(snapcachelock) {
|
||||
Long k = (snapcache != null) ? snapcache.reverselookup.remove(ref) : null;
|
||||
if(k != null) {
|
||||
snapcache.remove(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get hit rate (percent)
|
||||
*/
|
||||
public double getHitRate() {
|
||||
if(cache_attempts > 0) {
|
||||
return (100.0*cache_success)/(double)cache_attempts;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
/**
|
||||
* Reset cache stats
|
||||
*/
|
||||
public void resetStats() {
|
||||
cache_attempts = cache_success = 0;
|
||||
}
|
||||
/**
|
||||
* Cleanup
|
||||
*/
|
||||
public void cleanup() {
|
||||
synchronized(snapcachelock) {
|
||||
if(snapcache != null) {
|
||||
snapcache.clear();
|
||||
snapcache.reverselookup.clear();
|
||||
snapcache.reverselookup = null;
|
||||
snapcache = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package org.dynmap.common.chunk;
|
||||
|
||||
// Generic block location iterator - represents 3D position, but includes fast precomputed chunk and section offsets
|
||||
public class GenericChunkPos {
|
||||
public int x, y, z; // 3D world position
|
||||
public int cx, cz; // 2D chunk position (x / 16, z / 16)
|
||||
public int cy; // Vertical section index (Y / 16)
|
||||
public int sx, sy, sz; // 3D section position (x % 16, y % 16, z % 16)
|
||||
public int soffset; // Section offset (256 * sy) + (16 * sz) + sx
|
||||
public int sdiv4offset; // Subsection offset (16 * (sy / 4)) + (4 * (sz / 4)) + (sx / 4) (3D biomes)
|
||||
|
||||
public GenericChunkPos(int x, int y, int z) {
|
||||
setPos(x, y, z);
|
||||
}
|
||||
// Replace X value
|
||||
public final void setX(int x) {
|
||||
this.x = x;
|
||||
this.cx = x >> 4;
|
||||
this.sx = x & 0xF;
|
||||
this.soffset = (sy << 8) | (sz << 4) | sx;
|
||||
this.sdiv4offset = ((sy & 0xC) << 4) | (sz & 0xC) | ((sx & 0xC) >> 2);
|
||||
}
|
||||
// Replace Y value
|
||||
public final void setY(int y) {
|
||||
this.y = y;
|
||||
this.cy = y >> 4;
|
||||
this.sy = y & 0xF;
|
||||
this.soffset = (sy << 8) | (sz << 4) | sx;
|
||||
this.sdiv4offset = ((sy & 0xC) << 4) | (sz & 0xC) | ((sx & 0xC) >> 2);
|
||||
}
|
||||
// Replace Z value
|
||||
public final void setZ(int z) {
|
||||
this.z = z;
|
||||
this.cz = z >> 4;
|
||||
this.sz = z & 0xF;
|
||||
this.soffset = (sy << 8) | (sz << 4) | sx;
|
||||
this.sdiv4offset = ((sy & 0xC) << 4) | (sz & 0xC) | ((sx & 0xC) >> 2);
|
||||
}
|
||||
// Replace X, Y, and Z values
|
||||
public final void setPos(int x, int y, int z) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.cx = x >> 4;
|
||||
this.cy = y >> 4;
|
||||
this.cz = z >> 4;
|
||||
this.sx = x & 0xF;
|
||||
this.sy = y & 0xF;
|
||||
this.sz = z & 0xF;
|
||||
this.soffset = (sy << 8) | (sz << 4) | sx;
|
||||
this.sdiv4offset = ((sy & 0xC) << 4) | (sz & 0xC) | ((sx & 0xC) >> 2);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,367 @@
|
|||
package org.dynmap.common.chunk;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.dynmap.common.BiomeMap;
|
||||
import org.dynmap.renderer.DynmapBlockState;
|
||||
|
||||
// Generic section: represents 16 x 16 x 16 grid of blocks
|
||||
public class GenericChunkSection {
|
||||
public final BiomeAccess biomes; // Access for biome data
|
||||
public final BlockStateAccess blocks; // Access for block states
|
||||
public final LightingAccess sky; // Access for sky light data
|
||||
public final LightingAccess emitted; // Access for emitted light data
|
||||
public final boolean isEmpty; // if true, section is all air with default sky and emitted light
|
||||
|
||||
// Block state access interface
|
||||
public interface BlockStateAccess {
|
||||
public DynmapBlockState getBlock(int x, int y, int z);
|
||||
public DynmapBlockState getBlock(GenericChunkPos pos);
|
||||
}
|
||||
private static class BlockStateAccess3D implements BlockStateAccess {
|
||||
private final DynmapBlockState blocks[]; // YZX order
|
||||
// Array given to us by builder
|
||||
BlockStateAccess3D(DynmapBlockState bs[]) {
|
||||
blocks = bs;
|
||||
}
|
||||
public final DynmapBlockState getBlock(int x, int y, int z) {
|
||||
return blocks[((y & 0xF) << 8) | ((z & 0xF) << 4) | (x & 0xF)];
|
||||
}
|
||||
public final DynmapBlockState getBlock(GenericChunkPos pos) {
|
||||
return blocks[pos.soffset];
|
||||
}
|
||||
}
|
||||
private static class BlockStateAccess3DPalette implements BlockStateAccess {
|
||||
private final DynmapBlockState palette[];
|
||||
private final short[] blocks; // YZX order
|
||||
// Array given to us by builder
|
||||
BlockStateAccess3DPalette(DynmapBlockState pal[], short[] blks) {
|
||||
blocks = blks;
|
||||
palette = pal;
|
||||
}
|
||||
public final DynmapBlockState getBlock(int x, int y, int z) {
|
||||
return palette[blocks[((y & 0xF) << 8) | ((z & 0xF) << 4) | (x & 0xF)]];
|
||||
}
|
||||
public final DynmapBlockState getBlock(GenericChunkPos pos) {
|
||||
return palette[blocks[pos.soffset]];
|
||||
}
|
||||
}
|
||||
private static class BlockStateAccessSingle implements BlockStateAccess {
|
||||
private final DynmapBlockState block;
|
||||
BlockStateAccessSingle(DynmapBlockState bs) {
|
||||
block = bs;
|
||||
}
|
||||
public final DynmapBlockState getBlock(int x, int y, int z) {
|
||||
return block;
|
||||
}
|
||||
public final DynmapBlockState getBlock(GenericChunkPos pos) {
|
||||
return block;
|
||||
}
|
||||
}
|
||||
// Biome access interface
|
||||
public interface BiomeAccess {
|
||||
public BiomeMap getBiome(int x, int y, int z);
|
||||
public BiomeMap getBiome(GenericChunkPos pos);
|
||||
}
|
||||
// For classic 2D biome map
|
||||
private static class BiomeAccess2D implements BiomeAccess {
|
||||
private final BiomeMap biomes[]; // (16 * Z) + X
|
||||
// Array given to us by builder in right format
|
||||
BiomeAccess2D(BiomeMap b[]) {
|
||||
biomes = b;
|
||||
}
|
||||
public final BiomeMap getBiome(int x, int y, int z) {
|
||||
return biomes[((z & 0xF) << 4) + (x & 0xF)];
|
||||
}
|
||||
public final BiomeMap getBiome(GenericChunkPos pos) {
|
||||
return biomes[pos.soffset & 0xFF]; // Just ZX portion
|
||||
}
|
||||
public String toString() {
|
||||
return String.format("Biome2D(%s)", Arrays.deepToString(biomes));
|
||||
}
|
||||
}
|
||||
// For 3D biome map
|
||||
private static class BiomeAccess3D implements BiomeAccess {
|
||||
private final BiomeMap biomes[]; // (16 * (Y >> 2)) + (4 * (Z >> 2)) + (X >> 2)
|
||||
// Array given to us by builder in right format (64 - YZX divided by 4)
|
||||
BiomeAccess3D(BiomeMap[] b) {
|
||||
biomes = b;
|
||||
}
|
||||
public final BiomeMap getBiome(int x, int y, int z) {
|
||||
return biomes[ ((y & 0xC) << 2) | (z & 0xC) | ((x & 0xC) >> 2) ];
|
||||
}
|
||||
public final BiomeMap getBiome(GenericChunkPos pos) {
|
||||
return biomes[pos.sdiv4offset];
|
||||
}
|
||||
public String toString() {
|
||||
return String.format("Biome3D(%s)", Arrays.deepToString(biomes));
|
||||
}
|
||||
}
|
||||
// For single biome map
|
||||
private static class BiomeAccessSingle implements BiomeAccess {
|
||||
private final BiomeMap biome;
|
||||
BiomeAccessSingle(BiomeMap b) {
|
||||
biome = b;
|
||||
}
|
||||
public final BiomeMap getBiome(int x, int y, int z) {
|
||||
return biome;
|
||||
}
|
||||
public final BiomeMap getBiome(GenericChunkPos pos) {
|
||||
return biome;
|
||||
}
|
||||
public String toString() {
|
||||
return String.format("Biome1(%s)", biome);
|
||||
}
|
||||
}
|
||||
// Lighting access interface
|
||||
public interface LightingAccess {
|
||||
public int getLight(int x, int y, int z);
|
||||
public int getLight(GenericChunkPos pos);
|
||||
}
|
||||
private static class LightingAccess3D implements LightingAccess {
|
||||
private final long[] light; // Nibble array (16 * y) * z (nibble at << (4*x))
|
||||
|
||||
// Construct using nibble array (same as lighting format in NBT fields) (128*Y + 8*Z + X/2) (oddX high, evenX low)
|
||||
LightingAccess3D(byte[] lig) {
|
||||
light = new long[256];
|
||||
if (lig != null) {
|
||||
for (int off = 0; (off < lig.length) && (off < 2048); off++) {
|
||||
light[off >> 3] |= (0xFFL & (long)lig[off]) << ((off & 0x7) << 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
public final int getLight(int x, int y, int z) {
|
||||
return 0xF & (int)(light[((y & 0xF) << 4) | (z & 0xF)] >> ((x & 0xF) << 2));
|
||||
}
|
||||
public final int getLight(GenericChunkPos pos) {
|
||||
return 0xF & (int)(light[pos.soffset >> 4] >> (4 * pos.sx));
|
||||
}
|
||||
}
|
||||
private static class LightingAccessSingle implements LightingAccess {
|
||||
private final int light;
|
||||
LightingAccessSingle(int lig) {
|
||||
light = lig & 0xF;
|
||||
}
|
||||
public final int getLight(int x, int y, int z) {
|
||||
return light;
|
||||
}
|
||||
public final int getLight(GenericChunkPos pos) {
|
||||
return light;
|
||||
}
|
||||
}
|
||||
private GenericChunkSection(BlockStateAccess blks, BiomeAccess bio, LightingAccess skyac, LightingAccess emitac, boolean empty) {
|
||||
blocks = blks;
|
||||
biomes = bio;
|
||||
sky = skyac;
|
||||
emitted = emitac;
|
||||
isEmpty = empty;
|
||||
}
|
||||
public String toString() {
|
||||
return String.format("sect(bip:%s)", biomes);
|
||||
}
|
||||
private static BiomeAccess defaultBiome = new BiomeAccessSingle(BiomeMap.NULL);
|
||||
private static BlockStateAccess defaultBlockState = new BlockStateAccessSingle(DynmapBlockState.AIR);
|
||||
private static LightingAccess defaultLight = new LightingAccessSingle(0);
|
||||
|
||||
// Shared default empty section
|
||||
public static final GenericChunkSection EMPTY = new GenericChunkSection(defaultBlockState, defaultBiome, new LightingAccessSingle(15), defaultLight, true);
|
||||
|
||||
// Factory for building section
|
||||
public static class Builder {
|
||||
private LightingAccess sk;
|
||||
private LightingAccess em;
|
||||
private DynmapBlockState bsaccumsing; // Used for single
|
||||
private DynmapBlockState bsaccum[]; // Use for incremental setting of 3D - YZX order
|
||||
private short[] bsblks; // Use for incremental setting of 3D palette - XZY order
|
||||
private DynmapBlockState[] bspal; // Palette for bsblks
|
||||
private BiomeMap baaccumsingle; // Use for single
|
||||
private BiomeMap baaccum[]; // Use for incremental setting of 3D biome - YZX order or 2D biome (ZX order) length used to control which
|
||||
private boolean empty;
|
||||
// Initialize builder with empty state
|
||||
public Builder() {
|
||||
reset();
|
||||
}
|
||||
// Reset builder to default state
|
||||
public void reset() {
|
||||
bsaccumsing = DynmapBlockState.AIR;
|
||||
bsaccum = null;
|
||||
baaccumsingle = BiomeMap.NULL;
|
||||
baaccum = null;
|
||||
bsblks = null;
|
||||
bspal = null;
|
||||
sk = defaultLight;
|
||||
em = defaultLight;
|
||||
empty = true;
|
||||
}
|
||||
// Set sky lighting to single value
|
||||
public Builder singleSkyLight(int val) {
|
||||
sk = new LightingAccessSingle(val);
|
||||
return this;
|
||||
}
|
||||
// Set sky lighting to given nibble array (YZX order)
|
||||
public Builder skyLight(byte[] data) {
|
||||
sk = new LightingAccess3D(data);
|
||||
return this;
|
||||
}
|
||||
// Set emitted lighting to single value
|
||||
public Builder singleEmittedLight(int val) {
|
||||
em = new LightingAccessSingle(val);
|
||||
return this;
|
||||
}
|
||||
// Set emitted lighting to given nibble array (YZX order)
|
||||
public Builder emittedLight(byte[] data) {
|
||||
em = new LightingAccess3D(data);
|
||||
return this;
|
||||
}
|
||||
// Set bipme to single value
|
||||
public Builder singleBiome(BiomeMap bio) {
|
||||
baaccumsingle = bio;
|
||||
baaccum = null;
|
||||
return this;
|
||||
}
|
||||
// Set bipme for 2D style
|
||||
public Builder xzBiome(int x, int z, BiomeMap bio) {
|
||||
if ((baaccum == null) || (baaccum.length != 256)) {
|
||||
baaccum = new BiomeMap[256];
|
||||
Arrays.fill(baaccum, BiomeMap.NULL);
|
||||
baaccumsingle = BiomeMap.NULL;
|
||||
}
|
||||
baaccum[((z & 0xF) << 4) + (x & 0xF)] = bio;
|
||||
return this;
|
||||
}
|
||||
// Set bipme to 3D style
|
||||
public Builder xyzBiome(int xdiv4, int ydiv4, int zdiv4, BiomeMap bio) {
|
||||
if ((baaccum == null) || (baaccum.length != 64)) {
|
||||
baaccum = new BiomeMap[64];
|
||||
Arrays.fill(baaccum, BiomeMap.NULL);
|
||||
baaccumsingle = BiomeMap.NULL;
|
||||
}
|
||||
baaccum[((ydiv4 & 0x3) << 4) + ((zdiv4 & 0x3) << 2) + (xdiv4 & 0x3)] = bio;
|
||||
return this;
|
||||
}
|
||||
// Set block state to single value
|
||||
public Builder singleBlockState(DynmapBlockState block) {
|
||||
bsaccumsing = block;
|
||||
bsaccum = null;
|
||||
bsblks = null;
|
||||
bspal = null;
|
||||
empty = block.isAir();
|
||||
return this;
|
||||
}
|
||||
// Set block state
|
||||
public Builder xyzBlockState(int x, int y, int z, DynmapBlockState block) {
|
||||
if (bsaccum == null) {
|
||||
bsaccum = new DynmapBlockState[4096];
|
||||
Arrays.fill(bsaccum, DynmapBlockState.AIR);
|
||||
bsaccumsing = DynmapBlockState.AIR;
|
||||
}
|
||||
bsaccum[((y & 0xF) << 8) + ((z & 0xF) << 4) + (x & 0xF)] = block;
|
||||
empty = false;
|
||||
return this;
|
||||
}
|
||||
// Set block state palette (states will be indexes vs this
|
||||
public Builder xyzBlockStatePalette(DynmapBlockState[] bspalette) {
|
||||
if (bsblks == null) {
|
||||
bsblks = new short[4096];
|
||||
}
|
||||
bspal = Arrays.copyOf(bspalette, bspalette.length);
|
||||
return this;
|
||||
}
|
||||
// Set block state using palette
|
||||
public Builder xyzBlockStateInPalette(int x, int y, int z, short palidx) {
|
||||
if (bsblks == null) {
|
||||
bsblks = new short[4096];
|
||||
}
|
||||
bsblks[((y & 0xF) << 8) + ((z & 0xF) << 4) + (x & 0xF)] = palidx;
|
||||
empty = false;
|
||||
return this;
|
||||
}
|
||||
// Build copy from existing section with new skylight (YZX nibble array)
|
||||
public GenericChunkSection buildFrom(GenericChunkSection s, byte[] sky) {
|
||||
LightingAccess skyA = new LightingAccess3D(sky);
|
||||
return new GenericChunkSection(s.blocks, s.biomes, skyA, s.emitted, s.isEmpty);
|
||||
}
|
||||
// Build copy from existing section with new single value skylight
|
||||
public GenericChunkSection buildFrom(GenericChunkSection s, int singlesky) {
|
||||
LightingAccess skyA = new LightingAccessSingle(singlesky);
|
||||
return new GenericChunkSection(s.blocks, s.biomes, skyA, s.emitted, s.isEmpty);
|
||||
}
|
||||
|
||||
// Build section based on current builder state
|
||||
public GenericChunkSection build() {
|
||||
// Process state access - see if we can reduce
|
||||
if (bsaccum != null) {
|
||||
DynmapBlockState v = bsaccum[0]; // Get first
|
||||
boolean mismatch = false;
|
||||
for (int i = 0; i < bsaccum.length; i++) {
|
||||
if (bsaccum[i] != v) {
|
||||
mismatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!mismatch) { // All the same?
|
||||
bsaccumsing = v;
|
||||
bsaccum = null;
|
||||
}
|
||||
}
|
||||
BlockStateAccess bs;
|
||||
if (bsaccum != null) {
|
||||
bs = new BlockStateAccess3D(bsaccum);
|
||||
bsaccum = null;
|
||||
empty = false;
|
||||
}
|
||||
else if (bspal != null) { // 3D palette
|
||||
// Only one state in palette?
|
||||
if (bspal.length == 1) {
|
||||
bs = new BlockStateAccessSingle(bspal[0]); // Just single
|
||||
}
|
||||
else {
|
||||
bs = new BlockStateAccess3DPalette(bspal, bsblks);
|
||||
}
|
||||
bspal = null;
|
||||
bsblks = null;
|
||||
}
|
||||
else if (bsaccumsing == DynmapBlockState.AIR) { // Just air?
|
||||
bs = defaultBlockState;
|
||||
empty = true;
|
||||
}
|
||||
else {
|
||||
bs = new BlockStateAccessSingle(bsaccumsing);
|
||||
bsaccumsing = DynmapBlockState.AIR;
|
||||
}
|
||||
// See if biome access can be reduced to single
|
||||
if (baaccum != null) {
|
||||
BiomeMap v = baaccum[0]; // Get first
|
||||
boolean mismatch = false;
|
||||
for (int i = 0; i < baaccum.length; i++) {
|
||||
if (baaccum[i] != v) {
|
||||
mismatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!mismatch) { // All the same?
|
||||
baaccumsingle = v;
|
||||
baaccum = null;
|
||||
}
|
||||
}
|
||||
BiomeAccess ba;
|
||||
if (baaccum != null) {
|
||||
if (baaccum.length == 64) { // 3D?
|
||||
ba = new BiomeAccess3D(baaccum);
|
||||
}
|
||||
else {
|
||||
ba = new BiomeAccess2D(baaccum);
|
||||
}
|
||||
baaccum = null;
|
||||
}
|
||||
else if (baaccumsingle == BiomeMap.NULL) { // Just null?
|
||||
ba = defaultBiome;
|
||||
}
|
||||
else {
|
||||
ba = new BiomeAccessSingle(baaccumsingle);
|
||||
baaccumsingle = BiomeMap.NULL;
|
||||
}
|
||||
return new GenericChunkSection(bs, ba, sk, em, empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,41 @@
|
|||
package org.dynmap.common.chunk;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
// Generic interface for accessing an NBT Composite object
|
||||
public interface GenericNBTCompound {
|
||||
public final byte TAG_END = 0;
|
||||
public final byte TAG_BYTE = 1;
|
||||
public final byte TAG_SHORT = 2;
|
||||
public final byte TAG_INT = 3;
|
||||
public final byte TAG_LONG = 4;
|
||||
public final byte TAG_FLOAT = 5;
|
||||
public final byte TAG_DOUBLE = 6;
|
||||
public final byte TAG_BYTE_ARRAY = 7;
|
||||
public final byte TAG_STRING = 8;
|
||||
public final byte TAG_LIST = 9;
|
||||
public final byte TAG_COMPOUND = 10;
|
||||
public final byte TAG_INT_ARRAY = 11;
|
||||
public final byte TAG_LONG_ARRAY = 12;
|
||||
public final byte TAG_ANY_NUMERIC = 99;
|
||||
|
||||
public Set<String> getAllKeys();
|
||||
public boolean contains(String s);
|
||||
public boolean contains(String s, int i);
|
||||
public byte getByte(String s);
|
||||
public short getShort(String s);
|
||||
public int getInt(String s);
|
||||
public long getLong(String s);
|
||||
public float getFloat(String s);
|
||||
public double getDouble(String s);
|
||||
public String getString(String s);
|
||||
public byte[] getByteArray(String s);
|
||||
public int[] getIntArray(String s);
|
||||
public long[] getLongArray(String s);
|
||||
public GenericNBTCompound getCompound(String s);
|
||||
public GenericNBTList getList(String s, int i);
|
||||
public boolean getBoolean(String s);
|
||||
public String getAsString(String s); /// get(s).getAsString()
|
||||
// Factory for bit storage
|
||||
public GenericBitStorage makeBitStorage(int bits, int count, long[] data);
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package org.dynmap.common.chunk;
|
||||
|
||||
// Generic interface for accessing an NBT Composite object
|
||||
public interface GenericNBTList {
|
||||
public int size();
|
||||
public String getString(int idx);
|
||||
public GenericNBTCompound getCompound(int idx);
|
||||
}
|
||||
31
DynmapCore/src/main/java/org/dynmap/debug/Debug.java
Normal file
31
DynmapCore/src/main/java/org/dynmap/debug/Debug.java
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
package org.dynmap.debug;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class Debug {
|
||||
private static ArrayList<Debugger> debuggers = new ArrayList<Debugger>();
|
||||
|
||||
public synchronized static void addDebugger(Debugger d) {
|
||||
debuggers.add(d);
|
||||
}
|
||||
|
||||
public synchronized static void removeDebugger(Debugger d) {
|
||||
debuggers.remove(d);
|
||||
}
|
||||
|
||||
public synchronized static void clearDebuggers() {
|
||||
debuggers.clear();
|
||||
}
|
||||
|
||||
public synchronized static void debug(String message) {
|
||||
for(int i = 0; i < debuggers.size(); i++) debuggers.get(i).debug(message);
|
||||
}
|
||||
|
||||
public synchronized static void error(String message) {
|
||||
for(int i = 0; i < debuggers.size(); i++) debuggers.get(i).error(message);
|
||||
}
|
||||
|
||||
public synchronized static void error(String message, Throwable thrown) {
|
||||
for(int i = 0; i < debuggers.size(); i++) debuggers.get(i).error(message, thrown);
|
||||
}
|
||||
}
|
||||
9
DynmapCore/src/main/java/org/dynmap/debug/Debugger.java
Normal file
9
DynmapCore/src/main/java/org/dynmap/debug/Debugger.java
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package org.dynmap.debug;
|
||||
|
||||
public interface Debugger {
|
||||
void debug(String message);
|
||||
|
||||
void error(String message);
|
||||
|
||||
void error(String message, Throwable thrown);
|
||||
}
|
||||
27
DynmapCore/src/main/java/org/dynmap/debug/LogDebugger.java
Normal file
27
DynmapCore/src/main/java/org/dynmap/debug/LogDebugger.java
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package org.dynmap.debug;
|
||||
|
||||
import org.dynmap.ConfigurationNode;
|
||||
import org.dynmap.DynmapCore;
|
||||
import org.dynmap.Log;
|
||||
|
||||
public class LogDebugger implements Debugger {
|
||||
public LogDebugger(DynmapCore core, ConfigurationNode configuration) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void debug(String message) {
|
||||
Log.info(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(String message) {
|
||||
Log.severe(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(String message, Throwable thrown) {
|
||||
Log.severe(message);
|
||||
thrown.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
21
DynmapCore/src/main/java/org/dynmap/debug/NullDebugger.java
Normal file
21
DynmapCore/src/main/java/org/dynmap/debug/NullDebugger.java
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package org.dynmap.debug;
|
||||
|
||||
import org.dynmap.ConfigurationNode;
|
||||
import org.dynmap.DynmapCore;
|
||||
|
||||
public class NullDebugger implements Debugger {
|
||||
public static final NullDebugger instance = new NullDebugger(null, null);
|
||||
|
||||
public NullDebugger(DynmapCore core, ConfigurationNode configuration) {
|
||||
}
|
||||
|
||||
public void debug(String message) {
|
||||
}
|
||||
|
||||
public void error(String message) {
|
||||
}
|
||||
|
||||
public void error(String message, Throwable thrown) {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,363 @@
|
|||
package org.dynmap.exporter;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.dynmap.DynmapCore;
|
||||
import org.dynmap.DynmapLocation;
|
||||
import org.dynmap.DynmapWorld;
|
||||
import org.dynmap.MapManager;
|
||||
import org.dynmap.common.DynmapCommandSender;
|
||||
import org.dynmap.common.DynmapPlayer;
|
||||
import org.dynmap.hdmap.HDShader;
|
||||
|
||||
/**
|
||||
* Handler for export commands (/dynmapexp)
|
||||
*/
|
||||
public class DynmapExpCommands {
|
||||
private HashMap<String, ExportContext> sessions = new HashMap<String, ExportContext>();
|
||||
|
||||
private static class ExportContext {
|
||||
public String shader = "stdtexture";
|
||||
public int xmin = Integer.MIN_VALUE;
|
||||
public int ymin = Integer.MIN_VALUE;
|
||||
public int zmin = Integer.MIN_VALUE;
|
||||
public int xmax = Integer.MIN_VALUE;
|
||||
public int ymax = Integer.MIN_VALUE;
|
||||
public int zmax = Integer.MIN_VALUE;
|
||||
public String world;
|
||||
public boolean groupByChunk;
|
||||
public boolean groupByBlockID;
|
||||
public boolean groupByBlockIDData;
|
||||
public boolean groupByTexture;
|
||||
}
|
||||
|
||||
private String getContextID(DynmapCommandSender sender) {
|
||||
String id = "<console>";
|
||||
if (sender instanceof DynmapPlayer) {
|
||||
id = ((DynmapPlayer)sender).getName();
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
private ExportContext getContext(DynmapCommandSender sender) {
|
||||
String id = getContextID(sender);
|
||||
|
||||
ExportContext ctx = sessions.get(id);
|
||||
if (ctx == null) {
|
||||
ctx = new ExportContext();
|
||||
sessions.put(id, ctx);
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
public boolean processCommand(DynmapCommandSender sender, String cmd, String commandLabel, String[] args, DynmapCore core) {
|
||||
/* Re-parse args - handle doublequotes */
|
||||
args = DynmapCore.parseArgs(args, sender);
|
||||
if(args.length < 1)
|
||||
return false;
|
||||
if(!core.checkPlayerPermission(sender, "dynmapexp.export")) {
|
||||
return true;
|
||||
}
|
||||
cmd = args[0];
|
||||
boolean rslt = false;
|
||||
ExportContext ctx = getContext(sender);
|
||||
|
||||
if(cmd.equalsIgnoreCase("set")) {
|
||||
rslt = handleSetExport(sender, args, ctx, core);
|
||||
}
|
||||
else if (cmd.equalsIgnoreCase("radius")) {
|
||||
rslt = handleRadius(sender, args, ctx, core);
|
||||
}
|
||||
else if (cmd.equalsIgnoreCase("pos0")) {
|
||||
rslt = handlePosN(sender, args, ctx, core, 0);
|
||||
}
|
||||
else if (cmd.equalsIgnoreCase("pos1")) {
|
||||
rslt = handlePosN(sender, args, ctx, core, 1);
|
||||
}
|
||||
else if (cmd.equalsIgnoreCase("export")) {
|
||||
rslt = handleDoExport(sender, args, ctx, core);
|
||||
}
|
||||
else if(cmd.equalsIgnoreCase("reset")) {
|
||||
rslt = handleResetExport(sender, args, ctx, core);
|
||||
}
|
||||
else if(cmd.equalsIgnoreCase("purge")) {
|
||||
rslt = handlePurgeExport(sender, args, ctx, core);
|
||||
}
|
||||
else if(cmd.equalsIgnoreCase("info")) {
|
||||
rslt = handleInfo(sender, args, ctx, core);
|
||||
}
|
||||
|
||||
return rslt;
|
||||
}
|
||||
|
||||
public List<String> getTabCompletions(DynmapCommandSender sender, String[] args, DynmapCore core) {
|
||||
/* Re-parse args - handle doublequotes */
|
||||
args = DynmapCore.parseArgs(args, sender, true);
|
||||
|
||||
if (args == null || args.length <= 1) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
String cmd = args[0];
|
||||
|
||||
if(cmd.equalsIgnoreCase("set")) {
|
||||
List<String> keys = new ArrayList<>(
|
||||
Arrays.asList("x0", "x1", "y0", "y1", "z0", "z1", "world", "shader", "byChunk",
|
||||
"byBlockID", "byBlockIDData", "byTexture"));
|
||||
|
||||
if (args.length % 2 == 0) { // Args contain only complete key value argument pairs (plus subcommand)
|
||||
// Remove previous used keys
|
||||
for (int i = 1; i < args.length; i += 2) {
|
||||
keys.remove(args[i]);
|
||||
}
|
||||
|
||||
return keys;
|
||||
} else { // Incomplete key value argument pair, suggest values
|
||||
final String lastKey = args[args.length - 2];
|
||||
final String lastValue = args[args.length - 1];
|
||||
|
||||
switch(lastKey) {
|
||||
case "world":
|
||||
return core.getWorldSuggestions(lastValue);
|
||||
case "shader":
|
||||
return MapManager.mapman.hdmapman.shaders.keySet().stream()
|
||||
.filter(value -> value.startsWith(lastValue))
|
||||
.collect(Collectors.toList());
|
||||
case "byChunk":
|
||||
case "byBlockID":
|
||||
case "byBlockIDData":
|
||||
case "byTexture":
|
||||
return Stream.of("true", "false")
|
||||
.filter(value -> value.startsWith(lastValue))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private boolean handleInfo(DynmapCommandSender sender, String[] args, ExportContext ctx, DynmapCore core) {
|
||||
sender.sendMessage(String.format("Bounds: <%s,%s,%s> - <%s,%s,%s> on world '%s'", val(ctx.xmin), val(ctx.ymin), val(ctx.zmin),
|
||||
val(ctx.xmax), val(ctx.ymax), val(ctx.zmax), ctx.world));
|
||||
sender.sendMessage(String.format("groups: byChunk: %b, byBlockID: %b, byBlockIDData: %b, byTexture: %b", ctx.groupByChunk, ctx.groupByBlockID, ctx.groupByBlockIDData, ctx.groupByTexture));
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleSetExport(DynmapCommandSender sender, String[] args, ExportContext ctx, DynmapCore core) {
|
||||
if (args.length < 3) {
|
||||
sender.sendMessage(String.format("Bounds: <%s,%s,%s> - <%s,%s,%s> on world '%s'", val(ctx.xmin), val(ctx.ymin), val(ctx.zmin),
|
||||
val(ctx.xmax), val(ctx.ymax), val(ctx.zmax), ctx.world));
|
||||
return true;
|
||||
}
|
||||
for (int i = 1; i < (args.length-1); i += 2) {
|
||||
try {
|
||||
if (args[i].equals("x0")) {
|
||||
ctx.xmin = Integer.parseInt(args[i+1]);
|
||||
}
|
||||
else if (args[i].equals("x1")) {
|
||||
ctx.xmax = Integer.parseInt(args[i+1]);
|
||||
}
|
||||
else if (args[i].equals("y0")) {
|
||||
ctx.ymin = Integer.parseInt(args[i+1]);
|
||||
}
|
||||
else if (args[i].equals("y1")) {
|
||||
ctx.ymax = Integer.parseInt(args[i+1]);
|
||||
}
|
||||
else if (args[i].equals("z0")) {
|
||||
ctx.zmin = Integer.parseInt(args[i+1]);
|
||||
}
|
||||
else if (args[i].equals("z1")) {
|
||||
ctx.zmax = Integer.parseInt(args[i+1]);
|
||||
}
|
||||
else if (args[i].equals("world")) {
|
||||
DynmapWorld w = core.getWorld(args[i+1]);
|
||||
if (w != null) {
|
||||
ctx.world = args[i+1];
|
||||
}
|
||||
else {
|
||||
sender.sendMessage("Invalid world '" + args[i+1] + "'");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (args[i].equals("shader")) {
|
||||
HDShader s = MapManager.mapman.hdmapman.shaders.get(args[i+1]);
|
||||
if (s == null) {
|
||||
sender.sendMessage("Unknown shader '" + args[i+1] + "'");
|
||||
return true;
|
||||
}
|
||||
ctx.shader = args[i+1];
|
||||
}
|
||||
else if (args[i].equals("byChunk")) {
|
||||
ctx.groupByChunk = args[i+1].equalsIgnoreCase("true");
|
||||
}
|
||||
else if (args[i].equals("byBlockID")) {
|
||||
ctx.groupByBlockID = args[i+1].equalsIgnoreCase("true");
|
||||
}
|
||||
else if (args[i].equals("byBlockIDData")) {
|
||||
ctx.groupByBlockIDData = args[i+1].equalsIgnoreCase("true");
|
||||
}
|
||||
else if (args[i].equals("byTexture")) {
|
||||
ctx.groupByTexture = args[i+1].equalsIgnoreCase("true");
|
||||
}
|
||||
else { // Unknown setting
|
||||
sender.sendMessage("Unknown setting '" + args[i] + "'");
|
||||
return true;
|
||||
}
|
||||
} catch (NumberFormatException nfx) {
|
||||
sender.sendMessage("Invalid value for '" + args[i] + "' - " + args[i+1]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return handleInfo(sender, args, ctx, core);
|
||||
}
|
||||
|
||||
private boolean handleRadius(DynmapCommandSender sender, String[] args, ExportContext ctx, DynmapCore core) {
|
||||
if ((sender instanceof DynmapPlayer) == false) { // Not a player
|
||||
sender.sendMessage("Only usable by player");
|
||||
return true;
|
||||
}
|
||||
DynmapPlayer plyr = (DynmapPlayer) sender;
|
||||
DynmapLocation loc = plyr.getLocation();
|
||||
DynmapWorld world = null;
|
||||
if (loc != null) {
|
||||
world = core.getWorld(loc.world);
|
||||
}
|
||||
if (world == null) {
|
||||
sender.sendMessage("Location not found for player");
|
||||
return true;
|
||||
}
|
||||
int radius = 16;
|
||||
if (args.length >= 2) {
|
||||
try {
|
||||
radius = Integer.parseInt(args[1]);
|
||||
if (radius < 0) {
|
||||
sender.sendMessage("Invalid radius - " + args[1]);
|
||||
return true;
|
||||
}
|
||||
} catch (NumberFormatException nfx) {
|
||||
sender.sendMessage("Invalid radius - " + args[1]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
ctx.xmin = (int)Math.floor(loc.x) - radius;
|
||||
ctx.xmax = (int)Math.ceil(loc.x) + radius;
|
||||
ctx.zmin = (int)Math.floor(loc.z) - radius;
|
||||
ctx.zmax = (int)Math.ceil(loc.z) + radius;
|
||||
ctx.ymin = world.minY;
|
||||
ctx.ymax = world.worldheight - 1;
|
||||
ctx.world = world.getName();
|
||||
return handleInfo(sender, args, ctx, core);
|
||||
}
|
||||
|
||||
private boolean handlePosN(DynmapCommandSender sender, String[] args, ExportContext ctx, DynmapCore core, int n) {
|
||||
if ((sender instanceof DynmapPlayer) == false) { // Not a player
|
||||
sender.sendMessage("Only usable by player");
|
||||
return true;
|
||||
}
|
||||
DynmapPlayer plyr = (DynmapPlayer) sender;
|
||||
DynmapLocation loc = plyr.getLocation();
|
||||
DynmapWorld world = null;
|
||||
if (loc != null) {
|
||||
world = core.getWorld(loc.world);
|
||||
}
|
||||
if (world == null) {
|
||||
sender.sendMessage("Location not found for player");
|
||||
return true;
|
||||
}
|
||||
if (n == 0) {
|
||||
ctx.xmin = (int)Math.floor(loc.x);
|
||||
ctx.ymin = (int)Math.floor(loc.y);
|
||||
ctx.zmin = (int)Math.floor(loc.z);
|
||||
}
|
||||
else {
|
||||
ctx.xmax = (int)Math.floor(loc.x);
|
||||
ctx.ymax = (int)Math.floor(loc.y);
|
||||
ctx.zmax = (int)Math.floor(loc.z);
|
||||
}
|
||||
ctx.world = world.getName();
|
||||
|
||||
return handleInfo(sender, args, ctx, core);
|
||||
}
|
||||
|
||||
private boolean handleDoExport(DynmapCommandSender sender, String[] args, ExportContext ctx, DynmapCore core) {
|
||||
if ((ctx.world == null) || (ctx.xmin == Integer.MIN_VALUE) || (ctx.ymin == Integer.MIN_VALUE) ||
|
||||
(ctx.zmin == Integer.MIN_VALUE) || (ctx.xmax == Integer.MIN_VALUE) || (ctx.ymax == Integer.MIN_VALUE) ||
|
||||
(ctx.zmax == Integer.MIN_VALUE)) {
|
||||
sender.sendMessage("Bounds not set");
|
||||
return true;
|
||||
}
|
||||
DynmapWorld w = core.getWorld(ctx.world);
|
||||
if (w == null) {
|
||||
sender.sendMessage("Invalid world - " + ctx.world);
|
||||
return true;
|
||||
}
|
||||
HDShader s = MapManager.mapman.hdmapman.shaders.get(ctx.shader);
|
||||
if (s == null) {
|
||||
sender.sendMessage("Invalid shader - " + ctx.shader);
|
||||
return true;
|
||||
}
|
||||
|
||||
String basename = "dynmapexp";
|
||||
if (args.length > 1) {
|
||||
basename = args[1];
|
||||
}
|
||||
basename = basename.replace('/', '_');
|
||||
basename = basename.replace('\\', '_');
|
||||
File f = new File(core.getExportFolder(), basename + ".zip");
|
||||
int idx = 0;
|
||||
String finalBasename = basename;
|
||||
while (f.exists()) {
|
||||
idx++;
|
||||
finalBasename = basename + "_" + idx;
|
||||
f = new File(core.getExportFolder(), finalBasename + ".zip");
|
||||
}
|
||||
sender.sendMessage("Exporting to " + f.getPath());
|
||||
|
||||
OBJExport exp = new OBJExport(f, s, w, core, finalBasename);
|
||||
exp.setRenderBounds(ctx.xmin, ctx.ymin, ctx.zmin, ctx.xmax, ctx.ymax, ctx.zmax);
|
||||
exp.setGroupEnabled(OBJExport.GROUP_CHUNK, ctx.groupByChunk);
|
||||
exp.setGroupEnabled(OBJExport.GROUP_TEXTURE, ctx.groupByTexture);
|
||||
exp.setGroupEnabled(OBJExport.GROUP_BLOCKID, ctx.groupByBlockID);
|
||||
exp.setGroupEnabled(OBJExport.GROUP_BLOCKIDMETA, ctx.groupByBlockIDData);
|
||||
MapManager.mapman.startOBJExport(exp, sender);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleResetExport(DynmapCommandSender sender, String[] args, ExportContext ctx, DynmapCore core) {
|
||||
sessions.remove(getContextID(sender));
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handlePurgeExport(DynmapCommandSender sender, String[] args, ExportContext ctx, DynmapCore core) {
|
||||
if (args.length > 1) {
|
||||
String basename = args[1];
|
||||
basename = basename.replace('/', '_');
|
||||
basename = basename.replace('\\', '_');
|
||||
File f = new File(core.getExportFolder(), basename + ".zip");
|
||||
if (f.exists()) {
|
||||
f.delete();
|
||||
sender.sendMessage("Removed " + f.getPath());
|
||||
}
|
||||
else {
|
||||
sender.sendMessage(f.getPath() + " not found");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private String val(int v) {
|
||||
if (v == Integer.MIN_VALUE)
|
||||
return "N/A";
|
||||
else
|
||||
return Integer.toString(v);
|
||||
}
|
||||
}
|
||||
656
DynmapCore/src/main/java/org/dynmap/exporter/OBJExport.java
Normal file
656
DynmapCore/src/main/java/org/dynmap/exporter/OBJExport.java
Normal file
|
|
@ -0,0 +1,656 @@
|
|||
package org.dynmap.exporter;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import org.dynmap.DynmapChunk;
|
||||
import org.dynmap.DynmapCore;
|
||||
import org.dynmap.DynmapWorld;
|
||||
import org.dynmap.common.DynmapCommandSender;
|
||||
import org.dynmap.hdmap.CustomBlockModel;
|
||||
import org.dynmap.hdmap.HDBlockModels;
|
||||
import org.dynmap.hdmap.HDBlockStateTextureMap;
|
||||
import org.dynmap.hdmap.HDScaledBlockModels;
|
||||
import org.dynmap.hdmap.HDShader;
|
||||
import org.dynmap.hdmap.TexturePack.BlockTransparency;
|
||||
import org.dynmap.renderer.CustomRenderer;
|
||||
import org.dynmap.renderer.DynmapBlockState;
|
||||
import org.dynmap.renderer.RenderPatch;
|
||||
import org.dynmap.renderer.RenderPatchFactory.SideVisible;
|
||||
import org.dynmap.utils.BlockStep;
|
||||
import org.dynmap.utils.IndexedVector3D;
|
||||
import org.dynmap.utils.IndexedVector3DList;
|
||||
import org.dynmap.utils.MapChunkCache;
|
||||
import org.dynmap.utils.MapIterator;
|
||||
import org.dynmap.utils.PatchDefinition;
|
||||
import org.dynmap.utils.PatchDefinitionFactory;
|
||||
|
||||
public class OBJExport {
|
||||
private final File destZipFile; // Destination ZIP file
|
||||
private final HDShader shader; // Shader to be used for textures
|
||||
private final DynmapWorld world; // World to be rendered
|
||||
private final DynmapCore core;
|
||||
private final String basename;
|
||||
private int minX, minY, minZ; // Minimum world coordinates to be rendered
|
||||
private int maxX, maxY, maxZ; // Maximum world coordinates to be rendered
|
||||
private static Charset UTF8 = Charset.forName("UTF-8");
|
||||
private ZipOutputStream zos; // Output stream ZIP for result
|
||||
private double originX, originY, originZ; // Origin for exported model
|
||||
private double scale = 1.0; // Scale for exported model
|
||||
private boolean centerOrigin = true; // Center at origin
|
||||
private PatchDefinition[] defaultPathces; // Default patches for solid block, indexed by BlockStep.ordinal()
|
||||
private HashSet<String> matIDs = new HashSet<String>(); // Set of defined material ids for RP
|
||||
|
||||
private static class Face {
|
||||
String groupLine;
|
||||
String faceLine;
|
||||
}
|
||||
|
||||
private HashMap<String, List<Face>> facesByTexture = new HashMap<String, List<Face>>();
|
||||
private static final int MODELSCALE = 16;
|
||||
private static final double BLKSIZE = 1.0 / (double) MODELSCALE;
|
||||
|
||||
// Index of group settings
|
||||
public static final int GROUP_CHUNK = 0;
|
||||
public static final int GROUP_TEXTURE = 1;
|
||||
public static final int GROUP_BLOCKID = 2;
|
||||
public static final int GROUP_BLOCKIDMETA = 3;
|
||||
public static final int GROUP_COUNT = 4;
|
||||
private String[] group = new String[GROUP_COUNT];
|
||||
private boolean[] enabledGroups = new boolean[GROUP_COUNT];
|
||||
private String groupline = null;
|
||||
|
||||
// Vertex set
|
||||
private IndexedVector3DList vertices;
|
||||
// UV set
|
||||
private IndexedVector3DList uvs;
|
||||
// Scaled models
|
||||
private HDScaledBlockModels models;
|
||||
|
||||
public static final int ROT0 = 0;
|
||||
public static final int ROT90 = 1;
|
||||
public static final int ROT180 = 2;
|
||||
public static final int ROT270 = 3;
|
||||
public static final int HFLIP = 4;
|
||||
|
||||
private static final double[][] pp = {
|
||||
{ 0, 0, 0, 1, 0, 0, 0, 0, 1 },
|
||||
{ 0, 1, 1, 1, 1, 1, 0, 1, 0 },
|
||||
{ 1, 0, 0, 0, 0, 0, 1, 1, 0 },
|
||||
{ 0, 0, 1, 1, 0, 1, 0, 1, 1 },
|
||||
{ 0, 0, 0, 0, 0, 1, 0, 1, 0 },
|
||||
{ 1, 0, 1, 1, 0, 0, 1, 1, 1 }
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor for OBJ file export
|
||||
* @param dest - destination file (ZIP)
|
||||
* @param shader - shader to be used for coloring/texturing
|
||||
* @param world - world to be rendered
|
||||
* @param core - core object
|
||||
* @param basename - base file name
|
||||
*/
|
||||
public OBJExport(File dest, HDShader shader, DynmapWorld world, DynmapCore core, String basename) {
|
||||
destZipFile = dest;
|
||||
this.shader = shader;
|
||||
this.world = world;
|
||||
this.core = core;
|
||||
this.basename = basename;
|
||||
this.defaultPathces = new PatchDefinition[6];
|
||||
PatchDefinitionFactory fact = HDBlockModels.getPatchDefinitionFactory();
|
||||
for (BlockStep s : BlockStep.values()) {
|
||||
double[] p = pp[s.getFaceEntered()];
|
||||
int ord = s.ordinal();
|
||||
defaultPathces[ord] = fact.getPatch(p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8], 0, 1, 0, 0, 1, 1, SideVisible.TOP, ord);
|
||||
}
|
||||
vertices = new IndexedVector3DList(new IndexedVector3DList.ListCallback() {
|
||||
@Override
|
||||
public void elementAdded(IndexedVector3DList list, IndexedVector3D newElement) {
|
||||
try {
|
||||
/* Minecraft XYZ maps to OBJ YZX */
|
||||
addStringToExportedFile(String.format(Locale.US, "v %.4f %.4f %.4f\n",
|
||||
(newElement.x - originX) * scale,
|
||||
(newElement.y - originY) * scale,
|
||||
(newElement.z - originZ) * scale
|
||||
));
|
||||
} catch (IOException iox) {
|
||||
}
|
||||
}
|
||||
});
|
||||
uvs = new IndexedVector3DList(new IndexedVector3DList.ListCallback() {
|
||||
@Override
|
||||
public void elementAdded(IndexedVector3DList list, IndexedVector3D newElement) {
|
||||
try {
|
||||
addStringToExportedFile(String.format(Locale.US, "vt %.4f %.4f\n", newElement.x, newElement.y));
|
||||
} catch (IOException iox) {
|
||||
}
|
||||
}
|
||||
});
|
||||
// Get models
|
||||
models = HDBlockModels.getModelsForScale(MODELSCALE);
|
||||
}
|
||||
/**
|
||||
* Set render bounds
|
||||
*
|
||||
* @param minx - minimum X coord
|
||||
* @param miny - minimum Y coord
|
||||
* @param minz - minimum Z coord
|
||||
* @param maxx - maximum X coord
|
||||
* @param maxy - maximum Y coord
|
||||
* @param maxz - maximum Z coord
|
||||
*/
|
||||
public void setRenderBounds(int minx, int miny, int minz, int maxx, int maxy, int maxz) {
|
||||
if (minx < maxx) {
|
||||
minX = minx; maxX = maxx;
|
||||
}
|
||||
else {
|
||||
minX = maxx; maxX = minx;
|
||||
}
|
||||
if (miny < maxy) {
|
||||
minY = miny; maxY = maxy;
|
||||
}
|
||||
else {
|
||||
minY = maxy; maxY = miny;
|
||||
}
|
||||
if (minz < maxz) {
|
||||
minZ = minz; maxZ = maxz;
|
||||
}
|
||||
else {
|
||||
minZ = maxz; maxZ = minz;
|
||||
}
|
||||
if (minY < world.minY) minY = world.minY;
|
||||
if (maxY >= world.worldheight) maxY = world.worldheight - 1;
|
||||
if (centerOrigin) {
|
||||
originX = (maxX + minX) / 2.0;
|
||||
originY = minY;
|
||||
originZ = (maxZ + minZ) / 2.0;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Set origin for exported model
|
||||
* @param ox - origin x
|
||||
* @param oy - origin y
|
||||
* @param oz - origin z
|
||||
*/
|
||||
public void setOrigin(double ox, double oy, double oz) {
|
||||
originX = ox;
|
||||
originY = oy;
|
||||
originZ = oz;
|
||||
centerOrigin = false;
|
||||
}
|
||||
/**
|
||||
* Set scale for exported model
|
||||
* @param scale = scale
|
||||
*/
|
||||
public void setScale(double scale) {
|
||||
this.scale = scale;
|
||||
}
|
||||
/**
|
||||
* Process export
|
||||
*
|
||||
* @param sender - command sender: use for feedback messages
|
||||
* @return true if successful, false if not
|
||||
*/
|
||||
public boolean processExport(DynmapCommandSender sender) {
|
||||
boolean good = false;
|
||||
try {
|
||||
// Open ZIP file destination
|
||||
zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(destZipFile)));
|
||||
|
||||
List<DynmapChunk> requiredChunks = new ArrayList<DynmapChunk>();
|
||||
int mincx = (minX >> 4);
|
||||
int maxcx = (maxX + 15) >> 4;
|
||||
int mincz = (minZ >> 4);
|
||||
int maxcz = (maxZ + 15) >> 4;
|
||||
boolean[] edgebits = new boolean[6];
|
||||
|
||||
startExportedFile(basename + ".obj");
|
||||
// Add material library
|
||||
addStringToExportedFile("mtllib " + basename + ".mtl\n");
|
||||
|
||||
// Loop through - do 8x8 chunks at a time (plus 1 border each way)
|
||||
for (int cx = mincx; cx <= maxcx; cx += 4) {
|
||||
for (int cz = mincz; cz <= maxcz; cz += 4) {
|
||||
// Build chunk cache for block of chunks
|
||||
requiredChunks.clear();
|
||||
for (int i = -1; i < 5; i++) {
|
||||
for (int j = -1; j < 5; j++) {
|
||||
if (((cx+i) <= maxcx) && ((cz+j) <= maxcz) && ((cx+i) >= mincx) && ((cz+j) >= mincz)) {
|
||||
requiredChunks.add(new DynmapChunk(cx + i, cz + j));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Get the chunk buffer
|
||||
MapChunkCache cache = core.getServer().createMapChunkCache(world, requiredChunks, true, false, true, false);
|
||||
if (cache == null) {
|
||||
throw new IOException("Error loading chunk cache");
|
||||
}
|
||||
MapIterator iter = cache.getIterator(minX, minY, minZ);
|
||||
for (int x = cx * 16; (x < (cx * 16 + 64)) && (x <= maxX); x++) {
|
||||
if (x < minX) x = minX;
|
||||
edgebits[BlockStep.X_PLUS.ordinal()] = (x == minX);
|
||||
edgebits[BlockStep.X_MINUS.ordinal()] = (x == maxX);
|
||||
for (int z = cz * 16; (z < (cz * 16 + 64)) && (z <= maxZ); z++) {
|
||||
if (z < minZ) z = minZ;
|
||||
edgebits[BlockStep.Z_PLUS.ordinal()] = (z == minZ);
|
||||
edgebits[BlockStep.Z_MINUS.ordinal()] = (z == maxZ);
|
||||
iter.initialize(x, minY, z);
|
||||
updateGroup(GROUP_CHUNK, "chunk" + (x >> 4) + "_" + (z >> 4));
|
||||
// Do first (bottom)
|
||||
edgebits[BlockStep.Y_MINUS.ordinal()] = true;
|
||||
edgebits[BlockStep.Y_PLUS.ordinal()] = false;
|
||||
DynmapBlockState blk = iter.getBlockType();
|
||||
if (blk.isNotAir()) { // Not air
|
||||
handleBlock(blk, iter, edgebits);
|
||||
}
|
||||
// Do middle
|
||||
edgebits[BlockStep.Y_MINUS.ordinal()] = false;
|
||||
for (int y = minY + 1; y < maxY; y++) {
|
||||
iter.setY(y);
|
||||
blk = iter.getBlockType();
|
||||
if (blk.isNotAir()) { // Not air
|
||||
handleBlock(blk, iter, edgebits);
|
||||
}
|
||||
}
|
||||
// Do top
|
||||
edgebits[BlockStep.Y_PLUS.ordinal()] = true;
|
||||
iter.setY(maxY);
|
||||
blk = iter.getBlockType();
|
||||
if (blk.isNotAir()) { // Not air
|
||||
handleBlock(blk, iter, edgebits);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Output faces by texture
|
||||
String grp = "";
|
||||
for (String material : facesByTexture.keySet()) {
|
||||
List<Face> faces = facesByTexture.get(material);
|
||||
matIDs.add(material); // Record material use
|
||||
addStringToExportedFile(String.format("usemtl %s\n", material));
|
||||
for (Face face : faces) {
|
||||
if ((face.groupLine != null) && (!face.groupLine.equals(grp))) {
|
||||
grp = face.groupLine;
|
||||
addStringToExportedFile(grp);
|
||||
}
|
||||
addStringToExportedFile(face.faceLine);
|
||||
}
|
||||
}
|
||||
// Clear face table
|
||||
facesByTexture.clear();
|
||||
// Clean up vertices we've moved past
|
||||
vertices.resetSet(minX, minY, minZ, cx * 16 + 64, maxY, cz * 16 + 64);
|
||||
}
|
||||
}
|
||||
finishExportedFile();
|
||||
// If shader provided, add shader content to ZIP
|
||||
if (shader != null) {
|
||||
sender.sendMessage("Adding textures from shader " + shader.getName());
|
||||
shader.exportAsMaterialLibrary(sender, this);
|
||||
sender.sendMessage("Texture export completed");
|
||||
}
|
||||
// And close the ZIP
|
||||
zos.finish();
|
||||
zos.close();
|
||||
zos = null;
|
||||
good = true;
|
||||
sender.sendMessage("Export completed - " + destZipFile.getPath());
|
||||
} catch (IOException iox) {
|
||||
sender.sendMessage("Export failed: " + iox.getMessage());
|
||||
} finally {
|
||||
if (zos != null) {
|
||||
try { zos.close(); } catch (IOException e) {}
|
||||
zos = null;
|
||||
destZipFile.delete();
|
||||
}
|
||||
}
|
||||
return good;
|
||||
}
|
||||
/**
|
||||
* Start adding file to export
|
||||
* @param fname - path/name of file in destination zip
|
||||
* @throws IOException if error starting file
|
||||
*/
|
||||
public void startExportedFile(String fname) throws IOException {
|
||||
ZipEntry ze = new ZipEntry(fname);
|
||||
zos.putNextEntry(ze);
|
||||
}
|
||||
/**
|
||||
* Add bytes to current exported file
|
||||
* @param buf - buffer with bytes
|
||||
* @param off - offset of start
|
||||
* @param len - length to be added
|
||||
* @throws IOException if error adding to file
|
||||
*/
|
||||
public void addBytesToExportedFile(byte[] buf, int off, int len) throws IOException {
|
||||
zos.write(buf, off, len);
|
||||
}
|
||||
/**
|
||||
* Add string to curent exported file (UTF-8)
|
||||
* @param str - string to be written
|
||||
* @throws IOException if error adding to file
|
||||
*/
|
||||
public void addStringToExportedFile(String str) throws IOException {
|
||||
byte[] b = str.getBytes(UTF8);
|
||||
zos.write(b, 0, b.length);
|
||||
}
|
||||
/**
|
||||
* Finish adding file to export
|
||||
* @throws IOException if error completing file
|
||||
*/
|
||||
public void finishExportedFile() throws IOException {
|
||||
zos.closeEntry();
|
||||
}
|
||||
/**
|
||||
* Handle block at current iterator coord
|
||||
* @param id - block ID
|
||||
* @param iter - iterator
|
||||
* @param edgebits - bit N corresponds to side N being an endge (forge render)
|
||||
*/
|
||||
private void handleBlock(DynmapBlockState blk, MapIterator map, boolean[] edgebits) throws IOException {
|
||||
BlockStep[] steps = BlockStep.values();
|
||||
int[] txtidx = null;
|
||||
// See if the block has a patch model
|
||||
RenderPatch[] patches = models.getPatchModel(blk);
|
||||
/* If no patches, see if custom model */
|
||||
if(patches == null) {
|
||||
CustomBlockModel cbm = models.getCustomBlockModel(blk);
|
||||
if (cbm != null) { /* If so, get our meshes */
|
||||
patches = cbm.getMeshForBlock(map);
|
||||
}
|
||||
}
|
||||
if (patches != null) {
|
||||
steps = new BlockStep[patches.length];
|
||||
txtidx = new int[patches.length];
|
||||
for (int i = 0; i < txtidx.length; i++) {
|
||||
txtidx[i] = ((PatchDefinition) patches[i]).getTextureIndex();
|
||||
steps[i] = ((PatchDefinition) patches[i]).step;
|
||||
}
|
||||
}
|
||||
else { // See if volumetric
|
||||
short[] smod = models.getScaledModel(blk);
|
||||
if (smod != null) {
|
||||
patches = getScaledModelAsPatches(smod);
|
||||
steps = new BlockStep[patches.length];
|
||||
txtidx = new int[patches.length];
|
||||
for (int i = 0; i < patches.length; i++) {
|
||||
PatchDefinition pd = (PatchDefinition) patches[i];
|
||||
steps[i] = pd.step;
|
||||
txtidx[i] = pd.getTextureIndex();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Set block ID and ID+meta groups
|
||||
updateGroup(GROUP_BLOCKID, "blk" + blk.baseState.globalStateIndex);
|
||||
updateGroup(GROUP_BLOCKIDMETA, "blk" + blk.globalStateIndex);
|
||||
|
||||
// Get materials for patches
|
||||
String[] mats = shader.getCurrentBlockMaterials(blk, map, txtidx, steps);
|
||||
|
||||
if (patches != null) { // Patch based model?
|
||||
for (int i = 0; i < patches.length; i++) {
|
||||
addPatch((PatchDefinition) patches[i], map.getX(), map.getY(), map.getZ(), mats[i]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
boolean opaque = HDBlockStateTextureMap.getTransparency(blk) == BlockTransparency.OPAQUE;
|
||||
for (int face = 0; face < 6; face++) {
|
||||
DynmapBlockState blk2 = map.getBlockTypeAt(BlockStep.oppositeValues[face]); // Get block in direction
|
||||
// If we're not solid, or adjacent block is not solid, draw side
|
||||
if ((!opaque) || blk2.isAir() || edgebits[face] || (HDBlockStateTextureMap.getTransparency(blk2) != BlockTransparency.OPAQUE)) {
|
||||
addPatch(defaultPathces[face], map.getX(), map.getY(), map.getZ(), mats[face]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private int[] getTextureUVs(PatchDefinition pd, int rot) {
|
||||
int[] uv = new int[4];
|
||||
if (rot == ROT0) {
|
||||
uv[0] = uvs.getVectorIndex(pd.umin, pd.vmin, 0);
|
||||
uv[1] = uvs.getVectorIndex(pd.umax, pd.vmin, 0);
|
||||
uv[2] = uvs.getVectorIndex(pd.umax, pd.vmax, 0);
|
||||
uv[3] = uvs.getVectorIndex(pd.umin, pd.vmax, 0);
|
||||
}
|
||||
else if (rot == ROT90) { // 90 degrees on texture
|
||||
uv[0] = uvs.getVectorIndex(1.0 - pd.vmin, pd.umin, 0);
|
||||
uv[1] = uvs.getVectorIndex(1.0 - pd.vmin, pd.umax, 0);
|
||||
uv[2] = uvs.getVectorIndex(1.0 - pd.vmax, pd.umax, 0);
|
||||
uv[3] = uvs.getVectorIndex(1.0 - pd.vmax, pd.umin, 0);
|
||||
}
|
||||
else if (rot == ROT180) { // 180 degrees on texture
|
||||
uv[0] = uvs.getVectorIndex(1.0 - pd.umin, 1.0 - pd.vmin, 0);
|
||||
uv[1] = uvs.getVectorIndex(1.0 - pd.umax, 1.0 - pd.vmin, 0);
|
||||
uv[2] = uvs.getVectorIndex(1.0 - pd.umax, 1.0 - pd.vmax, 0);
|
||||
uv[3] = uvs.getVectorIndex(1.0 - pd.umin, 1.0 - pd.vmax, 0);
|
||||
}
|
||||
else if (rot == ROT270) { // 270 degrees on texture
|
||||
uv[0] = uvs.getVectorIndex(pd.vmin, 1.0 - pd.umin, 0);
|
||||
uv[1] = uvs.getVectorIndex(pd.vmin, 1.0 - pd.umax, 0);
|
||||
uv[2] = uvs.getVectorIndex(pd.vmax, 1.0 - pd.umax, 0);
|
||||
uv[3] = uvs.getVectorIndex(pd.vmax, 1.0 - pd.umin, 0);
|
||||
}
|
||||
else if (rot == HFLIP) {
|
||||
uv[0] = uvs.getVectorIndex(1.0 - pd.umin, pd.vmin, 0);
|
||||
uv[1] = uvs.getVectorIndex(1.0 - pd.umax, pd.vmin, 0);
|
||||
uv[2] = uvs.getVectorIndex(1.0 - pd.umax, pd.vmax, 0);
|
||||
uv[3] = uvs.getVectorIndex(1.0 - pd.umin, pd.vmax, 0);
|
||||
}
|
||||
else {
|
||||
uv[0] = uvs.getVectorIndex(pd.umin, pd.vmin, 0);
|
||||
uv[1] = uvs.getVectorIndex(pd.umax, pd.vmin, 0);
|
||||
uv[2] = uvs.getVectorIndex(pd.umax, pd.vmax, 0);
|
||||
uv[3] = uvs.getVectorIndex(pd.umin, pd.vmax, 0);
|
||||
}
|
||||
return uv;
|
||||
}
|
||||
/**
|
||||
* Add patch as face to output
|
||||
*/
|
||||
private void addPatch(PatchDefinition pd, double x, double y, double z, String material) throws IOException {
|
||||
// No material? No face
|
||||
if (material == null) {
|
||||
return;
|
||||
}
|
||||
int rot = 0;
|
||||
int rotidx = material.indexOf('@'); // Check for rotation modifier
|
||||
if (rotidx >= 0) {
|
||||
rot = material.charAt(rotidx+1) - '0'; // 0-3
|
||||
material = material.substring(0, rotidx);
|
||||
}
|
||||
int[] v = new int[4];
|
||||
int[] uv = getTextureUVs(pd, rot);
|
||||
// Get offsets for U and V from origin
|
||||
double ux = pd.xu - pd.x0;
|
||||
double uy = pd.yu - pd.y0;
|
||||
double uz = pd.zu - pd.z0;
|
||||
double vx = pd.xv - pd.x0;
|
||||
double vy = pd.yv - pd.y0;
|
||||
double vz = pd.zv - pd.z0;
|
||||
// Offset to origin corner
|
||||
x = x + pd.x0;
|
||||
y = y + pd.y0;
|
||||
z = z + pd.z0;
|
||||
// Origin corner, offset by umin, vmin
|
||||
v[0] = vertices.getVectorIndex(x + ux*pd.umin + vx*pd.vmin, y + uy*pd.umin + vy*pd.vmin, z + uz*pd.umin + vz*pd.vmin);
|
||||
uv[0] = uvs.getVectorIndex(pd.umin, pd.vmin, 0);
|
||||
// Second is end of U (umax, vmin)
|
||||
v[1] = vertices.getVectorIndex(x + ux*pd.umax + vx*pd.vmin, y + uy*pd.umax + vy*pd.vmin, z + uz*pd.umax + vz*pd.vmin);
|
||||
uv[1] = uvs.getVectorIndex(pd.umax, pd.vmin, 0);
|
||||
// Third is end of U+V (umax, vmax)
|
||||
v[2] = vertices.getVectorIndex(x + ux*pd.umax + vx*pd.vmax, y + uy*pd.umax + vy*pd.vmax, z + uz*pd.umax + vz*pd.vmax);
|
||||
uv[2] = uvs.getVectorIndex(pd.umax, pd.vmax, 0);
|
||||
// Forth is end of V (umin, vmax)
|
||||
v[3] = vertices.getVectorIndex(x + ux*pd.umin + vx*pd.vmax, y + uy*pd.umin + vy*pd.vmax, z + uz*pd.umin + vz*pd.vmax);
|
||||
uv[3] = uvs.getVectorIndex(pd.umin, pd.vmax, 0);
|
||||
// Add patch to file
|
||||
addPatchToFile(v, uv, pd.sidevis, material, rot);
|
||||
}
|
||||
private void addPatchToFile(int[] v, int[] uv, SideVisible sv, String material, int rot) throws IOException {
|
||||
List<Face> faces = facesByTexture.get(material);
|
||||
if (faces == null) {
|
||||
faces = new ArrayList<Face>();
|
||||
facesByTexture.put(material, faces);
|
||||
}
|
||||
// If needed, rotate the UV sequence
|
||||
if (rot == HFLIP) { // Flip horizonntal
|
||||
int newuv[] = new int[uv.length];
|
||||
for (int i = 0; i < uv.length; i++) {
|
||||
newuv[i] = uv[i ^ 1];
|
||||
}
|
||||
uv = newuv;
|
||||
}
|
||||
else if (rot != ROT0) {
|
||||
int newuv[] = new int[uv.length];
|
||||
for (int i = 0; i < uv.length; i++) {
|
||||
newuv[i] = uv[(i+4-rot) % uv.length];
|
||||
}
|
||||
uv = newuv;
|
||||
}
|
||||
Face f = new Face();
|
||||
f.groupLine = updateGroup(GROUP_TEXTURE, material);
|
||||
switch (sv) {
|
||||
case TOP:
|
||||
f.faceLine = String.format("f %d/%d %d/%d %d/%d %d/%d\n", v[0], uv[0], v[1], uv[1], v[2], uv[2], v[3], uv[3]);
|
||||
break;
|
||||
case TOPFLIP:
|
||||
f.faceLine += String.format("f %d/%d %d/%d %d/%d %d/%d\n", v[3], uv[2], v[2], uv[3], v[1], uv[0], v[0], uv[1]);
|
||||
break;
|
||||
case TOPFLIPV:
|
||||
f.faceLine = String.format("f %d/%d %d/%d %d/%d %d/%d\n", v[0], uv[0], v[1], uv[1], v[2], uv[2], v[3], uv[3]);
|
||||
break;
|
||||
case TOPFLIPHV:
|
||||
f.faceLine = String.format("f %d/%d %d/%d %d/%d %d/%d\n", v[0], uv[0], v[1], uv[1], v[2], uv[2], v[3], uv[3]);
|
||||
break;
|
||||
case BOTTOM:
|
||||
f.faceLine = String.format("f %d/%d %d/%d %d/%d %d/%d\n", v[3], uv[3], v[2], uv[2], v[1], uv[1], v[0], uv[0]);
|
||||
break;
|
||||
case BOTH:
|
||||
f.faceLine = String.format("f %d/%d %d/%d %d/%d %d/%d\n", v[0], uv[0], v[1], uv[1], v[2], uv[2], v[3], uv[3]);
|
||||
f.faceLine += String.format("f %d/%d %d/%d %d/%d %d/%d\n", v[3], uv[3], v[2], uv[2], v[1], uv[1], v[0], uv[0]);
|
||||
break;
|
||||
case FLIP:
|
||||
f.faceLine = String.format("f %d/%d %d/%d %d/%d %d/%d\n", v[0], uv[0], v[1], uv[1], v[2], uv[2], v[3], uv[3]);
|
||||
f.faceLine += String.format("f %d/%d %d/%d %d/%d %d/%d\n", v[3], uv[2], v[2], uv[3], v[1], uv[0], v[0], uv[1]);
|
||||
break;
|
||||
}
|
||||
faces.add(f);
|
||||
}
|
||||
|
||||
public Set<String> getMaterialIDs() {
|
||||
return matIDs;
|
||||
}
|
||||
|
||||
private static final boolean getSubblock(short[] mod, int x, int y, int z) {
|
||||
if ((x >= 0) && (x < MODELSCALE) && (y >= 0) && (y < MODELSCALE) && (z >= 0) && (z < MODELSCALE)) {
|
||||
return mod[MODELSCALE*MODELSCALE*y + MODELSCALE*z + x] != 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Scan along X axis
|
||||
private int scanX(short[] tmod, int x, int y, int z) {
|
||||
int xlen = 0;
|
||||
while (getSubblock(tmod, x+xlen, y, z)) {
|
||||
xlen++;
|
||||
}
|
||||
return xlen;
|
||||
}
|
||||
// Scan along Z axis for rows matching given x length
|
||||
private int scanZ(short[] tmod, int x, int y, int z, int xlen) {
|
||||
int zlen = 0;
|
||||
while (scanX(tmod, x, y, z+zlen) >= xlen) {
|
||||
zlen++;
|
||||
}
|
||||
return zlen;
|
||||
}
|
||||
// Scan along Y axis for layers matching given X and Z lengths
|
||||
private int scanY(short[] tmod, int x, int y, int z, int xlen, int zlen) {
|
||||
int ylen = 0;
|
||||
while (scanZ(tmod, x, y+ylen, z, xlen) >= zlen) {
|
||||
ylen++;
|
||||
}
|
||||
return ylen;
|
||||
}
|
||||
private void addSubblock(short[] tmod, int x, int y, int z, List<RenderPatch> list) {
|
||||
// Find dimensions of cuboid
|
||||
int xlen = scanX(tmod, x, y, z);
|
||||
int zlen = scanZ(tmod, x, y, z, xlen);
|
||||
int ylen = scanY(tmod, x, y, z, xlen, zlen);
|
||||
// Add equivalent of boxblock
|
||||
CustomRenderer.addBox(HDBlockModels.getPatchDefinitionFactory(), list,
|
||||
BLKSIZE * x, BLKSIZE * (x+xlen),
|
||||
BLKSIZE * y, BLKSIZE * (y+ylen),
|
||||
BLKSIZE * z, BLKSIZE * (z+zlen),
|
||||
HDBlockModels.boxPatchList);
|
||||
// And remove blocks from model (since we have them covered)
|
||||
for (int xx = 0; xx < xlen; xx++) {
|
||||
for (int yy = 0; yy < ylen; yy++) {
|
||||
for (int zz = 0; zz < zlen; zz++) {
|
||||
tmod[MODELSCALE*MODELSCALE*(y+yy) + MODELSCALE*(z+zz) + (x+xx)] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private PatchDefinition[] getScaledModelAsPatches(short[] mod) {
|
||||
ArrayList<RenderPatch> list = new ArrayList<RenderPatch>();
|
||||
short[] tmod = Arrays.copyOf(mod, mod.length); // Make copy
|
||||
for (int y = 0; y < MODELSCALE; y++) {
|
||||
for (int z = 0; z < MODELSCALE; z++) {
|
||||
for (int x = 0; x < MODELSCALE; x++) {
|
||||
if (getSubblock(tmod, x, y, z)) { // If occupied, try to add to list
|
||||
addSubblock(tmod, x, y, z, list);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
PatchDefinition[] pd = new PatchDefinition[list.size()];
|
||||
for (int i = 0; i < pd.length; i++) {
|
||||
pd[i] = (PatchDefinition) list.get(i);
|
||||
}
|
||||
return pd;
|
||||
}
|
||||
|
||||
private String updateGroup(int grpIndex, String newgroup) {
|
||||
if (enabledGroups[grpIndex]) {
|
||||
if (!newgroup.equals(group[grpIndex])) {
|
||||
group[grpIndex] = newgroup;
|
||||
String newline = "g";
|
||||
for (int i = 0; i < GROUP_COUNT; i++) {
|
||||
if (enabledGroups[i]) {
|
||||
newline += " " + group[i];
|
||||
}
|
||||
}
|
||||
newline += "\n";
|
||||
groupline = newline;
|
||||
}
|
||||
}
|
||||
return groupline;
|
||||
}
|
||||
|
||||
public boolean getGroupEnabled(int grpIndex) {
|
||||
if (grpIndex < enabledGroups.length) {
|
||||
return enabledGroups[grpIndex];
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public void setGroupEnabled(int grpIndex, boolean set) {
|
||||
if (grpIndex < enabledGroups.length) {
|
||||
enabledGroups[grpIndex] = set;
|
||||
}
|
||||
}
|
||||
public String getBaseName() {
|
||||
return basename;
|
||||
}
|
||||
}
|
||||
1595
DynmapCore/src/main/java/org/dynmap/hdmap/CTMTexturePack.java
Normal file
1595
DynmapCore/src/main/java/org/dynmap/hdmap/CTMTexturePack.java
Normal file
File diff suppressed because it is too large
Load diff
296
DynmapCore/src/main/java/org/dynmap/hdmap/CaveHDShader.java
Normal file
296
DynmapCore/src/main/java/org/dynmap/hdmap/CaveHDShader.java
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import static org.dynmap.JSONUtils.s;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.BitSet;
|
||||
import java.util.List;
|
||||
|
||||
import org.dynmap.Color;
|
||||
import org.dynmap.ConfigurationNode;
|
||||
import org.dynmap.DynmapCore;
|
||||
import org.dynmap.MapManager;
|
||||
import org.dynmap.common.DynmapCommandSender;
|
||||
import org.dynmap.exporter.OBJExport;
|
||||
import org.dynmap.renderer.DynmapBlockState;
|
||||
import org.dynmap.utils.BlockStep;
|
||||
import org.dynmap.utils.DynLongHashMap;
|
||||
import org.dynmap.utils.MapChunkCache;
|
||||
import org.dynmap.utils.MapIterator;
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
public class CaveHDShader implements HDShader {
|
||||
private String name;
|
||||
private boolean iflit;
|
||||
private Color startColor;
|
||||
private Color endColor;
|
||||
private BitSet hiddenids = new BitSet();
|
||||
|
||||
private void setHidden(DynmapBlockState blk) {
|
||||
hiddenids.set(blk.globalStateIndex);
|
||||
}
|
||||
private void setHidden(String blkname) {
|
||||
DynmapBlockState bbs = DynmapBlockState.getBaseStateByName(blkname);
|
||||
if (bbs.isNotAir()) {
|
||||
for (int i = 0; i < bbs.getStateCount(); i++) {
|
||||
setHidden(bbs.getState(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
private boolean isHidden(DynmapBlockState blk) {
|
||||
return hiddenids.get(blk.globalStateIndex);
|
||||
}
|
||||
public CaveHDShader(DynmapCore core, ConfigurationNode configuration) {
|
||||
name = (String) configuration.get("name");
|
||||
iflit = configuration.getBoolean("onlyiflit", false);
|
||||
startColor = configuration.getColor("startColor", null);
|
||||
endColor = configuration.getColor("endColor", null);
|
||||
for (int i = 0; i < DynmapBlockState.getGlobalIndexMax(); i++) {
|
||||
DynmapBlockState bs = DynmapBlockState.getStateByGlobalIndex(i);
|
||||
if (bs.isAir() || bs.isWater()) {
|
||||
setHidden(bs);
|
||||
}
|
||||
}
|
||||
|
||||
List<Object> hidden = configuration.getList("hiddennames");
|
||||
if(hidden != null) {
|
||||
for(Object o : hidden) {
|
||||
if(o instanceof String) {
|
||||
setHidden((String) o);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
setHidden(DynmapBlockState.LOG_BLOCK);
|
||||
setHidden(DynmapBlockState.LEAVES_BLOCK);
|
||||
setHidden(DynmapBlockState.GLASS_BLOCK);
|
||||
setHidden(DynmapBlockState.WOODEN_DOOR_BLOCK);
|
||||
setHidden(DynmapBlockState.IRON_DOOR_BLOCK);
|
||||
setHidden(DynmapBlockState.SNOW_BLOCK);
|
||||
setHidden(DynmapBlockState.ICE_BLOCK);
|
||||
setHidden(DynmapBlockState.SNOW_LAYER_BLOCK);
|
||||
for (int i = 0; i < DynmapBlockState.getGlobalIndexMax(); i++) {
|
||||
DynmapBlockState bs = DynmapBlockState.getStateByGlobalIndex(i);
|
||||
if (bs.isLeaves() || bs.isSnow() || bs.isLog()) {
|
||||
setHidden(bs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBiomeDataNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRawBiomeDataNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHightestBlockYDataNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBlockTypeDataNeeded() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSkyLightLevelNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmittedLightLevelNeeded() {
|
||||
return iflit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
private class OurShaderState implements HDShaderState {
|
||||
private Color color;
|
||||
protected MapIterator mapiter;
|
||||
protected HDMap map;
|
||||
private boolean air;
|
||||
private final int sealevel;
|
||||
private final int ymax, ymin;
|
||||
final int[] lightingTable;
|
||||
|
||||
private OurShaderState(MapIterator mapiter, HDMap map, MapChunkCache cache) {
|
||||
this.mapiter = mapiter;
|
||||
this.map = map;
|
||||
this.color = new Color();
|
||||
this.ymax = mapiter.getWorldHeight() - 1;
|
||||
this.ymin = mapiter.getWorldYMin();
|
||||
this.sealevel = mapiter.getWorldSeaLevel();
|
||||
if (MapManager.mapman.useBrightnessTable()) {
|
||||
lightingTable = cache.getWorld().getBrightnessTable();
|
||||
}
|
||||
else {
|
||||
lightingTable = null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get our shader
|
||||
*/
|
||||
@Override
|
||||
public HDShader getShader() {
|
||||
return CaveHDShader.this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get our map
|
||||
*/
|
||||
@Override
|
||||
public HDMap getMap() {
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get our lighting
|
||||
*/
|
||||
@Override
|
||||
public HDLighting getLighting() {
|
||||
return map.getLighting();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset renderer state for new ray
|
||||
*/
|
||||
@Override
|
||||
public void reset(HDPerspectiveState ps) {
|
||||
color.setTransparent();
|
||||
air = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process next ray step - called for each block on route
|
||||
* @return true if ray is done, false if ray needs to continue
|
||||
*/
|
||||
@Override
|
||||
public boolean processBlock(HDPerspectiveState ps) {
|
||||
DynmapBlockState blocktype = ps.getBlockState();
|
||||
if (isHidden(blocktype)) {
|
||||
blocktype = DynmapBlockState.AIR;
|
||||
}
|
||||
else if (blocktype.isNotAir()) {
|
||||
air = false;
|
||||
return false;
|
||||
}
|
||||
if (blocktype.isAir() && !air) {
|
||||
if(iflit && (ps.getMapIterator().getBlockEmittedLight() == 0)) {
|
||||
return false;
|
||||
}
|
||||
int cr, cg, cb;
|
||||
int mult;
|
||||
|
||||
int y = mapiter.getY();
|
||||
if((startColor != null) && (endColor != null))
|
||||
{
|
||||
double interp = ((double)(y - this.ymin)) / (this.ymax - this.ymin);
|
||||
cr = (int)(((1.0 - interp) * startColor.getRed()) + (interp * endColor.getRed()));
|
||||
cg = (int)(((1.0 - interp) * startColor.getGreen()) + (interp * endColor.getGreen()));
|
||||
cb = (int)(((1.0 - interp) * startColor.getBlue()) + (interp * endColor.getBlue()));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (y < this.sealevel) {
|
||||
cr = 0;
|
||||
cg = 64 + ((192 * (y - this.ymin)) / (this.sealevel - this.ymin));
|
||||
cb = 255 - (255 * (y - this.ymin)) / (this.sealevel - this.ymin);
|
||||
} else {
|
||||
cr = (255 * (y - this.sealevel)) / (this.ymax - this.sealevel);
|
||||
cg = 255;
|
||||
cb = 0;
|
||||
}
|
||||
}
|
||||
/* Figure out which color to use */
|
||||
switch(ps.getLastBlockStep()) {
|
||||
case X_PLUS:
|
||||
case X_MINUS:
|
||||
mult = 224;
|
||||
break;
|
||||
case Z_PLUS:
|
||||
case Z_MINUS:
|
||||
mult = 256;
|
||||
break;
|
||||
default:
|
||||
mult = 160;
|
||||
break;
|
||||
}
|
||||
cr = cr * mult / 256;
|
||||
cg = cg * mult / 256;
|
||||
cb = cb * mult / 256;
|
||||
|
||||
color.setRGBA(cr, cg, cb, 255);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Ray ended - used to report that ray has exited map (called if renderer has not reported complete)
|
||||
*/
|
||||
@Override
|
||||
public void rayFinished(HDPerspectiveState ps) {
|
||||
}
|
||||
/**
|
||||
* Get result color - get resulting color for ray
|
||||
* @param c - object to store color value in
|
||||
* @param index - index of color to request (renderer specific - 0=default, 1=day for night/day renderer
|
||||
*/
|
||||
@Override
|
||||
public void getRayColor(Color c, int index) {
|
||||
c.setColor(color);
|
||||
}
|
||||
/**
|
||||
* Clean up state object - called after last ray completed
|
||||
*/
|
||||
@Override
|
||||
public void cleanup() {
|
||||
}
|
||||
@Override
|
||||
public DynLongHashMap getCTMTextureCache() {
|
||||
return null;
|
||||
}
|
||||
@Override
|
||||
public int[] getLightingTable() {
|
||||
return lightingTable;
|
||||
}
|
||||
@Override
|
||||
public void setLastBlockState(DynmapBlockState new_lastbs) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get renderer state object for use rendering a tile
|
||||
* @param map - map being rendered
|
||||
* @param cache - chunk cache containing data for tile to be rendered
|
||||
* @param mapiter - iterator used when traversing rays in tile
|
||||
* @param scale - scale of perspective
|
||||
* @return state object to use for all rays in tile
|
||||
*/
|
||||
@Override
|
||||
public HDShaderState getStateInstance(HDMap map, MapChunkCache cache, MapIterator mapiter, int scale) {
|
||||
return new OurShaderState(mapiter, map, cache);
|
||||
}
|
||||
|
||||
/* Add shader's contributions to JSON for map object */
|
||||
public void addClientConfiguration(JSONObject mapObject) {
|
||||
s(mapObject, "shader", name);
|
||||
}
|
||||
@Override
|
||||
public void exportAsMaterialLibrary(DynmapCommandSender sender, OBJExport out) throws IOException {
|
||||
throw new IOException("Export unsupported");
|
||||
}
|
||||
private static final String[] nulllist = new String[0];
|
||||
@Override
|
||||
public String[] getCurrentBlockMaterials(DynmapBlockState blk, MapIterator mapiter, int[] txtidx, BlockStep[] steps) {
|
||||
return nulllist;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import static org.dynmap.JSONUtils.s;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.dynmap.Color;
|
||||
import org.dynmap.ConfigurationNode;
|
||||
import org.dynmap.DynmapCore;
|
||||
import org.dynmap.Log;
|
||||
import org.dynmap.MapManager;
|
||||
import org.dynmap.common.DynmapCommandSender;
|
||||
import org.dynmap.exporter.OBJExport;
|
||||
import org.dynmap.renderer.DynmapBlockState;
|
||||
import org.dynmap.utils.BlockStep;
|
||||
import org.dynmap.utils.DynLongHashMap;
|
||||
import org.dynmap.utils.MapChunkCache;
|
||||
import org.dynmap.utils.MapIterator;
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
// Shader for color coding by chunk data version
|
||||
public class ChunkStatusHDShader implements HDShader {
|
||||
private final String name;
|
||||
|
||||
private static class ChunkStatusMap {
|
||||
Color defcolor;
|
||||
ChunkStatusMap(String s, int c) {
|
||||
defcolor = new Color((c>>16)&0xFF, (c>>8)&0xFF, c&0xFF);
|
||||
statusmap.put(s, this);
|
||||
}
|
||||
};
|
||||
private static HashMap<String, ChunkStatusMap> statusmap = new HashMap<String, ChunkStatusMap>();
|
||||
|
||||
static {
|
||||
new ChunkStatusMap("empty", 0xFF0000);
|
||||
new ChunkStatusMap("structure_starts", 0xFF1493);
|
||||
new ChunkStatusMap("structure_references", 0xFF7F50);
|
||||
new ChunkStatusMap("biomes", 0xFFA500);
|
||||
new ChunkStatusMap("noise", 0xFFD700);
|
||||
new ChunkStatusMap("surface", 0xFFFF00);
|
||||
new ChunkStatusMap("carvers", 0xFFEFD5);
|
||||
new ChunkStatusMap("liquid_carvers", 0xF0E68C);
|
||||
new ChunkStatusMap("features", 0xBDB76B);
|
||||
new ChunkStatusMap("initialize_light", 0xAAA0AA);
|
||||
new ChunkStatusMap("light", 0xDDA0DD);
|
||||
new ChunkStatusMap("heightmaps", 0x9370DB);
|
||||
new ChunkStatusMap("spawn", 0xFF00FF);
|
||||
new ChunkStatusMap("full", 0x32CD32);
|
||||
}
|
||||
|
||||
final static Color unknown_color = new Color(255, 255, 255);
|
||||
|
||||
private ArrayList<String> unknown_state = new ArrayList<String>();
|
||||
|
||||
public ChunkStatusHDShader(DynmapCore core, ConfigurationNode configuration) {
|
||||
name = (String) configuration.get("name");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBiomeDataNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRawBiomeDataNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHightestBlockYDataNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBlockTypeDataNeeded() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSkyLightLevelNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmittedLightLevelNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
private class OurShaderState implements HDShaderState {
|
||||
private Color color[];
|
||||
private Color c;
|
||||
protected HDMap map;
|
||||
private HDLighting lighting;
|
||||
final int[] lightingTable;
|
||||
|
||||
private OurShaderState(MapIterator mapiter, HDMap map, MapChunkCache cache, int scale) {
|
||||
this.map = map;
|
||||
this.lighting = map.getLighting();
|
||||
if(lighting.isNightAndDayEnabled()) {
|
||||
color = new Color[] { new Color(), new Color() };
|
||||
}
|
||||
else {
|
||||
color = new Color[] { new Color() };
|
||||
}
|
||||
c = new Color();
|
||||
if (MapManager.mapman.useBrightnessTable()) {
|
||||
lightingTable = cache.getWorld().getBrightnessTable();
|
||||
}
|
||||
else {
|
||||
lightingTable = null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get our shader
|
||||
*/
|
||||
public HDShader getShader() {
|
||||
return ChunkStatusHDShader.this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get our map
|
||||
*/
|
||||
public HDMap getMap() {
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get our lighting
|
||||
*/
|
||||
public HDLighting getLighting() {
|
||||
return lighting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset renderer state for new ray
|
||||
*/
|
||||
public void reset(HDPerspectiveState ps) {
|
||||
for(int i = 0; i < color.length; i++)
|
||||
color[i].setTransparent();
|
||||
}
|
||||
/**
|
||||
* Process next ray step - called for each block on route
|
||||
* @return true if ray is done, false if ray needs to continue
|
||||
*/
|
||||
public boolean processBlock(HDPerspectiveState ps) {
|
||||
if (ps.getBlockState().isAir()) {
|
||||
return false;
|
||||
}
|
||||
String cs = ps.getMapIterator().getChunkStatus(); // Get data version
|
||||
|
||||
ChunkStatusMap csm = statusmap.get(cs);
|
||||
if (csm != null) {
|
||||
c.setColor(csm.defcolor);
|
||||
}
|
||||
else {
|
||||
c.setColor(unknown_color);
|
||||
if (!unknown_state.contains(cs)) {
|
||||
Log.warning("Unknown chunk status: " + cs);
|
||||
unknown_state.add(cs);
|
||||
}
|
||||
}
|
||||
/* Handle light level, if needed */
|
||||
lighting.applyLighting(ps, this, c, color);
|
||||
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Ray ended - used to report that ray has exited map (called if renderer has not reported complete)
|
||||
*/
|
||||
public void rayFinished(HDPerspectiveState ps) {
|
||||
}
|
||||
/**
|
||||
* Get result color - get resulting color for ray
|
||||
* @param c - object to store color value in
|
||||
* @param index - index of color to request (renderer specific - 0=default, 1=day for night/day renderer
|
||||
*/
|
||||
public void getRayColor(Color c, int index) {
|
||||
c.setColor(color[index]);
|
||||
}
|
||||
/**
|
||||
* Clean up state object - called after last ray completed
|
||||
*/
|
||||
public void cleanup() {
|
||||
}
|
||||
@Override
|
||||
public DynLongHashMap getCTMTextureCache() {
|
||||
return null;
|
||||
}
|
||||
@Override
|
||||
public int[] getLightingTable() {
|
||||
return lightingTable;
|
||||
}
|
||||
@Override
|
||||
public void setLastBlockState(DynmapBlockState new_lastbs) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get renderer state object for use rendering a tile
|
||||
* @param map - map being rendered
|
||||
* @param cache - chunk cache containing data for tile to be rendered
|
||||
* @param mapiter - iterator used when traversing rays in tile
|
||||
* @param scale - scale of perspecitve
|
||||
* @return state object to use for all rays in tile
|
||||
*/
|
||||
@Override
|
||||
public HDShaderState getStateInstance(HDMap map, MapChunkCache cache, MapIterator mapiter, int scale) {
|
||||
return new OurShaderState(mapiter, map, cache, scale);
|
||||
}
|
||||
|
||||
/* Add shader's contributions to JSON for map object */
|
||||
public void addClientConfiguration(JSONObject mapObject) {
|
||||
s(mapObject, "shader", name);
|
||||
}
|
||||
@Override
|
||||
public void exportAsMaterialLibrary(DynmapCommandSender sender, OBJExport out) throws IOException {
|
||||
throw new IOException("Export unsupported");
|
||||
}
|
||||
private static final String[] nulllist = new String[0];
|
||||
@Override
|
||||
public String[] getCurrentBlockMaterials(DynmapBlockState blk, MapIterator mapiter, int[] txtidx, BlockStep[] steps) {
|
||||
return nulllist;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import static org.dynmap.JSONUtils.s;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.dynmap.Color;
|
||||
import org.dynmap.ConfigurationNode;
|
||||
import org.dynmap.DynmapCore;
|
||||
import org.dynmap.Log;
|
||||
import org.dynmap.MapManager;
|
||||
import org.dynmap.common.DynmapCommandSender;
|
||||
import org.dynmap.exporter.OBJExport;
|
||||
import org.dynmap.renderer.DynmapBlockState;
|
||||
import org.dynmap.utils.BlockStep;
|
||||
import org.dynmap.utils.DynLongHashMap;
|
||||
import org.dynmap.utils.MapChunkCache;
|
||||
import org.dynmap.utils.MapIterator;
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
// Shader for color coding by chunk data version
|
||||
public class ChunkVersionHDShader implements HDShader {
|
||||
private final String name;
|
||||
|
||||
private static class DataVersionMap {
|
||||
int dataVersion;
|
||||
//String version;
|
||||
Color defcolor;
|
||||
DataVersionMap(int dv, String v, int c) {
|
||||
dataVersion = dv;
|
||||
//version = v;
|
||||
defcolor = new Color((c>>16)&0xFF, (c>>8)&0xFF, c&0xFF);
|
||||
|
||||
}
|
||||
};
|
||||
// Mapping from https://minecraft.wiki/w/Data_version
|
||||
final static DataVersionMap[] versionmap = {
|
||||
new DataVersionMap(0, "unknown", 0x202020),
|
||||
new DataVersionMap(1519, "1.13.0", 0xF9E79F),
|
||||
new DataVersionMap(1628, "1.13.1", 0xF4D03F),
|
||||
new DataVersionMap(1631, "1.13.2", 0xD4AC0D),
|
||||
new DataVersionMap(1952, "1.14.0", 0xABEBC6),
|
||||
new DataVersionMap(1957, "1.14.1", 0x58D68D),
|
||||
new DataVersionMap(1963, "1.14.2", 0x28B463),
|
||||
new DataVersionMap(1968, "1.14.3", 0x239B56),
|
||||
new DataVersionMap(1976, "1.14.4", 0x1D8348),
|
||||
new DataVersionMap(2225, "1.15.0", 0xAED6F1),
|
||||
new DataVersionMap(2227, "1.15.1", 0x5DADE2),
|
||||
new DataVersionMap(2230, "1.15.2", 0x2E86C1),
|
||||
new DataVersionMap(2566, "1.16.0", 0xD7BDE2),
|
||||
new DataVersionMap(2567, "1.16.1", 0xC39BD3),
|
||||
new DataVersionMap(2578, "1.16.2", 0xAF7AC5),
|
||||
new DataVersionMap(2580, "1.16.3", 0x9B59B6),
|
||||
new DataVersionMap(2584, "1.16.4", 0x884EA0),
|
||||
new DataVersionMap(2586, "1.16.5", 0x76448A),
|
||||
new DataVersionMap(2724, "1.17.0", 0xF5CBA7),
|
||||
new DataVersionMap(2730, "1.17.1", 0xEB984E),
|
||||
new DataVersionMap(2860, "1.18.0", 0xA3E4D7),
|
||||
new DataVersionMap(2865, "1.18.1", 0x48C9B0),
|
||||
new DataVersionMap(2975, "1.18.2", 0x38bfa5),
|
||||
new DataVersionMap(3105, "1.19", 0xd56f82),
|
||||
new DataVersionMap(3116, "1.19.1", 0xe196a4),
|
||||
new DataVersionMap(3120, "1.19.2", 0xe7aeb8),
|
||||
new DataVersionMap(3218, "1.19.3", 0xf8c0c8),
|
||||
new DataVersionMap(3337, "1.19.4", 0xffb6c1),
|
||||
new DataVersionMap(3465, "1.20.1", 0xe7aeb10),
|
||||
new DataVersionMap(3578, "1.20.2", 0xe196a7),
|
||||
new DataVersionMap(3698, "1.20.3", 0xe7aeb11),
|
||||
new DataVersionMap(3700, "1.20.4", 0xe196a8),
|
||||
new DataVersionMap(3837, "1.20.5", 0xe7aeb12),
|
||||
new DataVersionMap(3839, "1.20.6", 0xe196a9),
|
||||
new DataVersionMap(3953, "1.21.0", 0xe7aeb13),
|
||||
|
||||
};
|
||||
final static Color unknown_color = new Color(255, 255, 255);
|
||||
|
||||
private ArrayList<Integer> unknown_vers = new ArrayList<Integer>();
|
||||
|
||||
public ChunkVersionHDShader(DynmapCore core, ConfigurationNode configuration) {
|
||||
name = (String) configuration.get("name");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBiomeDataNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRawBiomeDataNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHightestBlockYDataNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBlockTypeDataNeeded() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSkyLightLevelNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmittedLightLevelNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
private class OurShaderState implements HDShaderState {
|
||||
private Color color[];
|
||||
private Color c;
|
||||
protected HDMap map;
|
||||
private HDLighting lighting;
|
||||
final int[] lightingTable;
|
||||
|
||||
private OurShaderState(MapIterator mapiter, HDMap map, MapChunkCache cache, int scale) {
|
||||
this.map = map;
|
||||
this.lighting = map.getLighting();
|
||||
if(lighting.isNightAndDayEnabled()) {
|
||||
color = new Color[] { new Color(), new Color() };
|
||||
}
|
||||
else {
|
||||
color = new Color[] { new Color() };
|
||||
}
|
||||
c = new Color();
|
||||
if (MapManager.mapman.useBrightnessTable()) {
|
||||
lightingTable = cache.getWorld().getBrightnessTable();
|
||||
}
|
||||
else {
|
||||
lightingTable = null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get our shader
|
||||
*/
|
||||
public HDShader getShader() {
|
||||
return ChunkVersionHDShader.this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get our map
|
||||
*/
|
||||
public HDMap getMap() {
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get our lighting
|
||||
*/
|
||||
public HDLighting getLighting() {
|
||||
return lighting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset renderer state for new ray
|
||||
*/
|
||||
public void reset(HDPerspectiveState ps) {
|
||||
for(int i = 0; i < color.length; i++)
|
||||
color[i].setTransparent();
|
||||
}
|
||||
/**
|
||||
* Process next ray step - called for each block on route
|
||||
* @return true if ray is done, false if ray needs to continue
|
||||
*/
|
||||
public boolean processBlock(HDPerspectiveState ps) {
|
||||
if (ps.getBlockState().isAir()) {
|
||||
return false;
|
||||
}
|
||||
int ver = ps.getMapIterator().getDataVersion(); // Get data version
|
||||
boolean match = false;
|
||||
// Find last record <= version
|
||||
for (int i = 0; i < versionmap.length; i++) {
|
||||
if (ver <= versionmap[i].dataVersion) {
|
||||
c.setColor(versionmap[i].defcolor);
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!match) {
|
||||
c.setColor(unknown_color);
|
||||
if (!unknown_vers.contains(ver)) {
|
||||
Log.warning("Unknown chunk dataVersion: " + ver);
|
||||
unknown_vers.add(ver);
|
||||
}
|
||||
}
|
||||
/* Handle light level, if needed */
|
||||
lighting.applyLighting(ps, this, c, color);
|
||||
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Ray ended - used to report that ray has exited map (called if renderer has not reported complete)
|
||||
*/
|
||||
public void rayFinished(HDPerspectiveState ps) {
|
||||
}
|
||||
/**
|
||||
* Get result color - get resulting color for ray
|
||||
* @param c - object to store color value in
|
||||
* @param index - index of color to request (renderer specific - 0=default, 1=day for night/day renderer
|
||||
*/
|
||||
public void getRayColor(Color c, int index) {
|
||||
c.setColor(color[index]);
|
||||
}
|
||||
/**
|
||||
* Clean up state object - called after last ray completed
|
||||
*/
|
||||
public void cleanup() {
|
||||
}
|
||||
@Override
|
||||
public DynLongHashMap getCTMTextureCache() {
|
||||
return null;
|
||||
}
|
||||
@Override
|
||||
public int[] getLightingTable() {
|
||||
return lightingTable;
|
||||
}
|
||||
@Override
|
||||
public void setLastBlockState(DynmapBlockState new_lastbs) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get renderer state object for use rendering a tile
|
||||
* @param map - map being rendered
|
||||
* @param cache - chunk cache containing data for tile to be rendered
|
||||
* @param mapiter - iterator used when traversing rays in tile
|
||||
* @param scale - scale of perspecitve
|
||||
* @return state object to use for all rays in tile
|
||||
*/
|
||||
@Override
|
||||
public HDShaderState getStateInstance(HDMap map, MapChunkCache cache, MapIterator mapiter, int scale) {
|
||||
return new OurShaderState(mapiter, map, cache, scale);
|
||||
}
|
||||
|
||||
/* Add shader's contributions to JSON for map object */
|
||||
public void addClientConfiguration(JSONObject mapObject) {
|
||||
s(mapObject, "shader", name);
|
||||
}
|
||||
@Override
|
||||
public void exportAsMaterialLibrary(DynmapCommandSender sender, OBJExport out) throws IOException {
|
||||
throw new IOException("Export unsupported");
|
||||
}
|
||||
private static final String[] nulllist = new String[0];
|
||||
@Override
|
||||
public String[] getCurrentBlockMaterials(DynmapBlockState blk, MapIterator mapiter, int[] txtidx, BlockStep[] steps) {
|
||||
return nulllist;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import java.util.BitSet;
|
||||
import java.util.Map;
|
||||
|
||||
import org.dynmap.Log;
|
||||
import org.dynmap.renderer.CustomRenderer;
|
||||
import org.dynmap.renderer.DynmapBlockState;
|
||||
import org.dynmap.renderer.MapDataContext;
|
||||
import org.dynmap.renderer.RenderPatch;
|
||||
|
||||
public class CustomBlockModel extends HDBlockModel {
|
||||
public CustomRenderer render;
|
||||
|
||||
public CustomBlockModel(DynmapBlockState bstate, BitSet databits, String classname, Map<String,String> classparm, String blockset) {
|
||||
super(bstate, databits, blockset);
|
||||
try {
|
||||
Class<?> cls = Class.forName(classname); /* Get class */
|
||||
render = (CustomRenderer) cls.getDeclaredConstructor().newInstance();
|
||||
if(render.initializeRenderer(HDBlockModels.pdf, bstate.blockName, databits, classparm) == false) {
|
||||
Log.severe("Error loading custom renderer - " + classname);
|
||||
render = null;
|
||||
}
|
||||
else {
|
||||
if(render.getTileEntityFieldsNeeded() != null) {
|
||||
DynmapBlockState bbs = bstate.baseState;
|
||||
for(int i = 0; i < bbs.getStateCount(); i++) {
|
||||
if (databits.isEmpty() || databits.get(i)) {
|
||||
DynmapBlockState bs = bbs.getState(i);
|
||||
HDBlockModels.customModelsRequestingTileData.set(bs.globalStateIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception x) {
|
||||
Log.severe("Error loading custom renderer - " + classname, x);
|
||||
render = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTextureCount() {
|
||||
return render.getMaximumTextureCount(HDBlockModels.pdf);
|
||||
}
|
||||
|
||||
public boolean isOnlyBlockStateSensitive() {
|
||||
return render.isOnlyBlockStateSensitive();
|
||||
}
|
||||
private static final RenderPatch[] empty_list = new RenderPatch[0];
|
||||
|
||||
public RenderPatch[] getMeshForBlock(MapDataContext ctx) {
|
||||
if(render != null)
|
||||
return render.getRenderPatchList(ctx);
|
||||
else
|
||||
return empty_list;
|
||||
}
|
||||
@Override
|
||||
public void removed(DynmapBlockState blk) {
|
||||
super.removed(blk);
|
||||
HDBlockModels.customModelsRequestingTileData.clear(blk.globalStateIndex);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import org.dynmap.Color;
|
||||
import org.dynmap.ConfigurationNode;
|
||||
import org.dynmap.DynmapCore;
|
||||
import org.dynmap.DynmapWorld;
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
import static org.dynmap.JSONUtils.s;
|
||||
|
||||
public class DefaultHDLighting implements HDLighting {
|
||||
private String name;
|
||||
protected boolean grayscale;
|
||||
protected boolean blackandwhite;
|
||||
protected int blackthreshold;
|
||||
protected final Color graytone;
|
||||
protected final Color graytonedark;
|
||||
|
||||
public DefaultHDLighting(DynmapCore core, ConfigurationNode configuration) {
|
||||
name = (String) configuration.get("name");
|
||||
grayscale = configuration.getBoolean("grayscale", false);
|
||||
graytone = configuration.getColor("graytone", "#FFFFFF");
|
||||
graytonedark = configuration.getColor("graytonedark", "#000000");
|
||||
blackandwhite = configuration.getBoolean("blackandwhite", false);
|
||||
if (blackandwhite) grayscale = false;
|
||||
blackthreshold = configuration.getInteger("blackthreshold", 0x40);
|
||||
}
|
||||
|
||||
protected void checkGrayscale(Color[] outcolor) {
|
||||
if (grayscale) {
|
||||
for (int i = 0; i < outcolor.length; i++) {
|
||||
outcolor[i].setGrayscale();
|
||||
outcolor[i].scaleColor(graytonedark,graytone);
|
||||
}
|
||||
}
|
||||
else if (blackandwhite) {
|
||||
for (int i = 0; i < outcolor.length; i++) {
|
||||
outcolor[i].setGrayscale();
|
||||
if (outcolor[i].getRed() > blackthreshold) {
|
||||
outcolor[i].setColor(graytone);
|
||||
}
|
||||
else {
|
||||
outcolor[i].setColor(graytonedark);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get lighting name */
|
||||
public String getName() { return name; }
|
||||
|
||||
/* Apply lighting to given pixel colors (1 outcolor if normal, 2 if night/day) */
|
||||
public void applyLighting(HDPerspectiveState ps, HDShaderState ss, Color incolor, Color[] outcolor) {
|
||||
for(int i = 0; i < outcolor.length; i++)
|
||||
outcolor[i].setColor(incolor);
|
||||
checkGrayscale(outcolor);
|
||||
}
|
||||
|
||||
/* Test if Biome Data is needed for this renderer */
|
||||
public boolean isBiomeDataNeeded() { return false; }
|
||||
|
||||
/* Test if raw biome temperature/rainfall data is needed */
|
||||
public boolean isRawBiomeDataNeeded() { return false; }
|
||||
|
||||
/* Test if highest block Y data is needed */
|
||||
public boolean isHightestBlockYDataNeeded() { return false; }
|
||||
|
||||
/* Tet if block type data needed */
|
||||
public boolean isBlockTypeDataNeeded() { return false; }
|
||||
|
||||
/* Test if night/day is enabled for this renderer */
|
||||
public boolean isNightAndDayEnabled() { return false; }
|
||||
|
||||
/* Test if sky light level needed */
|
||||
public boolean isSkyLightLevelNeeded() { return false; }
|
||||
|
||||
/* Test if emitted light level needed */
|
||||
public boolean isEmittedLightLevelNeeded() { return false; }
|
||||
|
||||
/* Add shader's contributions to JSON for map object */
|
||||
public void addClientConfiguration(JSONObject mapObject) {
|
||||
s(mapObject, "lighting", name);
|
||||
s(mapObject, "nightandday", isNightAndDayEnabled());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getBrightnessTable(DynmapWorld world) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
320
DynmapCore/src/main/java/org/dynmap/hdmap/DefaultHDShader.java
Normal file
320
DynmapCore/src/main/java/org/dynmap/hdmap/DefaultHDShader.java
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import static org.dynmap.JSONUtils.s;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.dynmap.Color;
|
||||
import org.dynmap.ColorScheme;
|
||||
import org.dynmap.ConfigurationNode;
|
||||
import org.dynmap.DynmapCore;
|
||||
import org.dynmap.MapManager;
|
||||
import org.dynmap.common.BiomeMap;
|
||||
import org.dynmap.common.DynmapCommandSender;
|
||||
import org.dynmap.exporter.OBJExport;
|
||||
import org.dynmap.renderer.DynmapBlockState;
|
||||
import org.dynmap.utils.BlockStep;
|
||||
import org.dynmap.utils.DynLongHashMap;
|
||||
import org.dynmap.utils.MapChunkCache;
|
||||
import org.dynmap.utils.MapIterator;
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
public class DefaultHDShader implements HDShader {
|
||||
private String name;
|
||||
protected ColorScheme colorScheme;
|
||||
|
||||
protected boolean transparency; /* Is transparency support active? */
|
||||
public enum BiomeColorOption {
|
||||
NONE, BIOME, TEMPERATURE, RAINFALL
|
||||
}
|
||||
protected BiomeColorOption biomecolored = BiomeColorOption.NONE; /* Use biome for coloring */
|
||||
|
||||
public DefaultHDShader(DynmapCore core, ConfigurationNode configuration) {
|
||||
name = (String) configuration.get("name");
|
||||
colorScheme = ColorScheme.getScheme(core, configuration.getString("colorscheme", "default"));
|
||||
transparency = configuration.getBoolean("transparency", true); /* Default on */
|
||||
String biomeopt = configuration.getString("biomecolored", "none");
|
||||
if(biomeopt.equals("biome")) {
|
||||
biomecolored = BiomeColorOption.BIOME;
|
||||
}
|
||||
else if(biomeopt.equals("temperature")) {
|
||||
biomecolored = BiomeColorOption.TEMPERATURE;
|
||||
}
|
||||
else if(biomeopt.equals("rainfall")) {
|
||||
biomecolored = BiomeColorOption.RAINFALL;
|
||||
}
|
||||
else {
|
||||
biomecolored = BiomeColorOption.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBiomeDataNeeded() {
|
||||
return biomecolored == BiomeColorOption.BIOME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRawBiomeDataNeeded() {
|
||||
return (biomecolored == BiomeColorOption.RAINFALL) || (biomecolored == BiomeColorOption.TEMPERATURE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHightestBlockYDataNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBlockTypeDataNeeded() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSkyLightLevelNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmittedLightLevelNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
private class OurShaderState implements HDShaderState {
|
||||
private Color color[];
|
||||
protected MapIterator mapiter;
|
||||
protected HDMap map;
|
||||
private Color tmpcolor[];
|
||||
private int pixelodd;
|
||||
private HDLighting lighting;
|
||||
final int[] lightingTable;
|
||||
|
||||
private OurShaderState(MapIterator mapiter, HDMap map, MapChunkCache cache) {
|
||||
this.mapiter = mapiter;
|
||||
this.map = map;
|
||||
this.lighting = map.getLighting();
|
||||
if(lighting.isNightAndDayEnabled()) {
|
||||
color = new Color[] { new Color(), new Color() };
|
||||
tmpcolor = new Color[] { new Color(), new Color() };
|
||||
}
|
||||
else {
|
||||
color = new Color[] { new Color() };
|
||||
tmpcolor = new Color[] { new Color() };
|
||||
}
|
||||
if (MapManager.mapman.useBrightnessTable()) {
|
||||
lightingTable = cache.getWorld().getBrightnessTable();
|
||||
}
|
||||
else {
|
||||
lightingTable = null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get our shader
|
||||
*/
|
||||
public HDShader getShader() {
|
||||
return DefaultHDShader.this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get our map
|
||||
*/
|
||||
public HDMap getMap() {
|
||||
return map;
|
||||
}
|
||||
/**
|
||||
* Get our lighting
|
||||
*/
|
||||
public HDLighting getLighting() {
|
||||
return lighting;
|
||||
}
|
||||
/**
|
||||
* Reset renderer state for new ray
|
||||
*/
|
||||
public void reset(HDPerspectiveState ps) {
|
||||
for(int i = 0; i < color.length; i++)
|
||||
color[i].setTransparent();
|
||||
pixelodd = (ps.getPixelX() & 0x3) + (ps.getPixelY()<<1);
|
||||
}
|
||||
|
||||
protected Color[] getBlockColors(DynmapBlockState block) {
|
||||
int idx = block.globalStateIndex;
|
||||
if (colorScheme.colors.length > idx) {
|
||||
return colorScheme.colors[idx];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process next ray step - called for each block on route
|
||||
* @return true if ray is done, false if ray needs to continue
|
||||
*/
|
||||
public boolean processBlock(HDPerspectiveState ps) {
|
||||
int i;
|
||||
DynmapBlockState blocktype = ps.getBlockState();
|
||||
if (blocktype.isAir())
|
||||
return false;
|
||||
Color[] colors = getBlockColors(blocktype);
|
||||
|
||||
if (colors != null) {
|
||||
int seq;
|
||||
int subalpha = ps.getSubmodelAlpha();
|
||||
/* Figure out which color to use */
|
||||
switch(ps.getLastBlockStep()) {
|
||||
case X_PLUS:
|
||||
case X_MINUS:
|
||||
seq = 2;
|
||||
break;
|
||||
case Z_PLUS:
|
||||
case Z_MINUS:
|
||||
seq = 0;
|
||||
break;
|
||||
default:
|
||||
//if(subalpha >= 0) /* We hit a block in a model */
|
||||
// seq = 4; /* Use smooth top */
|
||||
//else
|
||||
if(((pixelodd + mapiter.getY()) & 0x03) == 0)
|
||||
seq = 3;
|
||||
else
|
||||
seq = 1;
|
||||
break;
|
||||
}
|
||||
Color c = colors[seq];
|
||||
if (c.getAlpha() > 0) {
|
||||
/* Handle light level, if needed */
|
||||
lighting.applyLighting(ps, this, c, tmpcolor);
|
||||
/* If we got alpha from subblock model, use it instead */
|
||||
if(subalpha >= 0) {
|
||||
for(int j = 0; j < tmpcolor.length; j++)
|
||||
tmpcolor[j].setAlpha(Math.max(subalpha,tmpcolor[j].getAlpha()));
|
||||
}
|
||||
/* Blend color with accumulated color (weighted by alpha) */
|
||||
if(!transparency) { /* No transparency support */
|
||||
for(i = 0; i < color.length; i++)
|
||||
color[i].setARGB(tmpcolor[i].getARGB() | 0xFF000000);
|
||||
return true; /* We're done */
|
||||
}
|
||||
/* If no previous color contribution, use new color */
|
||||
else if(color[0].isTransparent()) {
|
||||
for(i = 0; i < color.length; i++)
|
||||
color[i].setColor(tmpcolor[i]);
|
||||
return (color[0].getAlpha() == 255);
|
||||
}
|
||||
/* Else, blend and generate new alpha */
|
||||
else {
|
||||
int alpha = color[0].getAlpha();
|
||||
int alpha2 = tmpcolor[0].getAlpha() * (255-alpha) / 255;
|
||||
int talpha = alpha + alpha2;
|
||||
for(i = 0; i < color.length; i++)
|
||||
color[i].setRGBA((tmpcolor[i].getRed()*alpha2 + color[i].getRed()*alpha) / talpha,
|
||||
(tmpcolor[i].getGreen()*alpha2 + color[i].getGreen()*alpha) / talpha,
|
||||
(tmpcolor[i].getBlue()*alpha2 + color[i].getBlue()*alpha) / talpha, talpha);
|
||||
return (talpha >= 254); /* If only one short, no meaningful contribution left */
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Ray ended - used to report that ray has exited map (called if renderer has not reported complete)
|
||||
*/
|
||||
public void rayFinished(HDPerspectiveState ps) {
|
||||
}
|
||||
/**
|
||||
* Get result color - get resulting color for ray
|
||||
* @param c - object to store color value in
|
||||
* @param index - index of color to request (renderer specific - 0=default, 1=day for night/day renderer
|
||||
*/
|
||||
public void getRayColor(Color c, int index) {
|
||||
c.setColor(color[index]);
|
||||
}
|
||||
/**
|
||||
* Clean up state object - called after last ray completed
|
||||
*/
|
||||
public void cleanup() {
|
||||
}
|
||||
@Override
|
||||
public DynLongHashMap getCTMTextureCache() {
|
||||
return null;
|
||||
}
|
||||
@Override
|
||||
public int[] getLightingTable() {
|
||||
return lightingTable;
|
||||
}
|
||||
@Override
|
||||
public void setLastBlockState(DynmapBlockState new_lastbs) {
|
||||
}
|
||||
}
|
||||
|
||||
private class OurBiomeShaderState extends OurShaderState {
|
||||
private OurBiomeShaderState(MapIterator mapiter, HDMap map, MapChunkCache cache) {
|
||||
super(mapiter, map, cache);
|
||||
}
|
||||
@Override
|
||||
protected Color[] getBlockColors(DynmapBlockState blk) {
|
||||
BiomeMap bio = mapiter.getBiome();
|
||||
if(bio != null)
|
||||
return colorScheme.biomecolors[bio.ordinal()];
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private class OurBiomeRainfallShaderState extends OurShaderState {
|
||||
private OurBiomeRainfallShaderState(MapIterator mapiter, HDMap map, MapChunkCache cache) {
|
||||
super(mapiter, map, cache);
|
||||
}
|
||||
@Override
|
||||
protected Color[] getBlockColors(DynmapBlockState blk) {
|
||||
return colorScheme.getRainColor(mapiter.getBiome().getRainfall());
|
||||
}
|
||||
}
|
||||
|
||||
private class OurBiomeTempShaderState extends OurShaderState {
|
||||
private OurBiomeTempShaderState(MapIterator mapiter, HDMap map, MapChunkCache cache) {
|
||||
super(mapiter, map, cache);
|
||||
}
|
||||
@Override
|
||||
protected Color[] getBlockColors(DynmapBlockState blk) {
|
||||
return colorScheme.getTempColor(mapiter.getBiome().getTemperature());
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get renderer state object for use rendering a tile
|
||||
* @param map - map being rendered
|
||||
* @param cache - chunk cache containing data for tile to be rendered
|
||||
* @param mapiter - iterator used when traversing rays in tile
|
||||
* @param scale - scale of the perspecitve
|
||||
* @return state object to use for all rays in tile
|
||||
*/
|
||||
@Override
|
||||
public HDShaderState getStateInstance(HDMap map, MapChunkCache cache, MapIterator mapiter, int scale) {
|
||||
switch(biomecolored) {
|
||||
case NONE:
|
||||
return new OurShaderState(mapiter, map, cache);
|
||||
case BIOME:
|
||||
return new OurBiomeShaderState(mapiter, map, cache);
|
||||
case RAINFALL:
|
||||
return new OurBiomeRainfallShaderState(mapiter, map, cache);
|
||||
case TEMPERATURE:
|
||||
return new OurBiomeTempShaderState(mapiter, map, cache);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/* Add shader's contributions to JSON for map object */
|
||||
public void addClientConfiguration(JSONObject mapObject) {
|
||||
s(mapObject, "shader", name);
|
||||
}
|
||||
@Override
|
||||
public void exportAsMaterialLibrary(DynmapCommandSender sender, OBJExport out) throws IOException {
|
||||
throw new IOException("Export unsupported");
|
||||
}
|
||||
private static final String[] nulllist = new String[0];
|
||||
@Override
|
||||
public String[] getCurrentBlockMaterials(DynmapBlockState blk, MapIterator mapiter, int[] txtidx,
|
||||
BlockStep[] steps) {
|
||||
return nulllist;
|
||||
}
|
||||
}
|
||||
39
DynmapCore/src/main/java/org/dynmap/hdmap/HDBlockModel.java
Normal file
39
DynmapCore/src/main/java/org/dynmap/hdmap/HDBlockModel.java
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import java.util.BitSet;
|
||||
|
||||
import org.dynmap.renderer.DynmapBlockState;
|
||||
|
||||
public abstract class HDBlockModel {
|
||||
private String blockset;
|
||||
/**
|
||||
* Block definition - positions correspond to Bukkit coordinates (+X is south, +Y is up, +Z is west)
|
||||
* @param bstate - block state
|
||||
* @param databits - bitmap of block data bits matching this model (bit N is set if data=N would match)
|
||||
* @param blockset - ID of block definition set
|
||||
*/
|
||||
protected HDBlockModel(DynmapBlockState bstate, BitSet databits, String blockset) {
|
||||
this.blockset = blockset;
|
||||
DynmapBlockState bblk = bstate.baseState;
|
||||
if (bblk.isNotAir()) {
|
||||
for (int i = 0; i < bblk.getStateCount(); i++) {
|
||||
if (databits.isEmpty() || databits.get(i)) {
|
||||
DynmapBlockState bs = bblk.getState(i);
|
||||
HDBlockModel prev = HDBlockModels.models_by_id_data[bs.globalStateIndex];
|
||||
HDBlockModels.models_by_id_data[bs.globalStateIndex] = this;
|
||||
if((prev != null) && (prev != this)) {
|
||||
prev.removed(bs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public String getBlockSet() {
|
||||
return blockset;
|
||||
}
|
||||
|
||||
public abstract int getTextureCount();
|
||||
|
||||
public void removed(DynmapBlockState blk) {
|
||||
}
|
||||
}
|
||||
1253
DynmapCore/src/main/java/org/dynmap/hdmap/HDBlockModels.java
Normal file
1253
DynmapCore/src/main/java/org/dynmap/hdmap/HDBlockModels.java
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,49 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import java.util.BitSet;
|
||||
|
||||
import org.dynmap.renderer.DynmapBlockState;
|
||||
import org.dynmap.utils.PatchDefinition;
|
||||
|
||||
public class HDBlockPatchModel extends HDBlockModel {
|
||||
/* Patch model specific attributes */
|
||||
private PatchDefinition[] patches;
|
||||
private final int max_texture;
|
||||
/**
|
||||
* Block definition - positions correspond to Bukkit coordinates (+X is south, +Y is up, +Z is west)
|
||||
* (for patch models)
|
||||
* @param bs - block state
|
||||
* @param databits - bitmap of block data bits matching this model (bit N is set if data=N would match)
|
||||
* @param patches - list of patches (surfaces composing model)
|
||||
* @param blockset - ID of set of blocks defining model
|
||||
*/
|
||||
public HDBlockPatchModel(DynmapBlockState bs, BitSet databits, PatchDefinition[] patches, String blockset) {
|
||||
super(bs, databits, blockset);
|
||||
this.patches = patches;
|
||||
int max = 0;
|
||||
for(int i = 0; i < patches.length; i++) {
|
||||
if((patches[i] != null) && (patches[i].textureindex > max))
|
||||
max = patches[i].textureindex;
|
||||
}
|
||||
this.max_texture = max + 1;
|
||||
}
|
||||
/**
|
||||
* Get patches for block model (if patch model)
|
||||
* @return patches for model
|
||||
*/
|
||||
public final PatchDefinition[] getPatches() {
|
||||
return patches;
|
||||
}
|
||||
/**
|
||||
* Set patches for block
|
||||
*/
|
||||
public final void setPatches(PatchDefinition[] p) {
|
||||
patches = p;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTextureCount() {
|
||||
return max_texture;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.BitSet;
|
||||
import java.util.Map;
|
||||
|
||||
import org.dynmap.Log;
|
||||
import org.dynmap.hdmap.TexturePack.BlockTransparency;
|
||||
import org.dynmap.hdmap.TexturePack.ColorizingData;
|
||||
import org.dynmap.renderer.CustomColorMultiplier;
|
||||
import org.dynmap.renderer.DynmapBlockState;
|
||||
|
||||
public class HDBlockStateTextureMap {
|
||||
|
||||
private static HDBlockStateTextureMap[] texmaps = new HDBlockStateTextureMap[DynmapBlockState.getGlobalIndexMax()]; // List of texture maps, indexed by global state index
|
||||
|
||||
int faces[]; /* texture index of image for each face (indexed by BlockStep.ordinal() OR patch index) */
|
||||
final byte[] layers; /* If layered, each index corresponds to faces index, and value is index of next layer */
|
||||
final private String blockset;
|
||||
final int colorMult;
|
||||
final CustomColorMultiplier custColorMult;
|
||||
final boolean stdrotate; // Marked for corrected to proper : stdrot=true
|
||||
final private Integer colorMapping; // If non-null, color mapping texture
|
||||
final BlockTransparency trans;
|
||||
|
||||
public static final HDBlockStateTextureMap BLANK = new HDBlockStateTextureMap();
|
||||
|
||||
// Default to a blank mapping
|
||||
HDBlockStateTextureMap() {
|
||||
blockset = null;
|
||||
colorMult = 0;
|
||||
custColorMult = null;
|
||||
faces = new int[] { TexturePack.TILEINDEX_BLANK, TexturePack.TILEINDEX_BLANK, TexturePack.TILEINDEX_BLANK, TexturePack.TILEINDEX_BLANK, TexturePack.TILEINDEX_BLANK, TexturePack.TILEINDEX_BLANK };
|
||||
layers = null;
|
||||
stdrotate = true;
|
||||
colorMapping = null;
|
||||
trans = BlockTransparency.TRANSPARENT;
|
||||
}
|
||||
// Create block state map with given attributes
|
||||
public HDBlockStateTextureMap(int[] faces, byte[] layers, int colorMult, CustomColorMultiplier custColorMult, String blockset, boolean stdrot, Integer colorIndex, BlockTransparency trans) {
|
||||
this.faces = faces;
|
||||
this.layers = layers;
|
||||
this.colorMult = colorMult;
|
||||
this.custColorMult = custColorMult;
|
||||
this.blockset = blockset;
|
||||
this.stdrotate = stdrot;
|
||||
this.colorMapping = colorIndex;
|
||||
this.trans = trans;
|
||||
}
|
||||
|
||||
// Shallow copy state from another state map
|
||||
public HDBlockStateTextureMap(HDBlockStateTextureMap map, BlockTransparency bt) {
|
||||
this.faces = map.faces;
|
||||
this.layers = map.layers;
|
||||
this.blockset = map.blockset;
|
||||
this.colorMult = map.colorMult;
|
||||
this.custColorMult = map.custColorMult;
|
||||
this.stdrotate = map.stdrotate;
|
||||
this.colorMapping = map.colorMapping;
|
||||
if (bt != null)
|
||||
this.trans = bt;
|
||||
else
|
||||
this.trans = map.trans;
|
||||
}
|
||||
|
||||
// Get texture index for given face
|
||||
public int getIndexForFace(int face) {
|
||||
if ((faces != null) && (faces.length > face))
|
||||
return faces[face];
|
||||
return TexturePack.TILEINDEX_BLANK;
|
||||
}
|
||||
|
||||
public void resizeFaces(int cnt) {
|
||||
int[] newfaces = new int[cnt];
|
||||
System.arraycopy(faces, 0, newfaces, 0, faces.length);
|
||||
for(int i = faces.length; i < cnt; i++) {
|
||||
newfaces[i] = TexturePack.TILEINDEX_BLANK;
|
||||
}
|
||||
faces = newfaces;
|
||||
}
|
||||
|
||||
// Add block state to table, with given block IDs and state indexes
|
||||
public void addToTable(Map<DynmapBlockState, BitSet> states, int lineNum) {
|
||||
/* Add entries to lookup table */
|
||||
for (DynmapBlockState baseblk : states.keySet()) {
|
||||
if (baseblk.isNotAir()) {
|
||||
BitSet stateidx = states.get(baseblk);
|
||||
for (int stateid = stateidx.nextSetBit(0); stateid >= 0; stateid = stateidx.nextSetBit(stateid+1)) {
|
||||
DynmapBlockState bs = baseblk.getState(stateid);
|
||||
if (bs.isAir()) {
|
||||
Log.warning("Invalid texture block state: " + baseblk.blockName + ":" + stateid);
|
||||
continue;
|
||||
}
|
||||
if ((this.blockset != null) && (this.blockset.equals("core") == false)) {
|
||||
HDBlockModels.resetIfNotBlockSet(bs, this.blockset);
|
||||
}
|
||||
copyToStateIndex(bs, this, null);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Log.warning("Invalid texture block name: " + baseblk.blockName + ((lineNum > 0) ? "(line " + lineNum + ")" : ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final void resize(int newend) {
|
||||
if (newend < texmaps.length) return;
|
||||
HDBlockStateTextureMap[] newm = new HDBlockStateTextureMap[newend+1];
|
||||
System.arraycopy(texmaps, 0, newm, 0, texmaps.length);
|
||||
Arrays.fill(newm, texmaps.length, newm.length, HDBlockStateTextureMap.BLANK);
|
||||
texmaps = newm;
|
||||
}
|
||||
|
||||
// Initialize/reset block texture table
|
||||
public static void initializeTable() {
|
||||
Arrays.fill(texmaps, HDBlockStateTextureMap.BLANK);
|
||||
}
|
||||
|
||||
// Lookup records by block state
|
||||
public static final HDBlockStateTextureMap getByBlockState(DynmapBlockState blk) {
|
||||
HDBlockStateTextureMap m = HDBlockStateTextureMap.BLANK;
|
||||
try {
|
||||
m = texmaps[blk.globalStateIndex];
|
||||
} catch (ArrayIndexOutOfBoundsException x) {
|
||||
resize(blk.globalStateIndex);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
// Copy given block state to given state index
|
||||
public static void copyToStateIndex(DynmapBlockState blk, HDBlockStateTextureMap map, TexturePack.BlockTransparency trans) {
|
||||
resize(blk.globalStateIndex);
|
||||
if (trans == null) {
|
||||
trans = map.trans;
|
||||
}
|
||||
// Force waterloogged blocks to use SEMITRANSPARENT (same as water)
|
||||
if ((trans == TexturePack.BlockTransparency.TRANSPARENT) && blk.isWaterlogged()) {
|
||||
trans = TexturePack.BlockTransparency.SEMITRANSPARENT;
|
||||
}
|
||||
texmaps[blk.globalStateIndex] = new HDBlockStateTextureMap(map, trans);
|
||||
}
|
||||
// Copy textures from source block ID to destination
|
||||
public static void remapTexture(String dest, String src) {
|
||||
DynmapBlockState dblk = DynmapBlockState.getBaseStateByName(dest);
|
||||
DynmapBlockState sblk = DynmapBlockState.getBaseStateByName(src);
|
||||
int scnt = sblk.getStateCount();
|
||||
for (int i = 0; i < dblk.getStateCount(); i++) {
|
||||
int didx = dblk.getState(i).globalStateIndex;
|
||||
int sidx = sblk.getState(i % scnt).globalStateIndex;
|
||||
texmaps[didx] = new HDBlockStateTextureMap(texmaps[sidx], null);
|
||||
}
|
||||
}
|
||||
// Get by global state index
|
||||
public static HDBlockStateTextureMap getByGlobalIndex(int gidx) {
|
||||
HDBlockStateTextureMap m = HDBlockStateTextureMap.BLANK;
|
||||
try {
|
||||
m = texmaps[gidx];
|
||||
} catch (ArrayIndexOutOfBoundsException x) {
|
||||
resize(gidx);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
// Get state by index
|
||||
public final HDBlockStateTextureMap getStateMap(DynmapBlockState blk, int stateid) {
|
||||
return getByGlobalIndex(blk.getState(stateid).globalStateIndex);
|
||||
}
|
||||
// Get transparency for given block ID
|
||||
public static BlockTransparency getTransparency(DynmapBlockState blk) {
|
||||
BlockTransparency trans = BlockTransparency.OPAQUE;
|
||||
try {
|
||||
trans = texmaps[blk.globalStateIndex].trans;
|
||||
} catch (ArrayIndexOutOfBoundsException x) {
|
||||
resize(blk.globalStateIndex);
|
||||
}
|
||||
return trans;
|
||||
}
|
||||
// Build copy of block colorization data
|
||||
public static ColorizingData getColorizingData() {
|
||||
ColorizingData map = new ColorizingData();
|
||||
for (int j = 0; j < texmaps.length; j++) {
|
||||
if (texmaps[j].colorMapping != null) {
|
||||
map.setBlkStateValue(DynmapBlockState.getStateByGlobalIndex(j), texmaps[j].colorMapping);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import java.util.BitSet;
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.dynmap.renderer.DynmapBlockState;
|
||||
|
||||
public class HDBlockVolumetricModel extends HDBlockModel {
|
||||
/* Volumetric model specific attributes */
|
||||
private long blockflags[];
|
||||
private int nativeres;
|
||||
private HashMap<Integer, short[]> scaledblocks;
|
||||
/**
|
||||
* Block definition - positions correspond to Bukkit coordinates (+X is south, +Y is up, +Z is west)
|
||||
* (for volumetric models)
|
||||
* @param bs - block state
|
||||
* @param databits - bitmap of block data bits matching this model (bit N is set if data=N would match)
|
||||
* @param nativeres - native subblocks per edge of cube (up to 64)
|
||||
* @param blockflags - array of native^2 long integers representing volume of block (bit X of element (nativeres*Y+Z) is set if that subblock is filled)
|
||||
* if array is short, other elements area are assumed to be zero (fills from bottom of block up)
|
||||
* @param blockset - ID of set of blocks defining model
|
||||
*/
|
||||
public HDBlockVolumetricModel(DynmapBlockState bs, BitSet databits, int nativeres, long[] blockflags, String blockset) {
|
||||
super(bs, databits, blockset);
|
||||
|
||||
this.nativeres = nativeres;
|
||||
this.blockflags = new long[nativeres * nativeres];
|
||||
System.arraycopy(blockflags, 0, this.blockflags, 0, blockflags.length);
|
||||
}
|
||||
/**
|
||||
* Test if given native block is filled (for volumetric model)
|
||||
*
|
||||
* @param x - X coordinate
|
||||
* @param y - Y coordinate
|
||||
* @param z - Z coordinate
|
||||
* @return true if set, false if not
|
||||
*/
|
||||
public final boolean isSubblockSet(int x, int y, int z) {
|
||||
return ((blockflags[nativeres*y+z] & (1 << x)) != 0);
|
||||
}
|
||||
/**
|
||||
* Set subblock value (for volumetric model)
|
||||
*
|
||||
* @param x - X coordinate
|
||||
* @param y - Y coordinate
|
||||
* @param z - Z coordinate
|
||||
* @param isset - true = set, false = clear
|
||||
*/
|
||||
public final void setSubblock(int x, int y, int z, boolean isset) {
|
||||
if(isset)
|
||||
blockflags[nativeres*y+z] |= (1 << x);
|
||||
else
|
||||
blockflags[nativeres*y+z] &= ~(1 << x);
|
||||
}
|
||||
/**
|
||||
* Get scaled map of block: will return array of alpha levels, corresponding to how much of the
|
||||
* scaled subblocks are occupied by the original blocks (indexed by Y*res*res + Z*res + X)
|
||||
* @param res - requested scale (res subblocks per edge of block)
|
||||
* @return array of alpha values (0-255), corresponding to resXresXres subcubes of block
|
||||
*/
|
||||
public short[] getScaledMap(int res) {
|
||||
if(scaledblocks == null) { scaledblocks = new HashMap<Integer, short[]>(); }
|
||||
short[] map = scaledblocks.get(Integer.valueOf(res));
|
||||
if(map == null) {
|
||||
map = new short[res*res*res];
|
||||
if(res == nativeres) {
|
||||
for(int i = 0; i < blockflags.length; i++) {
|
||||
for(int j = 0; j < nativeres; j++) {
|
||||
if((blockflags[i] & (1 << j)) != 0)
|
||||
map[res*i+j] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* If scaling from smaller sub-blocks to larger, each subblock contributes to 1-2 blocks
|
||||
* on each axis: need to calculate crossovers for each, and iterate through smaller
|
||||
* blocks to accumulate contributions
|
||||
*/
|
||||
else if(res > nativeres) {
|
||||
int weights[] = new int[res];
|
||||
int offsets[] = new int[res];
|
||||
/* LCM of resolutions is used as length of line (res * nativeres)
|
||||
* Each native block is (res) long, each scaled block is (nativeres) long
|
||||
* Each scaled block overlaps 1 or 2 native blocks: starting with native block 'offsets[]' with
|
||||
* 'weights[]' of its (res) width in the first, and the rest in the second
|
||||
*/
|
||||
for(int v = 0, idx = 0; v < res*nativeres; v += nativeres, idx++) {
|
||||
offsets[idx] = (v/res); /* Get index of the first native block we draw from */
|
||||
if((v+nativeres-1)/res == offsets[idx]) { /* If scaled block ends in same native block */
|
||||
weights[idx] = nativeres;
|
||||
}
|
||||
else { /* Else, see how much is in first one */
|
||||
weights[idx] = (offsets[idx] + res) - v;
|
||||
weights[idx] = (offsets[idx]*res + res) - v;
|
||||
}
|
||||
}
|
||||
/* Now, use weights and indices to fill in scaled map */
|
||||
for(int y = 0, off = 0; y < res; y++) {
|
||||
int ind_y = offsets[y];
|
||||
int wgt_y = weights[y];
|
||||
for(int z = 0; z < res; z++) {
|
||||
int ind_z = offsets[z];
|
||||
int wgt_z = weights[z];
|
||||
for(int x = 0; x < res; x++, off++) {
|
||||
int ind_x = offsets[x];
|
||||
int wgt_x = weights[x];
|
||||
int raw_w = 0;
|
||||
for(int xx = 0; xx < 2; xx++) {
|
||||
int wx = (xx==0)?wgt_x:(nativeres-wgt_x);
|
||||
if(wx == 0) continue;
|
||||
for(int yy = 0; yy < 2; yy++) {
|
||||
int wy = (yy==0)?wgt_y:(nativeres-wgt_y);
|
||||
if(wy == 0) continue;
|
||||
for(int zz = 0; zz < 2; zz++) {
|
||||
int wz = (zz==0)?wgt_z:(nativeres-wgt_z);
|
||||
if(wz == 0) continue;
|
||||
if(isSubblockSet(ind_x+xx, ind_y+yy, ind_z+zz)) {
|
||||
raw_w += wx*wy*wz;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
map[off] = (short)((255*raw_w) / (nativeres*nativeres*nativeres));
|
||||
if(map[off] > 255) map[off] = 255;
|
||||
if(map[off] < 0) map[off] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else { /* nativeres > res */
|
||||
int weights[] = new int[nativeres];
|
||||
int offsets[] = new int[nativeres];
|
||||
/* LCM of resolutions is used as length of line (res * nativeres)
|
||||
* Each native block is (res) long, each scaled block is (nativeres) long
|
||||
* Each native block overlaps 1 or 2 scaled blocks: starting with scaled block 'offsets[]' with
|
||||
* 'weights[]' of its (res) width in the first, and the rest in the second
|
||||
*/
|
||||
for(int v = 0, idx = 0; v < res*nativeres; v += res, idx++) {
|
||||
offsets[idx] = (v/nativeres); /* Get index of the first scaled block we draw to */
|
||||
if((v+res-1)/nativeres == offsets[idx]) { /* If native block ends in same scaled block */
|
||||
weights[idx] = res;
|
||||
}
|
||||
else { /* Else, see how much is in first one */
|
||||
weights[idx] = (offsets[idx]*nativeres + nativeres) - v;
|
||||
}
|
||||
}
|
||||
/* Now, use weights and indices to fill in scaled map */
|
||||
long accum[] = new long[map.length];
|
||||
for(int y = 0; y < nativeres; y++) {
|
||||
int ind_y = offsets[y];
|
||||
int wgt_y = weights[y];
|
||||
for(int z = 0; z < nativeres; z++) {
|
||||
int ind_z = offsets[z];
|
||||
int wgt_z = weights[z];
|
||||
for(int x = 0; x < nativeres; x++) {
|
||||
if(isSubblockSet(x, y, z)) {
|
||||
int ind_x = offsets[x];
|
||||
int wgt_x = weights[x];
|
||||
for(int xx = 0; xx < 2; xx++) {
|
||||
int wx = (xx==0)?wgt_x:(res-wgt_x);
|
||||
if(wx == 0) continue;
|
||||
for(int yy = 0; yy < 2; yy++) {
|
||||
int wy = (yy==0)?wgt_y:(res-wgt_y);
|
||||
if(wy == 0) continue;
|
||||
for(int zz = 0; zz < 2; zz++) {
|
||||
int wz = (zz==0)?wgt_z:(res-wgt_z);
|
||||
if(wz == 0) continue;
|
||||
accum[(ind_y+yy)*res*res + (ind_z+zz)*res + (ind_x+xx)] +=
|
||||
wx*wy*wz;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for(int i = 0; i < map.length; i++) {
|
||||
map[i] = (short)(accum[i]*255/nativeres/nativeres/nativeres);
|
||||
if(map[i] > 255) map[i] = 255;
|
||||
if(map[i] < 0) map[i] = 0;
|
||||
}
|
||||
}
|
||||
scaledblocks.put(Integer.valueOf(res), map);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
@Override
|
||||
public int getTextureCount() {
|
||||
return 6;
|
||||
}
|
||||
}
|
||||
|
||||
30
DynmapCore/src/main/java/org/dynmap/hdmap/HDLighting.java
Normal file
30
DynmapCore/src/main/java/org/dynmap/hdmap/HDLighting.java
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import org.dynmap.Color;
|
||||
import org.dynmap.DynmapWorld;
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
public interface HDLighting {
|
||||
/* Get lighting name */
|
||||
String getName();
|
||||
/* Apply lighting to given pixel colors (1 outcolor if normal, 2 if night/day) */
|
||||
void applyLighting(HDPerspectiveState ps, HDShaderState ss, Color incolor, Color[] outcolor);
|
||||
/* Test if Biome Data is needed for this renderer */
|
||||
boolean isBiomeDataNeeded();
|
||||
/* Test if raw biome temperature/rainfall data is needed */
|
||||
boolean isRawBiomeDataNeeded();
|
||||
/* Test if highest block Y data is needed */
|
||||
boolean isHightestBlockYDataNeeded();
|
||||
/* Tet if block type data needed */
|
||||
boolean isBlockTypeDataNeeded();
|
||||
/* Test if night/day is enabled for this renderer */
|
||||
boolean isNightAndDayEnabled();
|
||||
/* Test if sky light level needed */
|
||||
boolean isSkyLightLevelNeeded();
|
||||
/* Test if emitted light level needed */
|
||||
boolean isEmittedLightLevelNeeded();
|
||||
/* Add shader's contributions to JSON for map object */
|
||||
void addClientConfiguration(JSONObject mapObject);
|
||||
/* Get brightness table for given world */
|
||||
int[] getBrightnessTable(DynmapWorld world);
|
||||
}
|
||||
483
DynmapCore/src/main/java/org/dynmap/hdmap/HDMap.java
Normal file
483
DynmapCore/src/main/java/org/dynmap/hdmap/HDMap.java
Normal file
|
|
@ -0,0 +1,483 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import static org.dynmap.JSONUtils.a;
|
||||
import static org.dynmap.JSONUtils.s;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.dynmap.Client;
|
||||
import org.dynmap.ConfigurationNode;
|
||||
import org.dynmap.DynmapChunk;
|
||||
import org.dynmap.DynmapCore;
|
||||
import org.dynmap.DynmapWorld;
|
||||
import org.dynmap.Log;
|
||||
import org.dynmap.MapManager;
|
||||
import org.dynmap.MapTile;
|
||||
import org.dynmap.MapType;
|
||||
import org.dynmap.storage.MapStorage;
|
||||
import org.dynmap.storage.MapStorageTile;
|
||||
import org.dynmap.storage.MapStorageTileEnumCB;
|
||||
import org.dynmap.utils.TileFlags;
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
public class HDMap extends MapType {
|
||||
|
||||
private String name;
|
||||
private String prefix;
|
||||
private HDPerspective perspective;
|
||||
private HDShader shader;
|
||||
private HDLighting lighting;
|
||||
// private ConfigurationNode configuration;
|
||||
private int mapzoomout;
|
||||
private String imgfmtstring;
|
||||
private MapType.ImageFormat imgformat;
|
||||
private int bgcolornight;
|
||||
private int bgcolorday;
|
||||
private int tilescale;
|
||||
private String title;
|
||||
private String icon;
|
||||
private String bg_cfg;
|
||||
private String bg_day_cfg;
|
||||
private String bg_night_cfg;
|
||||
private String append_to_world;
|
||||
private int mapzoomin;
|
||||
private int boostzoom;
|
||||
public DynmapCore core;
|
||||
|
||||
public static final String IMGFORMAT_PNG = "png";
|
||||
public static final String IMGFORMAT_JPG = "jpg";
|
||||
|
||||
|
||||
public HDMap(DynmapCore core, ConfigurationNode configuration) {
|
||||
this.core = core;
|
||||
name = configuration.getString("name", null);
|
||||
if(name == null) {
|
||||
Log.severe("HDMap missing required attribute 'name' - disabled");
|
||||
return;
|
||||
}
|
||||
String perspectiveid = configuration.getString("perspective", "default");
|
||||
perspective = MapManager.mapman.hdmapman.perspectives.get(perspectiveid);
|
||||
if(perspective == null) {
|
||||
/* Try to use default */
|
||||
perspective = MapManager.mapman.hdmapman.perspectives.get("default");
|
||||
if(perspective == null) {
|
||||
Log.severe("HDMap '"+name+"' loaded invalid perspective '" + perspectiveid + "' - map disabled");
|
||||
name = null;
|
||||
return;
|
||||
}
|
||||
else {
|
||||
Log.severe("HDMap '"+name+"' loaded invalid perspective '" + perspectiveid + "' - using 'default' perspective");
|
||||
}
|
||||
}
|
||||
String shaderid = configuration.getString("shader", "default");
|
||||
shader = MapManager.mapman.hdmapman.shaders.get(shaderid);
|
||||
if(shader == null) {
|
||||
shader = MapManager.mapman.hdmapman.shaders.get("default");
|
||||
if(shader == null) {
|
||||
Log.severe("HDMap '"+name+"' loading invalid shader '" + shaderid + "' - map disabled");
|
||||
name = null;
|
||||
return;
|
||||
}
|
||||
else {
|
||||
Log.severe("HDMap '"+name+"' loading invalid shader '" + shaderid + "' - using 'default' shader");
|
||||
}
|
||||
}
|
||||
String lightingid = configuration.getString("lighting", "default");
|
||||
lighting = MapManager.mapman.hdmapman.lightings.get(lightingid);
|
||||
if(lighting == null) {
|
||||
lighting = MapManager.mapman.hdmapman.lightings.get("default");
|
||||
if(lighting == null) {
|
||||
Log.severe("HDMap '"+name+"' loading invalid lighting '" + lighting + "' - map disabled");
|
||||
name = null;
|
||||
return;
|
||||
}
|
||||
else {
|
||||
Log.severe("HDMap '"+name+"' loading invalid lighting '" + lighting + "' - using 'default' lighting");
|
||||
}
|
||||
}
|
||||
prefix = configuration.getString("prefix", name);
|
||||
|
||||
/* Compute extra zoom outs needed for this map */
|
||||
double scale = perspective.getScale();
|
||||
mapzoomout = 0;
|
||||
while(scale >= 1.0) {
|
||||
mapzoomout++;
|
||||
scale = scale / 2.0;
|
||||
}
|
||||
imgfmtstring = configuration.getString("image-format", "default");
|
||||
if(imgfmtstring.equals("default")) {
|
||||
imgformat = ImageFormat.fromID(core.getDefImageFormat());
|
||||
}
|
||||
else {
|
||||
imgformat = ImageFormat.fromID(imgfmtstring);
|
||||
}
|
||||
if(imgformat == null) {
|
||||
Log.severe("HDMap '"+name+"' set invalid image-format: " + imgfmtstring);
|
||||
imgformat = ImageFormat.FORMAT_PNG;
|
||||
}
|
||||
/* Get color info */
|
||||
String c = configuration.getString("background");
|
||||
if(c != null) {
|
||||
bgcolorday = bgcolornight = parseColor(c);
|
||||
}
|
||||
c = configuration.getString("backgroundday");
|
||||
if(c != null) {
|
||||
bgcolorday = parseColor(c);
|
||||
}
|
||||
c = configuration.getString("backgroundnight");
|
||||
if(c != null) {
|
||||
bgcolornight = parseColor(c);
|
||||
}
|
||||
if(imgformat != ImageFormat.FORMAT_PNG) { /* If JPG, set background color opacity */
|
||||
bgcolorday |= 0xFF000000;
|
||||
bgcolornight |= 0xFF000000;
|
||||
}
|
||||
this.title = configuration.getString("title", name);
|
||||
this.icon = configuration.getString("icon");
|
||||
this.bg_cfg = configuration.getString("background");
|
||||
this.bg_day_cfg = configuration.getString("backgroundday");
|
||||
this.bg_night_cfg = configuration.getString("backgroundnight");
|
||||
this.mapzoomin = configuration.getInteger("mapzoomin", 2);
|
||||
this.mapzoomout = configuration.getInteger("mapzoomout", this.mapzoomout);
|
||||
this.boostzoom = configuration.getInteger("boostzoom", 0);
|
||||
this.tilescale = configuration.getInteger("tilescale", core.getMapManager().getDefaultTileScale()); // 0 = 128, 1 = 256, ...
|
||||
if (this.tilescale <= 0) this.tilescale = 0;
|
||||
if (this.tilescale >= 4) this.tilescale = 4; // Limit to 2k x 2k
|
||||
if(this.boostzoom < 0) this.boostzoom = 0;
|
||||
if(this.boostzoom > 3) this.boostzoom = 3;
|
||||
// Map zoom in must be at least as big as boost zoom
|
||||
if (this.boostzoom > this.mapzoomin) {
|
||||
this.mapzoomin = this.boostzoom;
|
||||
}
|
||||
this.append_to_world = configuration.getString("append_to_world", "");
|
||||
setProtected(configuration.getBoolean("protected", false));
|
||||
setTileUpdateDelay(configuration.getInteger("tileupdatedelay", -1));
|
||||
setReadOnly(configuration.getBoolean("readonly", false));
|
||||
}
|
||||
|
||||
public ConfigurationNode saveConfiguration() {
|
||||
ConfigurationNode cn = super.saveConfiguration();
|
||||
cn.put("title", title);
|
||||
if(icon != null)
|
||||
cn.put("icon", icon);
|
||||
cn.put("prefix", prefix);
|
||||
if(perspective != null)
|
||||
cn.put("perspective", perspective.getName());
|
||||
if(shader != null)
|
||||
cn.put("shader", shader.getName());
|
||||
if(lighting != null)
|
||||
cn.put("lighting", lighting.getName());
|
||||
cn.put("image-format", imgfmtstring);
|
||||
cn.put("mapzoomin", mapzoomin);
|
||||
cn.put("mapzoomout", mapzoomout);
|
||||
cn.put("boostzoom", boostzoom);
|
||||
cn.put("tilescale", tilescale);
|
||||
if(bg_cfg != null)
|
||||
cn.put("background", bg_cfg);
|
||||
if(bg_day_cfg != null)
|
||||
cn.put("backgroundday", bg_day_cfg);
|
||||
if(bg_night_cfg != null)
|
||||
cn.put("backgroundnight", bg_night_cfg);
|
||||
cn.put("append_to_world", append_to_world);
|
||||
cn.put("protected", isProtected());
|
||||
cn.put("readonly", isReadOnly());
|
||||
if(this.tileupdatedelay > 0) {
|
||||
cn.put("tileupdatedelay", this.tileupdatedelay);
|
||||
}
|
||||
return cn;
|
||||
}
|
||||
|
||||
public final HDShader getShader() { return shader; }
|
||||
public final HDPerspective getPerspective() { return perspective; }
|
||||
public final HDLighting getLighting() { return lighting; }
|
||||
public final int getBoostZoom() { return boostzoom; }
|
||||
@Override
|
||||
public final int getTileSize() { return 128 << tilescale; }
|
||||
public final int getTileScale() { return tilescale; }
|
||||
|
||||
@Override
|
||||
public List<TileFlags.TileCoord> getTileCoords(DynmapWorld w, int x, int y, int z) {
|
||||
return perspective.getTileCoords(w, x, y, z, tilescale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TileFlags.TileCoord> getTileCoords(DynmapWorld w, int minx, int miny, int minz, int maxx, int maxy, int maxz) {
|
||||
return perspective.getTileCoords(w, minx, miny, minz, maxx, maxy, maxz, tilescale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapTile[] getAdjecentTiles(MapTile tile) {
|
||||
return perspective.getAdjecentTiles(tile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DynmapChunk> getRequiredChunks(MapTile tile) {
|
||||
return perspective.getRequiredChunks(tile);
|
||||
}
|
||||
|
||||
/* Return number of zoom levels needed by this map (before extra levels from extrazoomout) */
|
||||
public int getMapZoomOutLevels() {
|
||||
return mapzoomout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPrefix() {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
/* Get maps rendered concurrently with this map in this world */
|
||||
public List<MapType> getMapsSharingRender(DynmapWorld w) {
|
||||
ArrayList<MapType> maps = new ArrayList<MapType>();
|
||||
for(MapType mt : w.maps) {
|
||||
if(mt instanceof HDMap) {
|
||||
HDMap hdmt = (HDMap)mt;
|
||||
if((hdmt.perspective == this.perspective) && (hdmt.boostzoom == this.boostzoom)) { /* Same perspective */
|
||||
maps.add(hdmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
return maps;
|
||||
}
|
||||
|
||||
/* Get names of maps rendered concurrently with this map type in this world */
|
||||
public List<String> getMapNamesSharingRender(DynmapWorld w) {
|
||||
ArrayList<String> lst = new ArrayList<String>();
|
||||
for(MapType mt : w.maps) {
|
||||
if(mt instanceof HDMap) {
|
||||
HDMap hdmt = (HDMap)mt;
|
||||
if((hdmt.perspective == this.perspective) && (hdmt.boostzoom == this.boostzoom)) { /* Same perspective */
|
||||
if(hdmt.lighting.isNightAndDayEnabled())
|
||||
lst.add(hdmt.getName() + "(night/day)");
|
||||
else
|
||||
lst.add(hdmt.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
return lst;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageFormat getImageFormat() { return imgformat; }
|
||||
|
||||
@Override
|
||||
public void buildClientConfiguration(JSONObject worldObject, DynmapWorld world) {
|
||||
JSONObject o = new JSONObject();
|
||||
s(o, "type", "HDMapType");
|
||||
s(o, "name", name);
|
||||
s(o, "title", title);
|
||||
s(o, "icon", icon);
|
||||
s(o, "prefix", prefix);
|
||||
s(o, "background", bg_cfg);
|
||||
s(o, "backgroundday", bg_day_cfg);
|
||||
s(o, "backgroundnight", bg_night_cfg);
|
||||
s(o, "bigmap", true);
|
||||
s(o, "mapzoomout", (world.getExtraZoomOutLevels()+mapzoomout));
|
||||
s(o, "mapzoomin", mapzoomin);
|
||||
s(o, "boostzoom", boostzoom);
|
||||
s(o, "tilescale", tilescale);
|
||||
s(o, "protected", isProtected());
|
||||
s(o, "image-format", imgformat.getFileExt());
|
||||
if(append_to_world.length() > 0)
|
||||
s(o, "append_to_world", append_to_world);
|
||||
perspective.addClientConfiguration(o);
|
||||
shader.addClientConfiguration(o);
|
||||
lighting.addClientConfiguration(o);
|
||||
|
||||
a(worldObject, "maps", o);
|
||||
|
||||
}
|
||||
|
||||
private static int parseColor(String c) {
|
||||
int v = 0;
|
||||
if(c.startsWith("#")) {
|
||||
c = c.substring(1);
|
||||
if(c.length() == 3) { /* #rgb */
|
||||
try {
|
||||
v = Integer.valueOf(c, 16);
|
||||
} catch (NumberFormatException nfx) {
|
||||
return 0;
|
||||
}
|
||||
v = 0xFF000000 | ((v & 0xF00) << 12) | ((v & 0x0F0) << 8) | ((v & 0x00F) << 4);
|
||||
}
|
||||
else if(c.length() == 6) { /* #rrggbb */
|
||||
try {
|
||||
v = Integer.valueOf(c, 16);
|
||||
} catch (NumberFormatException nfx) {
|
||||
return 0;
|
||||
}
|
||||
v = 0xFF000000 | (v & 0xFFFFFF);
|
||||
}
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
public int getBackgroundARGBDay() {
|
||||
return bgcolorday;
|
||||
}
|
||||
|
||||
public int getBackgroundARGBNight() {
|
||||
return bgcolornight;
|
||||
}
|
||||
|
||||
public void purgeOldTiles(final DynmapWorld world, final TileFlags rendered) {
|
||||
final MapStorage ms = world.getMapStorage();
|
||||
ms.enumMapTiles(world, this, new MapStorageTileEnumCB() {
|
||||
@Override
|
||||
public void tileFound(MapStorageTile tile, ImageEncoding fmt) {
|
||||
if (fmt != getImageFormat().getEncoding()) { // Wrong format? toss it
|
||||
/* Otherwise, delete tile */
|
||||
tile.delete();
|
||||
}
|
||||
else if (tile.zoom == 1) { // First tier zoom? sensitive to newly rendered tiles
|
||||
// If any were rendered, already triggered (and still needed
|
||||
if (rendered.getFlag(tile.x, tile.y) || rendered.getFlag(tile.x+1, tile.y) ||
|
||||
rendered.getFlag(tile.x, tile.y-1) || rendered.getFlag(tile.x+1, tile.y-1)) {
|
||||
return;
|
||||
}
|
||||
tile.enqueueZoomOutUpdate();
|
||||
}
|
||||
else if (tile.zoom == 0) {
|
||||
if (rendered.getFlag(tile.x, tile.y)) { /* If we rendered this tile, its good */
|
||||
return;
|
||||
}
|
||||
/* Otherwise, delete tile */
|
||||
tile.delete();
|
||||
/* Push updates, clear hash code, and signal zoom tile update */
|
||||
MapManager.mapman.pushUpdate(world, new Client.Tile(tile.getURI()));
|
||||
tile.enqueueZoomOutUpdate();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
public int getMapZoomIn() {
|
||||
return mapzoomin;
|
||||
}
|
||||
public String getIcon() {
|
||||
return (icon == null)?"":icon;
|
||||
}
|
||||
|
||||
public boolean setPrefix(String s) {
|
||||
if(!s.equals(prefix)) {
|
||||
prefix = s;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean setTitle(String s) {
|
||||
if(!s.equals(title)) {
|
||||
title = s;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public boolean setAppendToWorld(String s) {
|
||||
if(!s.equals(append_to_world)) {
|
||||
append_to_world = s;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public String getAppendToWorld() {
|
||||
return append_to_world;
|
||||
}
|
||||
|
||||
public boolean setMapZoomIn(int mzi) {
|
||||
if(mzi != mapzoomin) {
|
||||
mapzoomin = mzi;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public boolean setMapZoomOut(int mzi) {
|
||||
if(mzi != mapzoomout) {
|
||||
mapzoomout = mzi;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public boolean setBoostZoom(int mzi) {
|
||||
if(mzi != this.boostzoom) {
|
||||
this.boostzoom = mzi;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public boolean setTileScale(int mzi) {
|
||||
if(mzi != this.tilescale) {
|
||||
this.tilescale = mzi;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public boolean setPerspective(HDPerspective p) {
|
||||
if(perspective != p) {
|
||||
perspective = p;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public boolean setShader(HDShader p) {
|
||||
if(shader != p) {
|
||||
shader = p;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public boolean setLighting(HDLighting p) {
|
||||
if(lighting != p) {
|
||||
lighting = p;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public boolean setImageFormatSetting(String f) {
|
||||
if(imgfmtstring.equals(f) == false) {
|
||||
MapType.ImageFormat newfmt;
|
||||
if(f.equals("default"))
|
||||
newfmt = MapType.ImageFormat.fromID(core.getDefImageFormat());
|
||||
else
|
||||
newfmt = MapType.ImageFormat.fromID(f);
|
||||
if(newfmt != null) {
|
||||
imgformat = newfmt;
|
||||
imgfmtstring = f;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public String getImageFormatSetting() {
|
||||
return imgfmtstring;
|
||||
}
|
||||
public boolean setIcon(String v) {
|
||||
if("".equals(v)) v = null;
|
||||
icon = v;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMapTiles(List<MapTile> list, DynmapWorld w, int tx, int ty) {
|
||||
list.add(new HDMapTile(w, this.perspective, tx, ty, boostzoom, tilescale));
|
||||
}
|
||||
|
||||
private static final ImageVariant[] dayVariant = { ImageVariant.STANDARD, ImageVariant.DAY };
|
||||
|
||||
@Override
|
||||
public ImageVariant[] getVariants() {
|
||||
if (lighting.isNightAndDayEnabled())
|
||||
return dayVariant;
|
||||
return super.getVariants();
|
||||
}
|
||||
|
||||
}
|
||||
196
DynmapCore/src/main/java/org/dynmap/hdmap/HDMapManager.java
Normal file
196
DynmapCore/src/main/java/org/dynmap/hdmap/HDMapManager.java
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
|
||||
import org.dynmap.ConfigurationNode;
|
||||
import org.dynmap.DynmapCore;
|
||||
import org.dynmap.DynmapWorld;
|
||||
import org.dynmap.Log;
|
||||
import org.dynmap.MapManager;
|
||||
import org.dynmap.MapType;
|
||||
import org.dynmap.utils.MapChunkCache;
|
||||
import org.dynmap.utils.MapIterator;
|
||||
|
||||
public class HDMapManager {
|
||||
public HashMap<String, HDShader> shaders = new HashMap<String, HDShader>();
|
||||
public HashMap<String, HDPerspective> perspectives = new HashMap<String, HDPerspective>();
|
||||
public HashMap<String, HDLighting> lightings = new HashMap<String, HDLighting>();
|
||||
public HashSet<HDMap> maps = new HashSet<HDMap>();
|
||||
public HashMap<String, ArrayList<HDMap>> maps_by_world_perspective = new HashMap<String, ArrayList<HDMap>>();
|
||||
|
||||
public void loadHDShaders(DynmapCore core) {
|
||||
Log.verboseinfo("Loading shaders...");
|
||||
/* Update mappings, if needed */
|
||||
TexturePack.handleBlockAlias();
|
||||
|
||||
File f = new File(core.getDataFolder(), "shaders.txt");
|
||||
if(!core.updateUsingDefaultResource("/shaders.txt", f, "shaders")) {
|
||||
return;
|
||||
}
|
||||
ConfigurationNode shadercfg = new ConfigurationNode(f);
|
||||
shadercfg.load();
|
||||
|
||||
for(HDShader shader : shadercfg.<HDShader>createInstances("shaders", new Class<?>[] { DynmapCore.class }, new Object[] { core })) {
|
||||
if(shader.getName() == null) continue;
|
||||
shaders.put(shader.getName(), shader);
|
||||
}
|
||||
/* Load custom shaders, if file is defined - or create empty one if not */
|
||||
f = new File(core.getDataFolder(), "custom-shaders.txt");
|
||||
core.createDefaultFileFromResource("/custom-shaders.txt", f);
|
||||
if(f.exists()) {
|
||||
ConfigurationNode customshadercfg = new ConfigurationNode(f);
|
||||
customshadercfg.load();
|
||||
for(HDShader shader : customshadercfg.<HDShader>createInstances("shaders", new Class<?>[] { DynmapCore.class }, new Object[] { core })) {
|
||||
if(shader.getName() == null) continue;
|
||||
shaders.put(shader.getName(), shader);
|
||||
}
|
||||
}
|
||||
Log.info("Loaded " + shaders.size() + " shaders.");
|
||||
}
|
||||
|
||||
public void loadHDPerspectives(DynmapCore core) {
|
||||
Log.verboseinfo("Loading perspectives...");
|
||||
// Update mappings, if needed
|
||||
HDBlockModels.handleBlockAlias();
|
||||
|
||||
File f = new File(core.getDataFolder(), "perspectives.txt");
|
||||
if(!core.updateUsingDefaultResource("/perspectives.txt", f, "perspectives")) {
|
||||
return;
|
||||
}
|
||||
ConfigurationNode perspectivecfg = new ConfigurationNode(f);
|
||||
perspectivecfg.load();
|
||||
for(HDPerspective perspective : perspectivecfg.<HDPerspective>createInstances("perspectives", new Class<?>[] { DynmapCore.class }, new Object[] { core })) {
|
||||
if(perspective.getName() == null) continue;
|
||||
perspectives.put(perspective.getName(), perspective);
|
||||
}
|
||||
/* Load custom perspectives, if file is defined - or create empty one if not */
|
||||
f = new File(core.getDataFolder(), "custom-perspectives.txt");
|
||||
core.createDefaultFileFromResource("/custom-perspectives.txt", f);
|
||||
if(f.exists()) {
|
||||
perspectivecfg = new ConfigurationNode(f);
|
||||
perspectivecfg.load();
|
||||
for(HDPerspective perspective : perspectivecfg.<HDPerspective>createInstances("perspectives", new Class<?>[] { DynmapCore.class }, new Object[] { core })) {
|
||||
if(perspective.getName() == null) continue;
|
||||
perspectives.put(perspective.getName(), perspective);
|
||||
}
|
||||
}
|
||||
Log.info("Loaded " + perspectives.size() + " perspectives.");
|
||||
}
|
||||
|
||||
public void loadHDLightings(DynmapCore core) {
|
||||
Log.verboseinfo("Loading lightings...");
|
||||
File f = new File(core.getDataFolder(), "lightings.txt");
|
||||
if(!core.updateUsingDefaultResource("/lightings.txt", f, "lightings")) {
|
||||
return;
|
||||
}
|
||||
ConfigurationNode lightingcfg = new ConfigurationNode(f);
|
||||
lightingcfg.load();
|
||||
|
||||
for(HDLighting lighting : lightingcfg.<HDLighting>createInstances("lightings", new Class<?>[] { DynmapCore.class }, new Object[] { core })) {
|
||||
if(lighting.getName() == null) continue;
|
||||
lightings.put(lighting.getName(), lighting);
|
||||
}
|
||||
/* Load custom lightings, if file is defined - or create empty one if not */
|
||||
f = new File(core.getDataFolder(), "custom-lightings.txt");
|
||||
core.createDefaultFileFromResource("/custom-lightings.txt", f);
|
||||
if(f.exists()) {
|
||||
lightingcfg = new ConfigurationNode(f);
|
||||
lightingcfg.load();
|
||||
for(HDLighting lighting : lightingcfg.<HDLighting>createInstances("lightings", new Class<?>[] { DynmapCore.class }, new Object[] { core })) {
|
||||
if(lighting.getName() == null) continue;
|
||||
lightings.put(lighting.getName(), lighting);
|
||||
}
|
||||
}
|
||||
Log.info("Loaded " + lightings.size() + " lightings.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize shader states for all shaders for given tile
|
||||
*
|
||||
* @param tile - tile to init
|
||||
* @param cache - chunk cache
|
||||
* @param mapiter - map iterator
|
||||
* @param mapname - map name
|
||||
* @param scale - map scale
|
||||
* @return array of shader states for all associated shaders
|
||||
*/
|
||||
public HDShaderState[] getShaderStateForTile(HDMapTile tile, MapChunkCache cache, MapIterator mapiter, String mapname, int scale) {
|
||||
DynmapWorld w = MapManager.mapman.worldsLookup.get(tile.getDynmapWorld().getName());
|
||||
if(w == null) {
|
||||
return new HDShaderState[0];
|
||||
}
|
||||
ArrayList<HDShaderState> shaders = new ArrayList<HDShaderState>();
|
||||
for(MapType map : w.maps) {
|
||||
if(map instanceof HDMap) {
|
||||
HDMap hdmap = (HDMap)map;
|
||||
// If same perspective, at same scale (tile and boost), render together
|
||||
if((hdmap.getPerspective() == tile.perspective) && (hdmap.getBoostZoom() == tile.boostzoom) && (hdmap.getTileScale() == tile.tilescale)) {
|
||||
/* If limited to one map, and this isn't it, skip */
|
||||
if((mapname != null) && (!hdmap.getName().equals(mapname)))
|
||||
continue;
|
||||
|
||||
// Maps can be set to read-only, which means they don't get re-rendered
|
||||
if (map.isReadOnly()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
shaders.add(hdmap.getShader().getStateInstance(hdmap, cache, mapiter, scale));
|
||||
}
|
||||
}
|
||||
}
|
||||
return shaders.toArray(new HDShaderState[shaders.size()]);
|
||||
}
|
||||
|
||||
private static final int BIOMEDATAFLAG = 0;
|
||||
private static final int HIGHESTZFLAG = 1;
|
||||
private static final int RAWBIOMEFLAG = 2;
|
||||
private static final int BLOCKTYPEFLAG = 3;
|
||||
|
||||
public boolean isBiomeDataNeeded(HDMapTile t) {
|
||||
return getCachedFlags(t)[BIOMEDATAFLAG];
|
||||
}
|
||||
|
||||
public boolean isHightestBlockYDataNeeded(HDMapTile t) {
|
||||
return getCachedFlags(t)[HIGHESTZFLAG];
|
||||
}
|
||||
|
||||
public boolean isRawBiomeDataNeeded(HDMapTile t) {
|
||||
return getCachedFlags(t)[RAWBIOMEFLAG];
|
||||
}
|
||||
|
||||
public boolean isBlockTypeDataNeeded(HDMapTile t) {
|
||||
return getCachedFlags(t)[BLOCKTYPEFLAG];
|
||||
}
|
||||
|
||||
private HashMap<String, boolean[]> cached_data_flags_by_world_perspective = new HashMap<String, boolean[]>();
|
||||
|
||||
private boolean[] getCachedFlags(HDMapTile t) {
|
||||
String w = t.getDynmapWorld().getName();
|
||||
String k = w + "/" + t.perspective.getName();
|
||||
boolean[] flags = cached_data_flags_by_world_perspective.get(k);
|
||||
if(flags != null)
|
||||
return flags;
|
||||
flags = new boolean[4];
|
||||
cached_data_flags_by_world_perspective.put(k, flags);
|
||||
DynmapWorld dw = MapManager.mapman.worldsLookup.get(w);
|
||||
if(dw == null) return flags;
|
||||
|
||||
for(MapType map : dw.maps) {
|
||||
if(map instanceof HDMap) {
|
||||
HDMap hdmap = (HDMap)map;
|
||||
if(hdmap.getPerspective() == t.perspective) {
|
||||
HDShader sh = hdmap.getShader();
|
||||
HDLighting lt = hdmap.getLighting();
|
||||
flags[BIOMEDATAFLAG] |= sh.isBiomeDataNeeded() | lt.isBiomeDataNeeded();
|
||||
flags[HIGHESTZFLAG] |= sh.isHightestBlockYDataNeeded() | lt.isHightestBlockYDataNeeded();
|
||||
flags[RAWBIOMEFLAG] |= sh.isRawBiomeDataNeeded() | lt.isRawBiomeDataNeeded();
|
||||
flags[BLOCKTYPEFLAG] |= sh.isBlockTypeDataNeeded() | lt.isBlockTypeDataNeeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
}
|
||||
116
DynmapCore/src/main/java/org/dynmap/hdmap/HDMapTile.java
Normal file
116
DynmapCore/src/main/java/org/dynmap/hdmap/HDMapTile.java
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import org.dynmap.DynmapChunk;
|
||||
import org.dynmap.DynmapWorld;
|
||||
import org.dynmap.MapManager;
|
||||
|
||||
import java.util.List;
|
||||
import org.dynmap.MapTile;
|
||||
import org.dynmap.utils.MapChunkCache;
|
||||
|
||||
public class HDMapTile extends MapTile {
|
||||
public final HDPerspective perspective;
|
||||
public final int tx, ty; /* Tile X and Tile Y are in tile coordinates (pixels/tile-size) */
|
||||
public final int boostzoom;
|
||||
public final int tilescale;
|
||||
|
||||
public HDMapTile(DynmapWorld world, HDPerspective perspective, int tx, int ty, int boostzoom, int tilescale) {
|
||||
super(world);
|
||||
this.perspective = perspective;
|
||||
this.tx = tx;
|
||||
this.ty = ty;
|
||||
this.boostzoom = boostzoom;
|
||||
this.tilescale = tilescale;
|
||||
}
|
||||
|
||||
// Used for restore of saved pending renders
|
||||
public HDMapTile(DynmapWorld world, String parm) throws Exception {
|
||||
super(world);
|
||||
|
||||
String[] parms = parm.split(",");
|
||||
if(parms.length < 3) throw new Exception("wrong parameter count");
|
||||
this.tx = Integer.parseInt(parms[0]);
|
||||
this.ty = Integer.parseInt(parms[1]);
|
||||
this.perspective = MapManager.mapman.hdmapman.perspectives.get(parms[2]);
|
||||
if(this.perspective == null) throw new Exception("invalid perspective");
|
||||
if(parms.length > 3)
|
||||
this.boostzoom = Integer.parseInt(parms[3]);
|
||||
else
|
||||
this.boostzoom = 0;
|
||||
if (parms.length > 4) {
|
||||
this.tilescale = Integer.parseInt(parms[4]);
|
||||
}
|
||||
else {
|
||||
this.tilescale = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String saveTileData() {
|
||||
return String.format("%d,%d,%s,%d,%d", tx, ty, perspective.getName(), boostzoom, tilescale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int h = perspective.hashCode();
|
||||
h = h * 31 + world.hashCode();
|
||||
h = h * 31 + tx;
|
||||
h = h * 31 + ty;
|
||||
h = h * 31 + boostzoom;
|
||||
h = h * 31 + tilescale;
|
||||
return h;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof HDMapTile) {
|
||||
return equals((HDMapTile) obj);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean equals(HDMapTile o) {
|
||||
return o.tx == tx && o.ty == ty && (perspective == o.perspective) && (o.world == world) && (o.boostzoom == boostzoom) && (o.tilescale == tilescale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return world.getName() + ":" + perspective.getName() + "," + tx + "," + ty + ":" + boostzoom + ":" + tilescale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTileSize() { return 128 << tilescale; }
|
||||
|
||||
@Override
|
||||
public boolean isBiomeDataNeeded() { return MapManager.mapman.hdmapman.isBiomeDataNeeded(this); }
|
||||
|
||||
@Override
|
||||
public boolean isHightestBlockYDataNeeded() { return MapManager.mapman.hdmapman.isHightestBlockYDataNeeded(this); }
|
||||
|
||||
@Override
|
||||
public boolean isRawBiomeDataNeeded() { return MapManager.mapman.hdmapman.isRawBiomeDataNeeded(this); }
|
||||
|
||||
@Override
|
||||
public boolean isBlockTypeDataNeeded() { return MapManager.mapman.hdmapman.isBlockTypeDataNeeded(this); }
|
||||
|
||||
@Override
|
||||
public boolean render(MapChunkCache cache, String mapname) {
|
||||
return perspective.render(cache, this, mapname);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DynmapChunk> getRequiredChunks() {
|
||||
return perspective.getRequiredChunks(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapTile[] getAdjecentTiles() {
|
||||
return perspective.getAdjecentTiles(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int tileOrdinalX() { return tx; }
|
||||
@Override
|
||||
public int tileOrdinalY() { return ty; }
|
||||
|
||||
}
|
||||
40
DynmapCore/src/main/java/org/dynmap/hdmap/HDPerspective.java
Normal file
40
DynmapCore/src/main/java/org/dynmap/hdmap/HDPerspective.java
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.dynmap.DynmapChunk;
|
||||
import org.dynmap.DynmapWorld;
|
||||
import org.dynmap.MapTile;
|
||||
import org.dynmap.utils.MapChunkCache;
|
||||
import org.dynmap.utils.TileFlags;
|
||||
import org.dynmap.utils.Vector3D;
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
public interface HDPerspective {
|
||||
/* Get name of perspective */
|
||||
String getName();
|
||||
/* Get tiles invalidated by change at given location */
|
||||
List<TileFlags.TileCoord> getTileCoords(DynmapWorld w, int x, int y, int z, int tilescale);
|
||||
/* Get tiles invalidated by change at given volume, defined by 2 opposite corner locations */
|
||||
List<TileFlags.TileCoord> getTileCoords(DynmapWorld w, int minx, int miny, int minz, int maxx, int maxy, int maxz, int tilescale);
|
||||
/* Get tiles adjacent to given tile */
|
||||
MapTile[] getAdjecentTiles(MapTile tile);
|
||||
/* Get chunks needed for given tile */
|
||||
List<DynmapChunk> getRequiredChunks(MapTile tile);
|
||||
/* Render given tile */
|
||||
boolean render(MapChunkCache cache, HDMapTile tile, String mapname);
|
||||
|
||||
public boolean isBiomeDataNeeded();
|
||||
public boolean isHightestBlockYDataNeeded();
|
||||
public boolean isRawBiomeDataNeeded();
|
||||
public boolean isBlockTypeDataNeeded();
|
||||
|
||||
double getScale();
|
||||
int getModelScale();
|
||||
|
||||
public void addClientConfiguration(JSONObject mapObject);
|
||||
|
||||
public void transformWorldToMapCoord(Vector3D input, Vector3D rslt);
|
||||
|
||||
public int hashCode();
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import org.dynmap.utils.MapIterator;
|
||||
import org.dynmap.renderer.DynmapBlockState;
|
||||
import org.dynmap.utils.BlockStep;
|
||||
import org.dynmap.utils.Vector3D;
|
||||
import org.dynmap.utils.LightLevels;
|
||||
|
||||
public interface HDPerspectiveState {
|
||||
/**
|
||||
* Get light levels - only available if shader requested it
|
||||
* @param ll - light levels (filled in when returned)
|
||||
*/
|
||||
void getLightLevels(LightLevels ll);
|
||||
/**
|
||||
* Get sky light level - only available if shader requested it
|
||||
* @param step - last step
|
||||
* @param ll - light levels (filled in when returned)
|
||||
*/
|
||||
void getLightLevelsAtStep(BlockStep step, LightLevels ll);
|
||||
/**
|
||||
* Get current block state
|
||||
* @return block
|
||||
*/
|
||||
DynmapBlockState getBlockState();
|
||||
/**
|
||||
* Get direction of last block step
|
||||
* @return last step direction
|
||||
*/
|
||||
BlockStep getLastBlockStep();
|
||||
/**
|
||||
* Get perspective scale
|
||||
* @return scale
|
||||
*/
|
||||
double getScale();
|
||||
/**
|
||||
* Get start of current ray, in world coordinates
|
||||
* @return start of ray
|
||||
*/
|
||||
Vector3D getRayStart();
|
||||
/**
|
||||
* Get end of current ray, in world coordinates
|
||||
* @return end of ray
|
||||
*/
|
||||
Vector3D getRayEnd();
|
||||
/**
|
||||
* Get pixel X coordinate
|
||||
* @return x coordinate
|
||||
*/
|
||||
int getPixelX();
|
||||
/**
|
||||
* Get pixel Y coordinate
|
||||
* @return y coordinate
|
||||
*/
|
||||
int getPixelY();
|
||||
/**
|
||||
* Get current patch shade setting (false = no shadows)
|
||||
*/
|
||||
boolean getShade();
|
||||
/**
|
||||
* Return submodel alpha value (-1 if no submodel rendered)
|
||||
* @return alpha value
|
||||
*/
|
||||
int getSubmodelAlpha();
|
||||
/**
|
||||
* Return subblock coordinates of current ray position
|
||||
* @return coordinates of ray
|
||||
*/
|
||||
int[] getSubblockCoord();
|
||||
/**
|
||||
* Check if point is on face
|
||||
*/
|
||||
boolean isOnFace();
|
||||
/**
|
||||
* Get map iterator
|
||||
* @return iterator
|
||||
*/
|
||||
MapIterator getMapIterator();
|
||||
/**
|
||||
* Get current texture index
|
||||
* @return texture index
|
||||
*/
|
||||
int getTextureIndex();
|
||||
/**
|
||||
* Get current U of patch intercept
|
||||
* @return U in patch
|
||||
*/
|
||||
double getPatchU();
|
||||
/**
|
||||
* Get current V of patch intercept
|
||||
* @return V in patch
|
||||
*/
|
||||
double getPatchV();
|
||||
/**
|
||||
* Light level cache
|
||||
* @param idx - index of light level (0-3)
|
||||
* @return light level
|
||||
*/
|
||||
LightLevels getCachedLightLevels(int idx);
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import org.dynmap.renderer.DynmapBlockState;
|
||||
import org.dynmap.utils.PatchDefinition;
|
||||
|
||||
public class HDScaledBlockModels {
|
||||
private short[][] modelvectors;
|
||||
// These are scale invariant - only need once
|
||||
private static PatchDefinition[][] patches;
|
||||
private static CustomBlockModel[] custom;
|
||||
|
||||
public HDScaledBlockModels(int scale) {
|
||||
short[][] blockmodels = new short[DynmapBlockState.getGlobalIndexMax()][];
|
||||
PatchDefinition[][] newpatches = null;
|
||||
if (patches == null) {
|
||||
newpatches = new PatchDefinition[DynmapBlockState.getGlobalIndexMax()][];
|
||||
patches = newpatches;
|
||||
}
|
||||
CustomBlockModel[] newcustom = null;
|
||||
if (custom == null) {
|
||||
newcustom = new CustomBlockModel[DynmapBlockState.getGlobalIndexMax()];
|
||||
custom = newcustom;
|
||||
}
|
||||
for(int gidx = 0; gidx < HDBlockModels.models_by_id_data.length; gidx++) {
|
||||
HDBlockModel m = HDBlockModels.models_by_id_data[gidx];
|
||||
if(m == null) continue;
|
||||
|
||||
if(m instanceof HDBlockVolumetricModel) {
|
||||
HDBlockVolumetricModel vm = (HDBlockVolumetricModel)m;
|
||||
short[] smod = vm.getScaledMap(scale);
|
||||
/* See if scaled model is full block : much faster to not use it if it is */
|
||||
if(smod != null) {
|
||||
boolean keep = false;
|
||||
for(int i = 0; (!keep) && (i < smod.length); i++) {
|
||||
if(smod[i] == 0) keep = true;
|
||||
}
|
||||
if(keep) {
|
||||
blockmodels[gidx] = smod;
|
||||
}
|
||||
else {
|
||||
blockmodels[gidx] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(m instanceof HDBlockPatchModel) {
|
||||
if (newpatches != null) {
|
||||
HDBlockPatchModel pm = (HDBlockPatchModel)m;
|
||||
newpatches[gidx] = pm.getPatches();
|
||||
}
|
||||
}
|
||||
else if(m instanceof CustomBlockModel) {
|
||||
if (newcustom != null) {
|
||||
CustomBlockModel cbm = (CustomBlockModel)m;
|
||||
newcustom[gidx] = cbm;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.modelvectors = blockmodels;
|
||||
}
|
||||
|
||||
public final short[] getScaledModel(DynmapBlockState blk) {
|
||||
int idx = blk.globalStateIndex;
|
||||
if(idx >= modelvectors.length) {
|
||||
short[][] newmodels = new short[idx + 1][];
|
||||
System.arraycopy(modelvectors, 0, newmodels, 0, modelvectors.length);
|
||||
modelvectors = newmodels;
|
||||
return null;
|
||||
}
|
||||
return modelvectors[idx];
|
||||
}
|
||||
public PatchDefinition[] getPatchModel(DynmapBlockState blk) {
|
||||
int idx = blk.globalStateIndex;
|
||||
if(idx >= patches.length) {
|
||||
PatchDefinition[][] newpatches = new PatchDefinition[idx + 1][];
|
||||
System.arraycopy(patches, 0, newpatches, 0, patches.length);
|
||||
patches = newpatches;
|
||||
return null;
|
||||
}
|
||||
return patches[idx];
|
||||
}
|
||||
|
||||
public CustomBlockModel getCustomBlockModel(DynmapBlockState blk) {
|
||||
int idx = blk.globalStateIndex;
|
||||
if(idx >= custom.length) {
|
||||
CustomBlockModel[] newcustom = new CustomBlockModel[idx + 1];
|
||||
System.arraycopy(custom, 0, newcustom, 0, custom.length);
|
||||
custom = newcustom;
|
||||
return null;
|
||||
}
|
||||
return custom[idx];
|
||||
}
|
||||
}
|
||||
43
DynmapCore/src/main/java/org/dynmap/hdmap/HDShader.java
Normal file
43
DynmapCore/src/main/java/org/dynmap/hdmap/HDShader.java
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.dynmap.common.DynmapCommandSender;
|
||||
import org.dynmap.exporter.OBJExport;
|
||||
import org.dynmap.renderer.DynmapBlockState;
|
||||
import org.dynmap.utils.BlockStep;
|
||||
import org.dynmap.utils.MapChunkCache;
|
||||
import org.dynmap.utils.MapIterator;
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
public interface HDShader {
|
||||
/* Get shader name */
|
||||
String getName();
|
||||
/**
|
||||
* Get renderer state object for use rendering a tile
|
||||
* @param map - map being rendered
|
||||
* @param cache - chunk cache containing data for tile to be rendered
|
||||
* @param mapiter - iterator used when traversing rays in tile
|
||||
* @param scale - scale
|
||||
* @return state object to use for all rays in tile
|
||||
*/
|
||||
HDShaderState getStateInstance(HDMap map, MapChunkCache cache, MapIterator mapiter, int scale);
|
||||
/* Test if Biome Data is needed for this renderer */
|
||||
boolean isBiomeDataNeeded();
|
||||
/* Test if raw biome temperature/rainfall data is needed */
|
||||
boolean isRawBiomeDataNeeded();
|
||||
/* Test if highest block Y data is needed */
|
||||
boolean isHightestBlockYDataNeeded();
|
||||
/* Tet if block type data needed */
|
||||
boolean isBlockTypeDataNeeded();
|
||||
/* Test if sky light level needed */
|
||||
boolean isSkyLightLevelNeeded();
|
||||
/* Test if emitted light level needed */
|
||||
boolean isEmittedLightLevelNeeded();
|
||||
/* Add shader's contributions to JSON for map object */
|
||||
void addClientConfiguration(JSONObject mapObject);
|
||||
/* Export shader as material library */
|
||||
void exportAsMaterialLibrary(DynmapCommandSender sender, OBJExport exp) throws IOException;
|
||||
/* Get materials for each patch on the current block (with +N for N*90 degree rotations) */
|
||||
String[] getCurrentBlockMaterials(DynmapBlockState blk, MapIterator mapiter, int[] txtidx, BlockStep[] steps);
|
||||
}
|
||||
67
DynmapCore/src/main/java/org/dynmap/hdmap/HDShaderState.java
Normal file
67
DynmapCore/src/main/java/org/dynmap/hdmap/HDShaderState.java
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import org.dynmap.Color;
|
||||
import org.dynmap.renderer.DynmapBlockState;
|
||||
import org.dynmap.utils.DynLongHashMap;
|
||||
|
||||
/**
|
||||
* This interface is used to define the operational state of a renderer during raytracing
|
||||
* All method should be considered performance critical
|
||||
*/
|
||||
public interface HDShaderState {
|
||||
/**
|
||||
* Get our shader
|
||||
* @return shader
|
||||
*/
|
||||
HDShader getShader();
|
||||
/**
|
||||
* Get our lighting
|
||||
* @return lighting
|
||||
*/
|
||||
HDLighting getLighting();
|
||||
/**
|
||||
* Get our map
|
||||
* @return map
|
||||
*/
|
||||
HDMap getMap();
|
||||
/**
|
||||
* Reset renderer state for new ray - passes in pixel coordinate for ray
|
||||
* @param ps - perspective state
|
||||
*/
|
||||
void reset(HDPerspectiveState ps);
|
||||
/**
|
||||
* Process next ray step - called for each block on route
|
||||
* @param ps - perspective state
|
||||
* @return true if ray is done, false if ray needs to continue
|
||||
*/
|
||||
boolean processBlock(HDPerspectiveState ps);
|
||||
/**
|
||||
* Ray ended - used to report that ray has exited map (called if renderer has not reported complete)
|
||||
* @param ps - perspective state
|
||||
*/
|
||||
void rayFinished(HDPerspectiveState ps);
|
||||
/**
|
||||
* Get result color - get resulting color for ray
|
||||
* @param c - object to store color value in
|
||||
* @param index - index of color to request (renderer specific - 0=default, 1=day for night/day renderer
|
||||
*/
|
||||
void getRayColor(Color c, int index);
|
||||
/**
|
||||
* Clean up state object - called after last ray completed
|
||||
*/
|
||||
void cleanup();
|
||||
/**
|
||||
* Get CTM texture cache
|
||||
* @return texture cache
|
||||
*/
|
||||
DynLongHashMap getCTMTextureCache();
|
||||
/**
|
||||
* Get lighting table
|
||||
* @return array of lighting values
|
||||
*/
|
||||
int[] getLightingTable();
|
||||
/**
|
||||
* Update last block state (called before moving to next block)
|
||||
*/
|
||||
void setLastBlockState(DynmapBlockState new_lastbs);
|
||||
}
|
||||
244
DynmapCore/src/main/java/org/dynmap/hdmap/InhabitedHDShader.java
Normal file
244
DynmapCore/src/main/java/org/dynmap/hdmap/InhabitedHDShader.java
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import static org.dynmap.JSONUtils.s;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import org.dynmap.Color;
|
||||
import org.dynmap.ConfigurationNode;
|
||||
import org.dynmap.DynmapCore;
|
||||
import org.dynmap.Log;
|
||||
import org.dynmap.MapManager;
|
||||
import org.dynmap.common.DynmapCommandSender;
|
||||
import org.dynmap.exporter.OBJExport;
|
||||
import org.dynmap.renderer.DynmapBlockState;
|
||||
import org.dynmap.utils.BlockStep;
|
||||
import org.dynmap.utils.DynLongHashMap;
|
||||
import org.dynmap.utils.MapChunkCache;
|
||||
import org.dynmap.utils.MapIterator;
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
public class InhabitedHDShader implements HDShader {
|
||||
private final String name;
|
||||
private final long filllevel[]; /* Values for colors */
|
||||
private final Color fillcolor[];
|
||||
|
||||
private Color readColor(String id, ConfigurationNode cfg) {
|
||||
String lclr = cfg.getString(id, null);
|
||||
if((lclr != null) && (lclr.startsWith("#"))) {
|
||||
try {
|
||||
int c = Integer.parseInt(lclr.substring(1), 16);
|
||||
return new Color((c>>16)&0xFF, (c>>8)&0xFF, c&0xFF);
|
||||
} catch (NumberFormatException nfx) {
|
||||
Log.severe("Invalid color value: " + lclr + " for '" + id + "'");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public InhabitedHDShader(DynmapCore core, ConfigurationNode configuration) {
|
||||
name = (String) configuration.get("name");
|
||||
HashMap<Long, Color> map = new HashMap<Long, Color>();
|
||||
for (String key : configuration.keySet()) {
|
||||
if (key.startsWith("color")) {
|
||||
try {
|
||||
long val = Long.parseLong(key.substring(5));
|
||||
Color clr = readColor(key, configuration);
|
||||
map.put(val, clr);
|
||||
} catch (NumberFormatException nfx) {
|
||||
}
|
||||
}
|
||||
}
|
||||
TreeSet<Long> keys = new TreeSet<Long>(map.keySet());
|
||||
filllevel = new long[keys.size()];
|
||||
fillcolor = new Color[keys.size()];
|
||||
int idx = 0;
|
||||
for (Long k : keys) {
|
||||
filllevel[idx] = k;
|
||||
fillcolor[idx] = map.get(k);
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBiomeDataNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRawBiomeDataNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHightestBlockYDataNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBlockTypeDataNeeded() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSkyLightLevelNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmittedLightLevelNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
private class OurShaderState implements HDShaderState {
|
||||
private Color color[];
|
||||
private Color c;
|
||||
protected HDMap map;
|
||||
private HDLighting lighting;
|
||||
final int[] lightingTable;
|
||||
|
||||
private OurShaderState(MapIterator mapiter, HDMap map, MapChunkCache cache, int scale) {
|
||||
this.map = map;
|
||||
this.lighting = map.getLighting();
|
||||
if(lighting.isNightAndDayEnabled()) {
|
||||
color = new Color[] { new Color(), new Color() };
|
||||
}
|
||||
else {
|
||||
color = new Color[] { new Color() };
|
||||
}
|
||||
c = new Color();
|
||||
if (MapManager.mapman.useBrightnessTable()) {
|
||||
lightingTable = cache.getWorld().getBrightnessTable();
|
||||
}
|
||||
else {
|
||||
lightingTable = null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get our shader
|
||||
*/
|
||||
public HDShader getShader() {
|
||||
return InhabitedHDShader.this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get our map
|
||||
*/
|
||||
public HDMap getMap() {
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get our lighting
|
||||
*/
|
||||
public HDLighting getLighting() {
|
||||
return lighting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset renderer state for new ray
|
||||
*/
|
||||
public void reset(HDPerspectiveState ps) {
|
||||
for(int i = 0; i < color.length; i++)
|
||||
color[i].setTransparent();
|
||||
}
|
||||
/**
|
||||
* Process next ray step - called for each block on route
|
||||
* @return true if ray is done, false if ray needs to continue
|
||||
*/
|
||||
public boolean processBlock(HDPerspectiveState ps) {
|
||||
if (ps.getBlockState().isAir()) {
|
||||
return false;
|
||||
}
|
||||
long ts = ps.getMapIterator().getInhabitedTicks() / 1200; // Get time, in minutes
|
||||
// Find top of range
|
||||
boolean match = false;
|
||||
for (int i = 0; i < filllevel.length; i++) {
|
||||
if (ts < filllevel[i]) { // Found it
|
||||
if (i > 0) { // Middle? Interpolate
|
||||
int interp = (int) ((256 * (ts - filllevel[i-1])) / (filllevel[i] - filllevel[i-1]));
|
||||
int red = (interp * fillcolor[i].getRed()) + ((256 - interp) * fillcolor[i-1].getRed());
|
||||
int green = (interp * fillcolor[i].getGreen()) + ((256 - interp) * fillcolor[i-1].getGreen());
|
||||
int blue = (interp * fillcolor[i].getBlue()) + ((256 - interp) * fillcolor[i-1].getBlue());
|
||||
c.setRGBA(red / 256, green / 256, blue / 256, 255);
|
||||
}
|
||||
else { // Else, use color
|
||||
c.setColor(fillcolor[i]);
|
||||
}
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!match) {
|
||||
c.setColor(fillcolor[fillcolor.length-1]);
|
||||
}
|
||||
|
||||
/* Handle light level, if needed */
|
||||
lighting.applyLighting(ps, this, c, color);
|
||||
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Ray ended - used to report that ray has exited map (called if renderer has not reported complete)
|
||||
*/
|
||||
public void rayFinished(HDPerspectiveState ps) {
|
||||
}
|
||||
/**
|
||||
* Get result color - get resulting color for ray
|
||||
* @param c - object to store color value in
|
||||
* @param index - index of color to request (renderer specific - 0=default, 1=day for night/day renderer
|
||||
*/
|
||||
public void getRayColor(Color c, int index) {
|
||||
c.setColor(color[index]);
|
||||
}
|
||||
/**
|
||||
* Clean up state object - called after last ray completed
|
||||
*/
|
||||
public void cleanup() {
|
||||
}
|
||||
@Override
|
||||
public DynLongHashMap getCTMTextureCache() {
|
||||
return null;
|
||||
}
|
||||
@Override
|
||||
public int[] getLightingTable() {
|
||||
return lightingTable;
|
||||
}
|
||||
@Override
|
||||
public void setLastBlockState(DynmapBlockState new_lastbs) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get renderer state object for use rendering a tile
|
||||
* @param map - map being rendered
|
||||
* @param cache - chunk cache containing data for tile to be rendered
|
||||
* @param mapiter - iterator used when traversing rays in tile
|
||||
* @param scale - scale of perspecitve
|
||||
* @return state object to use for all rays in tile
|
||||
*/
|
||||
@Override
|
||||
public HDShaderState getStateInstance(HDMap map, MapChunkCache cache, MapIterator mapiter, int scale) {
|
||||
return new OurShaderState(mapiter, map, cache, scale);
|
||||
}
|
||||
|
||||
/* Add shader's contributions to JSON for map object */
|
||||
public void addClientConfiguration(JSONObject mapObject) {
|
||||
s(mapObject, "shader", name);
|
||||
}
|
||||
@Override
|
||||
public void exportAsMaterialLibrary(DynmapCommandSender sender, OBJExport out) throws IOException {
|
||||
throw new IOException("Export unsupported");
|
||||
}
|
||||
private static final String[] nulllist = new String[0];
|
||||
@Override
|
||||
public String[] getCurrentBlockMaterials(DynmapBlockState blk, MapIterator mapiter, int[] txtidx, BlockStep[] steps) {
|
||||
return nulllist;
|
||||
}
|
||||
}
|
||||
1481
DynmapCore/src/main/java/org/dynmap/hdmap/IsoHDPerspective.java
Normal file
1481
DynmapCore/src/main/java/org/dynmap/hdmap/IsoHDPerspective.java
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,66 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import org.dynmap.Color;
|
||||
import org.dynmap.ConfigurationNode;
|
||||
import org.dynmap.DynmapCore;
|
||||
import org.dynmap.DynmapWorld;
|
||||
import org.dynmap.utils.LightLevels;
|
||||
|
||||
public class LightLevelHDLighting extends DefaultHDLighting {
|
||||
private final Color[] lightlevelcolors = new Color[16];
|
||||
protected final boolean night_and_day; /* If true, render both day (prefix+'-day') and night (prefix) tiles */
|
||||
private final boolean night;
|
||||
private final Color mincolor = new Color(0x40, 0x40, 0x40);
|
||||
private final Color maxcolor = new Color(0xFF, 0xFF, 0xFF);
|
||||
public LightLevelHDLighting(DynmapCore core, ConfigurationNode configuration) {
|
||||
super(core, configuration);
|
||||
grayscale = true; // Force to grayscale
|
||||
blackandwhite = false;
|
||||
for (int i = 0; i < 16; i++) {
|
||||
lightlevelcolors[i] = configuration.getColor("color" + i, null);
|
||||
}
|
||||
night = configuration.getBoolean("night", false);
|
||||
night_and_day = configuration.getBoolean("night-and-day", false);
|
||||
}
|
||||
|
||||
/* Apply lighting to given pixel colors (1 outcolor if normal, 2 if night/day) */
|
||||
public void applyLighting(HDPerspectiveState ps, HDShaderState ss, Color incolor, Color[] outcolor) {
|
||||
super.applyLighting(ps, ss, incolor, outcolor); // Apply default lighting (outcolors will be grayscale)
|
||||
// Compute light levels
|
||||
LightLevels ll = ps.getCachedLightLevels(0);
|
||||
ps.getLightLevels(ll);
|
||||
if (outcolor.length == 1) {
|
||||
// Scale range between 25% and 100% intensity (we don't want blacks, since that will prevent color from showing
|
||||
outcolor[0].scaleColor(mincolor, maxcolor);
|
||||
int lightlevel = night ? ll.emitted : Math.max(ll.sky, ll.emitted);
|
||||
if (lightlevelcolors[lightlevel] != null) {
|
||||
outcolor[0].blendColor(lightlevelcolors[lightlevel]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Scale range between 25% and 100% intensity (we don't want blacks, since that will prevent color from showing
|
||||
outcolor[0].scaleColor(mincolor, maxcolor);
|
||||
outcolor[1].scaleColor(mincolor, maxcolor);
|
||||
int daylightlevel = Math.max(ll.sky, ll.emitted);
|
||||
if (lightlevelcolors[ll.emitted] != null) {
|
||||
outcolor[0].blendColor(lightlevelcolors[ll.emitted]);
|
||||
}
|
||||
if (lightlevelcolors[daylightlevel] != null) {
|
||||
outcolor[1].blendColor(lightlevelcolors[daylightlevel]);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Test if night/day is enabled for this renderer */
|
||||
public boolean isNightAndDayEnabled() { return night_and_day; }
|
||||
|
||||
/* Test if sky light level needed */
|
||||
public boolean isSkyLightLevelNeeded() { return true; }
|
||||
|
||||
/* Test if emitted light level needed */
|
||||
public boolean isEmittedLightLevelNeeded() { return true; }
|
||||
|
||||
@Override
|
||||
public int[] getBrightnessTable(DynmapWorld world) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
196
DynmapCore/src/main/java/org/dynmap/hdmap/ShadowHDLighting.java
Normal file
196
DynmapCore/src/main/java/org/dynmap/hdmap/ShadowHDLighting.java
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import org.dynmap.Color;
|
||||
import org.dynmap.ConfigurationNode;
|
||||
import org.dynmap.DynmapCore;
|
||||
import org.dynmap.DynmapWorld;
|
||||
import org.dynmap.MapManager;
|
||||
import org.dynmap.utils.LightLevels;
|
||||
import org.dynmap.utils.BlockStep;
|
||||
|
||||
public class ShadowHDLighting extends DefaultHDLighting {
|
||||
|
||||
protected final int defLightingTable[]; /* index=skylight level, value = 256 * scaling value */
|
||||
protected final int lightscale[]; /* scale skylight level (light = lightscale[skylight] */
|
||||
protected final boolean night_and_day; /* If true, render both day (prefix+'-day') and night (prefix) tiles */
|
||||
protected final boolean smooth;
|
||||
protected final boolean useWorldBrightnessTable;
|
||||
|
||||
public ShadowHDLighting(DynmapCore core, ConfigurationNode configuration) {
|
||||
super(core, configuration);
|
||||
double shadowweight = configuration.getDouble("shadowstrength", 0.0);
|
||||
// See if we're using world's lighting table, or our own
|
||||
useWorldBrightnessTable = configuration.getBoolean("use-brightness-table", MapManager.mapman.useBrightnessTable());
|
||||
|
||||
defLightingTable = new int[16];
|
||||
defLightingTable[15] = 256;
|
||||
/* Normal brightness weight in MC is a 20% relative dropoff per step */
|
||||
for(int i = 14; i >= 0; i--) {
|
||||
double v = defLightingTable[i+1] * (1.0 - (0.2 * shadowweight));
|
||||
defLightingTable[i] = (int)v;
|
||||
if(defLightingTable[i] > 256) defLightingTable[i] = 256;
|
||||
if(defLightingTable[i] < 0) defLightingTable[i] = 0;
|
||||
}
|
||||
int v = configuration.getInteger("ambientlight", -1);
|
||||
if(v < 0) v = 15;
|
||||
if(v > 15) v = 15;
|
||||
night_and_day = configuration.getBoolean("night-and-day", false);
|
||||
lightscale = new int[16];
|
||||
for(int i = 0; i < 16; i++) {
|
||||
if(i < (15-v))
|
||||
lightscale[i] = 0;
|
||||
else
|
||||
lightscale[i] = i - (15-v);
|
||||
}
|
||||
smooth = configuration.getBoolean("smooth-lighting", MapManager.mapman.getSmoothLighting());
|
||||
}
|
||||
|
||||
private void applySmoothLighting(HDPerspectiveState ps, HDShaderState ss, Color incolor, Color[] outcolor, int[] shadowscale) {
|
||||
int[] xyz = ps.getSubblockCoord();
|
||||
int scale = (int)ps.getScale();
|
||||
int mid = scale / 2;
|
||||
BlockStep s1, s2;
|
||||
int w1, w2;
|
||||
/* Figure out which two neighbor directions to sample */
|
||||
switch(ps.getLastBlockStep()) {
|
||||
case X_MINUS:
|
||||
case X_PLUS:
|
||||
s1 = (xyz[1] < mid) ? BlockStep.Y_MINUS : BlockStep.Y_PLUS;
|
||||
w1 = Math.abs(xyz[1] - mid);
|
||||
s2 = (xyz[2] < mid) ? BlockStep.Z_MINUS : BlockStep.Z_PLUS;
|
||||
w2 = Math.abs(xyz[2] - mid);
|
||||
break;
|
||||
case Z_MINUS:
|
||||
case Z_PLUS:
|
||||
s1 = (xyz[0] < mid) ? BlockStep.X_MINUS : BlockStep.X_PLUS;
|
||||
w1 = Math.abs(xyz[0] - mid);
|
||||
s2 = (xyz[1] < mid) ? BlockStep.Y_MINUS : BlockStep.Y_PLUS;
|
||||
w2 = Math.abs(xyz[1] - mid);
|
||||
break;
|
||||
default:
|
||||
s1 = (xyz[0] < mid) ? BlockStep.X_MINUS : BlockStep.X_PLUS;
|
||||
w1 = Math.abs(xyz[0] - mid);
|
||||
s2 = (xyz[2] < mid) ? BlockStep.Z_MINUS : BlockStep.Z_PLUS;
|
||||
w2 = Math.abs(xyz[2] - mid);
|
||||
break;
|
||||
}
|
||||
/* Fetch the 3 needed light levels (once, shared by both night and day passes) */
|
||||
LightLevels ll0 = ps.getCachedLightLevels(0);
|
||||
ps.getLightLevels(ll0);
|
||||
LightLevels ll1 = ps.getCachedLightLevels(1);
|
||||
ps.getLightLevelsAtStep(s1, ll1);
|
||||
LightLevels ll2 = ps.getCachedLightLevels(2);
|
||||
ps.getLightLevelsAtStep(s2, ll2);
|
||||
|
||||
/* Night (ambient) pass */
|
||||
applySmoothedLightToColor(outcolor[0], incolor, ll0, ll1, ll2, true, w1, w2, scale, shadowscale);
|
||||
/* Day pass (only when night/day rendering is active) */
|
||||
if(outcolor.length > 1) {
|
||||
applySmoothedLightToColor(outcolor[1], incolor, ll0, ll1, ll2, false, w1, w2, scale, shadowscale);
|
||||
}
|
||||
}
|
||||
|
||||
/** Apply smooth lighting to a single output color for one pass (ambient or day). */
|
||||
private void applySmoothedLightToColor(Color out, Color incolor,
|
||||
LightLevels ll0, LightLevels ll1, LightLevels ll2,
|
||||
boolean useambient, int w1, int w2, int scale, int[] shadowscale) {
|
||||
int lv0 = getLightLevel(ll0, useambient);
|
||||
int lv1 = getLightLevel(ll1, useambient);
|
||||
int weight = 0;
|
||||
if(lv1 < lv0) weight -= w1;
|
||||
else if(lv1 > lv0) weight += w1;
|
||||
int lv2 = getLightLevel(ll2, useambient);
|
||||
if(lv2 < lv0) weight -= w2;
|
||||
else if(lv2 > lv0) weight += w2;
|
||||
out.setColor(incolor);
|
||||
int cscale = computeSmoothedCscale(lv0, weight, scale, shadowscale);
|
||||
if(cscale < 256) {
|
||||
out.scaleRGB(cscale);
|
||||
}
|
||||
}
|
||||
|
||||
/** Interpolate the shadow scale for a light level, blending toward the adjacent level by weight/scale. */
|
||||
private int computeSmoothedCscale(int ll0, int weight, int scale, int[] shadowscale) {
|
||||
if(weight == 0) {
|
||||
return shadowscale[ll0];
|
||||
}
|
||||
if(weight < 0) {
|
||||
weight = -weight;
|
||||
return (ll0 > 0)
|
||||
? (shadowscale[ll0] * (scale - weight) + shadowscale[ll0 - 1] * weight) / scale
|
||||
: shadowscale[ll0];
|
||||
}
|
||||
return (ll0 < 15)
|
||||
? (shadowscale[ll0] * (scale - weight) + shadowscale[ll0 + 1] * weight) / scale
|
||||
: shadowscale[ll0];
|
||||
}
|
||||
|
||||
private final int getLightLevel(final LightLevels ll, boolean useambient) {
|
||||
int lightlevel = useambient ? lightscale[ll.sky] : ll.sky;
|
||||
if(lightlevel < 15) {
|
||||
lightlevel = Math.max(ll.emitted, lightlevel);
|
||||
}
|
||||
return lightlevel;
|
||||
}
|
||||
|
||||
/* Apply lighting to given pixel colors (1 outcolor if normal, 2 if night/day) */
|
||||
public void applyLighting(HDPerspectiveState ps, HDShaderState ss, Color incolor, Color[] outcolor) {
|
||||
int[] shadowscale = ss.getLightingTable();
|
||||
if(shadowscale == null) {
|
||||
shadowscale = defLightingTable;
|
||||
}
|
||||
if(smooth && ps.getShade()) {
|
||||
applySmoothLighting(ps, ss, incolor, outcolor, shadowscale);
|
||||
checkGrayscale(outcolor);
|
||||
return;
|
||||
}
|
||||
/* Non-smooth: fetch light levels and apply flat shadow */
|
||||
LightLevels ll = ps.getCachedLightLevels(0);
|
||||
ps.getLightLevels(ll);
|
||||
int lightlevel = lightscale[ll.sky]; // apply ambient scale immediately
|
||||
int lightlevel_day = ll.sky;
|
||||
if((lightlevel < 15) || (lightlevel_day < 15)) {
|
||||
int emitted = ll.emitted;
|
||||
lightlevel = Math.max(emitted, lightlevel);
|
||||
lightlevel_day = Math.max(emitted, lightlevel_day);
|
||||
}
|
||||
outcolor[0].setColor(incolor);
|
||||
if(lightlevel < 15) {
|
||||
int s = shadowscale[lightlevel];
|
||||
if(s < 256) outcolor[0].scaleRGB(s);
|
||||
}
|
||||
if(outcolor.length > 1) {
|
||||
if(lightlevel_day == lightlevel) {
|
||||
outcolor[1].setColor(outcolor[0]);
|
||||
}
|
||||
else {
|
||||
outcolor[1].setColor(incolor);
|
||||
if(lightlevel_day < 15) {
|
||||
int s = shadowscale[lightlevel_day];
|
||||
if(s < 256) outcolor[1].scaleRGB(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
checkGrayscale(outcolor);
|
||||
}
|
||||
|
||||
|
||||
/* Test if night/day is enabled for this renderer */
|
||||
public boolean isNightAndDayEnabled() { return night_and_day; }
|
||||
|
||||
/* Test if sky light level needed */
|
||||
public boolean isSkyLightLevelNeeded() { return true; }
|
||||
|
||||
/* Test if emitted light level needed */
|
||||
public boolean isEmittedLightLevelNeeded() { return true; }
|
||||
|
||||
@Override
|
||||
public int[] getBrightnessTable(DynmapWorld world) {
|
||||
if (useWorldBrightnessTable) {
|
||||
return world.getBrightnessTable();
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
3634
DynmapCore/src/main/java/org/dynmap/hdmap/TexturePack.java
Normal file
3634
DynmapCore/src/main/java/org/dynmap/hdmap/TexturePack.java
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,51 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import org.dynmap.ConfigurationNode;
|
||||
import org.dynmap.DynmapCore;
|
||||
import org.dynmap.utils.BlockStep;
|
||||
import org.dynmap.utils.LightLevels;
|
||||
import org.dynmap.utils.MapChunkCache;
|
||||
import org.dynmap.utils.MapIterator;
|
||||
|
||||
public class TexturePackHDCaveShader extends TexturePackHDShader {
|
||||
private int maxskylevel;
|
||||
private int minemittedlevel;
|
||||
|
||||
class CaveShaderState extends TexturePackHDShader.ShaderState {
|
||||
private boolean ready;
|
||||
private LightLevels ll = new LightLevels();
|
||||
|
||||
protected CaveShaderState(MapIterator mapiter, HDMap map, MapChunkCache cache, int scale) {
|
||||
super(mapiter, map, cache, scale);
|
||||
}
|
||||
@Override
|
||||
public void reset(HDPerspectiveState ps) {
|
||||
super.reset(ps);
|
||||
ready = false;
|
||||
}
|
||||
/**
|
||||
* Process next ray step - called for each block on route
|
||||
* @return true if ray is done, false if ray needs to continue
|
||||
*/
|
||||
public boolean processBlock(HDPerspectiveState ps) {
|
||||
if(ready)
|
||||
return super.processBlock(ps);
|
||||
if((ps.getLastBlockStep() == BlockStep.Y_MINUS) && ps.getBlockState().isAir()) { /* In air? */
|
||||
ps.getLightLevels(ll);
|
||||
if((ll.sky <= maxskylevel) && (ll.emitted > minemittedlevel)) {
|
||||
ready = true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public TexturePackHDCaveShader(DynmapCore core, ConfigurationNode configuration) {
|
||||
super(core, configuration);
|
||||
maxskylevel = configuration.getInteger("max-sky-light", 0);
|
||||
minemittedlevel = configuration.getInteger("min-emitted-light", 1);
|
||||
}
|
||||
@Override
|
||||
public HDShaderState getStateInstance(HDMap map, MapChunkCache cache, MapIterator mapiter, int scale) {
|
||||
return new CaveShaderState(mapiter, map, cache, scale);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,381 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import static org.dynmap.JSONUtils.s;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.BitSet;
|
||||
import java.util.List;
|
||||
|
||||
import org.dynmap.Color;
|
||||
import org.dynmap.ConfigurationNode;
|
||||
import org.dynmap.DynmapCore;
|
||||
import org.dynmap.Log;
|
||||
import org.dynmap.MapManager;
|
||||
import org.dynmap.common.DynmapCommandSender;
|
||||
import org.dynmap.exporter.OBJExport;
|
||||
import org.dynmap.renderer.DynmapBlockState;
|
||||
import org.dynmap.utils.BlockStep;
|
||||
import org.dynmap.utils.DynLongHashMap;
|
||||
import org.dynmap.utils.MapChunkCache;
|
||||
import org.dynmap.utils.MapIterator;
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
public class TexturePackHDShader implements HDShader {
|
||||
private final String tpname;
|
||||
private final String name;
|
||||
private TexturePack tp;
|
||||
private boolean did_tp_load = false;
|
||||
private final boolean biome_shaded;
|
||||
private final boolean bettergrass;
|
||||
private final int gridscale;
|
||||
private final DynmapCore core;
|
||||
private final BitSet hiddenids;
|
||||
|
||||
public TexturePackHDShader(DynmapCore core, ConfigurationNode configuration) {
|
||||
tpname = configuration.getString("texturepack", "minecraft");
|
||||
name = configuration.getString("name", tpname);
|
||||
this.core = core;
|
||||
biome_shaded = configuration.getBoolean("biomeshaded", true);
|
||||
bettergrass = configuration.getBoolean("better-grass", MapManager.mapman.getBetterGrass());
|
||||
gridscale = configuration.getInteger("grid-scale", 0);
|
||||
List<Object> hidden = configuration.getList("hiddenids");
|
||||
if(hidden != null) {
|
||||
hiddenids = new BitSet();
|
||||
for(Object o : hidden) {
|
||||
if(o instanceof Integer) {
|
||||
int v = ((Integer)o);
|
||||
hiddenids.set(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
hiddenids = null;
|
||||
}
|
||||
}
|
||||
|
||||
private final TexturePack getTexturePack() {
|
||||
if (!did_tp_load) {
|
||||
tp = TexturePack.getTexturePack(this.core, this.tpname);
|
||||
if(tp == null) {
|
||||
Log.severe("Error: shader '" + name + "' cannot load texture pack '" + tpname + "'");
|
||||
}
|
||||
did_tp_load = true;
|
||||
}
|
||||
return tp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBiomeDataNeeded() {
|
||||
return biome_shaded;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRawBiomeDataNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHightestBlockYDataNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBlockTypeDataNeeded() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSkyLightLevelNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmittedLightLevelNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
class ShaderState implements HDShaderState {
|
||||
final private Color color[];
|
||||
final private Color tmpcolor[];
|
||||
final private Color c;
|
||||
final protected MapIterator mapiter;
|
||||
final protected HDMap map;
|
||||
final private TexturePack scaledtp;
|
||||
final private HDLighting lighting;
|
||||
protected DynmapBlockState lastblk;
|
||||
protected DynmapBlockState lastblkhit;
|
||||
final boolean do_biome_shading;
|
||||
final boolean do_better_grass;
|
||||
DynLongHashMap ctm_cache;
|
||||
final int[] lightingTable;
|
||||
|
||||
protected ShaderState(MapIterator mapiter, HDMap map, MapChunkCache cache, int scale) {
|
||||
this.mapiter = mapiter;
|
||||
this.map = map;
|
||||
this.lighting = map.getLighting();
|
||||
if(lighting.isNightAndDayEnabled()) {
|
||||
color = new Color[] { new Color(), new Color() };
|
||||
tmpcolor = new Color[] { new Color(), new Color() };
|
||||
}
|
||||
else {
|
||||
color = new Color[] { new Color() };
|
||||
tmpcolor = new Color[] { new Color() };
|
||||
}
|
||||
c = new Color();
|
||||
TexturePack tp = getTexturePack();
|
||||
if (tp != null)
|
||||
scaledtp = tp.resampleTexturePack(scale);
|
||||
else
|
||||
scaledtp = null;
|
||||
/* Biome raw data only works on normal worlds at this point */
|
||||
do_biome_shading = biome_shaded; // && (cache.getWorld().getEnvironment() == Environment.NORMAL);
|
||||
do_better_grass = bettergrass;
|
||||
if (MapManager.mapman.useBrightnessTable()) {
|
||||
lightingTable = cache.getWorld().getBrightnessTable();
|
||||
}
|
||||
else {
|
||||
lightingTable = null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get our shader
|
||||
*/
|
||||
@Override
|
||||
public HDShader getShader() {
|
||||
return TexturePackHDShader.this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get our map
|
||||
*/
|
||||
@Override
|
||||
public HDMap getMap() {
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get our lighting
|
||||
*/
|
||||
@Override
|
||||
public HDLighting getLighting() {
|
||||
return lighting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset renderer state for new ray
|
||||
*/
|
||||
@Override
|
||||
public void reset(HDPerspectiveState ps) {
|
||||
for(int i = 0; i < color.length; i++)
|
||||
color[i].setTransparent();
|
||||
setLastBlockState(DynmapBlockState.AIR);
|
||||
lastblkhit = DynmapBlockState.AIR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process next ray step - called for each block on route
|
||||
* @return true if ray is done, false if ray needs to continue
|
||||
*/
|
||||
@Override
|
||||
public boolean processBlock(HDPerspectiveState ps) {
|
||||
DynmapBlockState blocktype = ps.getBlockState();
|
||||
if ((hiddenids != null) && hiddenids.get(blocktype.globalStateIndex)) {
|
||||
blocktype = DynmapBlockState.AIR;
|
||||
}
|
||||
|
||||
if (blocktype.isAir()) {
|
||||
lastblkhit = blocktype;
|
||||
return false;
|
||||
}
|
||||
|
||||
DynmapBlockState lastblocktype = lastblk;
|
||||
|
||||
/* Get color from textures */
|
||||
if (scaledtp != null) {
|
||||
scaledtp.readColor(ps, mapiter, c, blocktype, lastblocktype, ShaderState.this);
|
||||
}
|
||||
lastblkhit = blocktype;
|
||||
|
||||
if (c.getAlpha() > 0) {
|
||||
/* Scale brightness depending upon face */
|
||||
if (ps.getShade()) {
|
||||
if (this.lightingTable != null) {
|
||||
switch (ps.getLastBlockStep()) {
|
||||
case X_MINUS:
|
||||
case X_PLUS:
|
||||
/* 60% brightness */
|
||||
c.blendColor(0xFF999999);
|
||||
break;
|
||||
case Y_MINUS:
|
||||
// 95% for even
|
||||
if((mapiter.getY() & 0x01) == 0) {
|
||||
c.blendColor(0xFFF3F3F3);
|
||||
}
|
||||
break;
|
||||
case Y_PLUS:
|
||||
/* 50%*/
|
||||
c.blendColor(0xFF808080);
|
||||
break;
|
||||
case Z_MINUS:
|
||||
case Z_PLUS:
|
||||
default:
|
||||
/* 80%*/
|
||||
c.blendColor(0xFFCDCDCD);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
switch (ps.getLastBlockStep()) {
|
||||
case X_MINUS:
|
||||
case X_PLUS:
|
||||
/* 60% brightness */
|
||||
c.blendColor(0xFFA0A0A0);
|
||||
break;
|
||||
case Y_MINUS:
|
||||
case Y_PLUS:
|
||||
/* 85% brightness for even, 90% for even*/
|
||||
if((mapiter.getY() & 0x01) == 0)
|
||||
c.blendColor(0xFFD9D9D9);
|
||||
else
|
||||
c.blendColor(0xFFE6E6E6);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Handle light level, if needed */
|
||||
lighting.applyLighting(ps, this, c, tmpcolor);
|
||||
/* If grid scale, add it */
|
||||
if(gridscale > 0) {
|
||||
int xx = mapiter.getX() % gridscale;
|
||||
int zz = mapiter.getZ() % gridscale;
|
||||
if(((xx == 0) && ((zz & 2) == 0)) || ((zz == 0) && ((xx & 2) == 0))) {
|
||||
for(int i = 0; i < tmpcolor.length; i++) {
|
||||
int v = tmpcolor[i].getARGB();
|
||||
tmpcolor[i].setARGB((v & 0xFF000000) | ((v & 0xFEFEFE) >> 1) | 0x808080);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* If no previous color contribution, use new color */
|
||||
if(color[0].isTransparent()) {
|
||||
for(int i = 0; i < color.length; i++)
|
||||
color[i].setColor(tmpcolor[i]);
|
||||
return (tmpcolor[0].getAlpha() == 255);
|
||||
}
|
||||
/* Else, blend and generate new alpha */
|
||||
else {
|
||||
int alpha = color[0].getAlpha();
|
||||
int alpha2 = tmpcolor[0].getAlpha() * (255-alpha) / 255;
|
||||
int talpha = alpha + alpha2;
|
||||
if(talpha > 0)
|
||||
for(int i = 0; i < color.length; i++) {
|
||||
int tc = tmpcolor[i].getARGB();
|
||||
int cc = color[i].getARGB();
|
||||
color[i].setARGB((talpha << 24)
|
||||
| (((((tc >> 16) & 0xFF) * alpha2 + ((cc >> 16) & 0xFF) * alpha) / talpha) << 16)
|
||||
| (((((tc >> 8) & 0xFF) * alpha2 + ((cc >> 8) & 0xFF) * alpha) / talpha) << 8)
|
||||
| ((( tc & 0xFF) * alpha2 + ( cc & 0xFF) * alpha) / talpha));
|
||||
}
|
||||
else
|
||||
for(int i = 0; i < color.length; i++)
|
||||
color[i].setTransparent();
|
||||
|
||||
return (talpha >= 254); /* If only one short, no meaningful contribution left */
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Ray ended - used to report that ray has exited map (called if renderer has not reported complete)
|
||||
*/
|
||||
@Override
|
||||
public void rayFinished(HDPerspectiveState ps) {
|
||||
}
|
||||
/**
|
||||
* Get result color - get resulting color for ray
|
||||
* @param c - object to store color value in
|
||||
* @param index - index of color to request (renderer specific - 0=default, 1=day for night/day renderer
|
||||
*/
|
||||
@Override
|
||||
public void getRayColor(Color c, int index) {
|
||||
c.setColor(color[index]);
|
||||
}
|
||||
/**
|
||||
* Clean up state object - called after last ray completed
|
||||
*/
|
||||
@Override
|
||||
public void cleanup() {
|
||||
if (ctm_cache != null) {
|
||||
ctm_cache.clear();
|
||||
ctm_cache = null;
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public DynLongHashMap getCTMTextureCache() {
|
||||
if (ctm_cache == null) {
|
||||
ctm_cache = new DynLongHashMap();
|
||||
}
|
||||
return ctm_cache;
|
||||
}
|
||||
@Override
|
||||
public int[] getLightingTable() {
|
||||
return lightingTable;
|
||||
}
|
||||
@Override
|
||||
public void setLastBlockState(DynmapBlockState new_lastbs) {
|
||||
lastblk = new_lastbs;
|
||||
}
|
||||
// Return last blockc with surface hit
|
||||
public DynmapBlockState getLastBlockHit() {
|
||||
return lastblkhit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get renderer state object for use rendering a tile
|
||||
* @param map - map being rendered
|
||||
* @param cache - chunk cache containing data for tile to be rendered
|
||||
* @param mapiter - iterator used when traversing rays in tile
|
||||
* @param scale - scale of perspective
|
||||
* @return state object to use for all rays in tile
|
||||
*/
|
||||
@Override
|
||||
public HDShaderState getStateInstance(HDMap map, MapChunkCache cache, MapIterator mapiter, int scale) {
|
||||
return new ShaderState(mapiter, map, cache, scale);
|
||||
}
|
||||
|
||||
/* Add shader's contributions to JSON for map object */
|
||||
@Override
|
||||
public void addClientConfiguration(JSONObject mapObject) {
|
||||
s(mapObject, "shader", name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportAsMaterialLibrary(DynmapCommandSender sender, OBJExport out) throws IOException {
|
||||
if (tp == null) {
|
||||
getTexturePack(); // Make sure its loaded
|
||||
}
|
||||
if (tp != null) {
|
||||
tp.exportAsOBJMaterialLibrary(out, out.getBaseName());
|
||||
return;
|
||||
}
|
||||
throw new IOException("Export unsupported - invalid texture pack");
|
||||
}
|
||||
@Override
|
||||
public String[] getCurrentBlockMaterials(DynmapBlockState blk, MapIterator mapiter, int[] txtidx, BlockStep[] steps) {
|
||||
if (tp == null) {
|
||||
getTexturePack(); // Make sure its loaded
|
||||
}
|
||||
if (tp != null) {
|
||||
return tp.getCurrentBlockMaterials(blk, mapiter, txtidx, steps);
|
||||
}
|
||||
return new String[txtidx.length];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import org.dynmap.ConfigurationNode;
|
||||
import org.dynmap.DynmapCore;
|
||||
import org.dynmap.renderer.DynmapBlockState;
|
||||
import org.dynmap.utils.MapChunkCache;
|
||||
import org.dynmap.utils.MapIterator;
|
||||
|
||||
public class TexturePackHDUnderwaterShader extends TexturePackHDShader {
|
||||
private boolean hide_land = true;
|
||||
|
||||
class UnderwaterShaderState extends TexturePackHDShader.ShaderState {
|
||||
private boolean ready;
|
||||
private DynmapBlockState full_water;
|
||||
|
||||
protected UnderwaterShaderState(MapIterator mapiter, HDMap map, MapChunkCache cache, int scale) {
|
||||
super(mapiter, map, cache, scale);
|
||||
full_water = DynmapBlockState.getBaseStateByName(DynmapBlockState.WATER_BLOCK);
|
||||
}
|
||||
@Override
|
||||
public void reset(HDPerspectiveState ps) {
|
||||
super.reset(ps);
|
||||
ready = (!hide_land); // Start ready if not hiding land
|
||||
}
|
||||
/**
|
||||
* Process next ray step - called for each block on route
|
||||
* @return true if ray is done, false if ray needs to continue
|
||||
*/
|
||||
public boolean processBlock(HDPerspectiveState ps) {
|
||||
DynmapBlockState bs = ps.getBlockState();
|
||||
if (bs.isWater() || bs.isWaterlogged()) {
|
||||
ready = true;
|
||||
this.lastblk = full_water;
|
||||
this.lastblkhit = full_water;
|
||||
}
|
||||
return ready ? super.processBlock(ps) : false;
|
||||
}
|
||||
}
|
||||
public TexturePackHDUnderwaterShader(DynmapCore core, ConfigurationNode configuration) {
|
||||
super(core, configuration);
|
||||
hide_land = configuration.getBoolean("hide-land", true);
|
||||
}
|
||||
@Override
|
||||
public HDShaderState getStateInstance(HDMap map, MapChunkCache cache, MapIterator mapiter, int scale) {
|
||||
return new UnderwaterShaderState(mapiter, map, cache, scale);
|
||||
}
|
||||
}
|
||||
172
DynmapCore/src/main/java/org/dynmap/hdmap/TexturePackLoader.java
Normal file
172
DynmapCore/src/main/java/org/dynmap/hdmap/TexturePackLoader.java
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
package org.dynmap.hdmap;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import org.dynmap.DynmapCore;
|
||||
import org.dynmap.Log;
|
||||
import org.dynmap.common.DynmapServerInterface;
|
||||
|
||||
public class TexturePackLoader {
|
||||
private ZipFile zf;
|
||||
private File tpdir;
|
||||
private DynmapServerInterface dsi;
|
||||
private static final String RESOURCEPATH = "texturepacks/standard";
|
||||
|
||||
private static class ModSource {
|
||||
ZipFile zf;
|
||||
File directory;
|
||||
}
|
||||
private HashMap<String, ModSource> src_by_mod = new HashMap<String, ModSource>();
|
||||
|
||||
public TexturePackLoader(File tp, DynmapCore core) {
|
||||
if (tp.isFile() && tp.canRead()) {
|
||||
try {
|
||||
zf = new ZipFile(tp);
|
||||
} catch (IOException e) {
|
||||
Log.severe("Error opening texture pack - " + tp.getPath());
|
||||
}
|
||||
}
|
||||
else if (tp.isDirectory() && tp.canRead()) {
|
||||
tpdir = tp;
|
||||
}
|
||||
else {
|
||||
Log.info("Texture pack not found - " + tp.getPath());
|
||||
}
|
||||
dsi = core.getServer();
|
||||
}
|
||||
public InputStream openTPResource(String rname, String altname) {
|
||||
InputStream is = openTPResource(rname);
|
||||
if (is == null) {
|
||||
if (altname != null) {
|
||||
is = openTPResource(altname);
|
||||
}
|
||||
}
|
||||
return is;
|
||||
}
|
||||
public InputStream openTPResource(String rname) {
|
||||
return openModTPResource(rname, null);
|
||||
}
|
||||
|
||||
public InputStream openModTPResource(String rname, String modname) {
|
||||
try {
|
||||
if (zf != null) {
|
||||
ZipEntry ze = zf.getEntry(rname);
|
||||
if ((ze != null) && (!ze.isDirectory())) {
|
||||
return new BufferedInputStream(zf.getInputStream(ze));
|
||||
}
|
||||
}
|
||||
else if (tpdir != null) {
|
||||
File f = new File(tpdir, rname);
|
||||
if (f.isFile() && f.canRead()) {
|
||||
return new BufferedInputStream(new FileInputStream(f));
|
||||
}
|
||||
}
|
||||
} catch (IOException iox) {
|
||||
}
|
||||
// Fall through - load as resource from mod, if possible, or from jar
|
||||
InputStream is = dsi.openResource(modname, rname);
|
||||
if (is != null) {
|
||||
return new BufferedInputStream(is);
|
||||
}
|
||||
if (modname != null) {
|
||||
ModSource ms = src_by_mod.get(modname);
|
||||
if (ms == null) {
|
||||
File f = dsi.getModContainerFile(modname);
|
||||
ms = new ModSource();
|
||||
if (f != null) {
|
||||
if (f.isFile()) {
|
||||
try {
|
||||
ms.zf = new ZipFile(f);
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
else {
|
||||
ms.directory = f;
|
||||
}
|
||||
}
|
||||
src_by_mod.put(modname, ms);
|
||||
}
|
||||
try {
|
||||
if (ms.zf != null) {
|
||||
ZipEntry ze = ms.zf.getEntry(rname);
|
||||
if ((ze != null) && (!ze.isDirectory())) {
|
||||
is = ms.zf.getInputStream(ze);
|
||||
}
|
||||
}
|
||||
else if (ms.directory != null) {
|
||||
File f = new File(ms.directory, rname);
|
||||
if (f.isFile() && f.canRead()) {
|
||||
is = new FileInputStream(f);
|
||||
}
|
||||
}
|
||||
} catch (IOException iox) {
|
||||
}
|
||||
}
|
||||
if (is == null) {
|
||||
is = getClass().getClassLoader().getResourceAsStream(RESOURCEPATH + "/" + rname);
|
||||
}
|
||||
if ((is == null) && (modname != null)) {
|
||||
Log.warning("Resource " + rname + " for mod " + modname + " not found");
|
||||
}
|
||||
|
||||
return (is != null) ? new BufferedInputStream(is) : null;
|
||||
}
|
||||
public void close() {
|
||||
if(zf != null) {
|
||||
try { zf.close(); } catch (IOException iox) {}
|
||||
zf = null;
|
||||
}
|
||||
for (ModSource ms : src_by_mod.values()) {
|
||||
if (ms.zf != null) {
|
||||
try { ms.zf.close(); } catch (IOException iox) {}
|
||||
}
|
||||
}
|
||||
src_by_mod.clear();
|
||||
}
|
||||
public void closeResource(InputStream is) {
|
||||
try {
|
||||
if (is != null)
|
||||
is.close();
|
||||
} catch (IOException iox) {
|
||||
}
|
||||
}
|
||||
public Set<String> getEntries() {
|
||||
HashSet<String> rslt = new HashSet<String>();
|
||||
if (zf != null) {
|
||||
Enumeration<? extends ZipEntry> lst = zf.entries();
|
||||
while(lst.hasMoreElements()) {
|
||||
rslt.add(lst.nextElement().getName());
|
||||
}
|
||||
}
|
||||
if (tpdir != null) {
|
||||
addFiles(rslt, tpdir, "");
|
||||
}
|
||||
return rslt;
|
||||
}
|
||||
|
||||
private void addFiles(HashSet<String> files, File dir, String path) {
|
||||
File[] listfiles = dir.listFiles();
|
||||
if(listfiles == null) return;
|
||||
for(File f : listfiles) {
|
||||
String fn = f.getName();
|
||||
if(fn.equals(".") || (fn.equals(".."))) continue;
|
||||
if(f.isFile()) {
|
||||
files.add(path + "/" + fn);
|
||||
}
|
||||
else if(f.isDirectory()) {
|
||||
addFiles(files, f, path + "/" + f.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue