Compare commits

..

30 Commits

Author SHA1 Message Date
Agatha Lovelace dd0626b8e9
make default avatar template actually work in discord 2021-08-27 21:31:54 +03:00
Agatha Lovelace eda95f3dfe
fix system user avatar not being registered 2021-08-27 21:26:26 +03:00
Agatha Lovelace 8df4024320
update readme, fix build errors (hopefully) 2021-08-27 17:38:20 +03:00
NikkyAI e561423884 update cursegradle 2019-04-04 01:11:30 +02:00
NikkyAI b12704d507 remove gson dependency, update dependency checks 2018-12-11 15:02:28 +13:00
NikkyAI 43ce9491b9 fix update check for CI builds 2018-11-28 21:53:20 +01:00
NikkyAI e506f14f76 update gitignore 2018-11-28 20:01:49 +01:00
NikkyAI e410515e05 make location logging also debug instead of info 2018-11-28 18:23:59 +01:00
NikkyAI 4392b442da reduce info logging 2018-11-28 17:57:35 +01:00
NikkyAI ecbb95391f accept older forge versions 2018-11-28 16:24:58 +01:00
NikkyAI 05ed3b4a9b do not use the same arguments as nonce and perm level 2018-11-28 15:23:47 +01:00
NikkyAI fe9cb31d72 little bit of debugging help 2018-11-28 15:15:39 +01:00
NikkyAI ef66087044 fix potential library mismatch.. again 2018-11-25 23:37:07 +01:00
NikkyAI 4ce29f0eb6 bumping forgelin, kotlin and libraries version 2018-11-25 00:34:03 +01:00
NikkyAI a0b0e3d24d fix strict json parsing 2018-11-24 23:39:54 +01:00
NikkyAI 04bc439880 make sure debug logging is actually debug 2018-11-23 19:38:18 +01:00
NikkyAI d4802d372f fix idiotic errors in config 2018-11-23 04:00:38 +01:00
NikkyAI a12a9ae74f make json parsing nonstrict 2018-11-18 02:19:41 +01:00
NikkyAI 4d854e5af8 update coroutines 2018-11-18 02:04:19 +01:00
NikkyAI 1bcafb47e1 fix dependency mismatch with Forgelin 2018-11-18 01:59:47 +01:00
NikkyAI 16c66782c6 forgot to shadow kotlinx-serialization-runtime 2018-11-18 00:05:21 +01:00
NikkyAI a2a24c6ed9 bump Fuel version 2018-11-17 14:57:36 +01:00
NikkyAI 2091d59219 fix async mess 2018-11-17 13:21:12 +01:00
NikkyAI f1cc1349ef version bump 2018-11-17 11:33:00 +01:00
NikkyAI 34887a833c update to kotlin 1.3
[1.7.10] fixed send to player NPE
2018-11-02 04:22:56 +01:00
nikky 3082d3e592 update Jankson 2018-08-26 18:47:35 +02:00
nikky 79c9ac1572 revert dropping 1.7.10, adding 1.9.4 instead of 1.10/1.11 2018-08-16 23:54:50 +02:00
nikky 7818afc920 move api into core 2018-08-15 18:42:43 +02:00
nikky e30e8132ca update 1.12.2 with locations 2018-08-15 00:20:21 +02:00
nikky 6cdd7ade7c drop 1.7.10 support 2018-08-08 20:44:42 +02:00
85 changed files with 2644 additions and 1819 deletions

6
.gitignore vendored
View File

@ -87,10 +87,6 @@ gradle-app.setting
# Cache of project
.gradletasknamecache
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
# gradle/wrapper/gradle-wrapper.properties
# End of https://www.gitignore.io/api/gradle,kotlin,intellij+iml
### MatterLink ###
@ -102,3 +98,5 @@ run/
\.floo
\.flooignore
bugreport/

3
.gitmodules vendored
View File

@ -1,6 +1,3 @@
[submodule "api"]
path = api
url = https://github.com/NikkyAI/MatterLinkApi.git
[submodule "Jankson"]
path = Jankson
url = https://github.com/falkreon/Jankson.git

4
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"java.configuration.updateBuildConfiguration": "automatic",
"java.home": "/usr/lib/jvm/java-8-openjdk/"
}

View File

@ -1,3 +0,0 @@
mc_version = 1.10.2
mcp_mappings = stable_29
forge_version = 12.18.3.2185

View File

@ -1,108 +0,0 @@
package matterlink
import matterlink.api.ApiMessage.Companion.USER_ACTION
import matterlink.config.cfg
import matterlink.handlers.*
import net.minecraft.command.server.CommandBroadcast
import net.minecraft.command.server.CommandEmote
import net.minecraft.entity.player.EntityPlayer
import net.minecraft.entity.player.EntityPlayerMP
import net.minecraft.server.dedicated.DedicatedServer
import net.minecraftforge.event.CommandEvent
import net.minecraftforge.event.ServerChatEvent
import net.minecraftforge.event.entity.living.LivingDeathEvent
import net.minecraftforge.event.entity.player.AchievementEvent
import net.minecraftforge.fml.common.Mod
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
import net.minecraftforge.fml.common.gameevent.PlayerEvent
import net.minecraftforge.fml.common.gameevent.TickEvent
//FORGE-DEPENDENT
@Mod.EventBusSubscriber
object EventHandler {
//MC-VERSION & FORGE DEPENDENT
@SubscribeEvent
@JvmStatic
fun progressEvent(e: AchievementEvent) {
val achievement = e.achievement
val entityPlayer = e.entityPlayer as? EntityPlayerMP ?: return
val statFile = entityPlayer.statFile
if (!statFile.canUnlockAchievement(achievement) || statFile.hasAchievementUnlocked(achievement)) {
return
}
ProgressHandler.handleProgress(
name = e.entityPlayer.displayName.unformattedText,
message = "has earned the achievement",
display = e.achievement.statName.unformattedText
)
}
//FORGE-DEPENDENT
@SubscribeEvent
@JvmStatic
fun chatEvent(e: ServerChatEvent) {
if(e.isCanceled) return
e.isCanceled = ChatProcessor.sendToBridge(
user = e.player.displayName.unformattedText,
msg = e.message,
event = "",
uuid = e.player.gameProfile.id
)
}
//FORGE-DEPENDENT
@SubscribeEvent
@JvmStatic
fun commandEvent(e: CommandEvent) {
val sender = when {
e.sender is DedicatedServer -> cfg.outgoing.systemUser
else -> e.sender.displayName.unformattedText
}
val args = e.parameters.joinToString(" ")
val type = when {
e.command is CommandEmote -> USER_ACTION
e.command.name == "me" -> USER_ACTION
e.command is CommandBroadcast -> ""
else -> return
}
ChatProcessor.sendToBridge(user = sender, msg = args, event = type)
}
//FORGE-DEPENDENT
@SubscribeEvent
@JvmStatic
fun deathEvent(e: LivingDeathEvent) {
if (e.entityLiving is EntityPlayer) {
DeathHandler.handleDeath(
player = e.entityLiving.displayName.unformattedText,
deathMessage = e.entityLiving.combatTracker.deathMessage.unformattedText,
damageType = e.source.damageType
)
}
}
//FORGE-DEPENDENT
@SubscribeEvent
@JvmStatic
fun joinEvent(e: PlayerEvent.PlayerLoggedInEvent) {
JoinLeaveHandler.handleJoin(e.player.displayName.unformattedText)
}
//FORGE-DEPENDENT
@SubscribeEvent
@JvmStatic
fun leaveEvent(e: PlayerEvent.PlayerLoggedOutEvent) {
JoinLeaveHandler.handleLeave(e.player.displayName.unformattedText)
}
//FORGE-DEPENDENT
@SubscribeEvent
@JvmStatic
fun serverTickEvent(e: TickEvent.ServerTickEvent) {
if (e.phase == TickEvent.Phase.END)
TickHandler.handleTick()
}
}

View File

@ -1,143 +0,0 @@
import net.minecraftforge.gradle.user.TaskSourceCopy
buildscript {
repositories {
jcenter()
maven {
url = 'http://files.minecraftforge.net/maven'
}
mavenCentral()
maven {
url = 'https://oss.sonatype.org/content/groups/public'
}
maven {
url = 'https://plugins.gradle.org/m2/'
}
}
dependencies {
classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '2.2-SNAPSHOT'
classpath group: 'com.github.jengelman.gradle.plugins', name: 'shadow', version: shadow_version
classpath group: 'gradle.plugin.com.matthewprenger', name: 'CurseGradle', version: cursegradle_version
}
}
apply plugin: 'net.minecraftforge.gradle.forge'
apply plugin: 'com.github.johnrengelman.shadow'
apply plugin: 'com.matthewprenger.cursegradle'
version = project.mc_version + '-' + project.mod_version
archivesBaseName = project.mod_name
sourceCompatibility = targetCompatibility = '1.8'
dependencies {
compile project(':core')
compile group: 'net.shadowfacts', name: 'Forgelin', version: project.forgelin_version
}
shadowJar {
classifier = ''
relocate 'blue.endless', 'matterlink.repack.blue.endless'
dependencies {
include project(':core')
include project(':api')
include project(':Jankson')
}
exclude 'dummyThing'
}
// Mad hacks to make source replacements work for Kotlin
// source: https://github.com/PaleoCrafter/VanillaImmersion/blob/ee82ecafb76659cf7d7822a722c8f63f43f41d01/build.gradle#L119
for (set in sourceSets) {
def taskName = "source${set.name.capitalize()}Kotlin"
def dir = new File(project.getBuildDir(), "sources/${set.name}/kotlin")
task(taskName, type: TaskSourceCopy) {
source = set.getKotlin()
output = dir
}
def compileTask = tasks[set.getCompileTaskName('kotlin')]
compileTask.source = dir
compileTask.dependsOn taskName
def dirPath = dir.toPath()
compileKotlin.include {
return it.file.toPath().startsWith(dirPath)
}
}
sourceJar.from sourceSets.main.kotlin
minecraft {
version = project.mc_version + '-' + project.forge_version
runDir = 'run'
mappings = project.mcp_mappings
replaceIn 'Constants.kt'
replace '@MODVERSION@', project.mod_version
replace '@MCVERSION@', project.mc_version
replace '@FORGELIN-VERSION@', project.forgelin_version
replace '@FORGE-VERSION@', project.forge_version
replace '-1//@BUILD_NUMBER@', System.env.BUILD_NUMBER ?: -1
}
processResources {
// this will ensure that this task is redone when the versions change.
inputs.property 'version', project.mod_version
inputs.property 'mcversion', project.minecraft.version
// replace stuff in mcmod.info, nothing else
from(project(':core').sourceSets.main.resources.srcDirs) {
include 'mcmod.info'
// replace version and mcversion
expand 'version': project.mod_version, 'mcversion': project.minecraft.version
}
// copy everything else except the mcmod.info
from(project(':core').sourceSets.main.resources.srcDirs) {
exclude 'mcmod.info'
}
}
sourceJar {
classifier = 'sources'
// copy all the minecraftforge specific classes
from sourceSets.main.allSource
// copy everything else except the mcmod.info
from(project(':core').sourceSets.main.allSource) {
exclude 'mcmod.info'
}
}
reobf {
shadowJar { mappingType = 'SEARGE' }
}
tasks.shadowJar.finalizedBy reobfShadowJar
curseforge {
if (project.hasProperty('CURSEFORGE_API_TOKEN') && project.hasProperty('release')) {
apiKey = CURSEFORGE_API_TOKEN
}
project {
id = project.curse_id
releaseType = project.curse_release_type
if (project.hasProperty('changelog_file')) {
println("changelog = $changelog_file")
changelogType = 'markdown'
changelog = file(changelog_file)
}
relations {
requiredLibrary 'shadowfacts-forgelin'
}
mainArtifact(shadowJar) {
displayName = "MatterLink $version"
}
}
}

View File

@ -1,3 +0,0 @@
mc_version = 1.11.2
mcp_mappings = stable_32
forge_version = 13.20.1.2386

View File

@ -1,8 +0,0 @@
package matterlink
const val MODID = "matterlink"
const val NAME = "MatterLink"
const val MODVERSION = "@MODVERSION@"
const val MCVERSION = "@MCVERSION@"
const val DEPENDENCIES = "required-after:forgelin@[@FORGELIN-VERSION@,);required-after:forge@[@FORGE-VERSION@,);"
const val BUILD_NUMBER = -1//@BUILD_NUMBER@

View File

@ -1,109 +0,0 @@
package matterlink
import matterlink.api.ApiMessage.Companion.USER_ACTION
import matterlink.config.cfg
import matterlink.handlers.*
import net.minecraft.command.server.CommandBroadcast
import net.minecraft.command.server.CommandEmote
import net.minecraft.entity.player.EntityPlayer
import net.minecraft.entity.player.EntityPlayerMP
import net.minecraft.server.dedicated.DedicatedServer
import net.minecraftforge.event.CommandEvent
import net.minecraftforge.event.ServerChatEvent
import net.minecraftforge.event.entity.living.LivingDeathEvent
import net.minecraftforge.event.entity.player.AchievementEvent
import net.minecraftforge.fml.common.Mod
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
import net.minecraftforge.fml.common.gameevent.PlayerEvent
import net.minecraftforge.fml.common.gameevent.TickEvent
//FORGE-DEPENDENT
@Mod.EventBusSubscriber
object EventHandler {
//MC-VERSION & FORGE DEPENDENT
@SubscribeEvent
@JvmStatic
fun progressEvent(e: AchievementEvent) {
val achievement = e.achievement
val entityPlayer = e.entityPlayer as? EntityPlayerMP ?: return
val statFile = entityPlayer.statFile
if (!statFile.canUnlockAchievement(achievement) || statFile.hasAchievementUnlocked(achievement)) {
return
}
ProgressHandler.handleProgress(
name = e.entityPlayer.displayName.unformattedText,
message = "has earned the achievement",
display = e.achievement.statName.unformattedText
)
}
//FORGE-DEPENDENT
@SubscribeEvent
@JvmStatic
fun chatEvent(e: ServerChatEvent) {
if(e.isCanceled) return
e.isCanceled = ChatProcessor.sendToBridge(
user = e.player.displayName.unformattedText,
msg = e.message,
event = "",
uuid = e.player.gameProfile.id
)
}
//FORGE-DEPENDENT
@SubscribeEvent
@JvmStatic
fun commandEvent(e: CommandEvent) {
val sender = when {
e.sender is DedicatedServer -> cfg.outgoing.systemUser
else -> e.sender.displayName.unformattedText
}
val args = e.parameters.joinToString(" ")
val type = when {
e.command is CommandEmote -> USER_ACTION
e.command.name == "me" -> USER_ACTION
e.command is CommandBroadcast -> ""
else -> return
}
ChatProcessor.sendToBridge(user = sender, msg = args, event = type)
}
//FORGE-DEPENDENT
@SubscribeEvent
@JvmStatic
fun deathEvent(e: LivingDeathEvent) {
if (e.entityLiving is EntityPlayer) {
DeathHandler.handleDeath(
player = e.entityLiving.displayName.unformattedText,
deathMessage = e.entityLiving.combatTracker.deathMessage.unformattedText,
damageType = e.source.damageType
)
}
}
//FORGE-DEPENDENT
@SubscribeEvent
@JvmStatic
fun joinEvent(e: PlayerEvent.PlayerLoggedInEvent) {
JoinLeaveHandler.handleJoin(e.player.displayName.unformattedText)
}
//FORGE-DEPENDENT
@SubscribeEvent
@JvmStatic
fun leaveEvent(e: PlayerEvent.PlayerLoggedOutEvent) {
JoinLeaveHandler.handleLeave(e.player.displayName.unformattedText)
}
//FORGE-DEPENDENT
@SubscribeEvent
@JvmStatic
fun serverTickEvent(e: TickEvent.ServerTickEvent) {
if (e.phase == TickEvent.Phase.END)
TickHandler.handleTick()
}
}

View File

@ -1,125 +0,0 @@
package matterlink
import com.mojang.authlib.GameProfile
import jline.internal.Log.warn
import matterlink.bridge.command.IBridgeCommand
import matterlink.command.MatterLinkCommand
import matterlink.command.MatterLinkCommandSender
import matterlink.config.BaseConfig
import matterlink.config.cfg
import net.minecraft.entity.ai.EntityMoveHelper
import net.minecraft.entity.player.EntityPlayerMP
import net.minecraft.util.text.TextComponentString
import net.minecraftforge.common.ForgeVersion
import net.minecraftforge.fml.common.FMLCommonHandler
import net.minecraftforge.fml.common.Mod
import net.minecraftforge.fml.common.event.FMLInitializationEvent
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent
import net.minecraftforge.fml.common.event.FMLServerStartingEvent
import net.minecraftforge.fml.common.event.FMLServerStoppingEvent
import org.apache.logging.log4j.Level
import org.apache.logging.log4j.Logger
import java.util.*
@Mod(
modid = MODID,
name = NAME, version = MODVERSION,
serverSideOnly = true,
useMetadata = true,
acceptableRemoteVersions = "*",
modLanguageAdapter = "net.shadowfacts.forgelin.KotlinAdapter",
dependencies = DEPENDENCIES
)
object MatterLink : IMatterLink() {
init {
instance = this
}
@Mod.EventHandler
fun preInit(event: FMLPreInitializationEvent) {
logger = event.modLog as org.apache.logging.log4j.core.Logger
logger.info("Building bridge!")
cfg = BaseConfig(event.modConfigurationDirectory).load()
}
@Mod.EventHandler
fun init(event: FMLInitializationEvent) {
this.registerBridgeCommands()
}
@Mod.EventHandler
fun serverStarting(event: FMLServerStartingEvent) {
logger.debug("Registering server commands")
event.registerServerCommand(MatterLinkCommand())
start()
}
@Mod.EventHandler
fun serverStopping(event: FMLServerStoppingEvent) {
stop()
}
//FORGE-DEPENDENT
override fun wrappedSendToPlayers(msg: String) {
FMLCommonHandler.instance().minecraftServerInstance.playerList.sendMessage(TextComponentString(msg))
}
override fun wrappedSendToPlayer(username: String, msg: String) {
val profile = profileByName(username) ?: run {
logger.error("cannot find player by name $username")
return
}
val player = playerByProfile(profile) ?: run {
logger.error("${profile.name} is not online")
return
}
player.sendMessage(TextComponentString(msg))
}
override fun wrappedSendToPlayer(uuid: UUID, msg: String) {
val profile = profileByUUID(uuid) ?: run {
logger.error("cannot find player by uuid $uuid")
return
}
val player = playerByProfile(profile) ?: run {
logger.error("${profile.name} is not online")
return
}
player.sendMessage(TextComponentString(msg))
}
override fun isOnline(username: String) = FMLCommonHandler.instance().minecraftServerInstance.onlinePlayerNames.contains(username)
private fun playerByProfile(gameProfile: GameProfile): EntityPlayerMP? = FMLCommonHandler.instance().minecraftServerInstance.playerList.getPlayerByUUID(gameProfile.id)
private fun profileByUUID(uuid: UUID): GameProfile? = try {
FMLCommonHandler.instance().minecraftServerInstance.playerProfileCache.getProfileByUUID(uuid)
} catch (e: IllegalArgumentException) {
logger.warn("cannot find profile by uuid $uuid")
null
}
private fun profileByName(username: String): GameProfile? = try {
FMLCommonHandler.instance().minecraftServerInstance.playerProfileCache.getGameProfileForUsername(username)
} catch (e: IllegalArgumentException) {
logger.warn("cannot find profile by username $username")
null
}
override fun nameToUUID(username: String): UUID? = profileByName(username)?.id
override fun uuidToName(uuid: UUID): String? = profileByUUID(uuid)?.name
override fun commandSenderFor(
user: String,
env: IBridgeCommand.CommandEnvironment,
op: Boolean
) = MatterLinkCommandSender(user, env, op)
override val mcVersion: String = MCVERSION
override val modVersion: String = MODVERSION
override val buildNumber = BUILD_NUMBER
override val forgeVersion = ForgeVersion.getVersion()
}

View File

@ -1,42 +0,0 @@
package matterlink.command
import matterlink.logger
import net.minecraft.command.CommandBase
import net.minecraft.command.ICommandSender
import net.minecraft.command.WrongUsageException
import net.minecraft.entity.player.EntityPlayer
import net.minecraft.server.MinecraftServer
import net.minecraft.util.text.TextComponentString
class AuthCommand : CommandBase() {
override fun getName(): String {
return CommandCoreAuth.name
}
override fun getUsage(sender: ICommandSender): String {
return CommandCoreAuth.usage
}
override fun getAliases(): List<String> {
return CommandCoreAuth.aliases
}
override fun getRequiredPermissionLevel(): Int {
return 0
}
override fun execute(server: MinecraftServer, sender: ICommandSender, args: Array<String>) {
if (args.isEmpty()) {
throw WrongUsageException("Invalid command! Valid uses: ${this.getUsage(sender)}")
}
val uuid = (sender as? EntityPlayer)?.uniqueID?.toString()
val reply = CommandCoreAuth.execute(args, sender.name, uuid)
if (reply.isNotEmpty() && sender.sendCommandFeedback()) {
sender.sendMessage(TextComponentString(reply))
}
}
}

View File

@ -1,36 +0,0 @@
package matterlink.command
import net.minecraft.command.CommandBase
import net.minecraft.command.ICommandSender
import net.minecraft.command.WrongUsageException
import net.minecraft.entity.player.EntityPlayer
import net.minecraft.server.MinecraftServer
import net.minecraft.util.text.TextComponentString
class MatterLinkCommand : CommandBase() {
override fun getName(): String {
return CommandCoreML.name
}
override fun getUsage(sender: ICommandSender): String {
return CommandCoreML.usage
}
override fun getAliases(): List<String> {
return CommandCoreML.aliases
}
override fun execute(server: MinecraftServer, sender: ICommandSender, args: Array<String>) {
if (args.isEmpty()) {
throw WrongUsageException("Invalid command! Valid uses: ${this.getUsage(sender)}")
}
val uuid = (sender as? EntityPlayer)?.uniqueID?.toString()
val reply = CommandCoreML.execute(args, sender.name, uuid)
if (reply.isNotEmpty() && sender.sendCommandFeedback()) {
sender.sendMessage(TextComponentString(reply))
}
}
}

View File

@ -1,66 +0,0 @@
package matterlink.command
import matterlink.bridge.command.IBridgeCommand
import matterlink.bridge.command.IMinecraftCommandSender
import net.minecraft.command.CommandResultStats
import net.minecraft.command.ICommandSender
import net.minecraft.entity.Entity
import net.minecraft.server.MinecraftServer
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Vec3d
import net.minecraft.util.text.ITextComponent
import net.minecraft.util.text.TextComponentString
import net.minecraft.world.World
import net.minecraftforge.fml.common.FMLCommonHandler
import javax.annotation.Nonnull
class MatterLinkCommandSender(
user: String,
env: IBridgeCommand.CommandEnvironment,
op: Boolean) : IMinecraftCommandSender(user, env, op), ICommandSender {
override fun execute(cmdString: String): Boolean {
return 0 < FMLCommonHandler.instance().minecraftServerInstance.commandManager.executeCommand(
this,
cmdString
).apply {
sendReply(cmdString)
}
}
override fun getDisplayName(): ITextComponent {
return TextComponentString(displayName)
}
override fun getName() = accountName
override fun getEntityWorld(): World {
return FMLCommonHandler.instance().minecraftServerInstance.getWorld(0)
}
override fun canUseCommand(permLevel: Int, commandName: String): Boolean {
//we now do permissions checking on our end
return canExecute(commandName)
}
override fun getServer(): MinecraftServer? {
return FMLCommonHandler.instance().minecraftServerInstance
}
override fun sendMessage(@Nonnull component: ITextComponent?) {
appendReply(component!!.unformattedComponentText)
}
override fun sendCommandFeedback(): Boolean {
return true
}
//WtfMojangWhy
override fun getPosition(): BlockPos = BlockPos.ORIGIN
override fun setCommandStat(type: CommandResultStats.Type?, amount: Int) {}
override fun getPositionVector(): Vec3d = Vec3d.ZERO
override fun getCommandSenderEntity(): Entity? = null
}

View File

@ -14,38 +14,35 @@ buildscript {
}
dependencies {
classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '2.3-SNAPSHOT'
classpath group: 'com.github.jengelman.gradle.plugins', name: 'shadow', version: shadow_version
classpath group: 'gradle.plugin.com.matthewprenger', name: 'CurseGradle', version: cursegradle_version
classpath group: 'com.github.jengelman.gradle.plugins', name: 'shadow', version: shadowVersion
classpath group: 'gradle.plugin.com.matthewprenger', name: 'CurseGradle', version: cursegradleVersion
classpath group: 'com.vanniktech', name: 'gradle-dependency-graph-generator-plugin', version: '0.5.0'
}
}
apply plugin: 'net.minecraftforge.gradle.forge'
apply plugin: 'com.github.johnrengelman.shadow'
apply plugin: 'com.matthewprenger.cursegradle'
apply plugin: "com.vanniktech.dependency.graph.generator"
version = project.mc_version + '-' + project.mod_version
version = project.mc_version + '-' + project.modVersion
archivesBaseName = project.mod_name
archivesBaseName = project.modName
sourceCompatibility = targetCompatibility = '1.8'
dependencies {
compile project(':core')
compile group: 'net.shadowfacts', name: 'Forgelin', version: project.forgelin_version
shadow (project(path: ':core', configuration: 'shadow')) { transitive = false }
compile group: 'net.shadowfacts', name: 'Forgelin', version: project.forgelinVersion
}
shadowJar {
classifier = ''
relocate 'blue.endless', 'matterlink.repack.blue.endless'
dependencies {
include project(':core')
include project(':api')
include project(':Jankson')
}
exclude 'dummyThing'
configurations = [project.configurations.shadow]
}
minecraft {
@ -55,16 +52,16 @@ minecraft {
mappings = project.mcp_mappings
replaceIn 'Constants.kt'
replace '@MODVERSION@', project.mod_version
replace '@MODVERSION@', project.modVersion
replace '@MCVERSION@', project.mc_version
replace '@FORGELIN-VERSION@', project.forgelin_version
replace '@FORGELIN-VERSION@', project.forgelinVersion
replace '@FORGE-VERSION@', project.forge_version
replace '-1//@BUILD_NUMBER@', System.env.BUILD_NUMBER ?: -1
}
processResources {
// this will ensure that this task is redone when the versions change.
inputs.property "version", project.mod_version
inputs.property "version", project.modVersion
inputs.property "mcversion", project.minecraft.version
// replace stuff in mcmod.info, nothing else
@ -72,7 +69,7 @@ processResources {
include 'mcmod.info'
// replace version and mcversion
expand 'version': project.mod_version, 'mcversion': project.minecraft.version
expand 'version': project.modVersion, 'mcversion': project.minecraft.version
}
// copy everything else except the mcmod.info
@ -96,7 +93,6 @@ sourceJar {
reobf {
shadowJar { mappingType = 'SEARGE' }
}
tasks.shadowJar.finalizedBy reobfShadowJar
curseforge {
@ -104,8 +100,8 @@ curseforge {
apiKey = CURSEFORGE_API_TOKEN
}
project {
id = project.curse_id
releaseType = project.curse_release_type
id = project.curseId
releaseType = project.curseReleaseType
if (project.hasProperty('changelog_file')) {
println("changelog = $changelog_file")
changelogType = 'markdown'
@ -119,3 +115,7 @@ curseforge {
}
}
}
runServer {
outputs.upToDateWhen { false }
}

View File

@ -1,3 +1,3 @@
mc_version = 1.12.2
mcp_mappings = snapshot_20171003
mcp_mappings = stable_39
forge_version = 14.23.1.2599

View File

@ -1,8 +1,13 @@
package matterlink
import matterlink.api.ApiMessage.Companion.USER_ACTION
import kotlinx.coroutines.runBlocking
import matterlink.config.cfg
import matterlink.handlers.*
import matterlink.handlers.ChatEvent
import matterlink.handlers.ChatProcessor
import matterlink.handlers.DeathHandler
import matterlink.handlers.JoinLeaveHandler
import matterlink.handlers.ProgressHandler
import matterlink.handlers.TickHandler
import net.minecraft.command.server.CommandBroadcast
import net.minecraft.command.server.CommandEmote
import net.minecraft.entity.player.EntityPlayer
@ -23,55 +28,80 @@ object EventHandler {
//MC-VERSION & FORGE DEPENDENT
@SubscribeEvent
@JvmStatic
fun progressEvent(e: AdvancementEvent) {
if (e.advancement.display == null) return
fun progressEvent(e: AdvancementEvent) = runBlocking {
if (e.advancement.display == null) return@runBlocking
ProgressHandler.handleProgress(
name = e.entityPlayer.displayName.unformattedText,
message = "has made the advancement",
display = e.advancement.displayText.unformattedText
name = e.entityPlayer.displayName.unformattedText,
message = "has made the advancement",
display = e.advancement.displayText.unformattedText,
x = e.entityPlayer.posX.toInt(),
y = e.entityPlayer.posY.toInt(),
z = e.entityPlayer.posZ.toInt(),
dimension = e.entityPlayer.dimension
)
}
//FORGE-DEPENDENT
@SubscribeEvent
@JvmStatic
fun chatEvent(e: ServerChatEvent) {
if(e.isCanceled) return
fun chatEvent(e: ServerChatEvent) = runBlocking {
if (e.isCanceled) return@runBlocking
e.isCanceled = ChatProcessor.sendToBridge(
user = e.player.displayName.unformattedText,
msg = e.message,
event = "",
uuid = e.player.gameProfile.id
user = e.player.displayName.unformattedText,
msg = e.message,
x = e.player.posX.toInt(),
y = e.player.posY.toInt(),
z = e.player.posZ.toInt(),
dimension = e.player.dimension,
event = ChatEvent.PLAIN,
uuid = e.player.gameProfile.id
)
}
//FORGE-DEPENDENT
@SubscribeEvent
@JvmStatic
fun commandEvent(e: CommandEvent) {
fun commandEvent(e: CommandEvent) = runBlocking {
val sender = when {
e.sender is DedicatedServer -> cfg.outgoing.systemUser
else -> e.sender.displayName.unformattedText
}
val args = e.parameters.joinToString(" ")
val type = when {
e.command is CommandEmote -> USER_ACTION
e.command.name.equals("me", true) -> USER_ACTION
e.command is CommandBroadcast -> ""
else -> return
val type = with(e.command) {
when {
this is CommandEmote || name.equals("me", true) -> ChatEvent.ACTION
this is CommandBroadcast || name.equals("say", true) -> ChatEvent.BROADCAST
else -> return@runBlocking
}
}
ChatProcessor.sendToBridge(user = sender, msg = args, event = type)
ChatProcessor.sendToBridge(
user = sender,
msg = args,
event = type,
x = e.sender.position.x,
y = e.sender.position.y,
z = e.sender.position.z,
dimension = when {
e.sender is DedicatedServer -> null
else -> e.sender.commandSenderEntity?.dimension ?: e.sender.entityWorld.provider.dimension
}
)
}
//FORGE-DEPENDENT
@SubscribeEvent
@JvmStatic
fun deathEvent(e: LivingDeathEvent) {
fun deathEvent(e: LivingDeathEvent) = runBlocking {
if (e.entityLiving is EntityPlayer) {
DeathHandler.handleDeath(
player = e.entityLiving.displayName.unformattedText,
deathMessage = e.entityLiving.combatTracker.deathMessage.unformattedText,
damageType = e.source.damageType
player = e.entityLiving.displayName.unformattedText,
deathMessage = e.entityLiving.combatTracker.deathMessage.unformattedText,
damageType = e.source.damageType,
x = e.entityLiving.posX.toInt(),
y = e.entityLiving.posY.toInt(),
z = e.entityLiving.posZ.toInt(),
dimension = e.entityLiving.dimension
)
}
}
@ -79,22 +109,35 @@ object EventHandler {
//FORGE-DEPENDENT
@SubscribeEvent
@JvmStatic
fun joinEvent(e: PlayerEvent.PlayerLoggedInEvent) {
JoinLeaveHandler.handleJoin(e.player.displayName.unformattedText)
fun joinEvent(e: PlayerEvent.PlayerLoggedInEvent) = runBlocking {
JoinLeaveHandler.handleJoin(
player = e.player.displayName.unformattedText,
x = e.player.posX.toInt(),
y = e.player.posY.toInt(),
z = e.player.posZ.toInt(),
dimension = e.player.dimension
)
}
//FORGE-DEPENDENT
@SubscribeEvent
@JvmStatic
fun leaveEvent(e: PlayerEvent.PlayerLoggedOutEvent) {
JoinLeaveHandler.handleLeave(e.player.displayName.unformattedText)
fun leaveEvent(e: PlayerEvent.PlayerLoggedOutEvent) = runBlocking {
JoinLeaveHandler.handleLeave(
player = e.player.displayName.unformattedText,
x = e.player.posX.toInt(),
y = e.player.posY.toInt(),
z = e.player.posZ.toInt(),
dimension = e.player.dimension
)
}
//FORGE-DEPENDENT
@SubscribeEvent
@JvmStatic
fun serverTickEvent(e: TickEvent.ServerTickEvent) {
fun serverTickEvent(e: TickEvent.ServerTickEvent) = runBlocking {
if (e.phase == TickEvent.Phase.END)
TickHandler.handleTick()
}
}

View File

@ -1,7 +1,7 @@
package matterlink
import com.mojang.authlib.GameProfile
import jline.internal.Log.warn
import kotlinx.coroutines.runBlocking
import matterlink.bridge.command.IBridgeCommand
import matterlink.command.AuthCommand
import matterlink.command.MatterLinkCommand
@ -19,17 +19,17 @@ import net.minecraftforge.fml.common.event.FMLServerStartingEvent
import net.minecraftforge.fml.common.event.FMLServerStoppingEvent
import org.apache.logging.log4j.Level
import org.apache.logging.log4j.core.config.Configurator
import java.util.*
import java.util.UUID
@Mod(
modid = MODID,
name = NAME, version = MODVERSION,
serverSideOnly = true,
useMetadata = true,
acceptableRemoteVersions = "*",
modLanguageAdapter = "net.shadowfacts.forgelin.KotlinAdapter",
dependencies = DEPENDENCIES
modid = MODID,
name = NAME, version = MODVERSION,
serverSideOnly = true,
useMetadata = true,
acceptableRemoteVersions = "*",
modLanguageAdapter = "net.shadowfacts.forgelin.KotlinAdapter",
dependencies = DEPENDENCIES
)
object MatterLink : IMatterLink() {
@ -40,7 +40,16 @@ object MatterLink : IMatterLink() {
@Mod.EventHandler
fun preInit(event: FMLPreInitializationEvent) {
logger = event.modLog as org.apache.logging.log4j.core.Logger
logger = with(event.modLog) {
object : Logger {
override fun info(message: String) = this@with.info(message)
override fun debug(message: String) = this@with.debug(message)
override fun error(message: String) = this@with.error(message)
override fun warn(message: String) = this@with.warn(message)
override fun trace(message: String) = this@with.trace(message)
}
}
logger.info("Building bridge!")
cfg = BaseConfig(event.modConfigurationDirectory).load()
@ -48,19 +57,22 @@ object MatterLink : IMatterLink() {
@Mod.EventHandler
fun init(event: FMLInitializationEvent) {
logger.debug("Registering bridge commands")
this.registerBridgeCommands()
}
@Mod.EventHandler
fun serverStarting(event: FMLServerStartingEvent) {
logger.debug("Registering server commands")
event.registerServerCommand(MatterLinkCommand())
event.registerServerCommand(AuthCommand())
start()
event.registerServerCommand(MatterLinkCommand)
event.registerServerCommand(AuthCommand)
runBlocking {
start()
}
}
@Mod.EventHandler
fun serverStopping(event: FMLServerStoppingEvent) {
fun serverStopping(event: FMLServerStoppingEvent) = runBlocking {
stop()
}
@ -93,32 +105,42 @@ object MatterLink : IMatterLink() {
player.sendMessage(TextComponentString(msg))
}
override fun isOnline(username: String) = FMLCommonHandler.instance().minecraftServerInstance.onlinePlayerNames.contains(username)
override fun isOnline(username: String) =
FMLCommonHandler.instance().minecraftServerInstance.onlinePlayerNames.contains(username)
private fun playerByProfile(gameProfile: GameProfile): EntityPlayerMP? = FMLCommonHandler.instance().minecraftServerInstance.playerList.getPlayerByUUID(gameProfile.id)
private fun playerByProfile(gameProfile: GameProfile): EntityPlayerMP? =
FMLCommonHandler.instance().minecraftServerInstance.playerList.getPlayerByUUID(gameProfile.id)
private fun profileByUUID(uuid: UUID): GameProfile? = try {
FMLCommonHandler.instance().minecraftServerInstance.playerProfileCache.getProfileByUUID(uuid)
} catch (e: IllegalArgumentException) {
warn("cannot find profile by uuid $uuid")
logger.warn("cannot find profile by uuid $uuid")
null
}
private fun profileByName(username: String): GameProfile? = try {
FMLCommonHandler.instance().minecraftServerInstance.playerProfileCache.getGameProfileForUsername(username)
} catch (e: IllegalArgumentException) {
warn("cannot find profile by username $username")
logger.warn("cannot find profile by username $username")
null
}
override fun collectPlayers(area: Area): Set<UUID> {
val players = FMLCommonHandler.instance().minecraftServerInstance.playerList.players.filter {
(area.allDimensions || area.dimensions.contains(it.dimension))
&& area.testInBounds(it.posX.toInt(), it.posY.toInt(), it.posZ.toInt())
}
return players.map { it.uniqueID }.toSet()
}
override fun nameToUUID(username: String): UUID? = profileByName(username)?.id
override fun uuidToName(uuid: UUID): String? = profileByUUID(uuid)?.name
override fun commandSenderFor(
user: String,
env: IBridgeCommand.CommandEnvironment,
op: Boolean
user: String,
env: IBridgeCommand.CommandEnvironment,
op: Boolean
) = MatterLinkCommandSender(user, env, op)
override val mcVersion: String = MCVERSION

View File

@ -1,6 +1,5 @@
package matterlink.command
import matterlink.logger
import net.minecraft.command.CommandBase
import net.minecraft.command.ICommandSender
import net.minecraft.command.WrongUsageException
@ -9,7 +8,7 @@ import net.minecraft.server.MinecraftServer
import net.minecraft.util.text.TextComponentString
class AuthCommand : CommandBase() {
object AuthCommand : CommandBase() {
override fun getName(): String {
return CommandCoreAuth.name
}

View File

@ -1,6 +1,6 @@
package matterlink.command
import matterlink.logger
import kotlinx.coroutines.runBlocking
import net.minecraft.command.CommandBase
import net.minecraft.command.ICommandSender
import net.minecraft.command.WrongUsageException
@ -9,7 +9,7 @@ import net.minecraft.server.MinecraftServer
import net.minecraft.util.text.TextComponentString
class MatterLinkCommand : CommandBase() {
object MatterLinkCommand : CommandBase() {
override fun getName(): String {
return CommandCoreML.name
}
@ -22,9 +22,9 @@ class MatterLinkCommand : CommandBase() {
return CommandCoreML.aliases
}
override fun execute(server: MinecraftServer, sender: ICommandSender, args: Array<String>) {
override fun execute(server: MinecraftServer, sender: ICommandSender, args: Array<String>) = runBlocking {
if (args.isEmpty()) {
throw WrongUsageException("Invalid command! Valid uses: ${this.getUsage(sender)}")
throw WrongUsageException("Invalid command! Valid uses: ${getUsage(sender)}")
}
val uuid = (sender as? EntityPlayer)?.uniqueID?.toString()

View File

@ -1,5 +1,6 @@
package matterlink.command
import kotlinx.coroutines.runBlocking
import matterlink.bridge.command.IBridgeCommand
import matterlink.bridge.command.IMinecraftCommandSender
import net.minecraft.command.ICommandSender
@ -11,14 +12,15 @@ import net.minecraftforge.fml.common.FMLCommonHandler
import javax.annotation.Nonnull
class MatterLinkCommandSender(
user: String,
env: IBridgeCommand.CommandEnvironment,
op: Boolean) : IMinecraftCommandSender(user, env, op), ICommandSender {
user: String,
env: IBridgeCommand.CommandEnvironment,
op: Boolean
) : IMinecraftCommandSender(user, env, op), ICommandSender {
override fun execute(cmdString: String): Boolean {
return 0 < FMLCommonHandler.instance().minecraftServerInstance.commandManager.executeCommand(
this,
cmdString
override fun execute(cmdString: String): Boolean = runBlocking {
0 < FMLCommonHandler.instance().minecraftServerInstance.commandManager.executeCommand(
this@MatterLinkCommandSender,
cmdString
).apply {
sendReply(cmdString)
}

View File

@ -11,16 +11,16 @@ buildscript {
}
dependencies {
classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '1.2-SNAPSHOT'
classpath group: 'gradle.plugin.com.matthewprenger', name: 'CurseGradle', version: cursegradle_version
classpath group: 'gradle.plugin.com.matthewprenger', name: 'CurseGradle', version: cursegradleVersion
}
}
apply plugin: 'forge'
apply plugin: 'com.matthewprenger.cursegradle'
version = project.mc_version + '-' + project.mod_version
version = project.mc_version + '-' + project.modVersion
archivesBaseName = project.mod_name
archivesBaseName = project.modName
sourceCompatibility = targetCompatibility = '1.8'
@ -30,21 +30,35 @@ configurations {
}
dependencies {
shade project(':core')
shade project(':api')
shade project(':Jankson')
shade (project(':core')) { transitive = true }
shade (project(':Jankson')) { transitive = false }
shade group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8', version: kotlinVersion
shade(group: "org.jetbrains.kotlinx", name: "kotlinx-coroutines-core", version: coroutinesVersion)
shade(group: "org.jetbrains.kotlinx", name: "kotlinx-serialization-runtime", version: serializationVersion)
shade(group: "com.github.kittinunf.Fuel", name: "fuel", version: fuelVersion)
shade(group: "com.github.kittinunf.Fuel", name: "fuel-coroutines", version: fuelVersion)
shade(group: "com.github.kittinunf.Fuel", name: "fuel-kotlinx-serialization", version: fuelVersion)
shade(group: 'com.github.kittinunf.result', name: 'result', version: resultVersion)
// shade group: 'com.github.kittinunf.fuel', name: 'fuel', version: fuelVersion
// shade group: 'com.github.kittinunf.result', name: 'result', version: resultVersion
}
minecraft {
version = project.mc_version + '-' + project.forge_version + '-' + project.mc_version
runDir = 'run'
mappings = project.mcp_mappings
srgExtra 'PK: kotlin matterlink/repack/kotlin'
srgExtra 'PK: org/jetbrains/annotations matterlink/repack/org/jetbrains/annotations'
srgExtra 'PK: org/intellij matterlink/repack/org/intellij'
srgExtra 'PK: blue/endless matterlink/repack/blue/endless'
// srgExtra 'PK: kotlin matterlink/repack/kotlin'
// srgExtra 'PK: org/jetbrains/annotations matterlink/repack/org/jetbrains/annotations'
// srgExtra 'PK: org/intellij matterlink/repack/org/intellij'
// srgExtra 'PK: blue/endless/ matterlink/repack/blue/endless/'
}
compileKotlin.doFirst {
@ -53,13 +67,13 @@ compileKotlin.doFirst {
from('src/templates/kotlin/matterlink/Constants.kt')
into(target)
}
ant.replaceregexp(match: '@MODVERSION@', replace: project.mod_version, flags: 'g', byline: true) {
ant.replaceregexp(match: '@MODVERSION@', replace: project.modVersion, flags: 'g', byline: true) {
fileset(dir: target, includes: 'Constants.kt')
}
ant.replaceregexp(match: '@MCVERSION@', replace: project.mc_version, flags: 'g', byline: true) {
fileset(dir: target, includes: 'Constants.kt')
}
ant.replaceregexp(match: '@FORGELIN-VERSION@', replace: project.forgelin_version, flags: 'g', byline: true) {
ant.replaceregexp(match: '@FORGELIN-VERSION@', replace: project.forgelinVersion, flags: 'g', byline: true) {
fileset(dir: target, includes: 'Constants.kt')
}
ant.replaceregexp(match: '@FORGE-VERSION@', replace: project.forge_version, flags: 'g', byline: true) {
@ -72,7 +86,7 @@ compileKotlin.doFirst {
processResources {
// this will ensure that this task is redone when the versions change.
inputs.property 'version', project.mod_version
inputs.property 'version', project.modVersion
inputs.property 'mcversion', project.minecraft.version
// replace stuff in mcmod.info, nothing else
@ -80,7 +94,7 @@ processResources {
include 'mcmod.info'
// replace version and mcversion
expand 'version': project.mod_version, 'mcversion': project.minecraft.version
expand 'version': project.modVersion, 'mcversion': project.minecraft.version
}
// copy everything else except the mcmod.info
@ -93,7 +107,6 @@ jar {
configurations.shade.each { dep ->
from(project.zipTree(dep)) {
exclude 'META-INF', 'META-INF/**'
exclude 'com/google/gson', 'com/google/gson/**'
}
}
}
@ -103,8 +116,8 @@ curseforge {
apiKey = CURSEFORGE_API_TOKEN
}
project {
id = project.curse_id
releaseType = project.curse_release_type
id = project.curseId
releaseType = project.curseReleaseType
if (project.hasProperty('changelog_file')) {
println("changelog = $changelog_file")
changelogType = 'markdown'

View File

@ -3,11 +3,17 @@ package matterlink
import cpw.mods.fml.common.eventhandler.SubscribeEvent
import cpw.mods.fml.common.gameevent.PlayerEvent
import cpw.mods.fml.common.gameevent.TickEvent
import matterlink.api.ApiMessage.Companion.USER_ACTION
import kotlinx.coroutines.runBlocking
import matterlink.config.cfg
import matterlink.handlers.*
import matterlink.handlers.ChatEvent
import matterlink.handlers.ChatProcessor
import matterlink.handlers.DeathHandler
import matterlink.handlers.JoinLeaveHandler
import matterlink.handlers.ProgressHandler
import matterlink.handlers.TickHandler
import net.minecraft.command.server.CommandBroadcast
import net.minecraft.command.server.CommandEmote
import net.minecraft.entity.Entity
import net.minecraft.entity.player.EntityPlayer
import net.minecraft.entity.player.EntityPlayerMP
import net.minecraft.server.dedicated.DedicatedServer
@ -21,80 +27,121 @@ object EventHandler {
//MC-VERSION & FORGE DEPENDENT
@SubscribeEvent
fun progressEvent(e: AchievementEvent) {
fun progressEvent(e: AchievementEvent) = runBlocking {
val achievement = e.achievement
val entityPlayer = e.entityPlayer as? EntityPlayerMP ?: return
val entityPlayer = e.entityPlayer as? EntityPlayerMP ?: return@runBlocking
val statFile = entityPlayer.statFile
if (!statFile.canUnlockAchievement(achievement) || statFile.hasAchievementUnlocked(achievement)) {
return
return@runBlocking
}
ProgressHandler.handleProgress(
name = e.entityPlayer.displayName,
message = "has earned the achievement",
display = e.achievement.statName.unformattedText
name = e.entityPlayer.displayName,
message = "has earned the achievement",
display = e.achievement.statName.unformattedText,
x = e.entityPlayer.posX.toInt(),
y = e.entityPlayer.posY.toInt(),
z = e.entityPlayer.posZ.toInt(),
dimension = e.entityPlayer.dimension
)
}
//FORGE-DEPENDENT
@SubscribeEvent
fun chatEvent(e: ServerChatEvent) {
if(e.isCanceled) return
fun chatEvent(e: ServerChatEvent) = runBlocking {
if (e.isCanceled) return@runBlocking
e.isCanceled = ChatProcessor.sendToBridge(
user = e.player.displayName,
msg = e.message,
event = "",
uuid = e.player.gameProfile.id
user = e.player.displayName,
msg = e.message,
x = e.player.posX.toInt(),
y = e.player.posY.toInt(),
z = e.player.posZ.toInt(),
dimension = e.player.dimension,
event = ChatEvent.PLAIN,
uuid = e.player.gameProfile.id
)
}
//FORGE-DEPENDENT
@SubscribeEvent
fun commandEvent(e: CommandEvent) {
fun commandEvent(e: CommandEvent) = runBlocking {
val sender = when {
e.sender is DedicatedServer -> cfg.outgoing.systemUser
else -> e.sender.commandSenderName
}
val args = e.parameters.joinToString(" ")
val type = when {
e.command is CommandEmote -> USER_ACTION
e.command.commandName == "me" -> USER_ACTION
e.command is CommandBroadcast -> ""
else -> return
val type = with(e.command) {
when {
this is CommandEmote || commandName.equals("me", true) -> ChatEvent.ACTION
this is CommandBroadcast || commandName.equals("say", true) -> ChatEvent.BROADCAST
else -> return@runBlocking
}
}
ChatProcessor.sendToBridge(user = sender, msg = args, event = type)
val s = e.sender
val (x, y, z) = when (s) {
is Entity -> Triple(s.posX.toInt(), s.posY.toInt(), s.posZ.toInt())
else -> with(s.commandSenderPosition) { Triple(posX, posY, posZ) }
}
ChatProcessor.sendToBridge(
user = sender,
msg = args,
event = type,
x = x,
y = y,
z = z,
dimension = when {
e.sender is DedicatedServer -> null
else -> e.sender.entityWorld.provider.dimensionId
}
)
}
//FORGE-DEPENDENT
@SubscribeEvent
fun deathEvent(e: LivingDeathEvent) {
fun deathEvent(e: LivingDeathEvent) = runBlocking {
if (e.entityLiving is EntityPlayer) {
val player = e.entityLiving as EntityPlayer
DeathHandler.handleDeath(
player = player.displayName,
deathMessage = e.entityLiving.combatTracker.func_151521_b().unformattedText,
damageType = e.source.damageType
player = player.displayName,
deathMessage = e.entityLiving.combatTracker.func_151521_b().unformattedText,
damageType = e.source.damageType,
x = e.entityLiving.posX.toInt(),
y = e.entityLiving.posY.toInt(),
z = e.entityLiving.posZ.toInt(),
dimension = e.entityLiving.dimension
)
}
}
//FORGE-DEPENDENT
@SubscribeEvent
fun joinEvent(e: PlayerEvent.PlayerLoggedInEvent) {
JoinLeaveHandler.handleJoin(e.player.displayName)
fun joinEvent(e: PlayerEvent.PlayerLoggedInEvent) = runBlocking {
JoinLeaveHandler.handleJoin(
player = e.player.displayName,
x = e.player.posX.toInt(),
y = e.player.posY.toInt(),
z = e.player.posZ.toInt(),
dimension = e.player.dimension
)
}
//FORGE-DEPENDENT
@SubscribeEvent
fun leaveEvent(e: PlayerEvent.PlayerLoggedOutEvent) {
JoinLeaveHandler.handleLeave(e.player.displayName)
fun leaveEvent(e: PlayerEvent.PlayerLoggedOutEvent) = runBlocking {
JoinLeaveHandler.handleLeave(
player = e.player.displayName,
x = e.player.posX.toInt(),
y = e.player.posY.toInt(),
z = e.player.posZ.toInt(),
dimension = e.player.dimension
)
}
//FORGE-DEPENDENT
@SubscribeEvent
fun serverTickEvent(e: TickEvent.ServerTickEvent) {
fun serverTickEvent(e: TickEvent.ServerTickEvent) = runBlocking {
if (e.phase == TickEvent.Phase.END)
TickHandler.handleTick()
}

View File

@ -7,6 +7,7 @@ import cpw.mods.fml.common.event.FMLInitializationEvent
import cpw.mods.fml.common.event.FMLPreInitializationEvent
import cpw.mods.fml.common.event.FMLServerStartingEvent
import cpw.mods.fml.common.event.FMLServerStoppingEvent
import kotlinx.coroutines.runBlocking
import matterlink.bridge.command.IBridgeCommand
import matterlink.command.AuthCommand
import matterlink.command.MatterLinkCommand
@ -18,13 +19,13 @@ import net.minecraft.server.MinecraftServer
import net.minecraft.util.ChatComponentText
import net.minecraftforge.common.ForgeVersion
import net.minecraftforge.common.MinecraftForge
import java.util.*
import java.util.UUID
@Mod(
modid = MODID,
name = NAME, version = MODVERSION,
useMetadata = true,
acceptableRemoteVersions = "*"
modid = MODID,
name = NAME, version = MODVERSION,
useMetadata = true,
acceptableRemoteVersions = "*"
)
class MatterLink : IMatterLink() {
init {
@ -36,7 +37,16 @@ class MatterLink : IMatterLink() {
MinecraftForge.EVENT_BUS.register(EventHandler)
FMLCommonHandler.instance().bus().register(EventHandler)
logger = event.modLog as org.apache.logging.log4j.core.Logger
logger = with(event.modLog) {
object : Logger {
override fun info(message: String) = this@with.info(message)
override fun debug(message: String) = this@with.debug(message)
override fun error(message: String) = this@with.error(message)
override fun warn(message: String) = this@with.warn(message)
override fun trace(message: String) = this@with.trace(message)
}
}
logger.info("Building bridge!")
cfg = BaseConfig(event.modConfigurationDirectory).load()
@ -48,15 +58,15 @@ class MatterLink : IMatterLink() {
}
@Mod.EventHandler
fun serverStarting(event: FMLServerStartingEvent) {
fun serverStarting(event: FMLServerStartingEvent) = runBlocking {
logger.debug("Registering server commands")
event.registerServerCommand(MatterLinkCommand())
event.registerServerCommand(AuthCommand())
event.registerServerCommand(MatterLinkCommand)
event.registerServerCommand(AuthCommand)
start()
}
@Mod.EventHandler
fun serverStopping(event: FMLServerStoppingEvent) {
fun serverStopping(event: FMLServerStoppingEvent) = runBlocking {
stop()
}
@ -90,10 +100,11 @@ class MatterLink : IMatterLink() {
}
override fun isOnline(username: String) = (FMLCommonHandler.instance()
.minecraftServerInstance.configurationManager.getPlayerByUsername(username) ?: null) != null
.minecraftServerInstance.configurationManager.getPlayerByUsername(username) ?: null) != null
private fun playerByProfile(gameProfile: GameProfile): EntityPlayerMP? {
return FMLCommonHandler.instance().minecraftServerInstance.configurationManager.createPlayerForUser(gameProfile)
return FMLCommonHandler.instance()
.minecraftServerInstance.configurationManager.getPlayerByUsername(gameProfile.name)
}
private fun profileByUUID(uuid: UUID): GameProfile? = try {
@ -110,14 +121,24 @@ class MatterLink : IMatterLink() {
null
}
override fun collectPlayers(area: Area): Set<UUID> {
val players = MinecraftServer.getServer().configurationManager.playerEntityList
.map { it as EntityPlayerMP }
.filter {
(area.allDimensions || area.dimensions.contains(it.dimension))
&& area.testInBounds(it.posX.toInt(), it.posY.toInt(), it.posZ.toInt())
}
return players.map { it.uniqueID }.toSet()
}
override fun nameToUUID(username: String): UUID? = profileByName(username)?.id
override fun uuidToName(uuid: UUID): String? = profileByUUID(uuid)?.name
override fun commandSenderFor(
user: String,
env: IBridgeCommand.CommandEnvironment,
op: Boolean
user: String,
env: IBridgeCommand.CommandEnvironment,
op: Boolean
) = MatterLinkCommandSender(user, env, op)
override val mcVersion: String = MCVERSION

View File

@ -7,7 +7,7 @@ import net.minecraft.entity.player.EntityPlayer
import net.minecraft.util.ChatComponentText
class AuthCommand : CommandBase() {
object AuthCommand : CommandBase() {
override fun getCommandName(): String {
return CommandCoreAuth.name
}

View File

@ -1,5 +1,6 @@
package matterlink.command
import kotlinx.coroutines.runBlocking
import net.minecraft.command.CommandBase
import net.minecraft.command.ICommandSender
import net.minecraft.command.WrongUsageException
@ -7,7 +8,7 @@ import net.minecraft.entity.player.EntityPlayer
import net.minecraft.util.ChatComponentText
class MatterLinkCommand : CommandBase() {
object MatterLinkCommand : CommandBase() {
override fun getCommandName(): String {
return CommandCoreML.name
}
@ -20,9 +21,9 @@ class MatterLinkCommand : CommandBase() {
return CommandCoreML.aliases
}
override fun processCommand(sender: ICommandSender, args: Array<String>) {
override fun processCommand(sender: ICommandSender, args: Array<String>) = runBlocking {
if (args.isEmpty()) {
throw WrongUsageException("Invalid command! Valid uses: ${this.getCommandUsage(sender)}")
throw WrongUsageException("Invalid command! Valid uses: ${getCommandUsage(sender)}")
}
val uuid = (sender as? EntityPlayer)?.uniqueID?.toString()

View File

@ -1,5 +1,6 @@
package matterlink.command
import kotlinx.coroutines.runBlocking
import matterlink.bridge.command.IBridgeCommand
import matterlink.bridge.command.IMinecraftCommandSender
import net.minecraft.command.ICommandSender
@ -10,14 +11,15 @@ import net.minecraft.util.IChatComponent
import net.minecraft.world.World
class MatterLinkCommandSender(
user: String,
env: IBridgeCommand.CommandEnvironment,
op: Boolean) : IMinecraftCommandSender(user, env, op), ICommandSender {
user: String,
env: IBridgeCommand.CommandEnvironment,
op: Boolean
) : IMinecraftCommandSender(user, env, op), ICommandSender {
override fun execute(cmdString: String): Boolean {
return 0 < MinecraftServer.getServer().commandManager.executeCommand(
this,
cmdString
override fun execute(cmdString: String): Boolean = runBlocking {
return@runBlocking 0 < MinecraftServer.getServer().commandManager.executeCommand(
this@MatterLinkCommandSender,
cmdString
).apply {
sendReply(cmdString)
}

View File

@ -11,8 +11,8 @@ buildscript {
}
dependencies {
classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '2.2-SNAPSHOT'
classpath group: 'com.github.jengelman.gradle.plugins', name: 'shadow', version: shadow_version
classpath group: 'gradle.plugin.com.matthewprenger', name: 'CurseGradle', version: cursegradle_version
classpath group: 'com.github.jengelman.gradle.plugins', name: 'shadow', version: shadowVersion
classpath group: 'gradle.plugin.com.matthewprenger', name: 'CurseGradle', version: cursegradleVersion
}
}
@ -20,32 +20,26 @@ apply plugin: 'net.minecraftforge.gradle.forge'
apply plugin: 'com.github.johnrengelman.shadow'
apply plugin: 'com.matthewprenger.cursegradle'
version = project.mc_version + '-' + project.mod_version
version = project.mc_version + '-' + project.modVersion
archivesBaseName = project.mod_name
archivesBaseName = project.modName
sourceCompatibility = targetCompatibility = '1.8'
dependencies {
compile project(':core')
compile group: 'net.shadowfacts', name: 'Forgelin', version: project.forgelin_version
shadow (project(path: ':core', configuration: 'shadow')) { transitive = false }
compile group: 'net.shadowfacts', name: 'Forgelin', version: project.forgelinVersion
}
shadowJar {
classifier ''
relocate 'blue.endless', 'matterlink.repack.blue.endless'
dependencies {
include project(':core')
include project(':api')
include project(':Jankson')
}
classifier = ''
exclude 'dummyThing'
configurations = [project.configurations.shadow]
}
import net.minecraftforge.gradle.user.TaskSourceCopy
// Mad hacks to make source replacements work for Kotlin
@ -74,16 +68,16 @@ minecraft {
mappings = project.mcp_mappings
replaceIn 'Constants.kt'
replace '@MODVERSION@', project.mod_version
replace '@MODVERSION@', project.modVersion
replace '@MCVERSION@', project.mc_version
replace '@FORGELIN-VERSION@', project.forgelin_version
replace '@FORGELIN-VERSION@', project.forgelinVersion
replace '@FORGE-VERSION@', project.forge_version
replace '-1//@BUILD_NUMBER@', System.env.BUILD_NUMBER ?: -1
}
processResources {
// this will ensure that this task is redone when the versions change.
inputs.property 'version', project.mod_version
inputs.property 'version', project.modVersion
inputs.property 'mcversion', project.minecraft.version
// replace stuff in mcmod.info, nothing else
@ -91,7 +85,7 @@ processResources {
include 'mcmod.info'
// replace version and mcversion
expand 'version': project.mod_version, 'mcversion': project.minecraft.version
expand 'version': project.modVersion, 'mcversion': project.minecraft.version
}
// copy everything else except the mcmod.info
@ -116,7 +110,6 @@ sourceJar {
reobf {
shadowJar { mappingType = 'SEARGE' }
}
tasks.shadowJar.finalizedBy reobfShadowJar
curseforge {
@ -124,8 +117,9 @@ curseforge {
apiKey = CURSEFORGE_API_TOKEN
}
project {
id = project.curse_id
releaseType = project.curse_release_type
id = project.curseId
releaseType = project.curseReleaseType
addGameVersion '1.10'
if (project.hasProperty('changelog_file')) {
println("changelog = $changelog_file")
changelogType = 'markdown'

3
1.9.4/gradle.properties Normal file
View File

@ -0,0 +1,3 @@
mc_version = 1.9.4
mcp_mappings = stable_26
forge_version = 12.17.0.2051

View File

@ -0,0 +1,139 @@
package matterlink
import kotlinx.coroutines.runBlocking
import matterlink.config.cfg
import matterlink.handlers.ChatEvent
import matterlink.handlers.ChatProcessor
import matterlink.handlers.DeathHandler
import matterlink.handlers.JoinLeaveHandler
import matterlink.handlers.ProgressHandler
import matterlink.handlers.TickHandler
import net.minecraft.command.server.CommandBroadcast
import net.minecraft.command.server.CommandEmote
import net.minecraft.entity.player.EntityPlayer
import net.minecraft.entity.player.EntityPlayerMP
import net.minecraft.server.dedicated.DedicatedServer
import net.minecraftforge.event.CommandEvent
import net.minecraftforge.event.ServerChatEvent
import net.minecraftforge.event.entity.living.LivingDeathEvent
import net.minecraftforge.event.entity.player.AchievementEvent
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
import net.minecraftforge.fml.common.gameevent.PlayerEvent
import net.minecraftforge.fml.common.gameevent.TickEvent
//FORGE-DEPENDENT
object EventHandler {
//MC-VERSION & FORGE DEPENDENT
@SubscribeEvent
fun progressEvent(e: AchievementEvent) = runBlocking {
val achievement = e.achievement
val entityPlayer = e.entityPlayer as? EntityPlayerMP ?: return@runBlocking
val statFile = entityPlayer.statFile
if (!statFile.canUnlockAchievement(achievement) || statFile.hasAchievementUnlocked(achievement)) {
return@runBlocking
}
ProgressHandler.handleProgress(
name = e.entityPlayer.displayName.unformattedText,
message = "has earned the achievement",
display = e.achievement.statName.unformattedText,
x = e.entityPlayer.posX.toInt(),
y = e.entityPlayer.posY.toInt(),
z = e.entityPlayer.posZ.toInt(),
dimension = e.entityPlayer.dimension
)
}
//FORGE-DEPENDENT
@SubscribeEvent
fun chatEvent(e: ServerChatEvent) = runBlocking {
if (e.isCanceled) return@runBlocking
e.isCanceled = ChatProcessor.sendToBridge(
user = e.player.displayName.unformattedText,
msg = e.message,
x = e.player.posX.toInt(),
y = e.player.posY.toInt(),
z = e.player.posZ.toInt(),
dimension = e.player.dimension,
event = ChatEvent.PLAIN,
uuid = e.player.gameProfile.id
)
}
//FORGE-DEPENDENT
@SubscribeEvent
fun commandEvent(e: CommandEvent) = runBlocking {
val sender = when {
e.sender is DedicatedServer -> cfg.outgoing.systemUser
else -> e.sender.displayName.unformattedText
}
val args = e.parameters.joinToString(" ")
val type = with(e.command) {
when {
this is CommandEmote || commandName.equals("me", true) -> ChatEvent.ACTION
this is CommandBroadcast || commandName.equals("say", true) -> ChatEvent.BROADCAST
else -> return@runBlocking
}
}
ChatProcessor.sendToBridge(
user = sender,
msg = args,
event = type,
x = e.sender.position.x,
y = e.sender.position.y,
z = e.sender.position.z,
dimension = when {
e.sender is DedicatedServer -> null
else -> e.sender.commandSenderEntity?.dimension ?: e.sender.entityWorld.provider.dimension
}
)
}
//FORGE-DEPENDENT
@SubscribeEvent
fun deathEvent(e: LivingDeathEvent) = runBlocking {
if (e.entityLiving is EntityPlayer) {
DeathHandler.handleDeath(
player = e.entityLiving.displayName.unformattedText,
deathMessage = e.entityLiving.combatTracker.deathMessage.unformattedText,
damageType = e.source.damageType,
x = e.entityLiving.posX.toInt(),
y = e.entityLiving.posY.toInt(),
z = e.entityLiving.posZ.toInt(),
dimension = e.entityLiving.dimension
)
}
}
//FORGE-DEPENDENT
@SubscribeEvent
fun joinEvent(e: PlayerEvent.PlayerLoggedInEvent) = runBlocking {
JoinLeaveHandler.handleJoin(
player = e.player.displayName.unformattedText,
x = e.player.posX.toInt(),
y = e.player.posY.toInt(),
z = e.player.posZ.toInt(),
dimension = e.player.dimension
)
}
//FORGE-DEPENDENT
@SubscribeEvent
fun leaveEvent(e: PlayerEvent.PlayerLoggedOutEvent) = runBlocking {
JoinLeaveHandler.handleLeave(
player = e.player.displayName.unformattedText,
x = e.player.posX.toInt(),
y = e.player.posY.toInt(),
z = e.player.posZ.toInt(),
dimension = e.player.dimension
)
}
//FORGE-DEPENDENT
@SubscribeEvent
fun serverTickEvent(e: TickEvent.ServerTickEvent) = runBlocking {
if (e.phase == TickEvent.Phase.END)
TickHandler.handleTick()
}
}

View File

@ -1,7 +1,9 @@
package matterlink
import com.mojang.authlib.GameProfile
import kotlinx.coroutines.runBlocking
import matterlink.bridge.command.IBridgeCommand
import matterlink.command.AuthCommand
import matterlink.command.MatterLinkCommand
import matterlink.command.MatterLinkCommandSender
import matterlink.config.BaseConfig
@ -9,24 +11,23 @@ import matterlink.config.cfg
import net.minecraft.entity.player.EntityPlayerMP
import net.minecraft.util.text.TextComponentString
import net.minecraftforge.common.ForgeVersion
import net.minecraftforge.common.MinecraftForge
import net.minecraftforge.fml.common.FMLCommonHandler
import net.minecraftforge.fml.common.Mod
import net.minecraftforge.fml.common.event.FMLInitializationEvent
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent
import net.minecraftforge.fml.common.event.FMLServerStartingEvent
import net.minecraftforge.fml.common.event.FMLServerStoppingEvent
import org.apache.logging.log4j.Level
import org.apache.logging.log4j.Logger
import java.util.*
import java.util.UUID
@Mod(
modid = MODID,
name = NAME, version = MODVERSION,
serverSideOnly = true,
useMetadata = true,
acceptableRemoteVersions = "*",
modLanguageAdapter = "net.shadowfacts.forgelin.KotlinAdapter",
dependencies = DEPENDENCIES
modid = MODID,
name = NAME, version = MODVERSION,
serverSideOnly = true,
useMetadata = true,
acceptableRemoteVersions = "*",
modLanguageAdapter = "net.shadowfacts.forgelin.KotlinAdapter",
dependencies = DEPENDENCIES
)
object MatterLink : IMatterLink() {
init {
@ -35,7 +36,17 @@ object MatterLink : IMatterLink() {
@Mod.EventHandler
fun preInit(event: FMLPreInitializationEvent) {
logger = event.modLog as org.apache.logging.log4j.core.Logger
MinecraftForge.EVENT_BUS.register(EventHandler)
logger = with(event.modLog) {
object : Logger {
override fun info(message: String) = this@with.info(message)
override fun debug(message: String) = this@with.debug(message)
override fun error(message: String) = this@with.error(message)
override fun warn(message: String) = this@with.warn(message)
override fun trace(message: String) = this@with.trace(message)
}
}
logger.info("Building bridge!")
cfg = BaseConfig(event.modConfigurationDirectory).load()
@ -47,14 +58,15 @@ object MatterLink : IMatterLink() {
}
@Mod.EventHandler
fun serverStarting(event: FMLServerStartingEvent) {
fun serverStarting(event: FMLServerStartingEvent) = runBlocking {
logger.debug("Registering server commands")
event.registerServerCommand(MatterLinkCommand())
event.registerServerCommand(MatterLinkCommand)
event.registerServerCommand(AuthCommand)
start()
}
@Mod.EventHandler
fun serverStopping(event: FMLServerStoppingEvent) {
fun serverStopping(event: FMLServerStoppingEvent) = runBlocking {
stop()
}
@ -72,7 +84,7 @@ object MatterLink : IMatterLink() {
error("${profile.name} is not online")
return
}
player.sendMessage(TextComponentString(msg))
player.addChatMessage(TextComponentString(msg))
}
override fun wrappedSendToPlayer(uuid: UUID, msg: String) {
@ -84,12 +96,14 @@ object MatterLink : IMatterLink() {
logger.error("${profile.name} is not online")
return
}
player.sendMessage(TextComponentString(msg))
player.addChatMessage(TextComponentString(msg))
}
override fun isOnline(username: String) = FMLCommonHandler.instance().minecraftServerInstance.onlinePlayerNames.contains(username)
override fun isOnline(username: String) =
FMLCommonHandler.instance().minecraftServerInstance.playerList.allUsernames.contains(username)
private fun playerByProfile(gameProfile: GameProfile): EntityPlayerMP? = FMLCommonHandler.instance().minecraftServerInstance.playerList.getPlayerByUUID(gameProfile.id)
private fun playerByProfile(gameProfile: GameProfile): EntityPlayerMP? =
FMLCommonHandler.instance().minecraftServerInstance.playerList.getPlayerByUUID(gameProfile.id)
private fun profileByUUID(uuid: UUID): GameProfile? = try {
@ -106,14 +120,25 @@ object MatterLink : IMatterLink() {
null
}
override fun collectPlayers(area: Area): Set<UUID> {
val playerList = FMLCommonHandler.instance().minecraftServerInstance.playerList
val players = playerList.allProfiles
.map { playerList.getPlayerByUUID(it.id) }
.filter {
(area.allDimensions || area.dimensions.contains(it.dimension))
&& area.testInBounds(it.posX.toInt(), it.posY.toInt(), it.posZ.toInt())
}
return players.map { it.uniqueID }.toSet()
}
override fun nameToUUID(username: String): UUID? = profileByName(username)?.id
override fun uuidToName(uuid: UUID): String? = profileByUUID(uuid)?.name
override fun commandSenderFor(
user: String,
env: IBridgeCommand.CommandEnvironment,
op: Boolean
user: String,
env: IBridgeCommand.CommandEnvironment,
op: Boolean
) = MatterLinkCommandSender(user, env, op)
override val mcVersion: String = MCVERSION

View File

@ -1,6 +1,5 @@
package matterlink.command
import matterlink.logger
import net.minecraft.command.CommandBase
import net.minecraft.command.ICommandSender
import net.minecraft.command.WrongUsageException
@ -9,16 +8,16 @@ import net.minecraft.server.MinecraftServer
import net.minecraft.util.text.TextComponentString
class AuthCommand : CommandBase() {
override fun getName(): String {
object AuthCommand : CommandBase() {
override fun getCommandName(): String {
return CommandCoreAuth.name
}
override fun getUsage(sender: ICommandSender): String {
override fun getCommandUsage(sender: ICommandSender): String {
return CommandCoreAuth.usage
}
override fun getAliases(): List<String> {
override fun getCommandAliases(): List<String> {
return CommandCoreAuth.aliases
}
@ -28,14 +27,14 @@ class AuthCommand : CommandBase() {
override fun execute(server: MinecraftServer, sender: ICommandSender, args: Array<String>) {
if (args.isEmpty()) {
throw WrongUsageException("Invalid command! Valid uses: ${this.getUsage(sender)}")
throw WrongUsageException("Invalid command! Valid uses: ${this.getCommandUsage(sender)}")
}
val uuid = (sender as? EntityPlayer)?.uniqueID?.toString()
val reply = CommandCoreAuth.execute(args, sender.name, uuid)
if (reply.isNotEmpty() && sender.sendCommandFeedback()) {
sender.sendMessage(TextComponentString(reply))
sender.addChatMessage(TextComponentString(reply))
}
}

View File

@ -1,5 +1,6 @@
package matterlink.command
import kotlinx.coroutines.runBlocking
import net.minecraft.command.CommandBase
import net.minecraft.command.ICommandSender
import net.minecraft.command.WrongUsageException
@ -8,29 +9,29 @@ import net.minecraft.server.MinecraftServer
import net.minecraft.util.text.TextComponentString
class MatterLinkCommand : CommandBase() {
override fun getName(): String {
object MatterLinkCommand : CommandBase() {
override fun getCommandName(): String {
return CommandCoreML.name
}
override fun getUsage(sender: ICommandSender): String {
override fun getCommandUsage(sender: ICommandSender): String {
return CommandCoreML.usage
}
override fun getAliases(): List<String> {
override fun getCommandAliases(): List<String> {
return CommandCoreML.aliases
}
override fun execute(server: MinecraftServer, sender: ICommandSender, args: Array<String>) {
override fun execute(server: MinecraftServer, sender: ICommandSender, args: Array<String>) = runBlocking {
if (args.isEmpty()) {
throw WrongUsageException("Invalid command! Valid uses: ${this.getUsage(sender)}")
throw WrongUsageException("Invalid command! Valid uses: ${getCommandUsage(sender)}")
}
val uuid = (sender as? EntityPlayer)?.uniqueID?.toString()
val reply = CommandCoreML.execute(args, sender.name, uuid)
if (reply.isNotEmpty() && sender.sendCommandFeedback()) {
sender.sendMessage(TextComponentString(reply))
sender.addChatMessage(TextComponentString(reply))
}
}
}

View File

@ -1,5 +1,6 @@
package matterlink.command
import kotlinx.coroutines.runBlocking
import matterlink.bridge.command.IBridgeCommand
import matterlink.bridge.command.IMinecraftCommandSender
import net.minecraft.command.CommandResultStats
@ -15,14 +16,14 @@ import net.minecraftforge.fml.common.FMLCommonHandler
import javax.annotation.Nonnull
class MatterLinkCommandSender(
user: String,
env: IBridgeCommand.CommandEnvironment,
op: Boolean) : IMinecraftCommandSender(user, env, op), ICommandSender {
override fun execute(cmdString: String): Boolean {
return 0 < FMLCommonHandler.instance().minecraftServerInstance.commandManager.executeCommand(
this,
cmdString
user: String,
env: IBridgeCommand.CommandEnvironment,
op: Boolean
) : IMinecraftCommandSender(user, env, op), ICommandSender {
override fun execute(cmdString: String): Boolean = runBlocking {
return@runBlocking 0 < FMLCommonHandler.instance().minecraftServerInstance.commandManager.executeCommand(
this@MatterLinkCommandSender,
cmdString
).apply {
sendReply(cmdString)
}
@ -38,7 +39,7 @@ class MatterLinkCommandSender(
return FMLCommonHandler.instance().minecraftServerInstance.worldServerForDimension(0)
}
override fun canUseCommand(permLevel: Int, commandName: String): Boolean {
override fun canCommandSenderUseCommand(permLevel: Int, commandName: String): Boolean {
//we check user on our end
return canExecute(commandName)
}
@ -47,7 +48,7 @@ class MatterLinkCommandSender(
return FMLCommonHandler.instance().minecraftServerInstance
}
override fun sendMessage(@Nonnull component: ITextComponent?) {
override fun addChatMessage(@Nonnull component: ITextComponent?) {
appendReply(component!!.unformattedComponentText)
}

@ -1 +1 @@
Subproject commit 93ab86ee821380584c22ac60d77737388976e531
Subproject commit 7d27c28784bacba17450faa9e723ca6b6eb39602

18
Jenkinsfile vendored
View File

@ -14,20 +14,12 @@ pipeline {
archiveArtifacts artifacts: '1.7.10/build/libs/*jar'
}
}
stage("1.10.2") {
stage("1.9.4") {
steps {
sh './gradlew :1.10.2:setupCiWorkspace'
sh './gradlew :1.10.2:clean'
sh './gradlew :1.10.2:build'
archiveArtifacts artifacts: '1.10.2/build/libs/*jar'
}
}
stage("1.11.2") {
steps {
sh './gradlew :1.11.2:setupCiWorkspace'
sh './gradlew :1.11.2:clean'
sh './gradlew :1.11.2:build'
archiveArtifacts artifacts: '1.11.2/build/libs/*jar'
sh './gradlew :1.9.4:setupCiWorkspace'
sh './gradlew :1.9.4:clean'
sh './gradlew :1.9.4:build'
archiveArtifacts artifacts: '1.9.4/build/libs/*jar'
}
}
stage("1.12.2") {

View File

@ -28,11 +28,7 @@ Chat with us on IRC: [#matterlink @ irc.esper.net](irc://irc.esper.net/matterlin
[![Download 1.12.2](https://curse.nikky.moe/api/img/287323?logo&style=for-the-badge&version=1.12.2)](https://curse.nikky.moe/api/url/287323?version=1.12.2)
[![Download 1.11.2](https://curse.nikky.moe/api/img/287323?logo&style=for-the-badge&version=1.11.2)](https://curse.nikky.moe/api/url/287323?version=1.11.2)
[![Download 1.10.2](https://curse.nikky.moe/api/img/287323?logo&style=for-the-badge&version=1.10.2)](https://curse.nikky.moe/api/url/287323?version=1.10.2)
[![Download 1.7.10](https://curse.nikky.moe/api/img/287323?logo&style=for-the-badge&version=1.7.10)](https://curse.nikky.moe/api/url/287323?version=1.7.10)
[![Download 1.9.4](https://curse.nikky.moe/api/img/287323?logo&style=for-the-badge&version=1.9.4)](https://curse.nikky.moe/api/url/287323?version=1.9.4)
## Dependencies
@ -167,6 +163,13 @@ matterbridge
now start the server with matterlink (and forgelin) in the mods folder
and then [RTFM!!!](https://github.com/42wim/matterbridge#configuration) and configure all your needed gateways, endpoints etc
and then [configure](https://github.com/42wim/matterbridge#configuration) all your needed gateways, endpoints etc
powered by wishful thinking
### Building
```bash
git clone --recursive https://git.lain.faith/sorceress/MatterLink.git
cd MatterLink/
./gradlew setupDecompWorkspace
./gradlew build
```

14
TODO.MD Normal file
View File

@ -0,0 +1,14 @@
# Replacement for guava
# using Fuel everywhere to simplify code
# Adittional player values
add optional feature:
GET json dictionary of player values like TEAM, NICK, TOWN
from url based on uuid / playername
config input: `http://rest.wurmcraft.com/user/find/{UUID}`
url: `http://rest.wurmcraft.com/user/find/148cf139-dd14-4bf4-97a2-08305dfef0a9`

1
api

@ -1 +0,0 @@
Subproject commit 75138cec00c66d479709f4b44d78ca3005993474

View File

@ -3,7 +3,8 @@ buildscript {
jcenter()
}
dependencies {
classpath group: "org.jetbrains.kotlin", name: "kotlin-gradle-plugin", version: kotlin_version
classpath group: "org.jetbrains.kotlin", name: "kotlin-gradle-plugin", version: kotlinVersion
classpath group: "org.jetbrains.kotlin", name: "kotlin-serialization", version: kotlinVersion
}
}
@ -14,13 +15,14 @@ plugins {
subprojects {
apply plugin: "kotlin"
apply plugin: "kotlinx-serialization"
apply plugin: "idea"
if (System.env.BUILD_NUMBER) {
mod_version += "-${System.env.BUILD_NUMBER}"
modVersion += "-${System.env.BUILD_NUMBER}"
} else if (!project.hasProperty('release')) {
// mod_version += "-dev"
mod_version += "-dev"
// modVersion += "-dev"
modVersion += "-dev"
}
idea {
@ -35,19 +37,28 @@ subprojects {
}
repositories {
jcenter()
mavenCentral()
maven {
name = "unascribed"
url = 'http://unascribed.com/maven/releases'
name = "bintray"
url = "http://jcenter.bintray.com"
}
maven {
name = "jitpack"
url = "https://jitpack.io"
}
maven {
name = "kotlinx"
url = "https://kotlin.bintray.com/kotlinx/"
}
// maven {
// name = "elytradev"
// // url = 'http://unascribed.com/maven/releases'
// url = "https://repo.elytradev.com"
// }
maven {
name = "shadowfacts"
url = "http://maven.shadowfacts.net/"
}
ivy {
//Resolves baubles and jankson
name = "endless.blue dependency mirror"
artifactPattern "https://endless.blue/files/ivy/[module]-[revision].[ext]"
}
}
compileKotlin {
kotlinOptions {

View File

@ -0,0 +1,11 @@
object MatterLink {
const val version = "1.6.5"
}
object Kotlin {
const val version = "1.3.0"
}
object Forgelin {
const val version = "1.8.0"
}

View File

@ -4,30 +4,40 @@ buildscript {
mavenCentral()
}
dependencies {
// classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath group: "com.github.jengelman.gradle.plugins", name: "shadow", version: shadow_version
classpath group: "org.jetbrains.kotlin", name: "kotlin-gradle-plugin", version: kotlinVersion
classpath group: "com.github.jengelman.gradle.plugins", name: "shadow", version: shadowVersion
classpath group: 'com.vanniktech', name: 'gradle-dependency-graph-generator-plugin', version: '0.5.0'
}
}
apply plugin: 'kotlin'
apply plugin: "com.github.johnrengelman.shadow"
apply plugin: "com.vanniktech.dependency.graph.generator"
sourceCompatibility = targetCompatibility = '1.8' // Need this here so eclipse task generates correctly.
dependencies {
compile project(':api')
compile project(":Jankson")
shadow(project(':Jankson')) { transitive = false }
// compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '+'
compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '+'
// compile group: 'commons-logging', name: 'commons-logging', version: '1.1.3'
compile group: 'com.google.code.gson', name: 'gson', version: '+'
compile group: 'com.google.guava', name: 'guava', version: '+'
compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '+'
compile(group: "com.github.kittinunf.Fuel", name: "fuel", version: fuelVersion)
shadow(group: "com.github.kittinunf.Fuel", name: "fuel", version: fuelVersion) { transitive = true }
compile(group: "com.github.kittinunf.Fuel", name: "fuel-coroutines", version: fuelVersion)
shadow(group: "com.github.kittinunf.Fuel", name: "fuel-coroutines", version: fuelVersion) { transitive = true }
compile(group: "com.github.kittinunf.Fuel", name: "fuel-kotlinx-serialization", version: fuelVersion)
shadow(group: "com.github.kittinunf.Fuel", name: "fuel-kotlinx-serialization", version: fuelVersion) { transitive = true }
// compile(group: 'com.github.kittinunf.result', name: 'result', version: resultVersion)
// shadow(group: 'com.github.kittinunf.result', name: 'result', version: resultVersion) { transitive = false }
compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8', version: project.kotlin_version
// compile(group: "org.jetbrains.kotlinx", name: "kotlinx-coroutines-core", version: coroutinesVersion)
// shadow(group: "org.jetbrains.kotlinx", name: "kotlinx-coroutines-core", version: coroutinesVersion) { transitive = false }
compile(group: "org.jetbrains.kotlinx", name: "kotlinx-serialization-runtime", version: serializationVersion)
shadow(group: "org.jetbrains.kotlinx", name: "kotlinx-serialization-runtime", version: serializationVersion) { transitive = false }
compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8', version: kotlinVersion
}
compileKotlin {
@ -37,10 +47,12 @@ compileKotlin {
}
shadowJar {
classifier ''
classifier = ''
dependencies {
include project(":api")
include project(":Jankson")
}
relocate 'blue.endless', 'matterlink.repack.blue.endless'
relocate 'com.github', 'matterlink.repack.com.github'
relocate 'kotlinx', 'matterlink.repack.kotlinx'
configurations = [project.configurations.shadow]
}
//tasks.build.dependsOn shadowJar

View File

@ -0,0 +1,185 @@
package matterlink
import blue.endless.jankson.JsonObject
import blue.endless.jankson.impl.SyntaxError
import kotlin.math.sqrt
fun JsonObject.parseDimensions(): List<Int> = getOrPutList("dimensions", emptyList(), "list of dimension ids")
fun JsonObject.parseAllDimensions(): Boolean = getOrDefault("allDimensions", false, "ignores dimension list")
sealed class Area {
abstract val type: String
abstract val dimensions: List<Int>
abstract val allDimensions: Boolean
abstract fun testInBounds(x: Int, y: Int, z: Int): Boolean
fun testForDim(dimension: Int?): Boolean {
if (allDimensions) return true
if (dimension == null) return false
return dimensions.contains(dimension)
}
companion object {
fun parse(jsonObj: JsonObject): Area {
val type: String = jsonObj.getOrDefault("type", "INFINITE", "Area type identifier")
return when (type.toUpperCase()) {
"INFINITE" -> Infinite.parse(jsonObj)
"RADIUS" -> Radius.parse(jsonObj)
"SPHERE" -> Sphere.parse(jsonObj)
"BOX" -> Box.parse(jsonObj)
"SQUARE" -> Square.parse(jsonObj)
else -> throw SyntaxError("no Area type '$type' found")
}
}
}
data class Infinite(
override val dimensions: List<Int> = listOf(),
override val allDimensions: Boolean = false
) : Area() {
override val type = "INFINITE"
override fun testInBounds(x: Int, y: Int, z: Int): Boolean {
return true
}
companion object {
fun parse(jsonObj: JsonObject): Area {
return Infinite(
dimensions = jsonObj.parseDimensions(),
allDimensions = jsonObj.parseAllDimensions()
)
}
}
}
data class Radius(
override val dimensions: List<Int> = listOf(),
override val allDimensions: Boolean = false,
val x: Int,
val z: Int,
val radius: Int?
) : Area() {
override val type = "RADIUS"
override fun testInBounds(x: Int, y: Int, z: Int): Boolean {
if (radius == null) return true
return sqrt(((this.x - x) * (this.x - x)) + ((this.z - z) * (this.z - z)).toFloat()) < this.radius
}
companion object {
fun parse(jsonObj: JsonObject): Area {
return Radius(
dimensions = jsonObj.parseDimensions(),
allDimensions = jsonObj.parseAllDimensions(),
x = jsonObj.getOrDefault("x", 0),
z = jsonObj.getOrDefault("z", 0),
radius = jsonObj.getReified("radius")
)
}
}
}
class Sphere(
override val dimensions: List<Int> = listOf(),
override val allDimensions: Boolean = false,
val x: Int,
val y: Int,
val z: Int,
val radius: Int? = null
) : Area() {
override val type = "SPHERE"
override fun testInBounds(x: Int, y: Int, z: Int): Boolean {
if (radius == null) return true
return sqrt(((this.x - x) * (this.x - x)) + ((this.y - y) * (this.y - y)) + ((this.z - z) * (this.z - z)).toFloat()) < this.radius
}
companion object {
fun parse(jsonObj: JsonObject): Area {
return Sphere(
dimensions = jsonObj.parseDimensions(),
allDimensions = jsonObj.parseAllDimensions(),
x = jsonObj.getOrDefault("x", 0),
y = jsonObj.getOrDefault("y", 0),
z = jsonObj.getOrDefault("z", 0),
radius = jsonObj.getReified("radius")
)
}
}
}
class Box(
override val dimensions: List<Int> = listOf(),
override val allDimensions: Boolean = false,
val x1: Int,
val x2: Int,
val y1: Int,
val y2: Int,
val z1: Int,
val z2: Int
) : Area() {
override val type = "BOX"
override fun testInBounds(x: Int, y: Int, z: Int): Boolean {
return x in x1..x2 && y in y1..y2 && z in z1..z2
}
companion object {
fun parse(jsonObj: JsonObject): Area {
return Box(
dimensions = jsonObj.parseDimensions(),
allDimensions = jsonObj.parseAllDimensions(),
x1 = jsonObj.getOrDefault("x1", 0),
x2 = jsonObj.getOrDefault("x2", 0),
y1 = jsonObj.getOrDefault("y1", 0),
y2 = jsonObj.getOrDefault("y2", 0),
z1 = jsonObj.getOrDefault("z1", 0),
z2 = jsonObj.getOrDefault("z2", 0)
)
}
}
}
class Square(
override val dimensions: List<Int> = listOf(),
override val allDimensions: Boolean = false,
val x1: Int,
val x2: Int,
val z1: Int,
val z2: Int
) : Area() {
override val type = "SQUARE"
override fun testInBounds(x: Int, y: Int, z: Int): Boolean {
return x in x1..x2 && z in z1..z2
}
companion object {
fun parse(jsonObj: JsonObject): Area {
return Square(
dimensions = jsonObj.parseDimensions(),
allDimensions = jsonObj.parseAllDimensions(),
x1 = jsonObj.getOrDefault("x1", 0),
x2 = jsonObj.getOrDefault("x2", 0),
z1 = jsonObj.getOrDefault("z1", 0),
z2 = jsonObj.getOrDefault("z2", 0)
)
}
}
}
//
//
// class FakePlayer (
// val x: Int,
// val y: Int,
// val z: Int,
// val name: String
// ): Area()
}

View File

@ -1,42 +0,0 @@
package matterlink
import matterlink.HttpClientUtil.client
import org.apache.http.HttpResponse
import org.apache.http.client.methods.HttpGet
import org.apache.http.client.methods.HttpRequestBase
import org.apache.http.impl.client.HttpClientBuilder
/**
* Created by nikky on 15/07/18.
* @author Nikky
*/
object HttpClientUtil {
val client = HttpClientBuilder.create().build()
}
fun String.httpGet(): HttpGet =
HttpGet(this)
fun HttpGet.header(pair: Pair<String, String>): HttpGet = this.apply {
addHeader(pair.first, pair.second)
}
fun HttpGet.responseString(): Triple<HttpRequestBase, HttpResponse, Result> {
val response = client.execute(this)
val result = response.entity.content.bufferedReader().use { it.readText() }
return Triple(this, response, Result.Success(result))
}
sealed class Result {
class Success(
val value: String
) : Result()
class Failure(
val error: Throwable
) : Result()
}

View File

@ -6,11 +6,11 @@ import matterlink.bridge.command.IBridgeCommand
import matterlink.bridge.command.IMinecraftCommandSender
import matterlink.config.cfg
import matterlink.update.UpdateChecker
import org.apache.logging.log4j.Logger
import java.util.*
import java.util.UUID
lateinit var logger: Logger
lateinit var instance: IMatterLink
lateinit var logger: Logger
abstract class IMatterLink {
abstract val mcVersion: String
@ -18,7 +18,11 @@ abstract class IMatterLink {
abstract val buildNumber: Int
abstract val forgeVersion: String
abstract fun commandSenderFor(user: String, env: IBridgeCommand.CommandEnvironment, op: Boolean): IMinecraftCommandSender
abstract fun commandSenderFor(
user: String,
env: IBridgeCommand.CommandEnvironment,
op: Boolean
): IMinecraftCommandSender
abstract fun wrappedSendToPlayers(msg: String)
@ -28,46 +32,19 @@ abstract class IMatterLink {
abstract fun nameToUUID(username: String): UUID?
abstract fun uuidToName(uuid: UUID): String?
fun start() {
// MessageHandlerInst.logger = { level, msg ->
// when (level) {
// "FATAL" -> logger.fatal(msg)
// "ERROR" -> logger.error(msg)
// "WARN" -> logger.warn(msg)
// "INFO" -> logger.info(msg)
// "DEBUG" -> logger.debug(msg)
// "TRACE" -> logger.trace(msg)
// }
// }
suspend fun start() {
MessageHandlerInst.logger = logger
serverStartTime = System.currentTimeMillis()
if (cfg.connect.autoConnect)
MessageHandlerInst.start("Server started, connecting to matterbridge API", true)
UpdateChecker.run()
UpdateChecker.check()
}
fun stop() {
suspend fun stop() {
MessageHandlerInst.stop("Server shutting down, disconnecting from matterbridge API")
}
// abstract fun log(level: String, formatString: String, vararg data: Any)
// fun fatal(formatString: String, vararg data: Any) = log("FATAL", formatString, *data)
// fun error(formatString: String, vararg data: Any) = log("ERROR", formatString, *data)
// fun warn(formatString: String, vararg data: Any) = log("WARN", formatString, *data)
// fun info(formatString: String, vararg data: Any) = log("INFO", formatString, *data)
//
// fun debug(formatString: String, vararg data: Any) {
// if (cfg.debug.logLevel == "DEBUG" || cfg.debug.logLevel == "TRACE")
// log("INFO", "DEBUG: " + formatString.replace("\n", "\nDEBUG: "), *data)
// }
//
// fun trace(formatString: String, vararg data: Any) {
// if (cfg.debug.logLevel == "TRACE")
// log("INFO", "TRACE: " + formatString.replace("\n", "\nTRACE: "), *data)
// }
/**
* in milliseconds
*/
@ -98,4 +75,6 @@ abstract class IMatterLink {
BridgeCommandRegistry.reloadCommands()
}
abstract fun collectPlayers(area: Area): Set<UUID>
}

View File

@ -0,0 +1,9 @@
package matterlink
interface Logger {
fun info(message: String)
fun debug(message: String)
fun error(message: String)
fun warn(message: String)
fun trace(message: String)
}

View File

@ -1,8 +1,6 @@
package matterlink
import blue.endless.jankson.Jankson
import blue.endless.jankson.JsonObject
import blue.endless.jankson.impl.Marshaller
import java.net.HttpURLConnection
import java.net.URL
@ -13,34 +11,34 @@ import java.net.URL
*/
data class Paste(
val encrypted: Boolean = false,
val description: String,
val sections: List<PasteSection>
val encrypted: Boolean = false,
val description: String,
val sections: List<PasteSection>
)
data class PasteSection(
val name: String,
val syntax: String = "text",
val contents: String
val name: String,
val syntax: String = "text",
val contents: String
)
data class PasteResponse(
val id: String,
val link: String
val id: String,
val link: String
)
object PasteUtil {
private const val DEFAULT_KEY = "uKJoyicVJFnmpnrIZMklOURWxrCKXYaiBWOzPmvon"
private val jankson = Jankson.builder()
.registerTypeAdapter {
PasteResponse(
id = it.getReified("id") ?: "",
link = it.getReified<String>("link")
?.replace("\\/", "/")
?: "invalid"
)
}
.registerTypeAdapter {
PasteResponse(
id = it.getReified("id") ?: "",
link = it.getReified<String>("link")
?.replace("\\/", "/")
?: "invalid"
)
}
// .registerSerializer { paste: Paste, marshaller: Marshaller ->
// JsonObject().apply {
// with(paste) {
@ -62,7 +60,7 @@ object PasteUtil {
// }
// }
// }
.build()
.build()
fun paste(paste: Paste, key: String = ""): PasteResponse {
val apiKey = key.takeIf { it.isNotBlank() } ?: DEFAULT_KEY
@ -73,8 +71,8 @@ object PasteUtil {
http.doOutput = true
val out = jankson.toJson(paste)
.toJson(false, false)
.toByteArray()
.toJson(false, false)
.toByteArray()
http.setFixedLengthStreamingMode(out.size)
http.setRequestProperty("Content-Type", "application/json; charset=UTF-8")

View File

@ -8,9 +8,7 @@ import blue.endless.jankson.impl.Marshaller
import matterlink.config.cfg
import java.io.PrintWriter
import java.io.StringWriter
import java.lang.Thread.yield
import java.util.*
import kotlin.streams.asSequence
private const val ZWSP: Char = '\u200b'
@ -64,27 +62,44 @@ val Exception.stackTraceString: String
}
fun randomString(length: Int = 6): String =
java.util.UUID.randomUUID().toString().replace("-", "").take(length)
java.util.UUID.randomUUID().toString().replace("-", "").take(length)
fun <T : Any> JsonObject.getOrDefault(key: String, default: T, comment: String? = null): T {
// logger.info("type: ${default.javaClass.name} key: $key json: >>>${this.getObject(key)?.toJson()}<<< default: $default")
return putDefault(key, default, comment)!!
logger.trace("type: ${default.javaClass.name} key: $key json: >>>${this.getObject(key)?.toJson()}<<< default: $default")
return putDefault(key, default, comment)!!.also {
setComment(key, comment)
}
}
inline fun <reified T : Any> Jankson.fromJson(obj: JsonObject): T = this.fromJson(obj, T::class.java)
inline fun <reified T : Any> Jankson.fromJson(json: String): T = this.fromJson(json, T::class.java)
inline fun <reified T : Any> Jankson.Builder.registerTypeAdapter(noinline adapter: (JsonObject) -> T) = this.registerTypeAdapter(T::class.java, adapter)
inline fun <reified T : Any> Jankson.Builder.registerTypeAdapter(noinline adapter: (JsonObject) -> T) =
this.registerTypeAdapter(T::class.java, adapter)
inline fun <reified T : Any> Jankson.Builder.registerPrimitiveTypeAdapter(noinline adapter: (Any) -> T) = this.registerPrimitiveTypeAdapter(T::class.java, adapter)
inline fun <reified T : Any> Jankson.Builder.registerPrimitiveTypeAdapter(noinline adapter: (Any) -> T) =
this.registerPrimitiveTypeAdapter(T::class.java, adapter)
inline fun <reified T : Any> Jankson.Builder.registerSerializer(noinline serializer: (T, Marshaller) -> JsonElement) = this.registerSerializer(T::class.java, serializer)
inline fun <reified T : Any> Jankson.Builder.registerSerializer(noinline serializer: (T, Marshaller) -> JsonElement) =
this.registerSerializer(T::class.java, serializer)
inline fun <reified T : Any> Marshaller.registerSerializer(noinline serializer: (T) -> JsonElement) = this.registerSerializer(T::class.java, serializer)
inline fun <reified T : Any> Marshaller.registerSerializer(noinline serializer: (T) -> JsonElement) =
this.registerSerializer(T::class.java, serializer)
inline fun <reified T : Any> Marshaller.registerSerializer(noinline serializer: (T, Marshaller) -> JsonElement) = this.registerSerializer(T::class.java, serializer)
inline fun <reified T : Any> Marshaller.registerSerializer(noinline serializer: (T, Marshaller) -> JsonElement) =
this.registerSerializer(T::class.java, serializer)
inline fun <reified T : Any> JsonObject.getReified(key: String): T? = this.get(T::class.java, key)
inline fun <reified T : Any> JsonObject.getReified(key: String, comment: String? = null): T? =
this.get(T::class.java, key)
?.also { setComment(key, comment) }
inline fun <reified T : Any> JsonObject.getReifiedOrDelete(key: String, comment: String? = null): T? =
this.get(T::class.java, key)
?.also { setComment(key, comment) }
?: run {
this.remove(key)
null
}
inline fun <reified T : Any> JsonObject.getList(key: String): List<T>? {
return this[key]?.let { array ->
@ -98,3 +113,37 @@ inline fun <reified T : Any> JsonObject.getList(key: String): List<T>? {
}
}
}
inline fun <reified T : Any> JsonObject.getOrPutList(key: String, default: List<T>, comment: String?): List<T> {
return this[key]?.let { array ->
when (array) {
is JsonArray -> {
array.indices.map { i ->
array.get(T::class.java, i) ?: throw NullPointerException("cannot parse ${array.get(i)}")
}
}
else -> null
}
}.also {
setComment(key, comment)
} ?: this.putDefault(key, default, comment) ?: default
}
inline fun <reified T : Any> JsonObject.getOrPutMap(
key: String,
default: Map<String, T>,
comment: String?
): Map<String, T> {
return this[key]?.let { map ->
when (map) {
is JsonObject -> {
map.mapValues { (key, element) ->
map.get(T::class.java, key) ?: throw NullPointerException("cannot parse $element")
}
}
else -> null
}
}.also {
setComment(key, comment)
} ?: this.putDefault(key, default, comment) ?: default
}

View File

@ -0,0 +1,88 @@
package matterlink.api
import kotlinx.serialization.Encoder
import kotlinx.serialization.Optional
import kotlinx.serialization.Serializable
import kotlinx.serialization.Serializer
import kotlinx.serialization.json.JSON
/**
* Created by nikky on 07/05/18.
*
* @author Nikky
* @version 1.0
*/
@Serializable
data class ApiMessage(
@Optional var username: String = "",
@Optional var text: String = "",
@Optional var gateway: String = "",
@Optional var timestamp: String = "",
@Optional var channel: String = "",
@Optional var userid: String = "",
@Optional var avatar: String = "",
@Optional var account: String = "",
@Optional var protocol: String = "",
@Optional var event: String = "",
@Optional var id: String = "",
@Optional var Extra: Map<String, String>? = null
) {
fun encode(): String {
return JSON.nonstrict.stringify(ApiMessage.serializer(), this)
}
override fun toString(): String = encode()
@Serializer(forClass = ApiMessage::class)
companion object {
val USER_ACTION = "user_action"
val JOIN_LEAVE = "join_leave"
override fun serialize(output: Encoder, obj: ApiMessage) {
val elemOutput = output.beginStructure(descriptor)
obj.username.takeIf { it.isNotEmpty() }?.let {
elemOutput.encodeStringElement(descriptor, 0, it)
}
obj.text.takeIf { it.isNotEmpty() }?.let {
elemOutput.encodeStringElement(descriptor, 1, it)
}
obj.gateway.takeIf { it.isNotEmpty() }?.let {
elemOutput.encodeStringElement(descriptor, 2, it)
}
obj.timestamp.takeIf { it.isNotEmpty() }?.let {
elemOutput.encodeStringElement(descriptor, 3, it)
}
obj.channel.takeIf { it.isNotEmpty() }?.let {
elemOutput.encodeStringElement(descriptor, 4, it)
}
obj.userid.takeIf { it.isNotEmpty() }?.let {
elemOutput.encodeStringElement(descriptor, 5, it)
}
obj.avatar.takeIf { it.isNotEmpty() }?.let {
elemOutput.encodeStringElement(descriptor, 6, it)
}
obj.account.takeIf { it.isNotEmpty() }?.let {
elemOutput.encodeStringElement(descriptor, 7, it)
}
obj.protocol.takeIf { it.isNotEmpty() }?.let {
elemOutput.encodeStringElement(descriptor, 8, it)
}
obj.event.takeIf { it.isNotEmpty() }?.let {
elemOutput.encodeStringElement(descriptor, 9, it)
}
obj.id.takeIf { it.isNotEmpty() }?.let {
elemOutput.encodeStringElement(descriptor, 10, it)
}
// obj.Extra.takeIf { ! it.isNullOrEmpty() }?.let {
// elemOutput.encodeStringElement(descriptor, 11, it)
// }
elemOutput.endStructure(descriptor)
}
fun decode(input: String): ApiMessage {
return JSON.nonstrict.parse(ApiMessage.serializer(), input)
}
}
}

View File

@ -0,0 +1,10 @@
package matterlink.api
data class Config(
var url: String = "",
var token: String = "",
var announceConnect: Boolean = true,
var announceDisconnect: Boolean = true,
var reconnectWait: Long = 500,
var systemUser: String = "Server"
)

View File

@ -0,0 +1,240 @@
package matterlink.api
import com.github.kittinunf.fuel.core.FuelManager
import com.github.kittinunf.fuel.core.Method
import com.github.kittinunf.fuel.core.ResponseDeserializable
import com.github.kittinunf.fuel.core.extensions.cUrlString
import com.github.kittinunf.fuel.core.extensions.jsonBody
import com.github.kittinunf.fuel.coroutines.awaitStringResponseResult
import com.github.kittinunf.fuel.httpGet
import com.github.kittinunf.fuel.httpPost
import com.github.kittinunf.result.Result
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.ObsoleteCoroutinesApi
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.channels.actor
import kotlinx.coroutines.channels.broadcast
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.JSON
import kotlinx.serialization.list
import matterlink.Logger
import java.io.Reader
import java.net.ConnectException
import kotlin.coroutines.CoroutineContext
/**
* Created by nikky on 07/05/18.
*
* @author Nikky
* @version 1.0
*/
open class MessageHandler : CoroutineScope {
override val coroutineContext: CoroutineContext = Job()
private var enabled = false
private var connectErrors = 0
private var reconnectCooldown = 0L
private var sendErrors = 0
private var sendChannel: SendChannel<ApiMessage> = senderActor()
private val messageStream = Channel<ApiMessage>(Channel.UNLIMITED)
@UseExperimental(ExperimentalCoroutinesApi::class)
var broadcast: BroadcastChannel<ApiMessage> = broadcast {
while (true) {
val msg = messageStream.receive()
send(msg)
}
}
private set
private val keepOpenManager = FuelManager().apply {
timeoutInMillisecond = 0
timeoutReadInMillisecond = 0
}
var config: Config = Config()
var logger = object : Logger {
override fun info(message: String) = println("INFO: $message")
override fun debug(message: String) = println("DEBUG: $message")
override fun error(message: String) = println("ERROR: $message")
override fun warn(message: String) = println("WARN: $message")
override fun trace(message: String) = println("TRACE: $message")
}
suspend fun stop(message: String? = null) {
if (message != null && config.announceDisconnect) {
sendStatusUpdate(message)
}
enabled = false
rcvJob?.cancel()
rcvJob = null
}
private var rcvJob: Job? = null
suspend fun start(message: String?, clear: Boolean) {
logger.debug("starting connection")
if (clear) {
clear()
}
enabled = true
rcvJob = messageBroadcast()
if (message != null && config.announceConnect) {
sendStatusUpdate(message)
}
}
private suspend fun clear() {
val url = "${config.url}/api/messages"
val (request, response, result) = url.httpGet()
.apply {
if (config.token.isNotEmpty()) {
headers["Authorization"] = "Bearer ${config.token}"
}
}
.awaitStringResponseResult()
when (result) {
is Result.Success -> {
val messages: List<ApiMessage> = JSON.nonstrict.parse(ApiMessage.list, result.value)
messages.forEach { msg ->
logger.trace("skipping $msg")
}
logger.debug("skipped ${messages.count()} messages")
}
is Result.Failure -> {
logger.error("failed to clear messages")
logger.error("url: $url")
logger.error("cUrl: ${request.cUrlString()}")
logger.error("response: $response")
logger.error(result.error.exception.localizedMessage)
result.error.exception.printStackTrace()
}
}
}
open suspend fun sendStatusUpdate(message: String) {
transmit(ApiMessage(text = message))
}
open suspend fun transmit(msg: ApiMessage) {
// if (streamConnection.isConnected || streamConnection.isConnecting) {
if (msg.username.isEmpty())
msg.username = config.systemUser
if (msg.gateway.isEmpty()) {
logger.error("missing gateway on message: $msg")
return
}
logger.debug("Transmitting: $msg")
sendChannel.send(msg)
// }
}
@Deprecated("use coroutine api", level = DeprecationLevel.ERROR)
fun checkConnection() {
}
@UseExperimental(ObsoleteCoroutinesApi::class)
private fun CoroutineScope.senderActor() = actor<ApiMessage>(context = Dispatchers.IO) {
consumeEach {
try {
logger.debug("sending $it")
val url = "${config.url}/api/message"
val (request, response, result) = url.httpPost()
.apply {
if (config.token.isNotEmpty()) {
headers["Authorization"] = "Bearer ${config.token}"
}
}
.jsonBody(it.encode())
.responseString()
when (result) {
is Result.Success -> {
logger.debug("sent $it")
sendErrors = 0
}
is Result.Failure -> {
sendErrors++
logger.error("failed to deliver: $it")
logger.error("url: $url")
logger.error("cUrl: ${request.cUrlString()}")
logger.error("response: $response")
logger.error(result.error.exception.localizedMessage)
result.error.exception.printStackTrace()
// close()
throw result.error.exception
}
}
} catch (connectError: ConnectException) {
connectError.printStackTrace()
sendErrors++
}
}
}
private fun CoroutineScope.messageBroadcast() = launch(context = Dispatchers.IO + CoroutineName("msgBroadcaster")) {
loop@ while (isActive) {
logger.info("opening connection")
val url = "${config.url}/api/stream"
val (request, response, result) = keepOpenManager.request(Method.GET, url)
.apply {
if (config.token.isNotEmpty()) {
headers["Authorization"] = "Bearer ${config.token}"
}
}
.responseObject(object : ResponseDeserializable<Unit> {
override fun deserialize(reader: Reader) =
runBlocking(Dispatchers.IO + CoroutineName("msgReceiver")) {
logger.info("connected successfully")
connectErrors = 0
reconnectCooldown = 0
reader.useLines { lines ->
lines.forEach { line ->
val msg = ApiMessage.decode(line)
logger.debug("received: $msg")
if (msg.event != "api_connect") {
messageStream.send(msg)
}
}
}
}
})
when (result) {
is Result.Success -> {
logger.info("connection closed")
}
is Result.Failure -> {
connectErrors++
reconnectCooldown = connectErrors * 1000L
logger.error("connectErrors: $connectErrors")
logger.error("connection error")
logger.error("curl: ${request.cUrlString()}")
logger.error(result.error.localizedMessage)
result.error.exception.printStackTrace()
if (connectErrors >= 10) {
logger.error("Caught too many errors, closing bridge")
stop("Interrupting connection to matterbridge API due to accumulated connection errors")
break@loop
}
}
}
delay(reconnectCooldown) // reconnect delay in ms
}
}
}

View File

@ -1,43 +1,63 @@
package matterlink.bridge
import matterlink.*
import matterlink.Paste
import matterlink.PasteSection
import matterlink.PasteUtil
import matterlink.antiping
import matterlink.api.ApiMessage
import matterlink.api.MessageHandler
import matterlink.config.cfg
import matterlink.handlers.ChatEvent
import matterlink.handlers.LocationHandler
import matterlink.mapFormat
import matterlink.stackTraceString
object MessageHandlerInst : MessageHandler() {
override fun transmit(msg: ApiMessage) {
override suspend fun transmit(msg: ApiMessage) {
transmit(msg, cause = "")
}
fun transmit(msg: ApiMessage, cause: String, maxLines: Int = cfg.outgoing.inlineLimit) {
override suspend fun sendStatusUpdate(message: String) {
LocationHandler.sendToLocations(
msg = message,
x = 0, y = 0, z = 0, dimension = null,
systemuser = true,
event = ChatEvent.STATUS,
cause = "status update message"
)
}
suspend fun transmit(msg: ApiMessage, cause: String, maxLines: Int = cfg.outgoing.inlineLimit) {
if (msg.username.isEmpty()) {
msg.username = cfg.outgoing.systemUser
if(msg.avatar.isEmpty() && cfg.outgoing.avatar.enable) {
if (msg.avatar.isEmpty() && cfg.outgoing.avatar.enable) {
msg.avatar = cfg.outgoing.avatar.systemUserAvatar
}
}
if (msg.gateway.isEmpty())
msg.gateway = cfg.connect.gateway
if (msg.gateway.isEmpty()) {
logger.error("dropped message '$msg' due to missing gateway")
return
}
if (msg.text.lines().count() >= maxLines) {
try {
val response = PasteUtil.paste(
Paste(
description = cause,
sections = listOf(
PasteSection(
name = "log.txt",
syntax = "text",
contents = msg.text
)
)
Paste(
description = cause,
sections = listOf(
PasteSection(
name = "log.txt",
syntax = "text",
contents = msg.text
)
)
)
)
msg.text = msg.text.substringBefore('\n')
.take(25) + "... " + response.link
} catch(e: Exception) {
.take(25) + "... " + response.link
} catch (e: Exception) {
logger.error(cause)
logger.error(e.stackTraceString)
}
}
@ -47,13 +67,13 @@ object MessageHandlerInst : MessageHandler() {
fun ApiMessage.format(fmt: String): String {
return fmt.mapFormat(
mapOf(
"{username}" to username,
"{text}" to text,
"{gateway}" to gateway,
"{channel}" to channel,
"{protocol}" to protocol,
"{username:antiping}" to username.antiping
)
mapOf(
"{username}" to username,
"{text}" to text,
"{gateway}" to gateway,
"{channel}" to channel,
"{protocol}" to protocol,
"{username:antiping}" to username.antiping
)
)
}

View File

@ -5,7 +5,7 @@ import matterlink.config.IdentitiesConfig
import matterlink.config.cfg
import matterlink.instance
import matterlink.randomString
import java.util.*
import java.util.UUID
object AuthBridgeCommand : IBridgeCommand() {
val syntax = "Syntax: auth [username]"
@ -13,7 +13,7 @@ object AuthBridgeCommand : IBridgeCommand() {
override val permLevel: Double
get() = cfg.command.defaultPermUnauthenticated
override fun execute(alias: String, user: String, env: CommandEnvironment, args: String): Boolean {
override suspend fun execute(alias: String, user: String, env: CommandEnvironment, args: String): Boolean {
if (env !is CommandEnvironment.BridgeEnv) {
env.respond("please initiate authentication from linked external chat")
return true
@ -28,8 +28,10 @@ object AuthBridgeCommand : IBridgeCommand() {
val argList = args.split(' ', limit = 2)
val target = argList.getOrNull(0) ?: run {
env.respond("no username/uuid provided\n" +
syntax)
env.respond(
"no username/uuid provided\n" +
syntax
)
return true
}
@ -62,7 +64,16 @@ object AuthBridgeCommand : IBridgeCommand() {
instance.wrappedSendToPlayer(targetUserName, "otherwise you may ignore this message")
IdentitiesConfig.authRequests.put(requestId, AuthRequest(username = targetUserName, uuid = targetUUid, nonce = nonce, platform = env.platform, userid = env.userId))
IdentitiesConfig.authRequests.put(
requestId,
AuthRequest(
username = targetUserName,
uuid = targetUUid,
nonce = nonce,
platform = env.platform,
userid = env.userId
)
)
env.respond("please accept the authentication request ingame, do not share the code")
return true

View File

@ -1,15 +1,13 @@
package matterlink.bridge.command
import matterlink.api.ApiMessage
import matterlink.bridge.MessageHandlerInst
import matterlink.config.CommandConfig
import matterlink.config.IdentitiesConfig
import matterlink.config.PermissionConfig
import matterlink.config.cfg
import matterlink.instance
import matterlink.logger
import matterlink.stripColorOut
import java.util.*
import java.util.HashMap
import java.util.UUID
object BridgeCommandRegistry {
@ -19,7 +17,7 @@ object BridgeCommandRegistry {
*
* @return consume message flag
*/
fun handleCommand(input: ApiMessage): Boolean {
suspend fun handleCommand(input: ApiMessage): Boolean {
if (!cfg.command.enable || input.text.isBlank()) return false
if (input.text[0] != cfg.command.prefix || input.text.length < 2) return false
@ -29,7 +27,13 @@ object BridgeCommandRegistry {
val uuid = IdentitiesConfig.getUUID(input.account, input.userid)
val env = IBridgeCommand.CommandEnvironment.BridgeEnv(input.username, input.userid, input.account, uuid)
val env = IBridgeCommand.CommandEnvironment.BridgeEnv(
input.username,
input.userid,
input.account,
input.gateway,
uuid
)
return commandMap[cmd[0]]?.let {
if (!it.reachedTimeout()) {
logger.debug("dropped command ${it.alias}")
@ -38,7 +42,7 @@ object BridgeCommandRegistry {
it.preExecute() // resets the tickCounter
if (!it.canExecute(uuid)) {
env.respond(
text = "${input.username} is not permitted to perform command: ${cmd[0]}"
text = "${input.username} is not permitted to perform command: ${cmd[0]}"
)
return false
}
@ -46,7 +50,7 @@ object BridgeCommandRegistry {
} ?: false
}
fun handleCommand(text: String, username: String, uuid: UUID): Boolean {
suspend fun handleCommand(text: String, username: String, uuid: UUID): Boolean {
if (!cfg.command.enable || text.isBlank()) return false
if (text[0] != cfg.command.prefix || text.length < 2) return false
@ -64,7 +68,7 @@ object BridgeCommandRegistry {
it.preExecute() // resets the tickCounter
if (!it.canExecute(uuid)) {
env.respond(
text = "$username is not permitted to perform command: ${cmd[0]}"
text = "$username is not permitted to perform command: ${cmd[0]}"
)
return false
}
@ -98,11 +102,11 @@ object BridgeCommandRegistry {
fun getCommandList(permLvl: Double): String {
return commandMap
.filterValues {
it.permLevel <= permLvl
}
.keys
.joinToString(" ")
.filterValues {
it.permLevel <= permLvl
}
.keys
.joinToString(" ")
}
fun reloadCommands() {

View File

@ -6,18 +6,18 @@ import matterlink.logger
import matterlink.stripColorIn
data class CustomCommand(
val type: CommandType = CommandType.RESPONSE,
val execute: String? = null,
val response: String? = null,
override val permLevel: Double = 0.0,
override val help: String = "",
override val timeout: Int = 20,
val defaultCommand: Boolean? = null,
val execOp: Boolean? = null,
val argumentsRegex: Regex? = null
val type: CommandType = CommandType.RESPONSE,
val execute: String? = null,
val response: String? = null,
override val permLevel: Double = 0.0,
override val help: String = "",
override val timeout: Int = 20,
val defaultCommand: Boolean? = null,
val execOp: Boolean? = null,
val argumentsRegex: Regex? = null
) : IBridgeCommand() {
override fun execute(alias: String, user: String, env: CommandEnvironment, args: String): Boolean {
override suspend fun execute(alias: String, user: String, env: CommandEnvironment, args: String): Boolean {
if (argumentsRegex != null) {
logger.debug("testing '$args' against '${argumentsRegex.pattern}'")
if (!argumentsRegex.matches(args)) {
@ -31,12 +31,14 @@ data class CustomCommand(
// uses a new commandsender for each use
val commandSender = instance.commandSenderFor(user, env, execOp ?: false)
val cmd = execute?.lazyFormat(getReplacements(user, env, args))?.stripColorIn
?: return false
?: return false
commandSender.execute(cmd) || commandSender.reply.isNotEmpty()
}
CommandType.RESPONSE -> {
env.respond(response?.lazyFormat(getReplacements(user, env, args))
?: "", cause = "response to command: $alias")
env.respond(
response?.lazyFormat(getReplacements(user, env, args))
?: "", cause = "response to command: $alias"
)
true
}
@ -60,34 +62,34 @@ data class CustomCommand(
val DEFAULT = CustomCommand()
fun getReplacements(user: String, env: CommandEnvironment, args: String): Map<String, () -> String?> = mapOf(
"{uptime}" to instance::getUptimeAsString,
"{user}" to { user },
"{userid}" to {
when (env) {
is CommandEnvironment.BridgeEnv -> env.userId
else -> null
}
},
"{uuid}" to {
when (env) {
is CommandEnvironment.BridgeEnv -> env.uuid.toString()
is CommandEnvironment.GameEnv -> env.uuid.toString()
}
},
"{username}" to {
when (env) {
is CommandEnvironment.BridgeEnv -> env.uuid
is CommandEnvironment.GameEnv -> env.uuid
}?.let { instance.uuidToName(it) }
},
"{platform}" to {
when (env) {
is CommandEnvironment.BridgeEnv -> env.platform
else -> null
}
},
"{args}" to { args },
"{version}" to { instance.modVersion }
"{uptime}" to instance::getUptimeAsString,
"{user}" to { user },
"{userid}" to {
when (env) {
is CommandEnvironment.BridgeEnv -> env.userId
else -> null
}
},
"{uuid}" to {
when (env) {
is CommandEnvironment.BridgeEnv -> env.uuid.toString()
is CommandEnvironment.GameEnv -> env.uuid.toString()
}
},
"{username}" to {
when (env) {
is CommandEnvironment.BridgeEnv -> env.uuid
is CommandEnvironment.GameEnv -> env.uuid
}?.let { instance.uuidToName(it) }
},
"{platform}" to {
when (env) {
is CommandEnvironment.BridgeEnv -> env.platform
else -> null
}
},
"{args}" to { args },
"{version}" to { instance.modVersion }
)
}
}

View File

@ -1,27 +1,24 @@
package matterlink.bridge.command
import matterlink.api.ApiMessage
import matterlink.bridge.MessageHandlerInst
import matterlink.config.cfg
import matterlink.stripColorOut
object HelpCommand : IBridgeCommand() {
override val help: String = "Returns the help string for the given command. Syntax: help <command>"
override val permLevel: Double
get() = cfg.command.defaultPermUnauthenticated
override fun execute(alias: String, user: String, env: CommandEnvironment, args: String): Boolean {
override suspend fun execute(alias: String, user: String, env: CommandEnvironment, args: String): Boolean {
val msg: String = when {
args.isEmpty() ->
"Available commands: ${BridgeCommandRegistry.getCommandList(IBridgeCommand.getPermLevel(env.uuid))}"
else -> args.split(" ", ignoreCase = false)
.joinToString(separator = "\n") {
"$it: ${BridgeCommandRegistry.getHelpString(it)}"
}
.joinToString(separator = "\n") {
"$it: ${BridgeCommandRegistry.getHelpString(it)}"
}
}
env.respond(
text = msg,
cause = "Help Requested $args"
text = msg,
cause = "Help Requested $args"
)
return true
}

View File

@ -8,7 +8,7 @@ import matterlink.handlers.TickHandler
import matterlink.instance
import matterlink.logger
import matterlink.stripColorOut
import java.util.*
import java.util.UUID
abstract class IBridgeCommand {
abstract val help: String
@ -20,28 +20,30 @@ abstract class IBridgeCommand {
abstract val username: String?
data class BridgeEnv(
val name: String,
val userId: String,
val platform: String,
override val uuid: UUID?
val name: String,
val userId: String,
val platform: String,
val gateway: String,
override val uuid: UUID?
) : CommandEnvironment() {
override val username: String?
get() = uuid?.let { instance.uuidToName(uuid) }
}
data class GameEnv(
override val username: String,
override val uuid: UUID
override val username: String,
override val uuid: UUID
) : CommandEnvironment()
fun respond(text: String, cause: String = "") {
suspend fun respond(text: String, cause: String = "") {
when (this) {
is BridgeEnv -> {
MessageHandlerInst.transmit(
ApiMessage(
text = text.stripColorOut
),
cause = cause
ApiMessage(
gateway = this.gateway,
text = text.stripColorOut
),
cause = cause
)
}
is GameEnv -> {
@ -70,7 +72,7 @@ abstract class IBridgeCommand {
*
* @return consume message flag
*/
abstract fun execute(alias: String, user: String, env: CommandEnvironment, args: String): Boolean
abstract suspend fun execute(alias: String, user: String, env: CommandEnvironment, args: String): Boolean
fun canExecute(uuid: UUID?): Boolean {
logger.trace("canExecute this: $this uuid: $uuid permLevel: $permLevel")

View File

@ -1,7 +1,5 @@
package matterlink.bridge.command
import matterlink.stripColorOut
abstract class IMinecraftCommandSender(val user: String, val env: IBridgeCommand.CommandEnvironment, val op: Boolean) {
/**
* @param cmdString The command to execute with its arguments
@ -13,7 +11,7 @@ abstract class IMinecraftCommandSender(val user: String, val env: IBridgeCommand
val displayName = env.username ?: user
val accountName = when (env) {
is IBridgeCommand.CommandEnvironment.BridgeEnv -> "$user (id=${env.userId} platform=${env.platform}${env.uuid?.let { " uuid=$it" }
?: ""}${env.username?.let { " username=$it" } ?: ""})"
?: ""}${env.username?.let { " username=$it" } ?: ""})"
is IBridgeCommand.CommandEnvironment.GameEnv -> "$user (username=${env.username} uuid=${env.uuid})"
}
@ -37,10 +35,10 @@ abstract class IMinecraftCommandSender(val user: String, val env: IBridgeCommand
reply += text
}
fun sendReply(cmdString: String) {
suspend fun sendReply(cmdString: String) {
env.respond(
text = reply.joinToString("\n"),
cause = "executed command: $cmdString"
text = reply.joinToString("\n"),
cause = "executed command: $cmdString"
)
finished = true
}

View File

@ -11,7 +11,7 @@ object RequestPermissionsCommand : IBridgeCommand() {
override val permLevel: Double
get() = cfg.command.defaultPermAuthenticated
override fun execute(alias: String, user: String, env: CommandEnvironment, args: String): Boolean {
override suspend fun execute(alias: String, user: String, env: CommandEnvironment, args: String): Boolean {
val uuid = env.uuid
if (uuid == null) {
@ -23,8 +23,10 @@ object RequestPermissionsCommand : IBridgeCommand() {
val requestedLevelArg = argList.getOrNull(0)
val requestedLevel = requestedLevelArg?.takeIf { it.isNotEmpty() }?.let {
it.toDoubleOrNull() ?: run {
env.respond("cannot parse permlevel '$requestedLevelArg'\n" +
syntax)
env.respond(
"cannot parse permlevel '$requestedLevelArg'\n" +
syntax
)
return true
}
}
@ -33,7 +35,10 @@ object RequestPermissionsCommand : IBridgeCommand() {
val requestId = user.toLowerCase()
PermissionConfig.permissionRequests.put(requestId, PermissionRequest(uuid = uuid, user = user, nonce = nonce, powerlevel = requestedLevel))
PermissionConfig.permissionRequests.put(
requestId,
PermissionRequest(uuid = uuid, user = user, nonce = nonce, powerlevel = requestedLevel)
)
env.respond("please ask a op to accept your permission elevation with `/ml permAccept $requestId $nonce [permLevel]`")
return true

View File

@ -1,11 +1,6 @@
package matterlink.command
import matterlink.bridge.MessageHandlerInst
import matterlink.bridge.command.BridgeCommandRegistry
import matterlink.config.IdentitiesConfig
import matterlink.config.PermissionConfig
import matterlink.config.baseCfg
import matterlink.config.cfg
object CommandCoreAuth {
val name = "auth"
@ -28,17 +23,23 @@ object CommandCoreAuth {
val nonce = args.getOrNull(2)?.toUpperCase() ?: run {
return "no code passed"
}
if(request.nonce != nonce) {
if (request.nonce != nonce) {
return "nonce in request does not match"
}
if(request.username != user) {
if (request.username != user) {
return "username in request does not match ${request.username} != $user"
}
if(request.uuid != uuid) {
if (request.uuid != uuid) {
return "uuid in request does not match ${request.uuid} != $uuid"
}
IdentitiesConfig.add(request.uuid, request.username, request.platform, request.userid, "Accepted by $user")
IdentitiesConfig.add(
request.uuid,
request.username,
request.platform,
request.userid,
"Accepted by $user"
)
IdentitiesConfig.authRequests.invalidate(requestId)
"${request.userid} on ${request.platform} is now identified as $user"
@ -54,13 +55,13 @@ object CommandCoreAuth {
val nonce = args.getOrNull(2)?.toUpperCase() ?: run {
return "no code passed"
}
if(request.nonce != nonce) {
if (request.nonce != nonce) {
return "nonce in request does not match"
}
if(request.username != user) {
if (request.username != user) {
return "username in request does not match ${request.username} != $user"
}
if(request.uuid != uuid) {
if (request.uuid != uuid) {
return "uuid in request does not match ${request.uuid} != $uuid"
}

View File

@ -13,7 +13,7 @@ object CommandCoreML {
val usage = "ml <connect|disconnect|reload|permAccept>"
fun execute(args: Array<String>, user: String, uuid: String?): String {
suspend fun execute(args: Array<String>, user: String, uuid: String?): String {
val cmd = args[0].toLowerCase()
return when (cmd) {
@ -45,12 +45,12 @@ object CommandCoreML {
return "no code passed"
}
if (request.nonce != nonce) {
return "nonce in request does not match"
return "nonce in request does not match with $nonce"
}
val powerLevelArg = args.getOrNull(2)?.toDoubleOrNull()
val powerLevel = powerLevelArg ?: run { return "permLevel cannot be parsed" }
?: request.powerlevel
?: return "no permLevel provided"
val powerLevelArg = args.getOrNull(3)?.toDoubleOrNull()
val powerLevel = powerLevelArg ?: run { return "permLevel cannot be parsed: ${args.getOrNull(3)}" }
?: request.powerlevel
?: return "no permLevel provided"
PermissionConfig.add(request.uuid, powerLevel, "${request.user} Authorized by $user")
PermissionConfig.permissionRequests.invalidate(requestId)
"added ${request.user} (uuid: ${request.uuid}) with power level: $powerLevel"

View File

@ -4,9 +4,13 @@ import blue.endless.jankson.Jankson
import blue.endless.jankson.JsonObject
import blue.endless.jankson.impl.Marshaller
import blue.endless.jankson.impl.SyntaxError
import matterlink.Area
import matterlink.bridge.MessageHandlerInst
import matterlink.getOrDefault
import matterlink.getOrPutList
import matterlink.getReifiedOrDelete
import matterlink.logger
import matterlink.registerSerializer
import matterlink.registerTypeAdapter
import matterlink.stackTraceString
import java.io.File
@ -20,376 +24,641 @@ data class BaseConfig(val rootDir: File) {
val configFile: File = cfgDirectory.resolve("matterlink.hjson")
init {
logger.info("Reading bridge blueprints... from {}", rootDir)
logger.info("Reading bridge blueprints... from $rootDir")
baseCfg = this
}
data class MatterLinkConfig(
val connect: ConnectOptions = ConnectOptions(),
val incoming: IncomingOptions = IncomingOptions(),
val outgoing: OutgoingOptions = OutgoingOptions(),
val command: CommandOptions = CommandOptions(),
val update: UpdateOptions = UpdateOptions()
val connect: ConnectOptions = ConnectOptions(),
val outgoingDefaults: DefaultSettingsOutgoing = DefaultSettingsOutgoing(),
val incomingDefaults: DefaultSettingsIncoming = DefaultSettingsIncoming(),
val locations: List<Location> = listOf(
Location(
label = "default",
gateway = "minecraft",
area = Area.Infinite(dimensions = listOf(-1, 0, 1), allDimensions = true),
outgoing = SettingsOutgoing(
plain = true,
action = true,
join = true,
leave = true,
advancement = true,
death = true,
broadcast = true,
status = true
),
incoming = SettingsIncoming(
plain = true,
action = true,
join_leave = true,
commands = true
)
)
),
val incoming: IncomingOptions = IncomingOptions(),
val outgoing: OutgoingOptions = OutgoingOptions(),
val command: CommandOptions = CommandOptions(),
val update: UpdateOptions = UpdateOptions()
)
data class DefaultSettingsOutgoing(
val plain: Boolean = true,
val action: Boolean = true,
val join: Boolean = false,
val leave: Boolean = false,
val advancement: Boolean = false,
val death: Boolean = false,
val broadcast: Boolean = false,
val status: Boolean = false
)
data class SettingsOutgoing(
val plain: Boolean? = null,
val action: Boolean? = null,
val join: Boolean? = null,
val leave: Boolean? = null,
val advancement: Boolean? = null,
val death: Boolean? = null,
val broadcast: Boolean? = null,
val status: Boolean? = null,
val skip: List<String> = listOf()
)
data class DefaultSettingsIncoming(
val plain: Boolean = true,
val action: Boolean = true,
val join_leave: Boolean = true,
val commands: Boolean = true
)
data class SettingsIncoming(
val plain: Boolean? = null,
val action: Boolean? = null,
val join_leave: Boolean? = null,
val commands: Boolean? = null,
val skip: List<String> = listOf()
)
data class Location(
val label: String = "unlabeled",
val gateway: String = "",
val area: Area = Area.Infinite(),
val outgoing: SettingsOutgoing = SettingsOutgoing(),
val incoming: SettingsIncoming = SettingsIncoming()
)
data class CommandOptions(
val prefix: Char = '!',
val enable: Boolean = true,
val authRequests: Boolean = true,
val permisionRequests: Boolean = true,
val defaultPermUnauthenticated: Double = 0.0,
val defaultPermAuthenticated: Double = 1.0
val prefix: Char = '!',
val enable: Boolean = true,
val authRequests: Boolean = true,
val permisionRequests: Boolean = true,
val defaultPermUnauthenticated: Double = 0.0,
val defaultPermAuthenticated: Double = 1.0
)
data class ConnectOptions(
val url: String = "http://localhost:4242",
val authToken: String = "",
val gateway: String = "minecraft",
val autoConnect: Boolean = true,
val reconnectWait: Long = 500
val url: String = "http://localhost:4242",
val authToken: String = "",
val autoConnect: Boolean = true,
val reconnectWait: Long = 500
)
data class IncomingOptions(
val chat: String = "<{username}> {text}",
val joinPart: String = "§6-- {username} {text}",
val action: String = "§5* {username} {text}",
val stripColors: Boolean = true
val chat: String = "<{username}> {text}",
val joinPart: String = "§6-- {username} {text}",
val action: String = "§5* {username} {text}",
val stripColors: Boolean = true
)
data class OutgoingOptions(
val systemUser: String = "Server",
//outgoing toggles
val announceConnect: Boolean = true,
val announceDisconnect: Boolean = true,
val advancements: Boolean = true,
val stripColors: Boolean = true,
val pasteEEKey: String = "",
val inlineLimit: Int = 5,
val systemUser: String = "Server",
//outgoing toggles
val announceConnect: Boolean = true,
val announceDisconnect: Boolean = true,
val advancements: Boolean = true,
val stripColors: Boolean = true,
val pasteEEKey: String = "",
val inlineLimit: Int = 5,
val joinPart: JoinPartOptions = JoinPartOptions(),
var avatar: AvatarOptions = AvatarOptions(),
val death: DeathOptions = DeathOptions()
val joinPart: JoinPartOptions = JoinPartOptions(),
var avatar: AvatarOptions = AvatarOptions(),
val death: DeathOptions = DeathOptions()
)
data class DeathOptions(
val enable: Boolean = true,
val damageType: Boolean = true,
val damageTypeMapping: Map<String, Array<String>> = mapOf(
"inFire" to arrayOf("\uD83D\uDD25"), //🔥
"lightningBolt" to arrayOf("\uD83C\uDF29"), //🌩
"onFire" to arrayOf("\uD83D\uDD25"), //🔥
"lava" to arrayOf("\uD83D\uDD25"), //🔥
"hotFloor" to arrayOf("♨️"),
"inWall" to arrayOf(),
"cramming" to arrayOf(),
"drown" to arrayOf("\uD83C\uDF0A"), //🌊
"starve" to arrayOf("\uD83D\uDC80"), //💀
"cactus" to arrayOf("\uD83C\uDF35"), //🌵
"fall" to arrayOf("\u2BEF"), //⯯️
"flyIntoWall" to arrayOf("\uD83D\uDCA8"), //💨
"outOfWorld" to arrayOf("\u2734"), //✴
"generic" to arrayOf("\uD83D\uDC7B"), //👻
"magic" to arrayOf("", ""),
"indirectMagic" to arrayOf("", ""),
"wither" to arrayOf("\uD83D\uDD71"), //🕱
"anvil" to arrayOf(),
"fallingBlock" to arrayOf(),
"dragonBreath" to arrayOf("\uD83D\uDC32"), //🐲
"fireworks" to arrayOf("\uD83C\uDF86"), //🎆
val enable: Boolean = true,
val damageType: Boolean = true,
val damageTypeMapping: Map<String, Array<String>> = mapOf(
"inFire" to arrayOf("\uD83D\uDD25"), //🔥
"lightningBolt" to arrayOf("\uD83C\uDF29"), //🌩
"onFire" to arrayOf("\uD83D\uDD25"), //🔥
"lava" to arrayOf("\uD83D\uDD25"), //🔥
"hotFloor" to arrayOf("♨️"),
"inWall" to arrayOf(),
"cramming" to arrayOf(),
"drown" to arrayOf("\uD83C\uDF0A"), //🌊
"starve" to arrayOf("\uD83D\uDC80"), //💀
"cactus" to arrayOf("\uD83C\uDF35"), //🌵
"fall" to arrayOf("\u2BEF"), //⯯️
"flyIntoWall" to arrayOf("\uD83D\uDCA8"), //💨
"outOfWorld" to arrayOf("\u2734"), //✴
"generic" to arrayOf("\uD83D\uDC7B"), //👻
"magic" to arrayOf("", ""),
"indirectMagic" to arrayOf("", ""),
"wither" to arrayOf("\uD83D\uDD71"), //🕱
"anvil" to arrayOf(),
"fallingBlock" to arrayOf(),
"dragonBreath" to arrayOf("\uD83D\uDC32"), //🐲
"fireworks" to arrayOf("\uD83C\uDF86"), //🎆
"mob" to arrayOf("\uD83D\uDC80"), //💀
"player" to arrayOf("\uD83D\uDDE1"), //🗡
"arrow" to arrayOf("\uD83C\uDFF9"), //🏹
"thrown" to arrayOf("彡°"),
"thorns" to arrayOf("\uD83C\uDF39"), //🌹
"explosion" to arrayOf("\uD83D\uDCA3", "\uD83D\uDCA5"), //💣 💥
"explosion.player" to arrayOf("\uD83D\uDCA3", "\uD83D\uDCA5") //💣 💥
)
"mob" to arrayOf("\uD83D\uDC80"), //💀
"player" to arrayOf("\uD83D\uDDE1"), //🗡
"arrow" to arrayOf("\uD83C\uDFF9"), //🏹
"thrown" to arrayOf("彡°"),
"thorns" to arrayOf("\uD83C\uDF39"), //🌹
"explosion" to arrayOf("\uD83D\uDCA3", "\uD83D\uDCA5"), //💣 💥
"explosion.player" to arrayOf("\uD83D\uDCA3", "\uD83D\uDCA5"), //💣 💥
"ieWireShock" to arrayOf("\uD83D\uDD0C", "\u26A1"), //🔌 ⚡
"immersiverailroading:hitByTrain" to arrayOf(
"\uD83D\uDE82",
"\uD83D\uDE83",
"\uD83D\uDE84",
"\uD83D\uDE85",
"\uD83D\uDE87",
"\uD83D\uDE88",
"\uD83D\uDE8A"
) //🚂 🚃 🚄 🚅 🚇 🚈 🚊
)
)
data class AvatarOptions(
val enable: Boolean = true,
val urlTemplate: String = "https://visage.surgeplay.com/head/512/{uuid}",
// https://www.freepik.com/free-icon/right-arrow-angle-and-horizontal-down-line-code-signs_732795.htm
val systemUserAvatar: String = "https://image.freepik.com/free-icon/right-arrow-angle-and-horizontal-down-line-code-signs_318-53994.jpg"
val enable: Boolean = true,
val urlTemplate: String = "https://mc-heads.net/head/{uuid}",
val systemUserAvatar: String = ""
)
data class JoinPartOptions(
val enable: Boolean = true,
val joinServer: String = "{username:antiping} has connected to the platform",
val partServer: String = "{username:antiping} has disconnected from the platform"
val enable: Boolean = true,
val joinServer: String = "{username:antiping} has connected to the server",
val partServer: String = "{username:antiping} has disconnected from the server"
)
data class UpdateOptions(
val enable: Boolean = true
val enable: Boolean = true
)
companion object {
val jankson = Jankson
.builder()
.registerTypeAdapter {
.builder()
.registerTypeAdapter {
with(MatterLinkConfig()) {
MatterLinkConfig(
command = it.getOrDefault(
"command",
CommandOptions(),
"User commands"
),
connect = it.getOrDefault(
"connect",
ConnectOptions(),
"Connection Settings"
),
incoming = it.getOrDefault(
"incoming",
IncomingOptions(),
"""
Gateway -> Server
Options all about receiving messages from the API
Formatting options:
Available variables: {username}, {text}, {gateway}, {channel}, {protocol}, {username:antiping}
""".trimIndent()
),
outgoing = it.getOrDefault(
"outgoing",
OutgoingOptions(),
"""
Server -> Gateway
Options all about sending messages to the API
""".trimIndent()
),
update = it.getOrDefault(
"update",
UpdateOptions(),
"Update Settings"
)
command = it.getOrDefault(
"command",
command,
"User commands"
),
outgoingDefaults = it.getOrDefault(
"outgoingDefaults",
outgoingDefaults,
"default settings for outgoing"
),
incomingDefaults = it.getOrDefault(
"incomingDefaults",
incomingDefaults,
"default settings for incoming"
),
locations = it.getOrPutList(
"locations",
locations,
"list of fixed chat locations"
),
connect = it.getOrDefault(
"connect",
connect,
"Connection Settings"
),
incoming = it.getOrDefault(
"incoming",
incoming,
"""
Gateway -> Server
Options all about receiving messages from the API
Formatting options:
Available variables: {username}, {text}, {gateway}, {channel}, {protocol}, {username:antiping}
""".trimIndent()
),
outgoing = it.getOrDefault(
"outgoing",
outgoing,
"""
Server -> Gateway
Options all about sending messages to the API
""".trimIndent()
),
update = it.getOrDefault(
"update",
update,
"Update Settings"
)
)
}
.registerTypeAdapter {
with(CommandOptions()) {
CommandOptions(
enable = it.getOrDefault(
"enable",
enable,
"Enable MC bridge commands"
),
prefix = it.getOrDefault(
"prefix",
prefix,
"Prefix for MC bridge commands. Accepts a single character (not alphanumeric or /)"
),
authRequests = it.getOrDefault(
"authRequests",
authRequests,
"Enable the 'auth' command for linking chat accounts to uuid / ingame account"
),
permisionRequests = it.getOrDefault(
"permisionRequests",
authRequests,
"Enable the 'request' command for requestion permissions from chat"
),
defaultPermUnauthenticated = it.getOrDefault(
"defaultPermUnauthenticated",
defaultPermUnauthenticated,
"default permission level for unauthenticated players"
),
defaultPermAuthenticated = it.getOrDefault(
"defaultPermAuthenticated",
defaultPermAuthenticated,
"default permission level for players that hve linked their accounts"
)
}
.registerTypeAdapter {
with(DefaultSettingsOutgoing()) {
DefaultSettingsOutgoing(
plain = it.getOrDefault(
"plain",
plain,
"plain text messages"
),
action = it.getOrDefault(
"action",
action,
"action messages"
),
join = it.getOrDefault(
"join",
join,
"handle join event"
),
leave = it.getOrDefault(
"leave",
leave,
"handle leave events"
),
advancement = it.getOrDefault(
"advancement",
advancement,
"handle advancement events"
),
death = it.getOrDefault(
"death",
death,
"handle death events"
),
broadcast = it.getOrDefault(
"broadcast",
broadcast,
"handle broadcast command"
),
status = it.getOrDefault(
"status",
status,
"handles tatus updates"
)
}
)
}
.registerTypeAdapter {
with(ConnectOptions()) {
ConnectOptions(
url = it.getOrDefault(
"url",
url,
"The URL or IP address of the bridge platform"
),
authToken = it.getOrDefault(
"authToken",
authToken,
"Auth token used to connect to the bridge platform"
),
gateway = it.getOrDefault(
"gateway",
gateway,
"MatterBridge gateway"
),
autoConnect = it.getOrDefault(
"autoConnect",
autoConnect,
"Connect the relay on startup"
)
}
.registerTypeAdapter {
with(SettingsOutgoing()) {
SettingsOutgoing(
plain = it.getReifiedOrDelete("plain", "transmit join events"),
action = it.getReifiedOrDelete("action", "transmit join events"),
join = it.getReifiedOrDelete("join", "transmit join events"),
leave = it.getReifiedOrDelete("leave", "transmit leave events"),
advancement = it.getReifiedOrDelete("advancement", "transmit advancements"),
death = it.getReifiedOrDelete("death", "transmit death messages"),
broadcast = it.getReifiedOrDelete("say", "transmit broadcasts"),
status = it.getReifiedOrDelete("status", "transmit status updates"),
skip = it.getOrPutList(
"skip",
skip,
"list of other locations to ignore after handling this"
)
}
)
}
.registerTypeAdapter {
with(IncomingOptions()) {
IncomingOptions(
chat = it.getOrDefault(
"chat",
chat,
"Generic chat event, just talking"
),
joinPart = it.getOrDefault(
"joinPart",
joinPart,
"Join and part events from other gateways"
),
action = it.getOrDefault(
"action",
action,
"User actions (/me) sent by users from other gateways"
),
stripColors = it.getOrDefault(
"stripColors",
stripColors,
"strip colors from incoming text"
)
}
.registerTypeAdapter {
with(DefaultSettingsIncoming()) {
DefaultSettingsIncoming(
plain = it.getOrDefault(
"plain",
plain,
"plain text messages"
),
action = it.getOrDefault(
"action",
action,
"action messages"
),
join_leave = it.getOrDefault(
"join_leave",
join_leave,
"handle join/leave event"
),
commands = it.getOrDefault(
"commands",
join_leave,
"receive commands"
)
}
)
}
.registerTypeAdapter {
with(OutgoingOptions()) {
OutgoingOptions(
systemUser = it.getOrDefault(
"systemUser",
systemUser,
"Name of the platform user (used by death and advancement messages and the /say command)"
),
advancements = it.getOrDefault(
"advancements",
advancements,
"Relay player achievements / advancements"
),
announceConnect = it.getOrDefault(
"announceConnect",
announceConnect,
"announce successful connection to the gateway"
),
announceDisconnect = it.getOrDefault(
"announceDisconnect",
announceConnect,
"announce intention to disconnect / reconnect"
),
stripColors = it.getOrDefault(
"stripColors",
stripColors,
"strip colors from nicknames and messages"
),
pasteEEKey = it.getOrDefault(
"pasteEEKey",
pasteEEKey,
"paste.ee api key, leave empty to use application default"
),
inlineLimit = it.getOrDefault(
"inlineLimit",
inlineLimit,
"messages with more lines than this will get shortened via paste.ee"
),
death = it.getOrDefault(
"death",
DeathOptions(),
"Death messages settings"
),
avatar = it.getOrDefault(
"avatar",
AvatarOptions(),
"Avatar options"
),
joinPart = it.getOrDefault(
"joinPart",
JoinPartOptions(),
"relay join and part messages to the gatway"
)
}
.registerTypeAdapter {
with(SettingsIncoming()) {
SettingsIncoming(
plain = it.getReifiedOrDelete("plain", "transmit join events"),
action = it.getReifiedOrDelete("action", "transmit join events"),
join_leave = it.getReifiedOrDelete("join_leave", "transmit join_leave events"),
commands = it.getReifiedOrDelete("commands", "receive commands"),
skip = it.getOrPutList(
"skip",
skip,
"list of other locations to ignore after handling this"
)
}
)
}
.registerTypeAdapter { jsonObj ->
with(DeathOptions()) {
DeathOptions(
enable = jsonObj.getOrDefault(
"enable",
enable,
"Relay player death messages"
),
damageType = jsonObj.getOrDefault(
"damageType",
damageType,
"Enable Damage type symbols on death messages"
),
damageTypeMapping = (jsonObj.getObject("damageTypeMapping")
?: Marshaller.getFallback().serialize(damageTypeMapping) as JsonObject)
.let {
jsonObj.setComment(
"damageTypMapping",
"Damage type mapping for death cause"
)
it.mapValues { (key, element) ->
it.getOrDefault(key, damageTypeMapping[key] ?: emptyArray(), key)
.apply { it[key] }.apply {
jsonObj["damageTypeMapping"] = it
}
}
}
.registerTypeAdapter {
with(Location()) {
Location(
label = it.getOrDefault(
"label",
label,
"location label for identification"
),
gateway = it.getOrDefault(
"gateway",
gateway,
"matterbridge gateway identifier"
),
area = Area.parse(it.getObject("area") ?: JsonObject()),
outgoing = it.getOrDefault(
"outgoing",
outgoing,
"Location outgoing settings"
),
incoming = it.getOrDefault(
"incoming",
incoming,
"incoming settings"
)
)
}
}
.registerTypeAdapter {
with(CommandOptions()) {
CommandOptions(
enable = it.getOrDefault(
"enable",
enable,
"Enable MC bridge commands"
),
prefix = it.getOrDefault(
"prefix",
prefix,
"Prefix for MC bridge commands. Accepts a single character (not alphanumeric or /)"
),
authRequests = it.getOrDefault(
"authRequests",
authRequests,
"Enable the 'auth' command for linking chat accounts to uuid / ingame account"
),
permisionRequests = it.getOrDefault(
"permisionRequests",
authRequests,
"Enable the 'request' command for requestion permissions from chat"
),
defaultPermUnauthenticated = it.getOrDefault(
"defaultPermUnauthenticated",
defaultPermUnauthenticated,
"default permission level for unauthenticated players"
),
defaultPermAuthenticated = it.getOrDefault(
"defaultPermAuthenticated",
defaultPermAuthenticated,
"default permission level for players that hve linked their accounts"
)
)
}
}
.registerTypeAdapter {
with(ConnectOptions()) {
ConnectOptions(
url = it.getOrDefault(
"url",
url,
"The URL or IP address of the bridge platform"
),
authToken = it.getOrDefault(
"authToken",
authToken,
"Auth token used to connect to the bridge platform"
),
autoConnect = it.getOrDefault(
"autoConnect",
autoConnect,
"Connect the relay on startup"
),
reconnectWait = it.getOrDefault(
"reconnectWait",
reconnectWait,
"base delay in milliseconds between attempting reconnects"
)
)
}
}
.registerTypeAdapter {
with(IncomingOptions()) {
IncomingOptions(
chat = it.getOrDefault(
"chat",
chat,
"Generic chat event, just talking"
),
joinPart = it.getOrDefault(
"joinPart",
joinPart,
"Join and part events from other gateways"
),
action = it.getOrDefault(
"action",
action,
"User actions (/me) sent by users from other gateways"
),
stripColors = it.getOrDefault(
"stripColors",
stripColors,
"strip colors from incoming text"
)
)
}
}
.registerTypeAdapter {
with(OutgoingOptions()) {
OutgoingOptions(
systemUser = it.getOrDefault(
"systemUser",
systemUser,
"Name of the platform user (used by death and advancement messages and the /say command)"
),
advancements = it.getOrDefault(
"advancements",
advancements,
"Relay player achievements / advancements"
),
announceConnect = it.getOrDefault(
"announceConnect",
announceConnect,
"announce successful connection to the gateway"
),
announceDisconnect = it.getOrDefault(
"announceDisconnect",
announceConnect,
"announce intention to disconnect / reconnect"
),
stripColors = it.getOrDefault(
"stripColors",
stripColors,
"strip colors from nicknames and messages"
),
pasteEEKey = it.getOrDefault(
"pasteEEKey",
pasteEEKey,
"paste.ee api key, leave empty to use application default"
),
inlineLimit = it.getOrDefault(
"inlineLimit",
inlineLimit,
"messages with more lines than this will get shortened via paste.ee"
),
death = it.getOrDefault(
"death",
DeathOptions(),
"Death messages settings"
),
avatar = it.getOrDefault(
"avatar",
AvatarOptions(),
"Avatar options"
),
joinPart = it.getOrDefault(
"joinPart",
JoinPartOptions(),
"relay join and part messages to the gatway"
)
)
}
}
.registerTypeAdapter { jsonObj ->
with(DeathOptions()) {
DeathOptions(
enable = jsonObj.getOrDefault(
"enable",
enable,
"Relay player death messages"
),
damageType = jsonObj.getOrDefault(
"damageType",
damageType,
"Enable Damage type symbols on death messages"
),
damageTypeMapping = (jsonObj.getObject("damageTypeMapping")
?: Marshaller.getFallback().serialize(damageTypeMapping) as JsonObject)
.let {
jsonObj.setComment(
"damageTypMapping",
"Damage type mapping for death cause"
)
it.mapValues { (key, _) ->
it.getOrDefault(key, damageTypeMapping[key] ?: emptyArray(), key)
.apply { it[key] }.apply {
jsonObj["damageTypeMapping"] = it
}
)
}
}
}
)
}
.registerTypeAdapter {
with(AvatarOptions()) {
AvatarOptions(
enable = it.getOrDefault(
"enable",
enable,
"enable ingame avatar"
),
urlTemplate = it.getOrDefault(
"urlTemplate",
urlTemplate,
"template for constructing the user avatar url using the uuid"
)
}
.registerTypeAdapter {
with(AvatarOptions()) {
AvatarOptions(
enable = it.getOrDefault(
"enable",
enable,
"enable ingame avatar"
),
urlTemplate = it.getOrDefault(
"urlTemplate",
urlTemplate,
"template for constructing the user avatar url using the uuid"
),
systemUserAvatar = it.getOrDefault(
"systemUserAvatar",
systemUserAvatar,
"avatar url for the system user"
)
}
)
}
.registerTypeAdapter {
with(JoinPartOptions()) {
JoinPartOptions(
enable = it.getOrDefault(
"enable",
enable,
"Relay when a player joins / parts the game" +
"\nany receiving end still needs to be configured with showJoinPart = true" +
"\nto display the messages"
),
joinServer = it.getOrDefault(
"joinServer",
joinServer,
"user join message sent to other gateways, available variables: {username}, {username:antiping}"
),
partServer = it.getOrDefault(
"partServer",
partServer,
"user part message sent to other gateways, available variables: {username}, {username:antiping}"
)
}
.registerTypeAdapter {
with(JoinPartOptions()) {
JoinPartOptions(
enable = it.getOrDefault(
"enable",
enable,
"Relay when a player joins / parts the game" +
"\nany receiving end still needs to be configured with showJoinPart = true" +
"\nto display the messages"
),
joinServer = it.getOrDefault(
"joinServer",
joinServer,
"user join message sent to other gateways, available variables: {username}, {username:antiping}"
),
partServer = it.getOrDefault(
"partServer",
partServer,
"user part message sent to other gateways, available variables: {username}, {username:antiping}"
)
}
)
}
.registerTypeAdapter {
with(UpdateOptions()) {
UpdateOptions(
enable = it.getOrDefault(
"enable",
enable,
"Enable Update checking"
)
}
.registerTypeAdapter {
with(UpdateOptions()) {
UpdateOptions(
enable = it.getOrDefault(
"enable",
enable,
"Enable Update checking"
)
}
)
}
.build()
}
.registerSerializer { locationSettings: SettingsOutgoing, marshaller: Marshaller ->
val jsonObject = JsonObject()
locationSettings.plain?.let {
jsonObject["plain"] = marshaller.serialize(it)
}
locationSettings.action?.let {
jsonObject["action"] = marshaller.serialize(it)
}
locationSettings.join?.let {
jsonObject["join"] = marshaller.serialize(it)
}
locationSettings.leave?.let {
jsonObject["leave"] = marshaller.serialize(it)
}
locationSettings.advancement?.let {
jsonObject["advancement"] = marshaller.serialize(it)
}
locationSettings.death?.let {
jsonObject["death"] = marshaller.serialize(it)
}
locationSettings.broadcast?.let {
jsonObject["broadcast"] = marshaller.serialize(it)
}
locationSettings.status?.let {
jsonObject["status"] = marshaller.serialize(it)
}
locationSettings.skip.let {
jsonObject["skip"] = marshaller.serialize(it)
}
jsonObject
}
.build()!!
}
fun load(): MatterLinkConfig {
@ -417,15 +686,15 @@ data class BaseConfig(val rootDir: File) {
logger.error("error parsing config: ${e.completeMessage} ")
logger.error(e.stackTraceString)
cfgDirectory.resolve("error.matterlink.hjson").writeText(jsonObject.toJson(false, true))
MatterLinkConfig()
if (::cfg.isInitialized) cfg else MatterLinkConfig()
} catch (e: IllegalStateException) {
logger.error(e.stackTraceString)
cfgDirectory.resolve("error.matterlink.hjson").writeText(jsonObject.toJson(false, true))
MatterLinkConfig()
if (::cfg.isInitialized) cfg else MatterLinkConfig()
} catch (e: NullPointerException) {
logger.error("error loading config: ${e.stackTraceString}")
cfgDirectory.resolve("error.matterlink.hjson").writeText(jsonObject.toJson(false, true))
MatterLinkConfig()
if (::cfg.isInitialized) cfg else MatterLinkConfig()
}
// val defaultJsonObject = jankson.load("{}")
@ -434,7 +703,6 @@ data class BaseConfig(val rootDir: File) {
MessageHandlerInst.config.url = tmpCfg.connect.url
MessageHandlerInst.config.token = tmpCfg.connect.authToken
MessageHandlerInst.config.gateway = tmpCfg.connect.gateway
MessageHandlerInst.config.reconnectWait = tmpCfg.connect.reconnectWait
MessageHandlerInst.config.systemUser = tmpCfg.outgoing.systemUser

View File

@ -4,9 +4,12 @@ import blue.endless.jankson.Jankson
import blue.endless.jankson.JsonObject
import blue.endless.jankson.JsonPrimitive
import blue.endless.jankson.impl.SyntaxError
import matterlink.*
import matterlink.bridge.command.CommandType
import matterlink.bridge.command.CustomCommand
import matterlink.getOrDefault
import matterlink.logger
import matterlink.registerPrimitiveTypeAdapter
import matterlink.registerTypeAdapter
import java.io.File
import java.io.FileNotFoundException
@ -17,89 +20,89 @@ object CommandConfig {
private val configFile: File = baseCfg.cfgDirectory.resolve("commands.hjson")
private val default: DefaultCommands = mapOf(
"tps" to ("""Your run off the mill tps commands, change it to /sampler tps or /cofh tps if you like
"tps" to ("""Your run off the mill tps commands, change it to /sampler tps or /cofh tps if you like
|make sure to disable defaultCommand if you want your edits to have any effect
""".trimMargin()
to CustomCommand(
type = CommandType.EXECUTE,
execute = "forge tps",
help = "Print platform tps",
timeout = 200,
defaultCommand = true
)),
"list" to ("lists all the players, this is just a straight pass-through"
to CustomCommand(
type = CommandType.EXECUTE,
execute = "list",
help = "List online players",
defaultCommand = true
)),
"seed" to ("another straight pass-through"
to CustomCommand(
type = CommandType.EXECUTE,
execute = "seed",
help = "Print platform world seed",
defaultCommand = true
)),
"uptime" to ("this is a reponse command, it uses the uptime function, time since the mod was first loaded"
to CustomCommand(
type = CommandType.RESPONSE,
response = "{uptime}",
help = "Print platform uptime",
defaultCommand = true
)),
"whoami" to ("this shows you some of the other response macros"
to CustomCommand(
type = CommandType.RESPONSE,
response = "name: `{user}` userid: `{userid}` platform: `{platform}` username: `{username}` uuid: `{uuid}`",
help = "Print debug user data",
timeout = 200,
defaultCommand = true
)),
"version" to ("are you out of date huh ?"
to CustomCommand(
type = CommandType.RESPONSE,
response = "{version}",
help = "are you out of date huh ?",
timeout = 200,
defaultCommand = true
)),
"exec" to ("this uses arguments in a passed-through command, you could restrict the arguments with a regex"
to CustomCommand(
type = CommandType.EXECUTE,
execute = "{args}",
argumentsRegex = ".*".toRegex(),
permLevel = 50.0,
help = "Execute any command as OP, be careful with this one",
execOp = true,
defaultCommand = true
))
to CustomCommand(
type = CommandType.EXECUTE,
execute = "forge tps",
help = "Print platform tps",
timeout = 200,
defaultCommand = true
)),
"list" to ("lists all the players, this is just a straight pass-through"
to CustomCommand(
type = CommandType.EXECUTE,
execute = "list",
help = "List online players",
defaultCommand = true
)),
"seed" to ("another straight pass-through"
to CustomCommand(
type = CommandType.EXECUTE,
execute = "seed",
help = "Print platform world seed",
defaultCommand = true
)),
"uptime" to ("this is a reponse command, it uses the uptime function, time since the mod was first loaded"
to CustomCommand(
type = CommandType.RESPONSE,
response = "{uptime}",
help = "Print platform uptime",
defaultCommand = true
)),
"whoami" to ("this shows you some of the other response macros"
to CustomCommand(
type = CommandType.RESPONSE,
response = "name: `{user}` userid: `{userid}` platform: `{platform}` username: `{username}` uuid: `{uuid}`",
help = "Print debug user data",
timeout = 200,
defaultCommand = true
)),
"version" to ("are you out of date huh ?"
to CustomCommand(
type = CommandType.RESPONSE,
response = "{version}",
help = "are you out of date huh ?",
timeout = 200,
defaultCommand = true
)),
"exec" to ("this uses arguments in a passed-through command, you could restrict the arguments with a regex"
to CustomCommand(
type = CommandType.EXECUTE,
execute = "{args}",
argumentsRegex = ".*".toRegex(),
permLevel = 50.0,
help = "Execute any command as OP, be careful with this one",
execOp = true,
defaultCommand = true
))
)
val commands: CommandMap = hashMapOf()
fun loadFile() {
val jankson = Jankson
.builder()
.registerTypeAdapter { jsonObj ->
with(CustomCommand.DEFAULT) {
CustomCommand(
type = jsonObj.get(CommandType::class.java, "type") ?: type,
execute = jsonObj.get(String::class.java, "execute") ?: execute,
response = jsonObj.get(String::class.java, "response") ?: response,
permLevel = jsonObj.get(Double::class.java, "permLevel") ?: permLevel,
help = jsonObj.get(String::class.java, "help") ?: help,
timeout = jsonObj.get(Int::class.java, "timeout") ?: timeout,
defaultCommand = jsonObj.get(Boolean::class.java, "defaultCommand") ?: defaultCommand,
execOp = jsonObj.get(Boolean::class.java, "execOp") ?: execOp,
argumentsRegex = jsonObj.get(Regex::class.java, "argumentsRegex") ?: argumentsRegex
)
}
.builder()
.registerTypeAdapter { jsonObj ->
with(CustomCommand.DEFAULT) {
CustomCommand(
type = jsonObj.get(CommandType::class.java, "type") ?: type,
execute = jsonObj.get(String::class.java, "execute") ?: execute,
response = jsonObj.get(String::class.java, "response") ?: response,
permLevel = jsonObj.get(Double::class.java, "permLevel") ?: permLevel,
help = jsonObj.get(String::class.java, "help") ?: help,
timeout = jsonObj.get(Int::class.java, "timeout") ?: timeout,
defaultCommand = jsonObj.get(Boolean::class.java, "defaultCommand") ?: defaultCommand,
execOp = jsonObj.get(Boolean::class.java, "execOp") ?: execOp,
argumentsRegex = jsonObj.get(Regex::class.java, "argumentsRegex") ?: argumentsRegex
)
}
.registerPrimitiveTypeAdapter {
it.toString().toRegex()
}
.build()
}
.registerPrimitiveTypeAdapter {
it.toString().toRegex()
}
.build()
jankson.marshaller.registerSerializer(Regex::class.java) { regex, _ ->
JsonPrimitive(regex.pattern)

View File

@ -11,34 +11,34 @@ import matterlink.logger
import matterlink.stackTraceString
import java.io.File
import java.io.FileNotFoundException
import java.util.*
import java.util.UUID
import java.util.concurrent.TimeUnit
typealias IdentMap = Map<String, Map<String, List<String>>>
data class AuthRequest(
val username: String,
val uuid: String,
val nonce: String,
val platform: String,
val userid: String
val username: String,
val uuid: String,
val nonce: String,
val platform: String,
val userid: String
)
object IdentitiesConfig {
val authRequests: Cache<String, AuthRequest> = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build()
private val jankson = Jankson
.builder()
.build()
.builder()
.build()
private val configFile: File = baseCfg.cfgDirectory.resolve("identities.hjson")
private val default = mapOf(
("edd31c45-b095-49c5-a9f5-59cec4cfed8c" to mapOf(
"discord.game" to (listOf("112228624366575616") to "discord id")
) to "username: NikkyAi")
("edd31c45-b095-49c5-a9f5-59cec4cfed8c" to mapOf(
"discord.game" to (listOf("112228624366575616") to "discord id")
) to "username: NikkyAi")
)
var idents: IdentMap = mapOf()

View File

@ -6,34 +6,33 @@ import blue.endless.jankson.impl.SyntaxError
import com.google.common.cache.Cache
import com.google.common.cache.CacheBuilder
import matterlink.getReified
import matterlink.instance
import matterlink.logger
import java.io.File
import java.io.FileNotFoundException
import java.util.*
import java.util.UUID
import java.util.concurrent.TimeUnit
typealias PermissionMap = Map<String, Double>
data class PermissionRequest(
val uuid: UUID,
val user: String,
val nonce: String,
val powerlevel: Double? = null
val uuid: UUID,
val user: String,
val nonce: String,
val powerlevel: Double? = null
)
object PermissionConfig {
val permissionRequests: Cache<String, PermissionRequest> = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build()
private val jankson = Jankson
.builder()
.build()
.builder()
.build()
private val configFile: File = baseCfg.cfgDirectory.resolve("permissions.hjson")
private val default = mapOf(
"edd31c45-b095-49c5-a9f5-59cec4cfed8c" to 9000.0 to "Superuser"
"edd31c45-b095-49c5-a9f5-59cec4cfed8c" to 9000.0 to "Superuser"
)
val perms: PermissionMap = mutableMapOf()

View File

@ -1,35 +1,38 @@
package matterlink.handlers
import matterlink.api.ApiMessage
import matterlink.bridge.MessageHandlerInst
import matterlink.bridge.command.BridgeCommandRegistry
import matterlink.config.cfg
import matterlink.instance
import matterlink.logger
import matterlink.stripColorOut
import java.util.*
import java.util.UUID
object ChatProcessor {
/**
* @return cancel message flag
*/
fun sendToBridge(user: String, msg: String, event: String, uuid: UUID? = null): Boolean {
suspend fun sendToBridge(
user: String,
msg: String,
x: Int,
y: Int,
z: Int,
dimension: Int?,
event: ChatEvent,
uuid: UUID? = null
): Boolean {
//TODO: pass message to Locations
logger.info("position: $x $y $z dimension: $dimension")
val message = msg.trim()
if(uuid != null && BridgeCommandRegistry.handleCommand(message, user, uuid)) return true
if (uuid != null && BridgeCommandRegistry.handleCommand(message, user, uuid)) return true
when {
message.isNotBlank() -> MessageHandlerInst.transmit(
ApiMessage(
username = user.stripColorOut,
text = message.stripColorOut,
event = event
).apply {
if(cfg.outgoing.avatar.enable) {
if(uuid != null)
avatar = cfg.outgoing.avatar.urlTemplate.replace("{uuid}", uuid.toString())
}
},
cause = "Message from $user"
message.isNotBlank() -> LocationHandler.sendToLocations(
user = user,
msg = message,
x = x, y = y, z = z, dimension = dimension,
event = event,
cause = "Message from $user",
uuid = uuid
)
else -> logger.warn("WARN: dropped blank message by '$user'")
}
return false

View File

@ -1,29 +1,35 @@
package matterlink.handlers
import matterlink.antiping
import matterlink.api.ApiMessage
import matterlink.bridge.MessageHandlerInst
import matterlink.config.cfg
import matterlink.stripColorOut
import java.util.*
import java.util.Random
object DeathHandler {
private val random = Random()
fun handleDeath(
player: String,
deathMessage: String,
damageType: String
suspend fun handleDeath(
player: String,
deathMessage: String,
damageType: String,
x: Int, y: Int, z: Int,
dimension: Int
) {
if (cfg.outgoing.death.enable) {
var msg = deathMessage.stripColorOut.replace(player, player.stripColorOut.antiping)
if (cfg.outgoing.death.damageType) {
val emojis = cfg.outgoing.death.damageTypeMapping[damageType]
?: arrayOf("\uD83D\uDC7B unknown type '$damageType'")
?: arrayOf("\uD83D\uDC7B unknown type '$damageType'")
val damageEmoji = emojis[random.nextInt(emojis.size)]
msg += " $damageEmoji"
}
MessageHandlerInst.transmit(ApiMessage(text = msg), cause = "Death Event of $player")
LocationHandler.sendToLocations(
msg = msg,
x = x, y = y, z = z, dimension = dimension,
event = ChatEvent.DEATH,
cause = "Death Event of $player",
systemuser = true
)
}
}
}

View File

@ -1,46 +1,51 @@
package matterlink.handlers
import matterlink.antiping
import matterlink.api.ApiMessage
import matterlink.api.ApiMessage.Companion.JOIN_LEAVE
import matterlink.bridge.MessageHandlerInst
import matterlink.config.cfg
import matterlink.mapFormat
import matterlink.stripColorOut
object JoinLeaveHandler {
fun handleJoin(player: String) {
suspend fun handleJoin(
player: String,
x: Int, y: Int, z: Int,
dimension: Int
) {
if (cfg.outgoing.joinPart.enable) {
val msg = cfg.outgoing.joinPart.joinServer.mapFormat(
mapOf(
"{username}" to player.stripColorOut,
"{username:antiping}" to player.stripColorOut.antiping
)
mapOf(
"{username}" to player.stripColorOut,
"{username:antiping}" to player.stripColorOut.antiping
)
)
MessageHandlerInst.transmit(
ApiMessage(
text = msg,
event = JOIN_LEAVE
),
cause = "$player joined"
LocationHandler.sendToLocations(
msg = msg,
x = x, y = y, z = z, dimension = dimension,
event = ChatEvent.JOIN,
systemuser = true,
cause = "$player joined"
)
}
}
fun handleLeave(player: String) {
suspend fun handleLeave(
player: String,
x: Int, y: Int, z: Int,
dimension: Int
) {
if (cfg.outgoing.joinPart.enable) {
val msg = cfg.outgoing.joinPart.partServer.mapFormat(
mapOf(
"{username}" to player.stripColorOut,
"{username:antiping}" to player.stripColorOut.antiping
)
mapOf(
"{username}" to player.stripColorOut,
"{username:antiping}" to player.stripColorOut.antiping
)
)
MessageHandlerInst.transmit(
ApiMessage(
text = msg,
event = JOIN_LEAVE
),
cause = "$player left"
LocationHandler.sendToLocations(
msg = msg,
x = x, y = y, z = z, dimension = dimension,
event = ChatEvent.JOIN,
systemuser = true,
cause = "$player left"
)
}
}

View File

@ -0,0 +1,110 @@
package matterlink.handlers
import matterlink.api.ApiMessage
import matterlink.bridge.MessageHandlerInst
import matterlink.config.cfg
import matterlink.logger
import matterlink.stripColorOut
import java.util.UUID
enum class ChatEvent {
PLAIN, ACTION, DEATH, JOIN, LEAVE, ADVANCEMENT, BROADCAST, STATUS
}
object LocationHandler {
suspend fun sendToLocations(
user: String = cfg.outgoing.systemUser,
msg: String,
x: Int, y: Int, z: Int,
dimension: Int?,
event: ChatEvent,
systemuser: Boolean = false,
uuid: UUID? = null,
cause: String
): Boolean {
val defaults = cfg.outgoingDefaults
var handled = false
val skips = mutableSetOf<String>()
logger.debug("locations: ${cfg.locations.map { it.label }}")
for (location in cfg.locations) {
val label = location.label
if (skips.contains(label)) {
logger.debug("skipping $label (contained in in $skips)")
continue
}
if (!location.area.testForDim(dimension)) {
logger.debug("location: $label dropped message '$msg' from $user due to mismatched dimension")
continue
}
if (!location.area.testInBounds(x, y, z)) {
logger.debug("location: $label dropped message '$msg' from $user out of coordinate bounds")
continue
}
val matchesEvent = when (event) {
ChatEvent.PLAIN -> location.outgoing.plain ?: defaults.plain
ChatEvent.ACTION -> location.outgoing.action ?: defaults.action
ChatEvent.DEATH -> location.outgoing.death ?: defaults.death
ChatEvent.JOIN -> location.outgoing.join ?: defaults.join
ChatEvent.LEAVE -> location.outgoing.leave ?: defaults.leave
ChatEvent.ADVANCEMENT -> location.outgoing.advancement ?: defaults.advancement
ChatEvent.BROADCAST -> location.outgoing.broadcast ?: defaults.broadcast
ChatEvent.STATUS -> location.outgoing.status ?: defaults.status
}
if (!matchesEvent) {
logger.debug("location: $label dropped message '$msg' from user: '$user', event not enabled")
logger.debug("event: $event")
logger.debug("location.outgoing: ${location.outgoing}")
logger.debug("defaults: $defaults")
continue
}
skips.addAll(location.outgoing.skip)
val eventStr = when (event) {
ChatEvent.PLAIN -> ""
ChatEvent.ACTION -> ApiMessage.USER_ACTION
ChatEvent.DEATH -> ""
ChatEvent.JOIN -> ApiMessage.JOIN_LEAVE
ChatEvent.LEAVE -> ApiMessage.JOIN_LEAVE
ChatEvent.ADVANCEMENT -> ""
ChatEvent.BROADCAST -> ""
ChatEvent.STATUS -> ""
}
val username = when {
systemuser -> cfg.outgoing.systemUser
else -> user
}
val avatar = when {
systemuser ->
cfg.outgoing.avatar.systemUserAvatar
cfg.outgoing.avatar.enable && uuid != null ->
cfg.outgoing.avatar.urlTemplate.replace("{uuid}", uuid.toString())
else ->
null
}
when {
msg.isNotBlank() -> MessageHandlerInst.transmit(
ApiMessage(
username = username.stripColorOut,
text = msg.stripColorOut,
event = eventStr,
gateway = location.gateway
).apply {
avatar?.let {
this.avatar = it
}
},
cause = cause
)
else -> logger.warn("WARN: dropped blank message by '$user'")
}
logger.debug("sent message through location: $label, cause: $cause")
handled = true
}
return handled
}
}

View File

@ -1,21 +1,24 @@
package matterlink.handlers
import matterlink.antiping
import matterlink.api.ApiMessage
import matterlink.bridge.MessageHandlerInst
import matterlink.config.cfg
import matterlink.stripColorOut
object ProgressHandler {
fun handleProgress(name: String, message: String, display: String) {
suspend fun handleProgress(
name: String, message: String, display: String,
x: Int, y: Int, z: Int,
dimension: Int
) {
if (!cfg.outgoing.advancements) return
val usr = name.stripColorOut.antiping
MessageHandlerInst.transmit(
ApiMessage(
text = "$usr $message $display".stripColorOut
),
cause = "Progress Event by $usr"
LocationHandler.sendToLocations(
msg = "$usr $message $display".stripColorOut,
x = x, y = y, z = z, dimension = dimension,
event = ChatEvent.ADVANCEMENT,
cause = "Progress Event by $usr",
systemuser = true
)
}
}

View File

@ -1,48 +1,132 @@
package matterlink.handlers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import matterlink.api.ApiMessage
import matterlink.bridge.MessageHandlerInst
import matterlink.bridge.command.BridgeCommandRegistry
import matterlink.bridge.format
import matterlink.config.cfg
import matterlink.instance
import matterlink.logger
import matterlink.stripColorIn
import java.util.UUID
object ServerChatHandler {
@UseExperimental(ExperimentalCoroutinesApi::class)
val rcvChannel = MessageHandlerInst.broadcast.openSubscription()
/**
* This method must be called every server tick with no arguments.
*/
fun writeIncomingToChat() {
if (MessageHandlerInst.queue.isNotEmpty())
logger.debug("incoming: " + MessageHandlerInst.queue.toString())
val nextMessage = MessageHandlerInst.queue.poll() ?: null
suspend fun writeIncomingToChat() {
val nextMessage = rcvChannel.poll() ?: return
if (nextMessage?.gateway == cfg.connect.gateway) {
if (!nextMessage.text.isBlank()) {
nextMessage.text = nextMessage.text.stripColorIn
val message = when (nextMessage.event) {
"user_action" -> nextMessage.format(cfg.incoming.action)
"" -> {
// try to handle command and do not handle as a chat message
if (BridgeCommandRegistry.handleCommand(nextMessage)) return
nextMessage.format(cfg.incoming.chat)
}
"join_leave" -> nextMessage.format(cfg.incoming.joinPart)
else -> {
val user = nextMessage.username
val text = nextMessage.text
val json = nextMessage.encode()
logger.debug("Threw out message with unhandled event: ${nextMessage.event}")
logger.debug(" Message contents:")
logger.debug(" User: $user")
logger.debug(" Text: $text")
logger.debug(" JSON: $json")
return
}
}
instance.wrappedSendToPlayers(message)
val defaults = cfg.incomingDefaults
val sourceGateway = nextMessage.gateway
// find all areas to send to
val targetUUIDs = mutableSetOf<UUID>()
val skips = mutableSetOf<String>()
val locations = cfg.locations.filter {
it.gateway == sourceGateway
}
if (nextMessage.event.isEmpty()) {
// filter command handlers
val commandLocations = locations.filter {
it.incoming.commands ?: cfg.incomingDefaults.commands
}
// process potential command
for ((label, location) in commandLocations) {
if (BridgeCommandRegistry.handleCommand(nextMessage)) return
}
}
for (location in locations) {
val label = location.label
if (skips.contains(label)) {
logger.debug("skipping $label")
continue
}
val matchesEvent = when (nextMessage.event) {
"" -> {
// if (location.incoming.commands ?: defaults.commands
// && BridgeCommandRegistry.handleCommand(nextMessage)) return
location.incoming.plain ?: defaults.plain
}
ApiMessage.JOIN_LEAVE -> location.incoming.join_leave ?: defaults.join_leave
ApiMessage.USER_ACTION -> location.incoming.action ?: defaults.action
else -> {
logger.error("unknown event type '${nextMessage.event}' on incoming message")
return
}
}
if (!matchesEvent) {
logger.info("location: $label dropped message '$nextMessage' event not enabled")
continue
}
targetUUIDs.addAll(instance.collectPlayers(location.area))
}
val message = when (nextMessage.event) {
"user_action" -> nextMessage.format(cfg.incoming.action)
"" -> {
// try to handle command and do not handle as a chat message
if (BridgeCommandRegistry.handleCommand(nextMessage)) return
nextMessage.format(cfg.incoming.chat)
}
"join_leave" -> nextMessage.format(cfg.incoming.joinPart)
else -> {
val user = nextMessage.username
val text = nextMessage.text
val json = nextMessage.encode()
logger.debug("Threw out message with unhandled event: ${nextMessage.event}")
logger.debug(" Message contents:")
logger.debug(" User: $user")
logger.debug(" Text: $text")
logger.debug(" JSON: $json")
return
}
}
targetUUIDs.forEach {
//TODO: optimize send to all at once
instance.wrappedSendToPlayer(it, message)
}
// if (nextMessage?.gateway == cfg.connect.gateway) {
// if (!nextMessage.text.isBlank()) {
// nextMessage.text = nextMessage.text.stripColorIn
// val message = when (nextMessage.event) {
// "user_action" -> nextMessage.format(cfg.incoming.action)
// "" -> {
// // try to handle command and do not handle as a chat message
// if (BridgeCommandRegistry.handleCommand(nextMessage)) return
// nextMessage.format(cfg.incoming.chat)
// }
// "join_leave" -> nextMessage.format(cfg.incoming.joinPart)
// else -> {
// val user = nextMessage.username
// val text = nextMessage.text
// val json = nextMessage.encode()
// logger.debug("Threw out message with unhandled event: ${nextMessage.event}")
// logger.debug(" Message contents:")
// logger.debug(" User: $user")
// logger.debug(" Text: $text")
// logger.debug(" JSON: $json")
// return
// }
// }
// instance.wrappedSendToPlayers(message)
// }
// }
}
}

View File

@ -1,6 +1,5 @@
package matterlink.handlers
import matterlink.bridge.MessageHandlerInst
import matterlink.update.UpdateChecker
/**
@ -13,17 +12,17 @@ object TickHandler {
private set
private var accumulator = 0
private const val updateInterval = 12 * 60 * 60 * 20
fun handleTick() {
suspend fun handleTick() {
tickCounter++
if (tickCounter % 100 == 0) {
MessageHandlerInst.checkConnection()
}
// if (tickCounter % 100 == 0) {
// MessageHandlerInst.checkConnection()
// }
ServerChatHandler.writeIncomingToChat()
if (accumulator++ > updateInterval) {
accumulator -= updateInterval
UpdateChecker.run()
UpdateChecker.check()
}
}
}

View File

@ -1,15 +1,15 @@
package matterlink.jenkins
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
/**
* Created by nikky on 03/02/18.
* @author Nikky
*/
//@JsonIgnoreProperties(ignoreUnknown = true)
@Serializable
data class Artifact(
val displayPath: String,
val fileName: String,
val relativePath: String
val displayPath: String,
val fileName: String,
val relativePath: String
)

View File

@ -1,11 +1,11 @@
package matterlink.jenkins
import com.google.gson.Gson
import matterlink.*
import matterlink.Result
import matterlink.header
import matterlink.httpGet
import matterlink.responseString
import com.github.kittinunf.fuel.httpGet
import com.github.kittinunf.fuel.serialization.kotlinxDeserializerOf
import com.github.kittinunf.result.Result
import kotlinx.serialization.json.JSON
import matterlink.logger
/**
* Created by nikky on 03/02/18.
@ -14,17 +14,17 @@ import matterlink.responseString
//@JsonIgnoreProperties(ignoreUnknown = true)
data class Build(
val number: Int,
val url: String
val number: Int,
val url: String
) {
fun details(userAgent: String): BuildWithDetails? {
val (request, response, result) = "$url/api/json"
.httpGet()
.header("User-Agent" to userAgent)
.responseString()
return when(result) {
.httpGet()
.header("User-Agent" to userAgent)
.responseObject(kotlinxDeserializerOf(loader = BuildWithDetails.serializer(), json = JSON.nonstrict))
return when (result) {
is Result.Success -> {
gson.fromJson(result.value, BuildWithDetails::class.java)
result.value
}
is Result.Failure -> {
logger.error(result.error.toString())
@ -32,9 +32,5 @@ data class Build(
}
}
}
companion object {
val gson = Gson()
}
}

View File

@ -1,12 +1,12 @@
package matterlink.jenkins
import java.util.*
import kotlinx.serialization.Serializable
import java.util.Date
//@JsonIgnoreProperties(ignoreUnknown = true)
@Serializable
data class BuildWithDetails(
val number: Int,
val url: String,
val artifacts: List<Artifact>,
// @JsonFormat(shape=JsonFormat.Shape.NUMBER, pattern="s")
val timestamp: Date
val number: Int,
val url: String,
val artifacts: List<Artifact>,
val timestamp: Date
)

View File

@ -1,12 +1,10 @@
package voodoo.util.jenkins
package matterlink.jenkins
import com.google.gson.Gson
import matterlink.Result
import matterlink.header
import matterlink.httpGet
import matterlink.responseString
import matterlink.jenkins.Job
import com.github.kittinunf.fuel.httpGet
import com.github.kittinunf.fuel.serialization.kotlinxDeserializerOf
import com.github.kittinunf.result.Result
import kotlinx.serialization.json.JSON
import matterlink.logger
/**
@ -22,12 +20,12 @@ class JenkinsServer(val url: String) {
fun getJob(job: String, userAgent: String): Job? {
val requestURL = getUrl(job) + "/api/json"
val (_, _, result) = requestURL
.httpGet()
.header("User-Agent" to userAgent)
.responseString()
.httpGet()
.header("User-Agent" to userAgent)
.responseObject(kotlinxDeserializerOf(loader = Job.serializer(), json = JSON.nonstrict))
return when (result) {
is Result.Success -> {
gson.fromJson(result.value, Job::class.java)
result.value
}
is Result.Failure -> {
logger.error(result.error.toString())
@ -36,8 +34,4 @@ class JenkinsServer(val url: String) {
}
}
companion object {
val gson = Gson()
}
}

View File

@ -1,20 +1,22 @@
package matterlink.jenkins;
package matterlink.jenkins
import kotlinx.serialization.Serializable
/**
* Created by nikky on 03/02/18.
* @author Nikky
*/
//@JsonIgnoreProperties(ignoreUnknown = true)
@Serializable
data class Job(
val url: String,
val name: String,
val fullName: String,
val displayName: String,
val fullDisplayName: String,
val builds: List<Build>?,
val lastSuccessfulBuild: Build?,
val lastStableBuild: Build?
val url: String,
val name: String,
val fullName: String,
val displayName: String,
val fullDisplayName: String,
val builds: List<Build>?,
val lastSuccessfulBuild: Build?,
val lastStableBuild: Build?
) {
fun getBuildByNumber(build: Int, userAgent: String): BuildWithDetails? {
return builds?.find { it.number == build }?.details(userAgent)

View File

@ -1,9 +1,13 @@
package matterlink.update
import kotlinx.serialization.Serializable
@Serializable
data class CurseFile(
val downloadURL: String,
val fileName: String,
val gameVersion: List<String>,
val releaseType: String,
val fileStatus: String
val downloadUrl: String,
val fileName: String,
val fileNameOnDisk: String,
val gameVersion: List<String>,
val releaseType: String,
val fileStatus: String
)

View File

@ -1,38 +1,40 @@
package matterlink.update
import com.google.gson.FieldNamingPolicy
import com.google.gson.GsonBuilder
import com.github.kittinunf.fuel.core.extensions.cUrlString
import com.github.kittinunf.fuel.httpGet
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.serialization.list
import matterlink.api.ApiMessage
import matterlink.bridge.MessageHandlerInst
import matterlink.config.cfg
import matterlink.handlers.ChatEvent
import matterlink.handlers.LocationHandler
import matterlink.instance
import matterlink.jenkins.JenkinsServer
import matterlink.logger
import voodoo.util.jenkins.JenkinsServer
import java.io.BufferedReader
import java.net.HttpURLConnection
import java.net.URL
import com.github.kittinunf.fuel.serialization.kotlinxDeserializerOf
import com.github.kittinunf.result.Result
import kotlinx.serialization.json.JSON
class UpdateChecker : Thread() {
companion object {
fun run() {
if (cfg.update.enable) {
UpdateChecker().start()
}
object UpdateChecker : CoroutineScope {
override val coroutineContext = Job() + CoroutineName("UpdateChacker")
suspend fun check() {
if (cfg.update.enable) {
run()
}
}
init {
name = "UpdateCheckerThread"
}
override fun run() {
private suspend fun run() {
if (instance.buildNumber > 0) {
val server = JenkinsServer("https://ci.elytradev.com")
val job = server.getJob("elytra/MatterLink/master", "MatterLink/${instance.modVersion}")
?: run {
logger.error("failed obtaining job: elytra/MatterLink/master")
return
}
?: run {
logger.error("failed obtaining job: elytra/MatterLink/master")
return
}
//TODO: add job name to constants at build time
val build = job.lastSuccessfulBuild ?: run {
logger.error("no successful build found")
@ -42,11 +44,12 @@ class UpdateChecker : Thread() {
when {
number > instance.buildNumber -> {
logger.warn("Mod out of date! New build $number available at $url")
val difference = number - build.number
MessageHandlerInst.transmit(
ApiMessage(
text = "MatterLink out of date! You are $difference builds behind! Please download new version from $url"
)
val difference = number - instance.buildNumber
LocationHandler.sendToLocations(
msg = "MatterLink out of date! You are $difference builds behind! Please download new version from $url",
x = 0, y = 0, z = 0, dimension = null,
event = ChatEvent.STATUS,
cause = "MatterLink update notice"
)
}
number < instance.buildNumber -> logger.error("lastSuccessfulBuild: $number is older than installed build: ${instance.buildNumber}")
@ -60,48 +63,38 @@ class UpdateChecker : Thread() {
return
}
val gson = GsonBuilder()
// .setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE)
.create()
logger.info("Checking for new versions...")
val url = URL("https://staging_cursemeta.dries007.net/api/v3/direct/addon/287323/files")
val con = url.openConnection() as HttpURLConnection
with(instance) {
val useragent = "MatterLink/$modVersion MinecraftForge/$mcVersion-$forgeVersion (https://github.com/elytra/MatterLink)"
val (request, response, result) = with(instance) {
val useragent =
"MatterLink/$modVersion MinecraftForge/$mcVersion-$forgeVersion (https://github.com/elytra/MatterLink)"
logger.debug("setting User-Agent: '$useragent'")
con.setRequestProperty("User-Agent", useragent)
"https://curse.nikky.moe/api/addon/287323/files".httpGet()
.header("User-Agent" to useragent)
.responseObject(kotlinxDeserializerOf(loader = CurseFile.serializer().list, json = JSON.nonstrict))
}
con.connect()
val apiUpdateList = if (200 == con.responseCode) { //HTTP 200 OK
val buffer: BufferedReader = con.inputStream.bufferedReader()
//put all of the buffer content onto the string
val content = buffer.readText()
logger.trace("updateData: $content")
gson.fromJson(content, Array<CurseFile>::class.java)
.filter {
it.fileStatus == "SemiNormal" && it.gameVersion.contains(instance.mcVersion)
}
.sortedByDescending { it.fileName.substringAfterLast(" ") }
} else {
logger.error("Could not check for updates!")
return
val apiUpdateList = when(result) {
is Result.Success -> {
result.value
}
is Result.Failure -> {
logger.error("Could not check for updates!")
logger.error("cUrl: ${request.cUrlString()}")
logger.error("request: $request")
logger.error("response: $response")
return
}
}
.filter { it.fileStatus == "SemiNormal" && it.gameVersion.contains(instance.mcVersion) }
val modVersionChunks = instance.modVersion
.substringBefore("-dev")
.substringBefore("-build")
.split('.')
.map {
it.toInt()
}
.substringBefore("-dev")
.substringBefore("-build")
.split('.')
.map {
it.toInt()
}
val possibleUpdates = mutableListOf<CurseFile>()
apiUpdateList.forEach {
@ -131,14 +124,14 @@ class UpdateChecker : Thread() {
logger.info("Matterlink out of date! You are $count $version behind")
possibleUpdates.forEach {
logger.info("version: {} download: {}", it.fileName, it.downloadURL)
logger.info("version: ${it.fileName} download: ${it.downloadUrl}")
}
logger.warn("Mod out of date! New $version available at ${latest.downloadURL}")
logger.warn("Mod out of date! New $version available at ${latest.downloadUrl}")
MessageHandlerInst.transmit(
ApiMessage(
text = "MatterLink out of date! You are $count $version behind! Please download new version from ${latest.downloadURL}"
)
ApiMessage(
text = "MatterLink out of date! You are $count $version behind! Please download new version from ${latest.downloadUrl}"
)
)
}
}

View File

@ -1,8 +1,12 @@
mod_name = MatterLink
mod_version = 1.6.3
forgelin_version = 1.6.0
kotlin_version = 1.2.41
shadow_version = 2.0.2
cursegradle_version = 1.0.10
curse_id = 287323
curse_release_type = beta
modName = MatterLink
modVersion = 1.6.4
forgelinVersion = 1.8.2
kotlinVersion = 1.3.10
coroutinesVersion = 1.0.1
serializationVersion = 0.9.1
shadowVersion = 2.0.2
fuelVersion = 8690665998
resultVersion = 2.0.0
cursegradleVersion = 1.1.2
curseId = 287323
curseReleaseType = beta

23
scripts/start.sh Executable file
View File

@ -0,0 +1,23 @@
#!/usr/bin/env bash
java -jar forge-installer.jar --installServer
FORGE_FILE=`grep "The server installed successfully, you should now be able to run the file " forge-installer.jar.log | tail -1`
FORGE_FILE=${FORGE_FILE#"The server installed successfully, you should now be able to run the file "}
echo $FORGE_FILE
cp -f "$FORGE_FILE" forge.jar
if [ ! $? -eq 0 ]; then
echo "Error installing forge"
exit 1
fi
echo "installed forge"
cp ../eula.txt .
mkdir -p config
rm -rf config/matterlink
cp -r ../matterlink_config config/matterlink
java -jar forge.jar

22
scripts/test12.sh Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
RUN="$PWD/run/1.12.2"
rm -rf "$RUN/mods"
mkdir -p "$RUN/mods"
"$PWD/gradlew" :1.12.2:clean :1.12.2:build && cp -f 1.12.2/build/libs/MatterLink-1.12.2-*-dev.jar "$RUN/mods"
if [ ! $? -eq 0 ]; then
echo "Error compiling matterlink"
exit 1
fi
cd "$RUN"
curl -o forge-installer.jar "https://files.minecraftforge.net/maven/net/minecraftforge/forge/1.12.2-14.23.5.2768/forge-1.12.2-14.23.5.2768-installer.jar"
curl -L -o "$RUN/mods/Forgelin.jar" "https://minecraft.curseforge.com/projects/shadowfacts-forgelin/files/2640952/download"
$DIR/scripts/start.sh

19
scripts/test7.sh Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
RUN="$PWD/run/1.7.10"
rm -rf "$RUN/mods"
mkdir -p "$RUN/mods"
"$PWD/gradlew" :1.7.10:clean :1.7.10:build && cp -f $DIR/1.7.10/build/libs/MatterLink-1.7.10-*-dev.jar "$RUN/mods"
if [ ! $? -eq 0 ]; then
echo "Error compiling matterlink"
exit 1
fi
cd "$RUN"
curl -o forge-installer.jar "https://files.minecraftforge.net/maven/net/minecraftforge/forge/1.7.10-10.13.4.1614-1.7.10/forge-1.7.10-10.13.4.1614-1.7.10-installer.jar"
$DIR/scripts/start.sh

22
scripts/test9.sh Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
RUN="$PWD/run/1.9.4"
rm -rf "$RUN/mods"
mkdir -p "$RUN/mods"
"$PWD/gradlew" :1.9.4:clean :1.9.4:build && cp -f 1.9.4/build/libs/MatterLink-1.9.4-*-dev.jar "$RUN/mods"
if [ ! $? -eq 0 ]; then
echo "Error compiling matterlink"
exit 1
fi
cd "$RUN"
curl -o forge-installer.jar "https://files.minecraftforge.net/maven/net/minecraftforge/forge/1.9.4-12.17.0.2051/forge-1.9.4-12.17.0.2051-installer.jar"
curl -L -o "$RUN/mods/Forgelin.jar" "https://minecraft.curseforge.com/projects/shadowfacts-forgelin/files/2640952/download" # "https://minecraft.curseforge.com/projects/shadowfacts-forgelin/files/2573311/download"
$DIR/scripts/start.sh

View File

@ -1,4 +1,4 @@
rootProject.name = 'MatterLink'
include 'core', 'api'
include 'core'
include 'Jankson'
include '1.12.2', '1.11.2', '1.10.2', '1.7.10'
include '1.12.2', '1.9.4', '1.7.10'