mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-30 22:58:50 +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:
@ -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")
|
||||
|
Reference in New Issue
Block a user