Compare commits

...

32 Commits

Author SHA1 Message Date
a17bb2a732
Don't know why, but all monsters do is constantly walk to left 2024-07-13 18:05:17 +07:00
6f2b8b7bbb
i regret going for third-party Lua-in-JVM implementation due to how bugged it is 2024-07-07 19:32:52 +07:00
307c67d976
Relaxed path finder "reached" check 2024-06-28 23:05:38 +07:00
d9b25c960d
Fix returnBest not being obeyed 2024-06-28 23:00:56 +07:00
34dcc68e15
Optimize StatusController.updateStats 2024-06-28 22:55:10 +07:00
f95bc9762f
Minimally working Monster entities
PathController, PathFinder

Actor movement controller Lua bindings

Game loading no longer block Universe thread, more efficient registry population synchronization

Environmental status effects now can be stat modifiers
2024-06-28 22:44:13 +07:00
5c6efaf03d
Implement quirk of Lua reference impl 'next', where removed key do not immediately become invisible to 'next' 2024-06-27 21:17:13 +07:00
77a9beb665
Per player system message, display notification in chat when player is kicked off world 2024-06-02 21:52:17 +07:00
56c154cc96
provideConfigBindings 2024-05-25 21:16:46 +07:00
242f372819
Don't do anything in invokeGlobal if init has not been called 2024-05-25 19:33:11 +07:00
1d77cf8f98
String.limit 2024-05-24 12:27:19 +07:00
56f4fe46a6
Kick player who spams entity messages instead of rejecting them 2024-05-22 19:05:12 +07:00
e2d27e34a5
Decouple add from nextFreeIndex in IdMap 2024-05-22 19:04:37 +07:00
5769cff60b
Wait for clients to disconnect (within reasonable timeframe) before initiating server shutdown 2024-05-22 19:04:05 +07:00
8ef6bead2c
Add missing defaults to StatusControllerConfig 2024-05-22 19:03:42 +07:00
6729f2decc
BasicNetworkedElement.bumpVersion call from outside now properly notifies listeners 2024-05-22 19:03:11 +07:00
06a202bf17
applyParameters variant for ActorMovementController 2024-05-22 19:02:47 +07:00
8eb1db919f
Intern strings originating from Lua 2024-05-22 19:02:19 +07:00
e81079479b
Add PersistentStatusEffect as typealias 2024-05-22 19:02:04 +07:00
d1865900f6
JsonPatch apply now accepts json reader promise 2024-05-22 19:01:49 +07:00
69e0a8737e
Update Color replacement to be actually useful 2024-05-22 19:01:27 +07:00
b41d45b3e9
Synchronize item registry loading using universe thread 2024-05-22 19:01:05 +07:00
a2cc4ba6c3
Cleaner server shutdown 2024-05-22 19:00:42 +07:00
0b3aac6189
Game loading now no longer blocks universe thread and it can perform other work 2024-05-22 18:59:57 +07:00
2f782d7825
NetworkedMap.markIndexDirty 2024-05-22 18:59:13 +07:00
b4902559ac
Fix crash related to starbound data structure being dumb 2024-05-22 18:59:02 +07:00
1b7076f04f
Use Lua's random in util bindings 2024-05-22 18:58:30 +07:00
96fdcccdd0
More random generator compat with original engine 2024-05-22 18:58:11 +07:00
032f32626e
Make RGBAColorTypeAdapter accept hex strings as input 2024-05-22 18:57:42 +07:00
16ccb84d3b
Don't let through long nicknames 2024-05-16 22:25:26 +07:00
19324c6247
Add cli command arguments 2024-05-16 19:32:08 +07:00
67ac2b272e
Busy.png makes a nice loading icon 2024-05-12 21:10:37 +07:00
151 changed files with 7491 additions and 943 deletions

View File

@ -4,7 +4,7 @@
This document briefly documents what have been added (or removed) regarding modding capabilities or engine behavior(s)
This document is non-exhaustive, engine contains way more behavior change bits than documented here,
but listing all of them will be a hassle, and will pollute actually useful changes.
but listing all of them will be a hassle, and will pollute this document.
---------------
@ -12,6 +12,8 @@ but listing all of them will be a hassle, and will pollute actually useful chang
* `treasurechests` now can specify `treasurePool` as array
* `damageTable` can be defined directly, without referencing other JSON file (experimental feature)
* `environmentStatusEffects` of visitable world parameters now accept `StatModifier`s and not just unique status effects as `String`s
* Keep in mind, putting stat modifiers there will blow up original clients due to Json deserializer expecting only strings (despite `StatusController` treating `environmentStatusEffects` as field containing either `StatModifier` or `String`)
## Biomes
* Tree biome placeables now have `variantsRange` (defaults to `[1, 1]`) and `subVariantsRange` (defaults to `[2, 2]`)
@ -32,7 +34,6 @@ val color: TileColor = TileColor.DEFAULT
```
* `item` brush now can accept proper item descriptors (in json object tag),
* Previous behavior remains unchanged (if specified as string, creates _randomized_ item, if as object, creates _exactly_ what have been specified)
* To stop randomizing as Tiled tileset brush, specify `"dont_randomize"` as anything (e.g. as `""`)
* `liquid` brush now can accept 'level' as second argument
* Previous behavior is unchanged, `["liquid", "water", true]` will result into infinite water as before, but `["liquid", "water", 0.5, false]` will spawn half-filled water
* In tiled, you already can do this using `"quantity"` property
@ -43,7 +44,7 @@ val color: TileColor = TileColor.DEFAULT
## .terrain
Please keep in mind that if you use new format or new terrain selectors original clients will
probably explode upon joining worlds where new terrain selectors are utilized.
explode upon joining worlds where new terrain selectors are utilized.
* All composing terrain selectors (such as `min`, `displacement`, `rotate`, etc) now can reference other terrain selectors by name (the `.terrain` files) instead of embedding entire config inside them
* They can be referenced by either specifying corresponding field as string, or as object like so: `{"name": "namedselector"}`
@ -96,13 +97,30 @@ probably explode upon joining worlds where new terrain selectors are utilized.
## .matmod
* `modId` is no longer essential and can be skipped, or specified as any number in 1 -> 2^31 range, with notes of `materialId` and `liquidId` apply.
## Monster Definitions
### New parameters merge rules
In addition to `add`, `multiply`, `merge` and `override` new merge methods are accepted:
* `sub` (a - b)
* `divide` (a / b)
* `inverse` (b / a)
* `min`
* `max`
* `pow` (a in power of b)
* `invpow` (b in power of a)
---------------
# Scripting
* In DamageSource, `sourceEntityId` combination with `rayCheck` has been fixed, and check for tile collision between victim and inflictor (this entity), not between victim and attacker (`sourceEntityId`)
* Contexts, where previously only `entity` bindings were available, now have entity-specific bindings exposed
* Example: Status controller scripts now get `monster` bindings when running in context of Monster's status controller, etc
* `behavior.behavior` third argument (specified commonly as `_ENV`) is ignored and can be omitted (set to nil)
* It was used solely to get Lua engine (Lua execution context), and could have been deprecated long time ago even in original engine, because there is now a way in original engine to get Lua engine when binding is called
### Random
## Random
* Added `random:randn(deviation: double, mean: double): double`, returns normally distributed double, where `deviation` stands for [Standard deviation](https://en.wikipedia.org/wiki/Standard_deviation), and `mean` specifies middle point
* Removed `random:addEntropy`
@ -121,6 +139,30 @@ probably explode upon joining worlds where new terrain selectors are utilized.
* Added `animator.hasEffect(effect: string): boolean`
* Added `animator.parts(): List<string>`
## mcontroller
* Added `mcontroller.collisionPolies(): List<Poly>`, since engine technically supports multiple convex bodies attached to one movement controller
* Added `mcontroller.collisionBodies(): List<Poly>`, since engine technically supports multiple convex bodies attached to one movement controller
* Added `mcontroller.liquidName(): String?`, returns nil if not touching any liquid
* This addition marks `mcontroller.liquidId(): Int` deprecated
## monster
* Added `monster.seedNumber(): Long`, use this instead of `monster.seed(): String`
* `monster.level(): Double?` returns nil if no monster level was specified
* `monster.setDamageParts(parts: Table?)` now accepts nil as equivalent of empty table (consistency fix)
* `monster.setPhysicsForces(forces: Table?)` now accepts nil as equivalent of empty table (consistency fix)
* `mosnter.setName(name: String?)` now accepts nil to reset custom name
## status
* Implemented `status.appliesEnvironmentStatusEffects(): Boolean`, which exists in original engine's code but was never hooked up to Lua bindings
* Implemented `status.appliesWeatherStatusEffects(): Boolean`, which exists in original engine's code but was never hooked up to Lua bindings
* Implemented `status.setAppliesEnvironmentStatusEffects(should: Boolean)`, which exists in original engine's code but was never hooked up to Lua bindings
* Implemented `status.setAppliesWeatherStatusEffects(should: Boolean)`, which exists in original engine's code but was never hooked up to Lua bindings
* Added `status.minimumLiquidStatusEffectPercentage(): Double`
* Added `status.setMinimumLiquidStatusEffectPercentage(value: Double)`
## world
#### Additions
@ -244,7 +286,10 @@ _slightly_ different results from execution to execution,
such as one microdungeon taking precedence over another microdungeon
if they happen to generate in proximity on chunk border (one dungeon generated in chunk A, second generated in chunk B,
and they happened to overlap each other),
and which one gets placed is determined by who finishes generating first.
and which one gets placed is determined by who finishes generating first; as well as case
of approaching same chunk in world from different sides (exploring world left to right can yield
different generation result when exploring from right to left, and this is not something that can be fixed,
unless world is pre-generated in its entirety).
---------------

View File

@ -23,6 +23,7 @@ val log4jVersion: String by project
val guavaVersion: String by project
val junitVersion: String by project
val sqliteVersion: String by project
val picocliVersion: String by project
repositories {
maven {
@ -91,6 +92,8 @@ dependencies {
implementation("com.github.ben-manes.caffeine:caffeine:$caffeineVersion")
implementation(project(":luna"))
implementation("info.picocli:picocli:$picocliVersion")
implementation("org.xerial:sqlite-jdbc:$sqliteVersion")
implementation("io.netty:netty-transport:$nettyVersion")

View File

@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
kotlinVersion=1.9.10
kotlinCoroutinesVersion=1.8.0
kommonsVersion=2.17.0
kommonsVersion=3.0.1
ffiVersion=2.2.13
lwjglVersion=3.3.0
@ -17,3 +17,5 @@ log4jVersion=2.17.1
guavaVersion=33.0.0-jre
junitVersion=5.8.2
sqliteVersion=3.45.3.0
picocliVersion=4.7.6

View File

@ -1344,7 +1344,7 @@ public final class StringLib {
}
} while (idx >= 0);
context.getReturnBuffer().setTo(bld.toString());
context.getReturnBuffer().setTo(ByteString.of(bld.toString()));
}
private static class SuspendedState {

View File

@ -46,6 +46,9 @@ public class TraversableHashMap<K, V> implements Map<K, V> {
private final Set<Map.Entry<K, V>> entrySet;
private K firstKey;
private K lastKey;
// replicate 'next' behavior of reference implementation, where removing key from table does not invalidate it immediately
private K lingeringKey;
private K lingeringSuccessor;
/**
* Constructs a new empty map.
@ -133,10 +136,12 @@ public class TraversableHashMap<K, V> implements Map<K, V> {
Entry<K, V> e = entries.remove(key);
if (e != null) {
K prevKey = e.getPreviousKey();
K nextKey = e.getNextKey();
lingeringKey = (K) key;
lingeringSuccessor = nextKey;
if (prevKey != null) {
entries.get(prevKey).setNextKey(nextKey);
} else {
@ -202,6 +207,9 @@ public class TraversableHashMap<K, V> implements Map<K, V> {
Objects.requireNonNull(key);
Entry<K, V> e = entries.get(key);
if (e == null) {
if (key.equals(lingeringKey))
return lingeringSuccessor;
throw new NoSuchElementException(key.toString());
}
return e.getNextKey();

View File

@ -51,3 +51,5 @@ inline fun <reified T> Gson.fromJson(reader: JsonReader): T? = fromJson<T>(reade
inline fun <reified T> Gson.fromJson(reader: JsonElement): T? = getAdapter(T::class.java).read(FastJsonTreeReader(reader))
fun <T> Gson.fromJsonFast(reader: JsonElement, type: Class<T>): T = getAdapter(type).read(FastJsonTreeReader(reader))
fun <T> Gson.fromJsonFast(reader: JsonElement, type: TypeToken<T>): T = getAdapter(type).read(FastJsonTreeReader(reader))
inline fun <reified T> Gson.fromJsonFast(reader: JsonElement): T = getAdapter(object : TypeToken<T>() {}).read(FastJsonTreeReader(reader))

View File

@ -13,13 +13,16 @@ import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.defs.ClientConfig
import ru.dbotthepony.kstarbound.defs.CurrencyDefinition
import ru.dbotthepony.kstarbound.defs.ElementalDamageType
import ru.dbotthepony.kstarbound.defs.MovementParameters
import ru.dbotthepony.kstarbound.defs.world.SpawnerConfig
import ru.dbotthepony.kstarbound.defs.UniverseServerConfig
import ru.dbotthepony.kstarbound.defs.WorldServerConfig
import ru.dbotthepony.kstarbound.defs.world.WorldServerConfig
import ru.dbotthepony.kstarbound.defs.actor.player.PlayerConfig
import ru.dbotthepony.kstarbound.defs.actor.player.ShipUpgrades
import ru.dbotthepony.kstarbound.defs.item.ItemDropConfig
import ru.dbotthepony.kstarbound.defs.item.ItemGlobalConfig
import ru.dbotthepony.kstarbound.defs.quest.QuestGlobalConfig
import ru.dbotthepony.kstarbound.defs.tile.TileDamageParameters
import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldsConfig
import ru.dbotthepony.kstarbound.defs.world.AsteroidWorldsConfig
@ -33,7 +36,6 @@ import ru.dbotthepony.kstarbound.defs.world.SystemWorldConfig
import ru.dbotthepony.kstarbound.defs.world.SystemWorldObjectConfig
import ru.dbotthepony.kstarbound.defs.world.WorldTemplateConfig
import ru.dbotthepony.kstarbound.json.listAdapter
import ru.dbotthepony.kstarbound.json.mapAdapter
import ru.dbotthepony.kstarbound.util.AssetPathStack
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future
@ -118,6 +120,15 @@ object Globals {
var shipUpgrades by Delegates.notNull<ShipUpgrades>()
private set
var quests by Delegates.notNull<QuestGlobalConfig>()
private set
var spawner by Delegates.notNull<SpawnerConfig>()
private set
var elementalTypes by Delegates.notNull<ImmutableMap<String, ElementalDamageType>>()
private set
private var profanityFilterInternal by Delegates.notNull<ImmutableList<String>>()
val profanityFilter: ImmutableSet<String> by lazy {
@ -224,11 +235,14 @@ object Globals {
tasks.add(load("/plants/bushDamage.config", ::bushDamage))
tasks.add(load("/tiles/defaultDamage.config", ::tileDamage))
tasks.add(load("/ships/shipupgrades.config", ::shipUpgrades))
tasks.add(load("/quests/quests.config", ::quests))
tasks.add(load("/spawning.config", ::spawner))
tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/dungeon_worlds.config", ::dungeonWorlds, mapAdapter("/dungeon_worlds.config")) }.asCompletableFuture())
tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/currencies.config", ::currencies, mapAdapter("/currencies.config")) }.asCompletableFuture())
tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/system_objects.config", ::systemObjects, mapAdapter("/system_objects.config")) }.asCompletableFuture())
tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/instance_worlds.config", ::instanceWorlds, mapAdapter("/instance_worlds.config")) }.asCompletableFuture())
tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/damage/elementaltypes.config", ::elementalTypes, mapAdapter("/damage/elementaltypes.config")) }.asCompletableFuture())
tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/names/profanityfilter.config", ::profanityFilterInternal, lazy { Starbound.gson.listAdapter() }) }.asCompletableFuture())

View File

@ -1,31 +1,79 @@
package ru.dbotthepony.kstarbound
import org.apache.logging.log4j.LogManager
import org.classdump.luna.lib.CoroutineLib
import org.classdump.luna.runtime.Coroutine
import org.lwjgl.Version
import picocli.CommandLine
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import picocli.CommandLine.Parameters
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.server.IntegratedStarboundServer
import java.io.File
import java.net.InetSocketAddress
import java.util.concurrent.Callable
import kotlin.system.exitProcess
private val LOGGER = LogManager.getLogger()
fun main() {
Starbound.addArchive(File("J:\\Steam\\steamapps\\common\\Starbound\\assets\\packed.pak"))
@Command(
name = "client",
mixinStandardHelpOptions = true,
description = ["Launches game client"]
)
class StartClientCommand : Callable<Int> {
@Option(names = ["-d", "--data"], description = ["Game folder where Starbound is installed.", "Defaults to working directory."])
var dataFolder: String = "./"
/*for (f in File("J:\\Steam\\steamapps\\workshop\\content\\211820").listFiles()!!) {
for (f2 in f.listFiles()!!) {
if (f2.isFile) {
Starbound.addArchive(f2)
}
@Option(names = ["-s", "--storage"], description = ["Location where to store game files.", "Defaults to game's 'storage' subfolder"])
var storageFolder: String? = null
override fun call(): Int {
val data = File(dataFolder)
if (!data.exists()) {
LOGGER.fatal("No such file ${data.absolutePath}")
return 1
}
}*/
LOGGER.info("Running LWJGL ${Version.getVersion()}")
val file = File("${dataFolder}/assets/packed.pak")
val client = StarboundClient()
if (!file.exists()) {
LOGGER.fatal("No such file ${file.absolutePath}")
return 1
}
Starbound.initializeGame().thenApply {
val server = IntegratedStarboundServer(client, File("./storage"))
server.channels.createChannel(InetSocketAddress(21060))
}.exceptionally { LOGGER.error("what", it); null }
Starbound.addArchive(file)
//Starbound.addPath(File("K:\\git\\kstarbound\\unpacked_assets"))
/*for (f in File("J:\\Steam\\steamapps\\workshop\\content\\211820").listFiles()!!) {
for (f2 in f.listFiles()!!) {
if (f2.isFile) {
Starbound.addArchive(f2)
}
}
}*/
LOGGER.info("Running LWJGL ${Version.getVersion()}")
val client = StarboundClient()
Starbound.initializeGame().thenApply {
val server = IntegratedStarboundServer(client, if (storageFolder == null) File(data, "storage") else File(storageFolder!!))
server.channels.createChannel(InetSocketAddress(21060))
}.exceptionally { LOGGER.error("what", it); null }
return 0
}
}
fun main(vararg args: String) {
val result = CommandLine(StartClientCommand()).execute(*args)
if (result != 0) {
exitProcess(result)
}
}

View File

@ -3,10 +3,12 @@ package ru.dbotthepony.kstarbound
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import kotlinx.coroutines.async
import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.future.await
@ -14,6 +16,7 @@ import kotlinx.coroutines.launch
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.DamageKind
import ru.dbotthepony.kstarbound.defs.Json2Function
import ru.dbotthepony.kstarbound.defs.JsonConfigFunction
import ru.dbotthepony.kstarbound.defs.JsonFunction
@ -32,6 +35,10 @@ import ru.dbotthepony.kstarbound.defs.animation.ParticleConfig
import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition
import ru.dbotthepony.kstarbound.defs.item.TreasureChestDefinition
import ru.dbotthepony.kstarbound.defs.ProjectileDefinition
import ru.dbotthepony.kstarbound.defs.actor.behavior.BehaviorDefinition
import ru.dbotthepony.kstarbound.defs.actor.behavior.BehaviorNodeDefinition
import ru.dbotthepony.kstarbound.defs.monster.MonsterPaletteSwap
import ru.dbotthepony.kstarbound.defs.monster.MonsterPartDefinition
import ru.dbotthepony.kstarbound.defs.quest.QuestTemplate
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.tile.RenderParameters
@ -42,17 +49,21 @@ import ru.dbotthepony.kstarbound.defs.world.BushVariant
import ru.dbotthepony.kstarbound.defs.world.GrassVariant
import ru.dbotthepony.kstarbound.defs.world.TreeVariant
import ru.dbotthepony.kstarbound.defs.world.BiomeDefinition
import ru.dbotthepony.kstarbound.defs.world.SpawnType
import ru.dbotthepony.kstarbound.item.ItemRegistry
import ru.dbotthepony.kstarbound.json.JsonPatch
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.fromJsonTreeFast
import ru.dbotthepony.kstarbound.world.terrain.TerrainSelectorType
import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.physics.CollisionType
import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future
import java.util.random.RandomGenerator
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
object Registries {
private val LOGGER = LogManager.getLogger()
@ -83,6 +94,10 @@ object Registries {
val treasureChests = Registry<TreasureChestDefinition>("treasure chest").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val monsterSkills = Registry<MonsterSkillDefinition>("monster skill").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val monsterTypes = Registry<MonsterTypeDefinition>("monster type").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val spawnTypes = Registry<SpawnType>("spawn type").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val monsterPalettes = Registry<MonsterPaletteSwap>("monster palette").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val behavior = Registry<BehaviorDefinition>("behavior").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val behaviorNodes = Registry<BehaviorNodeDefinition>("behavior node").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val worldObjects = Registry<ObjectDefinition>("world object").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val biomes = Registry<BiomeDefinition>("biome").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val terrainSelectors = Registry<TerrainSelectorType.Factory<*, *>>("terrain selector").also(registriesInternal::add).also { adapters.add(it.adapter()) }
@ -92,6 +107,55 @@ object Registries {
val bushVariants = Registry<BushVariant.Data>("bush variant").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val dungeons = Registry<DungeonDefinition>("dungeon").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val markovGenerators = Registry<MarkovTextGenerator>("markov text generator").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val damageKinds = Registry<DamageKind>("damage kind").also(registriesInternal::add).also { adapters.add(it.adapter()) }
private val monsterParts = HashMap<Pair<String, String>, HashMap<String, Pair<MonsterPartDefinition, IStarboundFile>>>()
private val loggedMisses = Collections.synchronizedSet(ObjectOpenHashSet<Pair<String, String>>())
fun selectMonsterPart(category: String, type: String, random: RandomGenerator): MonsterPartDefinition? {
val key = category to type
val get = monsterParts[key]
if (get.isNullOrEmpty()) {
if (loggedMisses.add(key)) {
LOGGER.error("No such monster part combination of category '$category' and type '$type'")
}
return null
}
return get.values.random(random, true).first
}
fun getMonsterPart(category: String, type: String, name: String): MonsterPartDefinition? {
return monsterParts[category to type]?.get(name)?.first
}
private fun loadMonsterParts(files: Collection<IStarboundFile>, patches: Map<String, Collection<IStarboundFile>>): List<Future<*>> {
val adapter by lazy { Starbound.gson.getAdapter(MonsterPartDefinition::class.java) }
return files.map { listedFile ->
Starbound.GLOBAL_SCOPE.launch {
try {
val elem = JsonPatch.applyAsync(listedFile.asyncJsonReader(), patches[listedFile.computeFullPath()])
val next = AssetPathStack(listedFile.computeFullPath()) { adapter.fromJsonTreeFast(elem) }
Starbound.submit {
val map = monsterParts.computeIfAbsent(next.category to next.type) { HashMap() }
val old = map[next.name]
if (old != null) {
LOGGER.error("Duplicate monster part '${next.name}' of category '${next.category}' and type '${next.type}', originating from $listedFile (old originate from ${old.second})")
} else {
map[next.name] = next to listedFile
}
}
} catch (err: Throwable) {
LOGGER.error("Loading monster part definition file $listedFile", err)
}
}.asCompletableFuture()
}
}
private fun <T> key(mapper: (T) -> String): (T) -> Pair<String, KOptional<Int?>> {
return { mapper.invoke(it) to KOptional() }
@ -122,7 +186,7 @@ object Registries {
return files.map { listedFile ->
Starbound.GLOBAL_SCOPE.launch {
try {
val elem = JsonPatch.applyAsync(Starbound.ELEMENTS_ADAPTER.read(listedFile.asyncJsonReader().await()), patches[listedFile.computeFullPath()])
val elem = JsonPatch.applyAsync(listedFile.asyncJsonReader(), patches[listedFile.computeFullPath()])
AssetPathStack(listedFile.computeFullPath()) {
val read = adapter.fromJsonTreeFast(elem)
@ -130,7 +194,7 @@ object Registries {
after(read, listedFile)
registry.add {
Starbound.submit {
registry.add(
key = keys.first,
value = read,
@ -138,7 +202,7 @@ object Registries {
json = elem,
file = listedFile
)
}
}.exceptionally { err -> LOGGER.error("Loading ${registry.name} definition file $listedFile", err); null }
}
} catch (err: Throwable) {
LOGGER.error("Loading ${registry.name} definition file $listedFile", err)
@ -147,10 +211,6 @@ object Registries {
}
}
fun finishLoad() {
registriesInternal.forEach { it.finishLoad() }
}
fun load(fileTree: Map<String, Collection<IStarboundFile>>, patchTree: Map<String, List<IStarboundFile>>): List<Future<*>> {
val tasks = ArrayList<Future<*>>()
@ -165,14 +225,18 @@ object Registries {
tasks.add(loadMetaMaterials())
tasks.addAll(loadRegistry(dungeons, patchTree, fileTree["dungeon"] ?: listOf(), key(DungeonDefinition::name)))
tasks.addAll(loadMonsterParts(fileTree["monsterpart"] ?: listOf(), patchTree))
tasks.addAll(loadRegistry(worldObjects, patchTree, fileTree["object"] ?: listOf(), key(ObjectDefinition::objectName)))
tasks.addAll(loadRegistry(monsterTypes, patchTree, fileTree["monstertype"] ?: listOf(), key(MonsterTypeDefinition::type)))
tasks.addAll(loadRegistry(monsterPalettes, patchTree, fileTree["monstercolors"] ?: listOf(), key(MonsterPaletteSwap::name)))
tasks.addAll(loadRegistry(monsterSkills, patchTree, fileTree["monsterskill"] ?: listOf(), key(MonsterSkillDefinition::name)))
tasks.addAll(loadRegistry(statusEffects, patchTree, fileTree["statuseffect"] ?: listOf(), key(StatusEffectDefinition::name)))
tasks.addAll(loadRegistry(species, patchTree, fileTree["species"] ?: listOf(), key(Species::kind)))
tasks.addAll(loadRegistry(particles, patchTree, fileTree["particle"] ?: listOf(), { (it.kind ?: throw NullPointerException("Missing 'kind' value")) to KOptional() }))
tasks.addAll(loadRegistry(questTemplates, patchTree, fileTree["questtemplate"] ?: listOf(), key(QuestTemplate::id)))
tasks.addAll(loadRegistry(techs, patchTree, fileTree["tech"] ?: listOf(), key(TechDefinition::name)))
tasks.addAll(loadRegistry(npcTypes, patchTree, fileTree["npctype"] ?: listOf(), key(NpcTypeDefinition::type)))
tasks.addAll(loadRegistry(monsterSkills, patchTree, fileTree["monsterskill"] ?: listOf(), key(MonsterSkillDefinition::name)))
tasks.addAll(loadRegistry(biomes, patchTree, fileTree["biome"] ?: listOf(), key(BiomeDefinition::name)))
tasks.addAll(loadRegistry(grassVariants, patchTree, fileTree["grass"] ?: listOf(), key(GrassVariant.Data::name)))
tasks.addAll(loadRegistry(treeStemVariants, patchTree, fileTree["modularstem"] ?: listOf(), key(TreeVariant.StemData::name)))
@ -180,6 +244,10 @@ object Registries {
tasks.addAll(loadRegistry(bushVariants, patchTree, fileTree["bush"] ?: listOf(), key(BushVariant.Data::name)))
tasks.addAll(loadRegistry(markovGenerators, patchTree, fileTree["namesource"] ?: listOf(), key(MarkovTextGenerator::name)))
tasks.addAll(loadRegistry(projectiles, patchTree, fileTree["projectile"] ?: listOf(), key(ProjectileDefinition::projectileName)))
tasks.addAll(loadRegistry(behavior, patchTree, fileTree["behavior"] ?: listOf(), key(BehaviorDefinition::name)))
tasks.addAll(loadRegistry(damageKinds, patchTree, fileTree["damage"] ?: listOf(), key(DamageKind::kind)))
tasks.addAll(loadCombined(behaviorNodes, fileTree["nodes"] ?: listOf(), patchTree))
tasks.addAll(loadCombined(jsonFunctions, fileTree["functions"] ?: listOf(), patchTree))
tasks.addAll(loadCombined(json2Functions, fileTree["2functions"] ?: listOf(), patchTree))
@ -187,6 +255,10 @@ object Registries {
tasks.addAll(loadCombined(treasureChests, fileTree["treasurechests"] ?: listOf(), patchTree) { name = it })
tasks.addAll(loadCombined(treasurePools, fileTree["treasurepools"] ?: listOf(), patchTree) { name = it })
// because someone couldn't handle their mushroom vine that day, and decided to make third way of
// declaring game data
tasks.addAll(loadMixed(spawnTypes, fileTree["spawntypes"] ?: listOf(), patchTree, SpawnType::name))
return tasks
}
@ -196,16 +268,16 @@ object Registries {
return files.map { listedFile ->
Starbound.GLOBAL_SCOPE.launch {
try {
val json = JsonPatch.applyAsync(Starbound.ELEMENTS_ADAPTER.read(listedFile.asyncJsonReader().await()), patches[listedFile.computeFullPath()]) as JsonObject
val json = JsonPatch.applyAsync(listedFile.asyncJsonReader(), patches[listedFile.computeFullPath()]) as JsonObject
for ((k, v) in json.entrySet()) {
try {
val value = adapter.fromJsonTreeFast(v)
transform(value, k)
registry.add {
Starbound.submit {
registry.add(k, value, v, listedFile)
}
}.exceptionally { err -> LOGGER.error("Loading ${registry.name} definition $k from file $listedFile", err); null }
} catch (err: Exception) {
LOGGER.error("Loading ${registry.name} definition $k from file $listedFile", err)
}
@ -217,13 +289,40 @@ object Registries {
}
}
private inline fun <reified T : Any> loadMixed(registry: Registry<T>, files: Collection<IStarboundFile>, patches: Map<String, Collection<IStarboundFile>>, noinline key: T.() -> String): List<Future<*>> {
val adapter by lazy { Starbound.gson.getAdapter(T::class.java) }
return files.map { listedFile ->
Starbound.GLOBAL_SCOPE.launch {
try {
val json = JsonPatch.applyAsync(listedFile.asyncJsonReader(), patches[listedFile.computeFullPath()]) as JsonArray
for ((i, v) in json.withIndex()) {
try {
val value = adapter.fromJsonTreeFast(v)
val getKey = key(value)
Starbound.submit {
registry.add(getKey, value, v, listedFile)
}.exceptionally { err -> LOGGER.error("Loading ${registry.name} definition at name '$getKey' from file $listedFile", err); null }
} catch (err: Exception) {
LOGGER.error("Loading ${registry.name} definition at index $i from file $listedFile", err)
}
}
} catch (err: Exception) {
LOGGER.error("Loading ${registry.name} definition $listedFile", err)
}
}.asCompletableFuture()
}
}
private suspend fun loadTerrainSelector(listedFile: IStarboundFile, type: TerrainSelectorType?, patches: Map<String, Collection<IStarboundFile>>) {
try {
val json = JsonPatch.applyAsync(Starbound.ELEMENTS_ADAPTER.read(listedFile.asyncJsonReader().await()), patches[listedFile.computeFullPath()]) as JsonObject
val json = JsonPatch.applyAsync(listedFile.asyncJsonReader(), patches[listedFile.computeFullPath()]) as JsonObject
val name = json["name"]?.asString ?: throw JsonSyntaxException("Missing 'name' field")
val factory = TerrainSelectorType.factory(json, false, type)
terrainSelectors.add {
Starbound.submit {
terrainSelectors.add(name, factory)
}
} catch (err: Exception) {
@ -268,7 +367,7 @@ object Registries {
val read2 = Starbound.gson.getAdapter(object : TypeToken<ImmutableList<MetaMaterialDef>>() {}).fromJsonTreeFast(read)
for (def in read2) {
tiles.add {
Starbound.submit {
tiles.add(key = "metamaterial:${def.name}", id = KOptional(def.materialId), value = TileDefinition(
isMeta = true,
materialId = def.materialId,

View File

@ -7,48 +7,34 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectFunction
import it.unimi.dsi.fastutil.ints.Int2ObjectMap
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.longs.LongOpenHashSet
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.XXHash64
import ru.dbotthepony.kstarbound.util.limit
import java.util.Collections
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.locks.ReentrantLock
import java.util.concurrent.locks.ReentrantReadWriteLock
import java.util.function.Supplier
import kotlin.collections.set
import kotlin.concurrent.read
import kotlin.concurrent.withLock
import kotlin.concurrent.write
class Registry<T : Any>(val name: String) {
class Registry<T : Any>(val name: String, val storeJson: Boolean = true) {
private val keysInternal = HashMap<String, Impl>()
private val idsInternal = Int2ObjectOpenHashMap<Impl>()
private val keyRefs = HashMap<String, RefImpl>()
private val idRefs = Int2ObjectOpenHashMap<RefImpl>()
private val backlog = ConcurrentLinkedQueue<Runnable>()
private val lock = ReentrantReadWriteLock()
private var hasBeenValidated = false
private val loggedMisses = ObjectOpenHashSet<String>()
// it is much cheaper to queue registry additions rather than locking during high congestion
fun add(task: Runnable) {
backlog.add(task)
}
fun finishLoad() {
lock.write {
var next = backlog.poll()
while (next != null) {
next.run()
next = backlog.poll()
}
}
}
// idiot-proof miss lookup. Surely, it will cause some entries to be never logged
// if they are missing, but at least if malicious actor spams with long-ass invalid data
// it won't explode memory usage of server
private val loggedMisses = LongOpenHashSet()
val keys: Map<String, Entry<T>> = Collections.unmodifiableMap(keysInternal)
val ids: Int2ObjectMap<out Entry<T>> = Int2ObjectMaps.unmodifiable(idsInternal)
@ -102,8 +88,21 @@ class Registry<T : Any>(val name: String) {
return this === other
}
private val hash: Int
init {
var x = key.hashCode()
// avalanche bits using murmur3 hash
x = x xor (x ushr 16)
x *= -0x7a143595
x = x xor (x ushr 13)
x *= -0x3d4d51cb
x = x xor (x ushr 16)
hash = x
}
override fun hashCode(): Int {
return key.hashCode()
return hash
}
override fun toString(): String {
@ -122,8 +121,21 @@ class Registry<T : Any>(val name: String) {
return this === other || other is Registry<*>.RefImpl && other.key == key && other.registry == registry
}
private val hash: Int
init {
var x = key.hashCode()
// avalanche bits using murmur3 hash
x = x xor (x ushr 16)
x *= -0x7a143595
x = x xor (x ushr 13)
x *= -0x3d4d51cb
x = x xor (x ushr 16)
hash = x
}
override fun hashCode(): Int {
return key.hashCode()
return hash
}
override fun toString(): String {
@ -138,9 +150,13 @@ class Registry<T : Any>(val name: String) {
val result = lock.read { keysInternal[index] }
if (result == null && hasBeenValidated) {
val hasher = XXHash64()
hasher.update(index.toByteArray())
val missIndex = hasher.digestAsLong()
lock.write {
if (loggedMisses.add(index)) {
LOGGER.warn("No such $name: $index")
if (loggedMisses.add(missIndex)) {
LOGGER.warn("No such $name: ${index.limit()}")
}
}
}
@ -152,8 +168,15 @@ class Registry<T : Any>(val name: String) {
val result = lock.read { idsInternal[index] }
if (result == null && hasBeenValidated) {
val hasher = XXHash64()
hasher.update(index.toByte())
hasher.update((index ushr 8).toByte())
hasher.update((index ushr 16).toByte())
hasher.update((index ushr 24).toByte())
val missIndex = hasher.digestAsLong()
lock.write {
if (loggedMisses.add(index.toString())) {
if (loggedMisses.add(missIndex)) {
LOGGER.warn("No such $name: ID $index")
}
}
@ -170,13 +193,23 @@ class Registry<T : Any>(val name: String) {
return get(index) ?: throw NoSuchElementException("No such $name: $index")
}
/**
* To be used inside network readers, so clients sending invalid data will get kicked
*/
fun refOrThrow(index: String): Ref<T> = get(index)?.ref ?: throw NoSuchElementException("No such $name: ${index.limit()}")
fun ref(index: String): Ref<T> = lock.write {
keyRefs.computeIfAbsent(index, Object2ObjectFunction {
val ref = RefImpl(Either.left(it as String))
ref.entry = keysInternal[it]
if (hasBeenValidated && ref.entry == null && loggedMisses.add(it)) {
LOGGER.warn("No such $name: $it")
if (hasBeenValidated && ref.entry == null) {
val hasher = XXHash64()
hasher.update(index.toByteArray())
if (loggedMisses.add(hasher.digestAsLong())) {
LOGGER.warn("No such $name: ${it.limit()}")
}
}
ref
@ -185,13 +218,26 @@ class Registry<T : Any>(val name: String) {
}
}
/**
* To be used inside network readers, so clients sending invalid data will get kicked
*/
fun refOrThrow(index: Int): Ref<T> = get(index)?.ref ?: throw NoSuchElementException("No such $name: ID $index")
fun ref(index: Int): Ref<T> = lock.write {
idRefs.computeIfAbsent(index, Int2ObjectFunction {
val ref = RefImpl(Either.right(it))
ref.entry = idsInternal[it]
if (hasBeenValidated && ref.entry == null && loggedMisses.add(it.toString())) {
LOGGER.warn("No such $name: ID $it")
if (hasBeenValidated && ref.entry == null) {
val hasher = XXHash64()
hasher.update(index.toByte())
hasher.update((index ushr 8).toByte())
hasher.update((index ushr 16).toByte())
hasher.update((index ushr 24).toByte())
if (loggedMisses.add(hasher.digestAsLong())) {
LOGGER.warn("No such $name: ID $it")
}
}
ref
@ -261,7 +307,10 @@ class Registry<T : Any>(val name: String) {
}
entry.value = value
entry.json = json
if (storeJson)
entry.json = json
entry.file = file
entry.isBuiltin = isBuiltin

View File

@ -13,8 +13,10 @@ import kotlinx.coroutines.Runnable
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import org.apache.logging.log4j.LogManager
import org.classdump.luna.compiler.CompilerChunkLoader
import org.classdump.luna.compiler.CompilerSettings
@ -344,7 +346,6 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
registerTypeAdapterFactory(JsonReference.Companion)
registerTypeAdapter(ColorReplacements.Companion)
registerTypeAdapterFactory(BlueprintLearnList.Companion)
registerTypeAdapter(RGBAColorTypeAdapter)
@ -619,7 +620,7 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
return files
}
private fun doInitialize() {
private suspend fun doInitialize() {
if (!initializing && !initialized) {
initializing = true
} else {
@ -665,21 +666,29 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
loaded = toLoad - tasks.size
loadingProgress = (total - tasks.size) / total
loadingProgressText = "Loading JSON assets, $loaded / $toLoad"
LockSupport.parkNanos(5_000_000L)
delay(10L)
}
Registries.finishLoad()
RecipeRegistry.finishLoad()
ItemRegistry.finishLoad()
Registries.validate()
initializing = false
initialized = true
// Suggest VM to reclaim memory after game has been loaded
System.gc()
}
private var initializationFuture: CompletableFuture<*>? = null
fun initializeGame(): CompletableFuture<*> {
return submit { doInitialize() }
return supplyAsync {
if (initializationFuture == null) {
initializationFuture = scope.launch { doInitialize() }.asCompletableFuture()
}
return@supplyAsync initializationFuture!!
}.thenCompose { it }
}
private var fontPath: File? = null

View File

@ -109,9 +109,9 @@ interface IStarboundFile : ISBFileLocator {
val children = file.children ?: return NonExistingFile(name = split.last(), fullPath = computeFullPath() + "/" + path)
val find = children[split[splitIndex]]
if (find is StarboundPak.SBDirectory) {
if (find is StarboundPak.SBDirectory || find != null && find.isDirectory) {
file = find
} else if (find is StarboundPak.SBFile) {
} else if (find is StarboundPak.SBFile || find != null && find.isFile) {
if (splitIndex + 1 != split.size) {
return NonExistingFile(name = split.last(), fullPath = computeFullPath() + "/" + path)
}
@ -270,10 +270,10 @@ class PhysicalFile(val real: File, override val parent: PhysicalFile? = null) :
get() = real.isFile
override val children: Map<String, PhysicalFile>?
get() {
return real.list()?.associate { it to PhysicalFile(File(it), this) }
return real.list()?.associate { it.lowercase() to PhysicalFile(File(real, it), this) }
}
override val name: String
get() = real.name
get() = if (parent == null) "" else real.name.lowercase()
private val fullPatch by lazy { super.computeFullPath().sbIntern() }
private val directory by lazy { super.computeDirectory(false).sbIntern() }

View File

@ -62,6 +62,7 @@ import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity
import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity
import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.formatBytesShort
import ru.dbotthepony.kstarbound.util.supplyAsync
import ru.dbotthepony.kstarbound.world.ChunkState
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.RayDirection
@ -78,12 +79,14 @@ import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.time.Duration
import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.ForkJoinWorkerThread
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock
import java.util.function.Consumer
import java.util.function.Function
import java.util.function.IntConsumer
import kotlin.collections.ArrayList
import kotlin.concurrent.withLock
@ -335,6 +338,33 @@ class StarboundClient private constructor(val clientID: Int) : BlockableEventLoo
texture
}
val busyTexture: CompletableFuture<GLTexture2D> by lazy(LazyThreadSafetyMode.NONE) {
Starbound.IO_EXECUTOR.supplyAsync {
MemoryStack.stackPush().use { stack ->
val pWidth = stack.mallocInt(1)
val pHeight = stack.mallocInt(1)
val pChannels = stack.mallocInt(1)
val readFromDisk = readInternalBytes("starbound_busy.png")
val buff = ByteBuffer.allocateDirect(readFromDisk.size)
buff.put(readFromDisk)
buff.position(0)
val data = STBImage.stbi_load_from_memory(buff, pWidth, pHeight, pChannels, 4)
?: throw IllegalStateException("Unable to decode starbound_busy.png")
(pWidth[0] to pHeight[0]) to data
}
}.thenApplyAsync(Function { (size, data) ->
val (w, h) = size
val texture = GLTexture2D(w, h, GL_RGBA8)
texture.upload(GL_RGBA, GL_UNSIGNED_BYTE, data)
texture.textureMagFilter = GL_NEAREST
texture
}, this)
}
val missingTexture by lazy(LazyThreadSafetyMode.NONE) {
val texture = GLTexture2D(8, 8, GL_RGB8)
val buffer = ByteBuffer.allocateDirect(3 * 8 * 8)
@ -621,26 +651,14 @@ class StarboundClient private constructor(val clientID: Int) : BlockableEventLoo
private var renderedLoadingScreen = false
private fun renderLoadingScreen() {
performOpenGLCleanup()
updateViewportParams()
clearColor = RGBAColor.BLACK
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)
private fun renderBusyDots() {
val min = viewportHeight.coerceAtMost(viewportWidth)
val size = min * 0.02f
val program = programs.positionColor
val builder = program.builder
uberShaderPrograms.forValidRefs { it.viewMatrix = viewportMatrixScreen }
fontShaderPrograms.forValidRefs { it.viewMatrix = viewportMatrixScreen }
stack.clear(Matrix3f.identity())
program.colorMultiplier = RGBAColor.WHITE
builder.builder.begin(GeometryType.QUADS)
if (dotCountdown-- <= 0) {
@ -664,6 +682,69 @@ class StarboundClient private constructor(val clientID: Int) : BlockableEventLoo
builder.builder.centeredQuad(viewportWidth * 0.5f - size * 2f, viewportHeight * 0.5f, size, size) { color(a) }
builder.builder.centeredQuad(viewportWidth * 0.5f + size * 2f, viewportHeight * 0.5f, size, size) { color(c) }
program.use()
builder.upload()
builder.draw()
}
private fun renderBusyLogo() {
val min = viewportHeight.coerceAtMost(viewportWidth)
val size = min * 0.0035f
val width = size * 46f
val height = size * 28f
val program = programs.positionTexture
val builder = program.builder
program.texture0 = 0
textures2D[0] = busyTexture.get()
program.colorMultiplier = RGBAColor.WHITE
builder.builder.begin(GeometryType.QUADS)
val x = viewportWidth * 0.5f
val y = viewportHeight * 0.5f
val index = ((System.nanoTime() / 40_000_000L) % 29L) / 29f
builder.builder.vertex(x - width, y - height).uv(index, 0f)
builder.builder.vertex(x - width, y + height).uv(index, 1f)
builder.builder.vertex(x + width, y + height).uv(index + 1f / 29f, 1f)
builder.builder.vertex(x + width, y - height).uv(index + 1f / 29f, 0f)
program.use()
builder.upload()
builder.draw()
}
fun renderBusyIcon() {
if (busyTexture.isDone && !busyTexture.isCompletedExceptionally) {
renderBusyLogo()
} else {
renderBusyDots()
}
}
private fun renderLoadingScreen() {
performOpenGLCleanup()
updateViewportParams()
clearColor = RGBAColor.BLACK
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)
uberShaderPrograms.forValidRefs { it.viewMatrix = viewportMatrixScreen }
fontShaderPrograms.forValidRefs { it.viewMatrix = viewportMatrixScreen }
stack.clear(Matrix3f.identity())
renderBusyIcon()
val program = programs.positionColor
val builder = program.builder
program.colorMultiplier = RGBAColor.WHITE
builder.builder.begin(GeometryType.QUADS)
builder.builder.quad(0f, viewportHeight - 20f, viewportWidth * Starbound.loadingProgress.toFloat(), viewportHeight.toFloat()) { color(RGBAColor.GREEN) }
GLFW.glfwSetWindowTitle(window, "KStarbound: ${Starbound.loadingProgressText}")
@ -671,7 +752,6 @@ class StarboundClient private constructor(val clientID: Int) : BlockableEventLoo
val runtime = Runtime.getRuntime()
//if (runtime.maxMemory() <= 4L * 1024L * 1024L * 1024L) {
builder.builder.centeredQuad(viewportWidth * 0.5f, viewportHeight * 0.1f, viewportWidth * 0.7f, 2f) { color(RGBAColor.WHITE) }
builder.builder.centeredQuad(viewportWidth * 0.5f, viewportHeight * 0.1f + 20f, viewportWidth * 0.7f, 2f) { color(RGBAColor.WHITE) }
builder.builder.centeredQuad(viewportWidth * (0.5f - 0.35f), viewportHeight * 0.1f + 10f, 2f, 20f) { color(RGBAColor.WHITE) }
@ -687,7 +767,6 @@ class StarboundClient private constructor(val clientID: Int) : BlockableEventLoo
leftEdge + viewportWidth * 0.7f * ((runtime.totalMemory() - runtime.freeMemory()) / runtime.maxMemory().toDouble()).toFloat(),
viewportHeight * 0.1f + 19f
) { color(RGBAColor(29, 140, 160)) }
//}
if (fontInitialized) {
drawPerformanceBasic(true)

View File

@ -1,31 +0,0 @@
package ru.dbotthepony.kstarbound.client.network.packets
import com.google.gson.JsonObject
import ru.dbotthepony.kommons.io.readUUID
import ru.dbotthepony.kommons.io.writeUUID
import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.json.readJsonObject
import ru.dbotthepony.kstarbound.json.writeJsonObject
import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.UUID
class SpawnWorldObjectPacket(val uuid: UUID, val data: JsonObject) : IClientPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readUUID(), stream.readJsonObject())
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeUUID(uuid)
stream.writeJsonObject(data)
}
override fun play(connection: ClientConnection) {
connection.client.mailbox.execute {
val world = connection.client.world ?: return@execute
val obj = WorldObject.fromJson(data)
//obj.uuid = uuid
obj.joinWorld(world)
}
}
}

View File

@ -19,7 +19,7 @@ class IdMap<T : Any>(val min: Int = 0, val max: Int = Int.MAX_VALUE, private val
return nextIndex
}
fun add(value: T): Int {
fun nextFreeIndex(): Int {
var i = 0
var index = next()
@ -41,6 +41,11 @@ class IdMap<T : Any>(val min: Int = 0, val max: Int = Int.MAX_VALUE, private val
}
}
return index
}
fun add(value: T): Int {
val index = nextFreeIndex()
map[index] = value
return index
}

View File

@ -1,30 +0,0 @@
package ru.dbotthepony.kstarbound.defs
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory
data class ActorMovementModifiers(
val groundMovementModifier: Double = 0.0,
val liquidMovementModifier: Double = 0.0,
val speedModifier: Double = 0.0,
val airJumpModifier: Double = 0.0,
val liquidJumpModifier: Double = 0.0,
val runningSuppressed: Boolean = false,
val jumpingSuppressed: Boolean = false,
val movementSuppressed: Boolean = false,
val facingSuppressed: Boolean = false,
) {
fun combine(other: ActorMovementModifiers): ActorMovementModifiers {
return ActorMovementModifiers(
groundMovementModifier = groundMovementModifier * other.groundMovementModifier,
liquidMovementModifier = liquidMovementModifier * other.liquidMovementModifier,
speedModifier = speedModifier * other.speedModifier,
airJumpModifier = airJumpModifier * other.airJumpModifier,
liquidJumpModifier = liquidJumpModifier * other.liquidJumpModifier,
runningSuppressed = runningSuppressed || other.runningSuppressed,
jumpingSuppressed = jumpingSuppressed || other.jumpingSuppressed,
movementSuppressed = movementSuppressed || other.movementSuppressed,
facingSuppressed = facingSuppressed || other.facingSuppressed,
)
}
}

View File

@ -1,13 +1,17 @@
package ru.dbotthepony.kstarbound.defs
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.math.RGBAColor
@JsonAdapter(ColorReplacements.Adapter::class)
class ColorReplacements private constructor(private val mapping: Int2IntOpenHashMap) {
constructor(mapping: Map<Int, Int>) : this(Int2IntOpenHashMap(mapping))
@ -15,9 +19,11 @@ class ColorReplacements private constructor(private val mapping: Int2IntOpenHash
return mapping.getOrDefault(color, color)
}
companion object : TypeAdapter<ColorReplacements>() {
val EMPTY = ColorReplacements(Int2IntOpenHashMap())
fun toImageOperator(): String {
return "replace;${mapping.int2IntEntrySet().joinToString(";") { "${RGBAColor.rgb(it.intKey).toHexStringRGB().substring(1)}=${RGBAColor.rgb(it.intValue).toHexStringRGB().substring(1)}" }}"
}
class Adapter(gson: Gson) : TypeAdapter<ColorReplacements>() {
override fun write(out: JsonWriter, value: ColorReplacements?) {
if (value == null)
out.nullValue()
@ -25,8 +31,8 @@ class ColorReplacements private constructor(private val mapping: Int2IntOpenHash
out.beginObject()
for ((k, v) in value.mapping) {
out.name(k.toString(16))
out.value(v.toString(16))
out.name(RGBAColor.rgb(k).toHexStringRGB())
out.value(RGBAColor.rgb(v).toHexStringRGB())
}
out.endObject()
@ -40,7 +46,7 @@ class ColorReplacements private constructor(private val mapping: Int2IntOpenHash
if (`in`.nextString() != "")
throw JsonSyntaxException("Invalid color replacement definition near ${`in`.path}")
return ColorReplacements.EMPTY
return EMPTY
}
val mapping = Int2IntOpenHashMap()
@ -48,10 +54,20 @@ class ColorReplacements private constructor(private val mapping: Int2IntOpenHash
`in`.beginObject()
while (`in`.peek() != JsonToken.END_OBJECT) {
val k = `in`.nextName()
val v = `in`.nextString()
val kGet = `in`.nextName()
val vGet = `in`.nextString()
val k = RGBAColor.fromHexStringRGB(kGet)
val v = RGBAColor.fromHexStringRGB(vGet)
mapping[k.toInt(16)] = v.toInt(16)
if (k == null) {
throw JsonSyntaxException("Not a valid Hex color: $kGet")
}
if (v == null) {
throw JsonSyntaxException("Not a valid Hex color: $vGet")
}
mapping[k.toRGBA()] = v.toRGBA()
}
`in`.endObject()
@ -59,4 +75,8 @@ class ColorReplacements private constructor(private val mapping: Int2IntOpenHash
return ColorReplacements(mapping)
}
}
companion object {
val EMPTY = ColorReplacements(Int2IntOpenHashMap())
}
}

View File

@ -1,8 +1,10 @@
package ru.dbotthepony.kstarbound.defs
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
@ -81,6 +83,13 @@ enum class DamageType(override val jsonName: String) : IStringSerializable {
STATUS("Environment");
}
@JsonFactory
data class DamageKind(
val kind: String,
val elementalType: String = "default",
val effects: JsonObject = JsonObject()
)
@JsonFactory
data class EntityDamageTeam(val type: TeamType = TeamType.NULL, val team: Int = 0) {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(TeamType.entries[stream.readUnsignedByte()], stream.readUnsignedShort())
@ -189,10 +198,10 @@ data class DamageData(
val hitType: HitType,
val damageType: DamageType,
val damage: Double,
val knockback: Vector2d,
val knockbackMomentum: Vector2d,
val sourceEntityId: Int,
val inflictorEntityId: Int = 0,
val kind: String,
val damageSourceKind: String,
val statusEffects: Collection<EphemeralStatusEffect>,
) {
constructor(stream: DataInputStream, isLegacy: Boolean, inflictorEntityId: Int) : this(
@ -213,9 +222,9 @@ data class DamageData(
stream.writeEnumStupid(hitType.ordinal, isLegacy)
stream.writeByte(damageType.ordinal)
stream.writeDouble(damage, isLegacy)
stream.writeStruct2d(knockback, isLegacy)
stream.writeStruct2d(knockbackMomentum, isLegacy)
stream.writeInt(sourceEntityId)
stream.writeBinaryString(kind)
stream.writeBinaryString(damageSourceKind)
stream.writeCollection(statusEffects) { it.write(this, isLegacy) }
}
}
@ -363,3 +372,5 @@ data class DamageSource(
val LEGACY_CODEC = legacyCodec(::DamageSource, DamageSource::write)
}
}
data class ElementalDamageType(val resistanceStat: String, val damageNumberParticles: ImmutableMap<HitType, String>)

View File

@ -1,16 +1,22 @@
package ru.dbotthepony.kstarbound.defs
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import ru.dbotthepony.kommons.io.readBinaryString
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.monster.MonsterVariant
import ru.dbotthepony.kstarbound.defs.`object`.ObjectType
import ru.dbotthepony.kstarbound.fromJsonFast
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import ru.dbotthepony.kstarbound.world.entities.MonsterEntity
import ru.dbotthepony.kstarbound.world.entities.ProjectileEntity
import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity
import ru.dbotthepony.kstarbound.world.entities.tile.ContainerObject
import ru.dbotthepony.kstarbound.world.entities.tile.LoungeableObject
import ru.dbotthepony.kstarbound.world.entities.tile.PlantEntity
import ru.dbotthepony.kstarbound.world.entities.tile.PlantPieceEntity
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
@ -36,7 +42,19 @@ enum class EntityType(override val jsonName: String, val storeName: String, val
}
override fun fromStorage(data: JsonObject): AbstractEntity {
return WorldObject.fromJson(data)
val prototype = Registries.worldObjects[data["name"]?.asString ?: throw IllegalArgumentException("Missing object name")] ?: throw IllegalArgumentException("No such object defined for '${data["name"]}'")
val result = when (prototype.value.objectType) {
ObjectType.OBJECT -> WorldObject(prototype)
ObjectType.LOUNGEABLE -> LoungeableObject(prototype)
ObjectType.CONTAINER -> ContainerObject(prototype)
ObjectType.FARMABLE -> TODO("ObjectType.FARMABLE")
ObjectType.TELEPORTER -> TODO("ObjectType.TELEPORTER")
ObjectType.PHYSICS -> TODO("ObjectType.PHYSICS")
}
result.deserialize(data)
return result
}
},
@ -92,11 +110,24 @@ enum class EntityType(override val jsonName: String, val storeName: String, val
MONSTER("monster", "MonsterEntity", false, false) {
override fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity {
TODO("MONSTER")
return MonsterEntity(MonsterVariant.read(stream, isLegacy))
}
override fun fromStorage(data: JsonObject): AbstractEntity {
TODO("MONSTER")
val variantJson = data["monsterVariant"].asJsonObject
val variant = if (variantJson.size() == 3) {
val type = variantJson["type"].asString
val seed = variantJson["seed"].asLong
val uniqueParameters = variantJson["uniqueParameters"].asJsonObject
Registries.monsterTypes.getOrThrow(type).value.create(seed, uniqueParameters)
} else {
Starbound.gson.fromJsonFast(variantJson, MonsterVariant::class.java)
}
val entity = MonsterEntity(variant)
entity.deserialize(data)
return entity
}
},

View File

@ -6,39 +6,41 @@ import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.io.readBinaryString
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.io.readNullableDouble
import ru.dbotthepony.kstarbound.io.writeNullable
import ru.dbotthepony.kstarbound.io.writeNullableDouble
import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter
import java.io.DataInputStream
import java.io.DataOutputStream
@JsonAdapter(EphemeralStatusEffect.Adapter::class)
data class EphemeralStatusEffect(val effect: String, val duration: Double? = null) {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readInternedString(), stream.readNullableDouble())
data class EphemeralStatusEffect(val effect: Registry.Ref<StatusEffectDefinition>, val duration: Double? = null) {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(Registries.statusEffects.refOrThrow(stream.readBinaryString()), stream.readNullableDouble())
constructor(effect: Registry.Entry<StatusEffectDefinition>, duration: Double? = null) : this(effect.ref, duration)
class Adapter(gson: Gson) : TypeAdapter<EphemeralStatusEffect>() {
private val factory = FactoryAdapter.createFor(EphemeralStatusEffect::class, gson = gson)
override fun write(out: JsonWriter, value: EphemeralStatusEffect) {
if (value.duration == null)
out.value(value.effect)
out.value(value.effect.key.left())
else
factory.write(out, value)
}
override fun read(`in`: JsonReader): EphemeralStatusEffect {
if (`in`.peek() == JsonToken.STRING)
return EphemeralStatusEffect(`in`.nextString())
return EphemeralStatusEffect(Registries.statusEffects.ref(`in`.nextString()))
else
return factory.read(`in`)
}
}
fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeBinaryString(effect)
stream.writeBinaryString(effect.key.left())
stream.writeNullableDouble(duration, isLegacy)
}
}

View File

@ -118,7 +118,7 @@ sealed class SpawnTarget {
tickets.addAll(world.permanentChunkTicket(testRect).await())
tickets.forEach { it.chunk.await() }
if (!world.anyCellSatisfies(testRect) { x, y, cell -> cell.foreground.material.value.collisionKind.isSolidCollision })
if (!world.chunkMap.anyCellSatisfies(testRect) { x, y, cell -> cell.foreground.material.value.collisionKind.isSolidCollision })
return testPos
}

View File

@ -21,7 +21,7 @@ import java.util.UUID
@JsonAdapter(WorldID.Adapter::class)
sealed class WorldID {
abstract fun write(stream: DataOutputStream, isLegacy: Boolean)
val isLimbo: Boolean get() = this is Limbo
val isLimbo: Boolean get() = this === Limbo
object Limbo : WorldID() {
override fun write(stream: DataOutputStream, isLegacy: Boolean) {

View File

@ -1,9 +1,9 @@
package ru.dbotthepony.kstarbound.defs.actor.player
package ru.dbotthepony.kstarbound.defs.actor
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory
class ActorMovementModifiers(
data class ActorMovementModifiers(
val groundMovementModifier: Double = 1.0,
val liquidMovementModifier: Double = 1.0,
val speedModifier: Double = 1.0,
@ -11,10 +11,10 @@ class ActorMovementModifiers(
val liquidJumpModifier: Double = 1.0,
val runningSuppressed: Boolean = false,
val jumpingSuppressed: Boolean = false,
val facingSuppressed: Boolean = false,
val movementSuppressed: Boolean = false,
val facingSuppressed: Boolean = false,
) {
fun combine(other: ActorMovementModifiers): ActorMovementModifiers {
fun merge(other: ActorMovementModifiers): ActorMovementModifiers {
return ActorMovementModifiers(
groundMovementModifier = groundMovementModifier * other.groundMovementModifier,
liquidMovementModifier = liquidMovementModifier * other.liquidMovementModifier,
@ -23,8 +23,8 @@ class ActorMovementModifiers(
liquidJumpModifier = liquidJumpModifier * other.liquidJumpModifier,
runningSuppressed = runningSuppressed || other.runningSuppressed,
jumpingSuppressed = jumpingSuppressed || other.jumpingSuppressed,
facingSuppressed = facingSuppressed || other.facingSuppressed,
movementSuppressed = movementSuppressed || other.movementSuppressed,
facingSuppressed = facingSuppressed || other.facingSuppressed,
)
}

View File

@ -10,19 +10,19 @@ import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory
data class StatusControllerConfig(
val statusProperties: JsonObject = JsonObject(),
val minimumLiquidStatusEffectPercentage: Double,
val appliesEnvironmentStatusEffects: Boolean,
val appliesWeatherStatusEffects: Boolean,
val minimumLiquidStatusEffectPercentage: Double = 0.5,
val appliesEnvironmentStatusEffects: Boolean = true,
val appliesWeatherStatusEffects: Boolean = true,
val environmentStatusEffectUpdateTimer: Double = 0.15,
val primaryAnimationConfig: AssetPath? = null,
val primaryScriptSources: ImmutableList<AssetPath> = ImmutableList.of(),
val primaryScriptDelta: Int = 1,
val primaryScriptDelta: Double = 1.0,
val keepDamageNotificationSteps: Int = 120,
val stats: ImmutableMap<String, Stat> = ImmutableMap.of(),
val resources: ImmutableMap<String, Resource> = ImmutableMap.of(),
) {
init {
require(primaryScriptDelta >= 1) { "Non-positive primaryScriptDelta: $primaryScriptDelta" }
require(primaryScriptDelta > 0.0) { "Non-positive primaryScriptDelta: $primaryScriptDelta" }
require(keepDamageNotificationSteps >= 1) { "Non-positive keepDamageNotificationSteps: $keepDamageNotificationSteps" }
}
@ -40,4 +40,8 @@ data class StatusControllerConfig(
val initialValue: Double? = null,
val initialPercentage: Double? = null,
)
companion object {
val EMPTY = StatusControllerConfig()
}
}

View File

@ -1,7 +1,13 @@
package ru.dbotthepony.kstarbound.defs.actor
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.StatusEffectDefinition
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
// stat modifier or named status effect
typealias PersistentStatusEffect = Either<StatModifier, Registry.Ref<StatusEffectDefinition>>
// uint8_t
enum class Gender(override val jsonName: String) : IStringSerializable {
MALE("Male"), FEMALE("Female");

View File

@ -0,0 +1,22 @@
package ru.dbotthepony.kstarbound.defs.actor.behavior
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory
data class BehaviorDefinition(
val name: String,
val parameters: ImmutableMap<String, JsonElement> = ImmutableMap.of(),
val scripts: ImmutableSet<AssetPath> = ImmutableSet.of(),
val root: JsonObject,
) {
val mappedParameters: ImmutableMap<String, NodeParameterValue> = parameters.entries
.stream()
.map { it.key to NodeParameterValue(null, it.value) }
.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second }))
}

View File

@ -0,0 +1,19 @@
package ru.dbotthepony.kstarbound.defs.actor.behavior
import com.google.common.collect.ImmutableMap
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
/**
* Reflects node on behavior graph.
*
* Behavior graph is not what most would expect, since this is code flow graph
* (nodes represent Lua functions), and not decision tree.
*/
@JsonFactory
data class BehaviorNodeDefinition(
/**
* actually should be called "parameters"
*/
val properties: ImmutableMap<String, NodeParameter> = ImmutableMap.of(),
val output: ImmutableMap<String, NodeOutput> = ImmutableMap.of(),
)

View File

@ -0,0 +1,20 @@
package ru.dbotthepony.kstarbound.defs.actor.behavior
import com.google.common.collect.ImmutableList
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.world.entities.behavior.AbstractBehaviorNode
import ru.dbotthepony.kstarbound.world.entities.behavior.BehaviorTree
import ru.dbotthepony.kstarbound.world.entities.behavior.Blackboard
import ru.dbotthepony.kstarbound.world.entities.behavior.DynamicNode
import ru.dbotthepony.kstarbound.world.entities.behavior.ParallelNode
import ru.dbotthepony.kstarbound.world.entities.behavior.RandomizeNode
import ru.dbotthepony.kstarbound.world.entities.behavior.SequenceNode
enum class CompositeNodeType(override val jsonName: String, val factory: (Map<String, NodeParameter>, ImmutableList<AbstractBehaviorNode>) -> AbstractBehaviorNode) : IStringSerializable {
SEQUENCE("Sequence", { _, c -> SequenceNode(c) }),
// in original engine, selector and sequence nodes are identical code-wise
SELECTOR("Selector", { _, c -> SequenceNode(c) }),
PARALLEL("Parallel", { p, c -> ParallelNode(p, c) }),
DYNAMIC("Dynamic", { _, c -> DynamicNode(c) }),
RANDOMIZE("Randomize", { _, c -> RandomizeNode(c) });
}

View File

@ -0,0 +1,42 @@
package ru.dbotthepony.kstarbound.defs.actor.behavior
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kstarbound.json.popObject
import ru.dbotthepony.kstarbound.util.asStringOrNull
import ru.dbotthepony.kstarbound.util.valueOf
import ru.dbotthepony.kstarbound.world.entities.behavior.NodeParameterType
@JsonAdapter(NodeOutput.Adapter::class)
data class NodeOutput(val type: NodeParameterType, val key: String? = null, val ephemeral: Boolean = false) {
class Adapter : TypeAdapter<NodeOutput>() {
override fun write(out: JsonWriter, value: NodeOutput) {
out.beginObject()
out.name("type")
out.value(value.type.jsonName)
if (value.key != null) {
out.name("key")
out.value(value.key)
}
out.name("ephemeral")
out.value(value.ephemeral)
out.endObject()
}
override fun read(`in`: JsonReader): NodeOutput {
val json = `in`.popObject()
return NodeOutput(
NodeParameterType.entries.valueOf(json["type"].asString),
json["key"]?.asStringOrNull,
json.get("ephemeral", false)
)
}
}
}

View File

@ -0,0 +1,49 @@
package ru.dbotthepony.kstarbound.defs.actor.behavior
import com.google.gson.JsonNull
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.value
import ru.dbotthepony.kstarbound.json.popObject
import ru.dbotthepony.kstarbound.util.valueOf
import ru.dbotthepony.kstarbound.world.entities.behavior.NodeParameterType
@JsonAdapter(NodeParameter.Adapter::class)
data class NodeParameter(val type: NodeParameterType, val value: NodeParameterValue) {
override fun toString(): String {
return "[$type as $value]"
}
class Adapter : TypeAdapter<NodeParameter>() {
override fun write(out: JsonWriter, value: NodeParameter) {
out.beginObject()
out.name("type")
out.value(value.type.jsonName)
if (value.value.key != null) {
out.name("key")
out.value(value.value.key)
} else {
out.name("value")
out.value(value.value.value)
}
out.endObject()
}
override fun read(`in`: JsonReader): NodeParameter {
val json = `in`.popObject()
val type = NodeParameterType.entries.valueOf(json["type"].asString)
if ("key" in json) {
return NodeParameter(type, NodeParameterValue(json["key"].asString, null))
} else {
return NodeParameter(type, NodeParameterValue(null, json["value"] ?: JsonNull.INSTANCE))
}
}
}
}

View File

@ -0,0 +1,48 @@
package ru.dbotthepony.kstarbound.defs.actor.behavior
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.value
import ru.dbotthepony.kstarbound.json.popObject
/**
* [key] - references earlier value
*
* [value] - carries a value
*/
@JsonAdapter(NodeParameterValue.Adapter::class)
data class NodeParameterValue(val key: String?, val value: JsonElement?) {
override fun toString(): String {
if (key != null)
return "key=$key"
else
return "value=$value"
}
class Adapter : TypeAdapter<NodeParameterValue>() {
override fun write(out: JsonWriter, value: NodeParameterValue) {
if (value.key != null) {
out.name("key")
out.value(value.key)
} else {
out.name("value")
out.value(value.value)
}
}
override fun read(`in`: JsonReader): NodeParameterValue {
val json = `in`.popObject()
if ("key" in json) {
return NodeParameterValue(json["key"].asString, null)
} else {
return NodeParameterValue(null, json["value"] ?: JsonNull.INSTANCE)
}
}
}
}

View File

@ -38,6 +38,7 @@ import ru.dbotthepony.kstarbound.lua.StateMachine
import ru.dbotthepony.kstarbound.lua.from
import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.indexNoYield
import ru.dbotthepony.kstarbound.lua.toByteString
import ru.dbotthepony.kstarbound.lua.toJson
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import java.io.DataInputStream
@ -198,7 +199,7 @@ data class ItemDescriptor(
}
return allocator.newTable(0, 3).also {
it.rawset("name", name)
it.rawset("name", name.toByteString())
it.rawset("count", count)
it.rawset("parameters", allocator.from(parameters))
}

View File

@ -1,7 +0,0 @@
package ru.dbotthepony.kstarbound.defs.monster
data class ActionDefinition(
val name: String, // ссылается на .nodes?
val cooldown: Double = -1.0,
// val parameters
)

View File

@ -0,0 +1,11 @@
package ru.dbotthepony.kstarbound.defs.monster
import com.google.common.collect.ImmutableList
import ru.dbotthepony.kstarbound.defs.ColorReplacements
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory
class MonsterPaletteSwap(
val name: String,
val swaps: ImmutableList<ColorReplacements>,
)

View File

@ -0,0 +1,17 @@
package ru.dbotthepony.kstarbound.defs.monster
import com.google.common.collect.ImmutableMap
import com.google.gson.JsonObject
import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory
data class MonsterPartDefinition(
val name: String,
val category: String,
val type: String,
// key -> image
val frames: ImmutableMap<String, AssetPath>,
val parameters: JsonObject = JsonObject(),
)

View File

@ -1,15 +1,15 @@
package ru.dbotthepony.kstarbound.defs.monster
import com.google.common.collect.ImmutableMap
import com.google.gson.JsonElement
import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import com.google.gson.JsonObject
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory
data class MonsterSkillDefinition(
val name: String,
val label: String? = null,
val image: SpriteReference? = null,
val config: ImmutableMap<String, JsonElement> = ImmutableMap.of(),
val animationParameters: ImmutableMap<String, JsonElement> = ImmutableMap.of(),
val label: String,
val image: String,
val config: JsonObject = JsonObject(),
val parameters: JsonObject = JsonObject(),
val animationParameters: JsonObject = JsonObject(),
)

View File

@ -3,34 +3,285 @@ package ru.dbotthepony.kstarbound.defs.monster
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import ru.dbotthepony.kommons.gson.JsonArrayCollector
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.getArray
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.IScriptable
import ru.dbotthepony.kstarbound.defs.IThingWithDescription
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition
import ru.dbotthepony.kstarbound.fromJsonFast
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonFlat
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.random.MWCRandom
import ru.dbotthepony.kstarbound.util.random.nextUInt
import ru.dbotthepony.kstarbound.util.random.random
import java.util.concurrent.CompletableFuture
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
private val specialMerge = ImmutableSet.of("scripts", "skills", "specialSkills", "baseSkills")
private fun mergeFinalParameters(params: List<JsonElement>): JsonObject {
val parameters = JsonObject()
for (baseParameters in params) {
if (baseParameters !is JsonObject)
continue
for ((k, v) in baseParameters.entrySet()) {
// Hard-coded merge for scripts and skills parameters, otherwise merge.
if (k in specialMerge) {
v as JsonArray
val v2 = parameters[k]
if (v2 !is JsonArray) {
parameters[k] = v.deepCopy()
} else {
val result = JsonArray(v.size() + v2.size())
result.addAll(v2)
result.addAll(v)
parameters[k] = result
}
} else {
parameters[k] = mergeJson((parameters[k] ?: JsonNull.INSTANCE).deepCopy(), v)
}
}
}
return parameters
}
@JsonFactory
data class MonsterTypeDefinition(
val type: String,
@JsonFlat
val desc: IThingWithDescription,
val categories: ImmutableSet<String> = ImmutableSet.of(),
val parts: ImmutableSet<String> = ImmutableSet.of(),
val shortdescription: String? = null,
val description: String? = null,
val categories: ImmutableSet<String>,
val parts: ImmutableSet<String>,
val animation: AssetReference<AnimationDefinition>,
// [ { "default" : "poptopTreasure", "bow" : "poptopHunting" } ],
// "dropPools" : [ "smallRobotTreasure" ],
val dropPools: Either<ImmutableList<ImmutableMap<String, Registry.Ref<TreasurePoolDefinition>>>, ImmutableList<Registry.Ref<TreasurePoolDefinition>>>,
val baseParameters: BaseParameters
) : IThingWithDescription by desc {
@JsonFactory
data class BaseParameters(
val movementSettings: ActorMovementParameters? = null,
@JsonFlat
val script: IScriptable,
) : IScriptable by script
val colors: String = "default",
val reversed: Boolean = false,
val dropPools: ImmutableList<Either<ImmutableMap<String, Registry.Ref<TreasurePoolDefinition>>, Registry.Ref<TreasurePoolDefinition>>> = ImmutableList.of(),
val baseParameters: JsonElement = JsonNull.INSTANCE,
@Deprecated("Raw property, use processed one", replaceWith = ReplaceWith("this.paramsOverrides"))
val partParameters: JsonElement = JsonNull.INSTANCE,
@Deprecated("Raw property, use processed one", replaceWith = ReplaceWith("this.paramsDesc"))
val partParameterDescription: JsonElement = JsonObject()
) {
val paramsDesc: CompletableFuture<JsonObject>
val paramsOverrides: CompletableFuture<JsonObject>
init {
if (partParameters.isJsonNull) {
// outdated monsters still have partParameterDescription defined
// directly in the
// .monstertype file
paramsDesc = CompletableFuture.completedFuture(partParameterDescription.asJsonObject)
paramsOverrides = CompletableFuture.completedFuture(JsonObject())
} else {
// for updated monsters, use the partParameterDescription from the
// .partparams file
val json = Starbound.loadJsonAsset(partParameters, AssetPathStack.lastFolder())
paramsDesc = json.thenApply {
it.asJsonObject.get("partParameterDescription").asJsonObject
}
paramsOverrides = json.thenApply {
it.asJsonObject.get("partParameters").asJsonObject
}
}
}
fun create(seed: Long, uniqueParameters: JsonObject): MonsterVariant {
// MWCRandom since this MUST product exactly the same result
// between legacy client and new server (or new client and legacy server)
// Thanks Chuckleclusterfuck
val random = MWCRandom(seed.toULong(), 256, 32)
val copyParameters = uniqueParameters.deepCopy()
// select a list of monster parts
val monsterParts = ArrayList<MonsterPartDefinition>()
val categoryName = categories.random(random, true)
var selectedParts = uniqueParameters["selectedParts"]
// key -> image
val animatorPartTags = HashMap<String, String>()
if (selectedParts !is JsonObject) {
selectedParts = JsonObject()
uniqueParameters["selectedParts"] = selectedParts
}
for (partTypeName in parts) {
val randPart = Registries.selectMonsterPart(categoryName, partTypeName, random)
val selectedPart = selectedParts[partTypeName]?.asString
if (selectedPart != null) {
val get = Registries.getMonsterPart(categoryName, partTypeName, selectedPart)
if (get != null) {
monsterParts.add(get)
}
} else if (randPart != null) {
monsterParts.add(randPart)
// cheat the system; while this will surely poison uniqueParameters, at least
// we will make sure selected parts are chosen deterministically across sessions / sides
// However, this hack will only work if we are server in both meanings;
// e.g. we are fully in charge of created monster. If monster is spawned by player,
// then, well, it sucks.
selectedParts[partTypeName] = randPart.name
}
}
for (partConfig in monsterParts) {
for ((k, v) in partConfig.frames) {
animatorPartTags[k] = v.fullPath
}
}
val partParameterList = ArrayList<JsonObject>()
for (partConfig in monsterParts) {
partParameterList.add(partConfig.parameters)
// Include part parameter overrides
if (partConfig.name in paramsOverrides.get()) {
partParameterList.add(paramsOverrides.get()[partConfig.name].asJsonObject)
}
}
// merge part parameters and unique parameters into base parameters
var parameters = JsonObject()
// First assign all the defaults.
for ((k, v) in paramsDesc.get().entrySet()) {
parameters[k] = v.asJsonArray.get(1).deepCopy()
}
// Then go through parameter list and merge based on the merge rules.
for (applyParams in partParameterList) {
for ((k, v) in applyParams.entrySet()) {
val mergeMethod = paramsDesc.get()[k]?.asJsonArray?.get(0)?.asString ?: "override"
var value = parameters[k] ?: JsonNull.INSTANCE
when (mergeMethod.lowercase()) {
"add" -> value = JsonPrimitive(value.asDouble + v.asDouble)
"sub" -> value = JsonPrimitive(value.asDouble - v.asDouble)
"multiply" -> value = JsonPrimitive(value.asDouble * v.asDouble)
"divide" -> value = JsonPrimitive(value.asDouble / v.asDouble)
"inverse" -> value = JsonPrimitive(v.asDouble / value.asDouble)
"min" -> value = JsonPrimitive(min(value.asDouble, v.asDouble))
"max" -> value = JsonPrimitive(max(value.asDouble, v.asDouble))
"pow" -> value = JsonPrimitive(value.asDouble.pow(v.asDouble))
"invpow" -> value = JsonPrimitive(v.asDouble.pow(value.asDouble))
"override" -> {
if (!v.isJsonNull) // why does original engine check this?
value = v
}
"merge" -> {
// "merge" means to either merge maps, or *append* lists together
if (!v.isJsonNull) {
if (value.isJsonNull) {
value = v.deepCopy()
} else if (value::class != v::class) {
value = v.deepCopy()
} else {
if (v is JsonArray) {
value = value.asJsonArray.also { it.addAll(v) }
} else if (v is JsonObject) {
value = mergeJson(value, v)
}
}
}
}
}
parameters[k] = value
}
}
parameters = mergeFinalParameters(listOf(baseParameters, parameters))
mergeJson(parameters, uniqueParameters)
var animationConfig = animation.json.get() ?: JsonNull.INSTANCE
val skillNames = ArrayList<String>()
if ("baseSkills" in parameters || "specialSkills" in parameters) {
val skillCount = parameters.get("skillCount", 2)
val baseSkillNames = parameters.getArray("baseSkills").deepCopy()
val specialSkillNames = parameters.getArray("specialSkills").deepCopy()
// First, pick from base skills
while (baseSkillNames.size() > 0 && skillNames.size < skillCount) {
val pick = random.nextUInt(baseSkillNames.size().toULong() - 1UL).toInt()
skillNames.add(baseSkillNames.remove(pick).asString)
}
// ...then fill in from special skills as needed
while (specialSkillNames.size() > 0 && skillNames.size < skillCount) {
val pick = random.nextUInt(specialSkillNames.size().toULong() - 1UL).toInt()
skillNames.add(specialSkillNames.remove(pick).asString)
}
} else if ("skills" in parameters) {
val availableSkillNames = parameters.getArray("skills").deepCopy()
val skillCount = min(parameters.get("skillCount", 2), availableSkillNames.size())
while (skillNames.size < skillCount) {
val pick = random.nextUInt(availableSkillNames.size().toULong() - 1UL).toInt()
skillNames.add(availableSkillNames.remove(pick).asString)
}
}
if (skillNames.isNotEmpty()) {
animationConfig = animationConfig.deepCopy()
val allParameters = ArrayList<JsonElement>()
allParameters.add(parameters)
for (skillName in skillNames) {
val skill = Registries.monsterSkills[skillName] ?: continue
allParameters.add(skill.value.parameters)
animationConfig = mergeJson(animationConfig, skill.value.animationParameters)
}
// Need to override the final list of skills, instead of merging the lists
parameters = mergeFinalParameters(allParameters)
parameters["skills"] = skillNames.stream().map { JsonPrimitive(it) }.collect(JsonArrayCollector)
}
val variant = MonsterVariant(
type = type,
description = description,
shortDescription = shortdescription,
seed = seed,
uniqueParameters = copyParameters,
animationConfig = Starbound.gson.fromJsonFast(animationConfig, AnimationDefinition::class.java),
reversed = reversed,
animatorPartTags = ImmutableMap.copyOf(animatorPartTags),
parameters = parameters,
dropPools = dropPools,
)
return variant
}
}

View File

@ -0,0 +1,145 @@
package ru.dbotthepony.kstarbound.defs.monster
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import ru.dbotthepony.kommons.io.readBinaryString
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.render.RenderLayer
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.defs.ClientEntityMode
import ru.dbotthepony.kstarbound.defs.ColorReplacements
import ru.dbotthepony.kstarbound.defs.JsonFunction
import ru.dbotthepony.kstarbound.defs.TeamType
import ru.dbotthepony.kstarbound.defs.actor.StatusControllerConfig
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition
import ru.dbotthepony.kstarbound.fromJsonFast
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.staticRandomInt
import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNIT
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.random.RandomGenerator
@JsonFactory
data class MonsterVariant(
val type: String,
val shortDescription: String? = null,
val description: String? = null,
val seed: Long = 0L,
val uniqueParameters: JsonObject = JsonObject(),
val animationConfig: AnimationDefinition,
val reversed: Boolean = false,
val animatorPartTags: ImmutableMap<String, String>,
val parameters: JsonObject,
@Deprecated("Raw property", replaceWith = ReplaceWith("this.actualDropPools"))
val dropPools: ImmutableList<Either<ImmutableMap<String, Registry.Ref<TreasurePoolDefinition>>, Registry.Ref<TreasurePoolDefinition>>> = ImmutableList.of(),
) {
@JsonFactory
data class CommonParameters(
val shortDescription: String? = null,
val description: String? = null,
val scripts: ImmutableList<AssetPath>,
val animationScripts: ImmutableList<AssetPath> = ImmutableList.of(),
val animationCustom: JsonElement = JsonNull.INSTANCE,
val initialScriptDelta: Double = 5.0,
val metaBoundBox: AABB,
val scale: Double,
val renderLayer: RenderLayer.Point = RenderLayer.Point(RenderLayer.Monster),
val movementSettings: ActorMovementParameters = ActorMovementParameters.EMPTY,
val dropPools: ImmutableList<Either<ImmutableMap<String, Registry.Ref<TreasurePoolDefinition>>, Registry.Ref<TreasurePoolDefinition>>>? = null,
val walkMultiplier: Double = 1.0,
val runMultiplier: Double = 1.0,
val jumpMultiplier: Double = 1.0,
val weightMultiplier: Double = 1.0,
val healthMultiplier: Double = 1.0,
val touchDamageMultiplier: Double = 1.0,
val touchDamage: JsonElement = JsonNull.INSTANCE,
val animationDamageParts: JsonObject = JsonObject(),
val statusSettings: StatusControllerConfig? = null,
val knockoutTime: Double = 1.0,
val knockoutEffect: String? = null,
val knockoutAnimationStates: ImmutableMap<String, String> = ImmutableMap.of(),
// in pixels
val mouthOffset: Vector2d,
// in pixels
val feetOffset: Vector2d,
val powerLevelFunction: Registry.Ref<JsonFunction> = Registries.jsonFunctions.ref("monsterLevelPowerMultiplier"),
val healthLevelFunction: Registry.Ref<JsonFunction> = Registries.jsonFunctions.ref("monsterLevelHealthMultiplier"),
val clientEntityMode: ClientEntityMode = ClientEntityMode.CLIENT_SLAVE_ONLY,
val persistent: Boolean = false /* WHAT. */,
val damageTeamType: TeamType = TeamType.ENEMY,
val damageTeam: Int = 2,
val selfDamagePoly: Poly = movementSettings.standingPoly?.map({ it }, { it.firstOrNull() }) ?: Poly.EMPTY,
val portraitIcon: String? = null,
val damageReceivedAggressiveDuration: Double = 1.0,
val onDamagedOthersAggressiveDuration: Double = 5.0,
val onFireAggressiveDuration: Double = 5.0,
val nametagColor: RGBAColor = RGBAColor.WHITE,
val colorSwap: ColorReplacements? = null,
) {
val mouthOffsetTiles = mouthOffset / PIXELS_IN_STARBOUND_UNIT
val feetOffsetTiles = feetOffset / PIXELS_IN_STARBOUND_UNIT
}
val entry = Registries.monsterTypes.getOrThrow(type)
val commonParameters = Starbound.gson.fromJsonFast(parameters, CommonParameters::class.java)
val animatorZoom: Double
get() = commonParameters.scale
val actualDropPools: ImmutableList<Either<ImmutableMap<String, Registry.Ref<TreasurePoolDefinition>>, Registry.Ref<TreasurePoolDefinition>>>
get() = commonParameters.dropPools ?: dropPools
val chosenDropPool: Either<ImmutableMap<String, Registry.Ref<TreasurePoolDefinition>>, Registry.Ref<TreasurePoolDefinition>>? = if (actualDropPools.isEmpty()) null else actualDropPools[staticRandomInt(0, actualDropPools.size - 1, seed, "MonsterDropPool")]
fun write(stream: DataOutputStream, isLegacy: Boolean) {
if (isLegacy) {
stream.writeBinaryString(type)
stream.writeLong(seed)
stream.writeJsonElement(uniqueParameters)
} else {
TODO("Native")
}
}
companion object {
fun read(stream: DataInputStream, isLegacy: Boolean): MonsterVariant {
if (isLegacy) {
val name = stream.readBinaryString()
val seed = stream.readLong()
val uniqueParameters = stream.readJsonElement() as JsonObject
val base = Registries.monsterTypes.getOrThrow(name)
return base.value.create(seed, uniqueParameters)
} else {
TODO("Native")
}
}
}
}

View File

@ -0,0 +1,7 @@
package ru.dbotthepony.kstarbound.defs.quest
import ru.dbotthepony.kstarbound.math.vector.Vector2d
data class QuestGlobalConfig(
val defaultIndicatorOffset: Vector2d = Vector2d.ZERO
)

View File

@ -4,6 +4,7 @@ import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kstarbound.defs.actor.PersistentStatusEffect
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@ -15,7 +16,7 @@ data class AsteroidWorldsConfig(
val gravityRange: Vector2d,
val worldSize: Vector2i,
val threatRange: Vector2d,
val environmentStatusEffects: ImmutableSet<String> = ImmutableSet.of(),
val environmentStatusEffects: ImmutableSet<PersistentStatusEffect> = ImmutableSet.of(),
val overrideTech: ImmutableSet<String>? = null,
val globalDirectives: ImmutableSet<String>? = null,
val beamUpRule: BeamUpRule = BeamUpRule.SURFACE, // TODO: ??? why surface? in asteroid field.

View File

@ -21,6 +21,7 @@ data class Biome(
val surfacePlaceables: BiomePlaceables = BiomePlaceables(),
val undergroundPlaceables: BiomePlaceables = BiomePlaceables(),
val parallax: Parallax? = null,
val spawnProfile: SpawnProfile? = null,
) {
@JvmName("hueShiftTile")
fun hueShift(block: Registry.Entry<TileDefinition>): Float {

View File

@ -2,6 +2,8 @@ package ru.dbotthepony.kstarbound.defs.world
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import ru.dbotthepony.kommons.collect.filterNotNull
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
@ -9,6 +11,7 @@ import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.JsonConfigFunction
import ru.dbotthepony.kstarbound.defs.actor.PersistentStatusEffect
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.json.NativeLegacy
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@ -21,7 +24,7 @@ import java.util.random.RandomGenerator
data class BiomeDefinition(
val airless: Boolean = false,
val name: String,
val statusEffects: ImmutableSet<String> = ImmutableSet.of(),
val statusEffects: ImmutableSet<PersistentStatusEffect> = ImmutableSet.of(),
val weather: ImmutableList<Pair<Double, ImmutableList<AssetReference<WeightedList<String>>>>> = ImmutableList.of(), // binned reference to other assets
val hueShiftOptions: ImmutableList<Double> = ImmutableList.of(),
val skyOptions: ImmutableList<SkyColoring> = ImmutableList.of(),
@ -34,6 +37,7 @@ data class BiomeDefinition(
val surfacePlaceables: BiomePlaceablesDefinition = BiomePlaceablesDefinition(),
val undergroundPlaceables: BiomePlaceablesDefinition = BiomePlaceablesDefinition(),
val parallax: AssetReference<Parallax.Data>? = null,
val spawnProfile: JsonObject? = null,
) {
data class CreationParams(
val hueShift: Double,
@ -82,7 +86,9 @@ data class BiomeDefinition(
.map { NativeLegacy.TileMod(Registries.tileModifiers.ref(it.first)) to it.second }
.filter { it.first.native.isPresent }
.collect(ImmutableList.toImmutableList())
}?.orElse(ImmutableList.of()) ?: ImmutableList.of())
}?.orElse(ImmutableList.of()) ?: ImmutableList.of()),
spawnProfile = spawnProfile?.let { SpawnProfile.create(it, random) }
)
}

View File

@ -8,6 +8,7 @@ import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.defs.actor.PersistentStatusEffect
import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@ -17,7 +18,7 @@ data class DungeonWorldsConfig(
val worldSize: Vector2i,
val gravity: Either<Double, Vector2d>,
val airless: Boolean = false,
val environmentStatusEffects: ImmutableSet<String> = ImmutableSet.of(),
val environmentStatusEffects: ImmutableSet<PersistentStatusEffect> = ImmutableSet.of(),
val overrideTech: ImmutableSet<String>? = null,
val globalDirectives: ImmutableSet<String>? = null,
val beamUpRule: BeamUpRule = BeamUpRule.SURFACE, // TODO: ??? why surface? in floating dungeon.

View File

@ -438,6 +438,7 @@ data class SkyGlobalConfig(
val starVelocityFactor: Double = 0.0,
val flyingTimer: Double = 0.0,
val flashTimer: Double = 1.0,
val dayTransitionTime: Double = 200.0,
) {
@JsonFactory
data class Stars(

View File

@ -0,0 +1,18 @@
package ru.dbotthepony.kstarbound.defs.world
import com.google.common.collect.ImmutableSet
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import java.util.Collections
import java.util.EnumSet
enum class SpawnArea(override val jsonName: String) : IStringSerializable {
SURFACE("surface"),
CEILING("ceiling"),
AIR("air"),
LIQUID("liquid"),
SOLID("solid");
companion object {
val ALL: Set<SpawnArea> = Collections.unmodifiableSet(EnumSet.allOf(SpawnArea::class.java))
}
}

View File

@ -0,0 +1,53 @@
package ru.dbotthepony.kstarbound.defs.world
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kstarbound.json.popObject
import ru.dbotthepony.kstarbound.json.stream
import ru.dbotthepony.kstarbound.util.valueOf
import java.util.EnumSet
import java.util.stream.Collectors
@JsonAdapter(SpawnParameters.Adapter::class)
data class SpawnParameters(val areas: Set<SpawnArea> = SpawnArea.ALL, val region: SpawnRegion = SpawnRegion.ALL, val time: SpawnTime = SpawnTime.ALL) {
fun isCompatible(other: SpawnParameters): Boolean {
return region.isCompatible(other.region) && time.isCompatible(other.time) && areas.any { it in other.areas }
}
class Adapter : TypeAdapter<SpawnParameters>() {
override fun write(out: JsonWriter, value: SpawnParameters) {
out.beginObject()
out.name("areas")
out.beginArray()
value.areas.forEach { out.value(it.jsonName) }
out.endArray()
out.name("region")
out.value(value.region.jsonName)
out.name("time")
out.value(value.time.jsonName)
out.endObject()
}
override fun read(`in`: JsonReader): SpawnParameters {
val json = `in`.popObject()
val areas = if ("area" in json) {
if (json["area"].asString == "all")
SpawnArea.ALL
else
setOf(SpawnArea.entries.valueOf(json["area"].asString))
} else if ("areas" in json) {
json["areas"].asJsonArray.stream().map { SpawnArea.entries.valueOf(it.asString) }.collect(Collectors.toCollection { EnumSet.noneOf(SpawnArea::class.java) })
} else {
setOf()
}
val region = SpawnRegion.entries.valueOf(json["region"].asString)
val time = SpawnTime.entries.valueOf(json["time"].asString)
return SpawnParameters(areas, region, time)
}
}
}

View File

@ -0,0 +1,39 @@
package ru.dbotthepony.kstarbound.defs.world
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.fromJsonFast
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import java.util.random.RandomGenerator
@JsonFactory
data class SpawnProfile(val spawnTypes: ImmutableSet<Registry.Ref<SpawnType>> = ImmutableSet.of(), val monsterParameters: JsonObject = JsonObject()) {
companion object {
fun create(json: JsonObject, random: RandomGenerator): SpawnProfile {
val spawnTypes = ArrayList<Registry.Ref<SpawnType>>()
if ("groups" in json) {
for (group in json["groups"].asJsonArray) {
val pool = group.asJsonObject["pool"]
val select = group.asJsonObject["select"].asJsonPrimitive.asInt
val typePool = if (pool is JsonPrimitive) {
Globals.spawner.spawnGroups[pool.asString] ?: throw NoSuchElementException("No such common spawn group with name $pool")
} else {
Starbound.gson.fromJsonFast(pool)
}
spawnTypes.addAll(typePool.sample(select, random))
}
}
return SpawnProfile(ImmutableSet.copyOf(spawnTypes), json["monsterParameters"] as? JsonObject ?: JsonObject())
}
}
}

View File

@ -0,0 +1,25 @@
package ru.dbotthepony.kstarbound.defs.world
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
enum class SpawnRegion(override val jsonName: String, val enclosed: Boolean, val exposed: Boolean) : IStringSerializable {
ALL("all", true, true) {
override fun isCompatible(other: SpawnRegion): Boolean {
return true
}
},
ENCLOSED("enclosed", true, false) {
override fun isCompatible(other: SpawnRegion): Boolean {
return other.enclosed
}
},
EXPOSED("exposed", false, true){
override fun isCompatible(other: SpawnRegion): Boolean {
return other.exposed
}
};
abstract fun isCompatible(other: SpawnRegion): Boolean
}

View File

@ -0,0 +1,25 @@
package ru.dbotthepony.kstarbound.defs.world
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
enum class SpawnTime(override val jsonName: String, val day: Boolean, val night: Boolean) : IStringSerializable {
ALL("all", true, true) {
override fun isCompatible(other: SpawnTime): Boolean {
return true
}
},
DAY("day", true, false) {
override fun isCompatible(other: SpawnTime): Boolean {
return other.day
}
},
NIGHT("night", false, true) {
override fun isCompatible(other: SpawnTime): Boolean {
return other.night
}
};
abstract fun isCompatible(other: SpawnTime): Boolean
}

View File

@ -0,0 +1,42 @@
package ru.dbotthepony.kstarbound.defs.world
import com.google.gson.JsonObject
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.util.XXHash64
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.defs.monster.MonsterTypeDefinition
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i
@JsonFactory
data class SpawnType(
val name: String,
val dayLevelAdjustment: Vector2d = Vector2d.ZERO,
val nightLevelAdjustment: Vector2d = Vector2d.ZERO,
val monsterType: Either<WeightedList<Registry.Ref<MonsterTypeDefinition>>, Registry.Ref<MonsterTypeDefinition>>,
val monsterParameters: JsonObject = JsonObject(),
val spawnParameters: SpawnParameters = SpawnParameters(),
val groupSize: Vector2i = Vector2i.POSITIVE_XY,
val spawnChance: Double,
// tard
@Deprecated("Raw property", replaceWith = ReplaceWith("this.actualSeedMix"))
val seedMix: Long? = null,
) {
val actualSeedMix: Long
init {
if (monsterType.isLeft && monsterType.left().isEmpty)
throw IllegalArgumentException("monsterType is empty")
if (seedMix != null) {
actualSeedMix = seedMix
} else {
val hasher = XXHash64()
hasher.update(name.toByteArray(Charsets.UTF_8))
actualSeedMix = hasher.digestAsLong()
}
}
}

View File

@ -0,0 +1,48 @@
package ru.dbotthepony.kstarbound.defs.world
import com.google.common.collect.ImmutableMap
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory
data class SpawnerConfig(
val spawnCellSize: Int,
val spawnCellMinimumEmptyTiles: Int,
val spawnCellMinimumLiquidTiles: Int,
val spawnCellMinimumNearSurfaceTiles: Int,
val spawnCellMinimumNearCeilingTiles: Int,
val spawnCellMinimumAirTiles: Int,
val spawnCellMinimumExposedTiles: Int,
val spawnCellNearSurfaceDistance: Int,
val spawnCellNearCeilingDistance: Int,
val minimumDayLevel: Double,
val minimumLiquidLevel: Double,
val spawnCheckResolution: Double,
val spawnSurfaceCheckDistance: Int,
val spawnCeilingCheckDistance: Int,
val spawnProhibitedCheckPadding: Double,
val spawnCellLifetime: Double,
val windowActivationBorder: Int,
val defaultActive: Boolean = true,
val debug: Boolean = false,
val spawnGroups: ImmutableMap<String, WeightedList<Registry.Ref<SpawnType>>> = ImmutableMap.of(),
) {
init {
require(spawnCellSize >= 1) { "Bad spawnCellSize: $spawnCellSize" }
require(spawnCellMinimumEmptyTiles >= 0) { "Negative spawnCellMinimumEmptyTiles: $spawnCellMinimumEmptyTiles" }
require(spawnCellMinimumLiquidTiles >= 0) { "Negative spawnCellMinimumLiquidTiles: $spawnCellMinimumLiquidTiles" }
require(spawnCellMinimumNearSurfaceTiles >= 0) { "Negative spawnCellMinimumNearSurfaceTiles: $spawnCellMinimumNearSurfaceTiles" }
require(spawnCellMinimumNearCeilingTiles >= 0) { "Negative spawnCellMinimumNearCeilingTiles: $spawnCellMinimumNearCeilingTiles" }
require(spawnCellMinimumAirTiles >= 0) { "Negative spawnCellMinimumAirTiles: $spawnCellMinimumAirTiles" }
require(spawnCellMinimumExposedTiles >= 0) { "Negative spawnCellMinimumExposedTiles: $spawnCellMinimumExposedTiles" }
require(spawnCellNearSurfaceDistance >= 0) { "Negative spawnCellNearSurfaceDistance: $spawnCellNearSurfaceDistance" }
require(spawnCellNearCeilingDistance >= 0) { "Negative spawnCellNearCeilingDistance: $spawnCellNearCeilingDistance" }
require(windowActivationBorder >= 0) { "Negative windowActivationBorder: $windowActivationBorder" }
}
}

View File

@ -540,7 +540,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
while (indices.isNotEmpty()) {
val v = indices.random(random)
shuffled.add(v)
indices.removeInt(indices.indexOf(v))
indices.rem(v)
}
while (secondaryRegionCount-- > 0 && shuffled.isNotEmpty()) {

View File

@ -23,10 +23,15 @@ import ru.dbotthepony.kommons.io.writeByteArray
import ru.dbotthepony.kommons.io.writeCollection
import ru.dbotthepony.kommons.io.writeStruct2i
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.defs.EphemeralStatusEffect
import ru.dbotthepony.kstarbound.defs.StatusEffectDefinition
import ru.dbotthepony.kstarbound.defs.actor.PersistentStatusEffect
import ru.dbotthepony.kstarbound.fromJson
import ru.dbotthepony.kstarbound.fromJsonFast
import ru.dbotthepony.kstarbound.io.readDouble
@ -110,7 +115,7 @@ abstract class VisitableWorldParameters {
protected set
var airless: Boolean = false
protected set
var environmentStatusEffects: Set<String> by Delegates.notNull()
var environmentStatusEffects: Set<PersistentStatusEffect> by Delegates.notNull()
protected set
var overrideTech: Set<String>? = null
protected set
@ -140,7 +145,7 @@ abstract class VisitableWorldParameters {
val worldSize: Vector2i,
val gravity: Either<Vector2d, Double>,
val airless: Boolean,
val environmentStatusEffects: Set<String>,
val environmentStatusEffects: Set<PersistentStatusEffect>,
val overrideTech: Set<String>? = null,
val globalDirectives: Set<String>? = null,
val beamUpRule: BeamUpRule,
@ -211,7 +216,7 @@ abstract class VisitableWorldParameters {
if (collection.isNotEmpty())
weatherPool = WeightedList(ImmutableList.copyOf(collection))
environmentStatusEffects = ImmutableSet.copyOf(stream.readCollection { readInternedString() })
environmentStatusEffects = ImmutableSet.copyOf(stream.readCollection { Either.right(Registries.statusEffects.ref(readInternedString())) })
overrideTech = stream.readNullable { ImmutableSet.copyOf(readCollection { readInternedString() }) }
globalDirectives = stream.readNullable { ImmutableSet.copyOf(readCollection { readInternedString() }) }
@ -234,7 +239,7 @@ abstract class VisitableWorldParameters {
else
stream.writeCollection(weatherPool!!.parent) { writeDouble(it.first); writeBinaryString(it.second) }
stream.writeCollection(environmentStatusEffects) { writeBinaryString(it) }
stream.writeCollection(environmentStatusEffects.filter { it.isRight }.map { it.right() }) { writeBinaryString(it.key.left()) }
stream.writeNullable(overrideTech) { writeCollection(it) { writeBinaryString(it) } }
stream.writeNullable(globalDirectives) { writeCollection(it) { writeBinaryString(it) } }
stream.writeByte(beamUpRule.ordinal)

View File

@ -1,7 +1,6 @@
package ru.dbotthepony.kstarbound.defs
package ru.dbotthepony.kstarbound.defs.world
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i
class WorldServerConfig(
val playerSpaceStartRegionSize: Vector2d,

View File

@ -6,11 +6,15 @@ import it.unimi.dsi.fastutil.objects.ObjectArrayList
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.math.AABBi
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.StatusEffectDefinition
import ru.dbotthepony.kstarbound.defs.actor.PersistentStatusEffect
import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition

View File

@ -1,6 +1,8 @@
package ru.dbotthepony.kstarbound.defs.world
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.math.vector.Vector2d
@JsonFactory
data class WorldTemplateConfig(
@ -14,4 +16,8 @@ data class WorldTemplateConfig(
val customTerrainBlendSize: Double = 0.0,
val surfaceCaveAttenuationDist: Double = 0.0,
val surfaceCaveAttenuationFactor: Double = 1.0,
)
val defaultGravity: Either<Vector2d, Double>,
) {
val defaultGravityVector = defaultGravity.map({ it }, { Vector2d(0.0, it) })
}

View File

@ -2,8 +2,6 @@ package ru.dbotthepony.kstarbound.io
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.longs.LongArrayList
import ru.dbotthepony.kommons.io.ByteKey
import ru.dbotthepony.kommons.io.readByteKeyRaw
import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.util.KOptional
import java.io.Closeable

View File

@ -0,0 +1,103 @@
package ru.dbotthepony.kstarbound.io
import ru.dbotthepony.kommons.io.writeVarInt
import java.io.InputStream
import java.io.OutputStream
import java.util.*
private fun toBytes(key: UUID): ByteArray {
val value = ByteArray(16)
value[0] = ((key.mostSignificantBits ushr 56) and 0xFFL).toByte()
value[1] = ((key.mostSignificantBits ushr 48) and 0xFFL).toByte()
value[2] = ((key.mostSignificantBits ushr 40) and 0xFFL).toByte()
value[3] = ((key.mostSignificantBits ushr 32) and 0xFFL).toByte()
value[4] = ((key.mostSignificantBits ushr 24) and 0xFFL).toByte()
value[5] = ((key.mostSignificantBits ushr 16) and 0xFFL).toByte()
value[6] = ((key.mostSignificantBits ushr 8) and 0xFFL).toByte()
value[7] = ((key.mostSignificantBits ushr 0) and 0xFFL).toByte()
value[8 + 0] = ((key.leastSignificantBits ushr 56) and 0xFFL).toByte()
value[8 + 1] = ((key.leastSignificantBits ushr 48) and 0xFFL).toByte()
value[8 + 2] = ((key.leastSignificantBits ushr 40) and 0xFFL).toByte()
value[8 + 3] = ((key.leastSignificantBits ushr 32) and 0xFFL).toByte()
value[8 + 4] = ((key.leastSignificantBits ushr 24) and 0xFFL).toByte()
value[8 + 5] = ((key.leastSignificantBits ushr 16) and 0xFFL).toByte()
value[8 + 6] = ((key.leastSignificantBits ushr 8) and 0xFFL).toByte()
value[8 + 7] = ((key.leastSignificantBits ushr 0) and 0xFFL).toByte()
return value
}
class ByteKey private constructor(private val bytes: ByteArray, mark: Nothing?) : Comparable<ByteKey> {
constructor(vararg bytes: Byte) : this(bytes, null)
constructor(key: String) : this(key.toByteArray(), null)
constructor(key: UUID) : this(toBytes(key), null)
override fun equals(other: Any?): Boolean {
return this === other || other is ByteKey && other.bytes.contentEquals(bytes)
}
val size: Int
get() = bytes.size
operator fun get(index: Int): Byte {
return bytes[index]
}
fun write(stream: OutputStream) {
stream.writeVarInt(bytes.size)
stream.write(bytes)
}
fun writeRaw(stream: OutputStream) {
stream.write(bytes)
}
fun toByteArray(): ByteArray {
return bytes.clone()
}
override fun hashCode(): Int {
return bytes.contentHashCode()
}
override fun toString(): String {
return "ByteKey[${bytes.map { it.toInt() and 0xFF }.joinToString(", ")}]"
}
override fun compareTo(other: ByteKey): Int {
val cmp = size.compareTo(other.size)
if (cmp != 0) return cmp
for (i in bytes.indices) {
if (bytes[i].toInt() and 0xFF > other.bytes[i].toInt() and 0xFF) {
return 1
} else if (bytes[i].toInt() and 0xFF < other.bytes[i].toInt() and 0xFF) {
return -1
}
}
return 0
}
companion object {
/**
* Constructs [ByteKey] without any copying of provided array
*/
@JvmStatic
fun wrap(bytes: ByteArray): ByteKey {
return ByteKey(bytes, null)
}
@JvmStatic
fun read(stream: InputStream): ByteKey {
return stream.readByteKey()
}
@JvmStatic
fun readRaw(stream: InputStream, size: Int): ByteKey {
return stream.readByteKeyRaw(size)
}
}
}

View File

@ -10,6 +10,7 @@ import ru.dbotthepony.kommons.io.readFloat
import ru.dbotthepony.kommons.io.readInt
import ru.dbotthepony.kommons.io.readSignedVarInt
import ru.dbotthepony.kommons.io.readSignedVarLong
import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeByteArray
import ru.dbotthepony.kommons.io.writeDouble
@ -322,3 +323,19 @@ fun InputStream.readDouble(precision: Double, isLegacy: Boolean): Double {
return readDouble()
}
}
fun InputStream.readByteKey(): ByteKey {
return ByteKey(*ByteArray(readVarInt()).also { read(it) })
}
fun InputStream.readByteKeyRaw(size: Int): ByteKey {
return ByteKey(*ByteArray(size).also { read(it) })
}
fun OutputStream.writeByteKey(key: ByteKey) {
key.write(this)
}
fun OutputStream.writeRawByteKey(key: ByteKey) {
key.writeRaw(this)
}

View File

@ -23,6 +23,7 @@ import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.fromJson
import ru.dbotthepony.kstarbound.json.JsonPatch
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.fromJsonTreeFast
import java.util.Collections
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.Future
@ -50,7 +51,6 @@ object ItemRegistry {
private val LOGGER = LogManager.getLogger()
private val entries = HashMap<String, Entry>()
private val tasks = ConcurrentLinkedQueue<Runnable>()
val AIR = Entry("", ItemType.GENERIC, JsonObject(), true, "air", ImmutableSet.of(), ImmutableSet.of(), null)
@ -134,9 +134,6 @@ object ItemRegistry {
}
fun finishLoad() {
tasks.forEach { it.run() }
tasks.clear()
for (obj in Registries.worldObjects.keys.values) {
if (obj.value.hasObjectItem) {
addObjectItem(obj)
@ -154,10 +151,10 @@ object ItemRegistry {
for (file in files) {
futures.add(Starbound.GLOBAL_SCOPE.launch {
try {
val read = JsonPatch.applyAsync(Starbound.ELEMENTS_ADAPTER.read(file.asyncJsonReader().await()), patches[file.computeFullPath()]).asJsonObject
val readData = data.fromJsonTree(read)
val read = JsonPatch.applyAsync(file.asyncJsonReader(), patches[file.computeFullPath()]).asJsonObject
val readData = data.fromJsonTreeFast(read)
tasks.add {
Starbound.submit {
if (readData.itemName in entries) {
LOGGER.warn("Overwriting item definition at ${readData.itemName} (old def originate from ${entries[readData.itemName]!!.file}; new from $file)")
}

View File

@ -58,7 +58,7 @@ import kotlin.properties.Delegates
@JsonAdapter(ItemStack.Adapter::class)
open class ItemStack(val entry: ItemRegistry.Entry, val config: JsonObject, parameters: JsonObject, size: Long) {
/**
* unique number utilized to determine whenever stack has changed
* Monotonically increasing global number utilized to determine whenever stack has changed
*/
var changeset: Long = CHANGESET.incrementAndGet()
private set

View File

@ -32,40 +32,6 @@ object RecipeRegistry {
val output2recipes: Map<String, List<Entry>> = Collections.unmodifiableMap(output2recipesBacking)
val input2recipes: Map<String, List<Entry>> = Collections.unmodifiableMap(input2recipesBacking)
private val tasks = ConcurrentLinkedQueue<Entry>()
private fun add(recipe: Entry) {
val value = recipe.value
recipesInternal.add(recipe)
for (group in value.groups) {
group2recipesInternal.computeIfAbsent(group, Object2ObjectFunction { p ->
ArrayList<Entry>(1).also {
group2recipesBacking[p as String] = Collections.unmodifiableList(it)
}
}).add(recipe)
}
output2recipesInternal.computeIfAbsent(value.output.name, Object2ObjectFunction { p ->
ArrayList<Entry>(1).also {
output2recipesBacking[p as String] = Collections.unmodifiableList(it)
}
}).add(recipe)
for (input in value.input) {
input2recipesInternal.computeIfAbsent(input.name, Object2ObjectFunction { p ->
ArrayList<Entry>(1).also {
input2recipesBacking[p as String] = Collections.unmodifiableList(it)
}
}).add(recipe)
}
}
fun finishLoad() {
tasks.forEach { add(it) }
tasks.clear()
}
fun load(fileTree: Map<String, Collection<IStarboundFile>>, patchTree: Map<String, List<IStarboundFile>>): List<Future<*>> {
val files = fileTree["recipe"] ?: return emptyList()
@ -75,9 +41,34 @@ object RecipeRegistry {
Starbound.EXECUTOR.submit {
try {
val json = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(listedFile.jsonReader()), patchTree[listedFile.computeFullPath()])
val value = recipes.fromJsonTree(json)
tasks.add(Entry(value, json, listedFile))
val recipe = Entry(value, json, listedFile)
Starbound.submit {
recipesInternal.add(recipe)
for (group in value.groups) {
group2recipesInternal.computeIfAbsent(group, Object2ObjectFunction { p ->
ArrayList<Entry>(1).also {
group2recipesBacking[p as String] = Collections.unmodifiableList(it)
}
}).add(recipe)
}
output2recipesInternal.computeIfAbsent(value.output.name, Object2ObjectFunction { p ->
ArrayList<Entry>(1).also {
output2recipesBacking[p as String] = Collections.unmodifiableList(it)
}
}).add(recipe)
for (input in value.input) {
input2recipesInternal.computeIfAbsent(input.name, Object2ObjectFunction { p ->
ArrayList<Entry>(1).also {
input2recipesBacking[p as String] = Collections.unmodifiableList(it)
}
}).add(recipe)
}
}
} catch (err: Throwable) {
LOGGER.error("Loading recipe definition file $listedFile", err)
}

View File

@ -5,11 +5,13 @@ import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import com.google.gson.stream.JsonReader
import kotlinx.coroutines.future.await
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kstarbound.IStarboundFile
import ru.dbotthepony.kstarbound.Starbound
import java.util.concurrent.CompletableFuture
enum class JsonPatch(val key: String) {
TEST("test") {
@ -165,5 +167,25 @@ enum class JsonPatch(val key: String) {
return base
}
suspend fun applyAsync(reader: CompletableFuture<JsonReader>, source: Collection<IStarboundFile>?): JsonElement {
source ?: return Starbound.ELEMENTS_ADAPTER.read(reader.await())
val loaded = source.map { it.asyncJsonReader() }
var base = Starbound.ELEMENTS_ADAPTER.read(reader.await())
val itr = source.iterator()
for (patch in loaded) {
val file = itr.next()
val read = Starbound.ELEMENTS_ADAPTER.read(patch.await())
if (read !is JsonArray) {
LOGGER.error("$patch root element is not an array")
} else {
base = apply(base, read, file)
}
}
return base
}
}
}

View File

@ -1,15 +1,16 @@
package ru.dbotthepony.kstarbound.json.factory
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.json.FastJsonTreeReader
class ImmutableArrayMapTypeAdapter<K, V>(val keyAdapter: TypeAdapter<K>, val elementAdapter: TypeAdapter<V>) : TypeAdapter<ImmutableMap<K, V>>() {
class Immutable2MapTypeAdapter<K, V>(val keyAdapter: TypeAdapter<K>, val elementAdapter: TypeAdapter<V>) : TypeAdapter<ImmutableMap<K, V>>() {
override fun write(out: JsonWriter, value: ImmutableMap<K, V>?) {
if (value == null) {
out.nullValue()
@ -30,6 +31,21 @@ class ImmutableArrayMapTypeAdapter<K, V>(val keyAdapter: TypeAdapter<K>, val ele
if (reader.consumeNull())
return null
if (reader.peek() == JsonToken.BEGIN_OBJECT) {
reader.beginObject()
val builder = ImmutableMap.Builder<K, V>()
while (reader.peek() !== JsonToken.END_OBJECT) {
builder.put(
keyAdapter.read(FastJsonTreeReader(JsonPrimitive(reader.nextName()))) ?: throw JsonSyntaxException("Nulls are not allowed, near ${reader.path}"),
elementAdapter.read(reader) ?: throw JsonSyntaxException("Nulls are not allowed, near ${reader.path}"))
}
reader.endObject()
return builder.build()
}
reader.beginArray()
val builder = ImmutableMap.Builder<K, V>()

View File

@ -31,7 +31,7 @@ class ImmutableCollectionAdapterFactory(val stringInterner: Interner<String> = I
return ImmutableMapTypeAdapter(stringInterner, gson.getAdapter(TypeToken.get(elementType1))) as TypeAdapter<T>
}
return ImmutableArrayMapTypeAdapter(
return Immutable2MapTypeAdapter(
gson.getAdapter(TypeToken.get(elementType0)),
gson.getAdapter(TypeToken.get(elementType1))
) as TypeAdapter<T>

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.json.factory
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
@ -29,6 +30,12 @@ object RGBAColorTypeAdapter : TypeAdapter<RGBAColor>() {
if (`in`.consumeNull())
return null
if (`in`.peek() == JsonToken.STRING) {
val str = `in`.nextString()
val color = RGBAColor.fromHexStringRGB(str) ?: throw JsonSyntaxException("Not a valid hex color: $str")
return color
}
`in`.beginArray()
val red = `in`.nextInt()

View File

@ -29,11 +29,13 @@ import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter
import ru.dbotthepony.kstarbound.math.AABBi
import ru.dbotthepony.kstarbound.math.Line2d
import ru.dbotthepony.kstarbound.util.sbIntern
import ru.dbotthepony.kstarbound.world.physics.Poly
fun ExecutionContext.toVector2i(table: Any): Vector2i {
val x = indexNoYield(table, 1L)
val y = indexNoYield(table, 2L)
returnBuffer.setTo()
if (x !is Number) throw ClassCastException("Expected table representing a vector, but value at [1] is not a number: $x")
if (y !is Number) throw ClassCastException("Expected table representing a vector, but value at [2] is not a number: $y")
@ -44,6 +46,7 @@ fun ExecutionContext.toVector2i(table: Any): Vector2i {
fun ExecutionContext.toVector2d(table: Any): Vector2d {
val x = indexNoYield(table, 1L)
val y = indexNoYield(table, 2L)
returnBuffer.setTo()
if (x !is Number) throw ClassCastException("Expected table representing a vector, but value at [1] is not a number: $x")
if (y !is Number) throw ClassCastException("Expected table representing a vector, but value at [2] is not a number: $y")
@ -57,6 +60,10 @@ fun ExecutionContext.toLine2d(table: Any): Line2d {
return Line2d(p0, p1)
}
fun String?.toByteString(): ByteString? {
return if (this == null) null else ByteString.of(this)
}
fun ExecutionContext.toPoly(table: Table): Poly {
val vertices = ArrayList<Vector2d>()
@ -70,6 +77,7 @@ fun ExecutionContext.toPoly(table: Table): Poly {
fun ExecutionContext.toVector2f(table: Any): Vector2f {
val x = indexNoYield(table, 1L)
val y = indexNoYield(table, 2L)
returnBuffer.setTo()
if (x !is Number) throw ClassCastException("Expected table representing a vector, but value at [1] is not a number: $x")
if (y !is Number) throw ClassCastException("Expected table representing a vector, but value at [2] is not a number: $y")
@ -82,6 +90,7 @@ fun ExecutionContext.toColor(table: Any): RGBAColor {
val y = indexNoYield(table, 2L)
val z = indexNoYield(table, 3L)
val w = indexNoYield(table, 4L) ?: 255
returnBuffer.setTo()
if (x !is Number) throw ClassCastException("Expected table representing a Color, but value at [1] is not a number: $x")
if (y !is Number) throw ClassCastException("Expected table representing a Color, but value at [2] is not a number: $y")
@ -96,6 +105,7 @@ fun ExecutionContext.toAABB(table: Any): AABB {
val y = indexNoYield(table, 2L)
val z = indexNoYield(table, 3L)
val w = indexNoYield(table, 4L)
returnBuffer.setTo()
if (x !is Number) throw ClassCastException("Expected table representing a AABB, but value at [1] is not a number: $x")
if (y !is Number) throw ClassCastException("Expected table representing a AABB, but value at [2] is not a number: $y")
@ -110,6 +120,7 @@ fun ExecutionContext.toAABBi(table: Any): AABBi {
val y = indexNoYield(table, 2L)
val z = indexNoYield(table, 3L)
val w = indexNoYield(table, 4L)
returnBuffer.setTo()
if (x !is Number) throw ClassCastException("Expected table representing a AABBi, but value at [1] is not a number: $x")
if (y !is Number) throw ClassCastException("Expected table representing a AABBi, but value at [2] is not a number: $y")
@ -122,8 +133,8 @@ fun ExecutionContext.toAABBi(table: Any): AABBi {
fun toJsonFromLua(value: Any?): JsonElement {
return when (value) {
null, is JsonNull -> JsonNull.INSTANCE
is String -> JsonPrimitive(value)
is ByteString -> JsonPrimitive(value.decode())
is String -> JsonPrimitive(value.sbIntern())
is ByteString -> JsonPrimitive(value.decode().sbIntern())
is Number -> JsonPrimitive(value)
is Boolean -> InternedJsonElementAdapter.of(value)
is Table -> value.toJson()
@ -320,6 +331,22 @@ fun TableFactory.from(value: JsonObject): Table {
return data
}
fun TableFactory.from(value: Int?): Long? {
return value?.toLong()
}
fun TableFactory.from(value: Long?): Long? {
return value
}
fun TableFactory.from(value: String?): ByteString? {
return value.toByteString()
}
fun TableFactory.from(value: ByteString?): ByteString? {
return value
}
fun TableFactory.from(value: JsonArray): Table {
val (_, nils, data) = createJsonTable(LUA_HINT_ARRAY, 0, value.size())

View File

@ -18,6 +18,10 @@ import org.classdump.luna.runtime.Dispatch
import org.classdump.luna.runtime.ExecutionContext
import org.classdump.luna.runtime.LuaFunction
import org.classdump.luna.runtime.UnresolvedControlThrowable
import java.util.Spliterator
import java.util.Spliterators
import java.util.stream.Stream
import java.util.stream.StreamSupport
import kotlin.math.max
import kotlin.math.min
@ -111,6 +115,14 @@ operator fun Table.iterator(): Iterator<Map.Entry<Any, Any>> {
}
}
fun Table.spliterator(): Spliterator<Map.Entry<Any, Any>> {
return Spliterators.spliteratorUnknownSize(iterator(), Spliterator.ORDERED)
}
fun Table.stream(): Stream<Map.Entry<Any, Any>> {
return StreamSupport.stream(spliterator(), false)
}
/**
* to be used in places where we need to "unpack" table, like this:
*

View File

@ -12,6 +12,8 @@ import org.classdump.luna.Variable
import org.classdump.luna.compiler.CompilerChunkLoader
import org.classdump.luna.compiler.CompilerSettings
import org.classdump.luna.env.RuntimeEnvironments
import org.classdump.luna.exec.CallPausedException
import org.classdump.luna.exec.Continuation
import org.classdump.luna.exec.DirectCallExecutor
import org.classdump.luna.impl.DefaultTable
import org.classdump.luna.impl.NonsuspendableFunctionException
@ -24,6 +26,7 @@ import org.classdump.luna.lib.TableLib
import org.classdump.luna.lib.Utf8Lib
import org.classdump.luna.load.ChunkFactory
import org.classdump.luna.runtime.AbstractFunction1
import org.classdump.luna.runtime.Coroutine
import org.classdump.luna.runtime.ExecutionContext
import org.classdump.luna.runtime.LuaFunction
import ru.dbotthepony.kstarbound.Starbound
@ -158,8 +161,6 @@ class LuaEnvironment : StateContext {
globals["print"] = PrintFunction(globals)
globals["require"] = LuaRequire()
// why not use _ENV anyway lol
globals["self"] = newTable()
@ -212,6 +213,7 @@ class LuaEnvironment : StateContext {
private val scripts = ObjectArraySet<ChunkFactory>()
private var initCalled = false
private val loadedScripts = ObjectArraySet<String>()
val require = LuaRequire()
inner class LuaRequire : AbstractFunction1<ByteString>() {
override fun resume(context: ExecutionContext?, suspendedState: Any?) {
@ -228,13 +230,32 @@ class LuaEnvironment : StateContext {
}
}
init {
globals["require"] = require
}
fun attach(script: ChunkFactory) {
scripts.add(script)
if (initCalled) {
executor.call(this@LuaEnvironment, script.newInstance(Variable(globals)))
} else {
scripts.add(script)
}
}
fun attach(scripts: Collection<AssetPath>) {
for (script in scripts) {
attach(Starbound.loadScript(script.fullPath))
if (initCalled) {
for (name in scripts) {
if (loadedScripts.add(name.fullPath)) {
val script = Starbound.loadScript(name.fullPath)
executor.call(this@LuaEnvironment, script.newInstance(Variable(globals)))
}
}
} else {
for (script in scripts) {
if (loadedScripts.add(script.fullPath)) {
this.scripts.add(Starbound.loadScript(script.fullPath))
}
}
}
}
@ -249,6 +270,8 @@ class LuaEnvironment : StateContext {
errorState = true
}
fun call(fn: Any, vararg args: Any?) = executor.call(this, fn, *args)
fun init(callInit: Boolean = true): Boolean {
check(!initCalled) { "Already called init()" }
initCalled = true
@ -257,7 +280,7 @@ class LuaEnvironment : StateContext {
try {
executor.call(this, script.newInstance(Variable(globals)))
} catch (err: Throwable) {
errorState = true
// errorState = true
LOGGER.error("Failed to attach script to environment", err)
scripts.clear()
return false
@ -273,7 +296,7 @@ class LuaEnvironment : StateContext {
try {
executor.call(this, init)
} catch (err: Throwable) {
errorState = true
// errorState = true
LOGGER.error("Exception on init()", err)
return false
}
@ -284,7 +307,7 @@ class LuaEnvironment : StateContext {
}
fun invokeGlobal(name: String, vararg arguments: Any?): Array<Any?> {
if (errorState)
if (errorState || !initCalled)
return arrayOf()
val load = globals[name]
@ -293,7 +316,7 @@ class LuaEnvironment : StateContext {
return try {
executor.call(this, load, *arguments)
} catch (err: Throwable) {
errorState = true
// errorState = true
LOGGER.error("Exception while calling global $name", err)
arrayOf()
}

View File

@ -30,15 +30,15 @@ class LuaMessageHandlerComponent(val lua: LuaEnvironment, val nameProvider: () -
private val logPacer = ActionPacer(1, 5)
fun handle(message: String, isLocal: Boolean, arguments: JsonArray): JsonElement {
val handler = handlers[message] ?: throw World.MessageCallException("No registered handler for $message")
fun handle(message: String, isLocal: Boolean, arguments: JsonArray): JsonElement? {
val handler = handlers[message] ?: return null
try {
val unpack = arguments.map { lua.from(it) }.toTypedArray()
val result = lua.executor.call(lua, handler, isLocal, *unpack)
if (result.isEmpty()) {
return JsonNull.INSTANCE
return null
} else {
return toJsonFromLua(result[0])
}

View File

@ -19,6 +19,19 @@ class LuaUpdateComponent(val lua: LuaEnvironment) {
}
}
fun update(delta: Double, preRun: () -> Unit) {
if (stepCount == 0.0)
return
steps += delta / Starbound.TIMESTEP
if (steps >= stepCount) {
steps %= stepCount
preRun()
lua.invokeGlobal("update", stepCount * Starbound.TIMESTEP)
}
}
fun update(delta: Double, vararg arguments: Any?) {
if (stepCount == 0.0)
return

View File

@ -13,6 +13,7 @@ import ru.dbotthepony.kstarbound.lua.luaFunction
import ru.dbotthepony.kstarbound.lua.luaFunctionArray
import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.toAABB
import ru.dbotthepony.kstarbound.lua.toByteString
import ru.dbotthepony.kstarbound.lua.toColor
import ru.dbotthepony.kstarbound.lua.toPoly
import ru.dbotthepony.kstarbound.lua.toVector2d
@ -71,7 +72,7 @@ fun provideAnimatorBindings(self: Animator, lua: LuaEnvironment) {
callbacks["rotationGroups"] = luaFunction {
val groups = self.rotationGroups()
val keys = newTable(groups.size, 0)
groups.withIndex().forEach { (i, v) -> keys[i + 1L] = v }
groups.withIndex().forEach { (i, v) -> keys[i + 1L] = v.toByteString() }
returnBuffer.setTo(keys)
}

View File

@ -6,26 +6,59 @@ import ru.dbotthepony.kstarbound.lua.from
import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.luaFunction
import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.toByteString
import ru.dbotthepony.kstarbound.math.Line2d
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.ActorEntity
import ru.dbotthepony.kstarbound.world.entities.MonsterEntity
import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
fun provideEntityBindings(self: AbstractEntity, lua: LuaEnvironment) {
if (self is WorldObject)
provideWorldObjectBindings(self, lua)
if (self is MonsterEntity)
provideMonsterBindings(self, lua)
if (self is ActorEntity)
provideStatusControllerBindings(self.statusController, lua)
provideWorldBindings(self.world, lua)
val table = lua.newTable()
lua.globals["entity"] = table
table["id"] = luaFunction { returnBuffer.setTo(self.entityID) }
table["id"] = luaFunction { returnBuffer.setTo(self.entityID.toLong()) }
table["position"] = luaFunction { returnBuffer.setTo(from(self.position)) }
table["entityType"] = luaFunction { returnBuffer.setTo(self.type.jsonName) }
table["uniqueId"] = luaFunction { returnBuffer.setTo(self.uniqueID.get()) }
table["entityType"] = luaFunction { returnBuffer.setTo(self.type.jsonName.toByteString()) }
table["uniqueId"] = luaFunction { returnBuffer.setTo(self.uniqueID.get().toByteString()) }
table["persistent"] = luaFunction { returnBuffer.setTo(self.isPersistent) }
table["entityInSight"] = luaFunction { TODO() }
table["isValidTarget"] = luaFunction { TODO() }
table["entityInSight"] = luaFunction { target: Number ->
val entity = self.world.entities[target.toInt()] ?: return@luaFunction returnBuffer.setTo(false)
returnBuffer.setTo(self.world.chunkMap.collide(Line2d(self.position, entity.position)) { it.type.isSolidCollision } == null)
}
table["isValidTarget"] = luaFunction { target: Number ->
val entity = self.world.entities[target.toInt()] ?: return@luaFunction returnBuffer.setTo(false)
if (!self.team.get().canDamage(entity.team.get(), self == entity)) // original engine always passes `false` to `isSelfDamage`
return@luaFunction returnBuffer.setTo(false)
if (entity is MonsterEntity)
return@luaFunction returnBuffer.setTo(entity.isAggressive)
// TODO: NPC handling here
returnBuffer.setTo(entity is PlayerEntity)
}
table["damageTeam"] = luaFunction {
val result = newTable()
result["team"] = self.team.get().team
result["type"] = self.type.jsonName
result["team"] = self.team.get().team.toLong()
result["type"] = self.type.jsonName.toByteString()
returnBuffer.setTo(result)
}

View File

@ -0,0 +1,193 @@
package ru.dbotthepony.kstarbound.lua.bindings
import com.google.common.collect.ImmutableSet
import org.classdump.luna.ByteString
import org.classdump.luna.Table
import ru.dbotthepony.kommons.collect.collect
import ru.dbotthepony.kommons.collect.map
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.DamageSource
import ru.dbotthepony.kstarbound.defs.EntityDamageTeam
import ru.dbotthepony.kstarbound.defs.PhysicsForceRegion
import ru.dbotthepony.kstarbound.fromJsonFast
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
import ru.dbotthepony.kstarbound.lua.from
import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.iterator
import ru.dbotthepony.kstarbound.lua.luaFunction
import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.stream
import ru.dbotthepony.kstarbound.lua.toJson
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.lua.toVector2d
import ru.dbotthepony.kstarbound.util.SBPattern
import ru.dbotthepony.kstarbound.util.sbIntern
import ru.dbotthepony.kstarbound.util.valueOf
import ru.dbotthepony.kstarbound.world.entities.ActorEntity
import ru.dbotthepony.kstarbound.world.entities.MonsterEntity
fun provideMonsterBindings(self: MonsterEntity, lua: LuaEnvironment) {
val config = lua.newTable()
lua.globals["config"] = config
config["getParameter"] = createConfigBinding { key, default ->
key.find(self.variant.parameters) ?: default
}
val callbacks = lua.newTable()
lua.globals["monster"] = callbacks
callbacks["type"] = luaFunction {
returnBuffer.setTo(self.variant.type)
}
callbacks["seed"] = luaFunction {
// what the fuck.
returnBuffer.setTo(self.variant.seed.toString())
}
callbacks["seedNumber"] = luaFunction {
returnBuffer.setTo(self.variant.seed)
}
callbacks["uniqueParameters"] = luaFunction {
returnBuffer.setTo(from(self.variant.uniqueParameters))
}
callbacks["level"] = luaFunction {
// TODO: this makes half sense
returnBuffer.setTo(self.monsterLevel ?: 0.0)
}
callbacks["setDamageOnTouch"] = luaFunction { damage: Boolean ->
self.damageOnTouch = damage
}
callbacks["setDamageSources"] = luaFunction { sources: Table? ->
self.customDamageSources.clear()
if (sources != null) {
for ((_, v) in sources) {
self.customDamageSources.add(Starbound.gson.fromJson((v as Table).toJson(), DamageSource::class.java))
}
}
}
callbacks["setDamageParts"] = luaFunction { parts: Table? ->
if (parts == null) {
self.animationDamageParts.clear()
} else {
val strings = parts.stream().map { (_, v) -> v.toString() }.collect(ImmutableSet.toImmutableSet())
self.animationDamageParts.removeIf { it !in strings }
for (v in strings) {
if (v !in self.animationDamageParts) {
self.animationDamageParts.add(v)
}
}
}
}
callbacks["setAggressive"] = luaFunction { isAggressive: Boolean ->
self.isAggressive = isAggressive
}
callbacks["setActiveSkillName"] = luaFunction { name: ByteString ->
self.activeSkillName = name.decode().sbIntern()
}
callbacks["setDropPool"] = luaFunction { pool: Any ->
self.dropPool = Starbound.gson.fromJsonFast(toJsonFromLua(pool))
}
callbacks["toAbsolutePosition"] = luaFunction { position: Table ->
returnBuffer.setTo(from(self.movement.getAbsolutePosition(toVector2d(position))))
}
callbacks["mouthPosition"] = luaFunction {
returnBuffer.setTo(from(self.mouthPosition))
}
// This callback is registered here rather than in
// makeActorMovementControllerCallbacks
// because it requires access to world
callbacks["flyTo"] = luaFunction { position: Table ->
self.movement.controlFly = self.world.geometry.diff(toVector2d(position), self.movement.position)
}
callbacks["setDeathParticleBurst"] = luaFunction { value: ByteString? ->
self.deathParticlesBurst = value?.decode()?.sbIntern() ?: ""
}
callbacks["setDeathSound"] = luaFunction { value: ByteString? ->
self.deathSound = value?.decode()?.sbIntern() ?: ""
}
callbacks["setPhysicsForces"] = luaFunction { forces: Table? ->
if (forces == null) {
self.forceRegions.clear()
} else {
self.forceRegions.clear()
for ((_, v) in forces) {
self.forceRegions.add(Starbound.gson.fromJsonFast(toJsonFromLua(v), PhysicsForceRegion::class.java))
}
}
}
callbacks["setName"] = luaFunction { name: ByteString? ->
self.networkName = name?.decode()
}
callbacks["setDisplayNametag"] = luaFunction { shouldDisplay: Boolean ->
self.displayNameTag = shouldDisplay
}
callbacks["say"] = luaFunction { line: ByteString, tags: Table? ->
var actualLine = line.decode()
if (tags != null) {
actualLine = SBPattern.of(actualLine).resolveOrSkip({ tags[it]?.toString() })
}
if (actualLine.isNotBlank()) {
self.addChatMessage(actualLine.sbIntern())
}
returnBuffer.setTo(actualLine.isNotBlank())
}
callbacks["sayPortrait"] = luaFunction { line: ByteString, portrait: ByteString, tags: Table? ->
var actualLine = line.decode()
if (tags != null) {
actualLine = SBPattern.of(actualLine).resolveOrSkip({ tags[it]?.toString() })
}
if (actualLine.isNotBlank()) {
self.addChatMessage(actualLine.sbIntern(), portrait.decode().sbIntern())
}
returnBuffer.setTo(actualLine.isNotBlank())
}
callbacks["setDamageTeam"] = luaFunction { team: Any ->
self.team.accept(Starbound.gson.fromJsonFast(toJsonFromLua(team), EntityDamageTeam::class.java))
}
callbacks["setUniqueId"] = luaFunction { name: ByteString? ->
self.uniqueID.accept(name?.decode()?.sbIntern())
}
callbacks["setDamageBar"] = luaFunction { type: ByteString ->
self.damageBarType = ActorEntity.DamageBarType.entries.valueOf(type.decode())
}
callbacks["setInteractive"] = luaFunction { isInteractive: Boolean ->
self.isInteractive = isInteractive
}
callbacks["setAnimationParameter"] = luaFunction { name: ByteString, value: Any? ->
self.scriptedAnimationParameters[name.decode().sbIntern()] = toJsonFromLua(value)
}
}

View File

@ -0,0 +1,328 @@
package ru.dbotthepony.kstarbound.lua.bindings
import org.classdump.luna.Table
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.actor.ActorMovementModifiers
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.fromJsonFast
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
import ru.dbotthepony.kstarbound.lua.from
import ru.dbotthepony.kstarbound.lua.luaFunction
import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.tableFrom
import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toByteString
import ru.dbotthepony.kstarbound.lua.toJson
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.lua.toVector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.entities.ActorMovementController
import ru.dbotthepony.kstarbound.world.entities.AnchorState
import ru.dbotthepony.kstarbound.world.physics.Poly
import kotlin.math.PI
class MovementControllerBindings(val self: ActorMovementController) {
fun init(lua: LuaEnvironment) {
val callbacks = lua.newTable()
lua.globals["mcontroller"] = callbacks
// pass-through
callbacks["mass"] = luaFunction {
returnBuffer.setTo(self.mass)
}
callbacks["localBoundBox"] = luaFunction {
returnBuffer.setTo(from(self.computeLocalCollisionAABB()))
}
callbacks["boundBox"] = luaFunction {
returnBuffer.setTo(from(self.computeLocalCollisionAABB()))
}
callbacks["collisionBoundBox"] = luaFunction {
returnBuffer.setTo(from(self.computeGlobalCollisionAABB()))
}
callbacks["collisionPoly"] = luaFunction {
returnBuffer.setTo(from(self.movementParameters.collisionPoly?.map({ it }, { it.firstOrNull() }) ?: Poly.EMPTY))
}
callbacks["collisionPolies"] = luaFunction {
returnBuffer.setTo(tableFrom((self.movementParameters.collisionPoly?.map({ listOf(it) }, { it }) ?: listOf(Poly.EMPTY)).map { from(it) }))
}
callbacks["collisionBody"] = luaFunction {
returnBuffer.setTo(from(self.computeGlobalHitboxes().firstOrNull() ?: Poly.EMPTY))
}
callbacks["collisionBodies"] = luaFunction {
returnBuffer.setTo(tableFrom(self.computeGlobalHitboxes().map { from(it) }))
}
callbacks["position"] = luaFunction { returnBuffer.setTo(tableOf(self.xPosition, self.yPosition)) }
callbacks["xPosition"] = luaFunction { returnBuffer.setTo(self.xPosition) }
callbacks["yPosition"] = luaFunction { returnBuffer.setTo(self.yPosition) }
callbacks["velocity"] = luaFunction { returnBuffer.setTo(tableOf(self.xVelocity, self.yVelocity)) }
callbacks["xVelocity"] = luaFunction { returnBuffer.setTo(self.xVelocity) }
callbacks["yVelocity"] = luaFunction { returnBuffer.setTo(self.yVelocity) }
callbacks["rotation"] = luaFunction { returnBuffer.setTo(self.rotation) }
callbacks["isColliding"] = luaFunction { returnBuffer.setTo(self.isColliding) }
callbacks["isNullColliding"] = luaFunction { returnBuffer.setTo(self.isCollidingWithNull) }
callbacks["isCollisionStuck"] = luaFunction { returnBuffer.setTo(self.isCollisionStuck) }
callbacks["stickingDirection"] = luaFunction { returnBuffer.setTo(self.stickingDirection) }
callbacks["liquidPercentage"] = luaFunction { returnBuffer.setTo(self.liquidPercentage) }
callbacks["liquidId"] = luaFunction { returnBuffer.setTo(self.liquid?.id ?: 0) }
callbacks["liquidName"] = luaFunction { returnBuffer.setTo(self.liquid?.key.toByteString()) }
callbacks["onGround"] = luaFunction { returnBuffer.setTo(self.isOnGround) }
callbacks["zeroG"] = luaFunction { returnBuffer.setTo(self.isZeroGravity) }
callbacks["atWorldLimit"] = luaFunction { bottomOnly: Boolean ->
returnBuffer.setTo(self.isAtWorldLimit(bottomOnly))
}
callbacks["setAnchorState"] = luaFunction { anchor: Number, index: Number ->
self.anchorState = AnchorState(anchor.toInt(), index.toInt())
}
callbacks["resetAnchorState"] = luaFunction {
self.anchorState = null
}
callbacks["anchorState"] = luaFunction {
val anchorState = self.anchorState
if (anchorState != null) {
returnBuffer.setTo(anchorState.entityID, anchorState.positionIndex)
}
}
callbacks["setPosition"] = luaFunction { value: Table ->
resetPathMove = true
self.position = toVector2d(value)
}
callbacks["setXPosition"] = luaFunction { value: Number ->
resetPathMove = true
self.xPosition = value.toDouble()
}
callbacks["setYPosition"] = luaFunction { value: Number ->
resetPathMove = true
self.yPosition = value.toDouble()
}
callbacks["translate"] = luaFunction { value: Table ->
resetPathMove = true
self.position += toVector2d(value)
}
callbacks["setVelocity"] = luaFunction { value: Table ->
resetPathMove = true
self.velocity = toVector2d(value)
}
callbacks["setXVelocity"] = luaFunction { value: Number ->
resetPathMove = true
self.xVelocity = value.toDouble()
}
callbacks["setYVelocity"] = luaFunction { value: Number ->
resetPathMove = true
self.yVelocity = value.toDouble()
}
callbacks["addMomentum"] = luaFunction { value: Table ->
resetPathMove = true
if (self.mass != 0.0) // let's not collapse into black hole
self.velocity += toVector2d(value) / self.mass
}
callbacks["setRotation"] = luaFunction { value: Number ->
resetPathMove = true
self.rotation = value.toDouble()
}
callbacks["baseParameters"] = luaFunction { returnBuffer.setTo(from(Starbound.gson.toJsonTree(self.actorMovementParameters))) }
callbacks["walking"] = luaFunction { returnBuffer.setTo(self.isWalking) }
callbacks["running"] = luaFunction { returnBuffer.setTo(self.isRunning) }
callbacks["movingDirection"] = luaFunction { returnBuffer.setTo(self.movingDirection.luaValue) }
callbacks["facingDirection"] = luaFunction { returnBuffer.setTo(self.facingDirection.luaValue) }
callbacks["crouching"] = luaFunction { returnBuffer.setTo(self.isCrouching) }
callbacks["flying"] = luaFunction { returnBuffer.setTo(self.isFlying) }
callbacks["falling"] = luaFunction { returnBuffer.setTo(self.isFalling) }
callbacks["canJump"] = luaFunction { returnBuffer.setTo(self.canJump) }
callbacks["jumping"] = luaFunction { returnBuffer.setTo(self.isJumping) }
callbacks["groundMovement"] = luaFunction { returnBuffer.setTo(self.isGroundMovement) }
callbacks["liquidMovement"] = luaFunction { returnBuffer.setTo(self.isLiquidMovement) }
// controls, stored locally, reset automatically or are persistent
callbacks["controlRotation"] = luaFunction { value: Number ->
controlRotationRate += value.toDouble()
}
callbacks["controlAcceleration"] = luaFunction { value: Table ->
controlAcceleration += toVector2d(value)
}
callbacks["controlForce"] = luaFunction { value: Table ->
controlForce += toVector2d(value)
}
callbacks["controlApproachVelocity"] = luaFunction { value: Table, rate: Number ->
controlApproachVelocity = ActorMovementController.ApproachVelocityCommand(toVector2d(value), rate.toDouble())
}
callbacks["controlApproachVelocityAlongAngle"] = luaFunction { angle: Number, targetVelocity: Number, maxControlForce: Number, positiveOnly: Boolean ->
controlApproachVelocityAlongAngle = ActorMovementController.ApproachVelocityAngleCommand(angle.toDouble(), targetVelocity.toDouble(), maxControlForce.toDouble(), positiveOnly)
}
callbacks["controlApproachXVelocity"] = luaFunction { targetVelocity: Number, maxControlForce: Number ->
controlApproachVelocityAlongAngle = ActorMovementController.ApproachVelocityAngleCommand(0.0, targetVelocity.toDouble(), maxControlForce.toDouble(), false)
}
callbacks["controlApproachYVelocity"] = luaFunction { targetVelocity: Number, maxControlForce: Number ->
controlApproachVelocityAlongAngle = ActorMovementController.ApproachVelocityAngleCommand(PI / 2.0, targetVelocity.toDouble(), maxControlForce.toDouble(), false)
}
callbacks["controlParameters"] = luaFunction { data: Table ->
controlParameters = controlParameters.merge(Starbound.gson.fromJsonFast(data.toJson(true), ActorMovementParameters::class.java))
}
callbacks["controlModifiers"] = luaFunction { data: Table ->
controlModifiers = controlModifiers.merge(Starbound.gson.fromJsonFast(data.toJson(true), ActorMovementModifiers::class.java))
}
callbacks["controlMove"] = luaFunction { direction: Number, shouldRun: Boolean? ->
// why?
val new = Direction.valueOf(direction)
if (new != null) {
controlMove = new
controlShouldRun = shouldRun ?: false
}
}
callbacks["controlFace"] = luaFunction { direction: Number ->
// why?
val new = Direction.valueOf(direction)
if (new != null)
controlFace = new
}
callbacks["controlDown"] = luaFunction { controlDown = true }
callbacks["controlCrouch"] = luaFunction { controlCrouch = true }
callbacks["controlJump"] = luaFunction { should: Boolean -> controlJump = should }
callbacks["controlHoldJump"] = luaFunction { controlHoldJump = true }
callbacks["controlFly"] = luaFunction { value: Table -> controlFly = toVector2d(value) }
callbacks["controlPathMove"] = luaFunction { position: Table, run: Boolean?, parameters: Table? ->
if (pathMoveResult?.first == toVector2d(position)) {
val pathMoveResult = pathMoveResult!!
this@MovementControllerBindings.pathMoveResult = null
returnBuffer.setTo(pathMoveResult.second)
} else {
pathMoveResult = null
val result = self.pathMove(toVector2d(position), run == true, Starbound.gson.fromJsonFast(toJsonFromLua(parameters)))
if (result == null) {
controlPathMove = toVector2d(position) to (run == true)
}
returnBuffer.setTo(result?.second)
}
}
callbacks["pathfinding"] = luaFunction {
returnBuffer.setTo(self.pathController.isPathfinding)
}
callbacks["autoClearControls"] = luaFunction {
returnBuffer.setTo(autoClearControls)
}
callbacks["setAutoClearControls"] = luaFunction { should: Boolean ->
autoClearControls = should
}
callbacks["clearControls"] = luaFunction { clearControls() }
}
private var autoClearControls = true
private var controlRotationRate = 0.0
private var controlAcceleration = Vector2d.ZERO
private var controlForce = Vector2d.ZERO
private var controlApproachVelocity: ActorMovementController.ApproachVelocityCommand? = null
private var controlApproachVelocityAlongAngle: ActorMovementController.ApproachVelocityAngleCommand? = null
private var controlParameters = ActorMovementParameters.EMPTY
private var controlModifiers = ActorMovementModifiers.EMPTY
private var controlMove: Direction? = null
private var controlFace: Direction? = null
private var controlShouldRun: Boolean? = null
private var controlDown = false
private var controlCrouch = false
private var controlJump = false
private var controlHoldJump = false
private var controlFly: Vector2d? = null
private var resetPathMove = false
private var pathMoveResult: Pair<Vector2d, Boolean>? = null
private var controlPathMove: Pair<Vector2d, Boolean>? = null
fun apply() {
self.controlRotationRate += controlRotationRate
self.controlAcceleration += controlAcceleration
self.controlForce += controlForce
if (controlApproachVelocity != null) self.approachVelocities.add(controlApproachVelocity!!)
if (controlApproachVelocityAlongAngle != null) self.approachVelocityAngles.add(controlApproachVelocityAlongAngle!!)
self.controlActorMovementParameters = self.controlActorMovementParameters.merge(controlParameters)
self.controlMovementModifiers = self.controlMovementModifiers.merge(controlModifiers)
if (controlMove != null) self.controlMove = controlMove
if (controlShouldRun != null) self.controlRun = controlShouldRun!!
if (controlFace != null) self.controlFace = controlFace
if (controlDown) self.controlDown = true
if (controlCrouch) self.controlCrouch = true
if (controlJump) self.controlJump = true
if (controlHoldJump && !self.isOnGround) self.controlJump = true
if (controlFly != null) self.controlFly = controlFly
// some action was taken that has priority over pathing, setting position or velocity
if (resetPathMove)
controlPathMove = null
if (controlPathMove != null && pathMoveResult == null)
pathMoveResult = self.controlPathMove(controlPathMove!!.first, controlPathMove!!.second)
}
fun clearControlsIfNeeded() {
if (autoClearControls)
clearControls()
}
private fun clearControls() {
controlRotationRate = 0.0
controlAcceleration = Vector2d.ZERO
controlForce = Vector2d.ZERO
controlApproachVelocity = null
controlApproachVelocityAlongAngle = null
controlParameters = ActorMovementParameters.EMPTY
controlModifiers = ActorMovementModifiers.EMPTY
controlMove = null
controlFace = null
controlShouldRun = null
controlDown = false
controlCrouch = false
controlJump = false
controlHoldJump = false
controlFly = null
pathMoveResult = null
controlPathMove = null
resetPathMove = false
}
}

View File

@ -1,5 +1,7 @@
package ru.dbotthepony.kstarbound.lua.bindings
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
import org.apache.logging.log4j.LogManager
import org.classdump.luna.ByteString
@ -37,6 +39,7 @@ import ru.dbotthepony.kstarbound.lua.nextOptionalInteger
import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.tableMapOf
import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toByteString
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.lua.toLuaInteger
import ru.dbotthepony.kstarbound.util.random.random
@ -125,7 +128,7 @@ private fun registryDef2(registry: Registry<*>): LuaFunction<Any?, *, *, *, *> {
if (def != null) {
returnBuffer.setTo(newTable(0, 2).also {
it["path"] = def.file?.computeFullPath()
it["path"] = def.file?.computeFullPath().toByteString()
it["config"] = from(def.json)
})
} else {
@ -144,7 +147,7 @@ private val recipesForItem = luaFunction { name: ByteString ->
val list = RecipeRegistry.output2recipes[name.decode()]
if (list == null) {
returnBuffer.setTo(newTable())
returnBuffer.setTo(tableOf())
} else {
returnBuffer.setTo(newTable(list.size, 0).also {
for ((i, v) in list.withIndex()) {
@ -192,17 +195,17 @@ private fun materialMiningSound(context: ExecutionContext, arguments: ArgumentIt
val mod = lookup(Registries.tiles, arguments.nextOptionalAny(null))
if (mod != null && mod.value.miningSounds.map({ it.isNotEmpty() }, { true })) {
context.returnBuffer.setTo(mod.value.miningSounds.map({ it.random() }, { it }))
context.returnBuffer.setTo(mod.value.miningSounds.map({ it.random() }, { it }).toByteString())
return
}
if (tile != null && tile.value.miningSounds.map({ it.isNotEmpty() }, { true })) {
context.returnBuffer.setTo(tile.value.miningSounds.map({ it.random() }, { it }))
context.returnBuffer.setTo(tile.value.miningSounds.map({ it.random() }, { it }).toByteString())
return
}
// original engine parity
context.returnBuffer.setTo("")
context.returnBuffer.setTo("".toByteString())
}
private fun materialFootstepSound(context: ExecutionContext, arguments: ArgumentIterator) {
@ -210,16 +213,16 @@ private fun materialFootstepSound(context: ExecutionContext, arguments: Argument
val mod = lookup(Registries.tiles, arguments.nextOptionalAny(null))
if (mod != null && mod.value.footstepSound.map({ it.isNotEmpty() }, { true })) {
context.returnBuffer.setTo(mod.value.footstepSound.map({ it.random() }, { it }))
context.returnBuffer.setTo(mod.value.footstepSound.map({ it.random() }, { it }).toByteString())
return
}
if (tile != null && tile.value.footstepSound.map({ it.isNotEmpty() }, { true })) {
context.returnBuffer.setTo(tile.value.footstepSound.map({ it.random() }, { it }))
context.returnBuffer.setTo(tile.value.footstepSound.map({ it.random() }, { it }).toByteString())
return
}
context.returnBuffer.setTo(Globals.client.defaultFootstepSound.map({ it }, { it.random() }))
context.returnBuffer.setTo(Globals.client.defaultFootstepSound.map({ it }, { it.random() }).toByteString())
}
private val materialHealth = luaFunction { id: Any ->
@ -227,7 +230,7 @@ private val materialHealth = luaFunction { id: Any ->
}
private val liquidName = luaFunction { id: Any ->
returnBuffer.setTo(lookupStrict(Registries.liquid, id).key)
returnBuffer.setTo(lookupStrict(Registries.liquid, id).key.toByteString())
}
private val liquidId = luaFunction { id: Any ->
@ -235,11 +238,11 @@ private val liquidId = luaFunction { id: Any ->
}
private val techType = luaFunction { id: Any ->
returnBuffer.setTo(lookupStrict(Registries.techs, id).value.type)
returnBuffer.setTo(lookupStrict(Registries.techs, id).value.type.toByteString())
}
private val techConfig = luaFunction { id: Any ->
returnBuffer.setTo(lookupStrict(Registries.techs, id).json)
returnBuffer.setTo(from(lookupStrict(Registries.techs, id).json))
}
private val jobject = luaFunction { returnBuffer.setTo(createJsonObject()) }
@ -362,11 +365,11 @@ private val createBiome = luaFunction { name: ByteString, seed: Number, vertical
}
private val treeStemDirectory = luaFunction { name: ByteString ->
returnBuffer.setTo(Registries.treeStemVariants[name.decode()]?.file?.computeDirectory(true) ?: "/")
returnBuffer.setTo(Registries.treeStemVariants[name.decode()]?.file?.computeDirectory(true).toByteString() ?: "/".toByteString())
}
private val treeFoliageDirectory = luaFunction { name: ByteString ->
returnBuffer.setTo(Registries.treeFoliageVariants[name.decode()]?.file?.computeDirectory(true) ?: "/")
returnBuffer.setTo(Registries.treeFoliageVariants[name.decode()]?.file?.computeDirectory(true).toByteString() ?: "/".toByteString())
}
private val itemConfig = luaFunction { descriptor: Any, level: Number?, seed: Number? ->
@ -403,7 +406,7 @@ private val createItem = luaFunction { descriptor: Any, level: Number?, seed: Nu
}
private val itemType = luaFunction { identifier: ByteString ->
returnBuffer.setTo(ItemRegistry[identifier.decode()].type.jsonName)
returnBuffer.setTo(ItemRegistry[identifier.decode()].type.jsonName.toByteString())
}
private val itemTags = luaFunction { identifier: ByteString ->
@ -414,6 +417,32 @@ private val itemHasTag = luaFunction { identifier: ByteString, tag: ByteString -
returnBuffer.setTo(tag.decode() in ItemRegistry[identifier.decode()].itemTags)
}
private val monsterSkillParameter = luaFunction { skillName: ByteString, configParameterName: ByteString ->
val skill = Registries.monsterSkills[skillName.decode()]
if (skill != null) {
returnBuffer.setTo(from(skill.value.config[configParameterName.decode()] ?: JsonNull.INSTANCE))
}
}
private val monsterParameters = luaFunction { monsterType: ByteString, seed: Number? ->
returnBuffer.setTo(from(Registries.monsterTypes.getOrThrow(monsterType.decode()).value.create(seed?.toLong() ?: 0L, JsonObject()).parameters))
}
private val monsterMovementSettings = luaFunction { monsterType: ByteString, seed: Number? ->
returnBuffer.setTo(from(Registries.monsterTypes.getOrThrow(monsterType.decode()).value.create(seed?.toLong() ?: 0L, JsonObject()).parameters["movementSettings"] ?: JsonObject()))
}
private val elementalResistance = luaFunction { damageKindName: ByteString ->
returnBuffer.setTo(Globals.elementalTypes[Registries.damageKinds.getOrThrow(damageKindName.decode()).value.elementalType]!!.resistanceStat.toByteString())
}
private val dungeonMetadata = luaFunction { dungeon: ByteString ->
returnBuffer.setTo(from(Registries.dungeons.getOrThrow(dungeon.decode()).jsonObject["metadata"]))
}
private val hasTech = registryDefExists(Registries.techs)
fun provideRootBindings(lua: LuaEnvironment) {
val table = lua.newTable()
lua.globals["root"] = table
@ -480,11 +509,11 @@ fun provideRootBindings(lua: LuaEnvironment) {
table["createBiome"] = createBiome
table["monsterSkillParameter"] = luaStub("monsterSkillParameter")
table["monsterParameters"] = luaStub("monsterParameters")
table["monsterMovementSettings"] = luaStub("monsterMovementSettings")
table["monsterSkillParameter"] = monsterSkillParameter
table["monsterParameters"] = monsterParameters
table["monsterMovementSettings"] = monsterMovementSettings
table["hasTech"] = registryDefExists(Registries.techs)
table["hasTech"] = hasTech
table["techType"] = techType
table["techConfig"] = techConfig
@ -493,7 +522,6 @@ fun provideRootBindings(lua: LuaEnvironment) {
table["collection"] = luaStub("collection")
table["collectables"] = luaStub("collectables")
table["elementalResistance"] = luaStub("elementalResistance")
table["dungeonMetadata"] = luaStub("dungeonMetadata")
table["behavior"] = luaStub("behavior")
table["elementalResistance"] = elementalResistance
table["dungeonMetadata"] = dungeonMetadata
}

View File

@ -23,6 +23,7 @@ import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toAABB
import ru.dbotthepony.kstarbound.lua.toAABBi
import ru.dbotthepony.kstarbound.lua.toByteString
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.lua.toVector2d
import ru.dbotthepony.kstarbound.lua.toVector2i
@ -234,7 +235,7 @@ fun provideServerWorldBindings(self: ServerWorld, callbacks: Table, lua: LuaEnvi
}
callbacks["fidelity"] = luaFunction {
returnBuffer.setTo("high")
returnBuffer.setTo("high".toByteString())
}
callbacks["setSkyTime"] = luaFunction { newTime: Number ->
@ -247,7 +248,7 @@ fun provideServerWorldBindings(self: ServerWorld, callbacks: Table, lua: LuaEnvi
}
callbacks["setUniverseFlag"] = luaFunction { flag: ByteString ->
returnBuffer.setTo(self.server.addUniverseFlag(flag.decode()))
returnBuffer.setTo(self.server.addUniverseFlag(flag.decode().sbIntern()))
}
callbacks["unsetUniverseFlag"] = luaFunction { flag: ByteString ->

View File

@ -0,0 +1,251 @@
package ru.dbotthepony.kstarbound.lua.bindings
import com.google.gson.JsonObject
import com.google.gson.reflect.TypeToken
import org.classdump.luna.ByteString
import org.classdump.luna.LuaRuntimeException
import org.classdump.luna.Table
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.DamageData
import ru.dbotthepony.kstarbound.defs.EphemeralStatusEffect
import ru.dbotthepony.kstarbound.defs.actor.PersistentStatusEffect
import ru.dbotthepony.kstarbound.fromJsonFast
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
import ru.dbotthepony.kstarbound.lua.from
import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.iterator
import ru.dbotthepony.kstarbound.lua.luaFunction
import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toByteString
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.util.sbIntern
import ru.dbotthepony.kstarbound.world.entities.StatusController
import kotlin.math.absoluteValue
private object PersistentStatusEffectToken : TypeToken<PersistentStatusEffect>()
private object CPersistentStatusEffectToken : TypeToken<ArrayList<PersistentStatusEffect>>()
fun provideStatusControllerBindings(self: StatusController, lua: LuaEnvironment) {
val callbacks = lua.newTable()
lua.globals["status"] = callbacks
callbacks["statusProperty"] = luaFunction { key: ByteString, ifMissing: Any? ->
val get = self.getProperty(key.decode())
if (get == null) {
returnBuffer.setTo(ifMissing)
} else {
returnBuffer.setTo(from(get))
}
}
callbacks["setStatusProperty"] = luaFunction { key: ByteString, value: Any? ->
self.setProperty(key.decode().sbIntern(), toJsonFromLua(value))
}
callbacks["stat"] = luaFunction { name: ByteString ->
returnBuffer.setTo(self.liveStats[name.decode()]?.effectiveModifiedValue ?: 0.0)
}
callbacks["statPositive"] = luaFunction { name: ByteString ->
returnBuffer.setTo(self.statPositive(name.decode()))
}
callbacks["resourceNames"] = luaFunction {
returnBuffer.setTo(tableOf(self.resources.keys.toTypedArray()))
}
callbacks["isResource"] = luaFunction { name: ByteString ->
returnBuffer.setTo(name.decode() in self.resources.keys)
}
callbacks["resource"] = luaFunction { name: ByteString ->
val resource = self.resources[name.decode()] ?: return@luaFunction returnBuffer.setTo(0.0)
returnBuffer.setTo(resource.value)
}
callbacks["resourcePositive"] = luaFunction { name: ByteString ->
val resource = self.resources[name.decode()] ?: return@luaFunction returnBuffer.setTo(false)
returnBuffer.setTo(resource.value > 0.0)
}
callbacks["setResource"] = luaFunction { name: ByteString, value: Number ->
val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name")
resource.value = value.toDouble()
}
callbacks["modifyResource"] = luaFunction { name: ByteString, value: Number ->
val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name")
resource.value += value.toDouble()
}
callbacks["giveResource"] = luaFunction { name: ByteString, value: Number ->
// while other functions throw an exception if resource does not exist
// this one returns 0
// Consistency is my first, second, and last name
val resource = self.resources[name.decode()] ?: return@luaFunction returnBuffer.setTo(0.0)
returnBuffer.setTo(resource.give(value.toDouble()))
}
callbacks["consumeResource"] = luaFunction { name: ByteString, value: Number ->
// while other functions throw an exception if resource does not exist
// this one returns 0
// Consistency is my first, second, and last name
val resource = self.resources[name.decode()] ?: return@luaFunction returnBuffer.setTo(false)
returnBuffer.setTo(resource.consume(value.toDouble()))
}
callbacks["overConsumeResource"] = luaFunction { name: ByteString, value: Number ->
// while other functions throw an exception if resource does not exist
// this one returns 0
// Consistency is my first, second, and last name
val resource = self.resources[name.decode()] ?: return@luaFunction returnBuffer.setTo(false)
returnBuffer.setTo(resource.consume(value.toDouble(), allowOverdraw = true))
}
callbacks["resourceLocked"] = luaFunction { name: ByteString ->
val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name")
returnBuffer.setTo(resource.isLocked)
}
callbacks["setResourceLocked"] = luaFunction { name: ByteString, isLocked: Boolean ->
val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name")
resource.isLocked = isLocked
}
callbacks["resetResource"] = luaFunction { name: ByteString, isLocked: Boolean ->
val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name")
resource.reset()
}
callbacks["resetAllResources"] = luaFunction { name: ByteString, isLocked: Boolean ->
self.resetResources()
}
callbacks["resourceMax"] = luaFunction { name: ByteString ->
val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name")
returnBuffer.setTo(resource.maxValue)
}
callbacks["resourcePercentage"] = luaFunction { name: ByteString ->
val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name")
returnBuffer.setTo(resource.percentage)
}
callbacks["setResourcePercentage"] = luaFunction { name: ByteString, value: Number ->
val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name")
resource.setAsPercentage(value.toDouble())
}
callbacks["modifyResourcePercentage"] = luaFunction { name: ByteString, value: Number ->
val resource = self.resources[name.decode()] ?: throw LuaRuntimeException("No such resource $name")
resource.modifyPercentage(value.toDouble())
}
callbacks["getPersistentEffects"] = luaFunction { category: ByteString ->
returnBuffer.setTo(tableOf(self.getPersistentEffects(category.decode()).map { it.map({ from(Starbound.gson.toJsonTree(it)) }, { it }) }))
}
callbacks["addPersistentEffect"] = luaFunction { category: ByteString, effect: Any ->
self.addPersistentEffect(category.decode().sbIntern(), Starbound.gson.fromJsonFast(toJsonFromLua(effect), PersistentStatusEffectToken))
}
callbacks["addPersistentEffects"] = luaFunction { category: ByteString, effect: Any ->
self.addPersistentEffects(category.decode().sbIntern(), Starbound.gson.fromJsonFast(toJsonFromLua(effect), CPersistentStatusEffectToken))
}
callbacks["setPersistentEffects"] = luaFunction { category: ByteString, effect: Any ->
self.setPersistentEffects(category.decode().sbIntern(), Starbound.gson.fromJsonFast(toJsonFromLua(effect), CPersistentStatusEffectToken))
}
callbacks["clearPersistentEffects"] = luaFunction { category: ByteString ->
self.removePersistentEffects(category.decode())
}
callbacks["clearAllPersistentEffects"] = luaFunction {
self.removeAllPersistentEffects()
}
callbacks["addEphemeralEffect"] = luaFunction { name: ByteString, duration: Number?, source: Number? ->
self.addEphemeralEffect(EphemeralStatusEffect(Registries.statusEffects.ref(name.decode().sbIntern()), duration?.toDouble()), source?.toInt())
}
callbacks["addEphemeralEffects"] = luaFunction { effects: Table, source: Number? ->
for ((_, effect) in effects) {
self.addEphemeralEffect(Starbound.gson.fromJsonFast(toJsonFromLua(effect), EphemeralStatusEffect::class.java), source?.toInt())
}
}
callbacks["removeEphemeralEffect"] = luaFunction { name: ByteString ->
self.removeEphemeralEffect(name.decode())
}
callbacks["clearEphemeralEffects"] = luaFunction {
self.removeEphemeralEffects()
}
callbacks["damageTakenSince"] = luaFunction { since: Number? ->
val (list, newSince) = self.recentDamageReceived(since?.toLong() ?: 0L)
returnBuffer.setTo(tableOf(*list.map { from(Starbound.gson.toJsonTree(it)) }.toTypedArray()), newSince)
}
callbacks["inflictedHitsSince"] = luaFunction { since: Number? ->
val (list, newSince) = self.recentHitsDealt(since?.toLong() ?: 0L)
returnBuffer.setTo(tableOf(*list.map { p ->
from(Starbound.gson.toJsonTree(p.second).also { it as JsonObject; it["targetEntityId"] = p.first })
}.toTypedArray()), newSince)
}
callbacks["inflictedDamageSince"] = luaFunction { since: Number? ->
val (list, newSince) = self.recentDamageDealt(since?.toLong() ?: 0L)
returnBuffer.setTo(tableOf(*list.map { from(Starbound.gson.toJsonTree(it)) }.toTypedArray()), newSince)
}
callbacks["activeUniqueStatusEffectSummary"] = luaFunction {
returnBuffer.setTo(tableOf(*self.activeUniqueStatusEffectSummary().map { tableOf(it.first.key, it.second) }.toTypedArray()))
}
callbacks["uniqueStatusEffectActive"] = luaFunction { name: ByteString ->
returnBuffer.setTo(self.uniqueStatusEffectActive(name.decode()))
}
callbacks["primaryDirectives"] = luaFunction {
returnBuffer.setTo(self.primaryDirectives.toByteString())
}
callbacks["setPrimaryDirectives"] = luaFunction { directives: ByteString? ->
self.primaryDirectives = directives?.decode()?.sbIntern() ?: ""
}
callbacks["applySelfDamageRequest"] = luaFunction { damage: Table ->
self.inflictSelfDamage(Starbound.gson.fromJsonFast(toJsonFromLua(damage), DamageData::class.java))
}
callbacks["appliesEnvironmentStatusEffects"] = luaFunction {
returnBuffer.setTo(self.appliesEnvironmentStatusEffects)
}
callbacks["appliesWeatherStatusEffects"] = luaFunction {
returnBuffer.setTo(self.appliesWeatherStatusEffects)
}
callbacks["minimumLiquidStatusEffectPercentage"] = luaFunction {
returnBuffer.setTo(self.minimumLiquidStatusEffectPercentage)
}
callbacks["setAppliesEnvironmentStatusEffects"] = luaFunction { should: Boolean ->
self.appliesEnvironmentStatusEffects = should
}
callbacks["setAppliesWeatherStatusEffects"] = luaFunction { should: Boolean ->
self.appliesWeatherStatusEffects = should
}
callbacks["setMinimumLiquidStatusEffectPercentage"] = luaFunction { value: Number ->
self.minimumLiquidStatusEffectPercentage = value.toDouble()
}
}

View File

@ -1,11 +1,14 @@
package ru.dbotthepony.kstarbound.lua.bindings
import com.google.gson.JsonElement
import org.apache.logging.log4j.LogManager
import org.classdump.luna.ByteString
import org.classdump.luna.Table
import org.classdump.luna.runtime.ExecutionContext
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters
import ru.dbotthepony.kstarbound.json.JsonPath
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
import ru.dbotthepony.kstarbound.lua.from
import ru.dbotthepony.kstarbound.lua.get
@ -14,7 +17,9 @@ import ru.dbotthepony.kstarbound.lua.luaFunctionArray
import ru.dbotthepony.kstarbound.lua.luaFunctionN
import ru.dbotthepony.kstarbound.lua.nextOptionalFloat
import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.toByteString
import ru.dbotthepony.kstarbound.lua.toJson
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.lua.toVector2d
import ru.dbotthepony.kstarbound.lua.userdata.LuaPerlinNoise
import ru.dbotthepony.kstarbound.lua.userdata.LuaRandomGenerator
@ -67,7 +72,7 @@ private val interpolateSinEase = luaFunctionArray { args ->
}
private val replaceTags = luaFunction { string: ByteString, tags: Table ->
returnBuffer.setTo(SBPattern.of(string.toString()).resolveOrSkip({ tags[it]?.toString() }))
returnBuffer.setTo(SBPattern.of(string.toString()).resolveOrSkip({ tags[it]?.toString() }).toByteString())
}
private val makePerlinSource = luaFunction { settings: Table ->
@ -94,15 +99,16 @@ private val staticRandomI32Range = luaFunctionN("staticRandomI32Range") {
returnBuffer.setTo(staticRandomLong(min, max, *it.copyRemaining()))
}
fun provideUtilityBindings(
lua: LuaEnvironment,
random: RandomGenerator = random()
) {
private val mergeJson = luaFunction { a: Any?, b: Any? ->
returnBuffer.setTo(from(ru.dbotthepony.kstarbound.json.mergeJson(toJsonFromLua(a), toJsonFromLua(b))))
}
fun provideUtilityBindings(lua: LuaEnvironment) {
val table = lua.newTable()
lua.globals["sb"] = table
table["makeUuid"] = luaFunction {
returnBuffer.setTo(UUID(random.nextLong(), random.nextLong()).toStarboundString())
returnBuffer.setTo(UUID(lua.random.nextLong(), lua.random.nextLong()).toStarboundString().toByteString())
}
table["logInfo"] = logInfo
@ -112,7 +118,7 @@ fun provideUtilityBindings(
table["nrand"] = luaFunctionN("nrand") { args ->
val stdev = args.nextOptionalFloat() ?: 1.0
val mean = args.nextOptionalFloat() ?: 0.0
random.nextNormalDouble(stdev, mean)
lua.random.nextNormalDouble(stdev, mean)
}
table["print"] = lua.globals["tostring"]
@ -120,7 +126,7 @@ fun provideUtilityBindings(
table["interpolateSinEase"] = interpolateSinEase
table["replaceTags"] = replaceTags
table["makeRandomSource"] = luaFunction { seed: Long? ->
returnBuffer.setTo(LuaRandomGenerator(random(seed ?: random.nextLong())))
returnBuffer.setTo(LuaRandomGenerator(random(seed ?: lua.random.nextLong())))
}
table["makePerlinSource"] = makePerlinSource
@ -132,4 +138,21 @@ fun provideUtilityBindings(
table["staticRandomDoubleRange"] = staticRandomDoubleRange
table["staticRandomI32Range"] = staticRandomI32Range
table["staticRandomI64Range"] = staticRandomI32Range
table["jsonMerge"] = mergeJson
}
fun provideConfigBindings(lua: LuaEnvironment, lookup: ExecutionContext.(path: JsonPath, ifMissing: Any?) -> Any?) {
val config = lua.newTable()
lua.globals["config"] = config
config["getParameter"] = createConfigBinding(lookup)
}
fun createConfigBinding(lookup: ExecutionContext.(path: JsonPath, ifMissing: Any?) -> Any?) = luaFunction { name: ByteString, default: Any? ->
val get = lookup(this, JsonPath.query(name.decode()), default)
if (get is JsonElement)
returnBuffer.setTo(from(get))
else
returnBuffer.setTo(get)
}

View File

@ -12,12 +12,15 @@ import org.classdump.luna.runtime.LuaFunction
import ru.dbotthepony.kommons.collect.map
import ru.dbotthepony.kommons.collect.toList
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile
import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldParameters
import ru.dbotthepony.kstarbound.fromJsonFast
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
@ -33,6 +36,7 @@ import ru.dbotthepony.kstarbound.lua.nextOptionalInteger
import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toAABB
import ru.dbotthepony.kstarbound.lua.toByteString
import ru.dbotthepony.kstarbound.lua.toJson
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.lua.toLine2d
@ -41,9 +45,11 @@ import ru.dbotthepony.kstarbound.lua.toVector2d
import ru.dbotthepony.kstarbound.lua.toVector2i
import ru.dbotthepony.kstarbound.lua.unpackAsArray
import ru.dbotthepony.kstarbound.lua.userdata.LuaFuture
import ru.dbotthepony.kstarbound.lua.userdata.LuaPathFinder
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.Line2d
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.CarriedExecutor
import ru.dbotthepony.kstarbound.util.GameTimer
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.shuffle
@ -57,6 +63,7 @@ import ru.dbotthepony.kstarbound.world.api.AbstractLiquidState
import ru.dbotthepony.kstarbound.world.castRay
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import ru.dbotthepony.kstarbound.world.entities.PathFinder
import ru.dbotthepony.kstarbound.world.entities.api.ScriptedEntity
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
import ru.dbotthepony.kstarbound.world.physics.CollisionType
@ -82,7 +89,7 @@ private fun ExecutionContext.resolvePolyCollision(self: World<*, *>, originalPol
val tiles = ObjectArrayList<Entry>()
for (tile in self.queryTileCollisions(poly.aabb.enlarge(maximumCorrection + 1.0, maximumCorrection + 1.0))) {
for (tile in self.chunkMap.queryTileCollisions(poly.aabb.enlarge(maximumCorrection + 1.0, maximumCorrection + 1.0))) {
if (tile.type in collisions) {
tiles.add(Entry(tile.poly, tile.poly.centre))
}
@ -185,6 +192,10 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) {
}
}
callbacks["distance"] = luaFunction { arg1: Table, arg2: Table ->
returnBuffer.setTo(from(self.geometry.diff(toVector2d(arg1), toVector2d(arg2))))
}
callbacks["polyContains"] = luaFunction { poly: Table, position: Table ->
returnBuffer.setTo(self.geometry.polyContains(toPoly(poly), toVector2d(position)))
}
@ -217,19 +228,19 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) {
callbacks["rectCollision"] = luaFunction { rect: Table, collisions: Table? ->
if (collisions == null) {
returnBuffer.setTo(self.collide(toPoly(rect), Predicate { it.type.isSolidCollision }).findAny().isPresent)
returnBuffer.setTo(self.chunkMap.collide(toPoly(rect), Predicate { it.type.isSolidCollision }).findAny().isPresent)
} else {
val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList())
returnBuffer.setTo(self.collide(toPoly(rect), Predicate { it.type in actualCollisions }).findAny().isPresent)
returnBuffer.setTo(self.chunkMap.collide(toPoly(rect), Predicate { it.type in actualCollisions }).findAny().isPresent)
}
}
callbacks["pointCollision"] = luaFunction { rect: Table, collisions: Table? ->
if (collisions == null) {
returnBuffer.setTo(self.collide(toVector2d(rect), Predicate { it.type.isSolidCollision }))
returnBuffer.setTo(self.chunkMap.collide(toVector2d(rect), Predicate { it.type.isSolidCollision }))
} else {
val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList())
returnBuffer.setTo(self.collide(toVector2d(rect), Predicate { it.type in actualCollisions }))
returnBuffer.setTo(self.chunkMap.collide(toVector2d(rect), Predicate { it.type in actualCollisions }))
}
}
@ -270,17 +281,18 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) {
callbacks["rectTileCollision"] = luaFunction { rect: Table, collisions: Table? ->
if (collisions == null) {
returnBuffer.setTo(self.anyCellSatisfies(toAABB(rect), World.CellPredicate { x, y, cell -> cell.foreground.material.value.collisionKind.isSolidCollision }))
returnBuffer.setTo(self.chunkMap.anyCellSatisfies(toAABB(rect), World.CellPredicate { x, y, cell -> cell.foreground.material.value.collisionKind.isSolidCollision }))
} else {
val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList())
returnBuffer.setTo(self.anyCellSatisfies(toAABB(rect), World.CellPredicate { x, y, cell -> cell.foreground.material.value.collisionKind in actualCollisions }))
val a = self.chunkMap.anyCellSatisfies(toAABB(rect), World.CellPredicate { x, y, cell -> cell.foreground.material.value.collisionKind in actualCollisions })
returnBuffer.setTo(a)
}
}
callbacks["lineCollision"] = luaFunction { pos0: Table, pos1: Table, collisions: Table? ->
val actualCollisions = if (collisions == null) CollisionType.SOLID else EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList())
val result = self.collide(Line2d(toVector2d(pos0), toVector2d(pos1))) { it.type in actualCollisions }
val result = self.chunkMap.collide(Line2d(toVector2d(pos0), toVector2d(pos1))) { it.type in actualCollisions }
if (result == null) {
returnBuffer.setTo()
@ -291,10 +303,10 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) {
callbacks["polyCollision"] = luaFunction { rect: Table, translate: Table?, collisions: Table? ->
if (collisions == null) {
returnBuffer.setTo(self.collide(toPoly(rect).let { if (translate != null) it + toVector2d(translate) else it }, Predicate { it.type.isSolidCollision }).findAny().isPresent)
returnBuffer.setTo(self.chunkMap.collide(toPoly(rect).let { if (translate != null) it + toVector2d(translate) else it }, Predicate { it.type.isSolidCollision }).findAny().isPresent)
} else {
val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList())
returnBuffer.setTo(self.collide(toPoly(rect).let { if (translate != null) it + toVector2d(translate) else it }, Predicate { it.type in actualCollisions }).findAny().isPresent)
returnBuffer.setTo(self.chunkMap.collide(toPoly(rect).let { if (translate != null) it + toVector2d(translate) else it }, Predicate { it.type in actualCollisions }).findAny().isPresent)
}
}
@ -461,7 +473,7 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) {
callbacks["spawnMonster"] = luaFunction {
// TODO
returnBuffer.setTo(0)
returnBuffer.setTo(0L)
}
callbacks["spawnNpc"] = luaStub("spawnNpc")
callbacks["spawnStagehand"] = luaStub("spawnStagehand")
@ -483,7 +495,7 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) {
}
callbacks["liquidAt"] = luaFunction { posOrRect: Table ->
if (posOrRect[1L] is Number) {
if (posOrRect[3L] !is Number) {
val cell = self.getCell(toVector2i(posOrRect))
if (cell.liquid.state.isNotEmptyLiquid) {
@ -499,7 +511,7 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) {
}
callbacks["liquidNameAt"] = luaFunction { posOrRect: Table ->
if (posOrRect[1L] is Number) {
if (posOrRect[3L] !is Number) {
val cell = self.getCell(toVector2i(posOrRect))
if (cell.liquid.state.isNotEmptyLiquid) {
@ -515,11 +527,11 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) {
}
callbacks["gravity"] = luaFunction { pos: Table ->
returnBuffer.setTo(self.gravityAt(toVector2d(pos)).y)
returnBuffer.setTo(self.chunkMap.gravityAt(toVector2d(pos)).y)
}
callbacks["gravityVector"] = luaFunction { pos: Table ->
returnBuffer.setTo(from(self.gravityAt(toVector2d(pos))))
returnBuffer.setTo(from(self.chunkMap.gravityAt(toVector2d(pos))))
}
callbacks["spawnLiquidPromise"] = luaFunction { pos: Table, liquid: Any, quantity: Number ->
@ -559,10 +571,32 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) {
callbacks["isTileProtected"] = luaFunction { pos: Table -> returnBuffer.setTo(self.isDungeonIDProtected(self.getCell(toVector2i(pos)).dungeonId)) }
callbacks["findPlatformerPath"] = luaStub("findPlatformerPath")
callbacks["platformerPathStart"] = luaStub("platformerPathStart")
callbacks["findPlatformerPath"] = luaFunction { start: Table, end: Table, actorParams: Table, searchParams: Table ->
LOGGER.warn("world.findPlatformerPath() was called, this will cause world lag. Consider switching to world.platformerPathStart(), since it is multithreaded in new engine.")
callbacks["type"] = luaFunction { returnBuffer.setTo(self.template.worldParameters?.typeName ?: "unknown") }
val finder = PathFinder(
self,
toVector2d(start),
toVector2d(end),
Starbound.gson.fromJsonFast(toJsonFromLua(actorParams), ActorMovementParameters::class.java),
Starbound.gson.fromJsonFast(toJsonFromLua(searchParams), PathFinder.Parameters::class.java))
finder.run(Int.MAX_VALUE)
returnBuffer.setTo(LuaPathFinder.convertPath(this, finder.result.orNull()))
}
val pacer = CarriedExecutor(Starbound.EXECUTOR)
callbacks["platformerPathStart"] = luaFunction { start: Table, end: Table, actorParams: Table, searchParams: Table ->
returnBuffer.setTo(LuaPathFinder(pacer, PathFinder(
self,
toVector2d(start),
toVector2d(end),
Starbound.gson.fromJsonFast(toJsonFromLua(actorParams), ActorMovementParameters::class.java),
Starbound.gson.fromJsonFast(toJsonFromLua(searchParams), PathFinder.Parameters::class.java))))
}
callbacks["type"] = luaFunction { returnBuffer.setTo(self.template.worldParameters?.typeName.toByteString() ?: "unknown".toByteString()) }
callbacks["size"] = luaFunction { returnBuffer.setTo(from(self.geometry.size)) }
callbacks["inSurfaceLayer"] = luaFunction { pos: Table -> returnBuffer.setTo(self.template.isSurfaceLayer(toVector2i(pos))) }
callbacks["surfaceLevel"] = luaFunction { returnBuffer.setTo(self.template.surfaceLevel) }
@ -610,4 +644,10 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) {
if (self is ServerWorld) {
provideServerWorldBindings(self, callbacks, lua)
}
// TODO
callbacks["debugPoint"] = luaFunction { }
callbacks["debugLine"] = luaFunction { }
callbacks["debugPoly"] = luaFunction { }
callbacks["debugText"] = luaFunction { }
}

View File

@ -22,6 +22,7 @@ import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.tableMapOf
import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toAABB
import ru.dbotthepony.kstarbound.lua.toByteString
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.lua.toLine2d
import ru.dbotthepony.kstarbound.lua.toPoly
@ -345,12 +346,12 @@ fun provideWorldEntitiesBindings(self: World<*, *>, callbacks: Table, lua: LuaEn
callbacks["entitySpecies"] = luaFunction { id: Number ->
val entity = self.entities[id.toInt()] as? HumanoidActorEntity ?: return@luaFunction returnBuffer.setTo()
returnBuffer.setTo(entity.species)
returnBuffer.setTo(entity.species.toByteString())
}
callbacks["entityGender"] = luaFunction { id: Number ->
val entity = self.entities[id.toInt()] as? HumanoidActorEntity ?: return@luaFunction returnBuffer.setTo()
returnBuffer.setTo(entity.gender.jsonName)
returnBuffer.setTo(entity.gender.jsonName.toByteString())
}
callbacks["entityName"] = luaFunction { id: Number ->
@ -358,8 +359,8 @@ fun provideWorldEntitiesBindings(self: World<*, *>, callbacks: Table, lua: LuaEn
// TODO
when (entity) {
is ActorEntity -> returnBuffer.setTo(entity.name)
is WorldObject -> returnBuffer.setTo(entity.config.key)
is ActorEntity -> returnBuffer.setTo(entity.name.toByteString())
is WorldObject -> returnBuffer.setTo(entity.config.key.toByteString())
}
}
@ -383,8 +384,8 @@ fun provideWorldEntitiesBindings(self: World<*, *>, callbacks: Table, lua: LuaEn
val entity = self.entities[id.toInt()] as? HumanoidActorEntity ?: return@luaFunction returnBuffer.setTo()
when (val gethand = hand.decode().lowercase()) {
"primary" -> returnBuffer.setTo(entity.primaryHandItem.entry.nameOrNull)
"alt", "secondary" -> returnBuffer.setTo(entity.secondaryHandItem.entry.nameOrNull)
"primary" -> returnBuffer.setTo(entity.primaryHandItem.entry.nameOrNull.toByteString())
"alt", "secondary" -> returnBuffer.setTo(entity.secondaryHandItem.entry.nameOrNull.toByteString())
else -> throw LuaRuntimeException("Unknown tool hand $gethand")
}
}

View File

@ -8,6 +8,7 @@ import org.classdump.luna.runtime.ExecutionContext
import ru.dbotthepony.kommons.collect.map
import ru.dbotthepony.kommons.collect.toList
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult
import ru.dbotthepony.kstarbound.defs.tile.TileDamageType
@ -26,6 +27,7 @@ import ru.dbotthepony.kstarbound.lua.nextOptionalInteger
import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.tableFrom
import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toByteString
import ru.dbotthepony.kstarbound.lua.toVector2d
import ru.dbotthepony.kstarbound.lua.toVector2i
import ru.dbotthepony.kstarbound.lua.userdata.LuaFuture
@ -107,7 +109,7 @@ fun provideWorldEnvironmentalBindings(self: World<*, *>, callbacks: Table, lua:
callbacks["windLevel"] = luaStub("windLevel")
callbacks["breathable"] = luaFunction { pos: Table ->
returnBuffer.setTo(self.isBreathable(toVector2i(pos)))
returnBuffer.setTo(self.chunkMap.isBreathable(toVector2i(pos)))
}
callbacks["underground"] = luaFunction { pos: Table ->
@ -123,7 +125,7 @@ fun provideWorldEnvironmentalBindings(self: World<*, *>, callbacks: Table, lua:
} else if (tile.material.isEmptyTile) {
returnBuffer.setTo(false)
} else {
returnBuffer.setTo(tile.material.key)
returnBuffer.setTo(tile.material.key.toByteString())
}
}
@ -132,32 +134,32 @@ fun provideWorldEnvironmentalBindings(self: World<*, *>, callbacks: Table, lua:
val tile = self.getCell(toVector2i(pos)).tile(isBackground)
if (tile.modifier.isNotEmptyModifier) {
returnBuffer.setTo(tile.modifier.key)
returnBuffer.setTo(tile.modifier.key.toByteString())
}
}
callbacks["materialHueShift"] = luaFunction { pos: Table, layer: ByteString ->
val isBackground = isBackground(layer)
val tile = self.getCell(toVector2i(pos)).tile(isBackground)
returnBuffer.setTo(tile.hueShift)
returnBuffer.setTo(tile.hueShift.toDouble())
}
callbacks["modHueShift"] = luaFunction { pos: Table, layer: ByteString ->
val isBackground = isBackground(layer)
val tile = self.getCell(toVector2i(pos)).tile(isBackground)
returnBuffer.setTo(tile.modifierHueShift)
returnBuffer.setTo(tile.modifierHueShift.toDouble())
}
callbacks["materialColor"] = luaFunction { pos: Table, layer: ByteString ->
val isBackground = isBackground(layer)
val tile = self.getCell(toVector2i(pos)).tile(isBackground)
returnBuffer.setTo(tile.color.ordinal)
returnBuffer.setTo(tile.color.ordinal.toLong())
}
callbacks["materialColorName"] = luaFunction { pos: Table, layer: ByteString ->
val isBackground = isBackground(layer)
val tile = self.getCell(toVector2i(pos)).tile(isBackground)
returnBuffer.setTo(tile.color.jsonName)
returnBuffer.setTo(tile.color.jsonName.toByteString())
}
callbacks["setMaterialColor"] = luaFunction { pos: Table, layer: ByteString, color: Any ->
@ -177,11 +179,11 @@ fun provideWorldEnvironmentalBindings(self: World<*, *>, callbacks: Table, lua:
}
callbacks["oceanLevel"] = luaFunction { pos: Table ->
returnBuffer.setTo(self.template.cellInfo(toVector2i(pos)).oceanLiquidLevel)
returnBuffer.setTo(self.template.cellInfo(toVector2i(pos)).oceanLiquidLevel.toLong())
}
callbacks["environmentStatusEffects"] = luaFunction { pos: Table ->
returnBuffer.setTo(tableFrom(self.environmentStatusEffects(toVector2i(pos))))
returnBuffer.setTo(tableFrom(self.environmentStatusEffects(toVector2i(pos)).map { from(Starbound.gson.toJsonTree(it)) }))
}
callbacks["damageTiles"] = luaFunctionN("damageTiles") {

View File

@ -21,12 +21,14 @@ import ru.dbotthepony.kstarbound.lua.iterator
import ru.dbotthepony.kstarbound.lua.luaFunction
import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toByteString
import ru.dbotthepony.kstarbound.lua.toColor
import ru.dbotthepony.kstarbound.lua.toJson
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.lua.toVector2d
import ru.dbotthepony.kstarbound.lua.toVector2i
import ru.dbotthepony.kstarbound.util.SBPattern
import ru.dbotthepony.kstarbound.util.sbIntern
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
@ -48,12 +50,12 @@ fun provideWorldObjectBindings(self: WorldObject, lua: LuaEnvironment) {
val table = lua.newTable()
lua.globals["object"] = table
table["name"] = luaFunction { returnBuffer.setTo(self.config.key) }
table["name"] = luaFunction { returnBuffer.setTo(self.config.key.toByteString()) }
table["direction"] = luaFunction { returnBuffer.setTo(self.direction.luaValue) }
table["position"] = luaFunction { returnBuffer.setTo(from(self.tilePosition)) }
table["setInteractive"] = luaFunction { interactive: Boolean -> self.isInteractive = interactive }
table["uniqueId"] = luaFunction { returnBuffer.setTo(self.uniqueID.get()) }
table["setUniqueId"] = luaFunction { id: ByteString? -> self.uniqueID.accept(id?.decode()) }
table["uniqueId"] = luaFunction { returnBuffer.setTo(self.uniqueID.get().toByteString()) }
table["setUniqueId"] = luaFunction { id: ByteString? -> self.uniqueID.accept(id?.decode()?.sbIntern()) }
table["boundBox"] = luaFunction { returnBuffer.setTo(from(self.metaBoundingBox)) }
// original engine parity, it returns occupied spaces in local coordinates
@ -127,8 +129,8 @@ fun provideWorldObjectBindings(self: WorldObject, lua: LuaEnvironment) {
returnBuffer.setTo(from(self.lightSourceColor))
}
table["inputNodeCount"] = luaFunction { returnBuffer.setTo(self.inputNodes.size) }
table["outputNodeCount"] = luaFunction { returnBuffer.setTo(self.outputNodes.size) }
table["inputNodeCount"] = luaFunction { returnBuffer.setTo(self.inputNodes.size.toLong()) }
table["outputNodeCount"] = luaFunction { returnBuffer.setTo(self.outputNodes.size.toLong()) }
table["getInputNodePosition"] = luaFunction { index: Long ->
returnBuffer.setTo(from(self.inputNodes[index.toInt()].position))

View File

@ -0,0 +1,126 @@
package ru.dbotthepony.kstarbound.lua.userdata
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import org.classdump.luna.ByteString
import org.classdump.luna.LuaRuntimeException
import org.classdump.luna.Table
import org.classdump.luna.Userdata
import org.classdump.luna.impl.ImmutableTable
import org.classdump.luna.runtime.LuaFunction
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.actor.behavior.BehaviorDefinition
import ru.dbotthepony.kstarbound.fromJsonFast
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.iterator
import ru.dbotthepony.kstarbound.lua.luaFunction
import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.tableMapOf
import ru.dbotthepony.kstarbound.lua.toJson
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.world.entities.behavior.AbstractBehaviorNode
import ru.dbotthepony.kstarbound.world.entities.behavior.BehaviorTree
import ru.dbotthepony.kstarbound.world.entities.behavior.Blackboard
import java.util.concurrent.atomic.AtomicLong
class BehaviorState(val tree: BehaviorTree) : Userdata<BehaviorState>() {
val blackboard get() = tree.blackboard
val lua get() = blackboard.lua
val functions = HashMap<String, LuaFunction<*, *, *, *, *>>()
init {
lua.attach(tree.scripts)
for (name in tree.functions) {
val get = lua.globals[name]
if (get == null) {
throw LuaRuntimeException("No such function for behavior: $name")
} else if (get !is LuaFunction<*, *, *, *, *>) {
throw LuaRuntimeException("Not a Lua function for behavior: $name")
} else {
this.functions[name] = get
}
}
}
fun run(delta: Double): AbstractBehaviorNode.Status {
val ephemerals = blackboard.takeEphemerals()
val status = tree.runAndReset(delta, this)
blackboard.clearEphemerals(ephemerals)
return status
}
override fun getMetatable(): Table {
return Companion.metatable
}
override fun setMetatable(mt: Table?): Table {
throw UnsupportedOperationException()
}
override fun getUserValue(): BehaviorState {
return this
}
override fun setUserValue(value: BehaviorState?): BehaviorState? {
throw UnsupportedOperationException()
}
companion object {
// required for Lua code
// in original engine, passes directly 32/64 bit pointer of *Node struct
val NODE_GARBAGE_INDEX = AtomicLong()
private fun __index(): Table {
return metatable
}
private val metatable = ImmutableTable.Builder()
.add("__index", luaFunction { _: Any?, index: Any -> returnBuffer.setTo(__index()[index]) })
.add("run", luaFunction { self: BehaviorState, delta: Number ->
returnBuffer.setTo(self.run(delta.toDouble()))
})
.add("clear", luaFunction { self: BehaviorState ->
self.tree.reset()
})
.add("blackboard", luaFunction { self: BehaviorState ->
returnBuffer.setTo(self.blackboard)
})
.build()
fun provideBindings(lua: LuaEnvironment) {
lua.globals["behavior"] = lua.tableMapOf(
"behavior" to luaFunction { config: Any, parameters: Table, _: Any?, blackboard: Blackboard? ->
val tree: BehaviorTree
if (config is ByteString) {
val base = Registries.behavior.getOrThrow(config.decode())
if (!parameters.iterator().hasNext()) {
tree = BehaviorTree(blackboard ?: Blackboard(lua), base.value)
} else {
tree = BehaviorTree(blackboard ?: Blackboard(lua), Starbound.gson.fromJsonFast(base.json.deepCopy().also {
it as JsonObject
it["parameters"] = mergeJson(it["parameters"] ?: JsonNull.INSTANCE, toJsonFromLua(parameters))
}, BehaviorDefinition::class.java))
}
} else {
val cast = (config as Table).toJson(true) as JsonObject
if (parameters.iterator().hasNext())
cast["parameters"] = mergeJson(cast["parameters"] ?: JsonNull.INSTANCE, toJsonFromLua(parameters))
tree = BehaviorTree(blackboard ?: Blackboard(lua), Starbound.gson.fromJsonFast(cast, BehaviorDefinition::class.java))
}
returnBuffer.setTo(BehaviorState(tree))
}
)
}
}
}

View File

@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.lua.userdata
import org.classdump.luna.Table
import org.classdump.luna.Userdata
import org.classdump.luna.impl.ImmutableTable
import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.luaFunction
import java.util.concurrent.CancellationException
import java.util.concurrent.CompletableFuture
@ -32,7 +33,12 @@ class LuaFuture(val future: CompletableFuture<out Any?>, val isLocal: Boolean) :
}
companion object {
private fun __index(): Table {
return metadata
}
private val metadata = ImmutableTable.Builder()
.add("__index", luaFunction { _: Any?, index: Any -> returnBuffer.setTo(__index()[index]) })
.add("finished", luaFunction { self: LuaFuture ->
returnBuffer.setTo(self.future.isDone)
})

View File

@ -0,0 +1,101 @@
package ru.dbotthepony.kstarbound.lua.userdata
import org.classdump.luna.Table
import org.classdump.luna.TableFactory
import org.classdump.luna.Userdata
import org.classdump.luna.impl.ImmutableTable
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.lua.from
import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.luaFunction
import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toByteString
import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState.Companion
import ru.dbotthepony.kstarbound.util.CarriedExecutor
import ru.dbotthepony.kstarbound.util.supplyAsync
import ru.dbotthepony.kstarbound.world.entities.PathFinder
import java.util.concurrent.CompletableFuture
class LuaPathFinder(val pacer: CarriedExecutor, val self: PathFinder) : Userdata<PathFinder>() {
override fun getMetatable(): Table {
return Companion.metatable
}
override fun setMetatable(mt: Table?): Table {
throw UnsupportedOperationException()
}
override fun getUserValue(): PathFinder {
return self
}
override fun setUserValue(value: PathFinder?): PathFinder {
throw UnsupportedOperationException()
}
private var isRunning = false
companion object {
private val cost = "const".toByteString()!!
private val action = "action".toByteString()!!
private val jumpVelocity = "jumpVelocity".toByteString()!!
private val source = "source".toByteString()!!
private val target = "target".toByteString()!!
fun convertPath(tables: TableFactory, list: List<PathFinder.Edge>?): Table? {
list ?: return null
val table = tables.newTable(list.size, 0)
var i = 1L
for (edge in list) {
val edgeTable = tables.tableOf()
edgeTable[cost] = edge.cost
edgeTable[action] = edge.action.luaName
edgeTable[jumpVelocity] = tables.from(edge.velocity)
edgeTable[source] = edge.source.toTable(tables)
edgeTable[target] = edge.target.toTable(tables)
table[i++] = edgeTable
}
return table
}
private fun __index(): Table {
return metatable
}
private val metatable = ImmutableTable.Builder()
.add("__index", luaFunction { _: Any?, index: Any -> returnBuffer.setTo(__index()[index]) })
.add("explore", luaFunction { self: LuaPathFinder, maxExplore: Number? ->
if (self.isRunning) {
if (self.self.result.isEmpty)
returnBuffer.setTo(null)
else
returnBuffer.setTo(self.self.result.value != null)
} else if (self.self.result.isPresent) {
returnBuffer.setTo(self.self.result.value != null)
} else {
val immediateMaxExplore = maxExplore?.toInt()?.times(4)?.coerceAtMost(800) ?: 800
val result = self.self.run(immediateMaxExplore)
if (result != null) {
returnBuffer.setTo(result)
} else if (self.self.result.isEmpty) {
// didn't explore enough, run in separate thread to not block main thread
self.pacer.supplyAsync(self.self)
self.isRunning = true
returnBuffer.setTo(null)
}
}
})
.add("result", luaFunction { self: LuaPathFinder ->
if (self.self.result.isEmpty)
return@luaFunction returnBuffer.setTo()
val list = self.self.result.value ?: return@luaFunction returnBuffer.setTo()
returnBuffer.setTo(convertPath(this, list))
})
.build()
}
}

View File

@ -3,7 +3,9 @@ package ru.dbotthepony.kstarbound.lua.userdata
import org.classdump.luna.Table
import org.classdump.luna.Userdata
import org.classdump.luna.impl.ImmutableTable
import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.luaFunction
import ru.dbotthepony.kstarbound.lua.userdata.LuaPathFinder.Companion
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
class LuaPerlinNoise(val noise: AbstractPerlinNoise) : Userdata<AbstractPerlinNoise>() {
@ -28,7 +30,12 @@ class LuaPerlinNoise(val noise: AbstractPerlinNoise) : Userdata<AbstractPerlinNo
}
companion object {
private fun __index(): Table {
return metatable
}
private val metatable = ImmutableTable.Builder()
.add("__index", luaFunction { _: Any?, index: Any -> returnBuffer.setTo(__index()[index]) })
.add("get", luaFunction { self: LuaPerlinNoise, x: Number, y: Number?, z: Number? ->
if (y != null && z != null) {
returnBuffer.setTo(self.noise[x.toDouble(), y.toDouble(), z.toDouble()])

View File

@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.lua.userdata
import org.classdump.luna.Table
import org.classdump.luna.Userdata
import org.classdump.luna.impl.ImmutableTable
import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.luaFunction
import ru.dbotthepony.kstarbound.util.random.nextNormalDouble
import ru.dbotthepony.kstarbound.util.random.random
@ -63,7 +64,12 @@ class LuaRandomGenerator(var random: RandomGenerator) : Userdata<RandomGenerator
}
companion object {
private fun __index(): Table {
return metatable
}
private val metatable = ImmutableTable.Builder()
.add("__index", luaFunction { _: Any?, index: Any -> returnBuffer.setTo(__index()[index]) })
.add("init", luaFunction { self: LuaRandomGenerator, seed: Long? ->
self.random = random(seed ?: System.nanoTime())
})

View File

@ -47,6 +47,11 @@ data class AABB(val mins: Vector2d, val maxs: Vector2d) {
operator fun times(other: Vector2d) = AABB(mins * other, maxs * other)
operator fun div(other: Vector2d) = AABB(mins / other, maxs / other)
operator fun plus(other: Vector2i) = AABB(mins + other, maxs + other)
operator fun minus(other: Vector2i) = AABB(mins - other, maxs - other)
operator fun times(other: Vector2i) = AABB(mins * other, maxs * other)
operator fun div(other: Vector2i) = AABB(mins / other, maxs / other)
operator fun times(other: Double) = AABB(mins * other, maxs * other)
operator fun div(other: Double) = AABB(mins / other, maxs / other)
@ -225,9 +230,15 @@ data class AABB(val mins: Vector2d, val maxs: Vector2d) {
)
}
fun padded(x: Double, y: Double): AABB {
return AABB(
mins - Vector2d(x, y),
maxs + Vector2d(x, y)
)
}
fun expand(value: IStruct2d) = expand(value.component1(), value.component2())
fun padded(value: IStruct2d) = expand(value.component1(), value.component2())
fun padded(x: Double, y: Double) = expand(x, y)
fun padded(value: IStruct2d) = padded(value.component1(), value.component2())
/**
* Returns AABB which edges are expanded by [x] and [y] along their normals
@ -308,6 +319,13 @@ data class AABB(val mins: Vector2d, val maxs: Vector2d) {
)
}
fun of(aabb: AABBi): AABB {
return AABB(
mins = Vector2d(aabb.mins.x.toDouble(), aabb.mins.y.toDouble()),
maxs = Vector2d(aabb.maxs.x.toDouble(), aabb.maxs.y.toDouble()),
)
}
@JvmField val ZERO = AABB(Vector2d.ZERO, Vector2d.ZERO)
@JvmField val NEVER = AABB(Vector2d(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY), Vector2d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY))
}

View File

@ -4,6 +4,7 @@ import ru.dbotthepony.kommons.math.intersectRectangles
import ru.dbotthepony.kommons.math.rectangleContainsRectangle
import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import kotlin.math.roundToInt
data class AABBi(val mins: Vector2i, val maxs: Vector2i) {
init {
@ -99,8 +100,14 @@ data class AABBi(val mins: Vector2i, val maxs: Vector2i) {
)
}
fun padded(x: Int, y: Int) = expand(x, y)
fun padded(value: IStruct2i) = expand(value.component1(), value.component2())
fun padded(x: Int, y: Int): AABBi {
return AABBi(
mins - Vector2i(x, y),
maxs + Vector2i(x, y)
)
}
fun padded(value: IStruct2i) = padded(value.component1(), value.component2())
fun expand(value: IStruct2i) = expand(value.component1(), value.component2())
/**
@ -151,5 +158,19 @@ data class AABBi(val mins: Vector2i, val maxs: Vector2i) {
}
@JvmField val ZERO = AABBi(Vector2i.ZERO, Vector2i.ZERO)
fun of(aabb: AABB): AABBi {
return AABBi(
Vector2i(aabb.mins.x.toInt(), aabb.mins.y.toInt()),
Vector2i(aabb.maxs.x.toInt(), aabb.maxs.y.toInt()),
)
}
fun ofRound(aabb: AABB): AABBi {
return AABBi(
Vector2i(aabb.mins.x.roundToInt(), aabb.mins.y.roundToInt()),
Vector2i(aabb.maxs.x.roundToInt(), aabb.maxs.y.roundToInt()),
)
}
}
}

View File

@ -69,6 +69,9 @@ data class Line2d(val p0: Vector2d, val p1: Vector2d) {
val difference: Vector2d
get() = p1 - p0
val direction: Vector2d
get() = difference.unitVector
fun reverse(): Line2d {
return Line2d(p1, p0)
}
@ -157,6 +160,13 @@ data class Line2d(val p0: Vector2d, val p1: Vector2d) {
return ((x - p0.x) * diff.x + (y - p0.y) * diff.y) / diff.lengthSquared
}
fun rotate(angle: Double): Line2d {
if (angle == 0.0)
return this
return Line2d(p0.rotate(angle), p1.rotate(angle))
}
fun distanceTo(other: IStruct2d, infinite: Boolean = false): Double {
var proj = project(other)

View File

@ -186,6 +186,9 @@ data class Vector2d(
fun coerceAtMost(value: Vector2d): Vector2d { val (x, y) = value; return coerceAtMost(x, y) }
fun rotate(angle: Double): Vector2d {
if (angle == 0.0)
return this
val s = sin(angle)
val c = cos(angle)
@ -193,6 +196,9 @@ data class Vector2d(
}
fun toAngle(zeroAngle: Vector2d): Double {
if (x == 0.0 && y == 0.0)
return 0.0 // assume angle is zero
val dot = unitVector.dot(zeroAngle.unitVector)
return if (y > 0.0) {

View File

@ -17,7 +17,6 @@ import ru.dbotthepony.kstarbound.client.network.packets.ForgetChunkPacket
import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket
import ru.dbotthepony.kstarbound.client.network.packets.ForgetEntityPacket
import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket
import ru.dbotthepony.kstarbound.client.network.packets.SpawnWorldObjectPacket
import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.DamageNotificationPacket
import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket
@ -399,7 +398,6 @@ class PacketRegistry(val isLegacy: Boolean) {
NATIVE.add(::JoinWorldPacket)
NATIVE.add(::ChunkCellsPacket)
NATIVE.add(::ForgetChunkPacket)
NATIVE.add(::SpawnWorldObjectPacket)
NATIVE.add(::ForgetEntityPacket)
NATIVE.add(::UniverseTimeUpdatePacket)
NATIVE.add(::StepUpdatePacket)

View File

@ -4,9 +4,7 @@ import it.unimi.dsi.fastutil.bytes.ByteArrayList
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import ru.dbotthepony.kommons.io.ByteKey
import ru.dbotthepony.kommons.io.readByteArray
import ru.dbotthepony.kommons.io.readByteKey
import ru.dbotthepony.kommons.io.readCollection
import ru.dbotthepony.kommons.io.readKOptional
import ru.dbotthepony.kommons.io.readMap
@ -17,6 +15,8 @@ import ru.dbotthepony.kommons.io.writeMap
import ru.dbotthepony.kommons.io.writeVarInt
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.io.ByteKey
import ru.dbotthepony.kstarbound.io.readByteKey
import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.network.ConnectionSide
import ru.dbotthepony.kstarbound.network.IClientPacket

View File

@ -1,21 +1,21 @@
package ru.dbotthepony.kstarbound.network.packets.serverbound
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.ByteKey
import ru.dbotthepony.kommons.io.readBinaryString
import ru.dbotthepony.kommons.io.readByteArray
import ru.dbotthepony.kommons.io.readByteKey
import ru.dbotthepony.kommons.io.readKOptional
import ru.dbotthepony.kommons.io.readMap
import ru.dbotthepony.kommons.io.readUUID
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeByteArray
import ru.dbotthepony.kommons.io.writeByteKey
import ru.dbotthepony.kommons.io.writeKOptional
import ru.dbotthepony.kommons.io.writeMap
import ru.dbotthepony.kommons.io.writeUUID
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.defs.actor.player.ShipUpgrades
import ru.dbotthepony.kstarbound.io.ByteKey
import ru.dbotthepony.kstarbound.io.readByteKey
import ru.dbotthepony.kstarbound.io.writeByteKey
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.ConnectFailurePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.ConnectSuccessPacket

View File

@ -42,11 +42,16 @@ open class BasicNetworkedElement<TYPE, LEGACY>(private var value: TYPE, protecte
val old = value
value = t
queue.clear()
bumpVersion()
super.bumpVersion()
valueListeners.forEach { it.callable.invoke(t, old) }
}
}
override fun bumpVersion() {
super.bumpVersion()
valueListeners.forEach { it.callable.invoke(value, value) }
}
override fun addListener(listener: Consumer<TYPE>): Listenable.L {
return Listener(listener)
}
@ -68,7 +73,7 @@ open class BasicNetworkedElement<TYPE, LEGACY>(private var value: TYPE, protecte
val old = value
value = if (isLegacy) fromLegacy(legacyCodec.read(data)) else codec.read(data)
queue.clear()
bumpVersion()
super.bumpVersion()
if (value != old) {
valueListeners.forEach { it.callable.invoke(value, old) }
@ -85,7 +90,7 @@ open class BasicNetworkedElement<TYPE, LEGACY>(private var value: TYPE, protecte
override fun readDelta(data: DataInputStream, interpolationDelay: Double, isLegacy: Boolean) {
val read = if (isLegacy) fromLegacy(legacyCodec.read(data)) else codec.read(data)
bumpVersion()
super.bumpVersion()
if (isInterpolating) {
queue.push(read, interpolationDelay)

View File

@ -77,6 +77,13 @@ class NetworkedMap<K, V>(
})
}
// forces re-network of specified index
fun markIndexDirty(index: K) {
if (index in this) {
backlog.add(currentVersion() to Entry(Action.ADD, KOptional(nativeKey.copy(index)), KOptional(nativeValue.copy(this[index] as V)))) // not null!! assert because V might be nullable
}
}
private val listeners = CopyOnWriteArrayList<Listener>()
private inner class Listener(val listener: ListenableMap.MapListener<K, V>) : Listenable.L {

View File

@ -19,6 +19,15 @@ class ChatHandler(val server: StarboundServer) {
))
}
fun systemMessage(target: ServerConnection, string: String) {
target.send(ChatReceivePacket(
ChatMessage(
MessageContext.BROADCAST,
text = string
)
))
}
fun handle(source: ServerConnection, packet: ChatSendPacket) {
when (packet.mode) {
ChatSendMode.BROADCAST -> {

View File

@ -12,11 +12,7 @@ class IntegratedStarboundServer(val client: StarboundClient, root: File) : Starb
override fun tick0(delta: Double) {
if (client.isShutdown) {
shutdown()
close()
}
}
override fun close0() {
}
}

View File

@ -17,6 +17,7 @@ import java.io.Closeable
import java.net.SocketAddress
import java.util.*
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
@ -112,15 +113,18 @@ class ServerChannels(val server: StarboundServer) : Closeable {
val channel = ServerBootstrap().channel(LocalServerChannel::class.java).group(Connection.NIO_POOL).childHandler(object : ChannelInitializer<Channel>() {
override fun initChannel(ch: Channel) {
LOGGER.info("Incoming connection from ${ch.remoteAddress()}")
lock.withLock {
if (isClosed) return
LOGGER.info("Incoming connection from ${ch.remoteAddress()}")
try {
val connection = ServerConnection(server, ConnectionType.MEMORY)
connections.add(connection)
connection.bind(ch)
} catch (err: Throwable) {
LOGGER.error("Error while accepting new connection from ${ch.remoteAddress()}", err)
ch.close()
try {
val connection = ServerConnection(server, ConnectionType.MEMORY)
connections.add(connection)
connection.bind(ch)
} catch (err: Throwable) {
LOGGER.error("Error while accepting new connection from ${ch.remoteAddress()}", err)
ch.close()
}
}
}
}).bind(LocalAddress.ANY).syncUninterruptibly()
@ -141,15 +145,18 @@ class ServerChannels(val server: StarboundServer) : Closeable {
lock.withLock {
val channel = ServerBootstrap().channel(NioServerSocketChannel::class.java).group(Connection.NIO_POOL).childHandler(object : ChannelInitializer<Channel>() {
override fun initChannel(ch: Channel) {
LOGGER.info("Incoming connection from ${ch.remoteAddress()}")
lock.withLock {
if (isClosed) return
LOGGER.info("Incoming connection from ${ch.remoteAddress()}")
try {
val connection = ServerConnection(server, ConnectionType.NETWORK)
connections.add(connection)
connection.bind(ch)
} catch (err: Throwable) {
LOGGER.error("Error while accepting new connection from ${ch.remoteAddress()}", err)
ch.close()
try {
val connection = ServerConnection(server, ConnectionType.NETWORK)
connections.add(connection)
connection.bind(ch)
} catch (err: Throwable) {
LOGGER.error("Error while accepting new connection from ${ch.remoteAddress()}", err)
ch.close()
}
}
}
}).bind(localAddress).syncUninterruptibly()
@ -168,9 +175,18 @@ class ServerChannels(val server: StarboundServer) : Closeable {
override fun close() {
lock.withLock {
if (isClosed) return
isClosed = true
connections.forEach { it.disconnect("Server shutting down") }
channels.forEach { it.channel().close() }
connections.forEach { it.disconnect("Server shutting down") }
// aeugh
connections.forEach {
if (!it.waitDisconnect(2L, TimeUnit.SECONDS)) {
LOGGER.warn("Giving up waiting for $it to disconnect")
}
}
channels.clear()
connections.clear()
}

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