40 Commits

Author SHA1 Message Date
22a744f65a Added Options
-Added options menu
-You can move from options to title and back
-Added language change button
*Double check the Russian, I just copied and pasted from other files
2022-01-08 21:16:51 -05:00
c6de19cf19 Rebased everything on master
Now has the dynamic version thing
fixed warnings
2022-01-08 13:57:57 -05:00
c1a57f7d7a Slightly better alignment
The mode for alignment is more concise
2022-01-08 12:45:28 -05:00
162b2249b1 Rewrote build script, improved packaging
- Renamed :packageWindows to :packageNsis
- ImageMagick now required for :packageNsis
- Packaging output is now in build/packages
- Rewrote build script
  - Packaging logic and LWJGL dependencies now in separate files
  - Packaging logic now implemented with Gradle, not Bash scripts
    - Cross-platform?
- Added :packageZip
  - Universal ZIP with start scripts
- Changed version detection logic
  - Tags of ancestor commits are now considered
  - Only tags with format vMAJOR.MINOR.PATCH[-SUFFIX] are considered
  - If the tag's commit isn't HEAD, PATCH is incremented
  - When version detection fails, dummy version is 999.0.0-<buildID or date>
- LWJGL target platforms can be overridden with forceTargets project property
2022-01-07 22:23:24 +03:00
dececb4589 Fixed window icons error on Wayland 2021-12-26 20:36:32 +03:00
ec17eb7065 Added window icons
- Added window icons for various sizes
  - Broken on Wayland
  - Due to a bug in texture loading, textures a flipped
    - Included non-flipped versions
- Added TextureSettings.allocateExactBuffer
  - When true, texture size is not rounded to a power of 2
2021-12-26 14:30:50 +03:00
e4d0570200 Added automatic version detection to Gradle and game 2021-12-25 20:24:45 +03:00
92f9639c4b Improved input event handling
- Added Component.passInputToChildren()
- Input events can now be processed in parallel or recursively
2021-12-21 00:31:35 +03:00
f350467942 Removed jCenter(); formatted build.gradle
- Fixed #23
- Fixed whitespace in build.gradle
2021-12-21 00:13:04 +03:00
23b0af1731 Update log4j dependency to version 2.17.0 😡
Fix for CVE-2021-45105
2021-12-19 23:46:30 +03:00
0389f97e59 Update log4j dependency to version 2.16.0
Fix for CVE-2021-45046
2021-12-16 18:09:37 +03:00
706800218c Merge branch 'overhaul-input'
Summary of changes:
- Refactored input handling
- Added noclip/freecam mode for debugging
- Added drag events
- Removed useless info from debug layer
- Movement controls now work in menus (feature? bug? not sure)
2021-12-16 15:17:08 +03:00
ca2c7b58d8 Added drag events 2021-12-16 15:12:49 +03:00
359879b0fe Completed input refactoring
- Moved everything related to controls into controls package
- Moved controls related to placing/breaking stuff into
InteractionControls
- Localizer now provides access to the list of languages
- Fixed a random bug in TestMusicPlayer because why not amirite
2021-12-15 00:20:50 +03:00
a06d8ee056 Fixed temporary player entity data transfer 2021-12-13 20:49:32 +03:00
c8138faabe Refactored camera controls and added noclip mode
- Refactored camera controls into a separate class
- Added noclip mode
  - Toggle with V
  - Simply uncouples camera from character; does not change interactions
or load chunks!
  - Implemented sloppily, expect bugs in the future
- EntityData's .equals() is now equivalent to == and .hashCode() hashes
entity ID only
- Collision can now be disabled for an object by setting its
CollisionModel to null
2021-12-13 19:36:26 +03:00
49d283e7a3 Refactored F3 layer
- Renamed from LayerTestGUI to LayerDebug
- Many of the displays have been removed
- Fullscreen and VSync displays have been merged with FPS display
- Updates are automatic
2021-12-13 17:03:23 +03:00
c93f0df30d Refactored movement controls
- Movement control code is now in its own class
- Movement direction controls are now more bug-resistant
  - No longer event-driven; uses InputTracker instead
  - Temporarily? function with menus open
- Other movement controls are now registered local Controls
- Removed flying and sprinting indicators from F3 layer
- TestPlayerControls is now a singleton
  - resetInstance() now resets the instance's fields
2021-12-13 11:06:28 +03:00
75ea7baf9c Update junit dependency to version 4.13.2
Fix for CVE-2020-15250
2021-12-12 15:41:10 +03:00
c4a9a17c7c Update log4j dependency to version 2.15.0 2021-12-10 11:05:30 +03:00
b4ff5114bd Refactored input event pipeline
- Merged Input functionality into InputEvent; removed Input
  - InputEvents can now be consumed directly
  - Removed Input.Target; functionality moved into InputBus
- Refactored GUI input event handling
  - Optimized and reduced recursion by unrolling recursion
  - All input events are now delivered to all components
    - Filtering occurs at listener level
- Refactored InputBus
  - Listeners may override former Input.Target logic
  - Improved registration method
  - Added exception handling
- Improved KeyMatcher
  - Removed builder in favour of copying with modifications
  - Added String parsing
  - Added KeyMatcher.matchesIgnoringAction()
  - Added integration with InputBus
- Renamed Component.{add,remove}Listener methods about InputListeners to
Component.{add,remove}InputListener to avoid confusion
2021-12-08 18:06:30 +03:00
efc6a8ecd0 Hastily added pine trees and hazel bushes
- Added ShapePrototype wrapper for ShapePart batch creation
- Added pine trees and hazel bushes
  - Leaves with custom block models
  - Improved tree generator
2021-11-19 21:26:04 +03:00
4b10dc82ed Improved formatting and phrasing 2021-10-31 16:19:21 +03:00
c04894a0c9 Finally made this
I think its pretty good, but I cant think very well rn.
2021-10-24 20:09:44 -04:00
576cfed99f Fixed import warnings 2021-10-17 14:05:40 +03:00
5af1b7309d Better Alignment
-Created some Layouts to properly adjust where things go
    -LayoutEdges for text on the same row on opposite sides of the screen
    -LayoutColumn for a non-max width column
-More accessible version string
2021-10-13 23:37:18 -04:00
0f0a94811f Cleaning
-Cleaned up offsets to make it look like a cube and not 6 squares.
-Fixed error where regions would incorrectly reset files
-Some additions to try to match the intended look. I really dont
understand layouts.
2021-09-25 20:45:17 -04:00
28b19c8f35 Added more flowers and more grass types, fixed cross tile render
- Added Clover, Daisy, Dandelion, Geranium, Knapweed, Yellow Pea,
Bluegrass
- Renamed grass herbs and flat flowers
- Cross tile renderer now uses a kostyl that forces face normals to be
oriented vertically
- Refactored flower generation
- Added a coherent error message when a texture could not be found
2021-09-22 23:48:38 +03:00
51bcca1499 Betterish title screen
-Black background used
	-New Background class for displaying a single texture to the back of
the screen(probably not the best way)
-Title now uses an image
	-Made TextureComponent, I think this probably exists elsewhere but I
couldnt find it
-Cube faces now change color based on where they are facing, looks a lot
like the source material except for rearranging
2021-09-13 13:05:59 -04:00
ce9e95e5ce Well, its [i]a[/i] cube.
-Added CubeComponent to render a cube to the screen
-Added it to the title screen
2021-09-11 21:11:59 -04:00
eb5aa59941 Removed plain sand block and tile; incremented version
To be honest, I kinda forgot to increment version in last commit, so I
decided to make a pointless change to justify an entire commit =\
2021-09-11 21:12:42 +03:00
409bbdb680 Merge branch 'save-world' 2021-09-11 19:42:36 +03:00
a85fc27f8b Added _UNFINISHED_ water, beaches and mantle
- Refactored terrain generation
- Added PiecewiseLinearFunction
- Added some placeholder content
  - Added Test:Water, the solid, opaque water
  - Added beaches
  - Added Test:Mantle
- Tweaked rock distribution parameters
2021-08-31 17:27:08 +03:00
711e4a2bb4 Added formatting templates guide for Intellij IDEA 2021-08-30 19:51:28 +03:00
f74c731a3d Fixed GUI buttons and a rare crash on startup 2021-08-27 12:20:58 +03:00
98c383bf7d Fixed a stupid typo in previous commit 2021-08-26 12:11:21 +03:00
dd80df2cf2 Fixed a GUI reassembly issue and added cursor disabling suppression
- Buttons no longer reassemble every frame
- Label.setFont() no longer requests reassembly
- Component.reassembleAt now actually works
- Using a very long flag, cursor capturing can be disabled to facilitate
GUI debugging
2021-08-26 12:09:22 +03:00
1727a2a4a1 Added Object fields to StatefulObjects and added some utility methods
- Added ObjectStateField
- Added WorldGenericContextRO.findClosestEntity, .forEachEntity
- Added VectorUtil.lookAt, .distance{,Sq}
2021-08-25 16:47:56 +03:00
20fb8f0597 Fixed missing popTrasnform in Statie render 2021-08-24 15:12:35 +03:00
1d28f32865 Implemented entity spawning and despawning and changed some stuff
- Non-player entities can now be added and removed properly
  - WorldLogic.spawnEntity can be used to add entity and create an
entity ID
- Statie is back, more beautiful than ever!
  - Place Test:StatieSpawner block and wait to make her spawn
- TPS display now features a visual tick indicator
- Updated Fern texture
2021-08-24 13:59:28 +03:00
221 changed files with 6615 additions and 2334 deletions

2
.gitattributes vendored
View File

@ -6,4 +6,4 @@
* text=auto eol=lf * text=auto eol=lf
*.bat text eol=crlf *.bat text eol=crlf
*.nsi text eol=crlf

View File

@ -1,23 +1,29 @@
/* /*
* build.gradle for Progressia * Build logic for Progressia
* build.gradle
*
* Please refer to
*
* docs/building/BuildScriptReference.md
*
* for user reference.
*/ */
plugins { plugins {
// Apply the java-library plugin to add support for Java Library id 'java'
id 'java-library'
/* // GrGit
* Uncomment the following line to enable the Eclipse plugin. // A JGit wrapper for Groovy used to access git commit info
* This is only necessary if you don't use Buildship plugin from the IDE id 'org.ajoberstar.grgit' version '4.1.1'
*/
//id 'eclipse'
} }
java {
/*
* We're Java 8 for now. /*
* Why? As of 2020, most users have Oracle Java, which only supports Java 8. * Configure Java version
*/ */
java {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8
} }
@ -34,16 +40,17 @@ compileJava {
} }
} }
/* /*
* Dependencies * Dependencies
*/ */
repositories { repositories {
mavenCentral() mavenCentral()
jcenter()
/* /*
* Specify Windcorp Maven repository * Windcorp Maven repository
* Currently used by: * Currently used by:
* - ru.windcorp.fork.io.github.java-graphics:glm:1.0.1 * - ru.windcorp.fork.io.github.java-graphics:glm:1.0.1
*/ */
@ -66,231 +73,316 @@ dependencies {
// Log4j // Log4j
// A logging library // A logging library
implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.13.3' implementation 'org.apache.logging.log4j:log4j-api:2.17.0'
implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.13.3' implementation 'org.apache.logging.log4j:log4j-core:2.17.0'
// JUnit // JUnit
// A unit-testing library // A unit-testing library
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.13.2'
// See LWJGL dependencies below // Also see LWJGL dependencies in build_logic/lwjgl.gradle
} }
/* /*
* Progressia uses the following LWJGL libraries: * Version resolution
* - Core libraries
* - OpenGL
* - OpenAL
* - GLFW
* - STB
*/ */
/* import org.ajoberstar.grgit.*
* LWJGL
* (auto-generated script)
* ((here be dragons))
*/
import org.gradle.internal.os.OperatingSystem // Pattern: vMAJOR.MINOR.PATCH[-SUFFIX]
project.ext.tagFormat = /^v(\d+)\.(\d+)\.(\d+)(-[\w\-]*)?$/
project.ext.lwjglVersion = "3.2.3" String version_parseVersion(String tag, boolean increment) {
try {
switch (OperatingSystem.current()) { def data = (tag =~ tagFormat)[0]
case OperatingSystem.LINUX:
def osArch = System.getProperty("os.arch")
project.ext.lwjglNatives = osArch.startsWith("arm") || osArch.startsWith("aarch64")
? "natives-linux-${osArch.contains("64") || osArch.startsWith("armv8") ? "arm64" : "arm32"}"
: "natives-linux"
break
case OperatingSystem.MAC_OS:
project.ext.lwjglNatives = "natives-macos"
break
case OperatingSystem.WINDOWS:
project.ext.lwjglNatives = System.getProperty("os.arch").contains("64") ? "natives-windows" : "natives-windows-x86"
break
}
dependencies { def major = data[1]
implementation platform("org.lwjgl:lwjgl-bom:$lwjglVersion") def minor = data[2]
def patch = data[3] as int
def suffix = data[4] ?: ''
implementation "org.lwjgl:lwjgl" if (increment) {
implementation "org.lwjgl:lwjgl-glfw" def oldVersion = "$major.$minor.$patch$suffix"
implementation "org.lwjgl:lwjgl-openal" patch++
implementation "org.lwjgl:lwjgl-opengl" def newVersion = "$major.$minor.$patch$suffix"
implementation "org.lwjgl:lwjgl-stb"
runtimeOnly "org.lwjgl:lwjgl::$lwjglNatives" logger.info "Version parsed from Git: $oldVersion, incremented to $newVersion"
runtimeOnly "org.lwjgl:lwjgl-glfw::$lwjglNatives" return newVersion
runtimeOnly "org.lwjgl:lwjgl-openal::$lwjglNatives"
runtimeOnly "org.lwjgl:lwjgl-opengl::$lwjglNatives"
runtimeOnly "org.lwjgl:lwjgl-stb::$lwjglNatives"
}
// LWJGL END
/*
* Tasks
*/
/*
* Additional native libraries specification
*/
project.ext.platforms = new HashSet<>()
task addNativeDependencies {
doFirst {
def archs = project.ext.platforms
switch (archs.size()) {
case 0:
println "Adding LWJGL native dependencies for local platform only:\n\t$lwjglNatives"
archs.add project.ext.lwjglNatives
break
case 1:
println "Adding LWJGL native dependencies for platform\n\t" + archs.get(0)
break
default:
println "Adding LWJGL native dependencies for platforms:\n\t" + archs.join("\n\t")
}
if (project.ext.lwjglNatives.isEmpty()) println "WTF"
dependencies {
archs.each { arch ->
runtimeOnly "org.lwjgl:lwjgl::$arch"
runtimeOnly "org.lwjgl:lwjgl-glfw::$arch"
runtimeOnly "org.lwjgl:lwjgl-openal::$arch"
runtimeOnly "org.lwjgl:lwjgl-opengl::$arch"
runtimeOnly "org.lwjgl:lwjgl-stb::$arch"
}
}
}
}
compileJava.mustRunAfter addNativeDependencies // Make sure runtimeOnly has not been resolved
task requestLinuxDependencies {
description 'Adds linux, linux-arm64 and linux-arm32 native libraries to built artifacts.'
doFirst {
project.ext.platforms.addAll(['natives-linux', 'natives-linux-arm64', 'natives-linux-arm32'])
}
}
task requestWindowsDependencies {
description 'Adds windows and windows-x86 native libraries to built artifacts.'
doFirst {
project.ext.platforms.addAll(['natives-windows', 'natives-windows-x86'])
}
}
task requestMacOSDependencies {
description 'Adds macos native libraries to built artifacts.'
doFirst {
project.ext.platforms.addAll(['natives-macos'])
}
}
def dependencySpecificationTasks = tasks.findAll { task -> task.name.startsWith('request') && task.name.endsWith('Dependencies') }
task requestCrossPlatformDependencies {
description 'Adds native libraries for all available platforms to built artifacts.'
dependsOn dependencySpecificationTasks
}
addNativeDependencies.mustRunAfter dependencySpecificationTasks
/*
* Determines if the provided dependency should be packaged
*/
def isDependencyRequested(String dep) {
if (dep.endsWith(".jar")) {
dep = dep.substring(0, dep.length() - ".jar".length())
}
return !dep.contains("natives-") ||
project.ext.platforms.contains(dep.substring(dep.indexOf("natives-"), dep.length()))
}
/*
* Manifest specification
*/
task specifyLocalManifest {
dependsOn addNativeDependencies // Make sure all native dependencies are specified
doFirst {
def classPath = []
configurations.runtimeClasspath.each {
if (isDependencyRequested(it.getName())) {
classPath.add("lib/" + it.getName())
} else { } else {
println "\tRemoving from JAR classpath (not requested): " + it.getName() def newVersion = "$major.$minor.$patch$suffix"
logger.info "Version parsed from Git: $newVersion"
return newVersion
}
} catch (any) {
logger.warn "Could not parse version from tag \"$tag\""
return tag
}
}
Tag version_findRelevantTag(Grgit git) {
def tags = git.tag.list()
def commits = [ git.head() ]
def visited = new HashSet<>()
while (true) {
if (commits.isEmpty()) return null
def nextCommits = new HashSet<>()
def formatSpecificationPrinted = false
for (def commit : commits) {
def tag = tags.findAll { it.commit == commit } ?.max { it.dateTime }
if (tag != null) {
if (tag.name ==~ tagFormat) {
return tag
} else {
if (!formatSpecificationPrinted) {
formatSpecificationPrinted = true
logger.info 'Expecting tag format: vMAJOR.MINOR.PATCH[-SUFFIX]'
}
logger.info 'Ignoring tag due to invalid format: {}', tag.name
} }
} }
if (classPath.size() == configurations.runtimeClasspath.size()) { nextCommits.addAll commit.parentIds.collect(git.resolve.&toCommit)
println "Nothing removed from JAR classpath"
} }
jar { visited.addAll commits
manifest { nextCommits.removeAll visited
attributes( commits = nextCommits
"Main-Class": "ru.windcorp.progressia.client.ProgressiaClientMain", }
"Class-Path": configurations.runtimeClasspath.collect { "lib/" + it.getName() } .findAll { isDependencyRequested(it) } .join(' ') }
task resolveVersion {
description 'Resolves version information from Git repository or project properties.'
doFirst {
try {
def git = Grgit.open(dir: project.projectDir)
project.ext.commit = git.head().id
project.ext.branch = git.branch.current().name
if (project.version != 'unspecified') {
// Leave version as-is
return
}
def tag = version_findRelevantTag(git)
if (tag == null) {
String suffix
if (project.hasProperty('buildId')) {
suffix = project.buildId;
} else {
suffix = java.time.ZonedDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern('yyyy_MM_dd'))
}
project.version = "999.0.0-$suffix"
logger.warn 'Git repository does not contain an applicable tag, using dummy version {}\nSpecify version with -Pversion=1.2.3 or create a Git tag named v1.2.3', project.version
} else {
project.version = version_parseVersion(tag.name, tag.commit != git.head())
}
} catch (org.eclipse.jgit.errors.RepositoryNotFoundException e) {
if (project.version == 'unspecified') project.version = 'dev'
project.ext.commit = '-'
project.ext.branch = '-'
logger.warn 'No Git repository found in project root, using dummy version {}\nSpecify version with -Pversion=1.2.3 or create a Git tag named v1.2.3', project.version
}
}
doLast {
if (!project.hasProperty('buildId')) {
project.ext.buildId = '-'
}
}
}
/*
* Configure JAR manifest
*/
task configureManifest {
description 'Populates JAR manifest with Main-Class, Class-Path and version metadata.'
jar.dependsOn configureManifest
dependsOn resolveVersion
doFirst {
jar.manifest.attributes(
'Main-Class': 'ru.windcorp.progressia.client.ProgressiaClientMain',
'Class-Path': configurations.runtimeClasspath.collect { "lib/${it.name}" } .join(' '),
'Specification-Title': 'Progressia',
'Implementation-Title': 'Progressia',
'Implementation-Version': project.version,
'Implementation-Version-Git-Commit': project.commit,
'Implementation-Version-Git-Branch': project.branch,
'Implementation-Version-BuildId': project.buildId,
) )
} }
}
}
} }
jar.dependsOn specifyLocalManifest
/* /*
* Library export * Copy libraries into buil/libs/lib directory, next to Progressia.jar
*/ */
task exportLibs(type: Sync) { task exportLibs(type: Sync) {
mustRunAfter addNativeDependencies description 'Copies runtime libraries into a subdirectory next to the output JAR.'
jar.dependsOn exportLibs
into libsDirectory.get().getAsFile().getPath() + "/lib"
exclude { !isDependencyRequested(it.getName()) }
from configurations.runtimeClasspath from configurations.runtimeClasspath
into 'build/libs/lib'
} }
jar.dependsOn(exportLibs)
/* /*
* Packaging * Apply LWJGL logic
*/
apply from: 'build_logic/lwjgl.gradle'
/*
* Packaging working directory configuration
*/ */
task packageDebian(type: Exec) { import java.nio.file.*
description 'Builds the project and creates a Debain package.' import java.nio.file.attribute.*
group 'Progressia'
dependsOn build task createPackagingDirs() {
dependsOn requestLinuxDependencies description 'Resets build/tmp/packaging directory.'
commandLine './buildPackages.sh', 'debian'
doLast { doLast {
println "Debian package available in build_packages/" def tmpDir = buildDir.toPath().resolve 'tmp/packaging'
def nuke = new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException {
if (e == null) {
Files.delete(dir);
return FileVisitResult.CONTINUE;
} else {
// directory iteration failed
throw e;
}
}
}
// Fckn nuke tmpDir from orbit
// I'm so done with deleting file trees in Java/Groovy/whatever
// ...Not using File.deleteDir() because the latter recurses into symlinks, and we don't want to wipe build/libs/lib
if (Files.exists(tmpDir)) {
Files.walkFileTree tmpDir, nuke
}
Files.createDirectories tmpDir.resolve('workingDir')
Files.createDirectories buildDir.toPath().resolve('packages')
} }
} }
task packageWindows(type: Exec) { task linkBuildOutputForPackaging() {
description 'Builds the project and creates a Windows installer.' description 'Symlinks the contents of build/libs into packaging working directory.'
group 'Progressia'
dependsOn build dependsOn build
dependsOn requestWindowsDependencies dependsOn createPackagingDirs
commandLine './buildPackages.sh', 'windows' onlyIf { preparePackaging.ext.mode == 'symlink' }
doLast { doLast {
println "Windows installer available in build_packages/" def from = buildDir.toPath().resolve 'libs'
def into = buildDir.toPath().resolve "tmp/packaging/workingDir/${preparePackaging.ext.buildDest}"
Files.createDirectories into
Files.list(from).each {
def fileName = it.fileName.toString()
// Exclude all JARs except the current one
if (fileName ==~ "${project.name}.*\\.jar" && fileName != tasks.jar.archiveFileName.get())
return
Files.createSymbolicLink into.resolve(it.fileName), it
}
} }
} }
task copyBuildOutputForPackaging(type: Copy) {
description 'Copies the contents of build/libs into packaging working directory.'
dependsOn build
dependsOn createPackagingDirs
onlyIf { preparePackaging.ext.mode == 'copy' }
from 'build/libs'
filesMatching("${project.name}*.jar") {
include tasks.jar.archiveFileName.get()
}
into "build/tmp/packaging/workingDir/${ -> preparePackaging.ext.buildDest}"
}
task preparePackaging {
preparePackaging.ext.buildDest = ''
preparePackaging.ext.mode = 'symlink'
dependsOn createPackagingDirs
dependsOn linkBuildOutputForPackaging
dependsOn copyBuildOutputForPackaging
}
/*
* Apply all packaging scripts
*/
new File(projectDir, 'build_logic/packaging').list().each {
apply from: "build_logic/packaging/$it/script.gradle"
}
/*
* Ensure no more than one packaging task is scheduled
*/
gradle.taskGraph.whenReady { graph ->
if (graph.allTasks.count { it.name ==~ /package[^_]*/ } > 1) {
def offenders = graph.allTasks.findAll { it.name ==~ /package[^_]*/ }
throw new GradleException("Cannot execute multiple package tasks within a single build\n" +
"\tOffending tasks: $offenders")
}
}
/*
* Convenience build tasks
*/
task buildCrossPlatform { task buildCrossPlatform {
description 'Builds the project including native libraries for all available platforms.' description 'Builds the project including native libraries for all available platforms.'
group 'Progressia' group 'Progressia'
@ -299,17 +391,17 @@ task buildCrossPlatform {
dependsOn build dependsOn build
doLast { doLast {
println "Native libraries for all platforms have been added" logger.info 'Native libraries for all platforms have been added'
} }
} }
task buildLocal { task buildLocal {
description "Builds the project including only native libraries for current platform ($lwjglNatives)." description "Builds the project including only native libraries for current platform (${lwjgl.localArch})."
group 'Progressia' group 'Progressia'
dependsOn build dependsOn build
doLast { doLast {
println "Native libraries only for platform $lwjglNatives have been added" logger.info "Native libraries only for platform ${lwjgl.localArch} have been added"
} }
} }

View File

@ -1,191 +0,0 @@
#!/bin/bash
#
# Progressia
# Copyright (C) 2020-2021 Wind Corporation and contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
echoerr() { echo "$@" 1>&2; }
buildDebianPackage() {
# Commands that must be available to execute this action
requiredCommands='dpkg-deb fakeroot'
# Package name. Sync with control file manually!
name='progressia-techdemo'
# Version that the package will receive. Sync with control file manually!
version='1.0_all'
# This directory will be copied into $tmpDir
templateDirectory="build_packages/DEB/template"
# Files that must be present
requiredFiles="$templateDirectory/DEBIAN/control"
nameAndVersion="$name-$version"
tmpDir="build_packages/DEB/$nameAndVersion"
outputFile="build_packages/DEB/$nameAndVersion.deb"
echo "Checking environment to build Debian package"
for item in $requiredCommands; do
if command -v "$item" &> /dev/null; then
echo "- $item found"
else
echoerr "Command $item not found, cannot package"
exit 100
fi
done
for file in $requiredFiles; do
if ! [ -r "$file" ]; then
echoerr "$file is missing or not readable, cannot package"
exit 101
else
echo "- $file is present and readable"
fi
done
echo "Environment OK; packaging Debian package"
exitCode=0
{
shareDir="$tmpDir/usr/share/progressia"
mkdir -p "$tmpDir" &&
mkdir -p "$shareDir" &&
cp -r "$templateDirectory"/* "$tmpDir" &&
cp -r 'build/libs/lib' "$shareDir/lib" &&
cp 'build/libs/Progressia.jar' "$shareDir/Progressia.jar" &&
echo "------ DPKG-DEB ------" &&
fakeroot dpkg-deb --build "$tmpDir" &&
echo "---- DPKG-DEB END ----" &&
mv "$outputFile" build_packages
} || {
echoerr "Could not create Debian package"
exitCode=1
}
{
if [ -d "$tmpDir" ]; then
rm -r "$tmpDir"
fi
echo "Cleaned up"
} || {
echoerr "Could not clean up after packaging Debian package"
exitCode=2
}
exit "$exitCode"
}
buildWindowsInstaller() {
# Commands that must be available to execute this action
requiredCommands='makensis'
# NSIS configuration file that must be present
configurationFile='build_packages/NSIS/ProgressiaInstaller.nsi'
# File that will be output
outputFile='build_packages/NSIS/ProgressiaInstaller.exe'
echo "Checking environment to build Windows installer"
for item in $requiredCommands; do
if command -v "$item" &> /dev/null; then
echo "- $item found"
else
echoerr "Command $item not found, cannot build"
exit 100
fi
done
if ! [ -r "$configurationFile" ]; then
echoerr "$configurationFile is missing or not readable, cannot build"
exit 101
else
echo "- $configurationFile is present and readable"
fi
echo "Environment OK; building Windows installer"
exitCode=0
{
cp -r 'build/libs/lib' 'build_packages/NSIS/lib' &&
cp 'build/libs/Progressia.jar' 'build_packages/NSIS/Progressia.jar' &&
cp 'LICENSE' 'build_packages/NSIS/LICENSE.txt' &&
echo "------ NSIS ------" &&
makensis "$configurationFile" &&
echo "---- NSIS END ----" &&
mv "$outputFile" build_packages
} || {
echoerr "Could not build Windows installer"
exitCode=1
}
{
if [ -d 'build_packages/NSIS/lib' ]; then
rm -r 'build_packages/NSIS/lib'
fi
if [ -e 'build_packages/NSIS/Progressia.jar' ]; then
rm 'build_packages/NSIS/Progressia.jar'
fi
if [ -e 'build_packages/NSIS/LICENSE.txt' ]; then
rm 'build_packages/NSIS/LICENSE.txt'
fi
echo "Cleaned up"
} || {
echoerr "Could not clean up after building Windows installer"
exitCode=2
}
exit "$exitCode"
}
printUsage() {
echoerr "Usage: $0 TARGET"
echoerr " where TARGET is 'debian' or 'windows'"
}
if [ -n "$2" ]; then
echoerr "Too many arguments."
printUsage
exit 202
fi
case "$1" in
"debian")
buildDebianPackage
;;
"windows")
buildWindowsInstaller
;;
"")
echoerr "No action specified"
printUsage
exit 200
;;
"--help" | "-help" | "help" | "?")
printUsage
;;
*)
echoerr "Unknown action '$1'"
printUsage
exit 201
;;
esac

117
build_logic/lwjgl.gradle Normal file
View File

@ -0,0 +1,117 @@
/*
* Build logic for Progressia
* LWJGL dependency logic
*/
project.ext.lwjgl = new HashMap<>()
// Version of LWJGL
lwjgl.version = '3.2.3'
/*
* Target platforms for current operation.
* This is filled in by the request* tasks. This is referenced by the addLwjglNatives task.
* When empty, current platform is assumed.
*/
lwjgl.targets = new HashSet<>()
// LWJGL components. To include org.lwjgl:lwjgl-foobar, add 'foobar' to this list.
lwjgl.libraries = [
'opengl',
'glfw',
'openal',
'stb'
]
// Determine the architecture of the build environment
import org.gradle.internal.os.OperatingSystem
switch (OperatingSystem.current()) {
case OperatingSystem.LINUX:
def osArch = System.getProperty('os.arch')
lwjgl.localArch = osArch.startsWith('arm') || osArch.startsWith('aarch64')
? "linux-${osArch.contains('64') || osArch.startsWith('armv8') ? 'arm64' : 'arm32'}"
: 'linux'
break
case OperatingSystem.MAC_OS:
lwjgl.localArch = 'macos'
break
case OperatingSystem.WINDOWS:
lwjgl.localArch = System.getProperty('os.arch').contains('64') ? 'windows' : 'windows-x86'
break
}
// Declare pure-Java dependencies
dependencies {
// BOM
implementation platform("org.lwjgl:lwjgl-bom:${lwjgl.version}")
// Core
implementation 'org.lwjgl:lwjgl'
// Components
lwjgl.libraries.each { implementation "org.lwjgl:lwjgl-$it" }
}
/*
* Adds LWJGL native libraries to runtimeOnly configuration
*/
task lwjgl_addNativesToRuntimeOnly {
// Make sure runtimeOnly has not been resolved
compileJava.dependsOn lwjgl_addNativesToRuntimeOnly
configureManifest.dependsOn lwjgl_addNativesToRuntimeOnly
exportLibs.dependsOn lwjgl_addNativesToRuntimeOnly
doFirst {
if (project.hasProperty('forceTargets')) {
try {
def oldTargets = lwjgl.targets.join(',')
lwjgl.targets.clear()
lwjgl.targets.addAll project.forceTargets.split(',')*.trim().collect { it == 'local' ? lwjgl.localArch : it }
logger.info 'Overriding selected platforms {} with {}', oldTargets, lwjgl.targets.join(',')
} catch (Exception e) {
throw new GradleException("Could not parse forceTargets \"${project.forceTargets}\", expecting platform-1,platform-2,local", e)
}
}
if (lwjgl.targets.isEmpty()) {
logger.info 'Adding LWJGL native dependencies for local platform only: {}', lwjgl.localArch
lwjgl.targets.add lwjgl.localArch
} else {
logger.info 'Adding LWJGL native dependencies for platforms: {}', lwjgl.targets.sort().join(', ')
}
dependencies {
lwjgl.targets.each { target ->
runtimeOnly "org.lwjgl:lwjgl::natives-$target"
lwjgl.libraries.each { lib ->
runtimeOnly "org.lwjgl:lwjgl-$lib::natives-$target"
}
}
}
}
}
task requestCrossPlatformDependencies {
description 'Adds LWJGL natives for all available platforms.'
lwjgl_addNativesToRuntimeOnly.mustRunAfter requestCrossPlatformDependencies
}
def requestTask(String name, String... targets) {
def theTask = task "request${name}Dependencies"
theTask.doFirst {
lwjgl.targets.addAll targets
}
theTask.description "Adds LWJGL natives for $name (${targets.join(', ')})."
requestCrossPlatformDependencies.dependsOn theTask
lwjgl_addNativesToRuntimeOnly.mustRunAfter theTask
}
requestTask 'Linux', 'linux', 'linux-arm32', 'linux-arm64'
requestTask 'Windows', 'windows', 'windows-x86'
requestTask 'MacOS', 'macos'

View File

@ -0,0 +1,38 @@
task packageDeb_processResources(type: Copy) {
dependsOn resolveVersion
dependsOn preparePackaging
from 'src/packaging/deb'
filesMatching('DEBIAN/control') {
expand(version: { -> project.version})
}
into 'build/tmp/packaging/workingDir'
}
task packageDeb_configure() {
preparePackaging.mustRunAfter packageDeb_configure
doLast {
tasks.preparePackaging.ext.buildDest = '/usr/share/progressia'
tasks.preparePackaging.ext.mode = 'copy'
}
}
task packageDeb(type: Exec) {
description 'Builds the project and creates a Debian package.'
group 'Progressia'
dependsOn packageDeb_configure
dependsOn requestLinuxDependencies
dependsOn build
dependsOn preparePackaging
dependsOn packageDeb_processResources
executable 'dpkg-deb'
args '--root-owner-group'
args '--build', 'build/tmp/packaging/workingDir'
args 'build/packages'
}

View File

@ -0,0 +1,50 @@
task packageNsis_processResources(type: Copy) {
dependsOn preparePackaging
from ('src/packaging/nsis') {
exclude 'left_side.png'
}
from('LICENSE') {
rename 'LICENSE', 'LICENSE.txt'
}
into 'build/tmp/packaging/workingDir'
}
task packageNsis_generateIcon(type: Exec) {
mustRunAfter preparePackaging
executable 'convert'
args files('src/main/resources/assets/icons/*.original.png').files*.path
args 'build/tmp/packaging/workingDir/logo.ico'
}
task packageNsis_generateLeftSide(type: Exec) {
mustRunAfter preparePackaging
executable 'convert'
args 'src/packaging/nsis/left_side.png'
args '-alpha', 'off'
args 'BMP3:build/tmp/packaging/workingDir/left_side.bmp'
}
task packageNsis(type: Exec) {
description 'Builds the project and creates a Windows NSIS installer.'
group 'Progressia'
dependsOn requestWindowsDependencies
dependsOn build
dependsOn resolveVersion
dependsOn preparePackaging
dependsOn packageNsis_processResources
dependsOn packageNsis_generateIcon
dependsOn packageNsis_generateLeftSide
executable 'makensis'
args '-NOCONFIG'
args "-DPROJECT_NAME=${project.name}"
args "-DPROJECT_VERSION=${ -> project.version}"
args "-DMAIN_JAR_FILE=${ -> project.tasks.jar.archiveFileName.get()}"
args "-DOUTPUT_DIR=${project.buildDir.absolutePath}/packages"
args 'build/tmp/packaging/workingDir/config.nsi'
}

View File

@ -0,0 +1,41 @@
task packageZip_processResources(type: Copy) {
dependsOn preparePackaging
from ('src/packaging/zip') {
filesMatching('start.*') {
filter(
org.apache.tools.ant.filters.ReplaceTokens,
tokens: [mainJarFile: project.tasks.jar.archiveFileName.get()]
)
}
}
from ('src/main/resource/assets/icons/logo256.original.png') {
rename 'logo256.original.png', 'logo.png'
}
from('LICENSE') {
rename 'LICENSE', 'LICENSE.txt'
}
into 'build/tmp/packaging/workingDir'
}
task packageZip(type: Zip) {
description 'Builds the project and creates a cross-platform ZIP package.'
group 'Progressia'
dependsOn resolveVersion
dependsOn requestCrossPlatformDependencies
dependsOn build
dependsOn preparePackaging
dependsOn packageZip_processResources
archiveBaseName = project.name
archiveAppendix = 'universal'
doFirst {
archiveVersion = project.version
}
from 'build/tmp/packaging/workingDir'
destinationDirectory = file('build/packages')
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

12
docs/ProgressiaRegion.md Normal file
View File

@ -0,0 +1,12 @@
# Progressia Region File
## Description
The `.progressia_region` file type is used for all region files in the game Progressia. Each region file contains a cube of 16x16x16 chunks.
## Header
The header of the file is 16&nbsp;400 bytes. Every file starts with the string byte sequence `\x50\x52\x4F\x47` (UTF-8 for `PROG`), followed with the three integer values of the region position, in region coordinates. After this, there is exactly 16KiB of space in the header, which stores the offsets to the chunks' data. This space holds an integer, 4 bytes, for each chunk in the region. The integer value starts at 0 for every chunk, and is changed to the location of the chunk data once created. These are indexed in order by flattening the 3D in-chunk coordinates into a number between 0 and 4095 according to the formula `offset = 256*x+ 16*y + z` for chunk at (x, y, z). To convert from this offset value to the offset in bytes, use `byte_offset = 16400 + 64*n`.
## Sectors
Sectors are what is used to store chunk data, and are not linear, but are followed until they reach an ending block. Each is 64 bytes, which is used in the header section to find the byte offset. Each sector starts with a identification byte, followed by the sector data.
0. Ending - This sector is empty, and marks the end of the chunk data (This may change in the future.
1. Data - This sector contains chunk data for a single chunk. The second byte of this sector contains a counter byte, which is a form of "checksum" to make sure that the program is reading the proper sectors in order. This starts at 0 for the first data sector and increments by one for each new data sector.
2. Partition Link - This sector only contains another offset value, which is where the next sector is. This allows for infinite chunk size, avoiding "chunk dupes" as were present in Minecraft without reverting any chunks.
3. Bulk Data - These would be used for many chunks in the same region that contain exactly the same data, e.g. all solid chunks underground. Exists so the program knows not to overwrite them, and just make new chunk data if modified. Not yet implemented.

View File

@ -1,8 +1,10 @@
# Build Guide # Build Guide
This document is a guide to building Progressia from source. This document is a guide to building Progressia from source. For quick reference, see
[Build Script Reference](BuildScriptReference.md).
Compilation should be possible on all platforms that support JDK 8 or later, however, packaging scripts require Bash. Compilation should be possible on all platforms that support JDK 8 or later, however, packaging scripts require
additional programs in `PATH`.
This guide assumes you are familiar with using a terminal or Windows Command Prompt or PowerShell. This guide assumes you are familiar with using a terminal or Windows Command Prompt or PowerShell.
@ -150,54 +152,46 @@ GNU/Linux and Windows natives:
./gradlew build requestLinuxDependencies requestWindowsDependencies ./gradlew build requestLinuxDependencies requestWindowsDependencies
``` ```
For finer control please edit `build.gradle` manually by adding the desired natives to the `project.ext.platforms` set like so:
```
project.ext.platforms = new HashSet<>()
project.ext.platforms.add 'natives-windows-x86'
```
## Packaging ## Packaging
A Debian package and a Windows installer can be created automatically on systems that support Bash. These tasks are delegated A universal ZIP distribution, a Debian package and a Windows NSIS installer may be created automatically by the build
by Gradle to `buildPackages.sh` in repository root. This script checks the environment and assembles the requested output; the script.
resulting files are moved into `build_packages`.
### Creating a universal ZIP package
A universal cross-platform ZIP archive can be created with the following Gradle task:
```
./gradlew packageZip
```
Gradle will then build all artifacts necessary to run the game on all available platforms and package game files,
libraries, launch scripts, etc. into a compressed ZIP archive.
The resulting file can be found in `build/packages/`
### Creating a Debian package ### Creating a Debian package
A Debian package can be created with the following Gradle task: A Debian package can be created with the following Gradle task:
``` ```
./gradlew packageDebian ./gradlew packageDeb
``` ```
Gradle will then build all artifacts necessary to run the game on GNU/Linux (all three architectures) and invoke Gradle will then build all artifacts necessary to run the game on GNU/Linux (all three architectures) and invoke
`./buildPackages.sh debian`. Commands `dpkg-deb` and `fakeroot` must be available in system path in order to build the package. `dpkg-deb`. Commands `dpkg-deb` must be available in system path in order to build the package.
### Creating a Windows installer ### Creating a Windows installer
A Windows installer can be created with the following Gradle task: A Windows NSIS installer can be created with the following Gradle task:
``` ```
./gradlew packageWindows ./gradlew packageNsis
``` ```
Gradle will then build all artifacts necessary to run the game on Windows (both x64 and x86 architectures) and invoke Gradle will then build all artifacts necessary to run the game on Windows (both x64 and x86 architectures) and invoke
`./buildPackages.sh windows`. `makensis`.
Windows installers are implemented with [NSIS](https://nsis.sourceforge.io/). Command `makensis` must be available in system Windows installers are implemented with [NSIS](https://nsis.sourceforge.io/). [ImageMagick](https://imagemagick.org),
path in order to build the installer. a command-line image editing tool, is used to generate some assets for the installer. Commands `makensis` and
`convert` (from ImageMagick) must be available in system path in order to build the installer.
## Gradle tasks summary
- `buildLocal` creates a build optimized for current platform. Use this to quickly build the game during development.
- `buildCrossPlatform` creates a build that supports all known architectures. Use this to build a universal version of the game.
- `build` currently a synonym of `buildLocal`; creates a default build.
- `packageDebian` creates a Debian package. Do not invoke together with `packageWindows`.
- `packageWindows` creates a Windows installer. Do not invoke together with `packageDebian`.
- `requestLinuxDependencies` requests that `natives-linux`, `natives-linux-arm32` and `natives-linux-arm64` binaries are included when building.
- `requestWindowsDependencies` requests that `natives-windows` and `natives-windows-x86` binaries are included when building.
- `requestMacOSDependencies` requests that `natives-macos` binaries are included when building.
- `requestCrossPlatformDependencies` requests that all binaries are included when building.
All other basic and Java-related Gradle tasks are available as well.

View File

@ -0,0 +1,103 @@
# Build Script Reference
This document is a user's reference for the build script of Progressia. For a beginner-friendly guide, see
[Build Guide](BuildGuide.md).
## Gradle tasks summary
- `buildLocal` creates a build optimized for current platform. Use this to quickly build the game during development.
- `buildCrossPlatform` creates a build that supports all known architectures. Use this to build a universal version of the game.
- `build` currently a synonym of `buildLocal`; creates a default build.
- `packageZip` creates a universal ZIP. Incompatible with other `package` tasks.
- `packageDeb` creates a Debian package. Incompatible with other `package` tasks.
- `packageNsis` creates a Windows NSIS installer. Incompatible with other `package` tasks.
- `requestLinuxDependencies` requests that `natives-linux`, `natives-linux-arm32` and `natives-linux-arm64` binaries are included when building.
- `requestWindowsDependencies` requests that `natives-windows` and `natives-windows-x86` binaries are included when building.
- `requestMacOSDependencies` requests that `natives-macos` binaries are included when building.
- `requestCrossPlatformDependencies` requests that all binaries are included when building.
To execute a task, run `./gradlew <task-name>`.
`build`-type tasks output the executable JAR and all libraries required at runtime into `build/libs`. `package`-type
tasks output packages into `build/packages`.
## Packaging tasks
Some packaging tasks require additional software in `PATH`.
| Task | Commands required in `PATH` |
|---------------|------------------------------------------|
| `packageDeb` | `dpkg-deb` |
| `packageZip` | _none_ |
| `packageNsis` | `makensis`, `convert` (from ImageMagick) |
## Version and metadata
### Version scheme
Progressia builds are identified by four parameters: version, Git commit, Git branch and build ID.
Versions roughly follow [semantic versioning](https://semver.org/spec/v2.0.0.html), with each version fitting the
`MAJOR.MINOR.PATCH[-SUFFIX]` pattern. Depending on the build environment (see below), version is either "real" with
no metadata (e.g. `0.43.2` or `1.2.1-beta`) or a dummy fallback with build metadata (e.g. `999.0.0-2021_07_23` or
`999.0.0-WJ3`).
### Version detection
Build script considers three scenarios when determining the version:
1. `version` project property is set explicitly. This may be done in a variety of ways, for example with command line
argument `-Pversion=1.2.3`
(see [Gradle docs](https://docs.gradle.org/current/userguide/build_environment.html#sec:project_properties))
2. Local Git repository is found, and HEAD is tagged appropriately: version is the tag name with leading `v`
stripped. Example: `v1.2.3` is version `1.2.3`
3. Local Git repository is found, and some ancestor of HEAD is tagged appropriately: version is the tag name with
leading `v` stripped and PATCH incremented by one. Example: `v1.2.3` is version `1.2.4`
Tags not named like `vMAJOR.MINOR.PATCH[-SUFFIX]` are ignored for cases 2 and 3.
In all other cases, a fallback dummy value is used for version, appended with build ID or current date.
### Git metadata
Git commit and Git branch are correspond to the state of the local Git repository, if any. In case Git metadata is
unavailable, `-` fallback is used for both fields.
### Build ID
Build ID uniquely identifies artifacts produced by automated build systems. For example, builds executed by WindCorp
Jenkins suite have build IDs like `WJ3` or `WJ142`. Build ID must be provided explicitly; it is `-` unless specified
otherwise.
Build ID may be set with `buildId` project property. This may be done in a variety of ways, for example with command
line argument `-PbuildId=WJ3`
(see [Gradle docs](https://docs.gradle.org/current/userguide/build_environment.html#sec:project_properties)).
## Native libraries
LWJGL uses native libraries. Build script declares platform-specific dependencies based on the set of target
platforms, `project.ext.lwjgl.targets` (aka `lwjgl.targets`). These dependencies are added to `runtimeOnly`
configuration.
When this set is empty, the script selects natives for current platform. Otherwise, all platforms in the set are
included.
`lwjgl.targets` is populated automatically by packaging tasks and by `buildCrossPlatform`. To add extra targets,
``requestXxxDependencies` tasks may be used.
Target selection mechanism may be overridden with `forceTargets` project property. This may be done in a variety of
ways, for example with command line argument `-PforceTargets=windows-x86,local`
(see [Gradle docs](https://docs.gradle.org/current/userguide/build_environment.html#sec:project_properties)). The
value is a comma-separated list of target architectures. `local` target will be replaced with the automatically
detected current architecture.
### Available targets
| Name | Task |
|---------------|------------------------------|
| `linux` | `requestLinuxDependencies` |
| `linux-arm32` | `requestLinuxDependencies` |
| `linux-arm64` | `requestLinuxDependencies` |
| `windows` | `requestWindowsDependencies` |
| `windows-x86` | `requestWindowsDependencies` |
| `macos` | `requestMacOSDependencies` |

View File

@ -42,3 +42,16 @@ Run configurations are used by Intellij IDEA to specify how a project must be ru
9. Click 'Apply' to save changes. 9. Click 'Apply' to save changes.
Step 8 is required to specify that the game must run in some directory other than the project root, which is the default in Intellij IDEA. Step 8 is required to specify that the game must run in some directory other than the project root, which is the default in Intellij IDEA.
### Applying formatting templates
Windcorp's Progressia repository is formatted with a style defined for Eclipse IDE (sic) in
`templates_and_presets/eclipse_ide`.
Please apply these templates to the project to automatically format the source in a similar fashion.
1. In project context menu, click 'File->Properties'. (`Ctrl+Alt+S`)
2. In 'Editor' > 'Code Style' > 'Java', press gear icon, then click 'Import Scheme' > 'Eclipse code style'
3. In Scheme select 'Project'
4. Open the file `templates_and_presets/eclipse_ide/FormatterProfile.xml` in 'Select Path'.
5. Inside 'Import Scheme' widow click 'Current Scheme' check box after press OK

1
logs/game.log Normal file
View File

@ -0,0 +1 @@
22:26:25.948 [Music Thread ] WARN ru.windcorp.progressia.test.TestMusicPlayer > No music found

View File

@ -18,6 +18,177 @@
package ru.windcorp.progressia; package ru.windcorp.progressia;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import org.apache.logging.log4j.LogManager;
import ru.windcorp.progressia.common.util.crash.CrashReports;
/**
* A class providing access to build metadata.
*/
public class Progressia { public class Progressia {
private static final String NAME = "Progressia";
private static String version;
private static String gitCommit;
private static String gitBranch;
private static String buildId;
static {
try {
Manifest manifest = findManifest();
if (manifest == null) {
setDevelopmentMetadata();
LogManager.getLogger().info(
"Manifest with Specification-Title not found. "
+ "Either you are in a development environment or something has gone horribly wrong with classloaders."
);
} else {
fillMetadata(manifest);
}
} catch (Throwable t) {
CrashReports.crash(t, "Something went wrong while loading metadata");
}
}
private static Manifest findManifest() {
try {
Enumeration<URL> resources = Progressia.class.getClassLoader().getResources("META-INF/MANIFEST.MF");
Collection<IOException> exceptions = new ArrayList<>();
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
try {
Manifest manifest = new Manifest(url.openStream());
Attributes mainAttributes = manifest.getMainAttributes();
if (NAME.equals(mainAttributes.getValue("Specification-Title"))) {
return manifest;
}
} catch (IOException e) {
exceptions.add(e);
}
}
if (exceptions.isEmpty()) {
return null;
}
IOException scapegoat = null;
for (IOException e : exceptions) {
if (scapegoat == null) {
scapegoat = e;
} else {
scapegoat.addSuppressed(e);
}
}
throw CrashReports.report(scapegoat, "Could not read manifest");
} catch (IOException e) {
throw CrashReports.report(e, "Could not read manifest");
}
}
private static void setDevelopmentMetadata() {
version = "dev";
gitCommit = "-";
gitBranch = "-";
buildId = "-";
}
private static void fillMetadata(Manifest manifest) {
version = getAttributeOrCrash(manifest, "Implementation-Version");
gitCommit = getAttributeOrCrash(manifest, "Implementation-Version-Git-Commit");
gitBranch = getAttributeOrCrash(manifest, "Implementation-Version-Git-Branch");
buildId = getAttributeOrCrash(manifest, "Implementation-Version-BuildId");
}
private static String getAttributeOrCrash(Manifest manifest, String key) {
String result = manifest.getMainAttributes().getValue(key);
if (result == null) {
throw CrashReports.report(null, "Manifest exists but attribute " + key + " not found");
}
return result;
}
public static String getName() {
return NAME;
}
/**
* Returns the version of the game as a String. Version data is retrieved
* from a {@code META-INF/MANIFEST.MF} file located in the main JAR. Version
* format depends on way the game was built:
* <ul>
* <li><code>dev</code> if no matching manifest was found, e.g. when launching from an IDE</li>
* <li>The value of <code>Implementation-Version</code> specified in the manifest:
* <ul>
* <li>[Stage-]Major.Minor.Patch, e.g. <code>alpha-0.3.2</code> or <code>1.4.2</code>, for released versions</li>
* <li>BuildId, e.g. <code>WJ7</code>, for snapshots built by automation systems</li>
* <li>YYYY-MM-DD, e.g. <code>2021-12-32</code>, for snapshots built manually</li>
* </ul>
* </li>
* </ul>
*
* @return the version
*/
public static String getVersion() {
return version;
}
public static String getFullerVersion() {
if (isDefaultGitBranch() || "-".equals(gitBranch)) {
return version;
} else {
return String.format("%s/%s", version, gitBranch);
}
}
/**
* @return the buildId or <code>"-"</code>
*/
public static String getBuildId() {
return buildId;
}
/**
* @return the Git commit or <code>"-"</code>
*/
public static String getGitCommit() {
return gitCommit;
}
public static String getGitCommitShort() {
if (gitCommit == null || "-".equals(gitCommit)) {
return gitCommit;
}
return gitCommit.substring(0, Math.min(7, gitCommit.length()));
}
/**
* @return the Git branch or <code>"-"</code>
*/
public static String getGitBranch() {
return gitBranch;
}
public static boolean isDefaultGitBranch() {
return "master".equals(gitBranch) || "main".equals(gitBranch);
}
public static String getFullVersion() {
return String.format("%s/%s/%s/%s", version, gitBranch, getGitCommitShort(), buildId);
}
} }

View File

@ -18,6 +18,8 @@
package ru.windcorp.progressia; package ru.windcorp.progressia;
import org.apache.logging.log4j.LogManager;
import ru.windcorp.progressia.client.graphics.GUI; import ru.windcorp.progressia.client.graphics.GUI;
import ru.windcorp.progressia.common.util.crash.CrashReports; import ru.windcorp.progressia.common.util.crash.CrashReports;
import ru.windcorp.progressia.common.util.crash.analyzers.OutOfMemoryAnalyzer; import ru.windcorp.progressia.common.util.crash.analyzers.OutOfMemoryAnalyzer;
@ -33,6 +35,8 @@ public class ProgressiaLauncher {
arguments = args.clone(); arguments = args.clone();
setupCrashReports(); setupCrashReports();
LogManager.getRootLogger().info("Launching " + Progressia.getName() + " version " + Progressia.getFullVersion());
proxy.initialize(); proxy.initialize();
ProgressiaLauncher.proxy = proxy; ProgressiaLauncher.proxy = proxy;
GUI.addTopLayer(new LayerTitle("Title")); GUI.addTopLayer(new LayerTitle("Title"));
@ -44,6 +48,7 @@ public class ProgressiaLauncher {
private static void setupCrashReports() { private static void setupCrashReports() {
// Context providers // Context providers
CrashReports.registerProvider(new VersionProvider());
CrashReports.registerProvider(new OSContextProvider()); CrashReports.registerProvider(new OSContextProvider());
CrashReports.registerProvider(new RAMContextProvider()); CrashReports.registerProvider(new RAMContextProvider());
CrashReports.registerProvider(new JavaVersionContextProvider()); CrashReports.registerProvider(new JavaVersionContextProvider());

View File

@ -143,20 +143,6 @@ public class ControlTriggers {
); );
} }
//
//
///
///
//
//
//
//
//
//
//
//
//
public static ControlTriggerInputBased localOf( public static ControlTriggerInputBased localOf(
String id, String id,
Consumer<InputEvent> action, Consumer<InputEvent> action,

View File

@ -19,7 +19,7 @@
package ru.windcorp.progressia.client.comms.controls; package ru.windcorp.progressia.client.comms.controls;
import ru.windcorp.progressia.client.Client; import ru.windcorp.progressia.client.Client;
import ru.windcorp.progressia.client.graphics.input.bus.Input; import ru.windcorp.progressia.client.graphics.input.InputEvent;
import ru.windcorp.progressia.common.comms.packets.Packet; import ru.windcorp.progressia.common.comms.packets.Packet;
public class InputBasedControls { public class InputBasedControls {
@ -36,12 +36,12 @@ public class InputBasedControls {
.toArray(ControlTriggerInputBased[]::new); .toArray(ControlTriggerInputBased[]::new);
} }
public void handleInput(Input input) { public void handleInput(InputEvent event) {
for (ControlTriggerInputBased c : controls) { for (ControlTriggerInputBased c : controls) {
Packet packet = c.onInputEvent(input.getEvent()); Packet packet = c.onInputEvent(event);
if (packet != null) { if (packet != null) {
input.consume(); event.consume();
client.getComms().sendPacket(packet); client.getComms().sendPacket(packet);
break; break;
} }

View File

@ -23,15 +23,8 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import com.google.common.eventbus.Subscribe;
import ru.windcorp.progressia.client.graphics.backend.GraphicsInterface; import ru.windcorp.progressia.client.graphics.backend.GraphicsInterface;
import ru.windcorp.progressia.client.graphics.input.CursorEvent;
import ru.windcorp.progressia.client.graphics.input.FrameResizeEvent;
import ru.windcorp.progressia.client.graphics.input.InputEvent; import ru.windcorp.progressia.client.graphics.input.InputEvent;
import ru.windcorp.progressia.client.graphics.input.KeyEvent;
import ru.windcorp.progressia.client.graphics.input.WheelEvent;
import ru.windcorp.progressia.client.graphics.input.bus.Input;
public class GUI { public class GUI {
@ -46,15 +39,6 @@ public class GUI {
private static final List<LayerStackModification> MODIFICATION_QUEUE = Collections private static final List<LayerStackModification> MODIFICATION_QUEUE = Collections
.synchronizedList(new ArrayList<>()); .synchronizedList(new ArrayList<>());
private static class ModifiableInput extends Input {
@Override
public void initialize(InputEvent event, Target target) {
super.initialize(event, target);
}
}
private static final ModifiableInput THE_INPUT = new ModifiableInput();
private GUI() { private GUI() {
} }
@ -126,43 +110,12 @@ public class GUI {
LAYERS.forEach(Layer::invalidate); LAYERS.forEach(Layer::invalidate);
} }
private static void dispatchInputEvent(InputEvent event) { public static void dispatchInput(InputEvent event) {
Input.Target target; synchronized (LAYERS) {
for (int i = 0; i < LAYERS.size(); ++i) {
if (event instanceof KeyEvent) { LAYERS.get(i).handleInput(event);
if (((KeyEvent) event).isMouse()) {
target = Input.Target.HOVERED;
} else {
target = Input.Target.FOCUSED;
} }
} else if (event instanceof CursorEvent) {
target = Input.Target.HOVERED;
} else if (event instanceof WheelEvent) {
target = Input.Target.HOVERED;
} else if (event instanceof FrameResizeEvent) {
return;
} else {
target = Input.Target.ALL;
} }
THE_INPUT.initialize(event, target);
LAYERS.forEach(l -> l.handleInput(THE_INPUT));
}
public static Object getEventSubscriber() {
return new Object() {
@Subscribe
public void onFrameResized(FrameResizeEvent event) {
GUI.invalidateEverything();
}
@Subscribe
public void onInput(InputEvent event) {
dispatchInputEvent(event);
}
};
} }
} }

View File

@ -21,7 +21,7 @@ package ru.windcorp.progressia.client.graphics;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import ru.windcorp.progressia.client.graphics.backend.GraphicsInterface; import ru.windcorp.progressia.client.graphics.backend.GraphicsInterface;
import ru.windcorp.progressia.client.graphics.input.bus.Input; import ru.windcorp.progressia.client.graphics.input.InputEvent;
public abstract class Layer { public abstract class Layer {
@ -106,7 +106,7 @@ public abstract class Layer {
protected abstract void doRender(); protected abstract void doRender();
protected abstract void handleInput(Input input); public abstract void handleInput(InputEvent input);
protected int getWidth() { protected int getWidth() {
return GraphicsInterface.getFrameWidth(); return GraphicsInterface.getFrameWidth();

View File

@ -19,6 +19,7 @@
package ru.windcorp.progressia.client.graphics.backend; package ru.windcorp.progressia.client.graphics.backend;
import glm.vec._2.i.Vec2i; import glm.vec._2.i.Vec2i;
import org.lwjgl.glfw.GLFWVidMode; import org.lwjgl.glfw.GLFWVidMode;
import static org.lwjgl.glfw.GLFW.*; import static org.lwjgl.glfw.GLFW.*;
@ -43,6 +44,13 @@ public class GraphicsBackend {
private static boolean isGLFWInitialized = false; private static boolean isGLFWInitialized = false;
private static boolean isOpenGLInitialized = false; private static boolean isOpenGLInitialized = false;
private static boolean allowDisablingCursor;
static {
String key = GraphicsBackend.class.getName() + ".allowDisablingCursor";
allowDisablingCursor = Boolean.parseBoolean(System.getProperty(key, "true"));
}
private static boolean forceCursorToCenter = false;
private GraphicsBackend() { private GraphicsBackend() {
} }
@ -114,6 +122,10 @@ public class GraphicsBackend {
frameLength = now - frameStart; frameLength = now - frameStart;
frameStart = now; frameStart = now;
} }
if (forceCursorToCenter) {
glfwSetCursorPos(windowHandle, FRAME_SIZE.x / 2.0, FRAME_SIZE.y / 2.0);
}
} }
static void endFrame() { static void endFrame() {
@ -194,10 +206,18 @@ public class GraphicsBackend {
} }
public static boolean isMouseCaptured() { public static boolean isMouseCaptured() {
if (!allowDisablingCursor) {
return forceCursorToCenter;
}
return glfwGetInputMode(windowHandle, GLFW_CURSOR) == GLFW_CURSOR_DISABLED; return glfwGetInputMode(windowHandle, GLFW_CURSOR) == GLFW_CURSOR_DISABLED;
} }
public static void setMouseCaptured(boolean capture) { public static void setMouseCaptured(boolean capture) {
if (!allowDisablingCursor) {
forceCursorToCenter = capture;
return;
}
int mode = capture ? GLFW_CURSOR_DISABLED : GLFW_CURSOR_NORMAL; int mode = capture ? GLFW_CURSOR_DISABLED : GLFW_CURSOR_NORMAL;
glfwSetInputMode(windowHandle, GLFW_CURSOR, mode); glfwSetInputMode(windowHandle, GLFW_CURSOR, mode);

View File

@ -69,6 +69,10 @@ public class GraphicsInterface {
InputHandler.register(listener); InputHandler.register(listener);
} }
public static void unsubscribeFromInputEvents(Object listener) {
InputHandler.unregister(listener);
}
public static void startNextLayer() { public static void startNextLayer() {
GraphicsBackend.startNextLayer(); GraphicsBackend.startNextLayer();
} }

View File

@ -39,6 +39,7 @@ public class InputHandler {
public void initialize(int key, int scancode, int action, int mods) { public void initialize(int key, int scancode, int action, int mods) {
this.setTime(GraphicsInterface.getTime()); this.setTime(GraphicsInterface.getTime());
this.setConsumed(false);
this.key = key; this.key = key;
this.scancode = scancode; this.scancode = scancode;
this.action = action; this.action = action;
@ -59,7 +60,7 @@ public class InputHandler {
if (GraphicsBackend.getWindowHandle() != window) if (GraphicsBackend.getWindowHandle() != window)
return; return;
THE_KEY_EVENT.initialize(key, scancode, action, mods); THE_KEY_EVENT.initialize(key, scancode, action, mods);
dispatch(THE_KEY_EVENT); INPUT_EVENT_BUS.post(THE_KEY_EVENT);
switch (action) { switch (action) {
case GLFW.GLFW_PRESS: case GLFW.GLFW_PRESS:
@ -90,6 +91,7 @@ public class InputHandler {
public void initialize(double x, double y) { public void initialize(double x, double y) {
this.setTime(GraphicsInterface.getTime()); this.setTime(GraphicsInterface.getTime());
this.setConsumed(false);
getNewPosition().set(x, y); getNewPosition().set(x, y);
} }
@ -109,7 +111,7 @@ public class InputHandler {
InputTracker.initializeCursorPosition(x, y); InputTracker.initializeCursorPosition(x, y);
THE_CURSOR_MOVE_EVENT.initialize(x, y); THE_CURSOR_MOVE_EVENT.initialize(x, y);
dispatch(THE_CURSOR_MOVE_EVENT); INPUT_EVENT_BUS.post(THE_CURSOR_MOVE_EVENT);
InputTracker.getCursorPosition().set(x, y); InputTracker.getCursorPosition().set(x, y);
} }
@ -124,6 +126,7 @@ public class InputHandler {
public void initialize(double xOffset, double yOffset) { public void initialize(double xOffset, double yOffset) {
this.setTime(GraphicsInterface.getTime()); this.setTime(GraphicsInterface.getTime());
this.setConsumed(false);
this.getOffset().set(xOffset, yOffset); this.getOffset().set(xOffset, yOffset);
} }
@ -139,7 +142,7 @@ public class InputHandler {
if (GraphicsBackend.getWindowHandle() != window) if (GraphicsBackend.getWindowHandle() != window)
return; return;
THE_WHEEL_SCROLL_EVENT.initialize(xoffset, yoffset); THE_WHEEL_SCROLL_EVENT.initialize(xoffset, yoffset);
dispatch(THE_WHEEL_SCROLL_EVENT); INPUT_EVENT_BUS.post(THE_WHEEL_SCROLL_EVENT);
} }
// FrameResizeEvent // FrameResizeEvent
@ -152,6 +155,7 @@ public class InputHandler {
public void initialize(int width, int height) { public void initialize(int width, int height) {
this.setTime(GraphicsInterface.getTime()); this.setTime(GraphicsInterface.getTime());
this.setConsumed(false);
this.getNewSize().set(width, height); this.getNewSize().set(width, height);
} }
@ -167,17 +171,17 @@ public class InputHandler {
int height int height
) { ) {
THE_FRAME_RESIZE_EVENT.initialize(width, height); THE_FRAME_RESIZE_EVENT.initialize(width, height);
dispatch(THE_FRAME_RESIZE_EVENT); INPUT_EVENT_BUS.post(THE_FRAME_RESIZE_EVENT);
} }
// Misc // Misc
private static void dispatch(InputEvent event) {
INPUT_EVENT_BUS.post(event);
}
public static void register(Object listener) { public static void register(Object listener) {
INPUT_EVENT_BUS.register(listener); INPUT_EVENT_BUS.register(listener);
} }
public static void unregister(Object listener) {
INPUT_EVENT_BUS.unregister(listener);
}
} }

View File

@ -22,9 +22,23 @@ import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.glfw.GLFW.*; import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.system.MemoryUtil.*; import static org.lwjgl.system.MemoryUtil.*;
import java.io.IOException;
import org.lwjgl.glfw.GLFWImage;
import org.lwjgl.opengl.GL; import org.lwjgl.opengl.GL;
import com.google.common.eventbus.Subscribe;
import ru.windcorp.progressia.Progressia;
import ru.windcorp.progressia.client.graphics.GUI; import ru.windcorp.progressia.client.graphics.GUI;
import ru.windcorp.progressia.client.graphics.input.FrameResizeEvent;
import ru.windcorp.progressia.client.graphics.input.InputEvent;
import ru.windcorp.progressia.client.graphics.texture.TextureDataEditor;
import ru.windcorp.progressia.client.graphics.texture.TextureLoader;
import ru.windcorp.progressia.client.graphics.texture.TextureSettings;
import ru.windcorp.progressia.common.resource.Resource;
import ru.windcorp.progressia.common.resource.ResourceManager;
import ru.windcorp.progressia.common.util.crash.CrashReports;
class LWJGLInitializer { class LWJGLInitializer {
@ -59,7 +73,13 @@ class LWJGLInitializer {
glfwWindowHint(GLFW_FOCUSED, GLFW_TRUE); glfwWindowHint(GLFW_FOCUSED, GLFW_TRUE);
glfwWindowHint(GLFW_MAXIMIZED, GLFW_TRUE); glfwWindowHint(GLFW_MAXIMIZED, GLFW_TRUE);
long handle = glfwCreateWindow(900, 900, "ProgressiaTest", NULL, NULL); long handle = glfwCreateWindow(
800,
600,
Progressia.getName() + " " + Progressia.getFullerVersion(),
NULL,
NULL
);
// TODO Check that handle != NULL // TODO Check that handle != NULL
@ -75,8 +95,30 @@ class LWJGLInitializer {
} }
private static void createWindowIcons() { private static void createWindowIcons() {
// TODO Auto-generated method stub if (glfwGetVersionString().toLowerCase().contains("wayland")) {
// glfwSetWindowIcon is not supported on Wayland
return;
}
final String prefix = "assets/icons/";
String[] sizes = ResourceManager.getResource(prefix + "logoSizes.txt").readAsString().split(" ");
try (GLFWImage.Buffer buffer = GLFWImage.malloc(sizes.length)) {
for (int i = 0; i < sizes.length; ++i) {
Resource resource = ResourceManager.getResource(prefix + "logo" + sizes[i].trim() + ".png");
TextureDataEditor icon = TextureLoader.loadPixels(resource, new TextureSettings(false, true));
buffer.position(i)
.width(icon.getContentWidth())
.height(icon.getContentHeight())
.pixels(icon.getData().getData());
}
glfwSetWindowIcon(GraphicsBackend.getWindowHandle(), buffer);
} catch (IOException e) {
throw CrashReports.report(e, "Could not load window icons");
}
} }
private static void initializeOpenGL() { private static void initializeOpenGL() {
@ -107,7 +149,20 @@ class LWJGLInitializer {
glfwSetScrollCallback(handle, InputHandler::handleWheelScroll); glfwSetScrollCallback(handle, InputHandler::handleWheelScroll);
GraphicsInterface.subscribeToInputEvents(GUI.getEventSubscriber()); GraphicsInterface.subscribeToInputEvents(new Object() {
@Subscribe
public void onFrameResized(FrameResizeEvent event) {
GUI.invalidateEverything();
}
@Subscribe
public void onInputEvent(InputEvent event) {
GUI.dispatchInput(event);
}
});
} }
} }

View File

@ -45,7 +45,7 @@ public class GNUUnifontLoader {
private static final AtlasGroup ATLAS_GROUP_GNU_UNIFONT = new AtlasGroup("GNUUnifont", 1 << 12); private static final AtlasGroup ATLAS_GROUP_GNU_UNIFONT = new AtlasGroup("GNUUnifont", 1 << 12);
private static final TextureSettings TEXTURE_SETTINGS = new TextureSettings(false); private static final TextureSettings TEXTURE_SETTINGS = new TextureSettings(false, false);
private static final int BITS_PER_HEX_DIGIT = 4; private static final int BITS_PER_HEX_DIGIT = 4;
private static final int PREFIX_LENGTH = "0000:".length(); private static final int PREFIX_LENGTH = "0000:".length();

View File

@ -0,0 +1,28 @@
package ru.windcorp.progressia.client.graphics.gui;
import glm.mat._4.Mat4;
import glm.vec._3.Vec3;
import ru.windcorp.progressia.client.graphics.flat.RenderTarget;
import ru.windcorp.progressia.client.graphics.texture.Texture;
public class Background extends GUILayer {
protected Texture backgroundTexture;
public Background(String name, Layout layout, Texture inTexture) {
super(name, layout);
backgroundTexture = inTexture;
}
@Override
protected void assemble(RenderTarget target) {
getRoot().setBounds(0, 0, getWidth(), getHeight());
getRoot().invalidate();
target.pushTransform(new Mat4(1).translate(new Vec3(0,0,500)));
target.drawTexture(0, 0, getWidth(), getHeight(), backgroundTexture);
target.popTransform();
getRoot().assemble(target);
}
}

View File

@ -54,18 +54,17 @@ public abstract class BasicButton extends Component {
reassembleAt(ARTrigger.HOVER, ARTrigger.FOCUS, ARTrigger.ENABLE); reassembleAt(ARTrigger.HOVER, ARTrigger.FOCUS, ARTrigger.ENABLE);
// Click triggers // Click triggers
addListener(KeyEvent.class, e -> { addInputListener(KeyEvent.class, e -> {
if (e.isRepeat()) { if (e.isRepeat())
return false; return;
} else if (
if (
e.isLeftMouseButton() || e.isLeftMouseButton() ||
e.getKey() == GLFW.GLFW_KEY_SPACE || e.getKey() == GLFW.GLFW_KEY_SPACE ||
e.getKey() == GLFW.GLFW_KEY_ENTER e.getKey() == GLFW.GLFW_KEY_ENTER
) { ) {
setPressed(e.isPress()); setPressed(e.isPress());
return true; e.consume();
} else {
return false;
} }
}); });
@ -124,6 +123,7 @@ public abstract class BasicButton extends Component {
public void setPressed(boolean isPressed) { public void setPressed(boolean isPressed) {
if (this.isPressed != isPressed) { if (this.isPressed != isPressed) {
this.isPressed = isPressed; this.isPressed = isPressed;
requestReassembly();
if (isPressed) { if (isPressed) {
takeFocus(); takeFocus();

View File

@ -23,6 +23,10 @@ import ru.windcorp.progressia.client.graphics.flat.RenderTarget;
import ru.windcorp.progressia.client.graphics.font.Font; import ru.windcorp.progressia.client.graphics.font.Font;
import ru.windcorp.progressia.client.graphics.Colors; import ru.windcorp.progressia.client.graphics.Colors;
/** Class for a traditional button that gets clicked to activate
*
* @author opfromthestart
*/
public class Button extends BasicButton { public class Button extends BasicButton {
public Button(String name, String label, Font labelFont) { public Button(String name, String label, Font labelFont) {
@ -51,9 +55,7 @@ public class Button extends BasicButton {
// Inside area // Inside area
if (isPressed()) { if (!isPressed()) {
// Do nothing
} else {
Vec4 backgroundColor; Vec4 backgroundColor;
if (isHovered() && isEnabled()) { if (isHovered() && isEnabled()) {
backgroundColor = Colors.HOVER_BLUE; backgroundColor = Colors.HOVER_BLUE;

View File

@ -25,8 +25,6 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import org.lwjgl.glfw.GLFW;
import com.google.common.eventbus.EventBus; import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
@ -39,9 +37,10 @@ import ru.windcorp.progressia.client.graphics.gui.event.EnableEvent;
import ru.windcorp.progressia.client.graphics.gui.event.FocusEvent; import ru.windcorp.progressia.client.graphics.gui.event.FocusEvent;
import ru.windcorp.progressia.client.graphics.gui.event.HoverEvent; import ru.windcorp.progressia.client.graphics.gui.event.HoverEvent;
import ru.windcorp.progressia.client.graphics.gui.event.ParentChangedEvent; import ru.windcorp.progressia.client.graphics.gui.event.ParentChangedEvent;
import ru.windcorp.progressia.client.graphics.input.CursorMoveEvent;
import ru.windcorp.progressia.client.graphics.input.InputEvent; import ru.windcorp.progressia.client.graphics.input.InputEvent;
import ru.windcorp.progressia.client.graphics.input.KeyEvent; import ru.windcorp.progressia.client.graphics.input.KeyEvent;
import ru.windcorp.progressia.client.graphics.input.bus.Input; import ru.windcorp.progressia.client.graphics.input.KeyMatcher;
import ru.windcorp.progressia.client.graphics.input.bus.InputBus; import ru.windcorp.progressia.client.graphics.input.bus.InputBus;
import ru.windcorp.progressia.client.graphics.input.bus.InputListener; import ru.windcorp.progressia.client.graphics.input.bus.InputListener;
import ru.windcorp.progressia.common.util.Named; import ru.windcorp.progressia.common.util.Named;
@ -55,7 +54,7 @@ public class Component extends Named {
private Component parent = null; private Component parent = null;
private EventBus eventBus = null; private EventBus eventBus = null;
private InputBus inputBus = null; private final InputBus inputBus = new InputBus(this);
private int x, y; private int x, y;
private int width, height; private int width, height;
@ -76,6 +75,9 @@ public class Component extends Named {
public Component(String name) { public Component(String name) {
super(name); super(name);
// Update hover flag when cursor moves
addInputListener(CursorMoveEvent.class, this::updateHoverFlag, InputBus.Option.ALWAYS);
} }
public Component getParent() { public Component getParent() {
@ -522,6 +524,10 @@ public class Component extends Named {
} }
} }
private void updateHoverFlag(CursorMoveEvent e) {
setHovered(contains((int) InputTracker.getCursorX(), (int) InputTracker.getCursorY()));
}
public void addListener(Object listener) { public void addListener(Object listener) {
if (eventBus == null) { if (eventBus == null) {
eventBus = ReportingEventBus.create(getName()); eventBus = ReportingEventBus.create(getName());
@ -542,121 +548,32 @@ public class Component extends Named {
eventBus.post(event); eventBus.post(event);
} }
public <T extends InputEvent> void addListener( public <T extends InputEvent> void addInputListener(Class<? extends T> type, InputListener<T> listener, InputBus.Option... options) {
Class<? extends T> type, inputBus.register(type, listener, options);
boolean handlesConsumed,
InputListener<T> listener
) {
if (inputBus == null) {
inputBus = new InputBus();
} }
inputBus.register(type, handlesConsumed, listener); public void addKeyListener(KeyMatcher matcher, InputListener<? super KeyEvent> listener, InputBus.Option... options) {
inputBus.register(matcher, listener, options);
} }
public <T extends InputEvent> void addListener(Class<? extends T> type, InputListener<T> listener) { public void removeInputListener(InputListener<?> listener) {
if (inputBus == null) {
inputBus = new InputBus();
}
inputBus.register(type, listener);
}
public void removeListener(InputListener<?> listener) {
if (inputBus != null) { if (inputBus != null) {
inputBus.unregister(listener); inputBus.unregister(listener);
} }
} }
protected void handleInput(Input input) { protected boolean passInputToChildren(InputEvent e) {
if (inputBus != null && isEnabled()) {
inputBus.dispatch(input);
}
}
public void dispatchInput(Input input) {
try {
switch (input.getTarget()) {
case FOCUSED:
dispatchInputToFocused(input);
break;
case HOVERED:
dispatchInputToHovered(input);
break;
case ALL:
default:
dispatchInputToAll(input);
break;
}
} catch (Exception e) {
throw CrashReports.report(e, "Could not dispatch input to Component %s", this);
}
}
private void dispatchInputToFocused(Input input) {
Component c = findFocused();
if (c == null)
return;
if (attemptFocusTransfer(input, c))
return;
while (c != null) {
c.handleInput(input);
c = c.getParent();
}
}
private void dispatchInputToHovered(Input input) {
getChildren().forEach(child -> {
if (child.containsCursor()) {
child.setHovered(true);
if (!input.isConsumed()) {
child.dispatchInput(input);
}
} else {
child.setHovered(false);
}
});
handleInput(input);
}
private void dispatchInputToAll(Input input) {
getChildren().forEach(c -> c.dispatchInput(input));
handleInput(input);
}
private boolean attemptFocusTransfer(Input input, Component focused) {
if (input.isConsumed())
return false;
if (!(input.getEvent() instanceof KeyEvent))
return false;
KeyEvent keyInput = (KeyEvent) input.getEvent();
if (keyInput.getKey() == GLFW.GLFW_KEY_TAB && !keyInput.isRelease()) {
input.consume();
if (keyInput.hasShift()) {
focused.focusPrevious();
} else {
focused.focusNext();
}
return true; return true;
} }
return false; InputBus getInputBus() {
return inputBus;
} }
public synchronized boolean contains(int x, int y) { public synchronized boolean contains(int x, int y) {
return x >= getX() && x < getX() + getWidth() && y >= getY() && y < getY() + getHeight(); return x >= getX() && x < getX() + getWidth() && y >= getY() && y < getY() + getHeight();
} }
public boolean containsCursor() {
return contains((int) InputTracker.getCursorX(), (int) InputTracker.getCursorY());
}
public void requestReassembly() { public void requestReassembly() {
if (parent != null) { if (parent != null) {
parent.requestReassembly(); parent.requestReassembly();
@ -807,7 +724,7 @@ public class Component extends Named {
for (ARTrigger trigger : triggers) { for (ARTrigger trigger : triggers) {
if (!autoReassemblyTriggerObjects.containsKey(trigger)) { if (!autoReassemblyTriggerObjects.containsKey(trigger)) {
Object triggerObject = createTriggerObject(trigger); Object triggerObject = createTriggerObject(trigger);
addListener(trigger); addListener(triggerObject);
autoReassemblyTriggerObjects.put(trigger, triggerObject); autoReassemblyTriggerObjects.put(trigger, triggerObject);
} }
} }

View File

@ -0,0 +1,77 @@
/*
* Progressia
* Copyright (C) 2020-2021 Wind Corporation and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package ru.windcorp.progressia.client.graphics.gui;
import java.util.Objects;
import glm.vec._2.d.Vec2d;
import ru.windcorp.progressia.client.graphics.gui.event.DragEvent;
import ru.windcorp.progressia.client.graphics.gui.event.DragStartEvent;
import ru.windcorp.progressia.client.graphics.gui.event.DragStopEvent;
import ru.windcorp.progressia.client.graphics.input.CursorMoveEvent;
import ru.windcorp.progressia.client.graphics.input.KeyEvent;
import ru.windcorp.progressia.client.graphics.input.KeyMatcher;
import ru.windcorp.progressia.client.graphics.input.bus.InputBus;
public class DragManager {
private Component component;
private boolean isDragged = false;
private final Vec2d change = new Vec2d();
public void install(Component c) {
Objects.requireNonNull(c, "c");
if (c == component) {
return;
}
if (component != null) {
throw new IllegalStateException("Already installed on " + component + "; attempted to install on " + c);
}
component = c;
c.addInputListener(CursorMoveEvent.class, this::onCursorMove, InputBus.Option.ALWAYS);
c.addKeyListener(KeyMatcher.LMB, this::onLMB, InputBus.Option.ALWAYS, InputBus.Option.IGNORE_ACTION);
}
private void onCursorMove(CursorMoveEvent e) {
if (isDragged) {
Vec2d currentChange = e.getChange(null);
change.add(currentChange);
component.dispatchEvent(new DragEvent(component, currentChange, change));
}
}
private void onLMB(KeyEvent e) {
if (isDragged && e.isRelease()) {
isDragged = false;
component.dispatchEvent(new DragStopEvent(component, change));
} else if (!isDragged && !e.isConsumed() && e.isPress() && component.isHovered()) {
isDragged = true;
change.set(0, 0);
component.dispatchEvent(new DragStartEvent(component));
e.consume();
}
}
}

View File

@ -18,9 +18,17 @@
package ru.windcorp.progressia.client.graphics.gui; package ru.windcorp.progressia.client.graphics.gui;
import java.util.Iterator;
import org.lwjgl.glfw.GLFW;
import ru.windcorp.progressia.client.graphics.flat.AssembledFlatLayer; import ru.windcorp.progressia.client.graphics.flat.AssembledFlatLayer;
import ru.windcorp.progressia.client.graphics.flat.RenderTarget; import ru.windcorp.progressia.client.graphics.flat.RenderTarget;
import ru.windcorp.progressia.client.graphics.input.bus.Input; import ru.windcorp.progressia.client.graphics.input.InputEvent;
import ru.windcorp.progressia.client.graphics.input.KeyEvent;
import ru.windcorp.progressia.client.graphics.input.bus.InputBus;
import ru.windcorp.progressia.common.util.LowOverheadCache;
import ru.windcorp.progressia.common.util.StashingStack;
public abstract class GUILayer extends AssembledFlatLayer { public abstract class GUILayer extends AssembledFlatLayer {
@ -33,7 +41,9 @@ public abstract class GUILayer extends AssembledFlatLayer {
public GUILayer(String name, Layout layout) { public GUILayer(String name, Layout layout) {
super(name); super(name);
getRoot().setLayout(layout); getRoot().setLayout(layout);
getRoot().addInputListener(KeyEvent.class, this::attemptFocusTransfer, InputBus.Option.IGNORE_FOCUS);
} }
public Component getRoot() { public Component getRoot() {
@ -47,9 +57,85 @@ public abstract class GUILayer extends AssembledFlatLayer {
getRoot().assemble(target); getRoot().assemble(target);
} }
/**
* Stack frame for {@link #handleInput(InputEvent)}.
*/
private static class EventHandlingFrame {
Component component;
Iterator<Component> children;
void init(Component c) {
component = c;
children = c.getChildren().iterator();
}
void reset() {
component = null;
children = null;
}
}
/**
* Stacks for {@link #handleInput(InputEvent)}.
*/
private final LowOverheadCache<StashingStack<EventHandlingFrame>> pathCache = new LowOverheadCache<>(
() -> new StashingStack<>(64, EventHandlingFrame::new)
);
/*
* This is essentially a depth-first iteration of the component tree. The
* recursive procedure has been unrolled to reduce call stack length.
*/
@Override @Override
protected void handleInput(Input input) { public void handleInput(InputEvent event) {
getRoot().dispatchInput(input); StashingStack<EventHandlingFrame> path = pathCache.grab();
if (!path.isEmpty()) {
throw new IllegalStateException("path is not empty: " + path);
}
path.push().init(root);
while (!path.isEmpty()) {
Iterator<Component> it = path.peek().children;
if (it.hasNext()) {
Component c = it.next();
if (c.isEnabled()) {
if (c.getChildren().isEmpty() || !c.passInputToChildren(event)) {
c.getInputBus().dispatch(event);
} else {
path.push().init(c);
}
}
} else {
path.peek().component.getInputBus().dispatch(event);
path.pop().reset();
}
}
pathCache.release(path);
}
private void attemptFocusTransfer(KeyEvent e) {
Component focused = getRoot().findFocused();
if (focused == null) {
return;
}
if (e.getKey() == GLFW.GLFW_KEY_TAB && !e.isRelease()) {
e.consume();
if (e.hasShift()) {
focused.focusPrevious();
} else {
focused.focusNext();
}
}
} }
@Override @Override

View File

@ -85,7 +85,6 @@ public class Label extends Component {
public void setFont(Font font) { public void setFont(Font font) {
this.font = font; this.font = font;
requestReassembly();
} }
public String getCurrentText() { public String getCurrentText() {

View File

@ -104,26 +104,22 @@ public class RadioButton extends BasicButton {
group.addChild(basicChild); group.addChild(basicChild);
addChild(group); addChild(group);
addListener(KeyEvent.class, e -> { addInputListener(KeyEvent.class, e -> {
if (e.isRelease()) if (e.isRelease()) return;
return false;
if (e.getKey() == GLFW.GLFW_KEY_LEFT || e.getKey() == GLFW.GLFW_KEY_UP) { if (e.getKey() == GLFW.GLFW_KEY_LEFT || e.getKey() == GLFW.GLFW_KEY_UP) {
if (this.group != null) { if (this.group != null) {
this.group.selectPrevious(); this.group.selectPrevious();
this.group.getSelected().takeFocus(); this.group.getSelected().takeFocus();
} }
e.consume();
return true;
} else if (e.getKey() == GLFW.GLFW_KEY_RIGHT || e.getKey() == GLFW.GLFW_KEY_DOWN) { } else if (e.getKey() == GLFW.GLFW_KEY_RIGHT || e.getKey() == GLFW.GLFW_KEY_DOWN) {
if (this.group != null) { if (this.group != null) {
this.group.selectNext(); this.group.selectNext();
this.group.getSelected().takeFocus(); this.group.getSelected().takeFocus();
} }
return true; e.consume();
} }
return false;
}); });
addAction(b -> setChecked(true)); addAction(b -> setChecked(true));

View File

@ -0,0 +1,23 @@
package ru.windcorp.progressia.client.graphics.gui;
import ru.windcorp.progressia.client.graphics.flat.RenderTarget;
import ru.windcorp.progressia.client.graphics.texture.Texture;
public class TextureComponent extends Component {
private final Texture texture;
public TextureComponent(String name, Texture texture2) {
super(name);
texture = texture2;
setPreferredSize(texture.getSprite().getWidth(),texture.getSprite().getHeight());
}
@Override
protected void assembleSelf(RenderTarget target)
{
target.drawTexture(getX(), getY(), getWidth(), getHeight(), texture);
}
}

View File

@ -0,0 +1,58 @@
/*
* Progressia
* Copyright (C) 2020-2021 Wind Corporation and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package ru.windcorp.progressia.client.graphics.gui.event;
import glm.vec._2.d.Vec2d;
import ru.windcorp.progressia.client.graphics.gui.Component;
public class DragEvent extends ComponentEvent {
private final Vec2d currentChange = new Vec2d();
private final Vec2d totalChange = new Vec2d();
public DragEvent(Component component, Vec2d currentChange, Vec2d totalChange) {
super(component);
this.currentChange.set(currentChange.x, currentChange.y);
this.totalChange.set(totalChange.x, totalChange.y);
}
public Vec2d getCurrentChange() {
return currentChange;
}
public double getCurrentChangeX() {
return currentChange.x;
}
public double getCurrentChangeY() {
return currentChange.y;
}
public Vec2d getTotalChange() {
return totalChange;
}
public double getTotalChangeX() {
return totalChange.x;
}
public double getTotalChangeY() {
return totalChange.y;
}
}

View File

@ -0,0 +1,28 @@
/*
* Progressia
* Copyright (C) 2020-2021 Wind Corporation and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package ru.windcorp.progressia.client.graphics.gui.event;
import ru.windcorp.progressia.client.graphics.gui.Component;
public class DragStartEvent extends ComponentEvent {
public DragStartEvent(Component component) {
super(component);
}
}

View File

@ -15,48 +15,30 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package ru.windcorp.progressia.client.graphics.gui.event;
package ru.windcorp.progressia.client.graphics.input.bus; import glm.vec._2.d.Vec2d;
import ru.windcorp.progressia.client.graphics.gui.Component;
import ru.windcorp.progressia.client.graphics.input.InputEvent; public class DragStopEvent extends ComponentEvent {
public class Input { private final Vec2d totalChange = new Vec2d();
public static enum Target { public DragStopEvent(Component component, Vec2d totalChange) {
FOCUSED, HOVERED, ALL super(component);
this.totalChange.set(totalChange.x, totalChange.y);
} }
private InputEvent event; public Vec2d getTotalChange() {
return totalChange;
private boolean isConsumed;
private Target target;
protected void initialize(InputEvent event, Target target) {
this.event = event;
this.target = target;
this.isConsumed = false;
} }
public InputEvent getEvent() { public double getTotalChangeX() {
return event; return totalChange.x;
} }
public boolean isConsumed() { public double getTotalChangeY() {
return isConsumed; return totalChange.y;
}
public void setConsumed(boolean isConsumed) {
this.isConsumed = isConsumed;
}
public void consume() {
setConsumed(true);
}
public Target getTarget() {
return target;
} }
} }

View File

@ -27,8 +27,8 @@ import ru.windcorp.progressia.client.graphics.gui.Layout;
public class LayoutAlign implements Layout { public class LayoutAlign implements Layout {
private final int margin; protected final int margin;
private double alignX, alignY; protected double alignX, alignY;
public LayoutAlign(double alignX, double alignY, int margin) { public LayoutAlign(double alignX, double alignY, int margin) {
this.alignX = alignX; this.alignX = alignX;
@ -72,7 +72,7 @@ public class LayoutAlign implements Layout {
Vec2i result = new Vec2i(0, 0); Vec2i result = new Vec2i(0, 0);
c.getChildren().stream() c.getChildren().stream()
.map(child -> child.getPreferredSize()) .map(Component::getPreferredSize)
.forEach(size -> { .forEach(size -> {
result.x = max(size.x, result.x); result.x = max(size.x, result.x);
result.y = max(size.y, result.y); result.y = max(size.y, result.y);

View File

@ -0,0 +1,56 @@
package ru.windcorp.progressia.client.graphics.gui.layout;
import glm.vec._2.i.Vec2i;
import ru.windcorp.progressia.client.graphics.gui.Component;
import ru.windcorp.progressia.client.graphics.gui.Layout;
import static java.lang.Math.max;
import static java.lang.Math.min;
public class LayoutEdges implements Layout {
private final int margin;
public LayoutEdges(int margin) {
this.margin = margin;
}
@Override
public void layout(Component c) {
for (int i=0;i<2;i++)
{
Component child = c.getChild(i);
Vec2i size = child.getPreferredSize();
int cWidth = c.getWidth() - 2 * margin;
int cHeight = c.getHeight() - 2 * margin;
size.x = min(size.x, cWidth);
size.y = min(size.y, cHeight);
if (i==0) {
child.setBounds(
c.getX() + margin,
c.getY(),
size
);
} else {
child.setBounds(
1920 - size.x - margin,
c.getY(),
size
);
}
}
}
@Override
public Vec2i calculatePreferredSize(Component c) {
Vec2i result = new Vec2i(1920,0);
c.getChildren().stream()
.map(Component::getPreferredSize)
.forEach(size -> result.y = max(Math.abs(size.y), result.y));
return result;
}
}

View File

@ -33,7 +33,6 @@ import ru.windcorp.progressia.client.graphics.gui.layout.LayoutFill;
import ru.windcorp.progressia.client.graphics.gui.layout.LayoutVertical; import ru.windcorp.progressia.client.graphics.gui.layout.LayoutVertical;
import ru.windcorp.progressia.client.graphics.input.InputEvent; import ru.windcorp.progressia.client.graphics.input.InputEvent;
import ru.windcorp.progressia.client.graphics.input.KeyEvent; import ru.windcorp.progressia.client.graphics.input.KeyEvent;
import ru.windcorp.progressia.client.graphics.input.bus.Input;
import ru.windcorp.progressia.client.localization.MutableString; import ru.windcorp.progressia.client.localization.MutableString;
import ru.windcorp.progressia.client.localization.MutableStringLocalized; import ru.windcorp.progressia.client.localization.MutableStringLocalized;
@ -97,11 +96,9 @@ public class MenuLayer extends GUILayer {
} }
@Override @Override
protected void handleInput(Input input) { public void handleInput(InputEvent event) {
if (!input.isConsumed()) {
InputEvent event = input.getEvent();
if (!event.isConsumed()) {
if (event instanceof KeyEvent) { if (event instanceof KeyEvent) {
KeyEvent keyEvent = (KeyEvent) event; KeyEvent keyEvent = (KeyEvent) event;
if (keyEvent.isPress() && keyEvent.getKey() == GLFW.GLFW_KEY_ESCAPE) { if (keyEvent.isPress() && keyEvent.getKey() == GLFW.GLFW_KEY_ESCAPE) {
@ -110,8 +107,8 @@ public class MenuLayer extends GUILayer {
} }
} }
super.handleInput(input); super.handleInput(event);
input.consume(); event.consume();
} }
} }

View File

@ -18,7 +18,6 @@
package ru.windcorp.progressia.client.graphics.input; package ru.windcorp.progressia.client.graphics.input;
import glm.vec._2.Vec2;
import glm.vec._2.d.Vec2d; import glm.vec._2.d.Vec2d;
public class CursorMoveEvent extends CursorEvent { public class CursorMoveEvent extends CursorEvent {
@ -81,7 +80,10 @@ public class CursorMoveEvent extends CursorEvent {
return getNewY() - getPreviousY(); return getNewY() - getPreviousY();
} }
public Vec2 getChange(Vec2 result) { public Vec2d getChange(Vec2d result) {
if (result == null) {
result = new Vec2d();
}
return result.set(getChangeX(), getChangeY()); return result.set(getChangeX(), getChangeY());
} }

View File

@ -18,10 +18,32 @@
package ru.windcorp.progressia.client.graphics.input; package ru.windcorp.progressia.client.graphics.input;
import ru.windcorp.progressia.client.graphics.gui.Component;
/**
* An instance of user input.
* <p>
* User input events are typically generated by graphics backend between frames
* and passed to the graphics layers from top to bottom. Layers that use
* {@link Component}s will forward this event through the Component hierarchy.
* <p>
* Events have a {@code consumed} flag. A freshly-generated event will have this
* flag set to {@code false}. Event listeners that process the event will
* usually choose to raise the flag ("consume the event") to ask future
* listeners to ignore this event. This is done to avoid multiple UI interfaces
* reacting to single input. By default, listeners will not receive consumed
* events; however, some listeners may choose to receive, handle and even
* un-consume the event.
* <p>
* {@code InputEvent} objects may be reused for future input events after their
* processing is complete; to obtain a static copy, use {@link #snapshot()}.
*/
public abstract class InputEvent { public abstract class InputEvent {
private double time; private double time;
private boolean isConsumed = false;
public InputEvent(double time) { public InputEvent(double time) {
this.time = time; this.time = time;
} }
@ -36,4 +58,16 @@ public abstract class InputEvent {
public abstract InputEvent snapshot(); public abstract InputEvent snapshot();
public boolean isConsumed() {
return isConsumed;
}
public void setConsumed(boolean isConsumed) {
this.isConsumed = isConsumed;
}
public void consume() {
setConsumed(true);
}
} }

View File

@ -18,16 +18,75 @@
package ru.windcorp.progressia.client.graphics.input; package ru.windcorp.progressia.client.graphics.input;
import java.util.function.Predicate; import java.util.Map;
import java.util.regex.Pattern;
import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFW;
import com.google.common.collect.ImmutableMap;
public class KeyMatcher { public class KeyMatcher {
private static final Pattern DECLAR_SPLIT_REGEX = Pattern.compile("\\s*\\+\\s*");
private static final Map<String, Integer> MOD_TOKENS = ImmutableMap.of(
"SHIFT", GLFW.GLFW_MOD_SHIFT,
"CONTROL", GLFW.GLFW_MOD_CONTROL,
"ALT", GLFW.GLFW_MOD_ALT,
"SUPER", GLFW.GLFW_MOD_SUPER
);
public static final KeyMatcher LMB = new KeyMatcher(GLFW.GLFW_MOUSE_BUTTON_LEFT);
public static final KeyMatcher RMB = new KeyMatcher(GLFW.GLFW_MOUSE_BUTTON_RIGHT);
public static final KeyMatcher MMB = new KeyMatcher(GLFW.GLFW_MOUSE_BUTTON_MIDDLE);
private final int key; private final int key;
private final int mods; private final int mods;
protected KeyMatcher(int key, int mods) { public KeyMatcher(int key, int mods) {
this.key = key;
this.mods = mods;
}
public KeyMatcher(int key) {
this.key = key;
this.mods = 0;
}
public KeyMatcher(String declar) {
String[] tokens = DECLAR_SPLIT_REGEX.split(declar);
if (tokens.length == 0) {
throw new IllegalArgumentException("No tokens found in \"" + declar + "\"");
}
int key = -1;
int mods = 0;
for (String token : tokens) {
token = token.toUpperCase();
if (MOD_TOKENS.containsKey(token)) {
int mod = MOD_TOKENS.get(token);
if ((mods & mod) != 0) {
throw new IllegalArgumentException("Duplicate modifier \"" + token + "\" in \"" + declar + "\"");
}
mods |= mod;
} else if (key != -1) {
throw new IllegalArgumentException("Too many non-modifier tokens in \"" + declar + "\": maximum one key, first offender: \"" + token + "\"");
} else {
token = token.replace(' ', '_');
if (token.startsWith("KEYPAD_")) {
token = "KP_" + token.substring("KEYPAD_".length());
}
key = Keys.getCode(token);
if (key == -1) {
throw new IllegalArgumentException("Unknown token \"" + token + "\" in \"" + declar + "\"");
}
}
}
this.key = key; this.key = key;
this.mods = mods; this.mods = mods;
} }
@ -43,6 +102,15 @@ public class KeyMatcher {
return true; return true;
} }
public boolean matchesIgnoringAction(KeyEvent event) {
if (event.getKey() != getKey())
return false;
if ((event.getMods() & getMods()) != getMods())
return false;
return true;
}
public int getKey() { public int getKey() {
return key; return key;
} }
@ -51,48 +119,24 @@ public class KeyMatcher {
return mods; return mods;
} }
public static KeyMatcher.Builder of(int key) { public KeyMatcher with(int modifier) {
return new KeyMatcher.Builder(key); return new KeyMatcher(key, mods | modifier);
} }
public static class Builder { public KeyMatcher withShift() {
private final int key;
private int mods = 0;
public Builder(int key) {
this.key = key;
}
public Builder with(int modifier) {
this.mods += modifier;
return this;
}
public Builder withShift() {
return with(GLFW.GLFW_MOD_SHIFT); return with(GLFW.GLFW_MOD_SHIFT);
} }
public Builder withCtrl() { public KeyMatcher withCtrl() {
return with(GLFW.GLFW_MOD_CONTROL); return with(GLFW.GLFW_MOD_CONTROL);
} }
public Builder withAlt() { public KeyMatcher withAlt() {
return with(GLFW.GLFW_MOD_ALT); return with(GLFW.GLFW_MOD_ALT);
} }
public Builder withSuper() { public KeyMatcher withSuper() {
return with(GLFW.GLFW_MOD_SUPER); return with(GLFW.GLFW_MOD_SUPER);
} }
public KeyMatcher build() {
return new KeyMatcher(key, mods);
}
public Predicate<KeyEvent> matcher() {
return build()::matches;
}
}
} }

View File

@ -139,7 +139,7 @@ public class Keys {
} }
public static int getCode(String internalName) { public static int getCode(String internalName) {
if (NAMES_TO_CODES.containsKey(internalName)) { if (!NAMES_TO_CODES.containsKey(internalName)) {
return -1; return -1;
} else { } else {
return NAMES_TO_CODES.get(internalName); return NAMES_TO_CODES.get(internalName);

View File

@ -20,68 +20,396 @@ package ru.windcorp.progressia.client.graphics.input.bus;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Objects;
import ru.windcorp.jputil.ArrayUtil;
import ru.windcorp.progressia.client.graphics.gui.Component;
import ru.windcorp.progressia.client.graphics.input.CursorEvent;
import ru.windcorp.progressia.client.graphics.input.InputEvent; import ru.windcorp.progressia.client.graphics.input.InputEvent;
import ru.windcorp.progressia.client.graphics.input.KeyEvent;
import ru.windcorp.progressia.client.graphics.input.KeyMatcher;
import ru.windcorp.progressia.client.graphics.input.WheelEvent;
import ru.windcorp.progressia.common.util.crash.CrashReports;
/**
* An event bus optionally related to a {@link Component} that delivers input
* events to input listeners. This bus may skip listeners based on circumstance;
* behavior can be customized for each listener with {@link Option}s.
* <p>
* By default, events are filtered by four checks before being delivered to each
* listener:
* <ol>
* <li><em>Consumption check</em>: unless {@link Option#RECEIVE_CONSUMED
* RECEIVE_CONSUMED} is set, events that are consumed will not be
* delivered.</li>
* <li><em>Hover check</em>: for certain event types (for example,
* {@link WheelEvent} or {@link KeyEvent} that {@link KeyEvent#isMouse()
* isMouse()}), the event will only be delivered if the component is hovered.
* This check may be bypassed with option {@link Option#IGNORE_HOVER
* IGNORE_HOVER} or made mandatory for all events with
* {@link Option#REQUIRE_HOVER REQUIRE_HOVER}. Hover check automatically
* succeeds if no component is provided.</li>
* <li><em>Focus check</em>: for certain event types (for example,
* {@link KeyEvent} that {@code !isMouse()}), the event will only be delivered
* if the component has focus. This check may be bypassed with option
* {@link Option#IGNORE_FOCUS IGNORE_FOCUS} or made mandatory for all events
* with {@link Option#REQUIRE_FOCUS REQUIRE_FOCUS}. Focus check automatically
* succeeds if no component is provided.</li>
* <li><em>Type check</em>: events of type {@code E} are only delivered to
* listeners registered with event type {@code T} if objects of type {@code E}
* can be cast to {@code T}.</li>
* </ol>
* Checks 1-3 are bypassed when option {@link Option#ALWAYS ALWAYS} is
* specified.
*/
public class InputBus { public class InputBus {
private static class WrappedListener { /**
* Options that allow customization of checks for listeners.
*/
public enum Option {
/**
* Ignore checks for consumed events, hover and focus; deliver event if
* at all possible. This is shorthand for {@link #RECEIVE_CONSUMED},
* {@link #IGNORE_HOVER} and {@link #IGNORE_FOCUS}.
*/
ALWAYS,
/**
* Receive events that were previously consumed.
*/
RECEIVE_CONSUMED,
/**
* Do not process events if the listener is registered with a component
* and the component is not hovered.
*/
REQUIRE_HOVER,
/**
* Deliver events even if the event is limited to hovered components by
* default.
*/
IGNORE_HOVER,
/**
* Do not process events if the listener is registered with a component
* and the component is not focused.
*/
REQUIRE_FOCUS,
/**
* Deliver events even if the event is limited to focused components by
* default.
*/
IGNORE_FOCUS,
/**
* Deliver events according to
* {@link KeyMatcher#matchesIgnoringAction(KeyEvent)} rather than
* {@link KeyMatcher#matches(KeyEvent)} when a {@link KeyMatcher} is
* specified.
*/
IGNORE_ACTION;
}
private enum YesNoDefault {
YES, NO, DEFAULT;
}
/**
* A listener with check preferences resolved and type specified.
*/
private class WrappedListener {
private final Class<?> type; private final Class<?> type;
private final boolean handleConsumed;
private final boolean dropIfConsumed;
private final YesNoDefault dropIfNotHovered;
private final YesNoDefault dropIfNotFocused;
private final InputListener<?> listener; private final InputListener<?> listener;
public WrappedListener( public WrappedListener(
Class<?> type, Class<?> type,
boolean handleConsumed, boolean dropIfConsumed,
YesNoDefault dropIfNotHovered,
YesNoDefault dropIfNotFocused,
InputListener<?> listener InputListener<?> listener
) { ) {
this.type = type; this.type = type;
this.handleConsumed = handleConsumed; this.dropIfConsumed = dropIfConsumed;
this.dropIfNotHovered = dropIfNotHovered;
this.dropIfNotFocused = dropIfNotFocused;
this.listener = listener; this.listener = listener;
} }
private boolean handles(Input input) { private boolean handles(InputEvent input) {
return (!input.isConsumed() || handleConsumed) && if (dropIfConsumed && input.isConsumed())
type.isInstance(input.getEvent()); return false;
switch (dropIfNotHovered) {
case YES:
if (!isHovered())
return false;
break;
case NO:
break;
default:
if (isHovered())
break;
if (input instanceof KeyEvent && ((KeyEvent) input).isMouse())
return false;
if (input instanceof CursorEvent)
return false;
if (input instanceof WheelEvent)
return false;
break;
} }
switch (dropIfNotFocused) {
case YES:
if (!isFocused())
return false;
break;
case NO:
break;
default:
if (isFocused())
break;
if (input instanceof KeyEvent && !((KeyEvent) input).isMouse())
return false;
break;
}
if (!type.isInstance(input))
return false;
return true;
}
/**
* Invokes the listener if the event is deemed appropriate by the four
* checks.
*
* @param event the event to deliver
*/
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void handle(Input input) { public void handle(InputEvent event) {
if (handles(input)) { if (handles(event)) {
boolean consumed = ((InputListener<InputEvent>) listener) // A runtime check of types has been performed; this is safe.
.handle( InputListener<InputEvent> castListener = (InputListener<InputEvent>) listener;
(InputEvent) type.cast(input.getEvent())
try {
castListener.handle(event);
} catch (Exception e) {
throw CrashReports.report(
e,
"InputListener %s for component %s has failed to receive event %s",
listener,
owner,
event
); );
}
input.setConsumed(consumed);
} }
} }
} }
/**
* The component queried for focus and hover. May be {@code null}.
*/
private final Component owner;
/**
* Registered listeners.
*/
private final Collection<WrappedListener> listeners = new ArrayList<>(4); private final Collection<WrappedListener> listeners = new ArrayList<>(4);
public void dispatch(Input input) { /**
listeners.forEach(l -> l.handle(input)); * Creates a new input bus that consults the specified {@link Component} to
* determine hover and focus.
*
* @param owner the component to use for hover and focus tests
* @see #InputBus()
*/
public InputBus(Component owner) {
this.owner = Objects.requireNonNull(owner, "owner");
} }
/**
* Creates a new input bus that assumes all hover and focus checks are
* successful.
*
* @see #InputBus(Component)
*/
public InputBus() {
this.owner = null;
}
/**
* Determines whether hover should be assumed for this event bus.
*
* @return {@code true} iff no component is linked or the linked component
* is hovered
*/
private boolean isHovered() {
return owner == null ? true : owner.isHovered();
}
/**
* Determines whether focus should be assumed for this event bus.
*
* @return {@code true} iff no component is linked or the linked component
* is focused
*/
private boolean isFocused() {
return owner == null ? true : owner.isFocused();
}
/**
* Dispatches (delivers) the provided event to all appropriate listeners.
*
* @param event the event to process
*/
public void dispatch(InputEvent event) {
Objects.requireNonNull(event, "event");
for (WrappedListener listener : listeners) {
listener.handle(event);
}
}
/**
* Registers a listener on this bus.
* <p>
* {@code type} specifies the class of events that should be passed to this
* listener. Only events of types that extend, implement or equal
* {@code type} are processed.
* <p>
* Zero or more {@link Option}s may be specified to enable or disable the
* processing of certain events in certain circumstances. See
* {@linkplain InputBus class description} for a detailed breakdown of the
* checks performed and the effects of various options. When providing
* options to this method, later options override the effects of previous
* options.
* <p>
* Option {@link Option#IGNORE_ACTION IGNORE_ACTION} is ignored silently.
*
* @param type the event class to deliver
* @param listener the listener
* @param options the options for this listener
*/
public <T extends InputEvent> void register( public <T extends InputEvent> void register(
Class<? extends T> type, Class<? extends T> type,
boolean handlesConsumed, InputListener<T> listener,
InputListener<T> listener Option... options
) { ) {
listeners.add(new WrappedListener(type, handlesConsumed, listener)); Objects.requireNonNull(type, "type");
Objects.requireNonNull(listener, "listener");
boolean dropIfConsumed = true;
YesNoDefault dropIfNotHovered = YesNoDefault.DEFAULT;
YesNoDefault dropIfNotFocused = YesNoDefault.DEFAULT;
if (options != null) {
for (Option option : options) {
switch (option) {
case ALWAYS:
dropIfConsumed = false;
dropIfNotHovered = YesNoDefault.NO;
dropIfNotFocused = YesNoDefault.NO;
break;
case RECEIVE_CONSUMED:
dropIfConsumed = false;
break;
case REQUIRE_HOVER:
dropIfNotHovered = YesNoDefault.YES;
break;
case IGNORE_HOVER:
dropIfNotFocused = YesNoDefault.NO;
break;
case REQUIRE_FOCUS:
dropIfNotHovered = YesNoDefault.YES;
break;
case IGNORE_FOCUS:
dropIfNotFocused = YesNoDefault.NO;
break;
case IGNORE_ACTION:
// Ignore
break;
default:
throw new IllegalArgumentException("Unexpected option " + option);
}
}
} }
public <T extends InputEvent> void register( listeners.add(new WrappedListener(type, dropIfConsumed, dropIfNotHovered, dropIfNotFocused, listener));
Class<? extends T> type,
InputListener<T> listener
) {
register(type, false, listener);
} }
/**
* Registers a {@link KeyEvent} listener on this bus. An event has to match
* the provided {@link KeyMatcher} to be delivered to the listener.
* <p>
* Zero or more {@link Option}s may be specified to enable or disable the
* processing of certain events in certain circumstances. See
* {@linkplain InputBus class description} for a detailed breakdown of the
* checks performed and the effects of various options. When providing
* options to this method, later options override the effects of previous
* options.
* <p>
* Option {@link Option#IGNORE_ACTION IGNORE_ACTION} requests that events
* are delivered according to
* {@link KeyMatcher#matchesIgnoringAction(KeyEvent)} rather than
* {@link KeyMatcher#matches(KeyEvent)}.
* specified.
*
* @param matcher an event filter
* @param listener the listener
* @param options the options for this listener
*/
public void register(KeyMatcher matcher, InputListener<? super KeyEvent> listener, Option... options) {
Objects.requireNonNull(matcher, "matcher");
Objects.requireNonNull(listener, "listener");
InputListener<KeyEvent> filteringListener;
if (ArrayUtil.firstIndexOf(options, Option.IGNORE_ACTION) != -1) {
filteringListener = e -> {
if (matcher.matchesIgnoringAction(e)) {
listener.handle(e);
}
};
} else {
filteringListener = e -> {
if (matcher.matches(e)) {
listener.handle(e);
}
};
}
register(KeyEvent.class, filteringListener, options);
}
/**
* Removes all occurrences of the provided listener from this bus.
*
* @param listener the listener to unregister
*/
public void unregister(InputListener<?> listener) { public void unregister(InputListener<?> listener) {
if (listener == null) {
return;
}
listeners.removeIf(l -> l.listener == listener); listeners.removeIf(l -> l.listener == listener);
} }

View File

@ -23,6 +23,6 @@ import ru.windcorp.progressia.client.graphics.input.InputEvent;
@FunctionalInterface @FunctionalInterface
public interface InputListener<T extends InputEvent> { public interface InputListener<T extends InputEvent> {
boolean handle(T event); void handle(T event);
} }

View File

@ -25,6 +25,7 @@ import glm.vec._3.Vec3;
import glm.vec._4.Vec4; import glm.vec._4.Vec4;
import ru.windcorp.progressia.client.graphics.model.ShapeRenderProgram.VertexBuilder; import ru.windcorp.progressia.client.graphics.model.ShapeRenderProgram.VertexBuilder;
import ru.windcorp.progressia.client.graphics.texture.Texture; import ru.windcorp.progressia.client.graphics.texture.Texture;
import ru.windcorp.progressia.client.graphics.world.WorldRenderProgram.WRPVertexBuilder;
import ru.windcorp.progressia.common.world.rels.AbsFace; import ru.windcorp.progressia.common.world.rels.AbsFace;
public class ShapeParts { public class ShapeParts {
@ -40,9 +41,48 @@ public class ShapeParts {
Vec3 width, Vec3 width,
Vec3 height, Vec3 height,
boolean flip boolean flip
) {
return createRectangle(program, texture, colorMultiplier, origin, width, height, flip, null);
}
public static ShapePart createRectangle(
ShapeRenderProgram program,
Texture texture,
Vec4 colorMultiplier,
Vec3 origin,
Vec3 width,
Vec3 height,
boolean flip,
Vec3 forcedNormals
) { ) {
VertexBuilder builder = program.getVertexBuilder(); VertexBuilder builder = program.getVertexBuilder();
if (forcedNormals != null && builder instanceof WRPVertexBuilder) {
((WRPVertexBuilder) builder).addVertex(
origin,
colorMultiplier,
new Vec2(0, 0),
forcedNormals
);
((WRPVertexBuilder) builder).addVertex(
origin.add_(height),
colorMultiplier,
new Vec2(0, 1),
forcedNormals
);
((WRPVertexBuilder) builder).addVertex(
origin.add_(width),
colorMultiplier,
new Vec2(1, 0),
forcedNormals
);
((WRPVertexBuilder) builder).addVertex(
origin.add_(width).add(height),
colorMultiplier,
new Vec2(1, 1),
forcedNormals
);
} else {
builder.addVertex( builder.addVertex(
origin, origin,
colorMultiplier, colorMultiplier,
@ -60,6 +100,7 @@ public class ShapeParts {
colorMultiplier, colorMultiplier,
new Vec2(1, 1) new Vec2(1, 1)
); );
}
ShortBuffer buffer = flip ? ShortBuffer.wrap( ShortBuffer buffer = flip ? ShortBuffer.wrap(
new short[] { new short[] {

View File

@ -0,0 +1,248 @@
/*
* Progressia
* Copyright (C) 2020-2021 Wind Corporation and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package ru.windcorp.progressia.client.graphics.model;
import java.nio.ShortBuffer;
import java.util.Objects;
import glm.mat._3.Mat3;
import glm.mat._4.Mat4;
import glm.vec._2.Vec2;
import glm.vec._3.Vec3;
import glm.vec._4.Vec4;
import ru.windcorp.progressia.client.graphics.Colors;
import ru.windcorp.progressia.client.graphics.model.ShapeRenderProgram.VertexBuilder;
import ru.windcorp.progressia.client.graphics.texture.Texture;
import ru.windcorp.progressia.client.graphics.world.WorldRenderProgram;
import ru.windcorp.progressia.client.graphics.world.WorldRenderProgram.WRPVertexBuilder;
import ru.windcorp.progressia.common.util.StashingStack;
import ru.windcorp.progressia.common.util.VectorUtil;
public class ShapePrototype {
private final ShapeRenderProgram program;
private Texture texture;
private final Vec3[] positions;
private final Vec4[] colorMultipliers;
private final Vec2[] textureCoords;
private final Vec3[] forcedNormals;
private ShortBuffer indices;
private ShortBuffer flippedIndices = null;
protected static final int TRANSFORM_STACK_SIZE = 64;
private final StashingStack<Mat4> transformStack = new StashingStack<>(
TRANSFORM_STACK_SIZE,
Mat4::new
);
public ShapePrototype(
ShapeRenderProgram program,
Texture texture,
Vec3[] positions,
Vec4[] colorMultipliers,
Vec2[] textureCoords,
Vec3[] forcedNormals,
ShortBuffer indices
) {
this.program = Objects.requireNonNull(program, "program");
this.texture = texture;
this.indices = Objects.requireNonNull(indices, "indices");
Objects.requireNonNull(positions, "positions");
Objects.requireNonNull(colorMultipliers, "colorMultipliers");
Objects.requireNonNull(textureCoords, "textureCoords");
if (forcedNormals != null && !(program instanceof WorldRenderProgram)) {
throw new IllegalArgumentException("Cannot force normals on non-WorldRenderPrograms cuz javahorse stupiddd");
}
if (forcedNormals == null) {
forcedNormals = new Vec3[positions.length];
}
this.positions = positions;
this.colorMultipliers = colorMultipliers;
this.textureCoords = textureCoords;
this.forcedNormals = forcedNormals;
if (positions.length != colorMultipliers.length) {
throw new IllegalArgumentException("positions.length (" + positions.length + ") != colorMultipliers.length (" + colorMultipliers + ")");
}
if (positions.length != textureCoords.length) {
throw new IllegalArgumentException("positions.length (" + positions.length + ") != textureCoords.length (" + textureCoords + ")");
}
if (positions.length != forcedNormals.length) {
throw new IllegalArgumentException("positions.length (" + positions.length + ") != forcedNormals.length (" + forcedNormals + ")");
}
transformStack.push().identity();
}
public ShapePart build() {
VertexBuilder builder = program.getVertexBuilder();
Vec3 transformedPosition = new Vec3();
for (int i = 0; i < positions.length; ++i) {
transformedPosition.set(positions[i]);
if (transformStack.getSize() > 1) {
VectorUtil.applyMat4(transformedPosition, transformStack.getHead());
}
if (forcedNormals[i] != null && builder instanceof WRPVertexBuilder) {
((WRPVertexBuilder) builder).addVertex(
transformedPosition, colorMultipliers[i], textureCoords[i], forcedNormals[i]
);
} else {
builder.addVertex(transformedPosition, colorMultipliers[i], textureCoords[i]);
}
}
return new ShapePart(texture, builder.assemble(), indices);
}
public ShapePrototype apply(Mat4 transform) {
for (Vec3 vector : positions) {
VectorUtil.applyMat4(vector, transform);
}
return this;
}
public ShapePrototype apply(Mat3 transform) {
for (Vec3 vector : positions) {
transform.mul(vector);
}
return this;
}
public Mat4 push() {
Mat4 previous = transformStack.getHead();
return transformStack.push().set(previous);
}
public ShapePrototype pop() {
transformStack.pop();
return this;
}
public ShapePrototype setTexture(Texture texture) {
this.texture = texture;
return this;
}
public ShapePrototype deleteTexture() {
this.texture = null;
return this;
}
public ShapePrototype resetColorMultiplier() {
for (Vec4 color : colorMultipliers) {
color.set(Colors.WHITE);
}
return this;
}
public ShapePrototype addColorMultiplier(Vec4 color) {
for (Vec4 c : colorMultipliers) {
c.mul(color);
}
return this;
}
public ShapePrototype flip() {
ShortBuffer tmp = indices;
indices = flippedIndices;
flippedIndices = tmp;
if (indices == null) {
int length = flippedIndices.limit();
indices = ShortBuffer.allocate(length);
for (int i = 0; i < length; ++i) {
indices.put(i, flippedIndices.get(length - i - 1));
}
}
return this;
}
public ShapePrototype makeDoubleSided() {
int length = indices.limit();
ShortBuffer newIndices = ShortBuffer.allocate(length * 2);
for (int i = 0; i < length; ++i) {
newIndices.put(i, indices.get(i));
}
for (int i = 0; i < length; ++i) {
newIndices.put(i + length, indices.get(length - i - 1));
}
indices = flippedIndices = newIndices;
return this;
}
public static ShapePrototype unitSquare(Texture texture, Vec4 color, ShapeRenderProgram program) {
return new ShapePrototype(
program,
texture,
new Vec3[] {
new Vec3(0, 0, 0),
new Vec3(0, 1, 0),
new Vec3(1, 0, 0),
new Vec3(1, 1, 0)
},
new Vec4[] {
new Vec4(color),
new Vec4(color),
new Vec4(color),
new Vec4(color)
},
new Vec2[] {
new Vec2(0, 0),
new Vec2(0, 1),
new Vec2(1, 0),
new Vec2(1, 1)
},
null,
ShortBuffer.wrap(new short[] {3, 1, 0, 2, 3, 0})
);
}
public static ShapePrototype unitSquare(Texture texture, Vec4 color) {
return unitSquare(texture, color, WorldRenderProgram.getDefault());
}
public static ShapePrototype unitSquare(Texture texture) {
return unitSquare(texture, Colors.WHITE, WorldRenderProgram.getDefault());
}
public static ShapePrototype unitSquare(Vec4 color) {
return unitSquare(null, color, WorldRenderProgram.getDefault());
}
public static ShapePrototype unitSquare() {
return unitSquare(null, Colors.WHITE, WorldRenderProgram.getDefault());
}
}

View File

@ -20,10 +20,12 @@ package ru.windcorp.progressia.client.graphics.model;
import java.util.Map; import java.util.Map;
import glm.mat._4.Mat4;
import glm.vec._3.Vec3; import glm.vec._3.Vec3;
import glm.vec._4.Vec4; import glm.vec._4.Vec4;
import ru.windcorp.progressia.client.graphics.backend.Usage; import ru.windcorp.progressia.client.graphics.backend.Usage;
import ru.windcorp.progressia.client.graphics.texture.Texture; import ru.windcorp.progressia.client.graphics.texture.Texture;
import ru.windcorp.progressia.common.util.VectorUtil;
import ru.windcorp.progressia.common.world.rels.AbsFace; import ru.windcorp.progressia.common.world.rels.AbsFace;
public class Shapes { public class Shapes {
@ -260,6 +262,34 @@ public class Shapes {
return this.setSize(size, size, size); return this.setSize(size, size, size);
} }
public PppBuilder centerAt(float x, float y, float z) {
origin.set(x, y, z);
origin.mul(2);
origin.sub(width);
origin.sub(height);
origin.sub(depth);
origin.div(2);
return this;
}
public PppBuilder apply(Mat4 transform) {
VectorUtil.applyMat4(origin, transform);
VectorUtil.rotateOnly(width, transform);
VectorUtil.rotateOnly(height, transform);
VectorUtil.rotateOnly(depth, transform);
return this;
}
public PppBuilder scale(float factor) {
origin.mul(factor);
width.mul(factor);
height.mul(factor);
depth.mul(factor);
return this;
}
public PppBuilder flip() { public PppBuilder flip() {
this.flip = true; this.flip = true;
return this; return this;

View File

@ -163,7 +163,7 @@ public class Atlases {
} }
} }
private static final TextureSettings SETTINGS = new TextureSettings(false); private static final TextureSettings SETTINGS = new TextureSettings(false, false);
private static final Map<Resource, Sprite> LOADED = new HashMap<>(); private static final Map<Resource, Sprite> LOADED = new HashMap<>();
private static final Multimap<AtlasGroup, Atlas> ATLASES = MultimapBuilder.hashKeys().arrayListValues().build(); private static final Multimap<AtlasGroup, Atlas> ATLASES = MultimapBuilder.hashKeys().arrayListValues().build();

View File

@ -28,7 +28,7 @@ import ru.windcorp.progressia.common.util.crash.CrashReports;
public class SimpleTextures { public class SimpleTextures {
private static final TextureSettings SETTINGS = new TextureSettings(false); private static final TextureSettings SETTINGS = new TextureSettings(false, false);
private static final Map<Resource, Texture> TEXTURES = new HashMap<>(); private static final Map<Resource, Texture> TEXTURES = new HashMap<>();

View File

@ -23,7 +23,7 @@ import static org.lwjgl.opengl.GL12.*;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
class TextureData { public class TextureData {
private final ByteBuffer data; private final ByteBuffer data;

View File

@ -28,6 +28,7 @@ import javax.imageio.ImageIO;
import ru.windcorp.progressia.common.resource.Resource; import ru.windcorp.progressia.common.resource.Resource;
import ru.windcorp.progressia.common.util.BinUtil; import ru.windcorp.progressia.common.util.BinUtil;
import ru.windcorp.progressia.common.util.crash.CrashReports;
public class TextureLoader { public class TextureLoader {
@ -41,8 +42,16 @@ public class TextureLoader {
int width = readResult.getWidth(); int width = readResult.getWidth();
int height = readResult.getHeight(); int height = readResult.getHeight();
int bufferWidth = BinUtil.roundToGreaterPowerOf2(width); int bufferWidth;
int bufferHeight = BinUtil.roundToGreaterPowerOf2(height); int bufferHeight;
if (settings.allocateExactBuffer()) {
bufferWidth = width;
bufferHeight = height;
} else {
bufferWidth = BinUtil.roundToGreaterPowerOf2(width);
bufferHeight = BinUtil.roundToGreaterPowerOf2(height);
}
WritableRaster raster = TextureUtil.createRaster( WritableRaster raster = TextureUtil.createRaster(
bufferWidth, bufferWidth,
@ -99,7 +108,12 @@ public class TextureLoader {
TextureSettings settings TextureSettings settings
) )
throws IOException { throws IOException {
return loadPixels(resource.getInputStream(), settings);
InputStream stream = resource.getInputStream();
if (stream == null) {
throw CrashReports.report(null, "Texture \"%s\" not found", resource.getName());
}
return loadPixels(stream, settings);
} }
private TextureLoader() { private TextureLoader() {

View File

@ -21,13 +21,19 @@ package ru.windcorp.progressia.client.graphics.texture;
public class TextureSettings { public class TextureSettings {
private final boolean isFiltered; private final boolean isFiltered;
private final boolean allocateExactBuffer;
public TextureSettings(boolean isFiltered) { public TextureSettings(boolean isFiltered, boolean allocateExactBuffer) {
this.isFiltered = isFiltered; this.isFiltered = isFiltered;
this.allocateExactBuffer = allocateExactBuffer;
} }
public boolean isFiltered() { public boolean isFiltered() {
return isFiltered; return isFiltered;
} }
public boolean allocateExactBuffer() {
return allocateExactBuffer;
}
} }

View File

@ -92,4 +92,13 @@ public class EntityAnchor implements Anchor {
return modes; return modes;
} }
public EntityData getEntity() {
return entity;
}
@Override
public String toString() {
return "Anchor for entity " + entity;
}
} }

View File

@ -31,7 +31,7 @@ import ru.windcorp.progressia.client.comms.controls.InputBasedControls;
import ru.windcorp.progressia.client.graphics.Layer; import ru.windcorp.progressia.client.graphics.Layer;
import ru.windcorp.progressia.client.graphics.backend.FaceCulling; import ru.windcorp.progressia.client.graphics.backend.FaceCulling;
import ru.windcorp.progressia.client.graphics.backend.GraphicsInterface; import ru.windcorp.progressia.client.graphics.backend.GraphicsInterface;
import ru.windcorp.progressia.client.graphics.input.bus.Input; import ru.windcorp.progressia.client.graphics.input.InputEvent;
import ru.windcorp.progressia.client.graphics.model.Renderable; import ru.windcorp.progressia.client.graphics.model.Renderable;
import ru.windcorp.progressia.client.graphics.model.ShapeRenderProgram; import ru.windcorp.progressia.client.graphics.model.ShapeRenderProgram;
import ru.windcorp.progressia.client.graphics.model.Shapes.PppBuilder; import ru.windcorp.progressia.client.graphics.model.Shapes.PppBuilder;
@ -45,7 +45,7 @@ import ru.windcorp.progressia.common.util.Vectors;
import ru.windcorp.progressia.common.world.GravityModel; import ru.windcorp.progressia.common.world.GravityModel;
import ru.windcorp.progressia.common.world.entity.EntityData; import ru.windcorp.progressia.common.world.entity.EntityData;
import ru.windcorp.progressia.test.CollisionModelRenderer; import ru.windcorp.progressia.test.CollisionModelRenderer;
import ru.windcorp.progressia.test.TestPlayerControls; import ru.windcorp.progressia.test.controls.TestPlayerControls;
public class LayerWorld extends Layer { public class LayerWorld extends Layer {
@ -214,6 +214,9 @@ public class LayerWorld extends Layer {
if (ClientState.getInstance().getLocalPlayer().getEntity() == entity && tmp_testControls.isFlying()) { if (ClientState.getInstance().getLocalPlayer().getEntity() == entity && tmp_testControls.isFlying()) {
return; return;
} }
if (entity.getId().equals("Test:NoclipCamera")) {
return;
}
Vec3 gravitationalAcceleration = Vectors.grab3(); Vec3 gravitationalAcceleration = Vectors.grab3();
gm.getGravity(entity.getPosition(), gravitationalAcceleration); gm.getGravity(entity.getPosition(), gravitationalAcceleration);
@ -225,14 +228,9 @@ public class LayerWorld extends Layer {
} }
@Override @Override
protected void handleInput(Input input) { public void handleInput(InputEvent event) {
if (input.isConsumed()) if (!event.isConsumed()) {
return; inputBasedControls.handleInput(event);
tmp_testControls.handleInput(input);
if (!input.isConsumed()) {
inputBasedControls.handleInput(input);
} }
} }

View File

@ -200,6 +200,10 @@ public class WorldRenderProgram extends ShapeRenderProgram {
4 * Float.BYTES + 4 * Float.BYTES +
2 * Float.BYTES); 2 * Float.BYTES);
if (!Float.isNaN(vertices.getFloat(offset + 0 * Float.BYTES))) {
return; // normals are forced
}
vertices.putFloat(offset + 0 * Float.BYTES, normal.x); vertices.putFloat(offset + 0 * Float.BYTES, normal.x);
vertices.putFloat(offset + 1 * Float.BYTES, normal.y); vertices.putFloat(offset + 1 * Float.BYTES, normal.y);
vertices.putFloat(offset + 2 * Float.BYTES, normal.z); vertices.putFloat(offset + 2 * Float.BYTES, normal.z);
@ -212,7 +216,7 @@ public class WorldRenderProgram extends ShapeRenderProgram {
return new WRPVertexBuilder(); return new WRPVertexBuilder();
} }
private static class WRPVertexBuilder implements VertexBuilder { public static class WRPVertexBuilder implements VertexBuilder {
// TODO Throw VertexBuilders out the window and rewrite completely. // TODO Throw VertexBuilders out the window and rewrite completely.
// I want to _extend_ VBs, not re-implement them for children of SRP // I want to _extend_ VBs, not re-implement them for children of SRP
@ -220,11 +224,17 @@ public class WorldRenderProgram extends ShapeRenderProgram {
final Vec3 position; final Vec3 position;
final Vec4 colorMultiplier; final Vec4 colorMultiplier;
final Vec2 textureCoords; final Vec2 textureCoords;
final Vec3 normals;
Vertex(Vec3 position, Vec4 colorMultiplier, Vec2 textureCoords) { Vertex(Vec3 position, Vec4 colorMultiplier, Vec2 textureCoords) {
this(position, colorMultiplier, textureCoords, new Vec3(Float.NaN, Float.NaN, Float.NaN));
}
Vertex(Vec3 position, Vec4 colorMultiplier, Vec2 textureCoords, Vec3 normals) {
this.position = position; this.position = position;
this.colorMultiplier = colorMultiplier; this.colorMultiplier = colorMultiplier;
this.textureCoords = textureCoords; this.textureCoords = textureCoords;
this.normals = normals;
} }
} }
@ -292,6 +302,24 @@ public class WorldRenderProgram extends ShapeRenderProgram {
return this; return this;
} }
public VertexBuilder addVertex(
Vec3 position,
Vec4 colorMultiplier,
Vec2 textureCoords,
Vec3 normals
) {
vertices.add(
new Vertex(
new Vec3(position),
new Vec4(colorMultiplier),
new Vec2(textureCoords),
new Vec3(normals)
)
);
return this;
}
@Override @Override
public ByteBuffer assemble() { public ByteBuffer assemble() {
ByteBuffer result = BufferUtils.createByteBuffer( ByteBuffer result = BufferUtils.createByteBuffer(
@ -309,9 +337,9 @@ public class WorldRenderProgram extends ShapeRenderProgram {
.putFloat(v.colorMultiplier.w) .putFloat(v.colorMultiplier.w)
.putFloat(v.textureCoords.x) .putFloat(v.textureCoords.x)
.putFloat(v.textureCoords.y) .putFloat(v.textureCoords.y)
.putFloat(Float.NaN) .putFloat(v.normals.x)
.putFloat(Float.NaN) .putFloat(v.normals.y)
.putFloat(Float.NaN); .putFloat(v.normals.z);
} }
result.flip(); result.flip();

View File

@ -73,6 +73,12 @@ public class Localizer {
return language; return language;
} }
public List<String> getLanguages() {
List<String> result = new ArrayList<>(langList.keySet());
result.sort(null);
return result;
}
public synchronized String getValue(String key) { public synchronized String getValue(String key) {
if (data == null) { if (data == null) {
throw new IllegalStateException("Localizer not yet initialized"); throw new IllegalStateException("Localizer not yet initialized");

View File

@ -42,7 +42,7 @@ public class EntityRenderRegistry extends NamespacedInstanceRegistry<EntityRende
ResourceManager.getTextureResource( ResourceManager.getTextureResource(
"entities/" + name "entities/" + name
), ),
new TextureSettings(false) new TextureSettings(false, false)
).getData() ).getData()
); );
} catch (IOException e) { } catch (IOException e) {

View File

@ -114,7 +114,8 @@ public class TileRenderCross extends TileRender implements TileOptimizedCustom {
origin, origin,
width, width,
height, height,
false false,
new Vec3(0, 0, 1)
) )
); );
output.accept( output.accept(
@ -125,7 +126,8 @@ public class TileRenderCross extends TileRender implements TileOptimizedCustom {
origin, origin,
width, width,
height, height,
true true,
new Vec3(0, 0, 1)
) )
); );
} }

View File

@ -105,6 +105,9 @@ public class Collider {
// For every pair of colls // For every pair of colls
for (int i = 0; i < colls.size(); ++i) { for (int i = 0; i < colls.size(); ++i) {
Collideable a = colls.get(i); Collideable a = colls.get(i);
if (a.getCollisionModel() == null) {
continue;
}
tuneWorldCollisionHelper(a, tickLength, world, workspace); tuneWorldCollisionHelper(a, tickLength, world, workspace);
@ -115,6 +118,10 @@ public class Collider {
for (int j = i + 1; j < colls.size(); ++j) { for (int j = i + 1; j < colls.size(); ++j) {
Collideable b = colls.get(j); Collideable b = colls.get(j);
if (b.getCollisionModel() == null) {
continue;
}
Collision collision = getCollision(a, b, tickLength, workspace); Collision collision = getCollision(a, b, tickLength, workspace);
result = workspace.updateLatestCollision(result, collision); result = workspace.updateLatestCollision(result, collision);
} }

View File

@ -29,6 +29,20 @@ public abstract class AbstractStatefulObjectLayout
super(objectId); super(objectId);
} }
@Override
public StateStorage createStorage() {
StateStorage storage = instantiateStorage();
int fieldCount = getFieldCount();
for (int i = 0; i < fieldCount; ++i) {
getField(i).setDefault(storage);
}
return storage;
}
protected abstract StateStorage instantiateStorage();
protected abstract int getFieldCount(); protected abstract int getFieldCount();
protected abstract StateField getField(int fieldIndex); protected abstract StateField getField(int fieldIndex);

View File

@ -0,0 +1,64 @@
/*
* Progressia
* Copyright (C) 2020-2021 Wind Corporation and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package ru.windcorp.progressia.common.state;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public interface Encodable {
/**
* Sets the state of this object according to the binary representation read
* from {@code input}. If {@code context == COMMS}, the state of local
* fields is unspecified after this operation.
*
* @param input a {@link DataInput} that a state can be read from
* @param context the context
* @throws IOException if the state is encoded poorly or an error occurs
* in {@code input}
*/
void read(DataInput input, IOContext context) throws IOException;
/**
* Writes the binary representation of the state of this object to the
* {@code output}.
*
* @param output a {@link DataOutput} that a state can be written to
* @param context the context
* @throws IOException if an error occurs in {@code output}
*/
void write(DataOutput output, IOContext context) throws IOException;
/**
* Turns {@code destination} into a deep copy of this object.
* <p>
* Changes the provided object so that:
* <ul>
* <li>the provided object equals this object; and</li>
* <li>the provided object is independent of this object, meaning no change
* to {@code destination} can affect this object.</li>
* </ul>
*
* @param destination the object to copy this object into. Runtime class
* must match this class
* @return {@code destination}
*/
void copy(Encodable destination);
}

View File

@ -19,11 +19,14 @@
package ru.windcorp.progressia.common.state; package ru.windcorp.progressia.common.state;
import gnu.trove.map.TIntIntMap; import gnu.trove.map.TIntIntMap;
import gnu.trove.map.TIntObjectMap;
import gnu.trove.map.hash.TIntIntHashMap; import gnu.trove.map.hash.TIntIntHashMap;
import gnu.trove.map.hash.TIntObjectHashMap;
public class HashMapStateStorage extends StateStorage { public class HashMapStateStorage extends StateStorage {
private final TIntIntMap ints = new TIntIntHashMap(); private final TIntIntMap ints = new TIntIntHashMap();
private final TIntObjectMap<Object> objects = new TIntObjectHashMap<>();
@Override @Override
public int getInt(int index) { public int getInt(int index) {
@ -35,4 +38,14 @@ public class HashMapStateStorage extends StateStorage {
ints.put(index, value); ints.put(index, value);
} }
@Override
public Object getObject(int index) {
return objects.get(index);
}
@Override
public void setObject(int index, Object object) {
objects.put(index, object);
}
} }

View File

@ -20,6 +20,9 @@ package ru.windcorp.progressia.common.state;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Supplier;
import ru.windcorp.progressia.common.state.codec.ObjectCodec;
public class InspectingStatefulObjectLayout public class InspectingStatefulObjectLayout
extends AbstractStatefulObjectLayout { extends AbstractStatefulObjectLayout {
@ -33,7 +36,7 @@ public class InspectingStatefulObjectLayout
} }
@Override @Override
public StateStorage createStorage() { public StateStorage instantiateStorage() {
return new HashMapStateStorage(); return new HashMapStateStorage();
} }
@ -82,6 +85,31 @@ public class InspectingStatefulObjectLayout
} }
private class Obj<T> implements StateFieldBuilder.Obj<T> {
private final ObjectCodec<T> codec;
private final Supplier<T> defaultValue;
public Obj(ObjectCodec<T> codec, Supplier<T> defaultValue) {
this.codec = codec;
this.defaultValue = defaultValue;
}
@Override
public ObjectStateField<T> build() {
return registerField(
new ObjectStateField<T>(
id,
isLocal,
fieldIndexCounters.getObjectsThenIncrement(),
codec,
defaultValue
)
);
}
}
private final String id; private final String id;
private boolean isLocal = true; private boolean isLocal = true;
@ -95,6 +123,11 @@ public class InspectingStatefulObjectLayout
return new Int(); return new Int();
} }
@Override
public <T> Obj<T> of(ObjectCodec<T> codec, Supplier<T> defaultValue) {
return new Obj<T>(codec, defaultValue);
}
@Override @Override
public StateFieldBuilder setLocal(boolean isLocal) { public StateFieldBuilder setLocal(boolean isLocal) {
this.isLocal = isLocal; this.isLocal = isLocal;

View File

@ -79,4 +79,9 @@ public class IntStateField extends StateField {
return get(a) == get(b); return get(a) == get(b);
} }
@Override
public void setDefault(StateStorage storage) {
storage.setInt(getIndex(), 0);
}
} }

View File

@ -0,0 +1,107 @@
/*
* Progressia
* Copyright (C) 2020-2021 Wind Corporation and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package ru.windcorp.progressia.common.state;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.function.Supplier;
import ru.windcorp.progressia.common.state.codec.ObjectCodec;
public class ObjectStateField<T> extends StateField {
private final ObjectCodec<T> codec;
private final Supplier<T> defaultValue;
public ObjectStateField(
String id,
boolean isLocal,
int index,
ObjectCodec<T> codec,
Supplier<T> defaultValue
) {
super(id, isLocal, index);
this.codec = codec;
this.defaultValue = defaultValue;
}
public ObjectCodec<T> getCodec() {
return codec;
}
@SuppressWarnings("unchecked")
public T get(StatefulObject object) {
return (T) object.getStorage().getObject(getIndex());
}
public void setNow(StatefulObject object, T value) {
object.getStorage().setObject(getIndex(), value);
}
public void set(StateChanger changer, T value) {
changer.setObject(this, value);
}
@Override
public void read(
StatefulObject object,
DataInput input,
IOContext context
)
throws IOException {
T previous = get(object);
T result = codec.read(previous, input, context);
object.getStorage().setObject(getIndex(), result);
}
@Override
public void write(
StatefulObject object,
DataOutput output,
IOContext context
)
throws IOException {
codec.write(object.getStorage().getObject(getIndex()), output, context);
}
@Override
public void copy(StatefulObject from, StatefulObject to) {
setNow(to, get(from));
}
@Override
public int computeHashCode(StatefulObject object) {
return codec.computeHashCode(get(object));
}
@Override
public boolean areEqual(StatefulObject a, StatefulObject b) {
return codec.areEqual(get(a), get(b));
}
@Override
public void setDefault(StateStorage storage) {
storage.setObject(getIndex(), defaultValue.get());
}
}

View File

@ -21,9 +21,11 @@ package ru.windcorp.progressia.common.state;
public class OptimizedStateStorage extends StateStorage { public class OptimizedStateStorage extends StateStorage {
private final int[] ints; private final int[] ints;
private final Object[] objects;
public OptimizedStateStorage(PrimitiveCounters sizes) { public OptimizedStateStorage(PrimitiveCounters sizes) {
this.ints = new int[sizes.getInts()]; this.ints = new int[sizes.getInts()];
this.objects = new Object[sizes.getObjects()];
} }
@Override @Override
@ -36,4 +38,14 @@ public class OptimizedStateStorage extends StateStorage {
ints[index] = value; ints[index] = value;
} }
@Override
public Object getObject(int index) {
return objects[index];
}
@Override
public void setObject(int index, Object object) {
objects[index] = object;
}
} }

View File

@ -19,9 +19,12 @@
package ru.windcorp.progressia.common.state; package ru.windcorp.progressia.common.state;
import java.util.List; import java.util.List;
import java.util.function.Supplier;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import ru.windcorp.progressia.common.state.codec.ObjectCodec;
public class OptimizedStatefulObjectLayout public class OptimizedStatefulObjectLayout
extends AbstractStatefulObjectLayout { extends AbstractStatefulObjectLayout {
@ -49,7 +52,7 @@ public class OptimizedStatefulObjectLayout
} }
@Override @Override
public StateStorage createStorage() { public StateStorage instantiateStorage() {
return new OptimizedStateStorage(sizes); return new OptimizedStateStorage(sizes);
} }
@ -72,6 +75,17 @@ public class OptimizedStatefulObjectLayout
}; };
} }
@Override
public <T> Obj<T> of(ObjectCodec<T> codec, Supplier<T> defaultValue) {
return new Obj<T>() {
@SuppressWarnings("unchecked")
@Override
public ObjectStateField<T> build() {
return (ObjectStateField<T>) result;
}
};
}
@Override @Override
public StateFieldBuilder setLocal(boolean isLocal) { public StateFieldBuilder setLocal(boolean isLocal) {
return this; return this;

View File

@ -21,12 +21,14 @@ package ru.windcorp.progressia.common.state;
class PrimitiveCounters { class PrimitiveCounters {
private int ints = 0; private int ints = 0;
private int objects = 0;
public PrimitiveCounters() { public PrimitiveCounters() {
} }
public PrimitiveCounters(PrimitiveCounters copyFrom) { public PrimitiveCounters(PrimitiveCounters copyFrom) {
this.ints = copyFrom.ints; this.ints = copyFrom.ints;
this.objects = copyFrom.objects;
} }
public int getInts() { public int getInts() {
@ -37,4 +39,12 @@ class PrimitiveCounters {
return this.ints++; return this.ints++;
} }
public int getObjects() {
return objects;
}
public int getObjectsThenIncrement() {
return this.objects++;
}
} }

View File

@ -21,5 +21,6 @@ package ru.windcorp.progressia.common.state;
public interface StateChanger { public interface StateChanger {
void setInt(IntStateField field, int value); void setInt(IntStateField field, int value);
<T> void setObject(ObjectStateField<T> field, T value);
} }

View File

@ -67,4 +67,6 @@ public abstract class StateField extends Namespaced {
public abstract boolean areEqual(StatefulObject a, StatefulObject b); public abstract boolean areEqual(StatefulObject a, StatefulObject b);
public abstract void setDefault(StateStorage storage);
} }

View File

@ -18,14 +18,59 @@
package ru.windcorp.progressia.common.state; package ru.windcorp.progressia.common.state;
import java.util.function.Supplier;
import ru.windcorp.progressia.common.state.codec.ObjectCodec;
import ru.windcorp.progressia.common.state.codec.ObjectCodecRegistry;
public interface StateFieldBuilder { public interface StateFieldBuilder {
public static interface Int { public static interface Int {
IntStateField build(); IntStateField build();
} }
public static interface Obj<T> {
ObjectStateField<T> build();
}
Int ofInt(); Int ofInt();
<T> Obj<T> of(ObjectCodec<T> codec, Supplier<T> defaultValue);
default <T> Obj<T> of(Class<T> clazz, Supplier<T> defaultValue) {
ObjectCodec<T> codec = ObjectCodecRegistry.get(clazz);
return of(codec, defaultValue);
}
default <T> Obj<T> of(ObjectCodec<T> codec, T defaultValue) {
return of(codec, (Supplier<T>) () -> codec.copy(defaultValue, null));
}
default <T> Obj<T> of(Class<T> clazz, T defaultValue) {
ObjectCodec<T> codec = ObjectCodecRegistry.get(clazz);
return of(codec, (Supplier<T>) () -> codec.copy(defaultValue, null));
}
default <T> Obj<T> of(ObjectCodec<T> codec) {
return of(codec, (Supplier<T>) () -> null);
}
default <T> Obj<T> of(Class<T> clazz) {
ObjectCodec<T> codec = ObjectCodecRegistry.get(clazz);
return of(codec, (Supplier<T>) () -> null);
}
@SuppressWarnings("unchecked")
default <T> Obj<T> def(Supplier<T> defaultValue) {
Class<T> clazz = (Class<T>) defaultValue.get().getClass();
return of(clazz, defaultValue);
}
@SuppressWarnings("unchecked")
default <T> Obj<T> def(T defaultValue) {
return of((Class<T>) defaultValue.getClass(), defaultValue);
}
StateFieldBuilder setLocal(boolean isLocal); StateFieldBuilder setLocal(boolean isLocal);
default StateFieldBuilder setLocal() { default StateFieldBuilder setLocal() {

View File

@ -24,4 +24,8 @@ public abstract class StateStorage {
public abstract void setInt(int index, int value); public abstract void setInt(int index, int value);
public abstract Object getObject(int index);
public abstract void setObject(int index, Object object);
} }

View File

@ -52,7 +52,7 @@ import ru.windcorp.progressia.common.util.namespaces.Namespaced;
* type.</li> * type.</li>
* </ul> * </ul>
*/ */
public abstract class StatefulObject extends Namespaced { public abstract class StatefulObject extends Namespaced implements Encodable {
private final StatefulObjectLayout layout; private final StatefulObjectLayout layout;
@ -133,6 +133,7 @@ public abstract class StatefulObject extends Namespaced {
* @throws IOException if the state is encoded poorly or an error occurs * @throws IOException if the state is encoded poorly or an error occurs
* in {@code input} * in {@code input}
*/ */
@Override
public void read(DataInput input, IOContext context) throws IOException { public void read(DataInput input, IOContext context) throws IOException {
getLayout().read(this, input, context); getLayout().read(this, input, context);
} }
@ -145,6 +146,7 @@ public abstract class StatefulObject extends Namespaced {
* @param context the context * @param context the context
* @throws IOException if an error occurs in {@code output} * @throws IOException if an error occurs in {@code output}
*/ */
@Override
public void write(DataOutput output, IOContext context) throws IOException { public void write(DataOutput output, IOContext context) throws IOException {
getLayout().write(this, output, context); getLayout().write(this, output, context);
} }
@ -166,7 +168,8 @@ public abstract class StatefulObject extends Namespaced {
* *
* @param destination the object to copy this object into. * @param destination the object to copy this object into.
*/ */
public StatefulObject copy(StatefulObject destination) { @Override
public void copy(Encodable destination) {
Objects.requireNonNull(destination, "destination"); Objects.requireNonNull(destination, "destination");
if (destination == this) { if (destination == this) {
@ -182,8 +185,7 @@ public abstract class StatefulObject extends Namespaced {
); );
} }
getLayout().copy(this, destination); getLayout().copy(this, (StatefulObject) destination);
return destination;
} }
/** /**
@ -233,7 +235,7 @@ public abstract class StatefulObject extends Namespaced {
StatefulObject statefulObj = (StatefulObject) obj; StatefulObject statefulObj = (StatefulObject) obj;
if (statefulObj.getId().equals(this.getId())) if (!statefulObj.getId().equals(this.getId()))
return false; return false;
return true; return true;

View File

@ -0,0 +1,52 @@
/*
* Progressia
* Copyright (C) 2020-2021 Wind Corporation and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package ru.windcorp.progressia.common.state.codec;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.Objects;
import ru.windcorp.progressia.common.state.Encodable;
import ru.windcorp.progressia.common.state.IOContext;
public class EncodableCodec extends ReusableObjectCodec<Encodable> {
public EncodableCodec() {
super(Encodable.class);
}
@Override
protected Encodable doRead(Encodable previous, DataInput input, IOContext context) throws IOException {
previous.read(input, context);
return previous;
}
@Override
protected void doWrite(Encodable obj, DataOutput output, IOContext context) throws IOException {
obj.write(output, context);
}
@Override
protected Encodable doCopy(Encodable object, Encodable previous) {
Objects.requireNonNull(previous, "previous");
object.copy(previous);
return previous;
}
}

View File

@ -0,0 +1,43 @@
/*
* Progressia
* Copyright (C) 2020-2021 Wind Corporation and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package ru.windcorp.progressia.common.state.codec;
import java.io.DataInput;
import java.io.IOException;
import ru.windcorp.progressia.common.state.IOContext;
public abstract class ImmutableObjectCodec<T> extends ObjectCodec<T> {
public ImmutableObjectCodec(Class<T> clazz) {
super(clazz);
}
@Override
public final T copy(T object, T previous) {
return object;
}
@Override
protected final T doRead(T previous, DataInput input, IOContext context) throws IOException {
return doRead(input, context);
}
protected abstract T doRead(DataInput input, IOContext context) throws IOException;
}

View File

@ -0,0 +1,73 @@
/*
* Progressia
* Copyright (C) 2020-2021 Wind Corporation and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package ru.windcorp.progressia.common.state.codec;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.Objects;
import ru.windcorp.progressia.common.state.IOContext;
public abstract class ObjectCodec<T> {
private final Class<T> clazz;
public ObjectCodec(Class<T> clazz) {
this.clazz = clazz;
}
public Class<T> getDataType() {
return clazz;
}
@SuppressWarnings("unchecked")
public final T read(Object previous, DataInput input, IOContext context) throws IOException {
assert previous == null || clazz.isInstance(previous)
: "Cannot use codec " + this + " on object of type " + previous.getClass();
T result = doRead((T) previous, input, context);
assert result == null || clazz.isInstance(previous)
: "Codec " + this + " read object of type " + previous.getClass();
return result;
}
protected abstract T doRead(T previous, DataInput input, IOContext context) throws IOException;
@SuppressWarnings("unchecked")
public final void write(Object value, DataOutput output, IOContext context) throws IOException {
assert value == null || clazz.isInstance(value)
: "Cannot use codec " + this + " on object of type " + value.getClass();
doWrite((T) value, output, context);
}
protected abstract void doWrite(T obj, DataOutput output, IOContext context) throws IOException;
public abstract T copy(T object, T previous);
public int computeHashCode(T object) {
return Objects.hashCode(object);
}
public boolean areEqual(T a, T b) {
return Objects.equals(a, b);
}
}

View File

@ -0,0 +1,49 @@
/*
* Progressia
* Copyright (C) 2020-2021 Wind Corporation and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package ru.windcorp.progressia.common.state.codec;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import ru.windcorp.progressia.common.state.Encodable;
public class ObjectCodecRegistry {
private static final Map<Class<?>, ObjectCodec<?>> CODECS = Collections.synchronizedMap(new HashMap<>());
private static final EncodableCodec ENCODABLE_FALLBACK = new EncodableCodec();
public static void register(ObjectCodec<?> codec) {
CODECS.put(codec.getDataType(), codec);
}
@SuppressWarnings("unchecked")
public static <T> ObjectCodec<T> get(Class<T> clazz) {
ObjectCodec<?> codec = CODECS.get(clazz);
if (codec != null) {
return (ObjectCodec<T>) codec;
}
if (Encodable.class.isAssignableFrom(clazz)) {
return (ObjectCodec<T>) ENCODABLE_FALLBACK;
}
throw new IllegalArgumentException("No codec registered for class " + clazz);
}
}

View File

@ -0,0 +1,42 @@
/*
* Progressia
* Copyright (C) 2020-2021 Wind Corporation and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package ru.windcorp.progressia.common.state.codec;
public abstract class ReusableObjectCodec<T> extends ObjectCodec<T> {
public ReusableObjectCodec(Class<T> clazz) {
super(clazz);
}
@Override
public final T copy(T object, T previous) {
if (object == null) {
return null;
}
T result = doCopy(object, previous);
assert result != null : "copy() returned null";
assert areEqual(object, result) : "copy() does not equal original: " + result + " != " + object;
return result;
}
protected abstract T doCopy(T object, T previous);
}

View File

@ -199,6 +199,63 @@ public class VectorUtil {
return rotateOnly(inOut, mat, inOut); return rotateOnly(inOut, mat, inOut);
} }
public static Mat4 lookAt(Mat4 applyTo, Vec3 target, Vec3 up, Mat4 output) {
if (output == null) {
output = new Mat4();
}
Mat4 lookAtTransform = Matrices.grab4();
// Adapted from Glm.lookAt - we use Z as up
// f(normalize(target))
float fX = target.x;
float fY = target.y;
float fZ = target.z;
float inverseSqrt = 1f / (float) Math.sqrt(fX * fX + fY * fY + fZ * fZ);
fX *= inverseSqrt;
fY *= inverseSqrt;
fZ *= inverseSqrt;
// s(normalize(cross(up, f)))
float sX = up.y * fZ - up.z * fY;
float sY = up.z * fX - up.x * fZ;
float sZ = up.x * fY - up.y * fX;
inverseSqrt = 1.0f / (float) Math.sqrt(sX * sX + sY * sY + sZ * sZ);
sX *= inverseSqrt;
sY *= inverseSqrt;
sZ *= inverseSqrt;
// u(cross(f, s))
float uX = fY * sZ - fZ * sY;
float uY = fZ * sX - fX * sZ;
float uZ = fX * sY - fY * sX;
lookAtTransform.m00 = fX;
lookAtTransform.m01 = fY;
lookAtTransform.m02 = fZ;
lookAtTransform.m03 = 0f;
lookAtTransform.m10 = sX;
lookAtTransform.m11 = sY;
lookAtTransform.m12 = sZ;
lookAtTransform.m13 = 0f;
lookAtTransform.m20 = uX;
lookAtTransform.m21 = uY;
lookAtTransform.m22 = uZ;
lookAtTransform.m23 = 0f;
lookAtTransform.m30 = 0;
lookAtTransform.m31 = 0;
lookAtTransform.m32 = 0;
lookAtTransform.m33 = 1f;
applyTo.mul(lookAtTransform, output);
Matrices.release(lookAtTransform);
return output;
}
public static Mat4 lookAt(Vec3 center, Vec3 up, Mat4 inOut) {
return lookAt(inOut, center, up, inOut);
}
public static Vec3 rotate(Vec3 in, Vec3 axis, float angle, Vec3 out) { public static Vec3 rotate(Vec3 in, Vec3 axis, float angle, Vec3 out) {
if (out == null) { if (out == null) {
out = new Vec3(); out = new Vec3();
@ -270,6 +327,20 @@ public class VectorUtil {
return projectOnVector(inOut, vector); return projectOnVector(inOut, vector);
} }
public static float distanceSq(Vec3 a, Vec3 b) {
float x = a.x - b.x;
float y = a.y - b.y;
float z = a.z - b.z;
return x * x + y * y + z * z;
}
public static float distance(Vec3 a, Vec3 b) {
float x = a.x - b.x;
float y = a.y - b.y;
float z = a.z - b.z;
return (float) Math.sqrt(x * x + y * y + z * z);
}
public static Vec3 linearCombination( public static Vec3 linearCombination(
Vec3 va, Vec3 va,
float ka, float ka,

View File

@ -0,0 +1,40 @@
/*
* Progressia
* Copyright (C) 2020-2021 Wind Corporation and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package ru.windcorp.progressia.common.util.crash.providers;
import java.util.Map;
import ru.windcorp.progressia.Progressia;
import ru.windcorp.progressia.common.util.crash.ContextProvider;
public class VersionProvider implements ContextProvider {
@Override
public void provideContext(Map<String, String> output) {
output.put("Version", Progressia.getVersion());
output.put("Git commit", Progressia.getGitCommit());
output.put("Git branch", Progressia.getGitBranch());
output.put("Build ID", Progressia.getBuildId());
}
@Override
public String getName() {
return "Version Provider";
}
}

View File

@ -0,0 +1,25 @@
/*
* Progressia
* Copyright (C) 2020-2021 Wind Corporation and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package ru.windcorp.progressia.common.util.math;
@FunctionalInterface
public interface FloatFunction {
float apply(float x);
}

View File

@ -0,0 +1,24 @@
/*
* Progressia
* Copyright (C) 2020-2021 Wind Corporation and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package ru.windcorp.progressia.common.util.math;
public interface FloatFunction2D {
float apply(float x, float y);
}

View File

@ -0,0 +1,24 @@
/*
* Progressia
* Copyright (C) 2020-2021 Wind Corporation and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package ru.windcorp.progressia.common.util.math;
public interface FloatFunction3D {
float apply(float x, float y, float z);
}

View File

@ -0,0 +1,124 @@
/*
* Progressia
* Copyright (C) 2020-2021 Wind Corporation and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package ru.windcorp.progressia.common.util.math;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import glm.vec._2.Vec2;
public class PiecewiseLinearFunction implements FloatFunction {
public static class Builder {
private final List<Vec2> points = new ArrayList<>();
private float slopeAtNegInf = 0;
private float slopeAtPosInf = 0;
public Builder add(float x, float y) {
points.add(new Vec2(x, y));
return this;
}
public Builder setNegativeSlope(float slope) {
slopeAtNegInf = slope;
return this;
}
public Builder setPositiveSlope(float slope) {
slopeAtPosInf = slope;
return this;
}
public Builder setDefaultUndefined() {
slopeAtPosInf = Float.NaN;
slopeAtNegInf = Float.NaN;
return this;
}
public PiecewiseLinearFunction build() {
float[] pointXs = new float[points.size()];
float[] pointYs = new float[points.size()];
points.sort(Comparator.comparingDouble(v -> v.x));
for (int i = 0; i < points.size(); ++i) {
pointXs[i] = points.get(i).x;
pointYs[i] = points.get(i).y;
}
return new PiecewiseLinearFunction(pointXs, pointYs, slopeAtNegInf, slopeAtPosInf);
}
}
public static Builder builder() {
return new Builder();
}
/**
* The set of the X coordinates of all defining points, sorted in increasing order
*/
private final float[] pointXs;
/**
* The set of the Y coordinates of all defining points, sorted to match the order of {@link #pointXs}
*/
private final float[] pointYs;
/**
* Slope of the segment (-inf; x[0]), or NaN to exclude the segment from the function
*/
private final float slopeAtNegInf;
/**
* Slope of the segment (x[x.length - 1]; +inf), or NaN to exclude the segment from the function
*/
private final float slopeAtPosInf;
protected PiecewiseLinearFunction(float[] pointXs, float[] pointYs, float slopeAtNegInf, float slopeAtPosInf) {
this.pointXs = pointXs;
this.pointYs = pointYs;
this.slopeAtNegInf = slopeAtNegInf;
this.slopeAtPosInf = slopeAtPosInf;
}
@Override
public float apply(float x) {
int index = Arrays.binarySearch(pointXs, x);
if (index >= 0) {
// Wow, exact match, me surprised
return pointYs[index];
}
int bigger = -index - 1;
int smaller = bigger - 1;
if (smaller == -1) {
return pointYs[bigger] + (x - pointXs[bigger]) * slopeAtNegInf;
} else if (bigger == pointXs.length) {
return pointYs[smaller] + (x - pointXs[smaller]) * slopeAtPosInf;
} else {
float t = (x - pointXs[smaller]) / (pointXs[bigger] - pointXs[smaller]);
return pointYs[smaller] * (1 - t) + pointYs[bigger] * t;
}
}
}

View File

@ -380,4 +380,14 @@ public class EntityData extends StatefulObject implements Collideable, EntityGen
super.read(input, context); super.read(input, context);
} }
@Override
public boolean equals(Object obj) {
return this == obj;
}
@Override
public int hashCode() {
return Long.hashCode(entityId);
}
} }

View File

@ -19,8 +19,11 @@ package ru.windcorp.progressia.common.world.generic.context;
import java.util.Collection; import java.util.Collection;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Predicate;
import glm.vec._3.Vec3;
import glm.vec._3.i.Vec3i; import glm.vec._3.i.Vec3i;
import ru.windcorp.progressia.common.util.VectorUtil;
import ru.windcorp.progressia.common.world.context.Context; import ru.windcorp.progressia.common.world.context.Context;
import ru.windcorp.progressia.common.world.generic.*; import ru.windcorp.progressia.common.world.generic.*;
import ru.windcorp.progressia.common.world.rels.RelFace; import ru.windcorp.progressia.common.world.rels.RelFace;
@ -141,6 +144,42 @@ public interface WorldGenericContextRO<
*/ */
E getEntity(long entityId); E getEntity(long entityId);
/*
* Convenience methods
*/
default void forEachEntity(Consumer<E> action) {
getEntities().forEach(action);
}
default E findClosestEntity(Vec3 location, Predicate<E> filter, float maxDistance) {
if (maxDistance <= 0) {
return null;
}
E result = getEntities().stream().filter(filter).min((a, b) -> {
float aDistance = VectorUtil.distanceSq(location, a.getPosition());
float bDistance = VectorUtil.distanceSq(location, b.getPosition());
return Float.compare(aDistance, bDistance);
}).orElse(null);
if (result == null) {
return null;
}
if (Float.isInfinite(maxDistance)) {
return result;
}
if (VectorUtil.distanceSq(location, result.getPosition()) > maxDistance * maxDistance) {
return null;
}
return result;
}
default E findClosestEntity(Vec3 location, float maxDistance) {
return findClosestEntity(location, e -> true, maxDistance);
}
/* /*
* Subcontexting * Subcontexting
*/ */

View File

@ -21,8 +21,6 @@ package ru.windcorp.progressia.server;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import org.apache.logging.log4j.LogManager;
import com.google.common.eventbus.EventBus; import com.google.common.eventbus.EventBus;
import glm.vec._3.i.Vec3i; import glm.vec._3.i.Vec3i;
@ -353,7 +351,6 @@ public class Server {
* reason * reason
*/ */
public void shutdown(String message) { public void shutdown(String message) {
LogManager.getLogger().warn("Server.shutdown() is not yet implemented");
serverThread.stop(); serverThread.stop();
getWorld().getContainer().close(); getWorld().getContainer().close();
} }

View File

@ -96,7 +96,7 @@ public class ClientManager {
} }
public void processPackets() { public void processPackets() {
getClients().forEach(CommsChannel::processPackets); clients.forEach(CommsChannel::processPackets);
} }
/** /**
@ -146,7 +146,7 @@ public class ClientManager {
return; return;
if (!(c instanceof ClientPlayer)) if (!(c instanceof ClientPlayer))
return; return;
if (!((ClientPlayer) c).isChunkVisible(entityId)) if (!((ClientPlayer) c).isEntityVisible(entityId))
return; return;
c.sendPacket(packet); c.sendPacket(packet);
}); });

View File

@ -19,6 +19,9 @@
package ru.windcorp.progressia.server.comms; package ru.windcorp.progressia.server.comms;
import glm.vec._3.i.Vec3i; import glm.vec._3.i.Vec3i;
import gnu.trove.TCollections;
import gnu.trove.set.TLongSet;
import gnu.trove.set.hash.TLongHashSet;
import ru.windcorp.progressia.common.world.generic.ChunkSet; import ru.windcorp.progressia.common.world.generic.ChunkSet;
import ru.windcorp.progressia.common.world.generic.ChunkSets; import ru.windcorp.progressia.common.world.generic.ChunkSets;
import ru.windcorp.progressia.server.Player; import ru.windcorp.progressia.server.Player;
@ -53,8 +56,19 @@ public abstract class ClientPlayer extends ClientChat {
return player.getServer().getLoadManager().getVisionManager().getVisibleChunks(player); return player.getServer().getLoadManager().getVisionManager().getVisibleChunks(player);
} }
public boolean isChunkVisible(long entityId) { public boolean isEntityVisible(long entityId) {
return true; if (player == null)
return false;
return player.getServer().getLoadManager().getVisionManager().isEntityVisible(entityId, player);
}
private static final TLongSet EMPTY_TLONGSET = TCollections.unmodifiableSet(new TLongHashSet(0));
public TLongSet getVisibleEntities(Player player) {
if (player == null) {
return EMPTY_TLONGSET;
}
return player.getServer().getLoadManager().getVisionManager().getVisibleEntities(player);
} }
} }

View File

@ -17,10 +17,14 @@
*/ */
package ru.windcorp.progressia.server.management.load; package ru.windcorp.progressia.server.management.load;
import java.util.HashSet;
import java.util.Set;
import glm.vec._3.i.Vec3i; import glm.vec._3.i.Vec3i;
import ru.windcorp.progressia.common.world.DefaultChunkData; import ru.windcorp.progressia.common.world.DefaultChunkData;
import ru.windcorp.progressia.common.world.PacketRevokeChunk; import ru.windcorp.progressia.common.world.PacketRevokeChunk;
import ru.windcorp.progressia.common.world.PacketSendChunk; import ru.windcorp.progressia.common.world.PacketSendChunk;
import ru.windcorp.progressia.common.world.entity.EntityData;
import ru.windcorp.progressia.common.world.DefaultWorldData; import ru.windcorp.progressia.common.world.DefaultWorldData;
import ru.windcorp.progressia.server.Player; import ru.windcorp.progressia.server.Player;
import ru.windcorp.progressia.server.Server; import ru.windcorp.progressia.server.Server;
@ -144,6 +148,11 @@ public class ChunkManager {
return false; return false;
} }
// TODO allow entities to be saved first
Set<EntityData> entitiesToRemove = new HashSet<>();
world.forEachEntityInChunk(chunkPos, entitiesToRemove::add);
entitiesToRemove.forEach(world::removeEntity);
world.removeChunk(chunk); world.removeChunk(chunk);
getContainer().save(chunk, getServer().getWorld().getData(), getServer()); getContainer().save(chunk, getServer().getWorld().getData(), getServer());

View File

@ -25,6 +25,9 @@ import java.util.function.Consumer;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import glm.vec._3.i.Vec3i; import glm.vec._3.i.Vec3i;
import gnu.trove.TCollections;
import gnu.trove.set.TLongSet;
import gnu.trove.set.hash.TLongHashSet;
import ru.windcorp.progressia.common.world.generic.ChunkSet; import ru.windcorp.progressia.common.world.generic.ChunkSet;
import ru.windcorp.progressia.common.world.generic.ChunkSets; import ru.windcorp.progressia.common.world.generic.ChunkSets;
import ru.windcorp.progressia.server.Player; import ru.windcorp.progressia.server.Player;
@ -85,6 +88,28 @@ public class VisionManager {
return vision.getVisibleChunks(); return vision.getVisibleChunks();
} }
public boolean isEntityVisible(long entityId, Player player) {
PlayerVision vision = getVision(player, false);
if (vision == null) {
return false;
}
return vision.isEntityVisible(entityId);
}
private static final TLongSet EMPTY_TLONGSET = TCollections.unmodifiableSet(new TLongHashSet(0));
public TLongSet getVisibleEntities(Player player) {
PlayerVision vision = getVision(player, false);
if (vision == null) {
return EMPTY_TLONGSET;
}
return vision.getVisibleEntities();
}
/** /**
* @return the loadManager * @return the loadManager
*/ */

View File

@ -21,6 +21,7 @@ package ru.windcorp.progressia.server.world;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import glm.Glm; import glm.Glm;
import glm.vec._3.i.Vec3i; import glm.vec._3.i.Vec3i;
@ -93,6 +94,27 @@ public class DefaultWorldLogic implements WorldLogic {
return getData().getEntity(entityId); return getData().getEntity(entityId);
} }
@Override
public void spawnEntity(EntityData entity) {
Objects.requireNonNull(entity, "entity");
if (entity.getEntityId() != EntityData.NULL_ENTITY_ID) {
throw new IllegalArgumentException(
"Entity ID of entity " + entity
+ " is not unassigned; use WorldData.addEntity to add entity with assigned entity ID"
);
}
long entityId;
// TODO this should be synchronized on the entity set
do {
entityId = server.getAdHocRandom().nextLong();
} while (entityId == EntityData.NULL_ENTITY_ID || getEntity(entityId) != null);
entity.setEntityId(entityId);
getData().addEntity(entity);
}
public Evaluation getTickEntitiesTask() { public Evaluation getTickEntitiesTask() {
return tickEntitiesTask; return tickEntitiesTask;
} }
@ -118,31 +140,49 @@ public class DefaultWorldLogic implements WorldLogic {
DefaultChunkData chunk = getGenerator().generate(chunkPos); DefaultChunkData chunk = getGenerator().generate(chunkPos);
if (!Glm.equals(chunkPos, chunk.getPosition())) { if (!Glm.equals(chunkPos, chunk.getPosition())) {
throw CrashReports.report(null, "Generator %s has generated a chunk at (%d; %d; %d) when requested to generate a chunk at (%d; %d; %d)", throw CrashReports.report(
null,
"Generator %s has generated a chunk at (%d; %d; %d) when requested to generate a chunk at (%d; %d; %d)",
getGenerator(), getGenerator(),
chunk.getX(), chunk.getY(), chunk.getZ(), chunk.getX(),
chunkPos.x, chunkPos.y, chunkPos.z chunk.getY(),
chunk.getZ(),
chunkPos.x,
chunkPos.y,
chunkPos.z
); );
} }
if (getData().getChunk(chunk.getPosition()) != chunk) { if (getData().getChunk(chunk.getPosition()) != chunk) {
if (isChunkLoaded(chunkPos)) { if (isChunkLoaded(chunkPos)) {
throw CrashReports.report(null, "Generator %s has returned a chunk different to the chunk that is located at (%d; %d; %d)", throw CrashReports.report(
null,
"Generator %s has returned a chunk different to the chunk that is located at (%d; %d; %d)",
getGenerator(), getGenerator(),
chunkPos.x, chunkPos.y, chunkPos.z chunkPos.x,
chunkPos.y,
chunkPos.z
); );
} else { } else {
throw CrashReports.report(null, "Generator %s has returned a chunk that is not loaded when requested to generate a chunk at (%d; %d; %d)", throw CrashReports.report(
null,
"Generator %s has returned a chunk that is not loaded when requested to generate a chunk at (%d; %d; %d)",
getGenerator(), getGenerator(),
chunkPos.x, chunkPos.y, chunkPos.z chunkPos.x,
chunkPos.y,
chunkPos.z
); );
} }
} }
if (!getChunk(chunk).isReady()) { if (!getChunk(chunk).isReady()) {
throw CrashReports.report(null, "Generator %s has returned a chunk that is not ready when requested to generate a chunk at (%d; %d; %d)", throw CrashReports.report(
null,
"Generator %s has returned a chunk that is not ready when requested to generate a chunk at (%d; %d; %d)",
getGenerator(), getGenerator(),
chunkPos.x, chunkPos.y, chunkPos.z chunkPos.x,
chunkPos.y,
chunkPos.z
); );
} }

View File

@ -21,10 +21,13 @@ import java.util.Collection;
import glm.vec._3.i.Vec3i; import glm.vec._3.i.Vec3i;
import ru.windcorp.progressia.common.world.WorldData; import ru.windcorp.progressia.common.world.WorldData;
import ru.windcorp.progressia.common.world.entity.EntityData;
import ru.windcorp.progressia.common.world.rels.BlockFace; import ru.windcorp.progressia.common.world.rels.BlockFace;
public interface WorldLogic extends WorldLogicRO { public interface WorldLogic extends WorldLogicRO {
void spawnEntity(EntityData entity);
/* /*
* Override return types * Override return types
*/ */

View File

@ -24,7 +24,6 @@ import java.util.List;
import glm.vec._3.Vec3; import glm.vec._3.Vec3;
import glm.vec._3.i.Vec3i; import glm.vec._3.i.Vec3i;
import ru.windcorp.progressia.common.util.FloatRangeMap;
import ru.windcorp.progressia.common.util.VectorUtil; import ru.windcorp.progressia.common.util.VectorUtil;
import ru.windcorp.progressia.common.world.DefaultChunkData; import ru.windcorp.progressia.common.world.DefaultChunkData;
import ru.windcorp.progressia.common.world.DecodingException; import ru.windcorp.progressia.common.world.DecodingException;
@ -32,7 +31,7 @@ import ru.windcorp.progressia.server.Server;
import ru.windcorp.progressia.server.world.generation.AbstractWorldGenerator; import ru.windcorp.progressia.server.world.generation.AbstractWorldGenerator;
import ru.windcorp.progressia.server.world.generation.surface.SurfaceFeature; import ru.windcorp.progressia.server.world.generation.surface.SurfaceFeature;
import ru.windcorp.progressia.server.world.generation.surface.SurfaceFloatField; import ru.windcorp.progressia.server.world.generation.surface.SurfaceFloatField;
import ru.windcorp.progressia.server.world.generation.surface.TerrainLayer; import ru.windcorp.progressia.server.world.generation.surface.TerrainSupplier;
public class PlanetGenerator extends AbstractWorldGenerator<Boolean> { public class PlanetGenerator extends AbstractWorldGenerator<Boolean> {
@ -46,7 +45,7 @@ public class PlanetGenerator extends AbstractWorldGenerator<Boolean> {
Server server, Server server,
Planet planet, Planet planet,
SurfaceFloatField heightMap, SurfaceFloatField heightMap,
FloatRangeMap<TerrainLayer> layers, TerrainSupplier terrain,
List<SurfaceFeature> features List<SurfaceFeature> features
) { ) {
super(id, server, Boolean.class, "Test:PlanetGravityModel"); super(id, server, Boolean.class, "Test:PlanetGravityModel");
@ -56,7 +55,7 @@ public class PlanetGenerator extends AbstractWorldGenerator<Boolean> {
PlanetGravityModel model = (PlanetGravityModel) this.getGravityModel(); PlanetGravityModel model = (PlanetGravityModel) this.getGravityModel();
model.configure(planet.getGravityModelSettings()); model.configure(planet.getGravityModelSettings());
this.terrainGenerator = new PlanetTerrainGenerator(this, heightMap, layers); this.terrainGenerator = new PlanetTerrainGenerator(this, heightMap, terrain);
this.featureGenerator = new PlanetFeatureGenerator(this, features); this.featureGenerator = new PlanetFeatureGenerator(this, features);
} }

View File

@ -21,7 +21,6 @@ import java.util.Map;
import glm.vec._3.Vec3; import glm.vec._3.Vec3;
import glm.vec._3.i.Vec3i; import glm.vec._3.i.Vec3i;
import ru.windcorp.progressia.common.util.FloatRangeMap;
import ru.windcorp.progressia.common.util.VectorUtil; import ru.windcorp.progressia.common.util.VectorUtil;
import ru.windcorp.progressia.common.world.DefaultChunkData; import ru.windcorp.progressia.common.world.DefaultChunkData;
import ru.windcorp.progressia.common.world.Coordinates; import ru.windcorp.progressia.common.world.Coordinates;
@ -33,7 +32,7 @@ import ru.windcorp.progressia.server.Server;
import ru.windcorp.progressia.server.world.generation.surface.Surface; import ru.windcorp.progressia.server.world.generation.surface.Surface;
import ru.windcorp.progressia.server.world.generation.surface.SurfaceFloatField; import ru.windcorp.progressia.server.world.generation.surface.SurfaceFloatField;
import ru.windcorp.progressia.server.world.generation.surface.SurfaceTerrainGenerator; import ru.windcorp.progressia.server.world.generation.surface.SurfaceTerrainGenerator;
import ru.windcorp.progressia.server.world.generation.surface.TerrainLayer; import ru.windcorp.progressia.server.world.generation.surface.TerrainSupplier;
class PlanetTerrainGenerator { class PlanetTerrainGenerator {
@ -43,7 +42,7 @@ class PlanetTerrainGenerator {
public PlanetTerrainGenerator( public PlanetTerrainGenerator(
PlanetGenerator generator, PlanetGenerator generator,
SurfaceFloatField heightMap, SurfaceFloatField heightMap,
FloatRangeMap<TerrainLayer> layers TerrainSupplier terrain
) { ) {
this.parent = generator; this.parent = generator;
@ -53,7 +52,7 @@ class PlanetTerrainGenerator {
face -> new SurfaceTerrainGenerator( face -> new SurfaceTerrainGenerator(
new Surface(face, seaLevel), new Surface(face, seaLevel),
heightMap, heightMap,
layers terrain
) )
); );
} }
@ -86,6 +85,11 @@ class PlanetTerrainGenerator {
} }
private void generateBorderTerrain(Server server, DefaultChunkData chunk) { private void generateBorderTerrain(Server server, DefaultChunkData chunk) {
if (chunk.getPosition().x == 0 && chunk.getPosition().y == 0 && chunk.getPosition().z == 0) {
generateCore(server, chunk);
return;
}
BlockData stone = BlockDataRegistry.getInstance().get("Test:Stone"); BlockData stone = BlockDataRegistry.getInstance().get("Test:Stone");
BlockData air = BlockDataRegistry.getInstance().get("Test:Air"); BlockData air = BlockDataRegistry.getInstance().get("Test:Air");
@ -109,4 +113,16 @@ class PlanetTerrainGenerator {
}); });
} }
private void generateCore(Server server, DefaultChunkData chunk) {
BlockData tux = BlockDataRegistry.getInstance().get("Test:Tux");
BlockData air = BlockDataRegistry.getInstance().get("Test:Air");
GenericChunks.forEachBiC(bic -> {
chunk.setBlock(bic, air, false);
});
chunk.setBlock(new Vec3i(7, 7, 0), tux, false);
}
} }

View File

@ -19,7 +19,6 @@ package ru.windcorp.progressia.server.world.generation.surface;
import glm.vec._3.Vec3; import glm.vec._3.Vec3;
import glm.vec._3.i.Vec3i; import glm.vec._3.i.Vec3i;
import ru.windcorp.progressia.common.util.FloatRangeMap;
import ru.windcorp.progressia.common.util.Vectors; import ru.windcorp.progressia.common.util.Vectors;
import ru.windcorp.progressia.common.world.DefaultChunkData; import ru.windcorp.progressia.common.world.DefaultChunkData;
import ru.windcorp.progressia.common.world.block.BlockData; import ru.windcorp.progressia.common.world.block.BlockData;
@ -33,12 +32,12 @@ public class SurfaceTerrainGenerator {
private final Surface surface; private final Surface surface;
private final SurfaceFloatField heightMap; private final SurfaceFloatField heightMap;
private final FloatRangeMap<TerrainLayer> layers; private final TerrainSupplier terrain;
public SurfaceTerrainGenerator(Surface surface, SurfaceFloatField heightMap, FloatRangeMap<TerrainLayer> layers) { public SurfaceTerrainGenerator(Surface surface, SurfaceFloatField heightMap, TerrainSupplier terrain) {
this.surface = surface; this.surface = surface;
this.heightMap = heightMap; this.heightMap = heightMap;
this.layers = layers; this.terrain = terrain;
} }
public void generateTerrain(Server server, DefaultChunkData chunk) { public void generateTerrain(Server server, DefaultChunkData chunk) {
@ -74,7 +73,7 @@ public class SurfaceTerrainGenerator {
location.set(north, west, altitude); location.set(north, west, altitude);
SurfaceBlockContext blockContext = context.push(location); SurfaceBlockContext blockContext = context.push(location);
BlockData block = layers.get(depth).get(blockContext, depth); BlockData block = terrain.get(blockContext, depth);
blockContext.pop(); blockContext.pop();

View File

@ -21,7 +21,7 @@ import ru.windcorp.progressia.common.world.block.BlockData;
import ru.windcorp.progressia.server.world.generation.surface.context.SurfaceBlockContext; import ru.windcorp.progressia.server.world.generation.surface.context.SurfaceBlockContext;
@FunctionalInterface @FunctionalInterface
public interface TerrainLayer { public interface TerrainSupplier {
BlockData get(SurfaceBlockContext context, float depth); BlockData get(SurfaceBlockContext context, float depth);

View File

@ -43,10 +43,8 @@ public class Region {
private static final boolean RESET_CORRUPTED = true; private static final boolean RESET_CORRUPTED = true;
public int loadedChunks; private final AtomicBoolean isUsing = new AtomicBoolean(false);
private final AtomicBoolean isClosed = new AtomicBoolean(false);
private AtomicBoolean isUsing = new AtomicBoolean(false);
private AtomicBoolean isClosed = new AtomicBoolean(false);
private final RegionFile file; private final RegionFile file;
@ -60,6 +58,7 @@ public class Region {
} catch (IOException e) { } catch (IOException e) {
RegionWorldContainer.LOG.debug("Uh the file broke"); RegionWorldContainer.LOG.debug("Uh the file broke");
RegionWorldContainer.LOG.debug(e.getLocalizedMessage());
if (RESET_CORRUPTED) { if (RESET_CORRUPTED) {
this.file.makeHeader(regionCoords); this.file.makeHeader(regionCoords);
} }
@ -132,7 +131,7 @@ public class Region {
DecodingException { DecodingException {
isUsing.set(true); isUsing.set(true);
int dataOffset = 0; int dataOffset;
Vec3i pos = RegionWorldContainer.getInRegionCoords(chunkPos); Vec3i pos = RegionWorldContainer.getInRegionCoords(chunkPos);
if (hasOffset(pos)) { if (hasOffset(pos)) {

Some files were not shown because too many files have changed in this diff Show More