mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-10 07:48:31 +07:00
Make mod categories curated in Json (#9542)
* Dynamic mod categories from online query * Move Working string to Constants * Move Mod categories to json * Move Mod categories to json - UI * Move Mod categories to json - initial json
This commit is contained in:
50
android/assets/jsons/ModCategories.json
Normal file
50
android/assets/jsons/ModCategories.json
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"label": "All mods",
|
||||||
|
"topic": "unciv-mod",
|
||||||
|
"createDate": "2020-08-26T11:35:39Z",
|
||||||
|
"modifyDate": "2023-06-08T05:39:47Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Rulesets",
|
||||||
|
"topic": "unciv-mod-rulesets",
|
||||||
|
"createDate": "2022-07-23T06:41:09Z",
|
||||||
|
"modifyDate": "2022-11-22T01:58:08Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Expansions",
|
||||||
|
"topic": "unciv-mod-expansions",
|
||||||
|
"createDate": "2022-07-23T03:55:05Z",
|
||||||
|
"modifyDate": "2023-02-25T19:44:45Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Graphics",
|
||||||
|
"topic": "unciv-mod-graphics",
|
||||||
|
"createDate": "2022-06-26T11:55:31Z",
|
||||||
|
"modifyDate": "2022-11-06T00:09:20Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Audio",
|
||||||
|
"topic": "unciv-mod-audio",
|
||||||
|
"createDate": "2022-06-26T11:55:15Z",
|
||||||
|
"modifyDate": "2023-02-19T11:55:56Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Maps",
|
||||||
|
"topic": "unciv-mod-maps",
|
||||||
|
"createDate": "2022-07-28T20:24:37Z",
|
||||||
|
"modifyDate": "2022-07-28T21:17:57Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Fun",
|
||||||
|
"topic": "unciv-mod-fun",
|
||||||
|
"createDate": "2022-07-24T08:15:47Z",
|
||||||
|
"modifyDate": "2023-04-07T16:32:11Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Mods of mods",
|
||||||
|
"topic": "unciv-mod-modsofmods",
|
||||||
|
"createDate": "2022-08-05T10:00:15Z",
|
||||||
|
"modifyDate": "2022-12-13T23:01:19Z"
|
||||||
|
}
|
||||||
|
]
|
@ -794,6 +794,7 @@ Advanced =
|
|||||||
Generate translation files =
|
Generate translation files =
|
||||||
Translation files are generated successfully. =
|
Translation files are generated successfully. =
|
||||||
Fastlane files are generated successfully. =
|
Fastlane files are generated successfully. =
|
||||||
|
Update Mod categories =
|
||||||
|
|
||||||
Screen orientation =
|
Screen orientation =
|
||||||
|
|
||||||
@ -1732,6 +1733,7 @@ Downloaded! =
|
|||||||
[modName] Downloaded! =
|
[modName] Downloaded! =
|
||||||
Could not download [modName] =
|
Could not download [modName] =
|
||||||
Online query result is incomplete =
|
Online query result is incomplete =
|
||||||
|
Sorting and filtering needs to wait until the online query finishes =
|
||||||
No description provided =
|
No description provided =
|
||||||
[stargazers]✯ =
|
[stargazers]✯ =
|
||||||
Author: [author] =
|
Author: [author] =
|
||||||
|
@ -70,6 +70,7 @@ object Constants {
|
|||||||
const val yes = "Yes"
|
const val yes = "Yes"
|
||||||
const val no = "No"
|
const val no = "No"
|
||||||
const val loading = "Loading..."
|
const val loading = "Loading..."
|
||||||
|
const val working = "Working..."
|
||||||
|
|
||||||
const val barbarians = "Barbarians"
|
const val barbarians = "Barbarians"
|
||||||
const val spectator = "Spectator"
|
const val spectator = "Spectator"
|
||||||
|
@ -16,14 +16,13 @@ import java.time.Duration
|
|||||||
* [Json] is not thread-safe. Use a new one for each parse.
|
* [Json] is not thread-safe. Use a new one for each parse.
|
||||||
*/
|
*/
|
||||||
fun json() = Json(JsonWriter.OutputType.json).apply {
|
fun json() = Json(JsonWriter.OutputType.json).apply {
|
||||||
|
// Gdx default output type is JsonWriter.OutputType.minimal, which generates invalid Json - e.g. most quotes removed.
|
||||||
|
// The constructor parameter above changes that to valid Json
|
||||||
|
// Note an instance set to json can read minimal and vice versa
|
||||||
|
|
||||||
setIgnoreDeprecated(true)
|
setIgnoreDeprecated(true)
|
||||||
ignoreUnknownFields = true
|
ignoreUnknownFields = true
|
||||||
|
|
||||||
// Default output type is JsonWriter.OutputType.minimal, which generates invalid Json - e.g. most quotes removed.
|
|
||||||
// To get better Json, use:
|
|
||||||
// setOutputType(JsonWriter.OutputType.json)
|
|
||||||
// Note an instance set to json can read minimal and vice versa
|
|
||||||
|
|
||||||
setSerializer(HashMapVector2.getSerializerClass(), HashMapVector2.createSerializer())
|
setSerializer(HashMapVector2.getSerializerClass(), HashMapVector2.createSerializer())
|
||||||
setSerializer(Duration::class.java, DurationSerializer())
|
setSerializer(Duration::class.java, DurationSerializer())
|
||||||
setSerializer(KeyCharAndCode::class.java, KeyCharAndCode.Serializer())
|
setSerializer(KeyCharAndCode::class.java, KeyCharAndCode.Serializer())
|
||||||
|
85
core/src/com/unciv/models/metadata/ModCategories.kt
Normal file
85
core/src/com/unciv/models/metadata/ModCategories.kt
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package com.unciv.models.metadata
|
||||||
|
|
||||||
|
import com.badlogic.gdx.Gdx
|
||||||
|
import com.unciv.json.json
|
||||||
|
import com.unciv.ui.screens.newgamescreen.TranslatedSelectBox
|
||||||
|
import com.unciv.ui.screens.pickerscreens.Github
|
||||||
|
|
||||||
|
|
||||||
|
class ModCategories : ArrayList<ModCategories.Category>() {
|
||||||
|
|
||||||
|
class Category(
|
||||||
|
val label: String,
|
||||||
|
val topic: String,
|
||||||
|
val hidden: Boolean,
|
||||||
|
/** copy of github created_at, no function except help evaluate */
|
||||||
|
@Suppress("unused")
|
||||||
|
val createDate: String,
|
||||||
|
/** copy of github updated_at, no function except help evaluate */
|
||||||
|
var modifyDate: String
|
||||||
|
) {
|
||||||
|
constructor() :
|
||||||
|
this("", "", false, "", "")
|
||||||
|
|
||||||
|
constructor(topic: Github.TopicSearchResponse.Topic) :
|
||||||
|
this(labelSuggestion(topic), topic.name, true, topic.created_at, topic.updated_at)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val All = Category("All mods", "unciv-mod", false, "", "")
|
||||||
|
fun labelSuggestion(topic: Github.TopicSearchResponse.Topic) =
|
||||||
|
topic.display_name?.takeUnless { it.isBlank() }
|
||||||
|
?: topic.name.removePrefix("unciv-mod-").replaceFirstChar(Char::titlecase)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?) = this === other || other is Category && topic == other.topic
|
||||||
|
override fun hashCode() = topic.hashCode()
|
||||||
|
override fun toString() = label
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val fileLocation = "jsons/ModCategories.json"
|
||||||
|
|
||||||
|
private val INSTANCE: ModCategories
|
||||||
|
|
||||||
|
init {
|
||||||
|
val file = Gdx.files.internal(fileLocation)
|
||||||
|
INSTANCE = if (file.exists())
|
||||||
|
json().fromJson(ModCategories::class.java, Category::class.java, file)
|
||||||
|
else ModCategories().apply { add(default()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun default() = Category.All
|
||||||
|
fun mergeOnline() = INSTANCE.mergeOnline()
|
||||||
|
fun fromSelectBox(selectBox: TranslatedSelectBox) = INSTANCE.fromSelectBox(selectBox)
|
||||||
|
fun asSequence() = INSTANCE.asSequence().filter { !it.hidden }
|
||||||
|
operator fun iterator() = asSequence().iterator()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun save() {
|
||||||
|
val json = json()
|
||||||
|
val compact = json.toJson(this, ModCategories::class.java, Category::class.java)
|
||||||
|
val verbose = json.prettyPrint(compact)
|
||||||
|
Gdx.files.local(fileLocation).writeString(verbose, false, "UTF-8")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromSelectBox(selectBox: TranslatedSelectBox): Category {
|
||||||
|
val selected = selectBox.selected.value
|
||||||
|
return firstOrNull { it.label == selected } ?: Category.All
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mergeOnline(): String {
|
||||||
|
val topics = Github.tryGetGithubTopics() ?: return "Failed"
|
||||||
|
var newCount = 0
|
||||||
|
for (topic in topics.items.sortedBy { it.name }) {
|
||||||
|
val existing = firstOrNull { it.topic == topic.name }
|
||||||
|
if (existing != null) {
|
||||||
|
existing.modifyDate = topic.updated_at
|
||||||
|
} else {
|
||||||
|
add(Category(topic))
|
||||||
|
newCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
save()
|
||||||
|
return "$newCount new categories"
|
||||||
|
}
|
||||||
|
}
|
@ -13,9 +13,11 @@ import com.badlogic.gdx.scenes.scene2d.ui.Cell
|
|||||||
import com.badlogic.gdx.scenes.scene2d.ui.SelectBox
|
import com.badlogic.gdx.scenes.scene2d.ui.SelectBox
|
||||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||||
import com.badlogic.gdx.utils.Array
|
import com.badlogic.gdx.utils.Array
|
||||||
|
import com.unciv.Constants
|
||||||
import com.unciv.GUI
|
import com.unciv.GUI
|
||||||
import com.unciv.UncivGame
|
import com.unciv.UncivGame
|
||||||
import com.unciv.models.metadata.GameSettings
|
import com.unciv.models.metadata.GameSettings
|
||||||
|
import com.unciv.models.metadata.ModCategories
|
||||||
import com.unciv.models.metadata.ScreenSize
|
import com.unciv.models.metadata.ScreenSize
|
||||||
import com.unciv.models.translations.TranslationFileWriter
|
import com.unciv.models.translations.TranslationFileWriter
|
||||||
import com.unciv.models.translations.tr
|
import com.unciv.models.translations.tr
|
||||||
@ -234,8 +236,8 @@ private fun addTranslationGeneration(table: Table, optionsPopup: OptionsPopup) {
|
|||||||
val generateTranslationsButton = "Generate translation files".toTextButton()
|
val generateTranslationsButton = "Generate translation files".toTextButton()
|
||||||
|
|
||||||
generateTranslationsButton.onActivation {
|
generateTranslationsButton.onActivation {
|
||||||
optionsPopup.tabs.selectPage("Advanced")
|
optionsPopup.tabs.selectPage("Advanced") // only because key F12 works from any page
|
||||||
generateTranslationsButton.setText("Working...".tr())
|
generateTranslationsButton.setText(Constants.working.tr())
|
||||||
Concurrency.run("WriteTranslations") {
|
Concurrency.run("WriteTranslations") {
|
||||||
val result = TranslationFileWriter.writeNewTranslationFiles()
|
val result = TranslationFileWriter.writeNewTranslationFiles()
|
||||||
launchOnGLThread {
|
launchOnGLThread {
|
||||||
@ -250,12 +252,24 @@ private fun addTranslationGeneration(table: Table, optionsPopup: OptionsPopup) {
|
|||||||
generateTranslationsButton.addTooltip("F12", 18f)
|
generateTranslationsButton.addTooltip("F12", 18f)
|
||||||
table.add(generateTranslationsButton).colspan(2).row()
|
table.add(generateTranslationsButton).colspan(2).row()
|
||||||
|
|
||||||
|
val updateModCategoriesButton = "Update Mod categories".toTextButton()
|
||||||
|
updateModCategoriesButton.onActivation {
|
||||||
|
updateModCategoriesButton.setText(Constants.working.tr())
|
||||||
|
Concurrency.run("GithubTopicQuery") {
|
||||||
|
val result = ModCategories.mergeOnline()
|
||||||
|
launchOnGLThread {
|
||||||
|
updateModCategoriesButton.setText(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
table.add(updateModCategoriesButton).colspan(2).row()
|
||||||
|
|
||||||
|
if (!UncivGame.Current.files.getSave("ScreenshotGenerationGame").exists()) return
|
||||||
|
|
||||||
val generateScreenshotsButton = "Generate screenshots".toTextButton()
|
val generateScreenshotsButton = "Generate screenshots".toTextButton()
|
||||||
|
|
||||||
generateScreenshotsButton.onActivation {
|
generateScreenshotsButton.onActivation {
|
||||||
optionsPopup.tabs.selectPage("Advanced")
|
generateScreenshotsButton.setText(Constants.working.tr())
|
||||||
generateScreenshotsButton.setText("Working...".tr())
|
|
||||||
Concurrency.run("GenerateScreenshot") {
|
Concurrency.run("GenerateScreenshot") {
|
||||||
val extraImagesLocation = "../../extraImages"
|
val extraImagesLocation = "../../extraImages"
|
||||||
// I'm not sure why we need to advance the y by 2 for every screenshot... but that's the only way it remains centered
|
// I'm not sure why we need to advance the y by 2 for every screenshot... but that's the only way it remains centered
|
||||||
@ -267,8 +281,8 @@ private fun addTranslationGeneration(table: Table, optionsPopup: OptionsPopup) {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// table.add(generateScreenshotsButton).colspan(2).row()
|
|
||||||
|
|
||||||
|
table.add(generateScreenshotsButton).colspan(2).row()
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ScreenshotConfig(val width: Int, val height: Int, val screenSize: ScreenSize, var fileLocation:String, var centerTile:Vector2, var attackCity:Boolean=true)
|
data class ScreenshotConfig(val width: Int, val height: Int, val screenSize: ScreenSize, var fileLocation:String, var centerTile:Vector2, var attackCity:Boolean=true)
|
||||||
|
@ -6,6 +6,7 @@ import com.badlogic.gdx.scenes.scene2d.actions.Actions
|
|||||||
import com.badlogic.gdx.scenes.scene2d.ui.Stack
|
import com.badlogic.gdx.scenes.scene2d.ui.Stack
|
||||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||||
import com.badlogic.gdx.utils.Align
|
import com.badlogic.gdx.utils.Align
|
||||||
|
import com.unciv.Constants
|
||||||
import com.unciv.GUI
|
import com.unciv.GUI
|
||||||
import com.unciv.UncivGame
|
import com.unciv.UncivGame
|
||||||
import com.unciv.logic.GameInfo
|
import com.unciv.logic.GameInfo
|
||||||
@ -289,7 +290,7 @@ class MainMenuScreen: BaseScreen(), RecreateOnResize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun quickstartNewGame() {
|
private fun quickstartNewGame() {
|
||||||
ToastPopup("Working...", this)
|
ToastPopup(Constants.working, this)
|
||||||
val errorText = "Cannot start game with the default new game parameters!"
|
val errorText = "Cannot start game with the default new game parameters!"
|
||||||
Concurrency.run("QuickStart") {
|
Concurrency.run("QuickStart") {
|
||||||
val newGame: GameInfo
|
val newGame: GameInfo
|
||||||
|
@ -4,6 +4,7 @@ import com.badlogic.gdx.Gdx
|
|||||||
import com.badlogic.gdx.scenes.scene2d.ui.ButtonGroup
|
import com.badlogic.gdx.scenes.scene2d.ui.ButtonGroup
|
||||||
import com.badlogic.gdx.scenes.scene2d.ui.CheckBox
|
import com.badlogic.gdx.scenes.scene2d.ui.CheckBox
|
||||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||||
|
import com.unciv.Constants
|
||||||
import com.unciv.logic.map.MapGeneratedMainType
|
import com.unciv.logic.map.MapGeneratedMainType
|
||||||
import com.unciv.logic.map.MapParameters
|
import com.unciv.logic.map.MapParameters
|
||||||
import com.unciv.logic.map.MapType
|
import com.unciv.logic.map.MapType
|
||||||
@ -59,9 +60,9 @@ class MapEditorGenerateTab(
|
|||||||
|
|
||||||
private fun setButtonsEnabled(enable: Boolean) {
|
private fun setButtonsEnabled(enable: Boolean) {
|
||||||
newTab.generateButton.isEnabled = enable
|
newTab.generateButton.isEnabled = enable
|
||||||
newTab.generateButton.setText( (if(enable) "Create" else "Working...").tr())
|
newTab.generateButton.setText( (if(enable) "Create" else Constants.working).tr())
|
||||||
partialTab.generateButton.isEnabled = enable
|
partialTab.generateButton.isEnabled = enable
|
||||||
partialTab.generateButton.setText( (if(enable) "Generate" else "Working...").tr())
|
partialTab.generateButton.setText( (if(enable) "Generate" else Constants.working).tr())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generate(step: MapGeneratorSteps) {
|
private fun generate(step: MapGeneratorSteps) {
|
||||||
|
@ -4,6 +4,7 @@ import com.badlogic.gdx.files.FileHandle
|
|||||||
import com.badlogic.gdx.graphics.Color
|
import com.badlogic.gdx.graphics.Color
|
||||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||||
import com.badlogic.gdx.scenes.scene2d.ui.TextField
|
import com.badlogic.gdx.scenes.scene2d.ui.TextField
|
||||||
|
import com.unciv.Constants
|
||||||
import com.unciv.logic.files.MapSaver
|
import com.unciv.logic.files.MapSaver
|
||||||
import com.unciv.logic.map.MapGeneratedMainType
|
import com.unciv.logic.map.MapGeneratedMainType
|
||||||
import com.unciv.logic.map.TileMap
|
import com.unciv.logic.map.TileMap
|
||||||
@ -81,7 +82,7 @@ class MapEditorSaveTab(
|
|||||||
|
|
||||||
private fun setSaveButton(enabled: Boolean) {
|
private fun setSaveButton(enabled: Boolean) {
|
||||||
saveButton.isEnabled = enabled
|
saveButton.isEnabled = enabled
|
||||||
saveButton.setText((if (enabled) "Save map" else "Working...").tr())
|
saveButton.setText((if (enabled) "Save map" else Constants.working).tr())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveHandler() {
|
private fun saveHandler() {
|
||||||
|
@ -2,6 +2,7 @@ package com.unciv.ui.screens.multiplayerscreens
|
|||||||
|
|
||||||
import com.badlogic.gdx.Gdx
|
import com.badlogic.gdx.Gdx
|
||||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||||
|
import com.unciv.Constants
|
||||||
import com.unciv.logic.IdChecker
|
import com.unciv.logic.IdChecker
|
||||||
import com.unciv.models.translations.tr
|
import com.unciv.models.translations.tr
|
||||||
import com.unciv.ui.screens.pickerscreens.PickerScreen
|
import com.unciv.ui.screens.pickerscreens.PickerScreen
|
||||||
@ -53,7 +54,7 @@ class AddMultiplayerGameScreen : PickerScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val popup = Popup(this)
|
val popup = Popup(this)
|
||||||
popup.addGoodSizedLabel("Working...")
|
popup.addGoodSizedLabel(Constants.working)
|
||||||
popup.open()
|
popup.open()
|
||||||
|
|
||||||
Concurrency.run("AddMultiplayerGame") {
|
Concurrency.run("AddMultiplayerGame") {
|
||||||
|
@ -2,6 +2,7 @@ package com.unciv.ui.screens.multiplayerscreens
|
|||||||
|
|
||||||
import com.badlogic.gdx.graphics.Color
|
import com.badlogic.gdx.graphics.Color
|
||||||
import com.badlogic.gdx.scenes.scene2d.ui.TextButton.TextButtonStyle
|
import com.badlogic.gdx.scenes.scene2d.ui.TextButton.TextButtonStyle
|
||||||
|
import com.unciv.Constants
|
||||||
import com.unciv.logic.multiplayer.OnlineMultiplayerGame
|
import com.unciv.logic.multiplayer.OnlineMultiplayerGame
|
||||||
import com.unciv.logic.multiplayer.storage.MultiplayerAuthException
|
import com.unciv.logic.multiplayer.storage.MultiplayerAuthException
|
||||||
import com.unciv.models.translations.tr
|
import com.unciv.models.translations.tr
|
||||||
@ -103,7 +104,7 @@ class EditMultiplayerGameInfoScreen(val multiplayerGame: OnlineMultiplayerGame)
|
|||||||
private fun resign(multiplayerGame: OnlineMultiplayerGame) {
|
private fun resign(multiplayerGame: OnlineMultiplayerGame) {
|
||||||
//Create a popup
|
//Create a popup
|
||||||
val popup = Popup(this)
|
val popup = Popup(this)
|
||||||
popup.addGoodSizedLabel("Working...").row()
|
popup.addGoodSizedLabel(Constants.working).row()
|
||||||
popup.open()
|
popup.open()
|
||||||
|
|
||||||
Concurrency.runOnNonDaemonThreadPool("Resign") {
|
Concurrency.runOnNonDaemonThreadPool("Resign") {
|
||||||
|
@ -206,7 +206,7 @@ class NewGameScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
rightSideButton.disable()
|
rightSideButton.disable()
|
||||||
rightSideButton.setText("Working...".tr())
|
rightSideButton.setText(Constants.working.tr())
|
||||||
|
|
||||||
setSkin()
|
setSkin()
|
||||||
// Creating a new game can take a while and we don't want ANRs
|
// Creating a new game can take a while and we don't want ANRs
|
||||||
@ -280,7 +280,7 @@ class NewGameScreen(
|
|||||||
private suspend fun startNewGame() = coroutineScope {
|
private suspend fun startNewGame() = coroutineScope {
|
||||||
val popup = Popup(this@NewGameScreen)
|
val popup = Popup(this@NewGameScreen)
|
||||||
launchOnGLThread {
|
launchOnGLThread {
|
||||||
popup.addGoodSizedLabel("Working...").row()
|
popup.addGoodSizedLabel(Constants.working).row()
|
||||||
popup.open()
|
popup.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -395,6 +395,48 @@ object Github {
|
|||||||
var avatar_url: String? = null
|
var avatar_url: String? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query GitHub for topics named "unciv-mod*"
|
||||||
|
* @return Parsed [TopicSearchResponse] json on success, `null` on failure.
|
||||||
|
*/
|
||||||
|
fun tryGetGithubTopics(): TopicSearchResponse? {
|
||||||
|
// `+repositories:>1` means ignore unused or practically unused topics
|
||||||
|
val link = "https://api.github.com/search/topics?q=unciv-mod+repositories:%3E1&sort=name&order=asc"
|
||||||
|
var retries = 2
|
||||||
|
while (retries > 0) {
|
||||||
|
retries--
|
||||||
|
// obey rate limit
|
||||||
|
if (RateLimit.waitForLimit()) return null
|
||||||
|
// try download
|
||||||
|
val inputStream = download(link) {
|
||||||
|
if (it.responseCode == 403 || it.responseCode == 200 && retries == 1) {
|
||||||
|
// Pass the response headers to the rate limit handler so it can process the rate limit headers
|
||||||
|
RateLimit.notifyHttpResponse(it)
|
||||||
|
retries++ // An extra retry so the 403 is ignored in the retry count
|
||||||
|
}
|
||||||
|
} ?: continue
|
||||||
|
return json().fromJson(TopicSearchResponse::class.java, inputStream.bufferedReader().readText())
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Topic search response */
|
||||||
|
@Suppress("PropertyName")
|
||||||
|
class TopicSearchResponse {
|
||||||
|
// Commented out: Github returns them, but we're not interested
|
||||||
|
// var total_count = 0
|
||||||
|
// var incomplete_results = false
|
||||||
|
var items = ArrayList<Topic>()
|
||||||
|
class Topic {
|
||||||
|
var name = ""
|
||||||
|
var display_name: String? = null // Would need to be curated, which is alottawork
|
||||||
|
// var featured = false
|
||||||
|
// var curated = false
|
||||||
|
var created_at = "" // iso datetime with "Z" timezone
|
||||||
|
var updated_at = "" // iso datetime with "Z" timezone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Rewrite modOptions file for a mod we just installed to include metadata we got from the GitHub api
|
/** Rewrite modOptions file for a mod we just installed to include metadata we got from the GitHub api
|
||||||
*
|
*
|
||||||
* (called on background thread)
|
* (called on background thread)
|
||||||
|
@ -7,6 +7,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table
|
|||||||
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
|
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
|
||||||
import com.badlogic.gdx.utils.Align
|
import com.badlogic.gdx.utils.Align
|
||||||
import com.unciv.Constants
|
import com.unciv.Constants
|
||||||
|
import com.unciv.models.metadata.ModCategories
|
||||||
import com.unciv.models.ruleset.Ruleset
|
import com.unciv.models.ruleset.Ruleset
|
||||||
import com.unciv.models.translations.tr
|
import com.unciv.models.translations.tr
|
||||||
import com.unciv.ui.images.ImageGetter
|
import com.unciv.ui.images.ImageGetter
|
||||||
@ -72,27 +73,6 @@ class ModManagementOptions(private val modManagementScreen: ModManagementScreen)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Category(
|
|
||||||
val label: String,
|
|
||||||
val topic: String
|
|
||||||
) {
|
|
||||||
All("All mods", "unciv-mod"),
|
|
||||||
Rulesets("Rulesets", "unciv-mod-rulesets"),
|
|
||||||
Expansions("Expansions", "unciv-mod-expansions"),
|
|
||||||
Graphics("Graphics", "unciv-mod-graphics"),
|
|
||||||
Audio("Audio", "unciv-mod-audio"),
|
|
||||||
Maps("Maps", "unciv-mod-maps"),
|
|
||||||
Fun("Fun", "unciv-mod-fun"),
|
|
||||||
ModsOfMods("Mods of mods", "unciv-mod-modsofmods");
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromSelectBox(selectBox: TranslatedSelectBox): Category {
|
|
||||||
val selected = selectBox.selected.value
|
|
||||||
return values().firstOrNull { it.label == selected } ?: All
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Filter(
|
class Filter(
|
||||||
val text: String,
|
val text: String,
|
||||||
val topic: String
|
val topic: String
|
||||||
@ -104,7 +84,8 @@ class ModManagementOptions(private val modManagementScreen: ModManagementScreen)
|
|||||||
|
|
||||||
private val textField = UncivTextField.create("Enter search text")
|
private val textField = UncivTextField.create("Enter search text")
|
||||||
|
|
||||||
var category = Category.All
|
var category = ModCategories.default()
|
||||||
|
|
||||||
var sortInstalled = SortType.Name
|
var sortInstalled = SortType.Name
|
||||||
var sortOnline = SortType.Stars
|
var sortOnline = SortType.Stars
|
||||||
|
|
||||||
@ -140,12 +121,12 @@ class ModManagementOptions(private val modManagementScreen: ModManagementScreen)
|
|||||||
}
|
}
|
||||||
|
|
||||||
categorySelect = TranslatedSelectBox(
|
categorySelect = TranslatedSelectBox(
|
||||||
Category.values().map { category -> category.label },
|
ModCategories.asSequence().map { it.label }.toList(),
|
||||||
category.label,
|
category.label,
|
||||||
BaseScreen.skin
|
BaseScreen.skin
|
||||||
)
|
)
|
||||||
categorySelect.onChange {
|
categorySelect.onChange {
|
||||||
category = Category.fromSelectBox(categorySelect)
|
category = ModCategories.fromSelectBox(categorySelect)
|
||||||
modManagementScreen.refreshInstalledModTable()
|
modManagementScreen.refreshInstalledModTable()
|
||||||
modManagementScreen.refreshOnlineModTable()
|
modManagementScreen.refreshOnlineModTable()
|
||||||
}
|
}
|
||||||
@ -209,10 +190,10 @@ class ModManagementOptions(private val modManagementScreen: ModManagementScreen)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getTextButton(nameString:String, topics: List<String>): TextButton {
|
private fun getTextButton(nameString: String, topics: List<String>): TextButton {
|
||||||
val categories = ArrayList<ModManagementOptions.Category>()
|
val categories = ArrayList<ModCategories.Category>()
|
||||||
for (category in ModManagementOptions.Category.values()) {
|
for (category in ModCategories) {
|
||||||
if (category== ModManagementOptions.Category.All) continue
|
if (category == ModCategories.default()) continue
|
||||||
if (topics.contains(category.topic)) categories += category
|
if (topics.contains(category.topic)) categories += category
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,8 +206,11 @@ private fun getTextButton(nameString:String, topics: List<String>): TextButton {
|
|||||||
return button
|
return button
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Helper class holds combined mod info for ModManagementScreen, used for both installed and online lists */
|
/** Helper class holds combined mod info for ModManagementScreen, used for both installed and online lists
|
||||||
class ModUIData(
|
*
|
||||||
|
* Note it is guaranteed either ruleset or repo are non-null, never both.
|
||||||
|
*/
|
||||||
|
class ModUIData private constructor(
|
||||||
val name: String,
|
val name: String,
|
||||||
val description: String,
|
val description: String,
|
||||||
val ruleset: Ruleset?,
|
val ruleset: Ruleset?,
|
||||||
@ -270,12 +254,10 @@ class ModUIData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun matchesCategory(filter: ModManagementOptions.Filter): Boolean {
|
private fun matchesCategory(filter: ModManagementOptions.Filter): Boolean {
|
||||||
val modTopic = repo?.topics ?: ruleset?.modOptions?.topics!!
|
if (filter.topic == ModCategories.default().topic)
|
||||||
if (filter.topic == ModManagementOptions.Category.All.topic)
|
|
||||||
return true
|
return true
|
||||||
if (modTopic.size < 2) return false
|
val modTopics = repo?.topics ?: ruleset?.modOptions?.topics!!
|
||||||
if (modTopic[1] == filter.topic) return true
|
return filter.topic in modTopics
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -718,7 +718,10 @@ class ModManagementScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal fun refreshOnlineModTable() {
|
internal fun refreshOnlineModTable() {
|
||||||
if (runningSearchJob != null) return // cowardice: prevent concurrent modification, avoid a manager layer
|
if (runningSearchJob != null) {
|
||||||
|
ToastPopup("Sorting and filtering needs to wait until the online query finishes", this)
|
||||||
|
return // cowardice: prevent concurrent modification, avoid a manager layer
|
||||||
|
}
|
||||||
|
|
||||||
val newHeaderText = optionsManager.getOnlineHeader()
|
val newHeaderText = optionsManager.getOnlineHeader()
|
||||||
onlineHeaderLabel?.setText(newHeaderText)
|
onlineHeaderLabel?.setText(newHeaderText)
|
||||||
|
@ -141,7 +141,7 @@ class LoadGameScreen : LoadOrSaveScreen() {
|
|||||||
private fun getLoadFromClipboardButton(): TextButton {
|
private fun getLoadFromClipboardButton(): TextButton {
|
||||||
val pasteButton = loadFromClipboard.toTextButton()
|
val pasteButton = loadFromClipboard.toTextButton()
|
||||||
pasteButton.onActivation {
|
pasteButton.onActivation {
|
||||||
pasteButton.setText("Working...".tr())
|
pasteButton.setText(Constants.working.tr())
|
||||||
pasteButton.disable()
|
pasteButton.disable()
|
||||||
Concurrency.run(loadFromClipboard) {
|
Concurrency.run(loadFromClipboard) {
|
||||||
try {
|
try {
|
||||||
|
@ -192,7 +192,7 @@ class NextTurnButton : IconTextButton("", null, 30) {
|
|||||||
class NextTurnAction(val text: String, val color: Color, val icon: String? = null, val action: () -> Unit) {
|
class NextTurnAction(val text: String, val color: Color, val icon: String? = null, val action: () -> Unit) {
|
||||||
companion object Prefabs {
|
companion object Prefabs {
|
||||||
val Default = NextTurnAction("", Color.BLACK) {}
|
val Default = NextTurnAction("", Color.BLACK) {}
|
||||||
val Working = NextTurnAction("Working...", Color.GRAY, "NotificationIcons/Working") {}
|
val Working = NextTurnAction(Constants.working, Color.GRAY, "NotificationIcons/Working") {}
|
||||||
val Waiting = NextTurnAction("Waiting for other players...",Color.GRAY, "NotificationIcons/Waiting") {}
|
val Waiting = NextTurnAction("Waiting for other players...",Color.GRAY, "NotificationIcons/Waiting") {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,23 +84,25 @@ In order to do this, all you need to do is:
|
|||||||
- Click the gear icon next to the About (top-right part of the page)
|
- Click the gear icon next to the About (top-right part of the page)
|
||||||
- In 'Topics', add "unciv-mod"
|
- In 'Topics', add "unciv-mod"
|
||||||
|
|
||||||
Optionally add one of the following topics (make sure this topic is added afer "unciv-mod"):
|
Optionally add one or more of the following topics to mark your mod as belonging to specific categories:
|
||||||
|
|
||||||
- unciv-mod-rulesets
|
- unciv-mod-rulesets (for base ruleset mods)
|
||||||
- unciv-mod-expansions
|
- unciv-mod-expansions (for mods extending vanilla rulesets - please use this, **not** unciv-mod-expansion)
|
||||||
- unciv-mod-graphics
|
- unciv-mod-graphics (for mods altering graphics - icons, portraits, tilesets)
|
||||||
- unciv-mod-audio
|
- unciv-mod-audio (for mods supplying music or modifying sounds)
|
||||||
- unciv-mod-maps
|
- unciv-mod-maps (for mods containing maps)
|
||||||
- unciv-mod-fun
|
- unciv-mod-fun (for mods mainly tweaking mechanics or other gameplay aspects)
|
||||||
- unciv-mod-modsofmods
|
- unciv-mod-modsofmods (for mods extending another mod's ruleset)
|
||||||
|
|
||||||
When you open your app, it will query Github's [list of repos with that topic](https://github.com/topics/unciv-mod), and now YOUR repo will appear there!
|
When you open Unciv's Mod Manager, it will query Github's [list of repos with that topic](https://github.com/topics/unciv-mod), and now YOUR repo will appear there!
|
||||||
|
The categories will appear als annotations on the mod buttons, and the user can filter for them. They are not required for the game to use the content - e.g. you can still load maps from mods lacking the unciv-mod-maps topic.
|
||||||
|
If you want new categories, github will accept any topic, but you'll have to ask the Unciv team to enable them in the game.
|
||||||
|
|
||||||
## I have the mod, now what?
|
## I have the mod, now what?
|
||||||
|
|
||||||
The primary use of mods is to add them when starting a new game, or configuring a map. This will mean that both the ruleset of the mod, and the images, will be in use for that specific game/map.
|
The primary use of mods is to add them when starting a new game, or configuring a map. This will mean that both the ruleset of the mod, and the images, will be in use for that specific game/map.
|
||||||
|
|
||||||
For mods which are primarily visual or audio, there is a second use - through the mod manager, you can enable them as **permanent audiovisual mods**. This means that the images, sounds (or upcoming: music) from the mod will replace the original media everywhere in the game.
|
For mods which are primarily visual or audio, there is a second use - through the mod manager, you can enable them as **permanent audiovisual mods**. This means that the images and/or sounds from the mod will replace the original media everywhere in the game, and contained music will be available - [see here](Images-and-Audio.md#supply-additional-music).
|
||||||
|
|
||||||
## Mod location for manual loading of mods
|
## Mod location for manual loading of mods
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user