Avatar, QuestInstance, серьёзные улучшения Lua

This commit is contained in:
DBotThePony 2023-03-02 15:15:54 +07:00
parent 61b21a595f
commit b0e978b2d1
Signed by: DBot
GPG Key ID: DCC23B5715498507
32 changed files with 2324 additions and 253 deletions

View File

@ -25,7 +25,7 @@ application {
mainClass.set("ru.dbotthepony.kstarbound.MainKt") mainClass.set("ru.dbotthepony.kstarbound.MainKt")
} }
java.toolchain.languageVersion.set(JavaLanguageVersion.of(20)) java.toolchain.languageVersion.set(JavaLanguageVersion.of(17))
tasks.compileKotlin { tasks.compileKotlin {
kotlinOptions { kotlinOptions {

View File

@ -12,7 +12,7 @@ static int lua_jniFunc(lua_State *state) {
jint result = (*env)->CallIntMethod(env, *lua_JCClosure, callback, (long long) state); jint result = (*env)->CallIntMethod(env, *lua_JCClosure, callback, (long long) state);
if (result != 0) { if (result <= -1) {
const char* errMsg = lua_tostring(state, -1); const char* errMsg = lua_tostring(state, -1);
if (errMsg == NULL) if (errMsg == NULL)
@ -21,7 +21,7 @@ static int lua_jniFunc(lua_State *state) {
return luaL_error(state, "%s", errMsg); return luaL_error(state, "%s", errMsg);
} }
return 0; return result;
} }
static int mark_closure_free(lua_State *state) { static int mark_closure_free(lua_State *state) {

View File

@ -32,7 +32,7 @@ public interface LuaJNR {
public void luaopen_debug(@NotNull Pointer luaState); public void luaopen_debug(@NotNull Pointer luaState);
@Nullable @Nullable
public Pointer lua_tolstring(@NotNull Pointer luaState, int index, @NotNull Pointer size); public Pointer lua_tolstring(@NotNull Pointer luaState, int index, @LongLong long size);
public int lua_load(@NotNull Pointer luaState, @LongLong long reader, long userData, @NotNull String chunkName, @NotNull String mode); public int lua_load(@NotNull Pointer luaState, @LongLong long reader, long userData, @NotNull String chunkName, @NotNull String mode);
@ -97,6 +97,7 @@ public interface LuaJNR {
public int lua_toboolean(@NotNull Pointer luaState, int index); public int lua_toboolean(@NotNull Pointer luaState, int index);
public int lua_tocfunction(@NotNull Pointer luaState, int index); public int lua_tocfunction(@NotNull Pointer luaState, int index);
public int lua_toclose(@NotNull Pointer luaState, int index); public int lua_toclose(@NotNull Pointer luaState, int index);
public int lua_gettable(@NotNull Pointer luaState, int index);
@LongLong @LongLong
public long lua_tointegerx(@NotNull Pointer luaState, int index, @LongLong long successCode); public long lua_tointegerx(@NotNull Pointer luaState, int index, @LongLong long successCode);

View File

@ -1,20 +1,20 @@
package ru.dbotthepony.kstarbound package ru.dbotthepony.kstarbound
import com.google.gson.GsonBuilder
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import com.sun.jna.Native
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import org.lwjgl.Version import org.lwjgl.Version
import org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose import org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose
import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.render.Animator import ru.dbotthepony.kstarbound.client.render.Animator
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
import ru.dbotthepony.kstarbound.defs.item.DynamicItemDefinition
import ru.dbotthepony.kstarbound.io.BTreeDB import ru.dbotthepony.kstarbound.io.BTreeDB
import ru.dbotthepony.kstarbound.lua.LuaState import ru.dbotthepony.kstarbound.lua.LuaState
import ru.dbotthepony.kstarbound.lua.LuaType
import ru.dbotthepony.kstarbound.player.Avatar
import ru.dbotthepony.kstarbound.player.Player
import ru.dbotthepony.kstarbound.player.QuestDescriptor
import ru.dbotthepony.kstarbound.player.QuestInstance
import ru.dbotthepony.kstarbound.util.JVMTimeSource
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.entities.ItemEntity import ru.dbotthepony.kstarbound.world.entities.ItemEntity
import ru.dbotthepony.kstarbound.world.entities.PlayerEntity import ru.dbotthepony.kstarbound.world.entities.PlayerEntity
@ -22,25 +22,12 @@ import ru.dbotthepony.kvector.vector.ndouble.Vector2d
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.DataInputStream import java.io.DataInputStream
import java.io.File import java.io.File
import java.util.Random import java.util.*
import java.util.zip.Inflater import java.util.zip.Inflater
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
fun main() { fun main() {
if (true) {
val lua = LuaState()
lua.load("return {lemon = 'Bouncy', [4] = 'four', [3] = 'watermelon', h = {h = 'h!'}}")
lua.call(numResults = 1)
val a = lua.popTable()!!
println("${a["lemon"]} $a")
return
}
val starbound = Starbound() val starbound = Starbound()
LOGGER.info("Running LWJGL ${Version.getVersion()}") LOGGER.info("Running LWJGL ${Version.getVersion()}")
@ -217,8 +204,22 @@ fun main() {
val animator = Animator(client.world!!, def) val animator = Animator(client.world!!, def)
//client.onPostDrawWorld {
// animator.render(client.gl.matrixStack)
//}
val avatar = Avatar(starbound, UUID.randomUUID())
val quest = QuestInstance(avatar, descriptor = QuestDescriptor("floran_mission1"))
quest.init()
quest.start()
var last = JVMTimeSource.INSTANCE.millis
client.onPostDrawWorld { client.onPostDrawWorld {
animator.render(client.gl.matrixStack) if (JVMTimeSource.INSTANCE.millis - last > 20L) {
quest.update(1)
last = JVMTimeSource.INSTANCE.millis
}
} }
} }

View File

@ -28,11 +28,19 @@ class RegistryObject<T : Any>(val value: T, private val json: JsonObject, val fi
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
return other is RegistryObject<*> && other.value == value && other.json == json return other === this || other is RegistryObject<*> && other.value == value && other.json == json
} }
private var computedHash = false
private var hash = 0
override fun hashCode(): Int { override fun hashCode(): Int {
return value.hashCode().rotateRight(13) xor json.hashCode() if (!computedHash) {
hash = value.hashCode().rotateRight(13) xor json.hashCode()
computedHash = true
}
return hash
} }
override fun toString(): String { override fun toString(): String {

View File

@ -32,6 +32,8 @@ import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.particle.ParticleDefinition import ru.dbotthepony.kstarbound.defs.particle.ParticleDefinition
import ru.dbotthepony.kstarbound.defs.player.BlueprintLearnList import ru.dbotthepony.kstarbound.defs.player.BlueprintLearnList
import ru.dbotthepony.kstarbound.defs.player.PlayerDefinition import ru.dbotthepony.kstarbound.defs.player.PlayerDefinition
import ru.dbotthepony.kstarbound.defs.player.TechDefinition
import ru.dbotthepony.kstarbound.defs.quest.QuestTemplate
import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.io.* import ru.dbotthepony.kstarbound.io.*
@ -40,6 +42,7 @@ import ru.dbotthepony.kstarbound.io.json.AABBiTypeAdapter
import ru.dbotthepony.kstarbound.io.json.EitherTypeAdapter import ru.dbotthepony.kstarbound.io.json.EitherTypeAdapter
import ru.dbotthepony.kstarbound.io.json.InternedJsonElementAdapter import ru.dbotthepony.kstarbound.io.json.InternedJsonElementAdapter
import ru.dbotthepony.kstarbound.io.json.InternedStringAdapter import ru.dbotthepony.kstarbound.io.json.InternedStringAdapter
import ru.dbotthepony.kstarbound.io.json.LongRangeAdapter
import ru.dbotthepony.kstarbound.io.json.NothingAdapter import ru.dbotthepony.kstarbound.io.json.NothingAdapter
import ru.dbotthepony.kstarbound.io.json.Vector2dTypeAdapter import ru.dbotthepony.kstarbound.io.json.Vector2dTypeAdapter
import ru.dbotthepony.kstarbound.io.json.Vector2fTypeAdapter import ru.dbotthepony.kstarbound.io.json.Vector2fTypeAdapter
@ -52,10 +55,14 @@ import ru.dbotthepony.kstarbound.io.json.builder.FactoryAdapter
import ru.dbotthepony.kstarbound.io.json.builder.JsonImplementationTypeFactory import ru.dbotthepony.kstarbound.io.json.builder.JsonImplementationTypeFactory
import ru.dbotthepony.kstarbound.io.json.factory.ArrayListAdapterFactory import ru.dbotthepony.kstarbound.io.json.factory.ArrayListAdapterFactory
import ru.dbotthepony.kstarbound.io.json.factory.ImmutableCollectionAdapterFactory import ru.dbotthepony.kstarbound.io.json.factory.ImmutableCollectionAdapterFactory
import ru.dbotthepony.kstarbound.lua.LuaState
import ru.dbotthepony.kstarbound.lua.loadInternalScript
import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.math.*
import ru.dbotthepony.kstarbound.util.ItemDescriptor
import ru.dbotthepony.kstarbound.util.PathStack import ru.dbotthepony.kstarbound.util.PathStack
import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.util.SBPattern
import ru.dbotthepony.kstarbound.util.WriteOnce import ru.dbotthepony.kstarbound.util.WriteOnce
import ru.dbotthepony.kstarbound.util.traverseJsonPath
import java.io.* import java.io.*
import java.text.DateFormat import java.text.DateFormat
import java.util.* import java.util.*
@ -69,8 +76,7 @@ import kotlin.collections.ArrayList
class Starbound : ISBFileLocator { class Starbound : ISBFileLocator {
private val logger = LogManager.getLogger() private val logger = LogManager.getLogger()
val stringInterner: Interner<String> = Interners.newWeakInterner() val pathStack = PathStack(Starbound.STRINGS)
val pathStack = PathStack(stringInterner)
private val _tiles = ObjectRegistry("tiles", TileDefinition::materialName, TileDefinition::materialId) private val _tiles = ObjectRegistry("tiles", TileDefinition::materialName, TileDefinition::materialId)
val tiles = _tiles.view val tiles = _tiles.view
@ -96,14 +102,20 @@ class Starbound : ISBFileLocator {
private val _items = ObjectRegistry("items", IItemDefinition::itemName) private val _items = ObjectRegistry("items", IItemDefinition::itemName)
val items = _items.view val items = _items.view
private val _questTemplates = ObjectRegistry("quest templates", QuestTemplate::id)
val questTemplates = _questTemplates.view
private val _techs = ObjectRegistry("techs", TechDefinition::name)
val techs = _techs.view
val gson: Gson = with(GsonBuilder()) { val gson: Gson = with(GsonBuilder()) {
serializeNulls() serializeNulls()
setDateFormat(DateFormat.LONG) setDateFormat(DateFormat.LONG)
setFieldNamingPolicy(FieldNamingPolicy.IDENTITY) setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
setPrettyPrinting() setPrettyPrinting()
registerTypeAdapter(InternedStringAdapter(stringInterner)) registerTypeAdapter(InternedStringAdapter(STRINGS))
registerTypeAdapter(InternedJsonElementAdapter(stringInterner)) registerTypeAdapter(InternedJsonElementAdapter(STRINGS))
registerTypeAdapter(Nothing::class.java, NothingAdapter) registerTypeAdapter(Nothing::class.java, NothingAdapter)
@ -111,7 +123,7 @@ class Starbound : ISBFileLocator {
registerTypeAdapterFactory(JsonImplementationTypeFactory) registerTypeAdapterFactory(JsonImplementationTypeFactory)
// ImmutableList, ImmutableSet, ImmutableMap // ImmutableList, ImmutableSet, ImmutableMap
registerTypeAdapterFactory(ImmutableCollectionAdapterFactory(stringInterner)) registerTypeAdapterFactory(ImmutableCollectionAdapterFactory(STRINGS))
// ArrayList // ArrayList
registerTypeAdapterFactory(ArrayListAdapterFactory) registerTypeAdapterFactory(ArrayListAdapterFactory)
@ -120,10 +132,10 @@ class Starbound : ISBFileLocator {
registerTypeAdapterFactory(EnumAdapter.Companion) registerTypeAdapterFactory(EnumAdapter.Companion)
// @JsonBuilder // @JsonBuilder
registerTypeAdapterFactory(BuilderAdapter.Factory(stringInterner)) registerTypeAdapterFactory(BuilderAdapter.Factory(STRINGS))
// @JsonFactory // @JsonFactory
registerTypeAdapterFactory(FactoryAdapter.Factory(stringInterner)) registerTypeAdapterFactory(FactoryAdapter.Factory(STRINGS))
// Either<> // Either<>
registerTypeAdapterFactory(EitherTypeAdapter) registerTypeAdapterFactory(EitherTypeAdapter)
@ -150,7 +162,7 @@ class Starbound : ISBFileLocator {
registerTypeAdapter(JsonFunction.Companion) registerTypeAdapter(JsonFunction.Companion)
// Общее // Общее
registerTypeAdapterFactory(ThingDescription.Factory(stringInterner)) registerTypeAdapterFactory(ThingDescription.Factory(STRINGS))
registerTypeAdapter(EnumAdapter(DamageType::class, default = DamageType.NORMAL)) registerTypeAdapter(EnumAdapter(DamageType::class, default = DamageType.NORMAL))
@ -162,6 +174,8 @@ class Starbound : ISBFileLocator {
registerTypeAdapterFactory(AssetReferenceFactory(pathStack, this@Starbound)) registerTypeAdapterFactory(AssetReferenceFactory(pathStack, this@Starbound))
registerTypeAdapter(ItemDescriptor.Adapter(this@Starbound))
registerTypeAdapterFactory(with(RegistryReferenceFactory()) { registerTypeAdapterFactory(with(RegistryReferenceFactory()) {
add(tiles::get) add(tiles::get)
add(tileModifiers::get) add(tileModifiers::get)
@ -172,11 +186,49 @@ class Starbound : ISBFileLocator {
add(particles::get) add(particles::get)
}) })
.create() registerTypeAdapter(LongRangeAdapter)
create()
} }
val atlasRegistry = AtlasConfiguration.Registry(this, pathStack, gson) val atlasRegistry = AtlasConfiguration.Registry(this, pathStack, gson)
fun item(name: String): ItemDescriptor {
return ItemDescriptor(items[name] ?: return ItemDescriptor.EMPTY)
}
fun item(name: String, count: Long): ItemDescriptor {
if (count <= 0L)
return ItemDescriptor.EMPTY
return ItemDescriptor(items[name] ?: return ItemDescriptor.EMPTY, count = count)
}
fun item(name: String, count: Long, parameters: JsonObject): ItemDescriptor {
if (count <= 0L)
return ItemDescriptor.EMPTY
return ItemDescriptor(items[name] ?: return ItemDescriptor.EMPTY, count = count, parameters = parameters)
}
fun item(descriptor: JsonObject): ItemDescriptor {
return item(
(descriptor["name"] as? JsonPrimitive)?.asString ?: return ItemDescriptor.EMPTY,
descriptor["count"]?.asLong ?: return ItemDescriptor.EMPTY,
(descriptor["parameters"] as? JsonObject)?.deepCopy() ?: JsonObject()
)
}
fun item(descriptor: JsonElement?): ItemDescriptor {
if (descriptor is JsonPrimitive) {
return item(descriptor.asString)
} else if (descriptor is JsonObject) {
return item(descriptor)
} else {
return ItemDescriptor.EMPTY
}
}
var initializing = false var initializing = false
private set private set
var initialized = false var initialized = false
@ -185,6 +237,70 @@ class Starbound : ISBFileLocator {
@Volatile @Volatile
var terminateLoading = false var terminateLoading = false
fun loadJsonAsset(path: String): JsonElement? {
val filename: String
val jsonPath: String?
if (path.contains(':')) {
filename = path.substringBefore(':')
jsonPath = path.substringAfter(':')
} else {
filename = path
jsonPath = null
}
val file = locate(filename)
if (!file.isFile) {
return null
}
return traverseJsonPath(jsonPath, gson.fromJson(file.reader(), JsonElement::class.java))
}
private fun luaRequire(it: LuaState, args: LuaState.ArgStack) {
val name = args.getString()
val file = locate(name)
if (!file.exists) {
throw FileNotFoundException("File $name does not exist")
}
if (!file.isFile) {
throw FileNotFoundException("File $name is a directory")
}
val read = file.readToString()
it.load(read, chunkName = file.computeFullPath())
it.call()
}
fun expose(state: LuaState) {
state.pushWeak(this) { args ->
luaRequire(args.lua, args)
0
}
state.storeGlobal("require")
state.pushTable()
state.storeGlobal("root")
state.loadGlobal("root")
val root = state.stackTop
state.push("assetJson")
state.pushWeak(this) {args ->
args.lua.push(loadJsonAsset(args.getString()))
1
}
state.setTableValue(root)
state.pop()
state.load(polyfill, "starbound.jar!/scripts/polyfill.lua")
state.call()
}
private val archivePaths = ArrayList<File>() private val archivePaths = ArrayList<File>()
private val fileSystems = ArrayList<IStarboundFile>() private val fileSystems = ArrayList<IStarboundFile>()
@ -380,6 +496,8 @@ class Starbound : ISBFileLocator {
loadStage(callback, _statusEffects, ext2files["statuseffect"] ?: listOf()) loadStage(callback, _statusEffects, ext2files["statuseffect"] ?: listOf())
loadStage(callback, _species, ext2files["species"] ?: listOf()) loadStage(callback, _species, ext2files["species"] ?: listOf())
loadStage(callback, _particles, ext2files["particle"] ?: listOf()) loadStage(callback, _particles, ext2files["particle"] ?: listOf())
loadStage(callback, _questTemplates, ext2files["questtemplate"] ?: listOf())
loadStage(callback, _techs, ext2files["tech"] ?: listOf())
pathStack.block("/") { pathStack.block("/") {
//playerDefinition = gson.fromJson(locate("/player.config").reader(), PlayerDefinition::class.java) //playerDefinition = gson.fromJson(locate("/player.config").reader(), PlayerDefinition::class.java)
@ -452,4 +570,17 @@ class Starbound : ISBFileLocator {
} }
} }
} }
companion object {
/**
* Глобальный [Interner] для [String]
*
* Так как нет смысла иметь множество [Interner]'ов для потенциального "больше одного" [Starbound],
* данный [Interner] доступен глобально
*/
@JvmField
val STRINGS: Interner<String> = Interners.newBuilder().weak().concurrencyLevel(8).build()
private val polyfill by lazy { loadInternalScript("polyfill") }
}
} }

View File

@ -37,12 +37,6 @@ fun interface ISBFileLocator {
* @throws FileNotFoundException if file does not exist * @throws FileNotFoundException if file does not exist
*/ */
fun reader(path: String) = locate(path).reader() fun reader(path: String) = locate(path).reader()
/**
* @throws IllegalStateException if file is a directory
* @throws FileNotFoundException if file does not exist
*/
fun readJson(path: String) = locate(path).readJson()
} }
interface IStarboundFile : ISBFileLocator { interface IStarboundFile : ISBFileLocator {
@ -144,13 +138,7 @@ interface IStarboundFile : ISBFileLocator {
* @throws IllegalStateException if file is a directory * @throws IllegalStateException if file is a directory
* @throws FileNotFoundException if file does not exist * @throws FileNotFoundException if file does not exist
*/ */
fun reader(): Reader = InputStreamReader(open()) fun reader(): Reader = InputStreamReader(BufferedInputStream(open()))
/**
* @throws IllegalStateException if file is a directory
* @throws FileNotFoundException if file does not exist
*/
fun readJson(): JsonElement = JsonParser.parseReader(reader())
/** /**
* @throws IllegalStateException if file is a directory * @throws IllegalStateException if file is a directory
@ -163,6 +151,17 @@ interface IStarboundFile : ISBFileLocator {
return ByteBuffer.wrap(read) return ByteBuffer.wrap(read)
} }
/**
* @throws IllegalStateException if file is a directory
* @throws FileNotFoundException if file does not exist
*/
fun readToString(): String {
val stream = open()
val read = stream.readAllBytes()
stream.close()
return String(read, charset = Charsets.UTF_8)
}
/** /**
* @throws IllegalStateException if file is a directory * @throws IllegalStateException if file is a directory
* @throws FileNotFoundException if file does not exist * @throws FileNotFoundException if file does not exist

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.client.render
import ru.dbotthepony.kstarbound.util.ITimeSource import ru.dbotthepony.kstarbound.util.ITimeSource
import ru.dbotthepony.kstarbound.util.JVMTimeSource import ru.dbotthepony.kstarbound.util.JVMTimeSource
import ru.dbotthepony.kstarbound.util.sbIntern2
/** /**
* Таймер для анимирования набора спрайтов * Таймер для анимирования набора спрайтов
@ -86,7 +87,7 @@ class FrameAnimator(
init { init {
for (i in 0 .. 500) { for (i in 0 .. 500) {
framenames.add(i.toString()) framenames.add(i.toString().sbIntern2())
} }
} }
} }

View File

@ -2,19 +2,24 @@ package ru.dbotthepony.kstarbound.defs.player
import com.google.common.collect.ImmutableSet import com.google.common.collect.ImmutableSet
import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.util.ItemDescriptor
import java.util.function.Predicate
@JsonFactory @JsonFactory
data class InventoryFilterConfig( data class BagFilterConfig(
val typeBlacklist: ImmutableSet<String>? = null, val typeBlacklist: ImmutableSet<String>? = null,
val typeWhitelist: ImmutableSet<String>? = null, val typeWhitelist: ImmutableSet<String>? = null,
val tagBlacklist: ImmutableSet<String>? = null, val tagBlacklist: ImmutableSet<String>? = null,
val categoryBlacklist: ImmutableSet<String>? = null, val categoryBlacklist: ImmutableSet<String>? = null,
) { ) : Predicate<ItemDescriptor> {
fun acceptsType(type: String): Boolean { override fun test(t: ItemDescriptor): Boolean {
if (t.isEmpty)
return false
if (typeBlacklist != null) { if (typeBlacklist != null) {
return !typeBlacklist.contains(type) return !typeBlacklist.contains(t.item!!.value.category)
} else if (typeWhitelist != null) { } else if (typeWhitelist != null) {
return typeWhitelist.contains(type) return typeWhitelist.contains(t.item!!.value.category)
} }
return true return true

View File

@ -2,17 +2,26 @@ package ru.dbotthepony.kstarbound.defs.player
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.util.WriteOnce
@JsonFactory @JsonFactory
data class InventoryConfig( data class InventoryConfig(
val customBarGroups: Int, val customBarGroups: Int,
val customBarIndexes: Int, val customBarIndexes: Int,
val itemBags: ImmutableMap<String, BagConfig>, val itemBags: ImmutableMap<String, Bag>,
) { ) {
@JsonFactory @JsonFactory
data class BagConfig( class Bag(
val priority: Int, val priority: Int,
val size: Int, val size: Int
) ) {
var name: String by WriteOnce()
}
init {
for ((k, v) in itemBags) {
v.name = k
}
}
} }

View File

@ -73,5 +73,5 @@ data class PlayerDefinition(
val genericScriptContexts: ImmutableMap<String, String>, val genericScriptContexts: ImmutableMap<String, String>,
val inventory: InventoryConfig, val inventory: InventoryConfig,
val inventoryFilters: ImmutableMap<String, InventoryFilterConfig>, val inventoryFilters: ImmutableMap<String, BagFilterConfig>,
) )

View File

@ -0,0 +1,15 @@
package ru.dbotthepony.kstarbound.defs.player
import com.google.common.collect.ImmutableList
import ru.dbotthepony.kstarbound.defs.DirectAssetReference
import ru.dbotthepony.kstarbound.defs.IScriptable
import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
@JsonFactory
data class TechDefinition(
val name: String,
val type: String,
val chipCost: Int,
override val scriptDelta: Int = 1,
override val scripts: ImmutableList<DirectAssetReference>
) : IScriptable

View File

@ -0,0 +1,17 @@
package ru.dbotthepony.kstarbound.defs.quest
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonObject
import ru.dbotthepony.kstarbound.defs.DirectAssetReference
import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
@JsonFactory
data class QuestTemplate(
val id: String,
val prerequisites: ImmutableSet<String> = ImmutableSet.of(),
val requiredItems: ImmutableSet<String> = ImmutableSet.of(),
val script: DirectAssetReference,
val updateDelta: Int = 10,
val moneyRange: LongRange,
val scriptConfig: JsonObject = JsonObject()
)

View File

@ -190,3 +190,17 @@ fun InputStream.readCString(): String {
} }
}.toString(Charsets.UTF_8) }.toString(Charsets.UTF_8)
} }
fun InputStream.readByteChar(): Char {
return read().toChar()
}
fun InputStream.readHeader(header: String) {
for ((i, char) in header.withIndex()) {
val read = readByteChar()
if (read != char) {
throw IllegalArgumentException("Malformed header at byte $i: expected $char (${char.code}) got $read (${char.code})")
}
}
}

View File

@ -0,0 +1,32 @@
package ru.dbotthepony.kstarbound.io.json
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
object LongRangeAdapter : TypeAdapter<LongRange>() {
override fun write(out: JsonWriter, value: LongRange?) {
if (value == null)
out.nullValue()
else {
out.beginArray()
out.value(value.first)
out.value(value.last)
out.endArray()
}
}
override fun read(`in`: JsonReader): LongRange? {
if (`in`.peek() == JsonToken.NULL) {
return null
} else {
`in`.beginArray()
val first = `in`.nextLong()
val last = `in`.nextLong()
`in`.endArray()
return LongRange(first, last)
}
}
}

View File

@ -21,16 +21,6 @@ class LuaGCException(message: String? = null, cause: Throwable? = null) : Throwa
class LuaException(message: String? = null, cause: Throwable? = null) : Throwable(message, cause) class LuaException(message: String? = null, cause: Throwable? = null) : Throwable(message, cause)
class LuaRuntimeException(message: String? = null, cause: Throwable? = null) : Throwable(message, cause) class LuaRuntimeException(message: String? = null, cause: Throwable? = null) : Throwable(message, cause)
fun throwLoadError(code: Int) {
when (code) {
LUA_OK -> {}
LUA_ERRSYNTAX -> throw InvalidLuaSyntaxException()
LUA_ERRMEM -> throw LuaMemoryAllocException()
// LUA_ERRGCMM -> throw LuaGCException()
else -> throw LuaException("Unknown Lua Loading error: $code")
}
}
fun throwPcallError(code: Int) { fun throwPcallError(code: Int) {
when (code) { when (code) {
LUA_OK -> {} LUA_OK -> {}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,66 @@
package ru.dbotthepony.kstarbound.lua
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import ru.dbotthepony.kstarbound.util.traverseJsonPath
fun loadInternalScript(name: String): String {
return LuaState::class.java.getResourceAsStream("/scripts/$name.lua")?.readAllBytes()?.toString(Charsets.UTF_8) ?: throw RuntimeException("/scripts/$name.lua is missing!")
}
private val messageHandlerLua by lazy { loadInternalScript("message_handler") }
private val configLua by lazy { loadInternalScript("config") }
fun LuaState.exposeConfig(config: JsonElement) {
load(configLua, "starbound.jar!/scripts/config.lua")
call()
loadGlobal("config")
val table = stackTop
push("_get")
push {
push(traverseJsonPath(getString(), config))
return@push 1
}
setTableValue(table)
pop()
}
class MessageHandler(val lua: LuaState) {
init {
lua.load(messageHandlerLua, "starbound.jar!/scripts/message_handler.lua")
lua.call()
lua.loadGlobal("message")
lua.setTableClosure("subscribe", this) { subscriptions.add(it.getString()) }
lua.pop()
}
private val subscriptions = ObjectOpenHashSet<String>()
fun isSubscribed(name: String): Boolean {
return name in subscriptions
}
fun call(name: String, vararg values: JsonElement): JsonElement? {
if (!isSubscribed(name))
return null
lua.loadGlobal("message")
lua.loadTableValue("call")
lua.push(name)
for (value in values)
lua.push(value)
lua.call(numArgs = 1 + values.size, numResults = 1)
val value = lua.getValue()
lua.pop() // message
return value
}
}

View File

@ -0,0 +1,492 @@
package ru.dbotthepony.kstarbound.player
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import ru.dbotthepony.kstarbound.RegistryObject
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.player.TechDefinition
import ru.dbotthepony.kstarbound.lua.LuaState
import ru.dbotthepony.kstarbound.lua.loadInternalScript
import ru.dbotthepony.kstarbound.util.ItemDescriptor
import ru.dbotthepony.kstarbound.util.immutableMap
import java.util.*
import kotlin.collections.ArrayList
/**
* Персонаж - как он есть.
*
* [Avatar] реализует Lua интерфейс `player`.
*/
class Avatar(val starbound: Starbound, val uniqueId: UUID) {
enum class EssentialSlot {
BEAM_AXE,
WIRE_TOOL,
PAINT_TOOL,
INSPECTION_TOOL;
}
enum class EquipmentSlot {
HEAD,
CHEST,
LEGS,
BACK,
HEAD_COSMETIC,
CHEST_COSMETIC,
LEGS_COSMETIC,
BACK_COSMETIC;
}
private val essentialSlots = EnumMap<EssentialSlot, ItemDescriptor>(EssentialSlot::class.java)
private val equipmentSlots = EnumMap<EquipmentSlot, ItemDescriptor>(EquipmentSlot::class.java)
private val bags = ArrayList<AvatarBag>()
private val quests = Object2ObjectOpenHashMap<String, QuestInstance>()
var cursorItem = ItemDescriptor.EMPTY
private val availableTechs = ObjectOpenHashSet<RegistryObject<TechDefinition>>()
private val enabledTechs = ObjectOpenHashSet<RegistryObject<TechDefinition>>()
private val equippedTechs = Object2ObjectOpenHashMap<String, RegistryObject<TechDefinition>>()
private val knownBlueprints = ObjectOpenHashSet<ItemDescriptor>()
// С подписью NEW
private val newBlueprints = ObjectOpenHashSet<ItemDescriptor>()
private val currencies = Object2LongOpenHashMap<String>()
/**
* Teaches the player any recipes which can be used to craft the specified item.
*/
fun giveBlueprint(name: String): Boolean {
val item = starbound.item(name).conciseToNull() ?: return false
if (knownBlueprints.add(item)) {
newBlueprints.add(item)
return true
}
return false
}
/**
* Returns `true` if the player knows one or more recipes to create the specified item and `false` otherwise.
*/
fun blueprintKnown(name: String): Boolean {
return (starbound.item(name).conciseToNull() ?: return false) in knownBlueprints
}
/**
* Returns `true` if the player knows one or more recipes to create the specified item and `false` otherwise.
*/
private fun blueprintKnown(name: JsonElement): Boolean {
if (name is JsonPrimitive) {
return (starbound.item(name.asString).conciseToNull() ?: return false) in knownBlueprints
} else if (name is JsonObject) {
return (starbound.item(name).conciseToNull() ?: return false) in knownBlueprints
} else {
return false
}
}
/**
* Teaches the player any recipes which can be used to craft the specified item.
*/
private fun giveBlueprint(name: JsonElement): Boolean {
val item: ItemDescriptor
if (name is JsonPrimitive) {
item = starbound.item(name.asString).conciseToNull() ?: return false
} else if (name is JsonObject) {
item = starbound.item(name).conciseToNull() ?: return false
} else {
return false
}
if (knownBlueprints.add(item)) {
newBlueprints.add(item)
return true
}
return false
}
/**
* Adds the specified tech to the player's list of available (unlockable) techs.
*/
fun makeTechAvailable(name: String): Boolean {
return availableTechs.add(starbound.techs[name] ?: return false)
}
/**
* Removes the specified tech from player's list of available (unlockable) techs.
*/
fun makeTechUnavailable(name: String): Boolean {
val tech = starbound.techs[name] ?: return false
if (availableTechs.remove(tech)) {
enabledTechs.remove(tech)
equippedTechs.remove(tech.value.type)
return true
}
return false
}
/**
* Unlocks the specified tech, allowing it to be equipped through the tech GUI.
*/
fun enableTech(name: String): Boolean {
val tech = starbound.techs[name] ?: return false
availableTechs.add(tech)
return enabledTechs.add(tech)
}
/**
* Equips the specified tech.
*/
fun equipTech(name: String): Boolean {
val tech = starbound.techs[name] ?: return false
availableTechs.add(tech)
enabledTechs.add(tech)
return equippedTechs.put(tech.value.type, tech) != tech
}
/**
* Unequips the specified tech.
*/
fun unequipTech(name: String): Boolean {
val tech = starbound.techs[name] ?: return false
return equippedTechs.remove(tech.value.type) == tech
}
/**
* Returns the player's current total reserves of the specified currency.
*/
fun currency(name: String): Long {
return currencies.getLong(name)
}
/**
* Increases the player's reserve of the specified currency by the specified amount.
*/
fun addCurrency(name: String, amount: Long) {
check(amount >= 0L) { "Negative amount of currency: $amount (currency: $name)" }
currencies.computeLong(name) { key, old -> (old ?: 0L) + amount }
}
/**
* Attempts to consume the specified amount of the specified currency and returns `true` if successful and `false` otherwise.
*/
fun consumeCurrency(name: String, amount: Long): Boolean {
check(amount >= 0L) { "Negative amount of currency: $amount (currency: $name)" }
val current = currencies.getLong(name)
if (current - amount >= 0L) {
currencies[name] = current - amount
return true
}
return false
}
/**
* Triggers an immediate cleanup of the player's inventory, removing item stacks with 0 quantity. May rarely be required in special cases of making several sequential modifications to the player's inventory within a single tick.
*/
fun cleanupItems() {
}
/**
* Adds the specified item to the player's inventory.
*/
fun giveItem(descriptor: ItemDescriptor) {
}
/**
* Returns `true` if the player's inventory contains an item matching the specified descriptor and `false` otherwise. If exactMatch is `true` then parameters as well as item name must match.
*/
fun hasItem(descriptor: ItemDescriptor, exactMatch: Boolean = false): Boolean {
return false
}
/**
* Returns the total number of items in the player's inventory matching the specified descriptor. If exactMatch is `true` then parameters as well as item name must match.
*/
fun hasCountOfItem(descriptor: ItemDescriptor, exactMatch: Boolean = false): Long {
return 0L
}
/**
* Attempts to consume the specified item from the player's inventory and returns the item consumed if successful. If consumePartial is `true`, matching stacks totalling fewer items than the requested count may be consumed, otherwise the operation will only be performed if the full count can be consumed. If exactMatch is `true` then parameters as well as item name must match.
*/
fun consumeItem(descriptor: ItemDescriptor, allowPartial: Boolean = false, exactMatch: Boolean = false): ItemDescriptor {
return ItemDescriptor.EMPTY
}
fun inventoryTags(): Map<String, Long> {
return mapOf()
}
fun itemsWithTag(): List<ItemDescriptor> {
return listOf()
}
fun consumeTaggedItem(tag: String): Long {
return 0L
}
fun hasItemWithParameter(name: String, value: JsonElement): Boolean {
return false
}
fun consumeItemWithParameter(name: String, value: JsonElement, count: Long): Long {
return 0L
}
fun getItemWithParameter(name: String, value: JsonElement): ItemDescriptor {
return ItemDescriptor.EMPTY
}
var primaryHandItem: ItemDescriptor? = null
var altHandItem: ItemDescriptor? = null
fun essentialItem(slotName: EssentialSlot): ItemDescriptor? {
return essentialSlots[slotName]?.conciseToNull()
}
fun giveEssentialItem(slotName: EssentialSlot, item: ItemDescriptor) {
}
fun removeEssentialItem(slotName: EssentialSlot) {
}
fun equippedItem(slotName: EquipmentSlot): ItemDescriptor {
return equipmentSlots[slotName] ?: ItemDescriptor.EMPTY
}
fun setEquippedItem(slotName: EquipmentSlot, item: ItemDescriptor) {
}
fun addQuest(quest: QuestInstance): QuestInstance? {
check(quest.avatar === this) { "$quest does not belong to $this" }
return quests.put(quest.id, quest)
}
private fun startQuest(value: JsonElement, serverID: String?, worldID: String?): String {
if (value is JsonPrimitive) {
val quest = QuestInstance(this, descriptor = QuestDescriptor(value.asString), serverID = serverID?.let(UUID::fromString), worldID = worldID)
addQuest(quest)
return quest.id
} else if (value is JsonObject) {
val seed = value["seed"]?.asLong ?: QuestDescriptor.makeSeed()
val questId = value["questId"]?.asString ?: throw IllegalArgumentException("Invalid 'questId' in quest descriptor")
val templateId = value["templateId"]?.asString ?: questId
val params = value["parameters"] as? JsonObject ?: JsonObject()
val quest = QuestInstance(this, descriptor = QuestDescriptor(questId, templateId, seed, params), serverID = serverID?.let(UUID::fromString), worldID = worldID)
addQuest(quest)
return quest.id
} else {
throw IllegalArgumentException("Invalid quest descriptor: $value")
}
}
fun expose(lua: LuaState) {
lua.load(playerLua)
lua.pushTable()
lua.setTableValue("uniqueId", uniqueId.toString())
lua.setTableValue("species", "human")
lua.setTableValue("gender", "male")
lua.call(numArgs = 1)
lua.loadGlobal("player")
lua.setTableClosure("giveBlueprint", this) { giveBlueprint(it.getValue()) }
lua.setTableFunction("blueprintKnown", this) { it.lua.push(blueprintKnown(it.getValue())); 1 }
lua.setTableClosure("makeTechAvailable", this) { makeTechAvailable(it.getString()) }
lua.setTableClosure("makeTechUnavailable", this) { makeTechUnavailable(it.getString()) }
lua.setTableClosure("enableTech", this) { enableTech(it.getString()) }
lua.setTableClosure("equipTech", this) { equipTech(it.getString()) }
lua.setTableClosure("unequipTech", this) { unequipTech(it.getString()) }
lua.setTableFunction("availableTechs", this) { it.lua.pushStrings(availableTechs.stream().map { it.value.name }.iterator()); 1 }
lua.setTableFunction("enabledTechs", this) { it.lua.pushStrings(enabledTechs.stream().map { it.value.name }.iterator()); 1 }
lua.setTableFunction("equippedTech", this) { it.lua.push(equippedTechs[it.getString()]?.value?.name); 1 }
lua.setTableFunction("currency", this) { it.lua.push(currency(it.getString())); 1 }
lua.setTableClosure("addCurrency", this) { addCurrency(it.getString(), it.getLong()) }
lua.setTableFunction("consumeCurrency", this) { it.lua.push(consumeCurrency(it.getString(), it.getLong())); 1 }
lua.setTableClosure("cleanupItems", this) { cleanupItems() }
lua.setTableClosure("giveItem", this) { giveItem(starbound.item(it.getValue())) }
lua.setTableFunction("hasItem", this) {
it.lua.push(hasItem(starbound.item(it.getValue()), it.getBoolOrNull() ?: false))
1
}
lua.setTableFunction("hasCountOfItem", this) {
it.lua.push(hasCountOfItem(starbound.item(it.getValue()), it.getBoolOrNull() ?: false))
1
}
lua.setTableFunction("consumeItem", this) {
it.lua.push(consumeItem(starbound.item(it.getValue()), it.getBoolOrNull() ?: false, it.getBoolOrNull() ?: false).toJson())
1
}
lua.setTableFunction("inventoryTags", this) {
val mapping = inventoryTags()
it.lua.pushTable(hashSize = mapping.size)
for ((k, v) in mapping) {
it.lua.setTableValue(k, v)
}
1
}
lua.setTableFunction("itemsWithTag", this) {
val mapping = itemsWithTag()
it.lua.pushTable(arraySize = mapping.size)
for ((k, v) in mapping.withIndex()) {
it.lua.setTableValue(k, v.toJson())
}
1
}
lua.setTableFunction("consumeTaggedItem", this) {
it.lua.push(consumeTaggedItem(it.getString()))
1
}
lua.setTableFunction("hasItemWithParameter", this) {
it.lua.push(hasItemWithParameter(it.getString(), it.getValue()))
1
}
lua.setTableFunction("consumeItemWithParameter", this) {
it.lua.push(consumeItemWithParameter(it.getString(), it.getValue(), it.getLong()))
1
}
lua.setTableFunction("getItemWithParameter", this) {
it.lua.push(getItemWithParameter(it.getString(), it.getValue()).toJson())
1
}
lua.setTableFunction("primaryHandItem", this) {
it.lua.push(primaryHandItem?.toJson())
1
}
lua.setTableFunction("altHandItem", this) {
it.lua.push(altHandItem?.toJson())
1
}
lua.setTableFunction("primaryHandItemTags", this) {
val tags = primaryHandItem?.item?.value?.itemTags
if (tags != null) it.lua.pushStrings(tags) else it.lua.push()
1
}
lua.setTableFunction("altHandItemTags", this) {
val tags = altHandItem?.item?.value?.itemTags
if (tags != null) it.lua.pushStrings(tags) else it.lua.push()
1
}
lua.setTableFunction("essentialItem", this) {
val name = it.getString()
val slot = essentialSlotsMap[name] ?: throw IllegalArgumentException("Invalid slot '$name'")
it.lua.push(essentialItem(slot)?.toJson())
1
}
lua.setTableClosure("giveEssentialItem", this) {
val name = it.getString()
val item = starbound.item(it.getValue())
val slot = essentialSlotsMap[name] ?: throw IllegalArgumentException("Invalid slot '$name'")
giveEssentialItem(slot, item)
}
lua.setTableClosure("removeEssentialItem", this) {
val name = it.getString()
val slot = essentialSlotsMap[name] ?: throw IllegalArgumentException("Invalid slot '$name'")
removeEssentialItem(slot)
}
lua.setTableFunction("equippedItem", this) {
val name = it.getString()
val slot = equipmentSlotsMap[name] ?: throw IllegalArgumentException("Invalid slot '$name'")
it.lua.push(equippedItem(slot).toJson())
1
}
lua.setTableClosure("setEquippedItem", this) {
val name = it.getString()
val item = starbound.item(it.getValue())
val slot = equipmentSlotsMap[name] ?: throw IllegalArgumentException("Invalid slot '$name'")
setEquippedItem(slot, item)
}
lua.setTableFunction("swapSlotItem", this) {
it.lua.push(cursorItem.toJson())
1
}
lua.setTableClosure("setSwapSlotItem", this) {
cursorItem = starbound.item(it.getValue())
}
lua.setTableFunction("startQuest", this) {
it.lua.push(startQuest(it.getValue(), it.getStringOrNull(), it.getStringOrNull()))
1
}
lua.setTableFunction("hasQuest", this) {
it.lua.push(quests[it.getString()] != null)
1
}
lua.setTableFunction("hasCompletedQuest", this) {
val quest = quests[it.getString()]
it.lua.push(quest != null && quest.state == QuestInstance.State.COMPLETE)
1
}
lua.pop()
}
companion object {
private val playerLua by lazy { loadInternalScript("player") }
private val essentialSlotsMap = immutableMap<String, EssentialSlot> {
put("beamaxe", EssentialSlot.BEAM_AXE)
put("inspectiontool", EssentialSlot.INSPECTION_TOOL)
put("wiretool", EssentialSlot.WIRE_TOOL)
put("painttool", EssentialSlot.PAINT_TOOL)
}
private val equipmentSlotsMap = immutableMap<String, EquipmentSlot> {
put("head", EquipmentSlot.HEAD)
put("chest", EquipmentSlot.CHEST)
put("legs", EquipmentSlot.LEGS)
put("back", EquipmentSlot.BACK)
put("headCosmetic", EquipmentSlot.HEAD_COSMETIC)
put("chestCosmetic", EquipmentSlot.CHEST_COSMETIC)
put("legsCosmetic", EquipmentSlot.LEGS_COSMETIC)
put("backCosmetic", EquipmentSlot.BACK_COSMETIC)
}
}
}

View File

@ -0,0 +1,57 @@
package ru.dbotthepony.kstarbound.player
import com.google.common.collect.ImmutableList
import ru.dbotthepony.kstarbound.defs.player.InventoryConfig
import ru.dbotthepony.kstarbound.util.ItemDescriptor
import java.util.function.Predicate
class AvatarBag(val avatar: Avatar, val config: InventoryConfig.Bag, val filter: Predicate<ItemDescriptor>) {
private val slots = ImmutableList.builder<Slot>().let {
for (i in 0 until config.size) {
it.add(Slot())
}
it.build()
}
private class Slot {
var item: ItemDescriptor? = null
fun mergeFrom(value: ItemDescriptor, simulate: Boolean) {
if (item == null) {
if (!simulate) {
item = value.copy().also { it.count = value.count.coerceAtMost(value.item!!.value.maxStack) }
}
value.count -= value.item!!.value.maxStack
} else {
item!!.mergeFrom(value, simulate)
}
}
}
operator fun set(index: Int, value: ItemDescriptor?) {
slots[index].item = value
}
operator fun get(index: Int): ItemDescriptor? {
return slots[index].item
}
fun put(item: ItemDescriptor, simulate: Boolean): ItemDescriptor {
if (!filter.test(item))
return item
val copy = item.copy()
for (slot in slots.indices) {
slots[slot].mergeFrom(copy, simulate)
if (copy.isEmpty) {
return copy
}
}
return copy
}
}

View File

@ -0,0 +1,12 @@
package ru.dbotthepony.kstarbound.player
import ru.dbotthepony.kstarbound.Starbound
/**
* Игрок - как он есть.
*
* [Player] - источник команд для [Avatar]
*/
class Player(val starbound: Starbound) {
var avatar: Avatar? = null
}

View File

@ -0,0 +1,18 @@
package ru.dbotthepony.kstarbound.player
import com.google.gson.JsonObject
import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
@JsonFactory
data class QuestDescriptor(
val questId: String,
val templateId: String = questId,
val seed: Long = makeSeed(),
val parameters: JsonObject = JsonObject()
) {
companion object {
fun makeSeed(): Long {
return System.nanoTime().rotateLeft(27).xor(System.currentTimeMillis())
}
}
}

View File

@ -0,0 +1,250 @@
package ru.dbotthepony.kstarbound.player
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.defs.quest.QuestTemplate
import ru.dbotthepony.kstarbound.lua.LuaState
import ru.dbotthepony.kstarbound.lua.MessageHandler
import ru.dbotthepony.kstarbound.lua.exposeConfig
import ru.dbotthepony.kstarbound.util.ItemDescriptor
import ru.dbotthepony.kstarbound.util.set
import java.util.UUID
class QuestInstance(
val avatar: Avatar,
val descriptor: QuestDescriptor,
val seed: Long = System.nanoTime().rotateLeft(27).xor(System.currentTimeMillis()),
val serverID: UUID? = null,
val worldID: String? = null
) {
val template: QuestTemplate = avatar.starbound.questTemplates[descriptor.templateId]?.value ?: throw IllegalArgumentException("No such quest template ${descriptor.templateId}")
val id get() = descriptor.questId
val lua = LuaState()
val messages = MessageHandler(lua)
enum class State(val serializedName: String) {
NEW("New"),
OFFER("Offer"),
ACTIVE("Active"),
COMPLETE("Complete"),
FAILED("Failed")
}
private val objectiveList = ArrayList<Pair<String, Boolean>>()
var canTurnIn = false
var failureText = ""
var completionText = ""
var text = ""
var title = ""
var state = State.NEW
var progress: Double? = null
var compassDirection: Double? = null
private val portraits = JsonObject()
private val params = descriptor.parameters.deepCopy()
private val portraitTitles = Object2ObjectOpenHashMap<String, String>()
private var isInitialized = false
private var successfulInit = false
private var calledStart = false
private var successfulStart = false
private val rewards = ArrayList<ItemDescriptor>()
fun complete() {
}
fun fail() {
}
private fun setObjectiveList(value: JsonArray) {
}
private fun addReward(value: JsonElement) {
val item = avatar.starbound.item(value)
if (!item.isEmpty) {
rewards.add(item)
}
}
init {
lua.pushTable()
lua.storeGlobal("self")
lua.pushTable()
lua.storeGlobal("storage")
lua.pushTable()
lua.storeGlobal("quest")
lua.loadGlobal("quest")
lua.setTableClosure("setParameter", this) { setParameter(it) }
lua.setTableFunction("parameters", this) {
it.lua.push(params)
1
}
lua.setTableClosure("setPortrait", this) { setPortrait(it) }
lua.setTableClosure("setPortraitTitle", this) { setPortraitTitle(it) }
lua.setTableFunction("state", this) { it.lua.push(state.serializedName); 1 }
lua.setTableClosure("complete", this) { complete() }
lua.setTableClosure("fail", this) { fail() }
lua.setTableClosure("setCanTurnIn", this) { canTurnIn = it.getBool() }
lua.setTableFunction("questDescriptor", this) { it.lua.push(avatar.starbound.gson.toJsonTree(descriptor)); 1 }
lua.setTableFunction("questId", this) { it.lua.push(id); 1 }
lua.setTableFunction("templateId", this) { it.lua.push(template.id); 1 }
lua.setTableFunction("seed", this) { it.lua.push(seed); 1 }
lua.setTableFunction("questArcDescriptor", this) { TODO(); 1 }
lua.setTableFunction("questArcPosition", this) { TODO(); 1 }
lua.setTableFunction("worldId", this) { it.lua.push(worldID); 1 }
lua.setTableFunction("serverUuid", this) { it.lua.push(serverID?.toString()); 1 }
lua.setTableClosure("setObjectiveList", this) { setObjectiveList(it.getArray()) }
lua.setTableClosure("setProgress", this) { progress = it.getDoubleOrNil() }
lua.setTableClosure("setCompassDirection", this) { compassDirection = it.getDoubleOrNil() }
lua.setTableClosure("setTitle", this) { title = it.getString() }
lua.setTableClosure("setText", this) { text = it.getString() }
lua.setTableClosure("setCompletionText", this) { completionText = it.getString() }
lua.setTableClosure("setFailureText", this) { failureText = it.getString() }
lua.setTableClosure("addReward", this) { addReward(it.getValue()) }
lua.pop()
avatar.starbound.expose(lua)
avatar.expose(lua)
lua.exposeConfig(template.scriptConfig)
}
private fun setParameter(argStack: LuaState.ArgStack) {
params.add(argStack.getString(), argStack.getValue())
}
private fun setPortrait(argStack: LuaState.ArgStack) {
val name = argStack.getString()
val value = argStack.getAnything()
if (value == null)
portraits.remove(name)
else
portraits.add(name, value)
}
private fun setPortraitTitle(argStack: LuaState.ArgStack) {
val name = argStack.getString()
val title = argStack.getStringOrNil()
if (title == null)
portraitTitles.remove(name)
else
portraitTitles[name] = title
}
init {
for ((k, v) in descriptor.parameters.entrySet()) {
params[k] = v.deepCopy()
}
}
/**
* Читает главный скриптовый файл квеста и вызывает его (глобальную) функцию init()
*/
fun init(): Boolean {
if (!isInitialized) {
isInitialized = true
val script = avatar.starbound.locate(template.script.fullPath)
if (!script.isFile) {
LOGGER.error("Quest ${template.id} specifies ${template.script.fullPath} as its script, but it is not a file or does not exist!")
return false
}
try {
lua.load(script.readToString(), template.script.fullPath)
} catch(err: Throwable) {
LOGGER.error("Error loading Lua code for quest ${descriptor.questId}", err)
return false
}
try {
lua.call()
} catch(err: Throwable) {
LOGGER.error("Error running Lua code for quest ${descriptor.questId}", err)
return false
}
try {
lua.loadGlobal("init")
lua.call()
} catch(err: Throwable) {
LOGGER.error("Error running init() function for quest ${descriptor.questId}", err)
return false
}
successfulInit = true
}
return successfulInit
}
fun start(): Boolean {
if (!init()) {
return false
}
if (calledStart) {
return successfulStart
}
lua.loadGlobal("questStart")
try {
if (lua.isFunction()) {
lua.call()
} else {
lua.pop()
}
} catch(err: Throwable) {
LOGGER.error("Error running questStart() function for quest ${descriptor.questId}", err)
return false
}
return successfulStart
}
fun update(delta: Int): Boolean {
if (!init()) {
return false
}
lua.loadGlobal("update")
try {
if (lua.isFunction()) {
lua.push(delta)
lua.call(numArgs = 1)
} else {
lua.pop()
}
} catch(err: Throwable) {
LOGGER.error("Error running update() function for quest ${descriptor.questId}", err)
return false
}
return true
}
companion object {
private val LOGGER = LogManager.getLogger()
}
}

View File

@ -0,0 +1,121 @@
package ru.dbotthepony.kstarbound.tools
import com.google.gson.GsonBuilder
import ru.dbotthepony.kstarbound.io.json.BinaryJsonReader
import ru.dbotthepony.kstarbound.io.readHeader
import ru.dbotthepony.kstarbound.io.readString
import ru.dbotthepony.kstarbound.io.readVarInt
import java.io.BufferedInputStream
import java.io.DataInputStream
import java.io.File
import java.io.OutputStreamWriter
import java.util.Scanner
import kotlin.system.exitProcess
fun main(vararg args: String) {
var originalFile = args.getOrNull(0)
val scanner = System.`in`?.let(::Scanner)
var interactive = false
if (originalFile == null) {
if (scanner == null) {
System.err.println("Usage: <original file path> [output file path]")
System.err.println("By default, new file is put in the same folder as original file")
System.err.println("with '.json' extension added to it")
exitProcess(1)
} else {
println("Input filename:")
originalFile = scanner.nextLine()
interactive = true
}
}
val f = File(originalFile!!).absoluteFile
if (!f.exists()) {
System.err.println("File $f does not exist")
exitProcess(1)
}
if (!f.isFile) {
System.err.println("$f is not a file")
exitProcess(1)
}
var newFile = args.getOrNull(1)
if (newFile == null) {
if (interactive) {
val def = f.parent + "/" + f.name + ".json"
println("Output filename [$def]:")
val read = scanner!!.nextLine()
if (read == "") {
newFile = def
} else {
newFile = read!!
}
} else {
newFile = f.parent + "/" + f.name + ".json"
}
}
val new = File(newFile)
if (!new.parentFile.exists()) {
System.err.println("Output directory ${new.parent} does not exist")
exitProcess(1)
}
if (!new.parentFile.isDirectory) {
System.err.println("Output directory ${new.parent} is not a directory")
exitProcess(1)
}
if (new.exists() && !new.isFile) {
System.err.println("Output path $new already exists and it is not a file")
exitProcess(1)
}
if (interactive && new.exists()) {
println("$new already exists. Overwrite? [Y/n]")
var next = scanner!!.nextLine()
if (next == "") next = "y"
if (next.lowercase()[0] != 'y') {
System.err.println("Halt")
exitProcess(1)
}
}
try {
val t = System.nanoTime()
val dataStream = DataInputStream(BufferedInputStream(f.inputStream()))
dataStream.readHeader("SBVJ01")
val name = dataStream.readString(dataStream.readVarInt())
val magic = dataStream.read()
val version = dataStream.readInt()
val data = BinaryJsonReader.readElement(dataStream)
val gson = GsonBuilder().setPrettyPrinting().create()
dataStream.close()
println("$f:")
println("JSON Name: $name")
println("Version: $version")
println("Magic: $magic")
println("JSON Type: ${data::class.simpleName}")
new.delete()
val output = OutputStreamWriter(new.outputStream())
gson.toJson(data, output)
output.close()
println("Successfully written data to $new ${"in %.2f ms".format(((System.nanoTime() - t) / 1_000L) / 1_000.0)}")
exitProcess(0)
} catch(err: Throwable) {
System.err.println(err)
exitProcess(1)
}
}

View File

@ -0,0 +1,16 @@
package ru.dbotthepony.kstarbound.util
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import ru.dbotthepony.kstarbound.io.json.InternedJsonElementAdapter
operator fun JsonObject.set(key: String, value: JsonElement) { add(key, value) }
operator fun JsonObject.set(key: String, value: String) { add(key, JsonPrimitive(value)) }
operator fun JsonObject.set(key: String, value: Int) { add(key, JsonPrimitive(value)) }
operator fun JsonObject.set(key: String, value: Long) { add(key, JsonPrimitive(value)) }
operator fun JsonObject.set(key: String, value: Float) { add(key, JsonPrimitive(value)) }
operator fun JsonObject.set(key: String, value: Double) { add(key, JsonPrimitive(value)) }
operator fun JsonObject.set(key: String, value: Boolean) { add(key, InternedJsonElementAdapter.of(value)) }
operator fun JsonObject.set(key: String, value: Nothing?) { add(key, JsonNull.INSTANCE) }

View File

@ -0,0 +1,145 @@
package ru.dbotthepony.kstarbound.util
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import com.google.gson.TypeAdapter
import com.google.gson.internal.bind.TypeAdapters
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kstarbound.RegistryObject
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.item.IItemDefinition
class ItemDescriptor private constructor(item: RegistryObject<IItemDefinition>?, count: Long, val parameters: JsonObject, marker: Unit) {
constructor(item: RegistryObject<IItemDefinition>, count: Long = 1L, parameters: JsonObject = JsonObject()) : this(item, count, parameters, Unit)
var item: RegistryObject<IItemDefinition>? = item
private set
var count = count
set(value) {
if (field == 0L)
return
field = value.coerceAtLeast(0L)
if (field == 0L) {
item = null
}
}
val isEmpty: Boolean
get() = count <= 0 || item == null
val isNotEmpty: Boolean
get() = count > 0 && item != null
fun grow(amount: Long) {
count += amount
}
fun shrink(amount: Long) {
count -= amount
}
/**
* Возвращает null если этот предмет пуст
*/
fun conciseToNull(): ItemDescriptor? {
if (isEmpty) {
return null
} else {
return this
}
}
fun mergeFrom(other: ItemDescriptor, simulate: Boolean) {
if (isStackable(other)) {
val newCount = (count + other.count).coerceAtMost(item!!.value.maxStack)
val diff = newCount - count
other.count -= diff
if (!simulate)
count = newCount
}
}
fun lenientEquals(other: Any?): Boolean {
if (other !is ItemDescriptor)
return false
if (isEmpty)
return other.isEmpty
return other.count == count && other.item == item
}
fun isStackable(other: ItemDescriptor): Boolean {
return count != 0L && other.count != 0L && item!!.value.maxStack < count && other.item == item && other.parameters == parameters
}
override fun equals(other: Any?): Boolean {
if (other !is ItemDescriptor)
return false
if (isEmpty)
return other.isEmpty
return other.count == count && other.item == item && other.parameters == parameters
}
// мы не можем делать хеш из count и parameters так как они изменяемы
override fun hashCode(): Int {
return item.hashCode()
}
override fun toString(): String {
if (isEmpty)
return "ItemDescriptor.EMPTY"
return "ItemDescriptor[${item!!.value.itemName}, count = $count, params = $parameters]"
}
fun copy(): ItemDescriptor {
if (isEmpty)
return this
return ItemDescriptor(item, count, parameters.deepCopy(), Unit)
}
fun toJson(): JsonObject? {
if (isEmpty) {
return null
}
return JsonObject().also {
it.add("name", JsonPrimitive(item!!.value.itemName))
it.add("count", JsonPrimitive(count))
it.add("parameters", parameters.deepCopy())
}
}
class Adapter(val starbound: Starbound) : TypeAdapter<ItemDescriptor>() {
override fun write(out: JsonWriter, value: ItemDescriptor?) {
val json = value?.toJson()
if (json == null)
out.nullValue()
else
TypeAdapters.JSON_ELEMENT.write(out, json)
}
override fun read(`in`: JsonReader): ItemDescriptor {
if (`in`.peek() == JsonToken.NULL)
return EMPTY
return starbound.item(TypeAdapters.JSON_ELEMENT.read(`in`))
}
}
companion object {
@JvmField
val EMPTY = ItemDescriptor(null, 0L, JsonObject(), Unit)
}
}

View File

@ -11,6 +11,7 @@ import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
import ru.dbotthepony.kstarbound.Starbound
/** /**
* Шаблонизировання строка в стиле Starbound'а * Шаблонизировання строка в стиле Starbound'а
@ -165,7 +166,7 @@ class SBPattern private constructor(
throw IllegalArgumentException("Malformed pattern string: $raw") throw IllegalArgumentException("Malformed pattern string: $raw")
} }
pieces.add(Piece(name = raw.substring(open + 1, closing))) pieces.add(Piece(name = Starbound.STRINGS.intern(raw.substring(open + 1, closing))))
i = closing + 1 i = closing + 1
} }
} }

View File

@ -0,0 +1,89 @@
package ru.dbotthepony.kstarbound.util
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.JsonObject
import ru.dbotthepony.kstarbound.Starbound
import java.util.*
import java.util.function.Consumer
import kotlin.collections.ArrayList
fun String.sbIntern(): String {
return Starbound.STRINGS.intern(this)
}
fun String.sbIntern2(): String {
return Starbound.STRINGS.intern(this.intern())
}
fun traverseJsonPath(path: String?, element: JsonElement?): JsonElement? {
element ?: return null
path ?: return element
if (path.contains('.')) {
var current: JsonElement? = element
for (name in path.split('.')) {
if (current is JsonObject) {
current = current[name]
} else if (current is JsonArray) {
val toInt = name.toIntOrNull() ?: return null
if (toInt !in 0 until current.size()) return null
current = current.get(toInt)
} else {
return null
}
}
return current
} else {
if (element is JsonObject) {
return element[path]
} else if (element is JsonArray) {
val toInt = path.toIntOrNull() ?: return null
if (toInt !in 0 until element.size()) return null
return element[toInt]
} else {
return null
}
}
}
inline fun <K : Any, V : Any> immutableMap(initializer: ImmutableMap.Builder<K, V>.() -> Unit): ImmutableMap<K, V> {
val builder = ImmutableMap.Builder<K, V>()
initializer.invoke(builder)
return builder.build()
}
inline fun <V : Any> immutableSet(initializer: Consumer<V>.() -> Unit): ImmutableSet<V> {
val builder = ImmutableSet.Builder<V>()
initializer.invoke(builder::add)
return builder.build()
}
inline fun <V : Any> immutableList(initializer: Consumer<V>.() -> Unit): ImmutableList<V> {
val builder = ImmutableList.Builder<V>()
initializer.invoke(builder::add)
return builder.build()
}
fun UUID.toStarboundString(): String {
val builder = StringBuilder(32)
val a = mostSignificantBits.toString(16)
val b = mostSignificantBits.toString(16)
for (i in a.length until 16)
builder.append("0")
builder.append(a)
for (i in b.length until 16)
builder.append("0")
builder.append(b)
return builder.toString()
}

View File

@ -0,0 +1,17 @@
config = {}
local config = config
function config.getParameter(name, default)
if type(name) ~= 'string' then
error('config.getParameter: name must be a string, got ' .. type(name), 2)
end
local get = config._get(name)
if get == nil then
return default
else
return get
end
end

View File

@ -0,0 +1,27 @@
message = {
handlers = {}
}
local message = message
function message.setHandler(name, handler)
if type(name) ~= 'string' then
error('message.setHandler: Handler name must be a string, got ' .. type(name), 2)
end
if type(handler) ~= 'function' then
error('message.setHandler: Handler itself must be a function, got ' .. type(handler), 2)
end
message.subscribe(name)
message.handlers[name] = handler
end
function message.call(name, ...)
local handler = message.handlers[name]
if handler ~= nil then
return handler(...)
end
end

View File

@ -0,0 +1,21 @@
local playerData = select(1, ...)
player = {}
local player = player
local uniqueId = playerData.uniqueId
local species = playerData.species
local gender = playerData.gender
function player.uniqueId()
return uniqueId
end
function player.species()
return species
end
function player.gender()
return gender
end

View File

@ -0,0 +1,8 @@
function jobject()
return {}
end
function jarray()
return {}
end