GithubHelp home page GithubHelp logo

manueldidonna / pokemon-save-editor-android Goto Github PK

View Code? Open in Web Editor NEW
0.0 1.0 0.0 1.18 MB

A work-in-progress pokémon save editor for Android

License: GNU General Public License v3.0

Kotlin 100.00%
android-application pokemon pokemon-editor save-editor jetpack-compose kotlin

pokemon-save-editor-android's Introduction

🧬 Pokemon Editor 🚀

A 🚧 work-in-progress 🚧 Pokémon core series save editor for Android.

  • 👷 The app is in the early stages of development and depends on some libraries that are not suitable for production use
  • 👷 Still unnamed and needs an icon

🕹️ Supported games

  • Red/Blue/Yellow
  • Gold/Silver/Crystal
  • Ruby/Sapphire/Emerald (planned)
  • FireRed/LeafGreen (planned)

👨‍💻 Android Development

📜 License

Copyright (C) 2020 Manuel Di Donna

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see https://www.gnu.org/licenses.

pokemon-save-editor-android's People

Contributors

manueldidonna avatar

Watchers

 avatar

pokemon-save-editor-android's Issues

Convert Version to a sealed class

Version is a class that should used only to retrieve the resources of specific games (pokemon species, stats, abilities, forms) and not to rule the editing logic. Library users won't ever need to implement custom version classes

sealed class Version {
    val index: Int
    data class FirstGeneration(val isYellow: Boolean) : Version() {
        override val index: Int = 1
    }
}

Support Hall of Fame

interface PokemonLeagueHall {
    val size: Int

    fun getRecords(index: Int): List<Record>

    data class Record(
        val speciesId: Int,
        val nickname: String,
        val level: Int
    )
}

Import pokemon from different game versions

The transfer of a pokemon between 2 games is 'import based' so I need to get a TimeMachine from the game that should receives the pokemon, because the game from which I get the pokemon shouldn't know knothing about the conversion mechanism

Ex:

  • From Gen1 to Gen2 I need a Gen2 TimeMachine and a Gen1 Pokemon instance.
  • From gen2 to Gen1 I need a Gen1 TimeMachine and a Gen2 Pokemon instance.
interface TimeMachine {
    fun isTransferAllowed(from: Version): Boolean

    /**
     * Return a new [Pokemon] instance derived from [pokemon] 
     * or null if the Pokémon is incompatible with this TimeMachine
     */
    fun transfer(pokemon: Pokemon): Pokemon?
}

interface SaveData {
    val timeMachine: TimeMachine
}

object Gen2TimeMachine : TimeMachine {
    override fun isTransferAllowed(from: Version) = from.generation == 1

    override fun transfer(pokemon: Pokemon): Pokemon? {
        if(!isTransferAllowed(pokemon.version)) return null

        return Gen2MutablePokemon().apply { 
            // apply all the pokemon values
            mutator
                .speciesId(pokemon.speciesId)
                .level(pokemon.level) // etc...
                .heldItem(getItemFromGen1CatchRate(pokemon.catchRate))
        }
    }
}

Inventory: Bulk Operations

Maximize current items quantity

fun Inventory.applyToCurrentItems(quantity: Int = maxAllowedQuantity) {
    val reusableItem = object : Inventory.Item {
        override var index = 0
        override var id = 0
        override val quantity = quantity
    }
    val mutateItem = fun (index: Int, id: Int, quantity: Int) -> Inventory.Item {  
        reusableItem.index = index
        reusableItem.id = id
        return reusableItem
    }
    for (index in 0 until itemCounts) {
        setItem(selectItem(index, mapTo = mutateItem))
    }
}

Give max quantity of all items

fun Inventory.giveAllItems() {
    val itemsCount = supportedItemIds.size.coerceAtMost(capacity)
    val reusableItem = object : Inventory.Item {
        override var index = 0
        override var id = 0
        override val quantity = maxAllowedQuantity
    }
    for (i in 0 until maxSupportedCount) {
        reusableItem.index = i
        reusableItem.id = supportedItemIds[i]
        setItem(reusableItem)
    }
}

Sort items by id

fun Inventory.sortItemsById() {
    val items = MutableList(size) { getItem(it) }
    items.sortBy { it.id }
    items.forEachIndexed { index, item ->
        setItem(item, index)
    }
}

Introduce Storage System

Replace StorageCollection with StorageSystem

/**
 * A collection of [Storage] with a fixed capacity given by [storageIndices] size.
 */
interface StorageSystem {
    /**
     * Indices of the [Storage] containted in the system.
     */
    val storageIndices: IntRange

    /**
     * Return a [Storage] instance.
     *
     * Should throw an [IllegalStateException] if [index] isn't contained in [storageIndices]
     */
    operator fun get(index: Int): Storage

    /**
     * Replaces the storage at the specified [index] with the specified [storage].
     *
     * Should throw an [IllegalStateException] if [index] isn't contained in [storageIndices]
     */
    operator fun set(index: Int, storage: Storage)
}

Support Inventory

  1. Each game use different ids for the inventory items
  2. Each id can have a different string representation based on the game version
interface InventoryResources {
    fun getItems(version: Version): List<String>
    fun getTypeName(type: Inventory.Type) : String
}
interface Inventory {
    /**
     * Every game supports a limited set of [Type]s
     * @see [SaveData.supportedInventoryTypes]
     */
    val type: Type

    enum class Type {
        General,
        Computer,
        Balls,
        Keys
    }

    /**
     * List of ids that an Inventory instance recognizes
     */
    val supportedItemIds: List<Int>

    /**
     * It is the value to which all [Item.quantity] are coerced
     */
    val maxAllowedQuantity: Int

    val maxAllowedItemCounts: Int

    val itemCounts: Int

    fun getItem(index: Int): Item

    /**
     * Should throw an [IllegalStateException] if [item] id isn't included in [supportedItemIds]
     */
    fun setItem(item: Item)

    data class Item(
        val index: Int,
        val id: Int,
        val quantity: Int
    )
}

interface SaveData {

    val supportedInventoryTypes: List<Inventory.Type>

    /**
     * Return null if [type] is not supported
    */
    fun getInventory(type: Inventory.Type) : Inventory?
}

Pokemon templates

interface PokemonTemplate {
    val name: String
    val description: String
    val speciesId: Int
    fun apply(pokemon: MutablePokemon)
}

class MewTemplate(trainer: Trainer) : PokemonTemplate {
    override val name = "Empty Pokemon"
    override val description = "An empty template for R/B/Y games"
    override val speciesId = 151
    override fun apply(pokemon: MutablePokemon) {
        pokemon.mutator
            .speciesId(151)
            .nickname("TEMPLATE")
            .trainer(trainer) // etc...
    }
}

Support Pokérus

Pokerus has been introduced in Gen 2 games. See #2

Pokerus info is stored in a byte. The upper 4 bits of the byte represent the specific strain of the Pokérus. The lower 4 bits represent the number of days remaining before the infected Pokémon is cured of the virus.

data class Pokerus (val strain: Int, val days: Int) {
    val isCured: Boolean = strain != 0 && days == 0

    companion object {
        val StrainValues: IntRange = 1..15 // 0 means never infected
        fun maxAllowedDays(strain: Int): Int = strain rem 4 + 1
        val NeverInfected = Pokerus(strain = 0, days = 0)
    }
}

interface Pokemon {
    val pokerus: Pokerus
}

interface Mutator {
    fun pokerus(value: Pokerus): Mutator
}

Support future Pokemon properties

Many properties have been added from one generation to the next. I don't want to create specific pokemon implementation for each gen.

sealed class Property<out T> {
    object Nothing: Property<Nothing>()
    data class Value<T>(val value: T): Property<T>()
}

inline fun <T> T.asProperty(): Property<T> = Property.Value(this)
inline fun <T> Property<T>.valueOrNull(): Property.Value<T>? = this as? Property.Value<T>

interface Pokemon {
    val heldItem: Property<Int> 
        get() = Property.Nothing
}

internal class Gen2Pokemon: Pokemon {
    override val heldItem = itemId.asProperty()
}

// From UI
@Composable
fun ShowInfo(pokemon: Pokemon) {
    pokemon.heldItem.valueOrNull()?.let {
        ShowHeldItem(it.value)
    }
}

Support different game languages

  • Create an enum class with all the supported languages
enum class Language {
    English,
    Italian,
    German,
    French,
    Spanish,
    Korean,
    Japanese
}
  • Add a language property to SaveData and Pokemon
interface SaveData {
    val language: Language
}

interface Pokemon {
    val language: Language
}

val Version.compatibleLanguages: List<Language>

// gen 1 & 2 - language is not stored anywhere
fun guessPokemonLanguage(): Language

Support pokemon with multiple forms

interface Pokemon {
    val form: Form?
    
    sealed class Form {
        /**
         * Accepted values for [letter] are A-Z, !, ?. 
         * [letter] is case-insensitive.
         */
        data class Unown(val letter: String) : Form()

        /**
         * Giratina has 2 forms: altered and origin
         */
        data class Giratina(val isAltered: Boolean): Form()
    }
}

fun getAllowedFormNames(pokemon: Pokemon): List<String> {
    return when(pokemon.form) {
        is Pokemon.Form.Unown -> listOf("A", "B", "C", ...)
        is Pokemon.Form.Giratina -> listOf("Altered", "Origin")
        else -> emptyList()
    }
}

fun setForm(pokemon: MutablePokemon, formName: String) {
    if(formName.isEmpty) return
    val currentForm = pokemon.form ?: return
    val newForm = when(currentForm) {
        is Pokemon.Form.Unown -> currentForm.copy(letter = formName)
        is Pokemon.Form.Giratina -> currentForm.copy(isAltered = formName == "Altered")
    }
    pokemon.mutator.form(newForm)
}
            

Load Pokemon from files

Create an immutable Pokemon instance from a file which has been converted to a UByteArray. The machism is the same used by SaveData.Factory

interface Pokemon {
    interface Factory {
        /**
         * Create a [Pokemon] instance from an array of bytes 
         * or null it it doesn't rapresent a valid pokemon.

         */
        fun create(data: UByteArray, position: Position): Pokemon?
    }
}

Related to #1

Add Pokemon.isNicknamed property

This issue depends on #32

Gen 1 & 2 don't save that information in the Pokemon data structure, we must infer it from the translated species name and the actual Pokemon nickname

// in pokemon:resources
fun getTranslatedSpeciesName(language: Language): String?

// gen 1 & 2
val Pokemon.isNicknamed: Boolean
    /// the result of this computation should be memorised for performance reasons
    get() = getTranslatedSpeciesName(language).uppercase() != nickname

Remove Property sealed class

Replace Property with nullable `properties. Property.Nothing would correspond to null

// before
interface Pokemon {
    val heldItem: Property<Int> get() = Property.Nothing
    val pokerus: Property<Pokerus> get() = Property.Nothing
}

// after
interface Pokemon {
    val heldItem: Int? get() = null
    val pokerus: Pokerus? get() = null
}

Pokedex: bulk operations

// to catch all -> pokedex.catch(species = 1..pokemonCounts)
fun Pokedex.catch(species: IntRange) {
    val reusableEntry = object : Pokedex.Entry {
        override val isSeen: Boolean = true
        override val isOwned: Boolean = true
        override var speciesId: Int = 0
    }
    for (i in species) {
        reusableEntry.speciesId = i
        setEntry(reusableEntry)
    }
}

// to see all -> pokedex.see(species = 1..pokemonCounts)
fun Pokedex.see(species: IntRange) {
    val reusableEntry = object : Pokedex.Entry {
        override val isSeen: Boolean = true
        override var isOwned: Boolean = false
        override var speciesId: Int = 0
    }
    for (i in species) {
        reusableEntry.speciesId = i
        reusableEntry.isOwned = getEntry(i).isOwned
        setEntry(reusableEntry)
    }
}

Rename Pokemon.Position properties

interface Pokemon {
    data class Position(
        /**
         * The index of the [Storage] in the [StorageCollection].
         * @see StorageCollection.getStorage
         */
        val storageIndex: Int,

        /**
         * The index of the [Pokemon] in the [Storage].
         * @see Storage.get
         */
        val pokemonIndex: Int,
    )
}

GSC Inventory for key items is bugged

In G/S/C games the inventory for the key items doesn't store the item count so its structure is different from the other inventory types.

  • First BYTE Total count
  • 1 BYTE from 0x1 to 0x1A Item ids
  • 0xFF after item ids Padding/Terminator

Edit Trainer Info

data class Trainer(
    val name: String,
    val visibleId: Int,
    val secretId: Int
)

interface SaveData {
    var trainer: Trainer
}

interface Pokemon {
    val trainer: Trainer
}

interface MutablePokemon {
    interface Mutator {
        fun trainer(value: Trainer): Mutator
    }
}

fun example(pokemon: MutablePokemon, saveData: SaveData) {
    val trainer = saveData.trainer
    // replace only the trainer name
    pokemon.mutator.trainer(pokemon.trainer.copy(name = trainer.name))
}

Support caught data

Needed information

  • Met level
  • Met location
  • Met date (Since Generation IV)
  • Time of day (Morning, Day, Night. Exclusive to Pokemon Crystal)
  • Pokeball (Since Generation III)
data class CaughtInfo(
    val level: Int,
    val locationId: Int,
    val time: Time,
    val ballId: Int
)

sealed class Time {
    data class Date(val time: OffsetDateTime): Time()
    /**
     * Accepted values for [time] are [Morning], [Day] and [Night]
     */
    data class DayTime(val time: Int) : Time() {
        companion object {
            const val Morning = 0
            const val Day = 1
            const val Night = 2
        }
    }
}

interface SaveData {
    val supportedLocationIds: List<Int>
}

Support Shiny Pokemon

Support shiny pokemon adding a boolean property to the Pokemon interface

interface Pokemon {
    val isShiny: Boolean
}

interface Mutator {
    fun shiny(value: Boolean): Mutator
}

Introduce Bag

Extract inventories management from SaveData to Bag

interface Bag {
    val inventoryTypes: Set<Inventory.Type>
    operator fun get(type: Inventory.Type): Inventory
}

interface SaveData {
    val bag: Bag
}

Simplify Storage API

Things to do:

  • Replace Storage.getMutablePokemon(slot) usages with Storage.getPokemon(slot).toMutablePokemon()
  • Use indexed access operator to get and set a pokemon
  • Rename deletePokemon to remoteAt and returns the removed pokemon
  • Remove Storage.index
interface Storage {
    /**
     * Return a [Pokemon] instance. 
     * @see Pokemon.toMutablePokemon
     *
     * Should throw an [IllegalStateException] if [index] isn't lower than [capacity]
     */
    operator fun get(index: Int): Pokemon
}

interface MutableStorage : Storage {
    /**
     * Replaces the pokemon at the specified [index] in this storage with the specified [pokemon].
     *
     * Should throw an [IllegalStateException] if [index] isn't lower than [capacity]
     */
    operator fun set(index: Int, pokemon: Pokemon)

    /**
     * Removes a pokemon at the specified [index] from the storage.
     * Return the pokemon that has been removed.
     *
     * Should throw an [IllegalStateException] if [index] isn't lower than [capacity]
     */
    fun removeAt(index: Int): Pokemon
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.