mirror of
https://github.com/yairm210/Unciv.git
synced 2024-12-22 20:44:25 +07:00
Unciv server (#6384)
* Background implementation for Unciv server with ktor. Server ip in settings, able to copy your own ip and copy ip from clipboard for easy sharing, created stub for the client-server data transfer. No actual data storage or server implementation. * Unciv server round 2 - implementing crud for files and it works! metadata seems to only be in use for mutex, which is currently unused That's all for today * When starting a new multiplayer game the files are correctly saved in the server, and the server can return the files, but the function in the game to retrieve the game info is non-blocking so it doesn't work. Still, progress! * Changed the Gdx http to basic Java http, as used for Dropbox, and now everything works!!!! * Documentation for running and using the server * Better texts, translations, etc * Trog is right this should be a PUT not POST
This commit is contained in:
parent
77839b4b9d
commit
2b1251258c
@ -1340,6 +1340,16 @@ Doing this will reset your current user ID to the clipboard contents - are you s
|
||||
ID successfully set! =
|
||||
Invalid ID! =
|
||||
|
||||
# Multiplayer options menu
|
||||
|
||||
Current IP address =
|
||||
Server's IP address =
|
||||
Reset to Dropbox =
|
||||
Check connection to server =
|
||||
Awaiting response... =
|
||||
Success! =
|
||||
Failed! =
|
||||
|
||||
|
||||
# Mods
|
||||
|
||||
|
@ -70,6 +70,12 @@ project(":desktop") {
|
||||
}
|
||||
|
||||
"implementation"("com.github.MinnDevelopment:java-discord-rpc:v2.0.1")
|
||||
|
||||
// For server-side
|
||||
|
||||
"implementation"("io.ktor:ktor-server-core:1.6.8")
|
||||
"implementation"("io.ktor:ktor-server-netty:1.6.8")
|
||||
"implementation"("ch.qos.logback:logback-classic:1.2.5")
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -68,6 +68,8 @@ object Constants {
|
||||
const val remove = "Remove "
|
||||
|
||||
const val uniqueOrDelimiter = "\" OR \""
|
||||
|
||||
const val dropboxMultiplayerServer = "Dropbox"
|
||||
|
||||
/**
|
||||
* Use this to determine whether a [MapUnit][com.unciv.logic.map.MapUnit]'s movement is exhausted
|
||||
|
@ -138,7 +138,6 @@ object DropBox {
|
||||
var name = ""
|
||||
private var server_modified = ""
|
||||
|
||||
override fun getFileName() = name
|
||||
override fun getLastModified(): Date {
|
||||
return server_modified.parseDate()
|
||||
}
|
||||
|
@ -1,9 +1,14 @@
|
||||
package com.unciv.logic.multiplayer
|
||||
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.badlogic.gdx.Net
|
||||
import com.unciv.Constants
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.GameInfo
|
||||
import com.unciv.logic.GameInfoPreview
|
||||
import com.unciv.logic.GameSaver
|
||||
import com.unciv.ui.saves.Gzip
|
||||
import com.unciv.ui.worldscreen.mainmenu.OptionsPopup
|
||||
import java.util.*
|
||||
|
||||
interface IFileStorage {
|
||||
@ -14,14 +19,59 @@ interface IFileStorage {
|
||||
}
|
||||
|
||||
interface IFileMetaData {
|
||||
fun getFileName(): String?
|
||||
fun getLastModified(): Date?
|
||||
}
|
||||
|
||||
|
||||
|
||||
class UncivServerFileStorage(val serverIp:String):IFileStorage {
|
||||
val serverUrl = "http://$serverIp:8080"
|
||||
override fun saveFileData(fileName: String, data: String) {
|
||||
OptionsPopup.SimpleHttp.sendRequest(Net.HttpMethods.PUT, "$serverUrl/files/$fileName", data){
|
||||
success: Boolean, result: String ->
|
||||
if (!success) {
|
||||
println(result)
|
||||
throw java.lang.Exception(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadFileData(fileName: String): String {
|
||||
var fileData = ""
|
||||
OptionsPopup.SimpleHttp.sendGetRequest("$serverUrl/files/$fileName"){
|
||||
success: Boolean, result: String ->
|
||||
if (!success) {
|
||||
println(result)
|
||||
throw java.lang.Exception(result)
|
||||
}
|
||||
else fileData = result
|
||||
}
|
||||
return fileData
|
||||
}
|
||||
|
||||
override fun getFileMetaData(fileName: String): IFileMetaData {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun deleteFile(fileName: String) {
|
||||
OptionsPopup.SimpleHttp.sendRequest(Net.HttpMethods.DELETE, "$serverUrl/files/$fileName", ""){
|
||||
success: Boolean, result: String ->
|
||||
if (!success) throw java.lang.Exception(result)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class FileStorageConflictException: Exception()
|
||||
|
||||
class OnlineMultiplayer {
|
||||
val fileStorage: IFileStorage = DropboxFileStorage()
|
||||
val fileStorage: IFileStorage
|
||||
init {
|
||||
val settings = UncivGame.Current.settings
|
||||
if (settings.multiplayerServer == Constants.dropboxMultiplayerServer)
|
||||
fileStorage = DropboxFileStorage()
|
||||
else fileStorage = UncivServerFileStorage(settings.multiplayerServer)
|
||||
}
|
||||
|
||||
fun tryUploadGame(gameInfo: GameInfo, withPreview: Boolean) {
|
||||
// We upload the gamePreview before we upload the game as this
|
||||
|
@ -2,6 +2,7 @@ package com.unciv.models.metadata
|
||||
|
||||
import com.badlogic.gdx.Application
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.unciv.Constants
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.GameSaver
|
||||
import java.text.Collator
|
||||
@ -50,6 +51,10 @@ class GameSettings {
|
||||
var windowState = WindowState()
|
||||
var isFreshlyCreated = false
|
||||
var visualMods = HashSet<String>()
|
||||
|
||||
|
||||
var multiplayerServer = Constants.dropboxMultiplayerServer
|
||||
|
||||
|
||||
var showExperimentalWorldWrap = false // We're keeping this as a config due to ANR problems on Android phones for people who don't know what they're doing :/
|
||||
|
||||
|
@ -214,7 +214,8 @@ class NewGameScreen(
|
||||
} catch (ex: Exception) {
|
||||
postCrashHandlingRunnable {
|
||||
Popup(this).apply {
|
||||
addGoodSizedLabel("Could not upload game!")
|
||||
addGoodSizedLabel("Could not upload game!").row()
|
||||
Gdx.input.inputProcessor = stage
|
||||
addCloseButton()
|
||||
open()
|
||||
}
|
||||
|
@ -263,9 +263,10 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
|
||||
if (!mapHolder.setCenterPosition(capital.location))
|
||||
game.setScreen(CityScreen(capital))
|
||||
}
|
||||
keyPressDispatcher[KeyCharAndCode.ctrl('O')] = { this.openOptionsPopup() } // Game Options
|
||||
keyPressDispatcher[KeyCharAndCode.ctrl('S')] = { game.setScreen(SaveGameScreen(gameInfo)) } // Save
|
||||
keyPressDispatcher[KeyCharAndCode.ctrl('L')] = { game.setScreen(LoadGameScreen(this)) } // Load
|
||||
// These cause crashes so disabling for now
|
||||
// keyPressDispatcher[KeyCharAndCode.ctrl('O')] = { this.openOptionsPopup() } // Game Options
|
||||
// keyPressDispatcher[KeyCharAndCode.ctrl('S')] = { game.setScreen(SaveGameScreen(gameInfo)) } // Save
|
||||
// keyPressDispatcher[KeyCharAndCode.ctrl('L')] = { game.setScreen(LoadGameScreen(this)) } // Load
|
||||
keyPressDispatcher[Input.Keys.NUMPAD_ADD] = { this.mapHolder.zoomIn() } // '+' Zoom
|
||||
keyPressDispatcher[Input.Keys.NUMPAD_SUBTRACT] = { this.mapHolder.zoomOut() } // '-' Zoom
|
||||
keyPressDispatcher.setCheckpoint()
|
||||
|
@ -3,15 +3,20 @@ package com.unciv.ui.worldscreen.mainmenu
|
||||
import com.badlogic.gdx.Application
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.badlogic.gdx.Input
|
||||
import com.badlogic.gdx.Net
|
||||
import com.badlogic.gdx.Net.HttpResponseListener
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Label
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.SelectBox
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextField
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.Constants
|
||||
import com.unciv.MainMenuScreen
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.MapSaver
|
||||
import com.unciv.logic.civilization.PlayerType
|
||||
import com.unciv.logic.multiplayer.FileStorageConflictException
|
||||
import com.unciv.models.UncivSound
|
||||
import com.unciv.models.ruleset.Ruleset
|
||||
import com.unciv.models.ruleset.Ruleset.RulesetError
|
||||
@ -31,6 +36,15 @@ import com.unciv.ui.utils.*
|
||||
import com.unciv.ui.utils.LanguageTable.Companion.addLanguageTables
|
||||
import com.unciv.ui.utils.UncivTooltip.Companion.addTooltip
|
||||
import com.unciv.ui.worldscreen.WorldScreen
|
||||
import java.io.BufferedReader
|
||||
import java.io.DataOutputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStreamReader
|
||||
import java.net.DatagramSocket
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.InetAddress
|
||||
import java.net.URL
|
||||
import java.nio.charset.Charset
|
||||
import java.util.*
|
||||
import kotlin.math.floor
|
||||
import com.badlogic.gdx.utils.Array as GdxArray
|
||||
@ -77,9 +91,7 @@ class OptionsPopup(val previousScreen: BaseScreen) : Popup(previousScreen) {
|
||||
tabs.addPage("Gameplay", getGamePlayTab(), ImageGetter.getImage("OtherIcons/Options"), 24f)
|
||||
tabs.addPage("Language", getLanguageTab(), ImageGetter.getImage("FlagIcons/${settings.language}"), 24f)
|
||||
tabs.addPage("Sound", getSoundTab(), ImageGetter.getImage("OtherIcons/Speaker"), 24f)
|
||||
// at the moment the notification service only exists on Android
|
||||
if (Gdx.app.type == Application.ApplicationType.Android)
|
||||
tabs.addPage("Multiplayer", getMultiplayerTab(), ImageGetter.getImage("OtherIcons/Multiplayer"), 24f)
|
||||
tabs.addPage("Multiplayer", getMultiplayerTab(), ImageGetter.getImage("OtherIcons/Multiplayer"), 24f)
|
||||
tabs.addPage("Advanced", getAdvancedTab(), ImageGetter.getImage("OtherIcons/Settings"), 24f)
|
||||
if (RulesetCache.size > 1) {
|
||||
tabs.addPage("Locate mod errors", getModCheckTab(), ImageGetter.getImage("OtherIcons/Mods"), 24f) { _, _ ->
|
||||
@ -228,19 +240,113 @@ class OptionsPopup(val previousScreen: BaseScreen) : Popup(previousScreen) {
|
||||
private fun getMultiplayerTab(): Table = Table(BaseScreen.skin).apply {
|
||||
pad(10f)
|
||||
defaults().pad(5f)
|
||||
|
||||
// at the moment the notification service only exists on Android
|
||||
if (Gdx.app.type == Application.ApplicationType.Android) {
|
||||
addCheckbox("Enable out-of-game turn notifications",
|
||||
settings.multiplayerTurnCheckerEnabled) {
|
||||
settings.multiplayerTurnCheckerEnabled = it
|
||||
settings.save()
|
||||
tabs.replacePage("Multiplayer", getMultiplayerTab())
|
||||
}
|
||||
|
||||
addCheckbox("Enable out-of-game turn notifications", settings.multiplayerTurnCheckerEnabled) {
|
||||
settings.multiplayerTurnCheckerEnabled = it
|
||||
settings.save()
|
||||
tabs.replacePage("Multiplayer", getMultiplayerTab())
|
||||
}
|
||||
if (settings.multiplayerTurnCheckerEnabled) {
|
||||
addMultiplayerTurnCheckerDelayBox()
|
||||
|
||||
if (settings.multiplayerTurnCheckerEnabled) {
|
||||
addMultiplayerTurnCheckerDelayBox()
|
||||
|
||||
addCheckbox("Show persistent notification for turn notifier service", settings.multiplayerTurnCheckerPersistentNotificationEnabled)
|
||||
addCheckbox("Show persistent notification for turn notifier service",
|
||||
settings.multiplayerTurnCheckerPersistentNotificationEnabled)
|
||||
{ settings.multiplayerTurnCheckerPersistentNotificationEnabled = it }
|
||||
}
|
||||
}
|
||||
|
||||
val connectionToServerButton = "Check connection to server".toTextButton()
|
||||
|
||||
val ipAddress = getIpAddress()
|
||||
add("{Current IP address}: $ipAddress".toTextButton().onClick {
|
||||
Gdx.app.clipboard.contents = ipAddress.toString()
|
||||
}).row()
|
||||
|
||||
val multiplayerServerTextField = TextField(settings.multiplayerServer, BaseScreen.skin)
|
||||
multiplayerServerTextField.programmaticChangeEvents = true
|
||||
val serverIpTable = Table()
|
||||
|
||||
serverIpTable.add("Server's IP address".toLabel().onClick {
|
||||
multiplayerServerTextField.text = Gdx.app.clipboard.contents
|
||||
}).padRight(10f)
|
||||
multiplayerServerTextField.onChange {
|
||||
settings.multiplayerServer = multiplayerServerTextField.text
|
||||
settings.save()
|
||||
connectionToServerButton.isEnabled = multiplayerServerTextField.text != Constants.dropboxMultiplayerServer
|
||||
}
|
||||
serverIpTable.add(multiplayerServerTextField)
|
||||
add(serverIpTable).row()
|
||||
|
||||
add("Reset to Dropbox".toTextButton().onClick {
|
||||
multiplayerServerTextField.text = Constants.dropboxMultiplayerServer
|
||||
}).row()
|
||||
|
||||
add(connectionToServerButton.onClick {
|
||||
val popup = Popup(screen).apply {
|
||||
addGoodSizedLabel("Awaiting response...").row()
|
||||
}
|
||||
popup.open(true)
|
||||
|
||||
successfullyConnectedToServer { success: Boolean, result: String ->
|
||||
if (success) {
|
||||
popup.addGoodSizedLabel("Success!").row()
|
||||
popup.addCloseButton()
|
||||
} else {
|
||||
popup.addGoodSizedLabel("Failed!").row()
|
||||
popup.addCloseButton()
|
||||
}
|
||||
}
|
||||
}).row()
|
||||
}
|
||||
|
||||
fun getIpAddress(): String? {
|
||||
DatagramSocket().use { socket ->
|
||||
socket.connect(InetAddress.getByName("8.8.8.8"), 10002)
|
||||
return socket.getLocalAddress().getHostAddress()
|
||||
}
|
||||
}
|
||||
|
||||
object SimpleHttp{
|
||||
fun sendGetRequest(url:String, action: (success: Boolean, result:String)->Unit){
|
||||
sendRequest(Net.HttpMethods.GET, url, "", action)
|
||||
}
|
||||
|
||||
fun sendRequest(method:String, url:String, content:String, action: (success:Boolean, result:String)->Unit){
|
||||
with(URL(url).openConnection() as HttpURLConnection) {
|
||||
requestMethod = method // default is GET
|
||||
|
||||
doOutput = true
|
||||
|
||||
try {
|
||||
if (content != "") {
|
||||
// StandardCharsets.UTF_8 requires API 19
|
||||
val postData: ByteArray = content.toByteArray(Charset.forName("UTF-8"))
|
||||
val outputStream = DataOutputStream(outputStream)
|
||||
outputStream.write(postData)
|
||||
outputStream.flush()
|
||||
}
|
||||
|
||||
val text = BufferedReader(InputStreamReader(inputStream)).readText()
|
||||
action(true, text)
|
||||
} catch (t: Throwable) {
|
||||
println(t.message)
|
||||
val errorMessageToReturn =
|
||||
if (errorStream != null) BufferedReader(InputStreamReader(errorStream)).readText()
|
||||
else t.message!!
|
||||
println(errorMessageToReturn)
|
||||
action(false, errorMessageToReturn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun successfullyConnectedToServer(action: (Boolean, String)->Unit){
|
||||
SimpleHttp.sendGetRequest( "http://"+ settings.multiplayerServer+":8080/isalive", action)
|
||||
}
|
||||
|
||||
private fun getAdvancedTab() = Table(BaseScreen.skin).apply {
|
||||
@ -442,7 +548,7 @@ class OptionsPopup(val previousScreen: BaseScreen) : Popup(previousScreen) {
|
||||
return deprecatedUniquesToReplacementText
|
||||
}
|
||||
|
||||
private fun autoUpdateUniques(mod: Ruleset, replaceableUniques: HashMap<String, String>, ) {
|
||||
private fun autoUpdateUniques(mod: Ruleset, replaceableUniques: HashMap<String, String>) {
|
||||
|
||||
if (mod.name.contains("mod"))
|
||||
println("mod")
|
||||
|
@ -13,7 +13,6 @@ import com.unciv.UncivGame
|
||||
import com.unciv.UncivGameParameters
|
||||
import com.unciv.logic.GameSaver
|
||||
import com.unciv.models.metadata.GameSettings
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.utils.Fonts
|
||||
import java.util.*
|
||||
import kotlin.concurrent.timer
|
||||
@ -58,7 +57,6 @@ internal object DesktopLauncher {
|
||||
val game = UncivGame(desktopParameters)
|
||||
|
||||
tryActivateDiscord(game)
|
||||
|
||||
Lwjgl3Application(game, config)
|
||||
}
|
||||
|
||||
|
53
desktop/src/com/unciv/app/desktop/UncivServer.kt
Normal file
53
desktop/src/com/unciv/app/desktop/UncivServer.kt
Normal file
@ -0,0 +1,53 @@
|
||||
package com.unciv.app.desktop
|
||||
|
||||
import io.ktor.application.*
|
||||
import io.ktor.response.*
|
||||
import io.ktor.routing.*
|
||||
import io.ktor.server.engine.*
|
||||
import io.ktor.server.netty.*
|
||||
import io.ktor.utils.io.jvm.javaio.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
|
||||
internal object UncivServer {
|
||||
@JvmStatic
|
||||
fun main(arg: Array<String>) {
|
||||
val fileFolderName = "MultiplayerFiles"
|
||||
File(fileFolderName).mkdir()
|
||||
println(File(fileFolderName).absolutePath)
|
||||
embeddedServer(Netty, port = 8080) {
|
||||
routing {
|
||||
get("/isalive") {
|
||||
call.respondText("true")
|
||||
}
|
||||
put("/files/{fileName}") {
|
||||
val fileName = call.parameters["fileName"] ?: throw Exception("No fileName!")
|
||||
withContext(Dispatchers.IO) {
|
||||
val recievedBytes =
|
||||
call.request.receiveChannel().toInputStream().readAllBytes()
|
||||
val textString = String(recievedBytes)
|
||||
println("Recieved text: $textString")
|
||||
File(fileFolderName, fileName).writeText(textString)
|
||||
}
|
||||
}
|
||||
get("/files/{fileName}") {
|
||||
val fileName = call.parameters["fileName"] ?: throw Exception("No fileName!")
|
||||
println("Get file: $fileName")
|
||||
val file = File(fileFolderName, fileName)
|
||||
if (!file.exists()) throw Exception("File does not exist!")
|
||||
val fileText = file.readText()
|
||||
println("Text read: $fileText")
|
||||
call.respondText(fileText)
|
||||
}
|
||||
delete("/files/{fileName}") {
|
||||
val fileName = call.parameters["fileName"] ?: throw Exception("No fileName!")
|
||||
val file = File(fileFolderName, fileName)
|
||||
if (!file.exists()) throw Exception("File does not exist!")
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
}.start(wait = true)
|
||||
}
|
||||
}
|
33
docs/Other/Hosting_a_Multiplayer_server.md
Normal file
33
docs/Other/Hosting_a_Multiplayer_server.md
Normal file
@ -0,0 +1,33 @@
|
||||
|
||||
# Hosting a Multiplayer server
|
||||
|
||||
Due to certain limitations on Dropbox's API, with the current influx of players, we've many times reached the point that Dropbox has become unavailable.
|
||||
|
||||
Therefore, you can now host your own Unciv server, when not on Android.
|
||||
|
||||
To do so, you must have a JDK installed.
|
||||
|
||||
From the directory where the Unciv.jar file is located, open a terminal and run the following line:
|
||||
`java -cp Unciv.jar com.unciv.app.desktop.UncivServer`
|
||||
|
||||
Your server has now started!
|
||||
|
||||
|
||||
In Unciv itself, from the same computer, enter Options > Multiplayer.
|
||||
|
||||
Click the first text (Current IP address) to copy the IP to the clipboard.
|
||||
Then, click the second the second (Server IP address) to put your computer's IP in the "Server IP" slot.
|
||||
|
||||
If you click "check connection to server" you should now get "Return result: true", which means it's working!
|
||||
|
||||
|
||||
So far you ran the server and connected yourself to it, but now for the interesting part - connecting other people!
|
||||
|
||||
The IP should still be in your clipboard - if not, just click the 'copy to clipboard' button again.
|
||||
Send the IP to the other device, there - copy it, and click 'copy from clipboard'.
|
||||
You can of course enter the IP manually if that's easier for you.
|
||||
|
||||
Click 'check connection' from the new device, and if you got the same result - congratulations, you're both connected to the same server and can start a multiplayer game on the server!
|
||||
|
||||
|
||||
Please note that devices NOT connected to the same server will NOT be able to participate in multiplayer games together!
|
Loading…
Reference in New Issue
Block a user