From 7857b8821ebdb1eb1c83dedb0c810907145c7b5b Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Thu, 18 Apr 2024 20:47:17 +0700 Subject: [PATCH] Working name generator --- .../ru/dbotthepony/kstarbound/Globals.kt | 31 +++++++ .../kotlin/ru/dbotthepony/kstarbound/Main.kt | 3 +- .../ru/dbotthepony/kstarbound/Registries.kt | 3 + .../ru/dbotthepony/kstarbound/Starbound.kt | 83 +++++++++++++++++- .../kstarbound/defs/MarkovTextGenerator.kt | 84 +++++++++++++++++++ .../kstarbound/defs/item/ItemDescriptor.kt | 2 +- .../kstarbound/lua/bindings/RootBindings.kt | 12 ++- 7 files changed, 212 insertions(+), 6 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/MarkovTextGenerator.kt diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt index 003de6d3..26095ad9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt @@ -1,6 +1,8 @@ package ru.dbotthepony.kstarbound +import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableMap +import com.google.common.collect.ImmutableSet import com.google.gson.TypeAdapter import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.defs.ActorMovementParameters @@ -24,6 +26,7 @@ import ru.dbotthepony.kstarbound.defs.world.SkyGlobalConfig 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 @@ -107,6 +110,32 @@ object Globals { var itemParameters by Delegates.notNull() private set + private var profanityFilterInternal by Delegates.notNull>() + + val profanityFilter: ImmutableSet by lazy { + // reverse "encryption" + val words = ImmutableSet.Builder() + + for (word in profanityFilterInternal) { + val chars = CharArray(word.length) + + for (i in word.indices) { + var c = word[i] + + if ((c >= 'a' + 13 && c <= 'm' + 13) || (c >= 'A' + 13 && c <= 'M' + 13)) + c -= 13 + else if ((c >= 'n' - 13 && c <= 'z' - 13) || (c >= 'N' - 13 && c <= 'Z' - 13)) + c += 13 + + chars[i] = c + } + + words.add(String(chars)) + } + + words.build() + } + private fun load(path: String, accept: KMutableProperty0, adapter: Lazy>): Future<*> { val file = Starbound.loadJsonAsset(path) @@ -161,6 +190,8 @@ object Globals { tasks.add(load("/system_objects.config", ::systemObjects, lazy { Starbound.gson.mapAdapter() })) tasks.add(load("/instance_worlds.config", ::instanceWorlds, lazy { Starbound.gson.mapAdapter() })) + tasks.add(load("/names/profanityfilter.config", ::profanityFilterInternal, lazy { Starbound.gson.listAdapter() })) + return tasks } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index 9255acad..6760bfbc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -4,6 +4,7 @@ import org.apache.logging.log4j.LogManager import org.lwjgl.Version import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.server.IntegratedStarboundServer +import ru.dbotthepony.kstarbound.util.random.random import java.io.File import java.net.InetSocketAddress @@ -27,5 +28,5 @@ fun main() { Starbound.initializeGame().thenApply { val server = IntegratedStarboundServer(client, File("./")) server.channels.createChannel(InetSocketAddress(21060)) - } + }.exceptionally { LOGGER.error("what", it); null } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt index c691d9e3..c597952e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt @@ -15,6 +15,7 @@ import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.Json2Function import ru.dbotthepony.kstarbound.defs.JsonConfigFunction import ru.dbotthepony.kstarbound.defs.JsonFunction +import ru.dbotthepony.kstarbound.defs.MarkovTextGenerator import ru.dbotthepony.kstarbound.defs.Species import ru.dbotthepony.kstarbound.defs.StatusEffectDefinition import ru.dbotthepony.kstarbound.defs.ThingDescription @@ -85,6 +86,7 @@ object Registries { val treeFoliageVariants = Registry("tree foliage variant").also(registriesInternal::add).also { adapters.add(it.adapter()) } val bushVariants = Registry("bush variant").also(registriesInternal::add).also { adapters.add(it.adapter()) } val dungeons = Registry("dungeon").also(registriesInternal::add).also { adapters.add(it.adapter()) } + val markovGenerators = Registry("markov text generator").also(registriesInternal::add).also { adapters.add(it.adapter()) } private fun key(mapper: (T) -> String): (T) -> Pair> { return { mapper.invoke(it) to KOptional() } @@ -170,6 +172,7 @@ object Registries { tasks.addAll(loadRegistry(treeStemVariants, patchTree, fileTree["modularstem"] ?: listOf(), key(TreeVariant.StemData::name))) tasks.addAll(loadRegistry(treeFoliageVariants, patchTree, fileTree["modularfoliage"] ?: listOf(), key(TreeVariant.FoliageData::name))) 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(loadCombined(jsonFunctions, fileTree["functions"] ?: listOf(), patchTree)) tasks.addAll(loadCombined(json2Functions, fileTree["2functions"] ?: listOf(), patchTree)) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 3e048030..17d71550 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -27,11 +27,11 @@ import ru.dbotthepony.kommons.gson.Vector4fTypeAdapter import ru.dbotthepony.kommons.gson.Vector4iTypeAdapter import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.collect.WeightedList import ru.dbotthepony.kstarbound.defs.* import ru.dbotthepony.kstarbound.defs.image.Image import ru.dbotthepony.kstarbound.defs.image.SpriteReference -import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition import ru.dbotthepony.kstarbound.defs.actor.player.BlueprintLearnList import ru.dbotthepony.kstarbound.defs.animation.Particle import ru.dbotthepony.kstarbound.defs.quest.QuestParameter @@ -69,12 +69,13 @@ import ru.dbotthepony.kstarbound.util.Directives import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.util.HashTableInterner import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise +import ru.dbotthepony.kstarbound.util.random.nextRange +import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.world.physics.Poly import java.io.* import java.lang.ref.Cleaner import java.text.DateFormat import java.time.Duration -import java.util.Collections import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executor @@ -87,6 +88,7 @@ import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.LockSupport +import java.util.random.RandomGenerator import kotlin.NoSuchElementException import kotlin.collections.ArrayList @@ -639,5 +641,82 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca } } } + + private fun processNameRules(rules: JsonArray, random: RandomGenerator): String { + if (rules.isEmpty) + return "" + + val meta: JsonObject + val result = StringBuilder() + var mode = "alts" + + var index = 0 + var uppercase = false + + if (rules[0] is JsonObject) { + meta = rules[0] as JsonObject + mode = meta.get("mode", mode) + uppercase = meta.get("titleCase", false) + index++ + } else { + meta = JsonObject() + } + + when (mode) { + "serie" -> { + while (index < rules.size()) { + val entry = rules[index++] + + if (entry is JsonArray) { + result.append(processNameRules(entry, random)) + } else { + result.append(entry.asString) + } + } + } + + "alts" -> { + val i = if (rules.size() == 1) throw RuntimeException("Николай не протоген") else random.nextInt(index, rules.size()) + val entry = rules[i] + + if (entry is JsonArray) { + result.append(processNameRules(entry, random)) + } else { + result.append(entry.asString) + } + } + + "markov" -> { + val source = Registries.markovGenerators.getOrThrow(meta.get("source").asString).value + val lengthRange = gson.fromJson(meta.get("targetLength"), Vector2i::class.java) + val targetLength = random.nextRange(lengthRange) + + result.append(source.generate(random, targetLength, lengthRange.y)) + } + + else -> throw IllegalArgumentException("Unknown name rule mode: $mode") + } + + if (uppercase) { + return result.toString().uppercase() + } + + return result.toString() + } + + fun generateName(asset: String, random: RandomGenerator): String { + val load = loadJsonAsset(asset) as? JsonArray ?: return "missingasset" + + var tries = 500 + var result = "" + + while (tries-- > 0 && (result.isEmpty() || result.lowercase() in Globals.profanityFilter)) + result = processNameRules(load, random) + + return result + } + + fun generateName(asset: String, seed: Long) = generateName(asset, random(seed)) + fun generateName(asset: String) = generateName(asset, System.nanoTime()) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/MarkovTextGenerator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/MarkovTextGenerator.kt new file mode 100644 index 00000000..fb3aa932 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/MarkovTextGenerator.kt @@ -0,0 +1,84 @@ +package ru.dbotthepony.kstarbound.defs + +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap +import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap +import it.unimi.dsi.fastutil.objects.Object2ObjectFunction +import it.unimi.dsi.fastutil.objects.ObjectArraySet +import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kstarbound.Globals +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.util.AssetPathStack +import ru.dbotthepony.kstarbound.util.random.random +import java.util.random.RandomGenerator + +@JsonFactory +data class MarkovTextGenerator( + val name: String, + val prefixSize: Int = 1, + val endSize: Int = 1, + val sourceNames: ImmutableList, +) { + val ends: ImmutableList + val starts: ImmutableList + val chains: ImmutableMap> + + init { + require(prefixSize > 0) { "Invalid prefix size: $prefixSize" } + require(endSize > 0) { "Invalid suffix size: $endSize" } + + val ends = ObjectArraySet() + val starts = ObjectArraySet() + val chains = Object2ObjectArrayMap>() + + for (sourceName in sourceNames) { + if (sourceName.length < prefixSize || sourceName.length < endSize) { + LOGGER.warn("Name $sourceName is too short for Markov name generator with prefix size of $prefixSize and suffix size $endSize; it will be ignored (generator: ${AssetPathStack.remap(name)})") + continue + } + + val sourceName = sourceName.lowercase() + ends.add(sourceName.substring(sourceName.length - endSize, sourceName.length)) + + for (i in 0 .. sourceName.length - prefixSize) { + val prefix = sourceName.substring(i, i + prefixSize) + + if (i == 0) + starts.add(prefix) + + if (i + prefixSize < sourceName.length) { + chains + .computeIfAbsent(prefix, Object2ObjectFunction { ObjectArraySet() }) + .add(sourceName[i + prefixSize].toString()) + } + } + } + + this.ends = ImmutableList.copyOf(ends) + this.starts = ImmutableList.copyOf(starts) + this.chains = chains.entries.stream().map { it.key to ImmutableList.copyOf(it.value) }.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second })) + } + + fun generate(random: RandomGenerator, targetLength: Int, maxLength: Int = targetLength, maxTries: Int = 50): String { + var tries = 0 + var piece: String + + do { + piece = starts.random(random) + + while ( + piece.length < targetLength || + piece.substring(piece.length - endSize, piece.length) !in ends + ) { + val link = piece.substring(piece.length - endSize, piece.length) + piece += (chains[link] ?: break).random(random) + } + } while (tries++ < maxTries && (piece.length > maxLength || piece in Globals.profanityFilter)) + + return piece + } + + companion object { + private val LOGGER = LogManager.getLogger() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt index 2a76eb09..bb3b2474 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt @@ -207,7 +207,7 @@ data class ItemDescriptor( lua.random = random ?: lua.random lua.init(false) - val (config, parameters) = lua.invokeGlobal("build", ref.directory, lua.from(ref.json), lua.from(parameters), level, seed) + val (config, parameters) = lua.invokeGlobal("build", ref.directory + "/", lua.from(ref.json), lua.from(parameters), level, seed) val jConfig = toJsonFromLua(config).asJsonObject val jParameters = toJsonFromLua(parameters).asJsonObject diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt index ee620025..702a74ca 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt @@ -13,6 +13,7 @@ import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.image.Image +import ru.dbotthepony.kstarbound.defs.image.SpriteReference import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.lua.LUA_HINT_ARRAY import ru.dbotthepony.kstarbound.lua.LuaEnvironment @@ -31,7 +32,9 @@ import ru.dbotthepony.kstarbound.lua.luaStub import ru.dbotthepony.kstarbound.lua.nextOptionalFloat import ru.dbotthepony.kstarbound.lua.nextOptionalInteger import ru.dbotthepony.kstarbound.lua.set +import ru.dbotthepony.kstarbound.lua.tableOf import ru.dbotthepony.kstarbound.lua.toLuaInteger +import ru.dbotthepony.kstarbound.util.random.random import kotlin.collections.component1 import kotlin.collections.component2 import kotlin.collections.isNotEmpty @@ -71,7 +74,9 @@ private fun evalFunction2(context: ExecutionContext, name: ByteString, value: Do } private fun imageSize(context: ExecutionContext, name: ByteString) { - context.returnBuffer.setTo(Image.get(name.decode())?.size ?: throw LuaRuntimeException("No such image $name")) + val ref = SpriteReference.create(name.decode()) + val sprite = ref.sprite ?: throw LuaRuntimeException("No such image or sprite $ref") + context.returnBuffer.setTo(context.tableOf(sprite.width, sprite.height)) } private fun imageSpaces(context: ExecutionContext, arguments: ArgumentIterator): StateMachine { @@ -373,7 +378,10 @@ fun provideRootBindings(lua: LuaEnvironment) { table["getMatchingTenants"] = luaFunction(::getMatchingTenants) table["liquidStatusEffects"] = luaFunction(::liquidStatusEffects) - table["generateName"] = luaStub("generateName") + table["generateName"] = luaFunction { asset: ByteString, seed: Number? -> + returnBuffer.setTo(Starbound.generateName(asset.decode(), if (seed == null) lua.random else random(seed.toLong()))) + } + table["questConfig"] = registryDef(Registries.questTemplates) table["npcPortrait"] = luaStub("npcPortrait")