Repository: keluokeda/hs_tracker
Branch: master
Commit: b4c42c2ecf38
Files: 279
Total size: 562.9 KB
Directory structure:
gitextract_40etbzh8/
├── .gitignore
├── 123456
├── LICENSE
├── README.md
├── app/
│ ├── .gitignore
│ ├── build.gradle
│ ├── proguard-rules.pro
│ ├── release/
│ │ ├── app-1-3-1.apk
│ │ └── output-metadata.json
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── com/
│ └── ke/
│ └── hs_tracker/
│ └── app/
│ ├── App.kt
│ └── MainActivity.kt
├── build.gradle
├── core/
│ ├── .gitignore
│ ├── build.gradle
│ └── src/
│ └── main/
│ └── java/
│ └── com/
│ └── ke/
│ └── hs_tracker/
│ └── core/
│ ├── api/
│ │ └── HearthStoneJsonApi.kt
│ ├── entity/
│ │ ├── BlockType.kt
│ │ ├── Card.kt
│ │ ├── CardClass.kt
│ │ ├── CurrentDeck.kt
│ │ ├── Entity.kt
│ │ ├── GameCardType.kt
│ │ ├── InsertStackResult.kt
│ │ ├── LogType.kt
│ │ ├── Mechanics.kt
│ │ ├── NestedTag.kt
│ │ ├── PowerTag.kt
│ │ └── Zone.kt
│ ├── extensions.kt
│ └── parser/
│ ├── BlockTagStack.kt
│ ├── DeckFileObserver.kt
│ ├── PowerFileObserver.kt
│ └── PowerParser.kt
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── libs.versions.toml
├── module/
│ ├── .gitignore
│ ├── build.gradle
│ ├── consumer-rules.pro
│ ├── proguard-rules.pro
│ ├── schemas/
│ │ └── com.ke.hs_tracker.module.db.Database/
│ │ ├── 1.json
│ │ ├── 2.json
│ │ ├── 3.json
│ │ └── 4.json
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── com/
│ │ └── ke/
│ │ └── hs_tracker/
│ │ └── module/
│ │ └── ExampleInstrumentedTest.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── assets/
│ │ │ ├── Decks.log
│ │ │ ├── Power.log
│ │ │ └── log.config
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── ke/
│ │ │ └── hs_tracker/
│ │ │ └── module/
│ │ │ ├── MainApplication.kt
│ │ │ ├── api/
│ │ │ │ └── HearthStoneJsonApi.kt
│ │ │ ├── data/
│ │ │ │ └── PreferenceStorage.kt
│ │ │ ├── db/
│ │ │ │ ├── CardClassesConvert.kt
│ │ │ │ ├── CardDao.kt
│ │ │ │ ├── Database.kt
│ │ │ │ ├── Game.kt
│ │ │ │ ├── GameDao.kt
│ │ │ │ ├── MechanicsListConvert.kt
│ │ │ │ ├── ZonePositionChangedEvent.kt
│ │ │ │ └── ZonePositionChangedEventDao.kt
│ │ │ ├── di/
│ │ │ │ ├── CoroutinesModule.kt
│ │ │ │ ├── CoroutinesQualifiers.kt
│ │ │ │ ├── LogFileDirQualifiers.kt
│ │ │ │ └── Module.kt
│ │ │ ├── domain/
│ │ │ │ ├── ClearCardTableUseCase.kt
│ │ │ │ ├── GetAllCardUseCase.kt
│ │ │ │ ├── GetCardListUseCase.kt
│ │ │ │ ├── GetDatabaseCardCountUseCase.kt
│ │ │ │ ├── GetLocalLogDirUseCase.kt
│ │ │ │ ├── GetRealLogDirUseCase.kt
│ │ │ │ ├── GetSaveLogFileEnableUseCase.kt
│ │ │ │ ├── InsertCardListToDatabaseUseCase.kt
│ │ │ │ ├── ParseDeckCodeUseCase.kt
│ │ │ │ ├── SaveLogFileUseCase.kt
│ │ │ │ ├── SetSaveLogFileEnableUseCase.kt
│ │ │ │ └── WriteLogConfigFileUseCase.kt
│ │ │ ├── entity/
│ │ │ │ ├── BlockType.kt
│ │ │ │ ├── Card.kt
│ │ │ │ ├── CardBean.kt
│ │ │ │ ├── CardClass.kt
│ │ │ │ ├── CardType.kt
│ │ │ │ ├── CurrentDeck.kt
│ │ │ │ ├── Entity.kt
│ │ │ │ ├── EntityWithPayload.kt
│ │ │ │ ├── EnumMoshiAdapter.kt
│ │ │ │ ├── FormatType.kt
│ │ │ │ ├── GameCardType.kt
│ │ │ │ ├── GameEvent.kt
│ │ │ │ ├── GameType.kt
│ │ │ │ ├── GraveyardCard.kt
│ │ │ │ ├── InsertStackResult.kt
│ │ │ │ ├── LogType.kt
│ │ │ │ ├── Mechanics.kt
│ │ │ │ ├── NestedTag.kt
│ │ │ │ ├── PowerTag.kt
│ │ │ │ ├── Race.kt
│ │ │ │ ├── Rarity.kt
│ │ │ │ ├── SpellSchool.kt
│ │ │ │ ├── Turn.kt
│ │ │ │ ├── Zone.kt
│ │ │ │ └── ZoneCard.kt
│ │ │ ├── parser/
│ │ │ │ ├── BlockTagStack.kt
│ │ │ │ ├── DeckCardObserver.kt
│ │ │ │ ├── DeckFileObserver.kt
│ │ │ │ ├── PowerFileObserver.kt
│ │ │ │ ├── PowerParser.kt
│ │ │ │ └── PowerTagHandler.kt
│ │ │ ├── service/
│ │ │ │ ├── ItemViewTouchListener.kt
│ │ │ │ ├── ScaleTouchListener.kt
│ │ │ │ └── WindowService.kt
│ │ │ └── ui/
│ │ │ ├── chart/
│ │ │ │ ├── GetSummaryChartViewDataUseCase.kt
│ │ │ │ ├── PieChartData.kt
│ │ │ │ ├── SummaryChartActivity.kt
│ │ │ │ └── SummaryChartViewData.kt
│ │ │ ├── classbattledetail/
│ │ │ │ ├── ClassBattleDetailActivity.kt
│ │ │ │ ├── ClassBattleDetailViewModel.kt
│ │ │ │ ├── ClassBattleItem.kt
│ │ │ │ └── GetClassBattleItemListUseCase.kt
│ │ │ ├── common/
│ │ │ │ ├── CardAdapter.kt
│ │ │ │ └── LoadingFragment.kt
│ │ │ ├── deck/
│ │ │ │ ├── DeckCodeParserActivity.kt
│ │ │ │ └── DeckCodeParserViewModel.kt
│ │ │ ├── deckbattledetail/
│ │ │ │ ├── BattleRecordsFragment.kt
│ │ │ │ ├── BattleRecordsViewModel.kt
│ │ │ │ ├── DeckBattleDetailActivity.kt
│ │ │ │ ├── DeckDetailFragment.kt
│ │ │ │ ├── DeckDetailViewModel.kt
│ │ │ │ ├── DeckFragment.kt
│ │ │ │ ├── DeckViewModel.kt
│ │ │ │ ├── GetGamesByDeckCodeAndNameUseCase.kt
│ │ │ │ ├── SummaryFragment.kt
│ │ │ │ └── SummaryViewModel.kt
│ │ │ ├── diagnose/
│ │ │ │ └── DiagnoseActivity.kt
│ │ │ ├── filter/
│ │ │ │ └── FilterActivity.kt
│ │ │ ├── main/
│ │ │ │ ├── CardListFragment.kt
│ │ │ │ ├── DeckCardListFragment.kt
│ │ │ │ ├── GraveyardFragment.kt
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── MainViewModel.kt
│ │ │ │ ├── OpponentGraveyardFragment.kt
│ │ │ │ ├── OpponentHandCardsFragment.kt
│ │ │ │ └── UserGraveyardFragment.kt
│ │ │ ├── migrate/
│ │ │ │ ├── MigrateDataConvert.kt
│ │ │ │ ├── MigrateMainActivity.kt
│ │ │ │ ├── SocketClientActivity.kt
│ │ │ │ └── SocketServerActivity.kt
│ │ │ ├── permissions/
│ │ │ │ ├── PermissionsActivity.kt
│ │ │ │ └── PermissionsViewModel.kt
│ │ │ ├── records/
│ │ │ │ ├── RecordAdapter.kt
│ │ │ │ ├── RecordsActivity.kt
│ │ │ │ └── RecordsViewModel.kt
│ │ │ ├── settings/
│ │ │ │ ├── SettingsActivity.kt
│ │ │ │ └── SettingsViewModel.kt
│ │ │ ├── splash/
│ │ │ │ ├── SplashActivity.kt
│ │ │ │ └── SplashViewModel.kt
│ │ │ ├── summary/
│ │ │ │ ├── BattleRateItem.kt
│ │ │ │ ├── BattleRateItemAdapter.kt
│ │ │ │ ├── BattleRateListFragment.kt
│ │ │ │ ├── BattleRateListViewModel.kt
│ │ │ │ ├── DeckBattleRateListViewModel.kt
│ │ │ │ ├── GetDeckBattleRateListUseCase.kt
│ │ │ │ ├── GetHeroBattleRateListUseCase.kt
│ │ │ │ ├── HeroBattleRateListViewModel.kt
│ │ │ │ ├── RateByDeckFragment.kt
│ │ │ │ ├── RateByHeroFragment.kt
│ │ │ │ └── SummaryActivity.kt
│ │ │ ├── support/
│ │ │ │ └── SupportActivity.kt
│ │ │ ├── sync/
│ │ │ │ ├── SyncCardDataActivity.kt
│ │ │ │ └── SyncCardDataViewModel.kt
│ │ │ ├── test/
│ │ │ │ ├── CreateRecordActivity.kt
│ │ │ │ ├── LocalFileParserActivity.kt
│ │ │ │ └── TestActivity.kt
│ │ │ ├── theme/
│ │ │ │ └── ThemeActivity.kt
│ │ │ ├── writeconfig/
│ │ │ │ └── WriteConfigActivity.kt
│ │ │ ├── zonecards/
│ │ │ │ └── ZoneCardsActivity.kt
│ │ │ └── zoneevents/
│ │ │ ├── ListModeFragment.kt
│ │ │ ├── ZoneEventsActivity.kt
│ │ │ └── ZoneEventsViewModel.kt
│ │ └── res/
│ │ ├── color/
│ │ │ └── module_game_state.xml
│ │ ├── drawable/
│ │ │ ├── module_baseline_arrow_back_white_24dp.xml
│ │ │ ├── module_baseline_clear_black_24dp.xml
│ │ │ ├── module_baseline_clear_red_500_24dp.xml
│ │ │ ├── module_baseline_done_black_24dp.xml
│ │ │ ├── module_baseline_done_green_500_24dp.xml
│ │ │ ├── module_baseline_done_white_24dp.xml
│ │ │ ├── module_baseline_drag_handle_white_24dp.xml
│ │ │ ├── module_baseline_keyboard_arrow_right_grey_500_24dp.xml
│ │ │ ├── module_baseline_pie_chart_white_24dp.xml
│ │ │ ├── module_baseline_play_arrow_white_24dp.xml
│ │ │ ├── module_baseline_settings_white_24dp.xml
│ │ │ ├── module_baseline_sync_white_24dp.xml
│ │ │ └── module_baseline_zoom_out_map_white_24dp.xml
│ │ ├── drawable-xxxhdpi/
│ │ │ └── module_bg_splash.xml
│ │ ├── layout/
│ │ │ ├── module_activity_class_battle_detail.xml
│ │ │ ├── module_activity_create_record.xml
│ │ │ ├── module_activity_deck_battle_detail.xml
│ │ │ ├── module_activity_deck_code_parser.xml
│ │ │ ├── module_activity_diagnose.xml
│ │ │ ├── module_activity_filter.xml
│ │ │ ├── module_activity_local_file_parser.xml
│ │ │ ├── module_activity_main.xml
│ │ │ ├── module_activity_migrate_main.xml
│ │ │ ├── module_activity_permissions.xml
│ │ │ ├── module_activity_records.xml
│ │ │ ├── module_activity_settings.xml
│ │ │ ├── module_activity_socket_client.xml
│ │ │ ├── module_activity_socket_server.xml
│ │ │ ├── module_activity_summary.xml
│ │ │ ├── module_activity_summary_chart.xml
│ │ │ ├── module_activity_support.xml
│ │ │ ├── module_activity_sync_card_data.xml
│ │ │ ├── module_activity_test.xml
│ │ │ ├── module_activity_theme.xml
│ │ │ ├── module_activity_write_config.xml
│ │ │ ├── module_activity_zone_cards.xml
│ │ │ ├── module_activity_zone_events.xml
│ │ │ ├── module_dialog_card_preview.xml
│ │ │ ├── module_floating_window.xml
│ │ │ ├── module_fragment_card_list.xml
│ │ │ ├── module_fragment_deck_battle_detail_deck_detail.xml
│ │ │ ├── module_fragment_deck_battle_detail_summary.xml
│ │ │ ├── module_fragment_graveyard.xml
│ │ │ ├── module_fragment_list_mode.xml
│ │ │ ├── module_fragment_loading.xml
│ │ │ ├── module_fragment_opponent_hand_cards.xml
│ │ │ ├── module_header_summary.xml
│ │ │ ├── module_item_card.xml
│ │ │ ├── module_item_chip_filter.xml
│ │ │ ├── module_item_class_battle_detail.xml
│ │ │ ├── module_item_footer_with_fab.xml
│ │ │ ├── module_item_opponent_hand_card.xml
│ │ │ ├── module_item_record.xml
│ │ │ ├── module_item_summary_battle.xml
│ │ │ ├── module_item_text.xml
│ │ │ ├── module_item_zone_card.xml
│ │ │ └── module_item_zone_event_list_mode.xml
│ │ ├── values/
│ │ │ ├── arrays.xml
│ │ │ ├── colors.xml
│ │ │ ├── dimens.xml
│ │ │ ├── strings.xml
│ │ │ └── themes.xml
│ │ ├── values-night/
│ │ │ └── themes.xml
│ │ └── values-zh-rCN/
│ │ └── strings.xml
│ └── test/
│ └── java/
│ └── com/
│ └── ke/
│ └── hs_tracker/
│ └── module/
│ └── ExampleUnitTest.kt
├── settings.gradle
├── shared/
│ ├── .gitignore
│ ├── build.gradle
│ └── src/
│ └── main/
│ └── java/
│ └── com/
│ └── ke/
│ └── hs/
│ └── shared/
│ └── entity/
│ └── CardType.kt
├── simulator/
│ ├── .gitignore
│ ├── build.gradle
│ ├── consumer-rules.pro
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── com/
│ │ └── ke/
│ │ └── hs/
│ │ └── simulator/
│ │ └── ExampleInstrumentedTest.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── com/
│ │ └── ke/
│ │ └── hs/
│ │ └── simulator/
│ │ └── cards/
│ │ └── base/
│ │ ├── HeroCard.kt
│ │ ├── ICard.kt
│ │ ├── MinionCard.kt
│ │ ├── SpellCard.kt
│ │ └── WeaponCard.kt
│ └── test/
│ └── java/
│ └── com/
│ └── ke/
│ └── hs/
│ └── simulator/
│ └── ExampleUnitTest.kt
└── writer/
├── .gitignore
├── build.gradle
├── proguard-rules.pro
├── release/
│ ├── output-metadata.json
│ └── writer-release.apk
└── src/
├── androidTest/
│ └── java/
│ └── com/
│ └── ke/
│ └── hs/
│ └── writer/
│ └── ExampleInstrumentedTest.kt
├── main/
│ ├── AndroidManifest.xml
│ └── res/
│ ├── drawable/
│ │ └── ic_launcher_background.xml
│ ├── drawable-v24/
│ │ └── ic_launcher_foreground.xml
│ ├── mipmap-anydpi-v26/
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ ├── values/
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ └── values-night/
│ └── themes.xml
└── test/
└── java/
└── com/
└── ke/
└── hs/
└── writer/
└── ExampleUnitTest.kt
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2023 keluokeda
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# hs_tracker
## Android14 use this https://github.com/keluokeda/Hs
An automatic Hearthstone tracker for Android
#### 炉石传说记牌器,支持Android12、Android13


### QQ群:825215274
================================================
FILE: app/.gitignore
================================================
/build
================================================
FILE: app/build.gradle
================================================
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
id 'com.google.dagger.hilt.android'
id 'kotlin-parcelize'
}
kapt {
correctErrorTypes true
}
android {
compileSdk libs.versions.compilesdk.get().toInteger()
defaultConfig {
applicationId "com.ke.hs_tracker.app"
minSdk libs.versions.minsdk.get().toInteger()
targetSdk libs.versions.targetsdk.get().toInteger()
versionCode 31
versionName "1.3.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildFeatures {
viewBinding = true
}
signingConfigs {
myConfig {
storeFile file(RELEASE_FILE)
storePassword RELEASE_storePassword
keyAlias RELEASE_keyAlias
keyPassword RELEASE_keyPassword
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled false
zipAlignEnabled true
signingConfig signingConfigs.myConfig
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
//signingConfig signingConfigs.config
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
namespace 'com.ke.hs_tracker.app'
}
dependencies {
implementation project(path: ':module')
implementation(libs.core.ktx)
implementation(libs.appcompat)
implementation(libs.material)
implementation(libs.constraint.layout)
implementation(libs.fragment.ktx)
implementation(libs.activity)
implementation(libs.lifecycle.viewmodel.ktx)
implementation(libs.lifecycle.livedata.ktx)
implementation(libs.lifecycle.runtime.ktx)
implementation(libs.support.v4)
implementation(libs.logger)
implementation(libs.hilt.android)
kapt(libs.hilt.compiler)
implementation(libs.ke.mvvm)
implementation(libs.moshi)
kapt(libs.moshi.codegen)
implementation(libs.hilt.android)
kapt(libs.hilt.compiler)
}
================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: app/release/output-metadata.json
================================================
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.ke.hs_tracker.app",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 31,
"versionName": "1.3.1",
"outputFile": "app-release.apk"
}
],
"elementType": "File"
}
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
================================================
FILE: app/src/main/java/com/ke/hs_tracker/app/App.kt
================================================
package com.ke.hs_tracker.app
import com.ke.hs_tracker.module.MainApplication
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class App : MainApplication()
================================================
FILE: app/src/main/java/com/ke/hs_tracker/app/MainActivity.kt
================================================
package com.ke.hs_tracker.app
//
//import android.os.Bundle
//import androidx.appcompat.app.AppCompatActivity
//import androidx.documentfile.provider.DocumentFile
//import androidx.lifecycle.lifecycleScope
//import com.ke.hs_tracker.app.databinding.ActivityMainBinding
//import com.ke.hs_tracker.module.findHSDataFilesDir
//import com.ke.hs_tracker.module.log
//import kotlinx.coroutines.Dispatchers
//import kotlinx.coroutines.launch
//import kotlinx.coroutines.withContext
//
//class MainActivity : AppCompatActivity() {
//
//
// private lateinit var powerLogFile: DocumentFile
//
// private lateinit var binding: ActivityMainBinding
//
//// private lateinit var inputStream: InputStream
//
// private var oldLogSize = 0L
//
// override fun onCreate(savedInstanceState: Bundle?) {
// super.onCreate(savedInstanceState)
// binding = ActivityMainBinding.inflate(layoutInflater)
// setContentView(binding.root)
//
//
//
// binding.init.setOnClickListener {
// lifecycleScope.launch {
//
// oldLogSize = 0
// val logsDir = findHSDataFilesDir("Logs")
//
// val file = logsDir?.findFile("Power.log")
// if (file != null) {
// "初始化成功 $file".log()
// powerLogFile = file
// } else {
// "初始化失败".log()
// }
//
//
// }
// }
//
// binding.delete.setOnClickListener {
// try {
// "删除本地日志结果 ${deleteFile("power.log")}".log()
//
// val result = powerLogFile.delete()
// "删除文件结果 $result".log()
// } catch (e: Exception) {
//// binding.content.text = e.message
// "删除文件失败".log()
// e.printStackTrace()
// }
// }
//
// binding.load.setOnClickListener {
//
// lifecycleScope.launch {
// try {
//
//
// contentResolver.openInputStream(powerLogFile.uri)!!.reader()
// .apply {
// if (oldLogSize > 0) {
// val skip = skip(oldLogSize)
// "跳过的字节数量 $skip".log()
// }
// binding.content.text = readText().also {
// oldLogSize += it.length
// writeToLocal(it)
// }
//
// close()
// }
//
// } catch (e: Exception) {
// binding.content.text = e.message
// }
// }
//
//
// }
// }
//
// private suspend fun writeToLocal(content: String) {
// withContext(Dispatchers.IO) {
// openFileOutput("power.log", MODE_APPEND)
// .writer().apply {
// append(content)
// flush()
// close()
// }
// }
// }
//
//
//// private suspend fun getPowerLogFileSize(): Long {
//// return withContext(Dispatchers.IO) {
//// try {
//// contentResolver.query(powerLogFile.uri, arrayOf(OpenableColumns.SIZE), null, null)
//// ?.apply {
//// moveToFirst()
//// return@withContext getLong(0).also {
//// close()
//// }
////
//// }
//// } catch (e: Exception) {
//// e.printStackTrace()
//// return@withContext 0
//// }
////
////
////
//// 0
//// }
//// }
//}
================================================
FILE: build.gradle
================================================
plugins {
id 'com.android.application' version '8.0.2' apply false
id 'com.android.library' version '8.0.2' apply false
id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
id 'org.jetbrains.kotlin.jvm' version '1.8.0' apply false
id 'com.google.dagger.hilt.android' version '2.45' apply false
}
task clean(type: Delete) {
delete rootProject.buildDir
}
================================================
FILE: core/.gitignore
================================================
/build
================================================
FILE: core/build.gradle
================================================
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
id 'kotlin-kapt'
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
dependencies {
implementation(libs.retrofit)
implementation(libs.retrofit.converter.moshi)
implementation(libs.moshi)
kapt(libs.moshi.codegen)
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
// implementation("com.squareup.moshi:moshi-kotlin:1.13.0")
}
================================================
FILE: core/src/main/java/com/ke/hs_tracker/core/api/HearthStoneJsonApi.kt
================================================
package com.ke.hs_tracker.core.api
import com.ke.hs_tracker.core.entity.Card
import retrofit2.http.GET
import retrofit2.http.Path
interface HearthStoneJsonApi {
/**
* 获取卡牌数据
*/
@GET("v1/{versionCode}/{region}/cards.json")
suspend fun getCardJsonList(
@Path("versionCode") versionCode: String,
@Path("region") region: String,
): List
companion object {
const val BASE_URL = "https://api.hearthstonejson.com/"
}
}
================================================
FILE: core/src/main/java/com/ke/hs_tracker/core/entity/BlockType.kt
================================================
package com.ke.hs_tracker.core.entity
enum class BlockType {
/**
* 攻击
*/
Attack,
/**
* 死亡
*/
Deaths,
/**
* 触发
*/
Trigger,
/**
* 打出一张卡牌
*/
Play,
/**
* 卡牌生效
*/
Power,
/**
* 交易
*/
Trade
}
internal fun String.toBlockType(fallback: BlockType = BlockType.Trigger): BlockType {
BlockType.values().forEach {
if (it.name.equals(this, true)) {
return it
}
}
return fallback
}
================================================
FILE: core/src/main/java/com/ke/hs_tracker/core/entity/Card.kt
================================================
package com.ke.hs_tracker.core.entity
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonClass
import com.squareup.moshi.ToJson
@JsonClass(generateAdapter = true)
data class Card(
val name: String,
val cost: Int = 0,
val id: String,
val dbfId: Int,
val text: String = "",
//属于哪个版本 例如 TGT
val set: String,
val type: CardType = CardType.None,
val cardClass: CardClass,
val classes: List = emptyList(),
val flavor: String,
val attach: Int = 0,
val health: Int = 0
)
enum class CardType {
/**
* 英雄
*/
Hero,
/**
* 英雄技能
*/
HeroPower,
/**
*衍生牌
*/
Enchantment,
/**
* 法术
*/
Spell,
/**
* 随从
*/
Minion,
/**
* 武器
*/
Weapon,
None
}
class CardTypeAdapter {
@FromJson
fun fromJson(value: String): CardType {
return CardType.values()
.find { it.name.equals(value.replace("_", ""), true) } ?: CardType.None
}
@ToJson
fun toJson(cardType: CardType) = cardType.name.uppercase()
}
================================================
FILE: core/src/main/java/com/ke/hs_tracker/core/entity/CardClass.kt
================================================
package com.ke.hs_tracker.core.entity
enum class CardClass {
/**
* 法师
*/
Mage,
/**
* 术士
*/
Warlock,
/**
* 牧师
*/
Priest,
/**
* 德鲁伊
*/
Druid,
Neutral
}
================================================
FILE: core/src/main/java/com/ke/hs_tracker/core/entity/CurrentDeck.kt
================================================
package com.ke.hs_tracker.core.entity
data class CurrentDeck(
val name: String,
val code: String
)
================================================
FILE: core/src/main/java/com/ke/hs_tracker/core/entity/Entity.kt
================================================
package com.ke.hs_tracker.core.entity
import com.ke.hs_tracker.core.parser.PowerParserImpl
//有三种形式
//1,Entity=[entityName=腐食研习 id=29 zone=PLAY zonePos=0 cardId=SCH_300 player=1]
//2,Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=29 zone=HAND zonePos=3 cardId= player=1]
//3,Entity=失落的裤子#5629
data class Entity(
val entityName: String,
val gameCardType: GameCardType? = null,
val id: Int = -1,
val zone: Zone = Zone.Play,
val zonePosition: Int = -1,
val cardId: String? = null,
val player: Int = -1
) {
val entityType: EntityType
get() = when {
gameCardType == null && id == -1 && zone == Zone.Play && zonePosition == -1 && cardId == null && player == -1 -> EntityType.Name
gameCardType == GameCardType.Invalid -> EntityType.Invalid
else -> EntityType.Clear
}
companion object {
internal fun createFromContent(content: String): Entity? {
if (content == "0") {
return null
}
var matchResult = PowerParserImpl.FULL_ENTITY_CONTENT1_PATTERN.matchEntire(content)
if (matchResult != null) {
return Entity(
matchResult.groupValues[1],
matchResult.groupValues[2].toCardType(GameCardType.Invalid),
matchResult.groupValues[3].toIntOrNull() ?: 0,
matchResult.groupValues[4].toZone(),
matchResult.groupValues[5].toIntOrNull() ?: 0,
matchResult.groupValues[6].ifBlank { null },
matchResult.groupValues[7].toIntOrNull() ?: 0
)
}
matchResult = PowerParserImpl.FULL_ENTITY_CONTENT2_PATTERN.matchEntire(content)
?: return Entity(content)
return Entity(
matchResult.groupValues[1],
GameCardType.Invalid,
matchResult.groupValues[2].toIntOrNull() ?: 0,
matchResult.groupValues[3].toZone(),
matchResult.groupValues[4].toIntOrNull() ?: 0,
matchResult.groupValues[5].ifBlank { null },
matchResult.groupValues[6].toIntOrNull() ?: 0
)
}
}
}
enum class EntityType {
//Entity=失落的裤子#5629
Name,
//Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=29 zone=HAND zonePos=3 cardId= player=1]
Invalid,
//Entity=[entityName=腐食研习 id=29 zone=PLAY zonePos=0 cardId=SCH_300 player=1]
Clear
}
data class GameEntity(
val gameCardType: GameCardType,
val entityId: Int
)
/**
* 玩家
*/
data class Player(
val entityId: Int,
val playerId: Int,
val controller: Int,
val gameCardType: GameCardType,
val heroEntity: Int,
/**
* 手牌上限
*/
val maxHandSize: Int,
/**
* 起始手牌
*/
val startHandSize: Int,
val teamId: Int,
/**
* 费用上限 一般为10
*/
val maxResources: Int
) {
companion object {
internal fun fromMap(map: Map): Player {
return Player(
entityId = map["entityid"]?.toIntOrNull() ?: 0,
playerId = map["playerid"]?.toIntOrNull() ?: 0,
controller = map["controller"]?.toIntOrNull() ?: 0,
gameCardType = (map["cardtype"] ?: "").toCardType(),
heroEntity = map["heroentity"]?.toIntOrNull() ?: 0,
maxHandSize = map["maxhandsize"]?.toIntOrNull() ?: 0,
startHandSize = map["starthandsize"]?.toIntOrNull() ?: 0,
teamId = map["teamid"]?.toIntOrNull() ?: 0,
maxResources = map["maxresources"]?.toIntOrNull() ?: 0
)
}
}
}
================================================
FILE: core/src/main/java/com/ke/hs_tracker/core/entity/GameCardType.kt
================================================
package com.ke.hs_tracker.core.entity
/**
* 卡牌类型
*/
enum class GameCardType {
Game,
/**
* 玩家
*/
Player,
/**
* 英雄
*/
Hero,
/**
* 英雄技能
*/
HeroPower,
/**
* 牌库中的牌的状态
*/
Invalid,
/**
* 随从身上的buff或战场上的buff(例如下一张法强怪法力值减少1)
*/
Enchantment,
/**
* 法术
*/
Spell,
/**
* 随从
*/
Minion
}
/**
* 字符串转 CardType类型
*/
internal fun String.toCardType(fallback: GameCardType = GameCardType.Game): GameCardType {
return GameCardType.values().find {
it.name.equals(this.replace("_", ""), true)
} ?: fallback
}
================================================
FILE: core/src/main/java/com/ke/hs_tracker/core/entity/InsertStackResult.kt
================================================
package com.ke.hs_tracker.core.entity
sealed interface InsertStackResult {
/**
* 插入成功
*/
object Success : InsertStackResult
/**
* 不能插入,例如不在Block内的TAG_CHANGE
*/
object CanNotInsert : InsertStackResult
/**
* 结束了
* @param powerTag tag
* @param handled 是否处理了本次log日志,例如 FULL_ENTITY - Updating 跟着一个 FULL_ENTITY - Updating的情况,handled就是true,表示已经处理了,不需要在进行处理;如果是FULL_ENTITY - Updating
* 跟着一个 TAG_CHANGE,就表示没有处理,需要调用这个方法的自行处理log数据
*/
data class Over(val powerTag: PowerTag, val handled: Boolean) : InsertStackResult
}
================================================
FILE: core/src/main/java/com/ke/hs_tracker/core/entity/LogType.kt
================================================
package com.ke.hs_tracker.core.entity
enum class LogType(val replace: String) {
PowerTaskList("PowerTaskList.DebugPrintPower() -"),
GameState("GameState.DebugPrintGame() -")
}
================================================
FILE: core/src/main/java/com/ke/hs_tracker/core/entity/Mechanics.kt
================================================
package com.ke.hs_tracker.core.entity
/**
* 类型
*/
enum class Mechanics {
/**
* 过载
*/
Overload,
/**
* 战吼
*/
Battlecry,
/**
* 嘲讽
*/
Taunt,
/**
* 突袭
*/
Rush,
/**
* 潜行
*/
Stealth,
/**
* 圣盾
*/
DivineShield,
/**
* 法强
*/
SpellPower,
/**
* 亡语
*/
Deathrattle,
/**
* 荣誉击杀
*/
hHonorableKill,
/**
* 法力迸发
*/
SpellBurst,
/**
* 腐蚀
*/
Corrupt,
/**
* 暴怒
*/
Frenzy,
/**
* 发现
*/
Discover,
/**
* 冻结
*/
Freeze,
/**
* 连击
*/
Combo,
/**
* 无法攻击
*/
CantAttack,
/**
* 触发
*/
TriggerVisual,
/**
* 奥秘
*/
Secret,
/**
* 交易
*/
Tradeable,
/**
* 激励
*/
Inspire,
/**
* 沉默
*/
Silence,
/**
* 风怒
*/
Windfury,
/**
* 回响
*/
Echo
}
================================================
FILE: core/src/main/java/com/ke/hs_tracker/core/entity/NestedTag.kt
================================================
package com.ke.hs_tracker.core.entity
internal sealed interface NestedTag {
object CreateGame : NestedTag
data class GameEntity(val id: Int) : NestedTag
data class Tag(val key: String, val value: String) : NestedTag {
fun toPair(): Pair = key to value
}
data class Player(val entityId: Int, val playerId: Int) : NestedTag
//FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=63 zone=DECK zonePos=0 cardId= player=2] CardID=
//FULL_ENTITY - Updating [entityName=全副武装! id=65 zone=PLAY zonePos=0 cardId=HERO_01bp player=1] CardID=HERO_01bp
data class FullEntity(
val entity: Entity
) : NestedTag
data class Block(
val blockType: BlockType,
val entity: Entity,
val target: Entity?
) : NestedTag
data class TagChange(
val entity: Entity,
val tag: String,
val value: String
) : NestedTag {
fun convert(): PowerTag.PowerTaskList.TagChange {
return PowerTag.PowerTaskList.TagChange(entity, tag, value)
}
}
data class ShowEntity(
val entity: Entity,
val cardId: String
) : NestedTag
object BlockEnd : NestedTag
}
//internal fun NestedTag.FullEntity.toUpdating(): PowerTag.PowerTaskList.FullEntity.Updating {
// return PowerTag.PowerTaskList.FullEntity.Updating(
// entityName, cardType, id, zone, zonePosition, cardId, player
// )
//}
================================================
FILE: core/src/main/java/com/ke/hs_tracker/core/entity/PowerTag.kt
================================================
package com.ke.hs_tracker.core.entity
sealed interface PowerTag {
sealed interface PowerTaskList : PowerTag {
/**
* 创建游戏
*/
//D 19:55:18.1257030 GameState.DebugPrintPower() - CREATE_GAME
//D 19:55:18.1257030 GameState.DebugPrintPower() - GameEntity EntityID=1
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=CARDTYPE value=GAME
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=ZONE value=PLAY
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=ENTITY_ID value=1
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=937 value=2
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=SPAWN_TIME_COUNT value=1
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=GAME_SEED value=1950487951
//D 19:55:18.1257030 GameState.DebugPrintPower() - Player EntityID=2 PlayerID=1 GameAccountId=[hi=144115211015832391 lo=191215280]
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=CONTROLLER value=1
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=CARDTYPE value=PLAYER
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=PLAYER_ID value=1
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=HERO_ENTITY value=64
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=MAXHANDSIZE value=10
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=STARTHANDSIZE value=4
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=TEAM_ID value=1
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=ZONE value=PLAY
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=ENTITY_ID value=2
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=MAXRESOURCES value=10
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=SPAWN_TIME_COUNT value=1
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=AVRANK value=336
//D 19:55:18.1257030 GameState.DebugPrintPower() - Player EntityID=3 PlayerID=2 GameAccountId=[hi=144115211015832391 lo=44511141]
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=CONTROLLER value=2
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=CARDTYPE value=PLAYER
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=PLAYER_ID value=2
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=HERO_ENTITY value=66
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=MAXHANDSIZE value=10
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=STARTHANDSIZE value=4
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=TEAM_ID value=2
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=ZONE value=PLAY
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=ENTITY_ID value=3
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=MAXRESOURCES value=10
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=SPAWN_TIME_COUNT value=1
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=AVRANK value=338
data class CreateGame(
val gameEntity: GameEntity,
val player1: Player,
val player2: Player
) : PowerTaskList
data class TagChange(
val entity: Entity,
val tag: String,
val value: String,
) : PowerTaskList {
/**
* 是否是游戏完成的标志
*/
val isGameComplete: Boolean =
entity.entityName == "GameEntity" && tag.equals("state", true) && value.equals(
"COMPLETE",
true
)
}
data class FullEntity(
val entity: Entity,
val payloads: MutableMap = mutableMapOf()
) : PowerTaskList {
fun append(value: Pair) {
payloads[value.first] = value.second
}
}
data class ShowEntity(
val entity: Entity,
val cardId: String,
val payloads: MutableMap = mutableMapOf()
) : PowerTaskList {
fun append(value: Pair) {
payloads[value.first] = value.second
}
}
data class Block(
val type: BlockType,
val entity: Entity,
val target: Entity?,
val list: List
) : PowerTaskList
}
sealed interface GameState : PowerTag {
data class BuildNumber(val number: String) : GameState
data class GameType(val type: String) : GameState
data class FormatType(val type: String) : GameState
data class ScenarioID(val id: String) : GameState
data class PlayerMapping(val id: Int, val name: String) : GameState {
/**
* 是否是先手
*/
val first: Boolean = id == 1
/**
* 是否是当前用户
*/
val isUser: Boolean = name != "UNKNOWN HUMAN PLAYER"
}
}
}
================================================
FILE: core/src/main/java/com/ke/hs_tracker/core/entity/Zone.kt
================================================
package com.ke.hs_tracker.core.entity
enum class Zone {
/**
* 战场
*/
Play,
/**
* 牌库
*/
Deck,
/**
* 发现的牌的位置
*/
SetAside,
/**
* 墓地 打出的法术牌和死亡的随从会进入
*/
Graveyard,
/**
* 手牌
*/
Hand
}
internal fun String.toZone(fallback: Zone = Zone.Deck): Zone {
return Zone.values().firstOrNull {
it.name.equals(this, true)
} ?: fallback
}
================================================
FILE: core/src/main/java/com/ke/hs_tracker/core/extensions.kt
================================================
package com.ke.hs_tracker.core
import com.ke.hs_tracker.core.parser.PowerParserImpl
import java.util.*
fun String.removeTime(): Triple {
val content = substring(PowerParserImpl.TIME_PREFIX_SIZE)
val start = substring(0, 1)
val hms = substring(2, 10).split(":")
val calendar = Calendar.getInstance()
calendar.set(
Calendar.HOUR_OF_DAY, hms[0].toInt()
)
calendar.set(
Calendar.MINUTE, hms[1].toInt()
)
calendar.set(
Calendar.SECOND, hms[2].toInt()
)
return Triple(
start,
calendar.time,
content
)
}
fun main(){
val text = "I 22:23:35.6401730 AAECAaoIBJzOA6beA8L2A9ySBA3buAPhzAPNzgPw1AOK5APq5wP67APk9gOF+gOogQSVkgT5nwT6nwQA"
println(text.removeTime().toString())
}
================================================
FILE: core/src/main/java/com/ke/hs_tracker/core/parser/BlockTagStack.kt
================================================
package com.ke.hs_tracker.core.parser
import com.ke.hs_tracker.core.entity.*
import java.lang.Exception
import java.util.*
import kotlin.text.lowercase
import kotlin.text.replace
import kotlin.text.toInt
interface BlockTagStack {
/**
* 插入一条日志
*/
fun insert(line: String): InsertStackResult
}
internal class BlockTagStackImpl : BlockTagStack {
private val nestedTagList = LinkedList()
override fun insert(line: String): InsertStackResult {
//CREATE_GAME
var matchResult = PowerParserImpl.CREATE_GAME_PATTERN.matchEntire(line)
if (matchResult != null) {
//游戏开始
nestedTagList.clear()
nestedTagList.add(NestedTag.CreateGame)
return InsertStackResult.Success
}
//GameEntity EntityID=1
matchResult = PowerParserImpl.GAME_ENTITY_PATTERN.matchEntire(line)
if (matchResult != null) {
val entityId = matchResult.groupValues[1].toInt()
nestedTagList.add(NestedTag.GameEntity(entityId))
return InsertStackResult.Success
}
//Player EntityID=2 PlayerID=1 GameAccountId=[hi=144115211015832391 lo=191215280]
matchResult = PowerParserImpl.PLAYER_ENTITY_PATTERN.matchEntire(line)
if (matchResult != null) {
val entityId = matchResult.groupValues[1].toInt()
val playerId = matchResult.groupValues[2].toInt()
nestedTagList.add(NestedTag.Player(entityId, playerId))
return InsertStackResult.Success
}
//tag=CARDTYPE value=GAME
matchResult = PowerParserImpl.TAG_PATTERN.matchEntire(line)
if (matchResult != null) {
val key = matchResult.groupValues[1]
val value = matchResult.groupValues[2]
nestedTagList.add(NestedTag.Tag(key, value))
return InsertStackResult.Success
}
//BLOCK_START
// BlockType=TRIGGER
// Entity=GameEntity
// EffectCardId=System.Collections.Generic.List`1[System.String]
// EffectIndex=-1
// Target=0
// SubOption=-1
// TriggerKeyword=TAG_NOT_SET
matchResult = PowerParserImpl.BLOCK_START_PATTERN.matchEntire(line)
if (matchResult != null) {
val blockType = matchResult.groupValues[1].toBlockType()
val entity = Entity.createFromContent(matchResult.groupValues[2])!!
val target = Entity.createFromContent(matchResult.groupValues[5])
val block = NestedTag.Block(blockType, entity, target)
nestedTagList.add(block)
return InsertStackResult.Success
}
matchResult = PowerParserImpl.BLOCK_END_PATTERN.matchEntire(line)
if (matchResult != null) {
//块结束了
nestedTagList.add(NestedTag.BlockEnd)
// val blockStartList = nestedTagList.filterIsInstance()
val blockCount = nestedTagList.count {
it is NestedTag.Block
}
val blockEndCount = nestedTagList.count { it is NestedTag.BlockEnd }
if (blockCount == blockEndCount) {
return InsertStackResult.Over(flushBlock(nestedTagList), true)
} else {
return InsertStackResult.Success
}
}
//TAG_CHANGE Entity=阿克萌德#51240 tag=CURRENT_PLAYER value=1
//TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=29 zone=DECK zonePos=0 cardId= player=1] tag=ZONE_POSITION value=1
matchResult = PowerParserImpl.TAG_CHANGE_PATTERN.matchEntire(line)
if (matchResult != null) {
return when (val first = nestedTagList.firstOrNull()) {
is NestedTag.FullEntity -> {
val powerTag = flushFullEntityWhenFirst()
//处理堆栈
InsertStackResult.Over(powerTag, false)
}
is NestedTag.ShowEntity -> {
val showEntity = PowerTag.PowerTaskList.ShowEntity(
first.entity,
first.cardId
)
nestedTagList.forEach {
if (it is NestedTag.TagChange) {
showEntity.payloads[it.tag] = it.value
}
}
nestedTagList.clear()
InsertStackResult.Over(showEntity, false)
}
is NestedTag.Block -> {
val tagChange = NestedTag.TagChange(
Entity.createFromContent(matchResult.groupValues[1])!!,
matchResult.groupValues[2],
matchResult.groupValues[3],
)
nestedTagList.add(tagChange)
InsertStackResult.Success
}
else -> {
InsertStackResult.CanNotInsert
}
}
}
//FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=4 zone=DECK zonePos=0 cardId= player=1] CardID=
matchResult = PowerParserImpl.FULL_ENTITY_PATTERN.matchEntire(line)
if (matchResult != null) {
val first = nestedTagList.firstOrNull()
if (first is NestedTag.CreateGame) {
//create game 接 full entity
val createGame = createCreateGameTag()
val content = matchResult.groupValues[1]
insertFullEntity(content)
return InsertStackResult.Over(createGame, true)
} else if (first is NestedTag.FullEntity) {
//连续两个full entity
val result = flushFullEntityWhenFirst()
insertFullEntity(matchResult.groupValues[1])
return InsertStackResult.Over(result, true)
}
insertFullEntity(matchResult.groupValues[1])
return InsertStackResult.Success
}
//SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=85 zone=SETASIDE zonePos=0 cardId= player=2] CardID=SCH_231e
matchResult = PowerParserImpl.SHOW_ENTITY.matchEntire(line)
if (matchResult != null) {
val first = nestedTagList.firstOrNull()
val fullEntity = if (first is NestedTag.FullEntity) {
flushFullEntityWhenFirst()
} else null
val entity = Entity.createFromContent(matchResult.groupValues[1])!!
val cardId = matchResult.groupValues[2]
nestedTagList.add(NestedTag.ShowEntity(entity, cardId))
return if (fullEntity == null) {
InsertStackResult.Success
} else {
InsertStackResult.Over(fullEntity, true)
}
}
return InsertStackResult.CanNotInsert
}
/**
* 如果栈中的第一个是FullEntity,就开始处理
*/
private fun flushFullEntityWhenFirst(): PowerTag.PowerTaskList.FullEntity {
val map = mutableMapOf()
val first = nestedTagList.removeFirst() as NestedTag.FullEntity
nestedTagList.map {
it as NestedTag.Tag
}.forEach {
map[it.key] = it.value
}
//清空栈
nestedTagList.clear()
return PowerTag.PowerTaskList.FullEntity(
first.entity,
map
)
// return when {
// isInsertCardToDeck(first) -> {
// //插入一张卡牌到牌库
// flushFullEntityInsertCardToDeck()
// }
// isInsertHeroToPlay(first) -> {
// //放置英雄牌到战场
// flushFullEntityInsertHeroToPlay()
// }
// isInsertHeroPowerToPlay(first) -> {
// //放置英雄技能到战场
// flushFullEntityInsertHeroPowerToPlay()
// }
// else -> throw RuntimeException("无法处理的 full entity $first")
// }
}
// /**
// * 是否是置入英雄技能到战场
// */
// private fun isInsertHeroPowerToPlay(fullEntity: NestedTag.FullEntity): Boolean {
// if (fullEntity.entity.zone != Zone.Play) {
// return false
// }
//
// nestedTagList.forEach {
// if (it is NestedTag.Tag && "cardType".equals(
// it.key,
// true
// ) && GameCardType.HeroPower.name.equals(
// it.value.replace("_", ""),
// true
// )
// ) {
// return true
// }
// }
//
//
// return false
// }
// private fun flushFullEntityInsertHeroPowerToPlay(): PowerTag.PowerTaskList.FullEntity.InsertHeroPowerToPlay {
// var last = nestedTagList.removeLastOrNull()
// //移除第一个
// val entity = (nestedTagList.removeFirst() as NestedTag.FullEntity).entity
// val map = mutableMapOf()
// while (last != null) {
// when (last) {
// is NestedTag.Tag -> {
// map[last.key] = last.value
// }
// else -> {
// throw RuntimeException("last的类型必须是Tag 但现在是 $last")
// }
// }
// last = nestedTagList.removeLastOrNull()
// }
// return PowerTag.PowerTaskList.FullEntity.InsertHeroPowerToPlay.createFromEntityAndMap(
// entity,
// map
// )
// }
//
// private fun flushFullEntityInsertHeroToPlay(): PowerTag.PowerTaskList.FullEntity.InsertHeroToPlay {
// var last = nestedTagList.removeLastOrNull()
// //移除第一个
// val entity = (nestedTagList.removeFirst() as NestedTag.FullEntity).entity
// val map = mutableMapOf()
// while (last != null) {
// when (last) {
// is NestedTag.Tag -> {
// map[last.key] = last.value
// }
// else -> {
// throw RuntimeException("last的类型必须是Tag 但现在是 $last")
// }
// }
// last = nestedTagList.removeLastOrNull()
// }
// return PowerTag.PowerTaskList.FullEntity.InsertHeroToPlay.createFromEntityAndMap(
// entity,
// map
// )
// }
// /**
// * 是否是置入英雄卡到战场
// */
// private fun isInsertHeroToPlay(fullEntity: NestedTag.FullEntity): Boolean {
// if (fullEntity.entity.zone != Zone.Play) {
// return false
// }
//
// nestedTagList.forEach {
// if (it is NestedTag.Tag && "cardType".equals(
// it.key,
// true
// ) && GameCardType.Hero.name.equals(
// it.value,
// true
// )
// ) {
// return true
// }
// }
//
//
// return false
// }
// /**
// * 是否是置入英雄卡到战场
// */
// private fun isInsertCardToDeck(fullEntity: NestedTag.FullEntity): Boolean {
//
// return fullEntity.entity.zone == Zone.Deck && fullEntity.entity.gameCardType == GameCardType.Invalid
// }
// private fun flushFullEntityInsertCardToDeck(): PowerTag.PowerTaskList.FullEntity.InsertToDeck {
// var last = nestedTagList.removeLastOrNull()
// //移除第一个
// val fullEntity = nestedTagList.removeFirst() as NestedTag.FullEntity
// val map = mutableMapOf()
// while (last != null) {
// when (last) {
// is NestedTag.Tag -> {
// map[last.key] = last.value
// }
// else -> {
// throw RuntimeException("last的类型必须是Tag 但现在是 $last")
// }
// }
// last = nestedTagList.removeLastOrNull()
// }
//
// if (map.size != 3) throw RuntimeException("在插入卡牌到牌库的情况下,tag数量必须是3个")
//
// return PowerTag.PowerTaskList.FullEntity.InsertToDeck.createFromEntityAndMap(
// fullEntity.entity,
// map
// )
//
// }
private fun insertFullEntity(content: String) {
val fullEntity = createFullEntityByContent(content)
nestedTagList.add(fullEntity)
}
/**
* 根据字符串创建FullEntity
*/
//[entityName=UNKNOWN ENTITY [cardType=INVALID] id=4 zone=DECK zonePos=0 cardId= player=1]
//[entityName=加尔鲁什·地狱咆哮 id=64 zone=PLAY zonePos=0 cardId=HERO_01 player=1]
private fun createFullEntityByContent(content: String): NestedTag.FullEntity {
val entity: Entity = Entity.createFromContent(content)!!
return NestedTag.FullEntity(entity)
}
private fun createCreateGameTag(): PowerTag.PowerTaskList.CreateGame {
val first = nestedTagList.removeFirstOrNull()
if (first == NestedTag.CreateGame) {
val keyValueMap = mutableMapOf()
var last = nestedTagList.removeLastOrNull()
var player1: Player? = null
var player2: Player? = null
while (last != null) {
when (last) {
is NestedTag.GameEntity -> {
val game = GameEntity(
GameCardType.Game,
last.id
)
return PowerTag.PowerTaskList.CreateGame(
game,
player1!!,
player2!!
)
}
is NestedTag.Player -> {
keyValueMap["playerid"] = last.playerId.toString()
keyValueMap["entityid"] = last.entityId.toString()
if (player2 == null) {
player2 = Player.fromMap(keyValueMap)
} else {
player1 = Player.fromMap(keyValueMap)
}
keyValueMap.clear()
}
is NestedTag.Tag -> {
keyValueMap[last.key.replace("_", "").lowercase()] = last.value
}
else -> throw IllegalArgumentException("非法状态错误 $last")
}
last = nestedTagList.removeLastOrNull()
}
} else {
throw IllegalArgumentException("第一个必须是 CreateGame,但现在是 $first")
}
throw RuntimeException("无法创建CreateGame")
}
private fun flushBlock(
source: MutableList,
): PowerTag.PowerTaskList.Block {
//有可能出现多级嵌套
val first = source.removeFirst() as? NestedTag.Block
if (first == null) {
throw RuntimeException("列表的第一个应该是Block,但现在是不是")
}
source.removeLast()
val payloads = mutableListOf()
val tempList = mutableListOf()
source.forEachIndexed { index, nestedTag ->
when (nestedTag) {
is NestedTag.Block -> {
// if (tempList.isEmpty()) {
tempList.add(nestedTag)
// } else {
// val blockStartCount = tempList.count {
// it is NestedTag.Block
// }
// if (blockStartCount != 1) {
// throw IllegalArgumentException("错误的block数量 $blockStartCount")
// }
//
// val pairedBlockEndIndex = findPairBlockEndIndex(source, index)
// if (pairedBlockEndIndex == -1) {
// throw IllegalArgumentException("找不到配对的结束标识")
// }
// val innerBlockList = source.subList(index, pairedBlockEndIndex)
// tempList.add(flushBlock(innerBlockList))
// }
}
is NestedTag.FullEntity -> {
if (tempList.isNotEmpty()) {
tempList.add(nestedTag)
} else {
payloads.add(PowerTag.PowerTaskList.FullEntity(nestedTag.entity))
}
}
is NestedTag.ShowEntity -> {
if (tempList.isNotEmpty()) {
tempList.add(nestedTag)
} else {
payloads.add(
PowerTag.PowerTaskList.ShowEntity(
nestedTag.entity, nestedTag.cardId
)
)
}
}
is NestedTag.Tag -> {
if (tempList.isNotEmpty()) {
tempList.add(nestedTag)
} else {
val last = payloads.last()
if (last is PowerTag.PowerTaskList.FullEntity) {
last.append(nestedTag.toPair())
} else if (last is PowerTag.PowerTaskList.ShowEntity) {
last.append(nestedTag.toPair())
}
}
}
is NestedTag.TagChange -> {
if (tempList.isNotEmpty()) {
tempList.add(nestedTag)
} else {
payloads.add(nestedTag.convert())
}
}
NestedTag.BlockEnd -> {
tempList.add(NestedTag.BlockEnd)
val blockStartCount = tempList.count {
it is NestedTag.Block
}
val blockEndCount = tempList.count {
it is NestedTag.BlockEnd
}
if (blockStartCount == blockEndCount) {
payloads.add(flushBlock(tempList))
tempList.clear()
}
}
else -> {
throw IllegalArgumentException("非法的数据 $nestedTag")
}
}
}
source.clear()
return PowerTag.PowerTaskList.Block(
first.blockType,
first.entity,
first.target,
payloads
)
}
}
private fun findPairBlockEndIndex(source: List, start: Int): Int {
var blockStartCount = 0
source.subList(start, source.size).forEachIndexed { index, it ->
if (it is NestedTag.Block) {
blockStartCount++
} else if (it is NestedTag.BlockEnd) {
if (blockStartCount == 0) {
return index
}
blockStartCount--
}
}
return -1
}
================================================
FILE: core/src/main/java/com/ke/hs_tracker/core/parser/DeckFileObserver.kt
================================================
package com.ke.hs_tracker.core.parser
import com.ke.hs_tracker.core.entity.CurrentDeck
import com.ke.hs_tracker.core.removeTime
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.io.InputStream
/**
* deck文件观察者
*/
class DeckFileObserver(
private val interval: Long = 2000,
private val deckFileInputStreamProvider: suspend () -> InputStream?,
) {
private var oldLogSize = 0L
fun reset(){
oldLogSize = 0
}
suspend fun start(): Flow = flow {
while (true) {
deckFileInputStreamProvider()?.reader()?.apply {
if (oldLogSize > 0) {
skip(oldLogSize)
}
val text = readText()
oldLogSize += text.length
val lines = text.lines()
.filter { it.isNotEmpty() }
listToDeck(lines)?.apply {
emit(this)
}
close()
}
delay(interval)
}
}
private fun listToDeck(list: List): CurrentDeck? {
if (list.isEmpty()) {
return null
}
val contentList = list.map {
it.removeTime().third
}
val name = contentList.findLast {
it.startsWith("###", true)
} ?: return null
val target =
contentList.subList(contentList.indexOf(name), contentList.size).toMutableList()
target.removeFirst()
target.removeFirst()
val code = target.removeFirst()
return CurrentDeck(
name.replace("### ", ""), code
)
}
}
================================================
FILE: core/src/main/java/com/ke/hs_tracker/core/parser/PowerFileObserver.kt
================================================
package com.ke.hs_tracker.core.parser
import com.ke.hs_tracker.core.entity.CurrentDeck
import com.ke.hs_tracker.core.entity.PowerTag
import com.ke.hs_tracker.core.removeTime
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.io.InputStream
/**
* deck文件观察者
*/
class PowerFileObserver(
private val interval: Long = 2000,
private val fileInputStreamProvider: suspend () -> InputStream?,
) {
private var oldLogSize = 0L
fun reset(){
oldLogSize = 0
}
suspend fun start(): Flow> = flow {
while (true) {
fileInputStreamProvider()?.reader()?.apply {
if (oldLogSize > 0) {
skip(oldLogSize)
}
val text = readText()
oldLogSize += text.length
val lines = text.lines()
.filter { it.isNotEmpty() }
emit(lines)
close()
}
delay(interval)
}
}
}
================================================
FILE: core/src/main/java/com/ke/hs_tracker/core/parser/PowerParser.kt
================================================
package com.ke.hs_tracker.core.parser
import com.ke.hs_tracker.core.entity.*
import com.ke.hs_tracker.core.entity.toCardType
import com.ke.hs_tracker.core.removeTime
/**
* 日志解析
*/
interface PowerParser {
/**
* 解析一行日志
*/
fun parse(content: String)
/**
* 解析结果监听
*/
var powerTagListener: ((PowerTag) -> Unit)?
}
class PowerParserImpl : PowerParser {
private val blockTagStack: BlockTagStack = BlockTagStackImpl()
override var powerTagListener: ((PowerTag) -> Unit)? = null
override fun parse(content: String) {
val pair = checkTypeAndReturnContent(content) ?: return
if (pair.first == LogType.PowerTaskList) {
handlePowerTaskListLog(pair.second)
} else {
handleGameStateLog(pair.second)
}
}
/**
* 检查日志类型并返回去掉时间和日期前缀的内容
*/
private fun checkTypeAndReturnContent(content: String): Pair? {
if (content.length < TIME_PREFIX_SIZE) {
return null
}
val noTimeContent = content.substring(TIME_PREFIX_SIZE)
if (noTimeContent.startsWith(LogType.GameState.replace)) {
return LogType.GameState to noTimeContent.replace(LogType.GameState.replace, "").trim()
} else if (noTimeContent.startsWith(LogType.PowerTaskList.replace)) {
return LogType.PowerTaskList to noTimeContent.replace(LogType.PowerTaskList.replace, "")
.trim()
}
return null
}
private fun handlePowerTaskListLog(line: String) {
val result = blockTagStack.insert(line)
when (result) {
InsertStackResult.CanNotInsert -> {
handleUnSupportNestedTag(line)
}
is InsertStackResult.Over -> {
powerTagListener?.invoke(result.powerTag)
if (!result.handled) {
//需要自己处理
handleUnSupportNestedTag(line)
}
}
InsertStackResult.Success -> {
}
}
}
private fun handleUnSupportNestedTag(line: String) {
var matchResult = TAG_CHANGE_PATTERN.matchEntire(line)
if (matchResult != null) {
handleTagChangeLine(
matchResult.groupValues[1],
matchResult.groupValues[2],
matchResult.groupValues[3]
)
}
}
private fun handleTagChangeLine(content: String, tag: String, value: String) {
val entity = Entity.createFromContent(content)!!
powerTagListener?.invoke(PowerTag.PowerTaskList.TagChange(entity, tag, value))
}
private fun handleGameStateLog(content: String) {
BUILD_NUMBER_PATTERN.matchEntire(content)?.apply {
val tag = PowerTag.GameState.BuildNumber(
groupValues[1]
)
powerTagListener?.invoke(tag)
return
}
GAME_TYPE_PATTERN.matchEntire(content)?.apply {
val tag = PowerTag.GameState.GameType(
groupValues[1]
)
powerTagListener?.invoke(tag)
return
}
FORMAT_TYPE_PATTERN.matchEntire(content)?.apply {
val tag = PowerTag.GameState.FormatType(
groupValues[1]
)
powerTagListener?.invoke(tag)
return
}
SCENARIO_ID_PATTERN.matchEntire(content)?.apply {
val tag = PowerTag.GameState.ScenarioID(
groupValues[1]
)
powerTagListener?.invoke(tag)
return
}
PLAYER_MAPPING_PATTERN.matchEntire(content)?.apply {
val tag = PowerTag.GameState.PlayerMapping(
groupValues[1].toInt(), groupValues[2]
)
powerTagListener?.invoke(tag)
return
}
}
companion object {
const val TIME_PREFIX_SIZE = 19
//CREATE_GAME
internal val CREATE_GAME_PATTERN = Regex("CREATE_GAME")
//BuildNumber=127581
internal val BUILD_NUMBER_PATTERN = Regex("BuildNumber=(.*)")
//GameType=GT_CASUAL
internal val GAME_TYPE_PATTERN = Regex("GameType=(.*)")
//FormatType=FT_WILD
internal val FORMAT_TYPE_PATTERN = Regex("FormatType=(.*)")
//ScenarioID=2
internal val SCENARIO_ID_PATTERN = Regex("ScenarioID=(.*)")
//PlayerID=2, PlayerName=阿克萌德#51240
internal val PLAYER_MAPPING_PATTERN = Regex("PlayerID=(.*), PlayerName=(.*)")
// tag=CARDTYPE value=GAME
internal val TAG_PATTERN = Regex("tag=(.*) value=(.*)")
//TAG_CHANGE Entity=GameEntity tag=STATE value=RUNNING
internal val TAG_CHANGE_PATTERN = Regex("TAG_CHANGE Entity=(.*) tag=(.*) value=(.*)")
//GameEntity EntityID=1
internal val GAME_ENTITY_PATTERN = Regex("GameEntity EntityID=(.*)")
//FULL_ENTITY - Updating [entityName=加尔鲁什·地狱咆哮 id=64 zone=PLAY zonePos=0 cardId=HERO_01 player=1] CardID=HERO_01
internal val FULL_ENTITY_PATTERN = Regex("FULL_ENTITY - Updating (.*) CardID=(.*)")
//[entityName=UNKNOWN ENTITY [cardType=INVALID] id=83 zone=DECK zonePos=0 cardId= player=2]
val FULL_ENTITY_CONTENT1_PATTERN =
Regex("\\[entityName=(.*) \\[cardType=(.*)] id=(.*) zone=(.*) zonePos=(.*) cardId=(.*) player=(.*)]")
val FULL_ENTITY_CONTENT2_PATTERN =
Regex("\\[entityName=(.*) id=(.*) zone=(.*) zonePos=(.*) cardId=(.*) player=(.*)]")
val SHOW_ENTITY = Regex("SHOW_ENTITY - Updating Entity=(.*) CardID=(.*)")
//Player EntityID=2 PlayerID=1 GameAccountId=[hi=144115211015832391 lo=191215280]
internal val PLAYER_ENTITY_PATTERN =
Regex("Player EntityID=(.*) PlayerID=(.*) GameAccountId=(.*)")
//BLOCK_START BlockType=ATTACK Entity=[entityName=瓦丝琪女士 id=87 zone=PLAY zonePos=1 cardId=BT_109 player=2]
// EffectCardId=System.Collections.Generic.List`1[System.String]
// EffectIndex=0 Target=[entityName=驯化的雷象 id=78 zone=PLAY zonePos=1 cardId=SCH_714 player=1] SubOption=-1
internal val BLOCK_START_PATTERN =
Regex("BLOCK_START BlockType=(.*) Entity=(.*) EffectCardId=(.*) EffectIndex=(.*) Target=(.*) SubOption=(.*)")
internal val BLOCK_START_CONTINUATION_PATTERN = Regex("(.*) TriggerKeyword=(.*)")
//BLOCK_END
internal val BLOCK_END_PATTERN = Regex("BLOCK_END")
}
}
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
#Tue Jan 18 13:48:58 CST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
================================================
FILE: gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
#android.jetifier.ignorelist=moshi-1.13.0
RELEASE_FILE=../123456
RELEASE_keyAlias=key0
RELEASE_storePassword=123456
RELEASE_keyPassword=123456
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
================================================
FILE: gradlew
================================================
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: libs.versions.toml
================================================
[versions]
compilesdk = "33"
minsdk = "24"
targetsdk = "33"
retrofit = "2.9.0"
constraintlayout = "2.1.2"
material = "1.6.0"
moshi = "1.14.0"
ktx = "1.10.0"
hilt = "2.45"
lifecycle = "2.4.0"
mmkv = "1.2.11"
arouter = "1.5.2"
okhttp = "4.9.0"
navigation = "2.3.5"
logger = "2.2.0"
gson = "2.8.7"
appcompat = "1.6.1"
fragment = "1.5.5"
activity = "1.6.1"
support = "1.0.0"
hibinding = "1.2.0"
adapterhelper = "3.0.4"
pickerview = "4.1.9"
photoview = "2.3.0"
glide = "4.12.0"
latest = "latest.integration"
kehud = "1.3.5"
kepermission = "v1.0.0"
kemvvm = "1.1.3"
keimagepicker = "1.0.1"
kewechat = "1.1.8"
keqrscanner = "1.1"
keaddresspicker = "1.0.5"
keapkinstaller = "1.0.0"
kefilepicker = "1.0.0"
keidcard = "1.0.6"
room = "2.5.1"
[libraries]
appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
material = { module = "com.google.android.material:material", version.ref = "material" }
constraint-layout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
core-ktx = { module = "androidx.core:core-ktx", version.ref = "ktx" }
navigation-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigation" }
navigation-ui = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigation" }
fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragment" }
activity = { module = "androidx.activity:activity", version.ref = "activity" }
lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle" }
lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
support-v4 = { module = "androidx.legacy:legacy-support-v4", version.ref = "support" }
room-runtime = { module = "androidx.room:room-runtime",version.ref = "room" }
room-compiler = { module = "androidx.room:room-compiler",version.ref = "room" }
room-ktx = { module = "androidx.room:room-ktx",version.ref = "room" }
hilt-android = { module = "com.google.dagger:hilt-android" , version.ref = "hilt"}
hilt-compiler = { module = "com.google.dagger:hilt-compiler" , version.ref = "hilt"}
glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
glide-compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" }
ke-mvvm = { module = "com.github.keluokeda:mvvm_base", version.ref = "kemvvm" }
logger = { module = "com.orhanobut:logger", version.ref = "logger" }
hi-binding = { module = "com.hi-dhl:binding", version.ref = "hibinding" }
mmkv = { module = "com.tencent:mmkv-static", version.ref = "mmkv" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" }
moshi = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" }
moshi-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" }
adapter-helper = { module = "com.github.CymChad:BaseRecyclerViewAdapterHelper", version.ref = "adapterhelper" }
================================================
FILE: module/.gitignore
================================================
/build
================================================
FILE: module/build.gradle
================================================
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
id 'com.google.dagger.hilt.android'
id 'kotlin-parcelize'
}
kapt {
correctErrorTypes true
}
android {
compileSdk libs.versions.compilesdk.get().toInteger()
defaultConfig {
minSdk libs.versions.minsdk.get().toInteger()
targetSdk libs.versions.targetsdk.get().toInteger()
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
javaCompileOptions {
annotationProcessorOptions {
arguments += [
"room.schemaLocation":"$projectDir/schemas".toString(),
"room.incremental":"true",
"room.expandProjection":"true"]
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
resourcePrefix 'module_'
buildFeatures {
viewBinding = true
}
namespace 'com.ke.hs_tracker.module'
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.junit.jupiter:junit-jupiter'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation(libs.core.ktx)
implementation(libs.appcompat)
implementation(libs.material)
implementation(libs.constraint.layout)
implementation(libs.fragment.ktx)
implementation(libs.activity)
implementation(libs.lifecycle.viewmodel.ktx)
implementation(libs.lifecycle.livedata.ktx)
implementation(libs.lifecycle.runtime.ktx)
implementation(libs.support.v4)
implementation(libs.logger)
implementation(libs.hilt.android)
kapt(libs.hilt.compiler)
implementation(libs.glide)
kapt(libs.glide.compiler)
implementation(libs.ke.mvvm)
implementation(libs.adapter.helper)
implementation(libs.retrofit)
implementation(libs.retrofit.converter.moshi)
implementation(libs.moshi)
kapt(libs.moshi.codegen)
implementation(libs.hi.binding)
implementation(libs.mmkv)
implementation(libs.room.runtime)
implementation(libs.room.ktx)
kapt(libs.room.compiler)
// implementation 'com.tencent.bugly:crashreport:latest.release'
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
// optional - helpers for implementing LifecycleOwner in a Service
implementation "androidx.lifecycle:lifecycle-service:2.6.1"
}
================================================
FILE: module/consumer-rules.pro
================================================
================================================
FILE: module/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: module/schemas/com.ke.hs_tracker.module.db.Database/1.json
================================================
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "d83bd3696913c5f4307118a1060d27e4",
"entities": [
{
"tableName": "card",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`artist` TEXT, `name` TEXT NOT NULL, `cost` INTEGER NOT NULL, `id` TEXT NOT NULL, `dbfId` INTEGER NOT NULL, `text` TEXT NOT NULL, `set` TEXT NOT NULL, `type` TEXT NOT NULL, `cardClass` TEXT, `classes` INTEGER NOT NULL, `mechanics` TEXT NOT NULL, `flavor` TEXT NOT NULL, `rarity` TEXT, `durability` INTEGER NOT NULL, `armor` INTEGER NOT NULL, `collectible` INTEGER NOT NULL, `spellSchool` TEXT, `race` TEXT, `attach` INTEGER NOT NULL, `health` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "artist",
"columnName": "artist",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cost",
"columnName": "cost",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "dbfId",
"columnName": "dbfId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "set",
"columnName": "set",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cardClass",
"columnName": "cardClass",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "classes",
"columnName": "classes",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mechanics",
"columnName": "mechanics",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "flavor",
"columnName": "flavor",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "rarity",
"columnName": "rarity",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "durability",
"columnName": "durability",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "armor",
"columnName": "armor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "collectible",
"columnName": "collectible",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "spellSchool",
"columnName": "spellSchool",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "race",
"columnName": "race",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "attach",
"columnName": "attach",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "health",
"columnName": "health",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "game",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `build_number` TEXT NOT NULL, `game_type` TEXT NOT NULL, `format_type` TEXT NOT NULL, `scenario_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `opponent_name` TEXT NOT NULL, `is_user_first` INTEGER, `user_deck_name` TEXT NOT NULL, `user_deck_code` TEXT NOT NULL, `is_user_win` INTEGER, `user_hero` TEXT, `opponent_class` TEXT, `start_time` INTEGER NOT NULL, `end_time` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "buildNumber",
"columnName": "build_number",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "gameType",
"columnName": "game_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "formatType",
"columnName": "format_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "scenarioID",
"columnName": "scenario_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userName",
"columnName": "user_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "opponentName",
"columnName": "opponent_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isUserFirst",
"columnName": "is_user_first",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "userDeckName",
"columnName": "user_deck_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userDeckCode",
"columnName": "user_deck_code",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isUserWin",
"columnName": "is_user_win",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "userHero",
"columnName": "user_hero",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "opponentHero",
"columnName": "opponent_class",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "startTime",
"columnName": "start_time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "endTime",
"columnName": "end_time",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd83bd3696913c5f4307118a1060d27e4')"
]
}
}
================================================
FILE: module/schemas/com.ke.hs_tracker.module.db.Database/2.json
================================================
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "1bb7b04a91def09f83b0a81252d181ba",
"entities": [
{
"tableName": "card",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`artist` TEXT, `name` TEXT NOT NULL, `cost` INTEGER NOT NULL, `id` TEXT NOT NULL, `dbfId` INTEGER NOT NULL, `text` TEXT NOT NULL, `set` TEXT NOT NULL, `type` TEXT NOT NULL, `cardClass` TEXT, `classes` INTEGER NOT NULL, `mechanics` TEXT NOT NULL, `flavor` TEXT NOT NULL, `rarity` TEXT, `durability` INTEGER NOT NULL, `armor` INTEGER NOT NULL, `collectible` INTEGER NOT NULL, `spellSchool` TEXT, `race` TEXT, `attach` INTEGER NOT NULL, `health` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "artist",
"columnName": "artist",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cost",
"columnName": "cost",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "dbfId",
"columnName": "dbfId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "set",
"columnName": "set",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cardClass",
"columnName": "cardClass",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "classes",
"columnName": "classes",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mechanics",
"columnName": "mechanics",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "flavor",
"columnName": "flavor",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "rarity",
"columnName": "rarity",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "durability",
"columnName": "durability",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "armor",
"columnName": "armor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "collectible",
"columnName": "collectible",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "spellSchool",
"columnName": "spellSchool",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "race",
"columnName": "race",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "attach",
"columnName": "attach",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "health",
"columnName": "health",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "game",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `build_number` TEXT NOT NULL, `game_type` TEXT NOT NULL, `format_type` TEXT NOT NULL, `scenario_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `opponent_name` TEXT NOT NULL, `is_user_first` INTEGER, `user_deck_name` TEXT NOT NULL, `user_deck_code` TEXT NOT NULL, `is_user_win` INTEGER, `user_hero` TEXT, `opponent_class` TEXT, `start_time` INTEGER NOT NULL, `end_time` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "buildNumber",
"columnName": "build_number",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "gameType",
"columnName": "game_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "formatType",
"columnName": "format_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "scenarioID",
"columnName": "scenario_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userName",
"columnName": "user_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "opponentName",
"columnName": "opponent_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isUserFirst",
"columnName": "is_user_first",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "userDeckName",
"columnName": "user_deck_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userDeckCode",
"columnName": "user_deck_code",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isUserWin",
"columnName": "is_user_win",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "userHero",
"columnName": "user_hero",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "opponentHero",
"columnName": "opponent_class",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "startTime",
"columnName": "start_time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "endTime",
"columnName": "end_time",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "in_game_card",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `build_number` TEXT NOT NULL, `card_id` TEXT, `entity_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `zone` TEXT NOT NULL, `turn` INTEGER NOT NULL, `is_user` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "buildNumber",
"columnName": "build_number",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cardId",
"columnName": "card_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "entityId",
"columnName": "entity_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "zone",
"columnName": "zone",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "turn",
"columnName": "turn",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isUser",
"columnName": "is_user",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1bb7b04a91def09f83b0a81252d181ba')"
]
}
}
================================================
FILE: module/schemas/com.ke.hs_tracker.module.db.Database/3.json
================================================
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "720ca460a50c2d8a7f6bf96c98cf4358",
"entities": [
{
"tableName": "card",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`artist` TEXT, `name` TEXT NOT NULL, `cost` INTEGER NOT NULL, `id` TEXT NOT NULL, `dbfId` INTEGER NOT NULL, `text` TEXT NOT NULL, `set` TEXT NOT NULL, `type` TEXT NOT NULL, `cardClass` TEXT, `classes` INTEGER NOT NULL, `mechanics` TEXT NOT NULL, `flavor` TEXT NOT NULL, `rarity` TEXT, `durability` INTEGER NOT NULL, `armor` INTEGER NOT NULL, `collectible` INTEGER NOT NULL, `spellSchool` TEXT, `race` TEXT, `attach` INTEGER NOT NULL, `health` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "artist",
"columnName": "artist",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cost",
"columnName": "cost",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "dbfId",
"columnName": "dbfId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "set",
"columnName": "set",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cardClass",
"columnName": "cardClass",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "classes",
"columnName": "classes",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mechanics",
"columnName": "mechanics",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "flavor",
"columnName": "flavor",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "rarity",
"columnName": "rarity",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "durability",
"columnName": "durability",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "armor",
"columnName": "armor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "collectible",
"columnName": "collectible",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "spellSchool",
"columnName": "spellSchool",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "race",
"columnName": "race",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "attach",
"columnName": "attach",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "health",
"columnName": "health",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "game",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `build_number` TEXT NOT NULL, `game_type` TEXT NOT NULL, `format_type` TEXT NOT NULL, `scenario_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `opponent_name` TEXT NOT NULL, `is_user_first` INTEGER, `user_deck_name` TEXT NOT NULL, `user_deck_code` TEXT NOT NULL, `is_user_win` INTEGER, `user_hero` TEXT, `opponent_class` TEXT, `start_time` INTEGER NOT NULL, `end_time` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "buildNumber",
"columnName": "build_number",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "gameType",
"columnName": "game_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "formatType",
"columnName": "format_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "scenarioID",
"columnName": "scenario_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userName",
"columnName": "user_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "opponentName",
"columnName": "opponent_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isUserFirst",
"columnName": "is_user_first",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "userDeckName",
"columnName": "user_deck_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userDeckCode",
"columnName": "user_deck_code",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isUserWin",
"columnName": "is_user_win",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "userHero",
"columnName": "user_hero",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "opponentHero",
"columnName": "opponent_class",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "startTime",
"columnName": "start_time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "endTime",
"columnName": "end_time",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "zone_position_updated_event",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `game_id` TEXT NOT NULL, `entity_id` INTEGER NOT NULL, `card_id` TEXT, `card_name` TEXT, `is_user` INTEGER NOT NULL, `current_zone` TEXT NOT NULL, `new_zone` TEXT NOT NULL, `current_position` INTEGER NOT NULL, `new_position` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "gameId",
"columnName": "game_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "entityId",
"columnName": "entity_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "cardId",
"columnName": "card_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "cardName",
"columnName": "card_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isUser",
"columnName": "is_user",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "currentZone",
"columnName": "current_zone",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "newZone",
"columnName": "new_zone",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "currentPosition",
"columnName": "current_position",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "newPosition",
"columnName": "new_position",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '720ca460a50c2d8a7f6bf96c98cf4358')"
]
}
}
================================================
FILE: module/schemas/com.ke.hs_tracker.module.db.Database/4.json
================================================
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "a2944b7e136b6abe232a7a446c5ea3db",
"entities": [
{
"tableName": "card",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`artist` TEXT, `name` TEXT NOT NULL, `cost` INTEGER NOT NULL, `id` TEXT NOT NULL, `dbfId` INTEGER NOT NULL, `text` TEXT NOT NULL, `set` TEXT NOT NULL, `type` TEXT NOT NULL, `cardClass` TEXT, `classes` INTEGER NOT NULL, `mechanics` TEXT NOT NULL, `flavor` TEXT NOT NULL, `rarity` TEXT, `durability` INTEGER NOT NULL, `armor` INTEGER NOT NULL, `collectible` INTEGER NOT NULL, `spellSchool` TEXT, `race` TEXT, `attack` INTEGER NOT NULL, `health` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "artist",
"columnName": "artist",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cost",
"columnName": "cost",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "dbfId",
"columnName": "dbfId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "set",
"columnName": "set",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cardClass",
"columnName": "cardClass",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "classes",
"columnName": "classes",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mechanics",
"columnName": "mechanics",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "flavor",
"columnName": "flavor",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "rarity",
"columnName": "rarity",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "durability",
"columnName": "durability",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "armor",
"columnName": "armor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "collectible",
"columnName": "collectible",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "spellSchool",
"columnName": "spellSchool",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "race",
"columnName": "race",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "attack",
"columnName": "attack",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "health",
"columnName": "health",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "game",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `build_number` TEXT NOT NULL, `game_type` TEXT NOT NULL, `format_type` TEXT NOT NULL, `scenario_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `opponent_name` TEXT NOT NULL, `is_user_first` INTEGER, `user_deck_name` TEXT NOT NULL, `user_deck_code` TEXT NOT NULL, `is_user_win` INTEGER, `user_hero` TEXT, `opponent_class` TEXT, `start_time` INTEGER NOT NULL, `end_time` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "buildNumber",
"columnName": "build_number",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "gameType",
"columnName": "game_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "formatType",
"columnName": "format_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "scenarioID",
"columnName": "scenario_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userName",
"columnName": "user_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "opponentName",
"columnName": "opponent_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isUserFirst",
"columnName": "is_user_first",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "userDeckName",
"columnName": "user_deck_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userDeckCode",
"columnName": "user_deck_code",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isUserWin",
"columnName": "is_user_win",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "userHero",
"columnName": "user_hero",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "opponentHero",
"columnName": "opponent_class",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "startTime",
"columnName": "start_time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "endTime",
"columnName": "end_time",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "zone_position_updated_event",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `game_id` TEXT NOT NULL, `entity_id` INTEGER NOT NULL, `card_id` TEXT, `card_name` TEXT, `is_user` INTEGER NOT NULL, `current_zone` TEXT NOT NULL, `new_zone` TEXT NOT NULL, `current_position` INTEGER NOT NULL, `new_position` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "gameId",
"columnName": "game_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "entityId",
"columnName": "entity_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "cardId",
"columnName": "card_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "cardName",
"columnName": "card_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isUser",
"columnName": "is_user",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "currentZone",
"columnName": "current_zone",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "newZone",
"columnName": "new_zone",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "currentPosition",
"columnName": "current_position",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "newPosition",
"columnName": "new_position",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a2944b7e136b6abe232a7a446c5ea3db')"
]
}
}
================================================
FILE: module/src/androidTest/java/com/ke/hs_tracker/module/ExampleInstrumentedTest.kt
================================================
package com.ke.hs_tracker.module
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.ke.hs_tracker.module.test", appContext.packageName)
}
}
================================================
FILE: module/src/main/AndroidManifest.xml
================================================
================================================
FILE: module/src/main/assets/Decks.log
================================================
I 22:23:04.0780690 Deck Contents Received:
I 22:23:04.0780690 ### 狗贼
I 22:23:04.0780690 # Deck ID: 7996212750
I 22:23:04.0780690 AAEBAZICCukB6awCtLsC1pkDiLED6LoDndgDv+ADpu8DiYsECooOoM0CmNICntIChOYCv/ICj/YCm84D1OgDr4AEAA==
I 22:23:04.0780690 ### 法术
I 22:23:04.0780690 # Deck ID: 7997355263
I 22:23:04.0780690 AAECAf0ECIy5A+DMA7/gA5PhA7L3A6CKBJiNBPyeBAvBuAOBvwPNzgP73QPr3gPQ7APR7AOn9wOKjQT9ngT7ogQA
I 22:23:04.0780690 ### 自定义 潜行者
I 22:23:04.0780690 # Deck ID: 8003374434
I 22:23:04.0780690 AAECAaIHAAAA
I 22:23:04.0780690 ### 小丑
I 22:23:04.0780690 # Deck ID: 8004144061
I 22:23:04.0780690 AAECAZICBrrQA/zeA6bvA9D5A4mLBOSkBAzougObzgPw1AP+2wOJ4AOV4AOi4QOk4QPR4QPm4QPe7AOvgAQA
I 22:23:04.0780690 ### 双古神
I 22:23:04.0780690 # Deck ID: 8005951524
I 22:23:04.0780690 AAECAZICCO66A53YA/zeA4rgA6bvA9D5A4mLBKWNBAvougObzgPw1AOJ4AOV4AOi4QOk4QPR4QPm4QOP5AOvgAQA
I 22:23:04.0780690 ### 圣契骑
I 22:23:04.0780690 # Deck ID: 8006192896
I 22:23:04.0780690 AAECAZ8FDOu5A+u5A+y5A+y5A4TBA5XNA5PQA7/RA/voA5HsA9n5A+CLBAn9uAPquQPKwQPA0QPM6wPw9gON+AO2gAT5pAQA
I 22:23:04.0780690 ### 对决套牌
I 22:23:04.0780690 # Deck ID: 8009171311
I 22:23:04.0780690 AAEBAbLwAw+WBc3OA6TRA9nRA7fSA4vnA9DsA9HsA6f3A673A6CKBIqNBP2eBMWgBPuiBAAA
I 22:23:04.0780690 ### 任务
I 22:23:04.0780690 # Deck ID: 8010668041
I 22:23:04.0780690 AAECAaoIBJzOA6beA8L2A9ySBA3buAPhzAPNzgPw1AOK5APq5wP67APk9gOF+gOogQSVkgT5nwT6nwQA
I 22:23:04.0780690 ### 卡扎库
I 22:23:04.0780690 # Deck ID: 8020766665
I 22:23:04.0780690 AAECAbr5AwPR4QOJiwSJnwQN5boD6LoD77oDm84D8NQDieADiuADjOQDj+QDr4AErp8EsKUEz6wEAA==
I 22:23:06.5584800 Deck Contents Received:
I 22:23:06.5584800 ### 狗贼
I 22:23:06.5584800 # Deck ID: 7996212750
I 22:23:06.5584800 AAEBAZICCukB6awCtLsC1pkDiLED6LoDndgDv+ADpu8DiYsECooOoM0CmNICntIChOYCv/ICj/YCm84D1OgDr4AEAA==
I 22:23:06.5584800 ### 法术
I 22:23:06.5584800 # Deck ID: 7997355263
I 22:23:06.5584800 AAECAf0ECIy5A+DMA7/gA5PhA7L3A6CKBJiNBPyeBAvBuAOBvwPNzgP73QPr3gPQ7APR7AOn9wOKjQT9ngT7ogQA
I 22:23:06.5584800 ### 自定义 潜行者
I 22:23:06.5584800 # Deck ID: 8003374434
I 22:23:06.5584800 AAECAaIHAAAA
I 22:23:06.5584800 ### 小丑
I 22:23:06.5584800 # Deck ID: 8004144061
I 22:23:06.5584800 AAECAZICBrrQA/zeA6bvA9D5A4mLBOSkBAzougObzgPw1AP+2wOJ4AOV4AOi4QOk4QPR4QPm4QPe7AOvgAQA
I 22:23:06.5584800 ### 双古神
I 22:23:06.5584800 # Deck ID: 8005951524
I 22:23:06.5584800 AAECAZICCO66A53YA/zeA4rgA6bvA9D5A4mLBKWNBAvougObzgPw1AOJ4AOV4AOi4QOk4QPR4QPm4QOP5AOvgAQA
I 22:23:06.5584800 ### 圣契骑
I 22:23:06.5584800 # Deck ID: 8006192896
I 22:23:06.5584800 AAECAZ8FDOu5A+u5A+y5A+y5A4TBA5XNA5PQA7/RA/voA5HsA9n5A+CLBAn9uAPquQPKwQPA0QPM6wPw9gON+AO2gAT5pAQA
I 22:23:06.5584800 ### 对决套牌
I 22:23:06.5584800 # Deck ID: 8009171311
I 22:23:06.5584800 AAEBAbLwAw+WBc3OA6TRA9nRA7fSA4vnA9DsA9HsA6f3A673A6CKBIqNBP2eBMWgBPuiBAAA
I 22:23:06.5584800 ### 任务
I 22:23:06.5584800 # Deck ID: 8010668041
I 22:23:06.5584800 AAECAaoIBJzOA6beA8L2A9ySBA3buAPhzAPNzgPw1AOK5APq5wP67APk9gOF+gOogQSVkgT5nwT6nwQA
I 22:23:06.5584800 ### 卡扎库
I 22:23:06.5584800 # Deck ID: 8020766665
I 22:23:06.5584800 AAECAbr5AwPR4QOJiwSJnwQN5boD6LoD77oDm84D8NQDieADiuADjOQDj+QDr4AErp8EsKUEz6wEAA==
I 22:23:35.6401730 Finding Game With Deck:
I 22:23:35.6401730 ### 任务
I 22:23:35.6401730 # Deck ID: 8010668041
I 22:23:35.6401730 AAECAaoIBJzOA6beA8L2A9ySBA3buAPhzAPNzgPw1AOK5APq5wP67APk9gOF+gOogQSVkgT5nwT6nwQA
================================================
FILE: module/src/main/assets/log.config
================================================
[Bob]
LogLevel=1
FilePrinting=True
ConsolePrinting=False
ScreenPrinting=False
Verbose=True
[Power]
LogLevel=1
FilePrinting=True
ConsolePrinting=False
ScreenPrinting=False
Verbose=True
[Achievements]
LogLevel=1
FilePrinting=True
ConsolePrinting=False
ScreenPrinting=False
Verbose=True
[Arena]
LogLevel=1
FilePrinting=True
ConsolePrinting=False
ScreenPrinting=False
Verbose=True
[FullScreenFX]
LogLevel=1
FilePrinting=True
ConsolePrinting=False
ScreenPrinting=False
Verbose=True
[LoadingScreen]
LogLevel=1
FilePrinting=True
ConsolePrinting=False
ScreenPrinting=False
Verbose=True
[Rachelle]
LogLevel=1
FilePrinting=True
ConsolePrinting=False
ScreenPrinting=False
Verbose=True
[Asset]
LogLevel=1
FilePrinting=True
ConsolePrinting=False
ScreenPrinting=False
Verbose=True
[Zone]
LogLevel=1
FilePrinting=True
ConsolePrinting=False
ScreenPrinting=False
Verbose=True
[Decks]
LogLevel=1
FilePrinting=True
ConsolePrinting=False
ScreenPrinting=False
Verbose=True
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/MainApplication.kt
================================================
package com.ke.hs_tracker.module
import android.app.Application
import android.content.Context
import android.net.Uri
import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.res.ResourcesCompat
import androidx.documentfile.provider.DocumentFile
import com.bumptech.glide.Glide
import com.ke.hs_tracker.module.data.PreferenceStorage
import com.ke.hs_tracker.module.databinding.ModuleDialogCardPreviewBinding
import com.ke.hs_tracker.module.databinding.ModuleItemCardBinding
import com.ke.hs_tracker.module.entity.Card
import com.ke.hs_tracker.module.parser.PowerParserImpl
import com.orhanobut.logger.AndroidLogAdapter
import com.orhanobut.logger.Logger
import com.orhanobut.logger.PrettyFormatStrategy
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.Calendar
import java.util.Date
import javax.inject.Inject
abstract class MainApplication : Application() {
@Inject
lateinit var preferenceStorage: PreferenceStorage
override fun onCreate() {
super.onCreate()
// CrashReport.initCrashReport(applicationContext, "abb84be20b", false)
AppCompatDelegate.setDefaultNightMode(preferenceStorage.theme)
Logger.addLogAdapter(
AndroidLogAdapter(
PrettyFormatStrategy.newBuilder()
.methodCount(5)
.build()
)
)
}
}
fun String.removeTime(): Triple {
val content = substring(PowerParserImpl.TIME_PREFIX_SIZE)
val start = substring(0, 1)
val hms = substring(2, 10).split(":")
val calendar = Calendar.getInstance()
calendar.set(
Calendar.HOUR_OF_DAY, hms[0].toInt()
)
calendar.set(
Calendar.MINUTE, hms[1].toInt()
)
calendar.set(
Calendar.SECOND, hms[2].toInt()
)
return Triple(
start,
calendar.time,
content
)
}
fun String.log() {
Logger.d(this)
}
/**
* 是否具备所有权限
*/
val Context.hasAllPermissions: Boolean
get() {
// val canWriteExternalStorage = ActivityCompat.checkSelfPermission(
// this,
// Manifest.permission.WRITE_EXTERNAL_STORAGE
// ) == PackageManager.PERMISSION_GRANTED
//
// return canWriteExternalStorage && canReadDataDir && isExternalStorageManager()
return canReadDataDir
}
/**
* 是否可以访问data目录
*/
val Context.canReadDataDir: Boolean
get() {
return DocumentFile.fromTreeUri(applicationContext, HS_DATA_FILE_DIR)?.canRead() ?: false
}
/**
* 是否具备外部存储管理权限,如果android版本低于11就返回true
*/
//fun isExternalStorageManager(): Boolean {
//
// return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Environment.isExternalStorageManager()
// } else {
// return true
// }
//}
const val HS_APPLICATION_ID = "com.blizzard.wtcg.hearthstone"
val HS_DATA_FILE_DIR =
Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata%2F${HS_APPLICATION_ID}")!!
//const val HUAWEI_HS_APPLICATION_ID = "com.blizzard.wtcg.hearthstone.cn.huawei"
/**
* 写入log.config文件
*/
suspend fun Context.writeLogConfigFile(forceWrite: Boolean = false): Boolean {
return withContext(Dispatchers.IO) {
val documentFile = findHSDataFilesDir() ?: return@withContext false
val fileName = "log.config"
val file = findHSDataFilesDir(fileName)
if (file != null) {
//文件已存在
"log.config文件已存在".log()
if (forceWrite) {
file.delete()
} else {
return@withContext true
}
}
val configFile = documentFile.createFile("plain/text", fileName)
?: return@withContext false
contentResolver.openOutputStream(configFile.uri)?.use {
assets.open("log.config")
.copyTo(it)
it.flush()
}
return@withContext true
}
}
//通过从根目录进入的方式可以创建文件
fun Context.findHSDataFilesDir(
fileName: String? = null,
): DocumentFile? {
DocumentFile.fromTreeUri(
applicationContext,
HS_DATA_FILE_DIR
)?.apply {
// listFiles().forEach {
// if (it.name == applicationId) {
val filesDir = this.findFile("files") ?: return null
return if (fileName == null) filesDir else filesDir.findFile(
fileName
)
// }
// }
}
return null
// if (applicationId == HUAWEI_HS_APPLICATION_ID) {
// return null
// }
//
// return findHSDataFilesDir(fileName, HUAWEI_HS_APPLICATION_ID)
}
fun ModuleItemCardBinding.bindCard(card: Card) {
name.text = card.name
cost.text = card.cost.toString()
card.rarity?.apply {
this@bindCard.cost.setBackgroundColor(
ResourcesCompat.getColor(
root.context.resources,
colorRes,
null
)
)
}
Glide.with(imageTile)
.load("https://art.hearthstonejson.com/v1/tiles/${card.id}.png")
.into(imageTile)
}
fun showCardImageDialog(context: Context, cardId: String) {
val binding = ModuleDialogCardPreviewBinding.inflate(LayoutInflater.from(context))
AlertDialog.Builder(context)
.show().apply {
window?.run {
setContentView(binding.root)
binding.root.setOnClickListener {
dismiss()
}
//去掉对话框的白色背景
setBackgroundDrawableResource(android.R.color.transparent)
}
}
Glide.with(binding.image)
.load("https://art.hearthstonejson.com/v1/render/latest/zhCN/512x/${cardId}.png")
.placeholder(R.mipmap.ic_launcher)
.into(binding.image)
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/api/HearthStoneJsonApi.kt
================================================
package com.ke.hs_tracker.module.api
import com.ke.hs_tracker.module.entity.Card
import retrofit2.http.GET
import retrofit2.http.Path
interface HearthStoneJsonApi {
/**
* 获取卡牌数据
*/
@GET("v1/{versionCode}/{region}/cards.json")
suspend fun getCardJsonList(
@Path("versionCode") versionCode: String,
@Path("region") region: String,
): List
companion object {
const val BASE_URL = "https://api.hearthstonejson.com/"
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/data/PreferenceStorage.kt
================================================
package com.ke.hs_tracker.module.data
import android.content.Context
import androidx.appcompat.app.AppCompatDelegate
import com.tencent.mmkv.MMKV
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
interface PreferenceStorage {
var theme: Int
/**
* 保存日志文件
*/
var saveLogFile: Boolean
var floatingEnable: Boolean
var crash: String?
}
class PreferenceStorageImpl @Inject constructor(
@ApplicationContext private val context: Context
) : PreferenceStorage {
init {
MMKV.initialize(context)
}
private val mmkv = MMKV.defaultMMKV()
override var theme: Int
get() = mmkv.getInt(KEY_THEME, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
set(value) {
AppCompatDelegate.setDefaultNightMode(value)
mmkv.encode(KEY_THEME, value)
}
override var saveLogFile: Boolean
get() = mmkv.getBoolean(KEY_SAVE_LOG_FILE, false)
set(value) {
mmkv.encode(KEY_SAVE_LOG_FILE, value)
}
override var floatingEnable: Boolean
get() = mmkv.getBoolean(KEY_FLOATING_ENABLE, true)
set(value) {
mmkv.putBoolean(KEY_FLOATING_ENABLE, value)
}
override var crash: String?
get() = mmkv.getString(KEY_CRASH, null)
set(value) {
mmkv.putString(KEY_CRASH, value)
}
companion object {
private const val KEY_THEME = "KEY_THEME"
private const val KEY_SAVE_LOG_FILE = "KEY_SAVE_LOG_FILE"
private const val KEY_FLOATING_ENABLE = "KEY_FLOATING_ENABLE"
private const val KEY_CRASH = "KEY_CRASH"
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/db/CardClassesConvert.kt
================================================
package com.ke.hs_tracker.module.db
import androidx.room.TypeConverter
import com.ke.hs_tracker.module.entity.CardClass
class CardClassesConvert {
@TypeConverter
fun longToClasses(value: Long?): List {
if (value == null) {
return emptyList()
}
return CardClass.values()
.map {
it to (1L shl it.ordinal)
}
.filter {
it.second and value == it.second
}
.map { it.first }
}
@TypeConverter
fun classesToLong(list: List): Long {
if (list.isEmpty()) {
return 0
}
var result = 0L
list.map {
1L shl it.ordinal
}.forEach {
result = result or it
}
return result
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/db/CardDao.kt
================================================
package com.ke.hs_tracker.module.db
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import com.ke.hs_tracker.module.entity.Card
@Dao
interface CardDao {
@Query("select * from card")
suspend fun getAll(): List
@Insert
suspend fun insert(list: List)
@Query("delete from card")
suspend fun deleteAll()
@Query("select COUNT(id) from card")
suspend fun getCount(): Int
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/db/Database.kt
================================================
package com.ke.hs_tracker.module.db
import androidx.room.*
import androidx.room.Database
import androidx.room.migration.AutoMigrationSpec
import com.ke.hs_tracker.module.entity.Card
const val DATABASE_VERSION = 4
@Database(
entities = [Card::class, Game::class, ZonePositionChangedEvent::class],
version = DATABASE_VERSION,
exportSchema = true,
autoMigrations = [
AutoMigration(
from = 3,
to = 4,
spec = com.ke.hs_tracker.module.db.Database.RenameAttachToAttackMigration::class
)
]
)
@TypeConverters(
CardClassesConvert::class, MechanicsListConvert::class
)
abstract class Database : RoomDatabase() {
@RenameColumn(tableName = "card", fromColumnName = "attach", toColumnName = "attack")
class RenameAttachToAttackMigration : AutoMigrationSpec
abstract fun cardDao(): CardDao
abstract fun gameDao(): GameDao
abstract fun zonePositionChangedEventDao(): ZonePositionChangedEventDao
companion object {
const val VERSION = 3
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/db/Game.kt
================================================
package com.ke.hs_tracker.module.db
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.ke.hs_tracker.module.entity.CardClass
import com.ke.hs_tracker.module.entity.FormatType
import com.ke.hs_tracker.module.entity.GameType
import com.squareup.moshi.JsonClass
import java.util.*
@JsonClass(generateAdapter = true)
@Entity(tableName = "game")
data class Game(
@PrimaryKey(autoGenerate = false)
val id: String = UUID.randomUUID().toString(),
//并不是唯一标识
@ColumnInfo(name = "build_number")
val buildNumber: String = "",
@ColumnInfo(name = "game_type")
var gameType: GameType = GameType.Unknown,
@ColumnInfo(name = "format_type")
var formatType: FormatType = FormatType.Unknown,
@ColumnInfo(name = "scenario_id")
var scenarioID: Int = 0,
@ColumnInfo(name = "user_name")
var userName: String = "",
@ColumnInfo(name = "opponent_name")
var opponentName: String = "",
@ColumnInfo(name = "is_user_first")
var isUserFirst: Boolean? = null,
@ColumnInfo(name = "user_deck_name")
var userDeckName: String = "",
@ColumnInfo(name = "user_deck_code")
var userDeckCode: String = "",
@ColumnInfo(name = "is_user_win")
var isUserWin: Boolean? = null,
@ColumnInfo(name = "user_hero")
var userHero: CardClass? = null,
@ColumnInfo(name = "opponent_class")
var opponentHero: CardClass? = null,
@ColumnInfo(name = "start_time")
var startTime: Long = 0,
@ColumnInfo(name = "end_time")
var endTime: Long = 0
) {
// val userName: String
// get() = if (isUserFirst == true) player1Name else player2Name
// var opponentName: String
// get() = if (isUserFirst == true) player2Name else player1Name
// set(value) {
// if (isUserFirst == true) {
// player2Name = value
// } else {
// player1Name = value
// }
// }
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/db/GameDao.kt
================================================
package com.ke.hs_tracker.module.db
import androidx.room.*
import com.ke.hs_tracker.module.entity.CardClass
@Dao
interface GameDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(game: Game)
/**
* 插入所有
*/
@Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insert(games: List)
@Update
suspend fun update(game: Game)
/**
* 删除全部
*/
@Query("delete from game")
suspend fun deleteAll()
/**
* 查询所有
*/
@Query("select * from game")
suspend fun getAll(): List
/**
* 查找用户某个英雄的总对局
*/
@Query("select * from game where user_hero = :cardClass")
suspend fun getByHero(cardClass: CardClass): List
/**
* 获取总的对局数
*/
@Query("select count(*) from game")
suspend fun getGameCount(): Int
/**
* 获取玩家胜率对局数
*/
@Query("select count(*) from game where is_user_win = 1")
suspend fun getUserWinCount(): Int
/**
* 根据卡组代码和名称查询
*/
@Query("select * from game where user_deck_code = :code and user_deck_name = :name")
suspend fun getByDeckCodeAndName(
code: String,
name: String
): List
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/db/MechanicsListConvert.kt
================================================
package com.ke.hs_tracker.module.db
import androidx.room.TypeConverter
import com.ke.hs_tracker.module.entity.CardClass
import com.ke.hs_tracker.module.entity.Mechanics
import com.ke.hs_tracker.module.entity.MechanicsAdapter
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import org.json.JSONArray
import javax.inject.Inject
class MechanicsListConvert {
@TypeConverter
fun stringToClasses(value: String?): List {
if (value == null) {
return emptyList()
}
val result = mutableListOf()
val jsonArray = JSONArray(value)
val mechanicsAdapter = MechanicsAdapter()
val size = jsonArray.length()
for (index in 0 until size) {
val m = mechanicsAdapter.fromJson(jsonArray.get(index).toString())
result.add(m)
}
return result
// return Mechanics.values()
// .map {
// it to (1L shl it.ordinal)
// }
// .filter {
// it.second and value == it.second
// }
// .map { it.first }
}
@TypeConverter
fun classesToString(list: List): String {
val jsonArray = JSONArray()
if (list.isEmpty()) {
return jsonArray.toString()
}
list.forEach {
jsonArray.put(it.name)
}
return jsonArray.toString()
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/db/ZonePositionChangedEvent.kt
================================================
package com.ke.hs_tracker.module.db
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.ke.hs_tracker.module.entity.Zone
import com.squareup.moshi.JsonClass
import java.util.*
@JsonClass(generateAdapter = true)
@Entity(tableName = "zone_position_updated_event")
data class ZonePositionChangedEvent(
@PrimaryKey(autoGenerate = false)
val id: String = UUID.randomUUID().toString(),
@ColumnInfo(name = "game_id")
var gameId: String = "",
@ColumnInfo(name = "entity_id")
val entityId: Int,
@ColumnInfo(name = "card_id")
var cardId: String?,
@ColumnInfo(name = "card_name")
var cardName: String? = null,
@ColumnInfo(name = "is_user")
var isUser: Boolean = false,
@ColumnInfo(name = "current_zone")
var currentZone: Zone,
@ColumnInfo(name = "new_zone")
var newZone: Zone,
@ColumnInfo(name = "current_position")
val currentPosition: Int,
@ColumnInfo(name = "new_position")
val newPosition: Int
) {
//TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=63 zone=DECK zonePos=0 cardId= player=2] tag=ZONE value=HAND
//TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=63 zone=DECK zonePos=0 cardId= player=2] tag=ZONE_POSITION value=1
fun plus(event: ZonePositionChangedEvent): ZonePositionChangedEvent {
if (event.entityId != entityId) {
throw RuntimeException("两个要进行加的 entity id 不一致 $event")
}
if (currentZone == Zone.Deck && newZone == Zone.Deck && newPosition != 0) {
//星界导致插入一张卡牌到事件中
currentZone = event.newZone
newZone = event.newZone
}
if (currentZone != event.currentZone) {
throw RuntimeException("两个要进行加的 zone 不一致 $event")
}
val newPosAndZone =
if (this.currentZone == event.newZone) event.newPosition to newZone else newPosition to event.newZone
return ZonePositionChangedEvent(
id,
gameId,
entityId,
cardId,
cardName,
isUser,
currentZone,
newPosAndZone.second,
currentPosition,
newPosAndZone.first
)
}
fun plusPlus(
second: ZonePositionChangedEvent,
third: ZonePositionChangedEvent
): ZonePositionChangedEvent {
val oldZone = currentZone
val list = listOf(this, second, third)
val newZone = list.map {
it.newZone
}.firstOrNull {
it != oldZone
}
val newPosition = third.newPosition
return ZonePositionChangedEvent(
id,
gameId,
entityId,
cardId,
cardName,
isUser,
currentZone,
newZone ?: this.newZone,
currentPosition,
newPosition
)
}
}
//public operator fun ZonePositionChangedEvent.plus(event: ZonePositionChangedEvent): ZonePositionChangedEvent {
// if (entityId != event.entityId) {
// throw RuntimeException("不支持不同 entityId 的 event 相加")
// }
// if (this == event) {
// return this
// }
//}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/db/ZonePositionChangedEventDao.kt
================================================
package com.ke.hs_tracker.module.db
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
@Dao
interface ZonePositionChangedEventDao {
@Insert
suspend fun insertAll(list: List)
@Query("select * from zone_position_updated_event where game_id = :gameId")
suspend fun getAllByGameId(gameId: String): List
/**
* 获取所有
*/
@Query("select * from zone_position_updated_event")
suspend fun getAll(): List
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/di/CoroutinesModule.kt
================================================
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.ke.hs_tracker.module.di
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
@InstallIn(SingletonComponent::class)
@Module
object CoroutinesModule {
@DefaultDispatcher
@Provides
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
@IoDispatcher
@Provides
fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
@MainDispatcher
@Provides
fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
@MainImmediateDispatcher
@Provides
fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/di/CoroutinesQualifiers.kt
================================================
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.ke.hs_tracker.module.di
import javax.inject.Qualifier
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class DefaultDispatcher
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class IoDispatcher
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class MainDispatcher
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class MainImmediateDispatcher
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class ApplicationScope
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/di/LogFileDirQualifiers.kt
================================================
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.ke.hs_tracker.module.di
import javax.inject.Qualifier
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class LocalLogFileDir
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class RealLogFileDir
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/di/Module.kt
================================================
package com.ke.hs_tracker.module.di
import android.content.Context
import androidx.room.Room
import com.ke.hs_tracker.module.api.HearthStoneJsonApi
import com.ke.hs_tracker.module.data.PreferenceStorage
import com.ke.hs_tracker.module.data.PreferenceStorageImpl
import com.ke.hs_tracker.module.db.*
import com.ke.hs_tracker.module.entity.*
import com.ke.hs_tracker.module.parser.*
import com.squareup.moshi.Moshi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class Module {
@Provides
@Singleton
fun provideHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.connectTimeout(30, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideMoshi(): Moshi {
return Moshi.Builder()
.add(CardClassAdapter())
.add(CardTypeAdapter())
.add(RarityAdapter())
.add(SpellSchoolAdapter())
.add(MechanicsAdapter())
.add(RaceAdapter())
.build()
}
@Provides
@Singleton
fun provideHearthStoneJsonApi(
okHttpClient: OkHttpClient,
moshi: Moshi
): HearthStoneJsonApi {
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.client(okHttpClient)
.baseUrl(HearthStoneJsonApi.BASE_URL)
.build()
.create(HearthStoneJsonApi::class.java)
}
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): Database {
return Room.databaseBuilder(
context,
Database::class.java,
"card.db"
).build()
}
@Provides
fun provideCardDao(database: Database): CardDao {
return database.cardDao()
}
@Provides
fun provideGameDao(database: Database): GameDao = database.gameDao()
@Provides
fun provideZonePositionChangedEventDao(database: Database): ZonePositionChangedEventDao =
database.zonePositionChangedEventDao()
@Provides
@Singleton
fun providePreferenceStorage(preferenceStorageImpl: PreferenceStorageImpl): PreferenceStorage {
return preferenceStorageImpl
}
@Provides
fun provideBlockTagStack(impl: BlockTagStackImpl): BlockTagStack {
return impl
}
@Provides
fun providePowerParser(powerParserImpl: PowerParserImpl): PowerParser {
return powerParserImpl
}
@Provides
fun providePowerTagHandler(powerTagHandlerImpl: PowerTagHandlerImpl): PowerTagHandler =
powerTagHandlerImpl
@Provides
fun provideDeckCardObserver(deckCardObserverImpl: DeckCardObserverImpl): DeckCardObserver =
deckCardObserverImpl
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/domain/ClearCardTableUseCase.kt
================================================
package com.ke.hs_tracker.module.domain
import com.ke.hs_tracker.module.db.CardDao
import com.ke.hs_tracker.module.di.IoDispatcher
import com.ke.mvvm.base.domian.UseCase
import kotlinx.coroutines.CoroutineDispatcher
import javax.inject.Inject
class ClearCardTableUseCase @Inject constructor(
private val cardDao: CardDao,
@IoDispatcher dispatcher: CoroutineDispatcher
) : UseCase(dispatcher) {
override suspend fun execute(parameters: Unit) {
cardDao.deleteAll()
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/domain/GetAllCardUseCase.kt
================================================
package com.ke.hs_tracker.module.domain
import com.ke.hs_tracker.module.db.CardDao
import com.ke.hs_tracker.module.di.IoDispatcher
import com.ke.hs_tracker.module.entity.Card
import com.ke.mvvm.base.domian.UseCase
import kotlinx.coroutines.CoroutineDispatcher
import javax.inject.Inject
class GetAllCardUseCase @Inject constructor(
@IoDispatcher dispatcher: CoroutineDispatcher,
private val cardDao: CardDao
) :
UseCase>(dispatcher) {
override suspend fun execute(parameters: Unit): List {
return cardDao.getAll()
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/domain/GetCardListUseCase.kt
================================================
package com.ke.hs_tracker.module.domain
import com.ke.hs_tracker.module.api.HearthStoneJsonApi
import com.ke.hs_tracker.module.di.IoDispatcher
import com.ke.hs_tracker.module.entity.Card
import com.ke.mvvm.base.domian.UseCase
import kotlinx.coroutines.CoroutineDispatcher
import javax.inject.Inject
class GetCardListUseCase @Inject constructor(
@IoDispatcher dispatcher: CoroutineDispatcher,
private val api: HearthStoneJsonApi
) :
UseCase, List>(dispatcher) {
override suspend fun execute(parameters: Pair): List {
return api.getCardJsonList(
parameters.first,
parameters.second
)
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/domain/GetDatabaseCardCountUseCase.kt
================================================
package com.ke.hs_tracker.module.domain
import com.ke.hs_tracker.module.db.CardDao
import com.ke.hs_tracker.module.di.IoDispatcher
import com.ke.mvvm.base.domian.UseCase
import kotlinx.coroutines.CoroutineDispatcher
import javax.inject.Inject
class GetDatabaseCardCountUseCase @Inject constructor(
@IoDispatcher dispatcher: CoroutineDispatcher,
private val cardDao: CardDao
) : UseCase(dispatcher) {
override suspend fun execute(parameters: Unit): Int {
return cardDao.getCount()
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/domain/GetLocalLogDirUseCase.kt
================================================
package com.ke.hs_tracker.module.domain
import android.content.Context
import androidx.documentfile.provider.DocumentFile
import com.ke.hs_tracker.module.di.IoDispatcher
import com.ke.hs_tracker.module.findHSDataFilesDir
import com.ke.mvvm.base.domian.UseCase
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import java.io.File
import javax.inject.Inject
class GetLocalLogDirUseCase @Inject constructor(
@IoDispatcher dispatcher: CoroutineDispatcher,
@ApplicationContext private val context: Context
) :
UseCase(dispatcher) {
override suspend fun execute(parameters: Unit): DocumentFile? {
val logsDir = context.getExternalFilesDir("Logs")!!
if (!logsDir.exists()) {
logsDir.mkdir()
}
// val decksFile = File(logsDir, "Decks.log")
// decksFile.createNewFile()
// context.assets.open("Decks.log").reader()
// .apply {
// decksFile.writeText(readText())
// }
val powerFile = File(logsDir, "Power.log")
powerFile.createNewFile()
return context.findHSDataFilesDir("Logs")
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/domain/GetRealLogDirUseCase.kt
================================================
package com.ke.hs_tracker.module.domain
import android.content.Context
import androidx.documentfile.provider.DocumentFile
import com.ke.hs_tracker.module.di.IoDispatcher
import com.ke.hs_tracker.module.findHSDataFilesDir
import com.ke.hs_tracker.module.log
import com.ke.mvvm.base.domian.UseCase
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import javax.inject.Inject
class GetRealLogDirUseCase @Inject constructor(
@IoDispatcher dispatcher: CoroutineDispatcher,
@ApplicationContext private val context: Context
) :
UseCase(dispatcher) {
// private var documentFile: DocumentFile? = null
override suspend fun execute(parameters: Unit): DocumentFile? {
// if (documentFile != null) {
// return documentFile
// }
val logsDir = context.findHSDataFilesDir("Logs")
?: return null
val listFiles = logsDir.listFiles()
return listFiles.filter {
"${it.name} ${it.lastModified()}".log()
(it.name?.startsWith("Hearthstone") ?: false) && it.isDirectory
}.maxByOrNull {
it.lastModified()
}?.apply {
"找到了目标目录 ${this.name} ${this.lastModified()}".log()
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/domain/GetSaveLogFileEnableUseCase.kt
================================================
package com.ke.hs_tracker.module.domain
import com.ke.hs_tracker.module.data.PreferenceStorage
import com.ke.hs_tracker.module.di.IoDispatcher
import com.ke.mvvm.base.domian.UseCase
import kotlinx.coroutines.CoroutineDispatcher
import javax.inject.Inject
class GetSaveLogFileEnableUseCase @Inject constructor(
private val preferenceStorage: PreferenceStorage,
@IoDispatcher dispatcher: CoroutineDispatcher
) :
UseCase(dispatcher) {
override suspend fun execute(parameters: Unit): Boolean {
return preferenceStorage.saveLogFile
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/domain/InsertCardListToDatabaseUseCase.kt
================================================
package com.ke.hs_tracker.module.domain
import com.ke.hs_tracker.module.db.CardDao
import com.ke.hs_tracker.module.di.IoDispatcher
import com.ke.hs_tracker.module.entity.Card
import com.ke.mvvm.base.domian.UseCase
import kotlinx.coroutines.CoroutineDispatcher
import javax.inject.Inject
class InsertCardListToDatabaseUseCase @Inject constructor(
@IoDispatcher dispatcher: CoroutineDispatcher,
private val cardDao: CardDao
) : UseCase, Unit>(dispatcher) {
override suspend fun execute(parameters: List) {
cardDao.insert(parameters)
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/domain/ParseDeckCodeUseCase.kt
================================================
package com.ke.hs_tracker.module.domain
import android.util.Base64
import com.ke.hs_tracker.module.db.CardDao
import com.ke.hs_tracker.module.di.IoDispatcher
import com.ke.hs_tracker.module.entity.Card
import com.ke.hs_tracker.module.entity.CardBean
import com.ke.mvvm.base.domian.UseCase
import kotlinx.coroutines.CoroutineDispatcher
import javax.inject.Inject
class ParseDeckCodeUseCase
@Inject constructor(
private val cardDao: CardDao,
@IoDispatcher dispatcher: CoroutineDispatcher
) : UseCase>(dispatcher) {
override suspend fun execute(parameters: String): List {
val allCards = cardDao.getAll()
val byteArray =
Base64.decode(parameters, Base64.DEFAULT)
// deckString.encodeToByteArray().decodeBase64()
//[0, 1, 2, 1, -3, 4, 8, -116, -71, 3, -32, -52, 3, -65, -32, 3, -109, -31, 3, -78, -9, 3, -96, -118, 4, -104, -115, 4, -4, -98, 4, 11, -63, -72, 3, -127, -65, 3, -51, -50, 3, -5, -35, 3, -21, -34, 3, -48, -20, 3, -47, -20, 3, -89, -9, 3, -118, -115, 4, -3, -98, 4, -5, -94, 4, 0]
// val size = byteArray.size
val byteList = mutableListOf()
byteArray.forEach {
byteList.add(it)
}
val keep = byteList.removeFirst()//移除第一个保留的字段
// assert(byteList.removeFirst().toInt() == 0)
// assert(byteList.removeFirst().toInt() != 0)//总是1
val version = byteList.removeFirst()
val cardType = byteList.removeFirst()//1是标准 2是狂野
val cardList = mutableListOf()
val heroCount = getVarInt(byteList)
for (i in 0 until heroCount) {
val id = getVarInt(byteList)
val hero =
findByDbfId(id, allCards) ?: throw IllegalArgumentException("找不到id为 $id 的卡牌")
// hero.count = heroCount
cardList.add(hero.updateCount(heroCount))
}
for (i in 1..3) {
val c = getVarInt(byteList)
for (j in 0 until c) {
val dbfId = getVarInt(byteList)
val count: Int
if (i == 3) {
count = getVarInt(byteList)
} else {
count = i
}
// result.cards.add(Card(dbfId, count))
val jsonObject =
findByDbfId(dbfId, allCards)
?: throw IllegalArgumentException("找不到id为 $dbfId 的卡牌")
// jsonObject.count = count
cardList.add(jsonObject.updateCount(count))
}
}
//移除英雄
cardList.removeFirst()
cardList.sortBy {
it.card.cost
}
return cardList
}
/**
* 获得无符号int
*/
private fun getVarInt(src: MutableList): Int {
var result = 0
var shift = 0
var b: Int
do {
if (shift >= 32) {
// Out of range
throw IndexOutOfBoundsException("varint too long")
}
// Get 7 bits from next byte
b = src.removeFirst().toInt()
result = result or (b and 0x7F shl shift)
shift += 7
} while (b and 0x80 != 0)
return result
}
private fun findByDbfId(id: Int, cardEntityList: List): CardBean? {
val cardEntity = cardEntityList.find { it.dbfId == id } ?: return null
return CardBean(cardEntity)
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/domain/SaveLogFileUseCase.kt
================================================
package com.ke.hs_tracker.module.domain
import android.content.Context
import android.net.Uri
import com.ke.hs_tracker.module.data.PreferenceStorage
import com.ke.mvvm.base.domian.UseCase
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import java.io.File
import java.io.InputStream
import javax.inject.Inject
class SaveLogFileUseCase @Inject constructor(
private val preferenceStorage: PreferenceStorage,
@ApplicationContext private val context: Context
) :
UseCase, Boolean>(Dispatchers.IO) {
override suspend fun execute(parameters: Pair): Boolean {
if (!preferenceStorage.saveLogFile) {
return false
}
val targetFileDir = File(context.getExternalFilesDir(null), "logs")
if (!targetFileDir.exists()) {
targetFileDir.mkdir()
}
val target = File(targetFileDir, parameters.first + ".log")
if (!target.exists()) {
target.createNewFile()
}
val text = parameters.second.bufferedReader().readText()
target.writeText(text)
return true
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/domain/SetSaveLogFileEnableUseCase.kt
================================================
package com.ke.hs_tracker.module.domain
import com.ke.hs_tracker.module.data.PreferenceStorage
import com.ke.hs_tracker.module.di.IoDispatcher
import com.ke.mvvm.base.domian.UseCase
import kotlinx.coroutines.CoroutineDispatcher
import javax.inject.Inject
class SetSaveLogFileEnableUseCase @Inject constructor(
private val preferenceStorage: PreferenceStorage,
@IoDispatcher dispatcher: CoroutineDispatcher
) :
UseCase(dispatcher) {
override suspend fun execute(parameters: Boolean): Boolean {
preferenceStorage.saveLogFile = parameters
return parameters
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/domain/WriteLogConfigFileUseCase.kt
================================================
package com.ke.hs_tracker.module.domain
import android.content.Context
import androidx.documentfile.provider.DocumentFile
import com.ke.hs_tracker.module.di.IoDispatcher
import com.ke.hs_tracker.module.findHSDataFilesDir
import com.ke.hs_tracker.module.log
import com.ke.hs_tracker.module.writeLogConfigFile
import com.ke.mvvm.base.domian.UseCase
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import javax.inject.Inject
class WriteLogConfigFileUseCase @Inject constructor(
@ApplicationContext private val context: Context,
@IoDispatcher private val dispatcher: CoroutineDispatcher
) :
UseCase(dispatcher) {
override suspend fun execute(parameters: Boolean): Boolean {
return context.writeLogConfigFile(parameters)
// val documentFile = context.findHSDataFilesDir() ?: return false
//
// val fileName = "log.config"
// val file = context.findHSDataFilesDir(fileName)
// if (file != null) {
// //文件已存在
// "log.config文件已存在".log()
//
// if (parameters) {
// file.delete()
// val configFile = documentFile.createFile("plain/text", fileName)
// ?: return false
//
// write(configFile)
// } else {
// return true
// }
// }
// val configFile = documentFile.createFile("plain/text", fileName)
// ?: return false
//
// write(configFile)
//
// return true
}
private fun write(configFile: DocumentFile) {
context.contentResolver.openOutputStream(configFile.uri)?.use {
context.assets.open("log.config")
.copyTo(it)
it.flush()
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/BlockType.kt
================================================
package com.ke.hs_tracker.module.entity
enum class BlockType {
/**
* 攻击
*/
Attack,
/**
* 死亡
*/
Deaths,
/**
* 触发
*/
Trigger,
/**
* 打出一张卡牌
*/
Play,
/**
* 卡牌生效
*/
Power,
/**
* 交易
*/
Trade
}
internal fun String.toBlockType(fallback: BlockType = BlockType.Trigger): BlockType {
BlockType.values().forEach {
if (it.name.equals(this, true)) {
return it
}
}
return fallback
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/Card.kt
================================================
package com.ke.hs_tracker.module.entity
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.squareup.moshi.JsonClass
import kotlinx.parcelize.Parcelize
@Entity(tableName = "card")
@JsonClass(generateAdapter = true)
@Parcelize
data class Card(
/**
* 画家
*/
val artist: String? = null,
/**
* 名称
*/
val name: String,
/**
* 费用
*/
val cost: Int = 0,
@PrimaryKey
val id: String,
val dbfId: Int,
val text: String = "",
//属于哪个版本 例如 TGT
val set: String = "",
val type: CardType = CardType.None,
val cardClass: CardClass? = null,
val classes: List = emptyList(),
val mechanics: List = emptyList(),
/**
* 个性介绍
*/
val flavor: String = "",
val rarity: Rarity? = null,
/**
* 武器耐久
*/
val durability: Int = 0,
/**
* 英雄牌的护甲
*/
val armor: Int = 0,
val collectible: Boolean = false,
/**
* 法术类型
*/
val spellSchool: SpellSchool? = null,
/**
* 随从种族
*/
val race: Race? = null,
/**
* 随从攻击力
*/
val attack: Int = 0,
/**
* 随从生命值
*/
val health: Int = 0,
) : Parcelable
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/CardBean.kt
================================================
package com.ke.hs_tracker.module.entity
data class CardBean(
val card: Card,
val count: Int = 0
) {
//防止在使用MutableStateFlow时无法更新数据
// override fun equals(other: Any?): Boolean {
// return false
// }
//
// override fun hashCode(): Int {
// return Random.nextInt()
// }
fun updateCount(count: Int): CardBean {
return CardBean(card, count)
}
fun toCardList(): List {
val list = mutableListOf()
repeat(count) {
list.add(card)
}
return list
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/CardClass.kt
================================================
package com.ke.hs_tracker.module.entity
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.ke.hs_tracker.module.R
import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
enum class CardClass(
@StringRes
val titleRes: Int,
@ColorRes
val color: Int = 0,
@DrawableRes
val roundIcon: Int? = null,
val isHero: Boolean = true
) {
/**
* 法师
*/
Mage(R.string.module_mage, R.color.module_mage, R.drawable.module_image_round_mage),
/**
* 术士
*/
Warlock(R.string.module_warlock, R.color.module_warlock, R.drawable.module_image_round_warlock),
/**
* 牧师
*/
Priest(R.string.module_priest, R.color.module_priest, R.drawable.module_image_round_priest),
/**
* 德鲁伊
*/
Druid(R.string.module_druid, R.color.module_druid, R.drawable.module_image_round_druid),
/**
* 盗贼
*/
Rogue(R.string.module_rogue, R.color.module_rogue, R.drawable.module_image_round_rogue),
/**
* 萨满
*/
Shaman(R.string.module_shaman, R.color.module_shaman, R.drawable.module_image_round_shaman),
/**
* 猎人
*/
Hunter(R.string.module_hunter, R.color.module_hunter, R.drawable.module_image_round_hunter),
/**
* 圣骑士
*/
Paladin(R.string.module_paladin, R.color.module_paladin, R.drawable.module_image_round_paladin),
/**
* 战士
*/
Warrior(R.string.module_warrior, R.color.module_warrior, R.drawable.module_image_round_warrior),
/**
* 恶魔猎手
*/
DemonHunter(
R.string.module_demon_hunter,
R.color.module_demon_hunter,
R.drawable.module_image_round_demon_hunter
),
/**
* 中立
*/
Neutral(R.string.module_neutral, R.color.module_neutral, isHero = false),
/**
* 威兹班
*/
Whizbang(R.string.module_whizbang, 0, isHero = false),
/**
* 梦境牌
*/
Dream(R.string.module_dream, 0, isHero = false),
/**
* 死亡骑士
*/
DeathKnight(R.string.module_death_knight, R.color.module_death_knight, isHero = false),
}
class CardClassAdapter {
@FromJson
fun fromJson(value: String): CardClass {
return EnumMoshiAdapter.fromJson(value, CardClass.values())
}
@ToJson
fun toJson(cardClass: CardClass) = cardClass.name.uppercase()
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/CardType.kt
================================================
package com.ke.hs_tracker.module.entity
import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
enum class CardType {
/**
* 英雄
*/
Hero,
/**
* 英雄技能
*/
HeroPower,
/**
*衍生牌
*/
Enchantment,
/**
* 法术
*/
Spell,
/**
* 随从
*/
Minion,
/**
* 武器
*/
Weapon,
None
}
class CardTypeAdapter {
@FromJson
fun fromJson(value: String): CardType {
return CardType.values()
.find { it.name.equals(value.replace("_", ""), true) } ?: CardType.None
}
@ToJson
fun toJson(cardType: CardType) = cardType.name.uppercase()
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/CurrentDeck.kt
================================================
package com.ke.hs_tracker.module.entity
data class CurrentDeck(
val name: String,
val code: String
)
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/Entity.kt
================================================
package com.ke.hs_tracker.module.entity
import com.ke.hs_tracker.module.parser.PowerParserImpl
//有三种形式
//1,Entity=[entityName=腐食研习 id=29 zone=PLAY zonePos=0 cardId=SCH_300 player=1]
//2,Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=29 zone=HAND zonePos=3 cardId= player=1]
//3,Entity=失落的裤子#5629
data class Entity(
val entityName: String,
val gameCardType: GameCardType? = null,
val id: Int = -1,
val zone: Zone = Zone.Play,
val zonePosition: Int = -1,
val cardId: String? = null,
val player: Int = -1
) {
val entityType: EntityType
get() = when {
gameCardType == null && id == -1 && zone == Zone.Play && zonePosition == -1 && cardId == null && player == -1 -> EntityType.Name
gameCardType == GameCardType.Invalid -> EntityType.Invalid
else -> EntityType.Clear
}
val isGameEntity: Boolean
get() = entityName == "GameEntity"
val isUserEntity: Boolean
get() = entityName.contains("#")
companion object {
internal fun createFromContent(content: String): Entity? {
if (content == "0") {
return null
}
var matchResult = PowerParserImpl.FULL_ENTITY_CONTENT1_PATTERN.matchEntire(content)
if (matchResult != null) {
return Entity(
matchResult.groupValues[1],
matchResult.groupValues[2].toCardType(GameCardType.Invalid),
matchResult.groupValues[3].toIntOrNull() ?: 0,
matchResult.groupValues[4].toZone(),
matchResult.groupValues[5].toIntOrNull() ?: 0,
matchResult.groupValues[6].ifBlank { null },
matchResult.groupValues[7].toIntOrNull() ?: 0
)
}
matchResult = PowerParserImpl.FULL_ENTITY_CONTENT2_PATTERN.matchEntire(content)
?: return Entity(content)
return Entity(
matchResult.groupValues[1],
GameCardType.Invalid,
matchResult.groupValues[2].toIntOrNull() ?: 0,
matchResult.groupValues[3].toZone(),
matchResult.groupValues[4].toIntOrNull() ?: 0,
matchResult.groupValues[5].ifBlank { null },
matchResult.groupValues[6].toIntOrNull() ?: 0
)
}
}
}
enum class EntityType {
//Entity=失落的裤子#5629
Name,
//Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=29 zone=HAND zonePos=3 cardId= player=1]
Invalid,
//Entity=[entityName=腐食研习 id=29 zone=PLAY zonePos=0 cardId=SCH_300 player=1]
Clear
}
data class GameEntity(
val gameCardType: GameCardType,
val entityId: Int
)
/**
* 玩家
*/
data class Player(
val entityId: Int,
val playerId: Int,
val controller: Int,
val gameCardType: GameCardType,
val heroEntity: Int,
/**
* 手牌上限
*/
val maxHandSize: Int,
/**
* 起始手牌
*/
val startHandSize: Int,
val teamId: Int,
/**
* 费用上限 一般为10
*/
val maxResources: Int
) {
companion object {
internal fun fromMap(map: Map): Player {
return Player(
entityId = map["entityid"]?.toIntOrNull() ?: 0,
playerId = map["playerid"]?.toIntOrNull() ?: 0,
controller = map["controller"]?.toIntOrNull() ?: 0,
gameCardType = (map["cardtype"] ?: "").toCardType(),
heroEntity = map["heroentity"]?.toIntOrNull() ?: 0,
maxHandSize = map["maxhandsize"]?.toIntOrNull() ?: 0,
startHandSize = map["starthandsize"]?.toIntOrNull() ?: 0,
teamId = map["teamid"]?.toIntOrNull() ?: 0,
maxResources = map["maxresources"]?.toIntOrNull() ?: 0
)
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/EntityWithPayload.kt
================================================
package com.ke.hs_tracker.module.entity
data class EntityWithPayload(
val entity: Entity,
private val payloads: MutableMap = mutableMapOf()
) {
fun add(pair: Pair) {
payloads[pair.first] = pair.second
}
fun getPayloads(): Map = payloads
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/EnumMoshiAdapter.kt
================================================
package com.ke.hs_tracker.module.entity
object EnumMoshiAdapter {
fun > fromJson(value: String, enumList: Array, fallback: T? = null): T {
val enum = enumList
.find { it.name.equals(value.replace("_", ""), true) } ?: fallback
if (enum == null) {
throw IllegalArgumentException("错误的 $value 类型")
}
return enum
}
fun > toJson(value: T) = value.name.uppercase()
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/FormatType.kt
================================================
package com.ke.hs_tracker.module.entity
import androidx.annotation.StringRes
import com.ke.hs_tracker.module.R
enum class FormatType(@StringRes val title: Int) {
Unknown(R.string.module_unknown),
/**
* 标准
*/
Standard(R.string.module_standard),
/**
* 狂野
*/
Wild(R.string.module_wild),
/**
* 经典
*/
Classic(R.string.module_classic)
}
val String.toFormatType: FormatType
get() = FormatType.values().find {
this.contains(it.name, true)
} ?: FormatType.Unknown
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/GameCardType.kt
================================================
package com.ke.hs_tracker.module.entity
/**
* 卡牌类型
*/
enum class GameCardType {
Game,
/**
* 玩家
*/
Player,
/**
* 英雄
*/
Hero,
/**
* 英雄技能
*/
HeroPower,
/**
* 牌库中的牌的状态
*/
Invalid,
/**
* 随从身上的buff或战场上的buff(例如下一张法强怪法力值减少1)
*/
Enchantment,
/**
* 法术
*/
Spell,
/**
* 随从
*/
Minion
}
/**
* 字符串转 CardType类型
*/
internal fun String.toCardType(fallback: GameCardType = GameCardType.Game): GameCardType {
return GameCardType.values().find {
it.name.equals(this.replace("_", ""), true)
} ?: fallback
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/GameEvent.kt
================================================
package com.ke.hs_tracker.module.entity
import com.ke.hs_tracker.module.db.Game
/**
* 游戏事件
*/
sealed interface GameEvent {
/**
* 游戏开始
*/
object OnGameStart : GameEvent
/**
* 游戏结束
*/
data class OnGameOver(
val game: Game
) : GameEvent
/**
* 插入一张卡牌到用户的牌库
*/
data class InsertCardToUserDeck(
val cardId: String
) : GameEvent
/**
* 从牌库中移除一张卡牌
*/
data class RemoveCardFromUserDeck(
val cardId: String
) : GameEvent
/**
* 插入一张卡牌到墓地
*/
data class InsertCardToGraveyard(val cardId: String, val isUser: Boolean) : GameEvent
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/GameType.kt
================================================
package com.ke.hs_tracker.module.entity
import androidx.annotation.StringRes
import com.ke.hs_tracker.module.R
enum class GameType(@StringRes val title: Int) {
/**
* 排名
*/
Ranked(R.string.module_ranked),
/**
* 休闲
*/
Casual(R.string.module_casual),
Unknown(R.string.module_unknown)
}
val String.toGameType: GameType
get() = GameType.values().find {
this.contains(it.name, true)
} ?: GameType.Unknown
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/GraveyardCard.kt
================================================
package com.ke.hs_tracker.module.entity
data class GraveyardCard(
val card: Card,
/**
* 插入时间
*/
val time: Long = System.currentTimeMillis()
)
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/InsertStackResult.kt
================================================
package com.ke.hs_tracker.module.entity
sealed interface InsertStackResult {
/**
* 插入成功
*/
object Success : InsertStackResult
/**
* 不能插入,例如不在Block内的TAG_CHANGE
*/
object CanNotInsert : InsertStackResult
/**
* 结束了
* @param powerTag tag
* @param handled 是否处理了本次log日志,例如 FULL_ENTITY - Updating 跟着一个 FULL_ENTITY - Updating的情况,handled就是true,表示已经处理了,不需要在进行处理;如果是FULL_ENTITY - Updating
* 跟着一个 TAG_CHANGE,就表示没有处理,需要调用这个方法的自行处理log数据
*/
data class Over(val powerTag: PowerTag, val handled: Boolean) : InsertStackResult
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/LogType.kt
================================================
package com.ke.hs_tracker.module.entity
enum class LogType(val replace: String) {
PowerTaskList("PowerTaskList.DebugPrintPower() -"),
GameState("GameState.DebugPrintGame() -")
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/Mechanics.kt
================================================
package com.ke.hs_tracker.module.entity
import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
/**
* 类型
*/
enum class Mechanics {
/**
* 注能
*/
Infuse,
/**
* 过载
*/
Overload,
/**
* 战吼
*/
BattleCry,
/**
* 光环
*/
Aura,
/**
* 冲锋
*/
Charge,
/**
* 嘲讽
*/
Taunt,
/**
* 突袭
*/
Rush,
/**
* 巨型
*/
Colossal,
/**
* 探底
*/
Dredge,
/**
* 潜行
*/
Stealth,
/**
* 圣盾
*/
DivineShield,
/**
* 法强
*/
SpellPower,
/**
* 抽到的时候
*/
TopDeck,
/**
* 亡语
*/
DeathRattle,
/**
* 秘密选择
*/
Counter,
InvisibleDeathRattle,
/**
* 变形
*/
Morph,
/**
* 激怒
*/
Enraged,
/**
* 50%几率攻击错误的敌人
*/
Forgetful,
/**
* 本回合生效
*/
TagOneTurnEffect,
/**
* 抉择
*/
ChooseOne,
/**
* 荣誉击杀
*/
HonorableKill,
/**
* 法力迸发
*/
SpellBurst,
/**
* 腐蚀
*/
Corrupt,
/**
* 暴怒
*/
Frenzy,
/**
* 发现
*/
Discover,
/**
* 冻结
*/
Freeze,
/**
* 连击
*/
Combo,
/**
* 无法攻击
*/
CantAttack,
/**
* 触发
*/
TriggerVisual,
/**
* 奥秘
*/
Secret,
/**
* 任务
*/
Quest,
/**
* 克苏恩
*/
Ritual,
/**
* 交易
*/
Tradeable,
/**
* 激励
*/
Inspire,
/**
* 沉默
*/
Silence,
/**
* 风怒
*/
Windfury,
/**
* 回响
*/
Echo,
/**
* 污手党
*/
GrimyGoons,
/**
* 暗金教
*/
Kabal,
/**
* 玉莲帮
*/
JadeLotus,
/**
* 青玉魔像
*/
JadeGolem,
/**
* 不可见的衍生牌
*/
EnchantmentInvisible,
/**
* 英雄技能造成额外的伤害
*/
HeroPowerDamage,
/**
* 回合结束时如果这张牌仍在手牌中,将其摧毁
*/
Ghostly,
/**
* 受到法强翻倍
*/
ReceivesDoubleSpellDamageBonus,
/**
* 零件
*/
SparePart,
AutoAttack,
/**
* 唤尸者专属
*/
DeathKnight,
/**
* 偶数
*/
CollectionmanagerFilterManaEven,
/**
* 奇数
*/
CollectionmanagerFilterManaOdd,
/**
* 不受法强影响的法术
*/
ImmuneToSpellPower,
/**
* 无法被沉默
*/
CantBeSilenced,
CantBeDestroyed,
/**
* 黑棋国王
*/
CantBeFatigued,
/**
* 支线任务
*/
SideQuest,
/**
* 双生法术
*/
TwinSpell,
/**
* 剧毒
*/
Poisonous,
/**
* 吸血
*/
LifeSteal,
/**
* 流放
*/
Outcast,
/**
* 不可接触的
*/
Untouchable,
/**
* 复仇
*/
Avenge,
/**
* 超杀
*/
Overkill,
/**
* 复生
*/
Reborn,
AIMustPlay,
/**
* 免疫
*/
Immune,
/**
* 无法成为法术的目标
*/
CantBeTargetedBySpells,
/**
* 无法成为英雄技能的目标
*/
CantBeTargetedByHeroPowers,
/**
* 磁力
*/
Modular,
/**
* 如果这张牌在你的手牌中,在你的回合开始时,你的英雄受到2点伤害
*/
EvilGlow,
/**
* 相邻的随从获得buff
*/
AdjacentBuff,
/**
* 对局开始时
*/
StartOfGame,
/**
* 在你攻击一个随从后,迫使其攻击相邻的一个随从
*/
FinishAttackSpellOnDamage,
AppearFunctionallyDead,
Gears,
Puzzle,
MultiplyBuffValue,
AffectedBySpellPower,
DungeonPassiveBuff,
IgnoreHideStatsForBigCard,
Summoned,
/**
* 法力渴求
*/
ManaThirst,
OVERHEAL,
VENOMOUS,
MAGNETIC,
FORGE,
TITAN
}
class MechanicsAdapter {
@FromJson
fun fromJson(value: String): Mechanics {
return EnumMoshiAdapter.fromJson(value, Mechanics.values())
}
@ToJson
fun toJson(mechanics: Mechanics) = EnumMoshiAdapter.toJson(mechanics)
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/NestedTag.kt
================================================
package com.ke.hs_tracker.module.entity
internal sealed interface NestedTag {
object CreateGame : NestedTag
data class GameEntity(val id: Int) : NestedTag
data class Tag(val key: String, val value: String) : NestedTag {
fun toPair(): Pair = key to value
}
data class Player(val entityId: Int, val playerId: Int) : NestedTag
//FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=63 zone=DECK zonePos=0 cardId= player=2] CardID=
//FULL_ENTITY - Updating [entityName=全副武装! id=65 zone=PLAY zonePos=0 cardId=HERO_01bp player=1] CardID=HERO_01bp
data class FullEntity(
val entity: Entity,
val cardId: String?
) : NestedTag
data class Block(
val blockType: BlockType,
val entity: Entity,
val target: Entity?
) : NestedTag
data class TagChange(
val entity: Entity,
val tag: String,
val value: String
) : NestedTag {
fun convert(): PowerTag.PowerTaskList.TagChange {
return PowerTag.PowerTaskList.TagChange(entity, tag, value)
}
}
data class ShowEntity(
val entity: Entity,
val cardId: String
) : NestedTag
object BlockEnd : NestedTag
}
//internal fun NestedTag.FullEntity.toUpdating(): PowerTag.PowerTaskList.FullEntity.Updating {
// return PowerTag.PowerTaskList.FullEntity.Updating(
// entityName, cardType, id, zone, zonePosition, cardId, player
// )
//}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/PowerTag.kt
================================================
package com.ke.hs_tracker.module.entity
import com.ke.hs_tracker.module.db.ZonePositionChangedEvent
sealed interface PowerTag {
sealed interface PowerTaskList : PowerTag {
/**
* 创建游戏
*/
//D 19:55:18.1257030 GameState.DebugPrintPower() - CREATE_GAME
//D 19:55:18.1257030 GameState.DebugPrintPower() - GameEntity EntityID=1
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=CARDTYPE value=GAME
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=ZONE value=PLAY
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=ENTITY_ID value=1
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=937 value=2
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=SPAWN_TIME_COUNT value=1
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=GAME_SEED value=1950487951
//D 19:55:18.1257030 GameState.DebugPrintPower() - Player EntityID=2 PlayerID=1 GameAccountId=[hi=144115211015832391 lo=191215280]
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=CONTROLLER value=1
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=CARDTYPE value=PLAYER
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=PLAYER_ID value=1
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=HERO_ENTITY value=64
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=MAXHANDSIZE value=10
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=STARTHANDSIZE value=4
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=TEAM_ID value=1
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=ZONE value=PLAY
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=ENTITY_ID value=2
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=MAXRESOURCES value=10
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=SPAWN_TIME_COUNT value=1
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=AVRANK value=336
//D 19:55:18.1257030 GameState.DebugPrintPower() - Player EntityID=3 PlayerID=2 GameAccountId=[hi=144115211015832391 lo=44511141]
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=CONTROLLER value=2
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=CARDTYPE value=PLAYER
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=PLAYER_ID value=2
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=HERO_ENTITY value=66
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=MAXHANDSIZE value=10
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=STARTHANDSIZE value=4
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=TEAM_ID value=2
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=ZONE value=PLAY
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=ENTITY_ID value=3
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=MAXRESOURCES value=10
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=SPAWN_TIME_COUNT value=1
//D 19:55:18.1257030 GameState.DebugPrintPower() - tag=AVRANK value=338
data class CreateGame(
val gameEntity: GameEntity,
val player1: Player,
val player2: Player
) : PowerTaskList
data class TagChange(
override val entity: Entity,
val tag: String,
val value: String,
) : PowerTaskList, ZoneUpdatable {
/**
* 是否是游戏完成的标志
*/
val isGameComplete: Boolean =
entity.entityName == "GameEntity" && tag.equals("state", true) && value.equals(
"COMPLETE",
true
)
/**
* 是否是玩家胜利或失败
*/
val isPlayerWonOrLost: Pair?
get() {
return if (tag == "PLAYSTATE" && value == "WON") {
entity.entityName to true
} else if (tag == "PLAYSTATE" && value == "LOST") {
entity.entityName to false
} else {
null
}
}
//TAG_CHANGE Entity=GameEntity tag=TURN value=1
fun isTurnChanged(): Int? {
if (entity.isGameEntity && tag == "TURN") {
return value.toIntOrNull()
}
return null
}
//TAG_CHANGE Entity=GameEntity tag=NUM_TURNS_IN_PLAY value=5
fun isNumTurnsInPlayChanged(): Int? {
if (entity.isGameEntity && tag == "NUM_TURNS_IN_PLAY") {
return value.toIntOrNull()
}
return null
}
override fun getZoneString(): String? {
return if (tag == "ZONE") value else null
}
override fun getZonePositionString(): String? {
return if (tag == "ZONE_POSITION") value else null
}
}
data class FullEntity(
override val entity: Entity,
val payloads: MutableMap = mutableMapOf(),
val cardId: String?
) : PowerTaskList, ZoneUpdatable {
fun append(value: Pair) {
payloads[value.first] = value.second
}
/**
* 是否是更新英雄置于战场
*/
fun isUpdateHero(): Pair? {
if (entity.zone == Zone.Play && payloads.count {
it.key == "CARDTYPE" && it.value == "HERO"
} == 1) {
return entity.player to entity.cardId
}
return null
}
//FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=50 zone=DECK zonePos=0 cardId= player=2] CardID=
// tag=ZONE value=DECK
// tag=CONTROLLER value=2
// tag=ENTITY_ID value=50
override fun getZoneString(): String? {
return payloads["ZONE"]
}
override fun getZonePositionString(): String? {
return payloads["ZONE_POSITION"]
}
override fun convertEntity(entity: Entity): Entity {
entity.run {
return Entity(
entityName,
gameCardType,
id,
zone,
zonePosition,
this@FullEntity.cardId,
player
)
}
}
//起始发牌
//FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=49 zone=DECK zonePos=0 cardId= player=2] CardID=
// tag=ZONE value=DECK
// tag=CONTROLLER value=2
// tag=ENTITY_ID value=49
//发现一张牌
//FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=113 zone=SETASIDE zonePos=0 cardId= player=2] CardID=
// tag=ZONE value=SETASIDE
// tag=CONTROLLER value=2
// tag=ENTITY_ID value=113
// override fun shouldIgnoreSameZone(): Boolean {
// return true
// }
}
data class ShowEntity(
override val entity: Entity,
val cardId: String,
val payloads: MutableMap = mutableMapOf()
) : PowerTaskList, ZoneUpdatable {
fun append(value: Pair) {
payloads[value.first] = value.second
}
fun entityWithCardId(): Entity {
return entity.run {
Entity(
entityName,
gameCardType,
id,
zone,
zonePosition,
this@ShowEntity.cardId,
player
)
}
}
override fun getZoneString(): String? {
return payloads["ZONE"]
}
override fun getZonePositionString(): String? {
return payloads["ZONE_POSITION"]
}
override fun convertEntity(entity: Entity): Entity {
entity.run {
return Entity(
entityName,
gameCardType,
id,
zone,
zonePosition,
this@ShowEntity.cardId,
player
)
}
}
// override fun isUpdated(): Pair? {
// val newZoneString = payloads["ZONE"] ?: return null
// val newZone = newZoneString.toZone(Zone.Unknown)
// if (newZone == Zone.Unknown) {
// throw RuntimeException("错误的zone $newZoneString")
// }
// if (entity.zone == newZone) {
// return null
// }
// return entity to newZone
// }
}
data class Block(
val type: BlockType,
val entity: Entity,
val target: Entity?,
val list: List
) : PowerTaskList {
/**
* 是否是第一回合
*/
fun ifFirstTurn(): Boolean {
return type == BlockType.Trigger && entity.isGameEntity && list.mapNotNull {
it as? TagChange
}.any {
it.tag == "FIRST_PLAYER" && it.value == "1"
}
}
private fun isTurnChanged(): Int? {
return list.mapNotNull {
it as? TagChange
}.find {
it.isTurnChanged() != null
}?.isTurnChanged()
}
private fun isNumTurnsInPlayChanged(): Int? {
return list.mapNotNull {
it as? TagChange
}.find {
it.isNumTurnsInPlayChanged() != null
}?.isNumTurnsInPlayChanged()
}
private fun flush(): List {
val entityWithPayloadList = mutableListOf()
list.forEach {
when (it) {
is ShowEntity -> {
val entity = it.entityWithCardId()
val entityWithPayload = EntityWithPayload(entity)
// entityWithPayload.payload.addAll(it.payloads)
it.payloads.forEach { entry ->
entityWithPayload.add(entry.toPair())
}
entityWithPayloadList.add(entityWithPayload)
}
is TagChange -> {
findEntityFromList(it.entity.id, entityWithPayloadList)?.apply {
add(it.tag to it.value)
}
}
else -> {
}
}
}
return entityWithPayloadList
}
private fun findEntityFromList(
entityId: Int,
list: List
): EntityWithPayload? {
return list.find {
it.entity.id == entityId
}
}
}
}
sealed interface GameState : PowerTag {
data class BuildNumber(val number: String) : GameState
data class GameType(val type: String) : GameState
data class FormatType(val type: String) : GameState
data class ScenarioID(val id: String) : GameState
data class PlayerMapping(val id: Int, val name: String) : GameState {
/**
* 是否是先手
*/
val first: Boolean = id == 1
/**
* 是否是当前用户
*/
val isUser: Boolean = name != "UNKNOWN HUMAN PLAYER"
}
}
}
interface ZoneUpdatable {
/**
* 是否更新了位置
*/
fun isUpdateZone(userPlayerId: Int?): ZonePositionChangedEvent? {
val newZoneString = getZoneString()
val newZone = newZoneString?.toZone(Zone.Unknown)
if (newZone == Zone.Unknown) {
throw RuntimeException("错误的zone $newZoneString")
}
val position = getZonePositionString()?.toIntOrNull()
if (newZone == null && position == null) {
return null
}
val entity = convertEntity(entity)
return ZonePositionChangedEvent(
entityId = entity.id,
cardId = entity.cardId,
currentZone = entity.zone,
isUser = entity.player == userPlayerId,
currentPosition = entity.zonePosition,
newZone = newZone ?: entity.zone,
newPosition = position ?: entity.zonePosition
)
}
fun getZoneString(): String?
fun getZonePositionString(): String?
val entity: Entity
fun convertEntity(entity: Entity): Entity = entity
}
//interface ZonePositionUpdatable {
// /**
// * 是否更新了位置
// */
// fun isUpdatePosition(): Pair? {
//
// val position = getZonePositionString()?.toIntOrNull() ?: return null
//
// return entity to position
// }
//
// fun getZonePositionString(): String?
//
// val entity: Entity
//
//
//}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/Race.kt
================================================
package com.ke.hs_tracker.module.entity
import androidx.annotation.StringRes
import com.ke.hs_tracker.module.R
import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
/**
* 随从种族
*/
enum class Race(@StringRes val titleRes: Int? = null, val tradition: Boolean = false) {
/**
* 海盗
*/
Pirate(R.string.module_pirate, true),
/**
* 机械
*/
Mechanical(R.string.module_mechanical, true),
/**
* 龙
*/
Dragon(R.string.module_dragon, true),
/**
* 野兽
*/
Beast(R.string.module_beast, true),
/**
* 鱼人
*/
Murloc(R.string.module_murloc, true),
/**
* 恶魔
*/
Demon(R.string.module_demon, true),
/**
* 图腾
*/
Totem(R.string.module_totem, true),
/**
* 元素
*/
Elemental(R.string.module_elemental, true),
/**
* 娜迦
*/
Naga(R.string.module_naga, true),
Error,
/**
* 野猪人
*/
Quilboar(R.string.module_quilboar),
/**
* 全部
*/
All(R.string.module_all),
/**
* 佣兵
*/
ORC,
Troll,
/**
* 暗夜精灵
*/
NightElf,
/**
* 食人魔
*/
Ogre,
/**
* 牛头人
*/
Tauren,
Lock,
Human,
Bloodelf,
OldGod,
/**
* 亡灵
*/
Undead,
Gnome,
Dwarf,
/**
* 德莱尼
*/
Draenei,
Halforc,
Centaur,
Goblin,
Furbolg,
Egg,
Worgen,
Treant,
Highelf,
}
class RaceAdapter {
@FromJson
fun fromJson(value: String): Race {
return EnumMoshiAdapter.fromJson(value, Race.values(), Race.Error)
}
@ToJson
fun toJson(value: Race) = EnumMoshiAdapter.toJson(value)
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/Rarity.kt
================================================
package com.ke.hs_tracker.module.entity
import androidx.annotation.ColorRes
import androidx.annotation.StringRes
import com.ke.hs_tracker.module.R
import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
enum class Rarity(@StringRes val titleRes: Int, @ColorRes val colorRes: Int) {
/**
* 免费
*/
Free(R.string.module_rarity_free, R.color.module_rarity_common),
/**
* 普通
*/
Common(R.string.module_rarity_common, R.color.module_rarity_common),
/**
* 稀有
*/
Rare(R.string.module_rarity_rare, R.color.module_rarity_rare),
/**
* 史诗
*/
Epic(R.string.module_rarity_epic, R.color.module_rarity_epic),
/**
* 传说
*/
Legendary(R.string.module_rarity_legendary, R.color.module_rarity_legendary)
}
class RarityAdapter {
@FromJson
fun fromJson(value: String): Rarity {
return EnumMoshiAdapter.fromJson(
value, Rarity.values()
)
}
@ToJson
fun toJson(value: Rarity) = EnumMoshiAdapter.toJson(value)
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/SpellSchool.kt
================================================
package com.ke.hs_tracker.module.entity
import androidx.annotation.StringRes
import com.ke.hs_tracker.module.R
import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
/**
* 法术类型
*/
enum class SpellSchool(@StringRes val titleRes: Int) {
/**
* 奥数
*/
Arcane(R.string.module_arcane),
// /**
// * 冰霜
// */
// Freeze,
/**
* 冰霜
*/
Frost(R.string.module_frost),
/**
* 火焰
*/
Fire(R.string.module_fire),
/**
* 自然
*/
Nature(R.string.module_nature),
/**
* 暗影
*/
Shadow(R.string.module_shadow),
/**
* 神圣
*/
Holy(R.string.module_holy),
/**
* 邪能
*/
Fel(R.string.module_Fel),
/**
* 冲锋攻击
*/
PhysicalCombat(R.string.module_all)
}
class SpellSchoolAdapter {
@FromJson
fun fromJson(value: String): SpellSchool? {
return SpellSchool.values().find { it.name.equals(value.replace("_", ""), true) }
}
@ToJson
fun toJson(spellSchool: SpellSchool?) = spellSchool?.name?.uppercase()
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/Turn.kt
================================================
package com.ke.hs_tracker.module.entity
sealed interface Turn {
/**
* 游戏初始化
*/
data class InitialGame(
val buildNumber: String,
val gameType: String,
val formatType: String,
val scenarioID: Int,
val player1: Pair,
val player2: Pair,
)
/**
* 卡牌初始化
*/
data class CreateGame(
val player1Cards: List,
val player2Cards: List,
val player1HeroId: String,
val player1HeroPowerId: String,
val player2HeroId: String,
val player2HeroPowerId: String
) : Turn
/**
* 确定手牌回合
*/
data class First(
val firstPlayerName: String,
val player1Cards: List,
val player2Cards: List
) : Turn
}
data class PlayerInitialCard(
val playerId: Int,
val position: Int,
val entityId: Int,
//自己的卡牌才能看到id
val cardId: String?
)
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/Zone.kt
================================================
package com.ke.hs_tracker.module.entity
enum class Zone {
/**
* 战场
*/
Play,
/**
* 牌库
*/
Deck,
/**
* 发现的牌的位置
*/
SetAside,
/**
* 墓地 打出的法术牌和死亡的随从会进入
*/
Graveyard,
/**
* 手牌
*/
Hand,
/**
* 位置
*/
Unknown,
/**
* 衍生牌被消耗后会去到这个地方
*/
//TAG_CHANGE Entity=[entityName=研习符文 id=98 zone=PLAY zonePos=0 cardId=SCH_270e2 player=2] tag=1068 value=5
//TAG_CHANGE Entity=[entityName=研习符文 id=98 zone=PLAY zonePos=0 cardId=SCH_270e2 player=2] tag=1068 value=0
//TAG_CHANGE Entity=[entityName=研习符文 id=98 zone=PLAY zonePos=0 cardId=SCH_270e2 player=2] tag=ZONE value=REMOVEDFROMGAME
//TAG_CHANGE Entity=[entityName=研习符文 id=98 zone=PLAY zonePos=0 cardId=SCH_270e2 player=2] tag=1234 value=94
RemovedFromGame,
/**
* 奥秘
*/
Secret
}
internal fun String.toZone(fallback: Zone = Zone.Deck): Zone {
return Zone.values().firstOrNull {
it.name.equals(this, true)
} ?: fallback
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/entity/ZoneCard.kt
================================================
package com.ke.hs_tracker.module.entity
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class ZoneCard(
val card: Card?,
val entityId: Int,
val position: Int
) : Parcelable
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/parser/BlockTagStack.kt
================================================
package com.ke.hs_tracker.module.parser
import com.ke.hs_tracker.module.entity.*
import com.orhanobut.logger.Logger
import java.util.*
import javax.inject.Inject
interface BlockTagStack {
/**
* 插入一条日志
*/
fun insert(line: String): InsertStackResult
}
class BlockTagStackImpl @Inject constructor() : BlockTagStack {
private val nestedTagList = LinkedList()
override fun insert(line: String): InsertStackResult {
//CREATE_GAME
var matchResult = PowerParserImpl.CREATE_GAME_PATTERN.matchEntire(line)
if (matchResult != null) {
//游戏开始
nestedTagList.clear()
nestedTagList.add(NestedTag.CreateGame)
return InsertStackResult.Success
}
//GameEntity EntityID=1
matchResult = PowerParserImpl.GAME_ENTITY_PATTERN.matchEntire(line)
if (matchResult != null) {
val entityId = matchResult.groupValues[1].toInt()
nestedTagList.add(NestedTag.GameEntity(entityId))
return InsertStackResult.Success
}
//Player EntityID=2 PlayerID=1 GameAccountId=[hi=144115211015832391 lo=191215280]
matchResult = PowerParserImpl.PLAYER_ENTITY_PATTERN.matchEntire(line)
if (matchResult != null) {
val entityId = matchResult.groupValues[1].toInt()
val playerId = matchResult.groupValues[2].toInt()
nestedTagList.add(NestedTag.Player(entityId, playerId))
return InsertStackResult.Success
}
//tag=CARDTYPE value=GAME
matchResult = PowerParserImpl.TAG_PATTERN.matchEntire(line)
if (matchResult != null) {
val key = matchResult.groupValues[1]
val value = matchResult.groupValues[2]
nestedTagList.add(NestedTag.Tag(key, value))
return InsertStackResult.Success
}
//BLOCK_START
// BlockType=TRIGGER
// Entity=GameEntity
// EffectCardId=System.Collections.Generic.List`1[System.String]
// EffectIndex=-1
// Target=0
// SubOption=-1
// TriggerKeyword=TAG_NOT_SET
matchResult = PowerParserImpl.BLOCK_START_PATTERN.matchEntire(line)
if (matchResult != null) {
val blockType = matchResult.groupValues[1].toBlockType()
val entity = Entity.createFromContent(matchResult.groupValues[2])!!
val target = Entity.createFromContent(matchResult.groupValues[5])
val block = NestedTag.Block(blockType, entity, target)
nestedTagList.add(block)
return InsertStackResult.Success
}
matchResult = PowerParserImpl.BLOCK_END_PATTERN.matchEntire(line)
if (matchResult != null) {
//块结束了
if (nestedTagList.isEmpty()) {
Logger.d("准备插入一个end到空的列表里面")
return InsertStackResult.Success
}
nestedTagList.add(NestedTag.BlockEnd)
// val blockStartList = nestedTagList.filterIsInstance()
val blockCount = nestedTagList.count {
it is NestedTag.Block
}
val blockEndCount = nestedTagList.count { it is NestedTag.BlockEnd }
if (blockCount == blockEndCount) {
return InsertStackResult.Over(flushBlock(nestedTagList), true)
} else {
return InsertStackResult.Success
}
}
//TAG_CHANGE Entity=阿克萌德#51240 tag=CURRENT_PLAYER value=1
//TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=29 zone=DECK zonePos=0 cardId= player=1] tag=ZONE_POSITION value=1
matchResult = PowerParserImpl.TAG_CHANGE_PATTERN.matchEntire(line)
if (matchResult != null) {
return when (val first = nestedTagList.firstOrNull()) {
is NestedTag.FullEntity -> {
val powerTag = flushFullEntityWhenFirst()
//处理堆栈
InsertStackResult.Over(powerTag, false)
}
is NestedTag.ShowEntity -> {
val showEntity = PowerTag.PowerTaskList.ShowEntity(
first.entity,
first.cardId
)
nestedTagList.forEach {
if (it is NestedTag.TagChange) {
showEntity.payloads[it.tag] = it.value
}
}
nestedTagList.clear()
InsertStackResult.Over(showEntity, false)
}
is NestedTag.Block -> {
val tagChange = NestedTag.TagChange(
Entity.createFromContent(matchResult.groupValues[1])!!,
matchResult.groupValues[2],
matchResult.groupValues[3],
)
nestedTagList.add(tagChange)
InsertStackResult.Success
}
else -> {
InsertStackResult.CanNotInsert
}
}
}
//FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=4 zone=DECK zonePos=0 cardId= player=1] CardID=
matchResult = PowerParserImpl.FULL_ENTITY_PATTERN.matchEntire(line)
if (matchResult != null) {
val first = nestedTagList.firstOrNull()
if (first is NestedTag.CreateGame) {
//create game 接 full entity
val createGame = createCreateGameTag()
val pair = matchResult.groupValues[1] to matchResult.groupValues[2]
insertFullEntity(pair)
return InsertStackResult.Over(createGame, true)
} else if (first is NestedTag.FullEntity) {
//连续两个full entity
val result = flushFullEntityWhenFirst()
insertFullEntity(matchResult.groupValues[1] to matchResult.groupValues[2])
return InsertStackResult.Over(result, true)
}
insertFullEntity(matchResult.groupValues[1] to matchResult.groupValues[2])
return InsertStackResult.Success
}
//SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=85 zone=SETASIDE zonePos=0 cardId= player=2] CardID=SCH_231e
matchResult = PowerParserImpl.SHOW_ENTITY.matchEntire(line)
if (matchResult != null) {
val first = nestedTagList.firstOrNull()
val fullEntity = if (first is NestedTag.FullEntity) {
flushFullEntityWhenFirst()
} else null
val entity = Entity.createFromContent(matchResult.groupValues[1])!!
val cardId = matchResult.groupValues[2]
nestedTagList.add(NestedTag.ShowEntity(entity, cardId))
return if (fullEntity == null) {
InsertStackResult.Success
} else {
InsertStackResult.Over(fullEntity, true)
}
}
return InsertStackResult.CanNotInsert
}
/**
* 如果栈中的第一个是FullEntity,就开始处理
*/
private fun flushFullEntityWhenFirst(): PowerTag.PowerTaskList.FullEntity {
val map = mutableMapOf()
val first = nestedTagList.removeFirst() as NestedTag.FullEntity
nestedTagList.map {
val result =
it as? NestedTag.Tag ?: throw IllegalArgumentException("错误的类型,应该是Tag 但现在是 $it")
result
}.forEach {
map[it.key] = it.value
}
//清空栈
nestedTagList.clear()
return PowerTag.PowerTaskList.FullEntity(
first.entity,
map,
first.cardId
)
// return when {
// isInsertCardToDeck(first) -> {
// //插入一张卡牌到牌库
// flushFullEntityInsertCardToDeck()
// }
// isInsertHeroToPlay(first) -> {
// //放置英雄牌到战场
// flushFullEntityInsertHeroToPlay()
// }
// isInsertHeroPowerToPlay(first) -> {
// //放置英雄技能到战场
// flushFullEntityInsertHeroPowerToPlay()
// }
// else -> throw RuntimeException("无法处理的 full entity $first")
// }
}
// /**
// * 是否是置入英雄技能到战场
// */
// private fun isInsertHeroPowerToPlay(fullEntity: NestedTag.FullEntity): Boolean {
// if (fullEntity.entity.zone != Zone.Play) {
// return false
// }
//
// nestedTagList.forEach {
// if (it is NestedTag.Tag && "cardType".equals(
// it.key,
// true
// ) && GameCardType.HeroPower.name.equals(
// it.value.replace("_", ""),
// true
// )
// ) {
// return true
// }
// }
//
//
// return false
// }
// private fun flushFullEntityInsertHeroPowerToPlay(): PowerTag.PowerTaskList.FullEntity.InsertHeroPowerToPlay {
// var last = nestedTagList.removeLastOrNull()
// //移除第一个
// val entity = (nestedTagList.removeFirst() as NestedTag.FullEntity).entity
// val map = mutableMapOf()
// while (last != null) {
// when (last) {
// is NestedTag.Tag -> {
// map[last.key] = last.value
// }
// else -> {
// throw RuntimeException("last的类型必须是Tag 但现在是 $last")
// }
// }
// last = nestedTagList.removeLastOrNull()
// }
// return PowerTag.PowerTaskList.FullEntity.InsertHeroPowerToPlay.createFromEntityAndMap(
// entity,
// map
// )
// }
//
// private fun flushFullEntityInsertHeroToPlay(): PowerTag.PowerTaskList.FullEntity.InsertHeroToPlay {
// var last = nestedTagList.removeLastOrNull()
// //移除第一个
// val entity = (nestedTagList.removeFirst() as NestedTag.FullEntity).entity
// val map = mutableMapOf()
// while (last != null) {
// when (last) {
// is NestedTag.Tag -> {
// map[last.key] = last.value
// }
// else -> {
// throw RuntimeException("last的类型必须是Tag 但现在是 $last")
// }
// }
// last = nestedTagList.removeLastOrNull()
// }
// return PowerTag.PowerTaskList.FullEntity.InsertHeroToPlay.createFromEntityAndMap(
// entity,
// map
// )
// }
// /**
// * 是否是置入英雄卡到战场
// */
// private fun isInsertHeroToPlay(fullEntity: NestedTag.FullEntity): Boolean {
// if (fullEntity.entity.zone != Zone.Play) {
// return false
// }
//
// nestedTagList.forEach {
// if (it is NestedTag.Tag && "cardType".equals(
// it.key,
// true
// ) && GameCardType.Hero.name.equals(
// it.value,
// true
// )
// ) {
// return true
// }
// }
//
//
// return false
// }
// /**
// * 是否是置入英雄卡到战场
// */
// private fun isInsertCardToDeck(fullEntity: NestedTag.FullEntity): Boolean {
//
// return fullEntity.entity.zone == Zone.Deck && fullEntity.entity.gameCardType == GameCardType.Invalid
// }
// private fun flushFullEntityInsertCardToDeck(): PowerTag.PowerTaskList.FullEntity.InsertToDeck {
// var last = nestedTagList.removeLastOrNull()
// //移除第一个
// val fullEntity = nestedTagList.removeFirst() as NestedTag.FullEntity
// val map = mutableMapOf()
// while (last != null) {
// when (last) {
// is NestedTag.Tag -> {
// map[last.key] = last.value
// }
// else -> {
// throw RuntimeException("last的类型必须是Tag 但现在是 $last")
// }
// }
// last = nestedTagList.removeLastOrNull()
// }
//
// if (map.size != 3) throw RuntimeException("在插入卡牌到牌库的情况下,tag数量必须是3个")
//
// return PowerTag.PowerTaskList.FullEntity.InsertToDeck.createFromEntityAndMap(
// fullEntity.entity,
// map
// )
//
// }
private fun insertFullEntity(pair: Pair) {
val fullEntity =
createFullEntityByContent(pair.first, pair.second.ifEmpty { null })
nestedTagList.add(fullEntity)
}
/**
* 根据字符串创建FullEntity
*/
//[entityName=UNKNOWN ENTITY [cardType=INVALID] id=4 zone=DECK zonePos=0 cardId= player=1]
//[entityName=加尔鲁什·地狱咆哮 id=64 zone=PLAY zonePos=0 cardId=HERO_01 player=1]
private fun createFullEntityByContent(content: String, cardId: String?): NestedTag.FullEntity {
val entity: Entity = Entity.createFromContent(content)!!
return NestedTag.FullEntity(entity, cardId)
}
private fun createCreateGameTag(): PowerTag.PowerTaskList.CreateGame {
val first = nestedTagList.removeFirstOrNull()
if (first == NestedTag.CreateGame) {
val keyValueMap = mutableMapOf()
var last = nestedTagList.removeLastOrNull()
var player1: Player? = null
var player2: Player? = null
while (last != null) {
when (last) {
is NestedTag.GameEntity -> {
val game = GameEntity(
GameCardType.Game,
last.id
)
return PowerTag.PowerTaskList.CreateGame(
game,
player1!!,
player2!!
)
}
is NestedTag.Player -> {
keyValueMap["playerid"] = last.playerId.toString()
keyValueMap["entityid"] = last.entityId.toString()
if (player2 == null) {
player2 = Player.fromMap(keyValueMap)
} else {
player1 = Player.fromMap(keyValueMap)
}
keyValueMap.clear()
}
is NestedTag.Tag -> {
keyValueMap[last.key.replace("_", "").lowercase()] = last.value
}
else -> throw IllegalArgumentException("非法状态错误 $last")
}
last = nestedTagList.removeLastOrNull()
}
} else {
throw IllegalArgumentException("第一个必须是 CreateGame,但现在是 $first")
}
throw RuntimeException("无法创建CreateGame")
}
private fun flushBlock(
source: MutableList,
): PowerTag.PowerTaskList.Block {
//有可能出现多级嵌套
val header = source.removeFirst()
val first = header as? NestedTag.Block
if (first == null) {
throw RuntimeException("列表的第一个应该是Block,但现在是不是 现在是 $header,当前列表为 $source")
}
source.removeLast()
val payloads = mutableListOf()
val tempList = mutableListOf()
source.forEachIndexed { index, nestedTag ->
when (nestedTag) {
is NestedTag.Block -> {
// if (tempList.isEmpty()) {
tempList.add(nestedTag)
// } else {
// val blockStartCount = tempList.count {
// it is NestedTag.Block
// }
// if (blockStartCount != 1) {
// throw IllegalArgumentException("错误的block数量 $blockStartCount")
// }
//
// val pairedBlockEndIndex = findPairBlockEndIndex(source, index)
// if (pairedBlockEndIndex == -1) {
// throw IllegalArgumentException("找不到配对的结束标识")
// }
// val innerBlockList = source.subList(index, pairedBlockEndIndex)
// tempList.add(flushBlock(innerBlockList))
// }
}
is NestedTag.FullEntity -> {
if (tempList.isNotEmpty()) {
tempList.add(nestedTag)
} else {
payloads.add(
PowerTag.PowerTaskList.FullEntity(
nestedTag.entity,
mutableMapOf(),
nestedTag.cardId
)
)
}
}
is NestedTag.ShowEntity -> {
if (tempList.isNotEmpty()) {
tempList.add(nestedTag)
} else {
payloads.add(
PowerTag.PowerTaskList.ShowEntity(
nestedTag.entity, nestedTag.cardId
)
)
}
}
is NestedTag.Tag -> {
if (tempList.isNotEmpty()) {
tempList.add(nestedTag)
} else {
val last = payloads.last()
if (last is PowerTag.PowerTaskList.FullEntity) {
last.append(nestedTag.toPair())
} else if (last is PowerTag.PowerTaskList.ShowEntity) {
last.append(nestedTag.toPair())
}
}
}
is NestedTag.TagChange -> {
if (tempList.isNotEmpty()) {
tempList.add(nestedTag)
} else {
payloads.add(nestedTag.convert())
}
}
NestedTag.BlockEnd -> {
tempList.add(NestedTag.BlockEnd)
val blockStartCount = tempList.count {
it is NestedTag.Block
}
val blockEndCount = tempList.count {
it is NestedTag.BlockEnd
}
if (blockStartCount == blockEndCount) {
payloads.add(flushBlock(tempList))
tempList.clear()
}
}
else -> {
throw IllegalArgumentException("非法的数据 $nestedTag")
}
}
}
source.clear()
return PowerTag.PowerTaskList.Block(
first.blockType,
first.entity,
first.target,
payloads
)
}
}
private fun findPairBlockEndIndex(source: List, start: Int): Int {
var blockStartCount = 0
source.subList(start, source.size).forEachIndexed { index, it ->
if (it is NestedTag.Block) {
blockStartCount++
} else if (it is NestedTag.BlockEnd) {
if (blockStartCount == 0) {
return index
}
blockStartCount--
}
}
return -1
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/parser/DeckCardObserver.kt
================================================
package com.ke.hs_tracker.module.parser
import android.content.Context
import androidx.documentfile.provider.DocumentFile
import com.ke.hs_tracker.module.db.GameDao
import com.ke.hs_tracker.module.domain.GetAllCardUseCase
import com.ke.hs_tracker.module.domain.GetRealLogDirUseCase
import com.ke.hs_tracker.module.domain.ParseDeckCodeUseCase
import com.ke.hs_tracker.module.entity.*
import com.ke.hs_tracker.module.log
import com.ke.hs_tracker.module.ui.main.powerFileName
import com.ke.mvvm.base.data.successOr
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.io.InputStream
import javax.inject.Inject
/**
* 剩余卡牌监听器
*/
interface DeckCardObserver {
/**
* 牌库的卡牌
*/
val deckCardList: StateFlow>
/**
* 自己的墓地
*/
val userGraveyardCardList: StateFlow>
/**
* 对手的墓地
*/
val opponentGraveyardCardList: StateFlow>
/**
* 初始化
*/
fun init(scope: CoroutineScope)
}
class DeckCardObserverImpl @Inject constructor(
private val powerParser: PowerParser,
private val powerTagHandler: PowerTagHandler,
private val getAllCardUseCase: GetAllCardUseCase,
private val parseDeckCodeUseCase: ParseDeckCodeUseCase,
private val getLogDirUseCase: GetRealLogDirUseCase,
private val gameDao: GameDao,
@ApplicationContext private val context: Context
) : DeckCardObserver {
// private val _userGraveyardCardList = MutableStateFlow>(emptyList())
//
// override val userGraveyardCardList: StateFlow>
// get() = _userGraveyardCardList
/**
* 当前用户的卡组
*/
private var currentUserDeck: CurrentDeck? = null
private val _deckCardList =
MutableStateFlow>(emptyList())
// Channel>(capacity = Channel.CONFLATED)
override val deckCardList: StateFlow>
get() = _deckCardList
private val _userGraveyardCardList = MutableStateFlow>(emptyList())
override val userGraveyardCardList: StateFlow>
get() = _userGraveyardCardList
private val _opponentGraveyardCardList = MutableStateFlow>(emptyList())
override val opponentGraveyardCardList: StateFlow>
get() = _opponentGraveyardCardList
/**
* 所有卡牌
*/
private var allCards = emptyList()
/**
* 当前卡组的卡牌
*/
private var currentDeckList: List = emptyList()
/**
* 当前卡组剩余的卡牌
*/
private var deckLeftCardList: List = listOf()
/**
* 获取炉石log文件夹
*/
private suspend fun getLogsDir(): DocumentFile? {
return getLogDirUseCase(Unit).successOr(null)
}
/**
* 获取文件流
*/
private suspend fun getFileStream(fileName: String): InputStream? =
withContext(Dispatchers.IO) {
val documentFile = getLogsDir()?.findFile(fileName)
if (documentFile == null) {
"无法访问 $fileName 文件".log()
return@withContext null
}
context.contentResolver.openInputStream(documentFile.uri)
}
override fun init(
scope: CoroutineScope,
) {
val interval = 1500L
scope.launch {
clearPowerLogFile()
}
scope.launch {
///获取所有卡牌
allCards = getAllCardUseCase(Unit).successOr(emptyList())
}
val deckFileObserver = DeckFileObserver(interval) {
getFileStream("Decks.log")
}
val powerFileObserver = PowerFileObserver(interval) {
getFileStream(powerFileName)
}.apply {
}
scope.launch {
//监听牌库
delay(1000)
deckFileObserver
.start()
.flowOn(Dispatchers.IO)
.map {
currentUserDeck = it
parseDeckCodeUseCase(it.code).successOr(emptyList())
}.collect {
currentDeckList = it
_deckCardList.value = it.toList()
// _deckCardList.send(it)
}
}
scope.launch {
powerTagHandler.gameEventFlow.collect {
when (it) {
null -> {
}
is GameEvent.OnGameOver -> {
_userGraveyardCardList.value = emptyList()
_opponentGraveyardCardList.value = emptyList()
clearPowerLogFile()
deckLeftCardList = currentDeckList.toList()
_deckCardList.value = deckLeftCardList.toList()
// _deckCardList.send(deckLeftCardList)
it.game.apply {
userDeckCode = currentUserDeck?.code ?: ""
userDeckName = currentUserDeck?.name ?: ""
scope.launch {
gameDao.insert(this@apply)
}
}
powerFileObserver.reset()
}
GameEvent.OnGameStart -> {
_userGraveyardCardList.value = emptyList()
_opponentGraveyardCardList.value = emptyList()
// deckLeftCardList = currentDeckList
// "清空卡牌 OnGameStart ,deckLeftCardList ${deckLeftCardList.size} , currentDeckList ${currentDeckList.size}".log()
// deckLeftCardList.clear()
// deckLeftCardList.addAll(currentDeckList)
deckLeftCardList = currentDeckList.toList()
_deckCardList.value = deckLeftCardList.toList()
// _deckCardList.send(deckLeftCardList)
}
is GameEvent.RemoveCardFromUserDeck -> {
onUserDeckCardListChanged(it.cardId, true)
}
is GameEvent.InsertCardToUserDeck -> {
onUserDeckCardListChanged(it.cardId, false)
}
is GameEvent.InsertCardToGraveyard -> {
onGraveyardCardsChanged(it.cardId, it.isUser)
}
}
}
}
powerParser.powerTagListener = {
powerTagHandler.handle(it)
}
scope.launch {
powerFileObserver.start()
.flowOn(Dispatchers.IO)
.collect { list ->
list.forEach {
powerParser.parse(it)
}
}
}
}
private fun onGraveyardCardsChanged(cardId: String, isUser: Boolean) {
//TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=106 zone=PLAY zonePos=0 cardId= player=1] tag=ZONE value=GRAVEYARD
//TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=5 zone=SECRET zonePos=0 cardId= player=1] tag=COST value=2
//如果对面打出一张奥秘拍 会直接进入墓地
// ?: throw RuntimeException("没有id $entity")
val card = allCards.find {
it.id == cardId
} ?: return
if (card.type == CardType.Enchantment) {
// "衍生牌 $card 不能放到墓地去".log()
return
}
// "插入一张牌到墓地 $card $entity".log()
//TAG_CHANGE Entity=[entityName=破霰元素 id=62 zone=PLAY zonePos=1 cardId=AV_260 player=2] tag=ZONE value=GRAVEYARD
if (isUser) {
_userGraveyardCardList.value += CardBean(card, 1)
} else {
_opponentGraveyardCardList.value += CardBean(card, 1)
}
}
/**
* 清空log文件
*/
private suspend fun clearPowerLogFile() {
val documentFile = getLogsDir()?.findFile(powerFileName)
documentFile?.apply {
context.contentResolver.openOutputStream(uri, "wt")?.use {
it.write("".encodeToByteArray())
it.flush()
it.close()
}
}
}
/**
* 用户牌库的卡牌发生了变化
*/
private fun onUserDeckCardListChanged(cardId: String, remove: Boolean) {
val card = allCards.find {
it.id == cardId
} ?: throw IllegalArgumentException("找不到id是 $cardId 的卡牌")
if (card.type == CardType.Enchantment) {
return
}
// "牌库的卡牌发生了变化 $card $remove ".log()
val bean = deckLeftCardList.find {
it.card.id == card.id
}
val list = mutableListOf()
list.addAll(deckLeftCardList)
if (bean == null) {
list.add(CardBean(card, 1))
} else {
// bean.count =
val newCount = if (remove) bean.count - 1 else bean.count + 1
list[deckLeftCardList.indexOf(bean)] = bean.updateCount(newCount)
}
// if (bean?.count == 3) {
// "插入了3张进去? ".log()
// }
val newList = list.sortedBy {
it.card.cost
}.filter {
it.count > 0
}
// deckLeftCardList.clear()
// deckLeftCardList.addAll(newList)
deckLeftCardList = newList
_deckCardList.value = deckLeftCardList.toList()
// _deckCardList.send(deckLeftCardList)
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/parser/DeckFileObserver.kt
================================================
package com.ke.hs_tracker.module.parser
import android.os.Looper
import com.ke.hs_tracker.module.entity.CurrentDeck
import com.ke.hs_tracker.module.removeTime
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.io.InputStream
/**
* deck文件观察者
*/
class DeckFileObserver constructor(
private val interval: Long = 2000,
private val deckFileInputStreamProvider: suspend () -> InputStream?,
) {
private var oldLogSize = 0L
fun reset() {
oldLogSize = 0
}
suspend fun start(): Flow = flow {
if (Thread.currentThread() == Looper.getMainLooper().thread) {
throw RuntimeException("不能运行在主线程")
}
while (true) {
deckFileInputStreamProvider()?.reader()?.apply {
if (oldLogSize > 0) {
skip(oldLogSize)
}
val text = readText()
oldLogSize += text.length
val lines = text.lines()
.filter { it.isNotEmpty() }
listToDeck(lines)?.apply {
emit(this)
}
close()
}
delay(interval)
}
}
private fun listToDeck(list: List): CurrentDeck? {
if (list.isEmpty()) {
return null
}
val contentList = list.map {
it.removeTime().third
}
val name = contentList.findLast {
it.startsWith("###", true)
} ?: return null
val target =
contentList.subList(contentList.indexOf(name), contentList.size).toMutableList()
target.removeFirst()
target.removeFirst()
val code = target.removeFirst()
return CurrentDeck(
name.replace("### ", ""), code
)
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/parser/PowerFileObserver.kt
================================================
package com.ke.hs_tracker.module.parser
import android.os.Looper
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.io.InputStream
/**
* power文件观察者
*/
class PowerFileObserver(
private val interval: Long = 2000,
private val fileInputStreamProvider: suspend () -> InputStream?,
) {
private var oldLogSize = 0L
fun reset() {
oldLogSize = 0
}
suspend fun start(): Flow> = flow {
if (Thread.currentThread() == Looper.getMainLooper().thread) {
throw RuntimeException("不能运行在主线程")
}
delay(interval)
while (true) {
fileInputStreamProvider()
?.reader()
?.apply {
if (oldLogSize > 0) {
skip(oldLogSize)
}
val text = readText()
// try {
// readText()
// } catch (error: Throwable) {
// if (BuildConfig.DEBUG) {
// error.printStackTrace()
// }
// val size = fileInputStreamProvider()?.available() ?: 0
// Logger.d("内存溢出了 ,文件大小 $size,当前oldLogSize = $oldLogSize")
// oldLogSize += size
//
// readLines()
// ""
// }
// readLines()
// readTextFromFile()
oldLogSize += text.length
val lines = text.lines()
.filter {
it.startsWith("D", true)
}
emit(lines)
close()
}
delay(interval)
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/parser/PowerParser.kt
================================================
package com.ke.hs_tracker.module.parser
import com.ke.hs_tracker.module.entity.Entity
import com.ke.hs_tracker.module.entity.InsertStackResult
import com.ke.hs_tracker.module.entity.LogType
import com.ke.hs_tracker.module.entity.PowerTag
import javax.inject.Inject
/**
* 日志解析
*/
interface PowerParser {
/**
* 解析一行日志
*/
fun parse(content: String)
/**
* 解析结果监听
*/
var powerTagListener: ((PowerTag) -> Unit)?
}
class PowerParserImpl @Inject constructor(
private val blockTagStack: BlockTagStack
) : PowerParser {
// private val blockTagStack: BlockTagStack = BlockTagStackImpl()
override var powerTagListener: ((PowerTag) -> Unit)? = null
override fun parse(content: String) {
val pair = checkTypeAndReturnContent(content) ?: return
if (pair.first == LogType.PowerTaskList) {
handlePowerTaskListLog(pair.second)
} else {
handleGameStateLog(pair.second)
}
}
/**
* 检查日志类型并返回去掉时间和日期前缀的内容
*/
private fun checkTypeAndReturnContent(content: String): Pair? {
if (content.length < TIME_PREFIX_SIZE) {
return null
}
val noTimeContent = content.substring(TIME_PREFIX_SIZE)
if (noTimeContent.startsWith(LogType.GameState.replace)) {
return LogType.GameState to noTimeContent.replace(LogType.GameState.replace, "").trim()
} else if (noTimeContent.startsWith(LogType.PowerTaskList.replace)) {
return LogType.PowerTaskList to noTimeContent.replace(LogType.PowerTaskList.replace, "")
.trim()
}
return null
}
private fun handlePowerTaskListLog(line: String) {
when (val result = blockTagStack.insert(line)) {
InsertStackResult.CanNotInsert -> {
handleUnSupportNestedTag(line)
}
is InsertStackResult.Over -> {
powerTagListener?.invoke(result.powerTag)
if (!result.handled) {
//需要自己处理
handleUnSupportNestedTag(line)
}
}
InsertStackResult.Success -> {
}
}
}
private fun handleUnSupportNestedTag(line: String) {
var matchResult = TAG_CHANGE_PATTERN.matchEntire(line)
if (matchResult != null) {
handleTagChangeLine(
matchResult.groupValues[1],
matchResult.groupValues[2],
matchResult.groupValues[3]
)
}
}
private fun handleTagChangeLine(content: String, tag: String, value: String) {
val entity = Entity.createFromContent(content)!!
powerTagListener?.invoke(PowerTag.PowerTaskList.TagChange(entity, tag, value))
}
private fun handleGameStateLog(content: String) {
BUILD_NUMBER_PATTERN.matchEntire(content)?.apply {
val tag = PowerTag.GameState.BuildNumber(
groupValues[1]
)
powerTagListener?.invoke(tag)
return
}
GAME_TYPE_PATTERN.matchEntire(content)?.apply {
val tag = PowerTag.GameState.GameType(
groupValues[1]
)
powerTagListener?.invoke(tag)
return
}
FORMAT_TYPE_PATTERN.matchEntire(content)?.apply {
val tag = PowerTag.GameState.FormatType(
groupValues[1]
)
powerTagListener?.invoke(tag)
return
}
SCENARIO_ID_PATTERN.matchEntire(content)?.apply {
val tag = PowerTag.GameState.ScenarioID(
groupValues[1]
)
powerTagListener?.invoke(tag)
return
}
PLAYER_MAPPING_PATTERN.matchEntire(content)?.apply {
val tag = PowerTag.GameState.PlayerMapping(
groupValues[1].toInt(), groupValues[2]
)
powerTagListener?.invoke(tag)
return
}
}
companion object {
const val TIME_PREFIX_SIZE = 19
//CREATE_GAME
internal val CREATE_GAME_PATTERN = Regex("CREATE_GAME")
//BuildNumber=127581
internal val BUILD_NUMBER_PATTERN = Regex("BuildNumber=(.*)")
//GameType=GT_CASUAL
internal val GAME_TYPE_PATTERN = Regex("GameType=(.*)")
//FormatType=FT_WILD
internal val FORMAT_TYPE_PATTERN = Regex("FormatType=(.*)")
//ScenarioID=2
internal val SCENARIO_ID_PATTERN = Regex("ScenarioID=(.*)")
//PlayerID=2, PlayerName=阿克萌德#51240
internal val PLAYER_MAPPING_PATTERN = Regex("PlayerID=(.*), PlayerName=(.*)")
// tag=CARDTYPE value=GAME
internal val TAG_PATTERN = Regex("tag=(.*) value=(.*)")
//TAG_CHANGE Entity=GameEntity tag=STATE value=RUNNING
internal val TAG_CHANGE_PATTERN = Regex("TAG_CHANGE Entity=(.*) tag=(.*) value=(.*)")
//GameEntity EntityID=1
internal val GAME_ENTITY_PATTERN = Regex("GameEntity EntityID=(.*)")
//FULL_ENTITY - Updating [entityName=加尔鲁什·地狱咆哮 id=64 zone=PLAY zonePos=0 cardId=HERO_01 player=1] CardID=HERO_01
internal val FULL_ENTITY_PATTERN = Regex("FULL_ENTITY - Updating (.*) CardID=(.*)")
//[entityName=UNKNOWN ENTITY [cardType=INVALID] id=83 zone=DECK zonePos=0 cardId= player=2]
val FULL_ENTITY_CONTENT1_PATTERN =
Regex("\\[entityName=(.*) \\[cardType=(.*)] id=(.*) zone=(.*) zonePos=(.*) cardId=(.*) player=(.*)]")
val FULL_ENTITY_CONTENT2_PATTERN =
Regex("\\[entityName=(.*) id=(.*) zone=(.*) zonePos=(.*) cardId=(.*) player=(.*)]")
val SHOW_ENTITY = Regex("SHOW_ENTITY - Updating Entity=(.*) CardID=(.*)")
//Player EntityID=2 PlayerID=1 GameAccountId=[hi=144115211015832391 lo=191215280]
internal val PLAYER_ENTITY_PATTERN =
Regex("Player EntityID=(.*) PlayerID=(.*) GameAccountId=(.*)")
//BLOCK_START BlockType=ATTACK Entity=[entityName=瓦丝琪女士 id=87 zone=PLAY zonePos=1 cardId=BT_109 player=2]
// EffectCardId=System.Collections.Generic.List`1[System.String]
// EffectIndex=0 Target=[entityName=驯化的雷象 id=78 zone=PLAY zonePos=1 cardId=SCH_714 player=1] SubOption=-1
internal val BLOCK_START_PATTERN =
Regex("BLOCK_START BlockType=(.*) Entity=(.*) EffectCardId=(.*) EffectIndex=(.*) Target=(.*) SubOption=(.*)")
internal val BLOCK_START_CONTINUATION_PATTERN = Regex("(.*) TriggerKeyword=(.*)")
//BLOCK_END
internal val BLOCK_END_PATTERN = Regex("BLOCK_END")
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/parser/PowerTagHandler.kt
================================================
package com.ke.hs_tracker.module.parser
import com.ke.hs_tracker.module.db.Game
import com.ke.hs_tracker.module.domain.GetAllCardUseCase
import com.ke.hs_tracker.module.entity.*
import com.ke.hs_tracker.module.log
import com.ke.mvvm.base.data.successOr
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* power tag处理器
*/
interface PowerTagHandler {
/**
* 处理一个事件
*/
fun handle(powerTag: PowerTag)
/**
* 游戏事件流
*/
val gameEventFlow: Flow
}
class PowerTagHandlerImpl @Inject constructor(
private val getAllCardUseCase: GetAllCardUseCase
) : PowerTagHandler {
lateinit var allCard: List
init {
GlobalScope.launch {
allCard = getAllCardUseCase(Unit).successOr(emptyList())
}
}
private val _gameEventFlow =
// Channel(capacity = Channel.UNLIMITED)
MutableStateFlow(null)
override val gameEventFlow: Flow
get() = _gameEventFlow
/**
* 当前卡组
*/
// var currentDeck: CurrentDeck? = null
/**
* 玩家
*/
private var user: PowerTag.GameState.PlayerMapping? = null
/**
* 对手
*/
private var opponent: PowerTag.GameState.PlayerMapping? = null
/**
* 当前回合数
*/
private var currentTurn = 0
/**
* 游戏实体
*/
private var game: Game = Game()
private val entityIdAndCardIdMap = mutableMapOf()
override fun handle(powerTag: PowerTag) {
when (powerTag) {
is PowerTag.GameState.BuildNumber -> {
game = Game(buildNumber = powerTag.number)
// game.userDeckName = currentDeck?.name ?: ""
// game.userDeckCode = currentDeck?.code ?: ""
}
is PowerTag.GameState.FormatType -> {
game.formatType = powerTag.type.toFormatType
}
is PowerTag.GameState.GameType -> {
game.gameType = powerTag.type.toGameType
}
is PowerTag.GameState.PlayerMapping -> {
if (powerTag.isUser) {
game.userName = powerTag.name
user = powerTag
// game.isUserFirst = tag.first
} else {
game.opponentName = powerTag.name
opponent = powerTag
}
}
is PowerTag.GameState.ScenarioID -> {
game.scenarioID = powerTag.id.toInt()
}
is PowerTag.PowerTaskList.Block -> {
powerTag.list.forEach {
handle(it)
}
}
is PowerTag.PowerTaskList.CreateGame -> {
//初始化卡牌
onGameStarted()
// gameEventListener(GameEvent.OnGameStarted)
}
is PowerTag.PowerTaskList.FullEntity -> {
// handleFullEntity(tag)
handleFullEntity(powerTag)
//FULL_ENTITY - Updating [entityName=萨尔 id=74 zone=PLAY zonePos=0 cardId=HERO_02 player=2] CardID=HERO_02
// tag=CONTROLLER value=2
// tag=CARDTYPE value=HERO
// tag=HEALTH value=30
// tag=ZONE value=PLAY
// tag=ENTITY_ID value=74
// tag=FACTION value=NEUTRAL
// tag=RARITY value=FREE
// tag=HERO_POWER value=687
// tag=SPAWN_TIME_COUNT value=1
powerTag.cardId?.apply {
entityIdAndCardIdMap[powerTag.entity.id] = this
}
}
is PowerTag.PowerTaskList.ShowEntity -> {
handleShowEntity(powerTag)
entityIdAndCardIdMap[powerTag.entity.id] = powerTag.cardId
}
is PowerTag.PowerTaskList.TagChange -> {
handleTagChange(powerTag)
}
}
// tag.toString().log()
(powerTag as? ZoneUpdatable)?.apply {
isUpdateZone(user?.id)?.let {
val cardId = it.cardId ?: entityIdAndCardIdMap[it.entityId]
// val event = ZonePositionChangedEvent(
// entityId = it.first.id,
// cardId = it.first.cardId,
// isUser = it.first.player == user?.id,
// currentZone = it.first.zone,
// newZone = it.second,
// currentPosition = it.first.zonePosition,
// newPosition = it.first.zonePosition
// )
// zoneChangedEventList.add(it)
if (it.currentZone == Zone.Deck && it.newZone != Zone.Deck && it.isUser) {
//从牌库中抽取一张卡
//SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=48 zone=DECK zonePos=0 cardId= player=2]
// tag=CONTROLLER value=2
// tag=CARDTYPE value=SPELL
// tag=TAG_LAST_KNOWN_COST_IN_HAND value=1
// tag=COST value=1
// tag=PREMIUM value=1
// tag=ZONE value=HAND
// tag=ENTITY_ID value=48
// tag=ELITE value=1
// tag=CLASS value=SHAMAN
// tag=RARITY value=LEGENDARY
// tag=478 value=2
// tag=QUEST_PROGRESS_TOTAL value=3
// tag=676 value=1
// tag=839 value=1
// tag=1043 value=1
// tag=1068 value=0
// tag=QUEST_REWARD_DATABASE_ID value=64323
// tag=SPAWN_TIME_COUNT value=1
//或者 探底
//TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=1068 value=3
//TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=1068 value=0
//TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=1037 value=1
//TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=ZONE value=HAND
//TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=ZONE_POSITION value=6
//探底抽上来的卡是没有cardId的
if (cardId != null)
removeCardFromDeck(cardId)
} else if (it.newZone == Zone.Deck && it.currentZone != Zone.Deck && it.isUser) {
//当心探底
//SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] CardID=DED_002
// tag=CONTROLLER value=1
// tag=CARDTYPE value=SPELL
// tag=TAG_LAST_KNOWN_COST_IN_HAND value=2
// tag=COST value=2
// tag=ZONE value=DECK
// tag=ENTITY_ID value=28
// tag=RARITY value=RARE
// tag=DISCOVER value=1
// tag=478 value=1
// tag=1043 value=1
// tag=1068 value=0
// tag=USE_DISCOVER_VISUALS value=1
// tag=SPAWN_TIME_COUNT value=1
// tag=SPELL_SCHOOL value=1
// tag=1711 value=1
// tag=MINI_SET value=1
//有牌插入到牌库
//TAG_CHANGE Entity=[entityName=冷风 id=70 zone=HAND zonePos=2 cardId=AV_266 player=2] tag=ZONE value=DECK
//会出现id为空的情况
//FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=17 zone=DECK zonePos=0 cardId= player=1] CardID=
// tag=ZONE value=DECK
// tag=CONTROLLER value=1
// tag=ENTITY_ID value=17
if (cardId != null)
insertCardToDeck(cardId)
} else if (it.currentZone == Zone.Play && it.newZone == Zone.Graveyard) {
onGraveyardCardsChanged(cardId, it.isUser)
} else if (it.newZone == Zone.Hand || it.currentZone == Zone.Hand) {
//手牌
if (!it.isUser) {
// handleOpponentHandChanged(it)
}
}
}
}
}
private fun onGraveyardCardsChanged(cardId: String?, user: Boolean) {
if (cardId == null) {
return
}
_gameEventFlow.value = GameEvent.InsertCardToGraveyard(cardId, user)
}
// private fun findCardById(cardId: String) =
// allCard.find { it.id == cardId } ?: throw RuntimeException("id为 $cardId 没有这张牌")
private fun removeCardFromDeck(cardId: String) {
_gameEventFlow.value = (GameEvent.RemoveCardFromUserDeck(cardId))
}
private fun insertCardToDeck(cardId: String) {
_gameEventFlow.value = (GameEvent.InsertCardToUserDeck(cardId))
}
private fun handleTagChange(tagChange: PowerTag.PowerTaskList.TagChange) {
tagChange.isTurnChanged()?.apply {
currentTurn = this
}
if (tagChange.entity.player == user?.id && tagChange.entity.zone == Zone.Hand && tagChange.tag == "ZONE" && tagChange.value == "DECK") {
//TAG_CHANGE Entity=[entityName=冷风 id=15 zone=HAND zonePos=3 cardId=AV_266 player=1] tag=ZONE value=DECK
// insertCardToDeck(tagChange.entity.cardId!!)
} else if (tagChange.tag == "ZONE" && tagChange.value == "GRAVEYARD" && tagChange.entity.zone == Zone.Play) {
//TAG_CHANGE Entity=[entityName=破霰元素 id=62 zone=PLAY zonePos=1 cardId=AV_260 player=2] tag=ZONE value=GRAVEYARD
//随从死亡后进入墓地
//TAG_CHANGE Entity=[entityName=始生研习 id=63 zone=PLAY zonePos=0 cardId=SCH_270 player=2] tag=ZONE value=GRAVEYARD
//打出法术
// onGraveyardCardsChanged(tagChange.entity)
} else if (tagChange.isGameComplete) {
"游戏结束了".log()
onGameOver()
}
val pair = tagChange.isPlayerWonOrLost
if (pair != null) {
"有玩家胜利或失败了 $pair $game".log()
if (pair.first == game.userName) {
game.isUserWin = pair.second
} else {
game.opponentName = pair.first
}
}
if (tagChange.tag == "FIRST_PLAYER" && tagChange.value == "1") {
game.isUserFirst = tagChange.entity.entityName == game.userName
}
}
private fun onGameOver() {
// _deckLeftCardList.value = deckCardList
game.endTime = System.currentTimeMillis()
_gameEventFlow.value = GameEvent.OnGameOver(game)
}
private fun handleShowEntity(showEntity: PowerTag.PowerTaskList.ShowEntity) {
if (showEntity.entity.player == user?.id && showEntity.entity.zone == Zone.Deck && showEntity.payloads["ZONE"].equals(
"hand",
true
)
) {
//起始手牌
//SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=15 zone=DECK zonePos=0 cardId= player=1] CardID=AV_266
// tag=CONTROLLER value=1
// tag=CARDTYPE value=SPELL
// tag=TAG_LAST_KNOWN_COST_IN_HAND value=1
// tag=COST value=1
// tag=PREMIUM value=1
// tag=ZONE value=HAND
// tag=ENTITY_ID value=15
// tag=RARITY value=COMMON
// tag=478 value=1
// tag=1043 value=1
// tag=1068 value=0
// tag=SPAWN_TIME_COUNT value=1
// tag=SPELL_SCHOOL value=3
// removeCardFromDeck(showEntity.cardId)
} else if (showEntity.entity.player == user?.id && showEntity.entity.zone == Zone.Deck && showEntity.payloads["ZONE"].equals(
"GRAVEYARD",
true
)
) {
//爆牌
// ShowEntity(entity=Entity(entityName=UNKNOWN ENTITY, gameCardType=Invalid, id=54, zone=Deck, zonePosition=0, cardId=null, player=2), cardId=OG_176, payloads={CONTROLLER=2, CARDTYPE=SPELL, TAG_LAST_KNOWN_COST_IN_HAND=3, COST=3, ZONE=GRAVEYARD, ENTITY_ID=54, RARITY=COMMON, 478=2, 1037=2, 1043=1, 1068=0, SPAWN_TIME_COUNT=1, SPELL_SCHOOL=6})
// removeCardFromDeck(showEntity.cardId)
}
}
/**
* 游戏开始
*/
private fun onGameStarted() {
"游戏开始了".log()
game.startTime = System.currentTimeMillis()
currentTurn = 0
_gameEventFlow.value = (GameEvent.OnGameStart)
}
private fun handleFullEntity(fullEntity: PowerTag.PowerTaskList.FullEntity) {
fullEntity.isUpdateHero()?.apply {
val cardClass = allCard.find {
it.id == second
}?.cardClass ?: return
"更新英雄 $this $cardClass".log()
if (first == user?.id) {
game.userHero = cardClass
} else if (first == opponent?.id) {
game.opponentHero = cardClass
}
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/service/ItemViewTouchListener.kt
================================================
package com.ke.hs_tracker.module.service
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
class ItemViewTouchListener(
private val wl: WindowManager.LayoutParams,
private val windowManager: WindowManager,
private val rootView: View
) :
View.OnTouchListener {
private var x = 0
private var y = 0
override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
when (motionEvent.action) {
MotionEvent.ACTION_DOWN -> {
x = motionEvent.rawX.toInt()
y = motionEvent.rawY.toInt()
}
MotionEvent.ACTION_MOVE -> {
val nowX = motionEvent.rawX.toInt()
val nowY = motionEvent.rawY.toInt()
val movedX = nowX - x
val movedY = nowY - y
x = nowX
y = nowY
wl.apply {
x += movedX
y += movedY
}
//更新悬浮球控件位置
windowManager.updateViewLayout(rootView, wl)
}
else -> {
}
}
return false
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/service/ScaleTouchListener.kt
================================================
package com.ke.hs_tracker.module.service
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
class ScaleTouchListener constructor(
private val windowManager: WindowManager,
private val rootView: View,
private val layoutParams: WindowManager.LayoutParams
) : View.OnTouchListener {
private var x = 0
private var y = 0
override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
when (motionEvent.action) {
MotionEvent.ACTION_DOWN -> {
x = motionEvent.rawX.toInt()
y = motionEvent.rawY.toInt()
}
MotionEvent.ACTION_MOVE -> {
val nowX = motionEvent.rawX.toInt()
val nowY = motionEvent.rawY.toInt()
val movedX = nowX - x
val movedY = nowY - y
x = nowX
y = nowY
layoutParams.apply {
// x += movedX
// y += movedY
width += movedX
height += movedY
}
//更新悬浮球控件位置
// windowManager.updateViewLayout(rootView, layoutParams)
windowManager.updateViewLayout(rootView, layoutParams)
}
else -> {
}
}
return true
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/service/WindowService.kt
================================================
package com.ke.hs_tracker.module.service
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import android.widget.AdapterView
import android.widget.AdapterView.OnItemSelectedListener
import android.widget.ArrayAdapter
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import com.ke.hs_tracker.module.R
import com.ke.hs_tracker.module.databinding.ModuleFloatingWindowBinding
import com.ke.hs_tracker.module.log
import com.ke.hs_tracker.module.parser.DeckCardObserver
import com.ke.hs_tracker.module.ui.common.CardAdapter
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class WindowService : LifecycleService() {
private val windowManager: WindowManager by lazy {
getSystemService(WINDOW_SERVICE) as WindowManager
}
private val binding: ModuleFloatingWindowBinding by lazy {
val layoutInflater = LayoutInflater.from(applicationContext)
ModuleFloatingWindowBinding.inflate(layoutInflater)
}
private val deckAdapter = CardAdapter()
private val graveyardAdapter = CardAdapter()
private val opponentGraveyardAdapter = CardAdapter()
@Inject
lateinit var deckCardObserver: DeckCardObserver
private var showList = false
private fun showView() {
val layoutParams = WindowManager.LayoutParams()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}
layoutParams.width = resources.getDimension(R.dimen.module_floating_window_width).toInt()
layoutParams.height =
// LayoutParams.WRAP_CONTENT
resources.getDimension(R.dimen.module_floating_window_height).toInt()
//需要设置 这个 不然空白地方无法点击
layoutParams.flags =
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
// layoutParams.alpha = 0.8f
layoutParams.gravity = Gravity.START or Gravity.TOP
windowManager.addView(binding.root, layoutParams)
binding.recyclerView.adapter = deckAdapter
binding.scale.setOnTouchListener(
ScaleTouchListener(windowManager, binding.root, layoutParams)
)
binding.hide.setOnClickListener {
// binding.recyclerView.isVisible = !binding.recyclerView.isVisible
layoutParams.height = if (showList) {
resources.getDimension(R.dimen.module_floating_window_height).toInt()
} else {
resources.getDimension(R.dimen.module_floating_window_header_height).toInt()
}
windowManager.updateViewLayout(binding.root, layoutParams)
showList = !showList
}
binding.close.setOnClickListener {
windowManager.removeView(binding.root)
stopSelf()
}
binding.spinner.adapter = ArrayAdapter.createFromResource(
applicationContext,
R.array.module_spinner,
android.R.layout.simple_list_item_1
)
binding.spinner.onItemSelectedListener = object : OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>,
view: View,
position: Int,
id: Long
) {
val adapter = when (position) {
0 -> {
deckAdapter
}
1 -> {
graveyardAdapter
}
2 -> {
opponentGraveyardAdapter
}
else -> throw IllegalArgumentException("错误的position $position")
}
binding.recyclerView.adapter = adapter
windowManager.updateViewLayout(binding.root, layoutParams)
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
binding.spinner.setSelection(0)
binding.root.setOnTouchListener(
ItemViewTouchListener(
layoutParams,
windowManager,
binding.root
)
)
}
override fun onCreate() {
super.onCreate()
showView()
deckCardObserver.init(lifecycleScope)
// lifecycleScope.launch {
// delay(5000)
// throw RuntimeException("测试异常")
// }
lifecycleScope.launch {
deckCardObserver.deckCardList.collect {
// adapter.setList(it)
deckAdapter.setDiffNewData(it.toMutableList())
}
}
lifecycleScope.launch {
deckCardObserver.userGraveyardCardList.collect {
graveyardAdapter.setDiffNewData(it.toMutableList())
}
}
lifecycleScope.launch {
deckCardObserver.opponentGraveyardCardList.collect {
opponentGraveyardAdapter.setDiffNewData(it.toMutableList())
}
}
}
override fun onDestroy() {
super.onDestroy()
"service 挂了".log()
}
override fun onBind(intent: Intent): IBinder? {
return null
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/chart/GetSummaryChartViewDataUseCase.kt
================================================
package com.ke.hs_tracker.module.ui.chart
import com.ke.hs_tracker.module.db.GameDao
import com.ke.hs_tracker.module.di.IoDispatcher
import com.ke.hs_tracker.module.entity.CardClass
import com.ke.mvvm.base.domian.UseCase
import kotlinx.coroutines.CoroutineDispatcher
import javax.inject.Inject
internal class GetSummaryChartViewDataUseCase @Inject constructor(
@IoDispatcher dispatcher: CoroutineDispatcher,
private val gameDao: GameDao
) : UseCase(dispatcher) {
override suspend fun execute(parameters: Unit): SummaryChartViewData {
val games = gameDao.getAll()
val winCount = games.count {
it.isUserWin == true
}
val lossCount = games.count {
it.isUserWin == false
}
val firstHandCount = games.count {
it.isUserFirst == true
}
val secondHandCount = games.count {
it.isUserFirst == false
}
val firstHandWinCount = games.count {
it.isUserFirst == true && it.isUserWin == true
}
val firstHandLossCount = games.count {
it.isUserFirst == true && it.isUserWin == false
}
val secondHandWinCount = games.count {
it.isUserFirst == false && it.isUserWin == true
}
val secondHandLossCount = games.count {
it.isUserFirst == false && it.isUserWin == false
}
val classCounts = CardClass.values()
.filter {
it.isHero
}.map {
it to games.count { game ->
game.opponentHero == it
}
}
return SummaryChartViewData(
winCount,
lossCount,
firstHandCount,
secondHandCount,
firstHandWinCount,
firstHandLossCount,
secondHandWinCount,
secondHandLossCount,
classCounts
)
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/chart/PieChartData.kt
================================================
package com.ke.hs_tracker.module.ui.chart
import androidx.annotation.ColorRes
data class PieChartData(
val label: String,
val value: Int,
@ColorRes
val color: Int
)
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/chart/SummaryChartActivity.kt
================================================
package com.ke.hs_tracker.module.ui.chart
import android.graphics.Color
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.github.mikephil.charting.charts.PieChart
import com.github.mikephil.charting.data.PieData
import com.github.mikephil.charting.data.PieDataSet
import com.github.mikephil.charting.data.PieEntry
import com.github.mikephil.charting.formatter.PercentFormatter
import com.ke.hs_tracker.module.R
import com.ke.hs_tracker.module.databinding.ModuleActivitySummaryChartBinding
import com.ke.mvvm.base.data.Result
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class SummaryChartActivity : AppCompatActivity() {
@Inject
internal lateinit var getSummaryChartViewDataUseCase: GetSummaryChartViewDataUseCase
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.module_activity_summary_chart)
val binding: ModuleActivitySummaryChartBinding =
ModuleActivitySummaryChartBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.toolbar.setNavigationOnClickListener {
onBackPressed()
}
// binding.all.apply {
// //设置成实心
// holeRadius = 0f
// transparentCircleRadius = 0f
// //禁用描述
// description.isEnabled = false
// //禁用颜色描述
// legend.isEnabled = false
// setUsePercentValues(true)
// val pieEntryList = mutableListOf()
// pieEntryList.add(PieEntry(41f, "胜利41"))
// pieEntryList.add(PieEntry(60f, "失败60"))
// val pieDataSet = PieDataSet(pieEntryList, "")
// pieDataSet.valueTextSize = 20f
// pieDataSet.valueTextColor = Color.WHITE
//
// pieDataSet.colors = listOf(R.color.module_win, R.color.module_loss).map {
// resources.getColor(it, null)
// }
// val pieData = PieData(pieDataSet)
// //显示次数
// pieData.setDrawValues(true)
// pieData.setValueFormatter(PercentFormatter())
// data = pieData
// invalidate()
// }
lifecycleScope.launch {
val result = getSummaryChartViewDataUseCase(Unit)
when (result) {
is Result.Success -> {
setChartData(result.data, binding)
}
is Result.Error -> {
result.exception.printStackTrace()
}
}
}
}
private fun setChartData(
summaryChartViewData: SummaryChartViewData,
binding: ModuleActivitySummaryChartBinding
) {
setupPieChart(
listOf(
PieChartData(
"${getString(R.string.module_win)}${summaryChartViewData.winCount}",
summaryChartViewData.winCount,
R.color.module_win
),
PieChartData(
"${getString(R.string.module_loss)}${summaryChartViewData.lossCount}",
summaryChartViewData.lossCount,
R.color.module_loss
),
),
binding.all
)
setupPieChart(
listOf(
PieChartData(
"${getString(R.string.module_first_hand)}${summaryChartViewData.firstHandCount}",
summaryChartViewData.firstHandCount,
R.color.module_win
),
PieChartData(
"${getString(R.string.module_second_hand)}${summaryChartViewData.secondHandCount}",
summaryChartViewData.secondHandCount,
R.color.module_loss
),
),
binding.firstHandPercent
)
setupPieChart(
listOf(
PieChartData(
"${getString(R.string.module_win)}${summaryChartViewData.firstHandWinCount}",
summaryChartViewData.firstHandWinCount,
R.color.module_win
),
PieChartData(
"${getString(R.string.module_loss)}${summaryChartViewData.firstHandLossCount}",
summaryChartViewData.firstHandLossCount,
R.color.module_loss
),
),
binding.firstHandRate
)
setupPieChart(
listOf(
PieChartData(
"${getString(R.string.module_win)}${summaryChartViewData.secondHandWinCount}",
summaryChartViewData.secondHandWinCount,
R.color.module_win
),
PieChartData(
"${getString(R.string.module_loss)}${summaryChartViewData.secondHandLossCount}",
summaryChartViewData.secondHandLossCount,
R.color.module_loss
),
),
binding.secondHandRate
)
setupPieChart(
summaryChartViewData.classCounts.map {
PieChartData(
getString(it.first.titleRes) + it.second,
it.second,
it.first.color
)
},
binding.classDistribution
)
}
private fun setupPieChart(list: List, pieChart: PieChart) {
pieChart.apply {
//设置成实心
holeRadius = 0f
transparentCircleRadius = 0f
//禁用描述
description.isEnabled = false
//禁用颜色描述
legend.isEnabled = false
setUsePercentValues(true)
val pieDataSet = PieDataSet(list.map {
return@map PieEntry(
it.value.toFloat(), it.label
)
}, "")
pieDataSet.valueTextSize = 16f
pieDataSet.valueTextColor = Color.WHITE
pieDataSet.colors = list.map {
it.color
}.map {
resources.getColor(it, null)
}
val pieData = PieData(pieDataSet)
// pieData.setDrawValues(true)
pieData.setValueFormatter(PercentFormatter(this))
// pieData.setValueFormatter(object : ValueFormatter() {
// override fun getFormattedValue(value: Float): String {
// return "$value %"
// }
// })
data = pieData
invalidate()
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/chart/SummaryChartViewData.kt
================================================
package com.ke.hs_tracker.module.ui.chart
import com.ke.hs_tracker.module.entity.CardClass
internal data class SummaryChartViewData(
/**
* 总胜利次数
*/
val winCount: Int,
/**
* 总失败次数
*/
val lossCount: Int,
/**
* 先手次数
*/
val firstHandCount: Int,
/**
* 后手次数
*/
val secondHandCount: Int,
/**
* 先手胜利次数
*/
val firstHandWinCount: Int,
/**
* 先手失败次数
*/
val firstHandLossCount: Int,
/**
* 后手胜利次数
*/
val secondHandWinCount: Int,
/**
* 后手失败次数
*/
val secondHandLossCount: Int,
/**
* 职业出现频度
*/
val classCounts: List>
)
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/classbattledetail/ClassBattleDetailActivity.kt
================================================
package com.ke.hs_tracker.module.ui.classbattledetail
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.DividerItemDecoration
import com.ke.hs_tracker.module.databinding.ModuleActivityClassBattleDetailBinding
import com.ke.hs_tracker.module.databinding.ModuleItemClassBattleDetailBinding
import com.ke.hs_tracker.module.entity.CardClass
import com.ke.mvvm.base.data.ViewStatus
import com.ke.mvvm.base.ui.BaseViewBindingAdapter
import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
@AndroidEntryPoint
class ClassBattleDetailActivity : AppCompatActivity() {
internal val viewModel: ClassBattleDetailViewModel by viewModels()
private val adapter by lazy {
object : BaseViewBindingAdapter() {
override fun bindItem(
item: ClassBattleItem,
viewBinding: ModuleItemClassBattleDetailBinding,
viewType: Int,
position: Int
) {
viewBinding.apply {
value1.setText(item.hero.titleRes)
value2.text = item.times.toString()
value3.text = item.win.toString()
value4.text = item.loss
.toString()
value5.text = "${item.rate}%"
}
}
override fun createViewBinding(
inflater: LayoutInflater,
parent: ViewGroup,
viewType: Int
): ModuleItemClassBattleDetailBinding {
return ModuleItemClassBattleDetailBinding.inflate(inflater, parent, false)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.module_activity_class_battle_detail)
val binding = ModuleActivityClassBattleDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.toolbar.setNavigationOnClickListener {
onBackPressed()
}
binding.recyclerView.adapter = adapter
binding.recyclerView.addItemDecoration(
DividerItemDecoration(
this,
DividerItemDecoration.VERTICAL
)
)
launchAndRepeatWithViewLifecycle {
viewModel.viewStatus.collect {
when (it) {
is ViewStatus.Loading -> {
}
is ViewStatus.Content -> {
adapter.setList(it.data)
}
is ViewStatus.Error -> {
}
}
}
}
}
companion object {
fun createIntent(context: Context, cardClass: CardClass): Intent {
return Intent(context, ClassBattleDetailActivity::class.java).apply {
putExtra(EXTRA_CARD_CLASS, cardClass)
}
}
internal const val EXTRA_CARD_CLASS = "extra_card_class"
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/classbattledetail/ClassBattleDetailViewModel.kt
================================================
package com.ke.hs_tracker.module.ui.classbattledetail
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.ke.hs_tracker.module.entity.CardClass
import com.ke.mvvm.base.data.successOr
import com.ke.mvvm.base.ui.BaseContentViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
internal class ClassBattleDetailViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val getClassBattleItemListUseCase: GetClassBattleItemListUseCase
) :
BaseContentViewModel>() {
internal val cardClass: CardClass =
savedStateHandle.get(ClassBattleDetailActivity.EXTRA_CARD_CLASS)!!
init {
viewModelScope.launch {
showLoading()
showContent(getClassBattleItemListUseCase(cardClass).successOr(emptyList()))
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/classbattledetail/ClassBattleItem.kt
================================================
package com.ke.hs_tracker.module.ui.classbattledetail
import com.ke.hs_tracker.module.entity.CardClass
internal data class ClassBattleItem(
val hero: CardClass,
val times: Int,
val win: Int,
val loss: Int,
val rate: Int
)
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/classbattledetail/GetClassBattleItemListUseCase.kt
================================================
package com.ke.hs_tracker.module.ui.classbattledetail
import com.ke.hs_tracker.module.db.GameDao
import com.ke.hs_tracker.module.di.IoDispatcher
import com.ke.hs_tracker.module.entity.CardClass
import com.ke.mvvm.base.domian.UseCase
import kotlinx.coroutines.CoroutineDispatcher
import javax.inject.Inject
internal class GetClassBattleItemListUseCase @Inject constructor(
@IoDispatcher
dispatcher: CoroutineDispatcher,
private val dao: GameDao
) : UseCase>(dispatcher) {
override suspend fun execute(parameters: CardClass): List {
val items = mutableListOf()
dao.getByHero(parameters)
.groupBy {
it.opponentHero!!
}.forEach { cardClass, list ->
val times = list.size
val win = list.count {
it.isUserWin == true
}
val loss = times - win
val rate = win * 100 / times
val item = ClassBattleItem(cardClass, times, win, loss, rate)
items.add(item)
}
return items
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/common/CardAdapter.kt
================================================
package com.ke.hs_tracker.module.ui.common
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import com.ke.hs_tracker.module.bindCard
import com.ke.hs_tracker.module.databinding.ModuleItemCardBinding
import com.ke.hs_tracker.module.entity.CardBean
import com.ke.mvvm.base.ui.BaseViewBindingAdapter
class CardAdapter : BaseViewBindingAdapter() {
init {
setDiffCallback(object : DiffUtil.ItemCallback() {
override fun areItemsTheSame(oldItem: CardBean, newItem: CardBean): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: CardBean, newItem: CardBean): Boolean {
return oldItem == newItem
}
})
}
override fun bindItem(
item: CardBean,
viewBinding: ModuleItemCardBinding,
viewType: Int,
position: Int
) {
viewBinding.bindCard(item.card)
// viewBinding.name.text = item.card.name
// viewBinding.cost.text = item.card.cost.toString()
viewBinding.count.text = item.count.toString()
// item.card.rarity?.apply {
// viewBinding.name.setTextColor(
// ResourcesCompat.getColor(
// viewBinding.root.context.resources,
// colorRes,
// null
// )
// )
//
// }
//
// Glide.with(viewBinding.imageTile)
// .load("https://art.hearthstonejson.com/v1/tiles/${item.card.id}.png")
// .into(viewBinding.imageTile)
}
override fun createViewBinding(
inflater: LayoutInflater,
parent: ViewGroup,
viewType: Int
): ModuleItemCardBinding {
return ModuleItemCardBinding.inflate(inflater, parent, false)
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/common/LoadingFragment.kt
================================================
package com.ke.hs_tracker.module.ui.common
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.hi.dhl.binding.viewbind
import com.ke.hs_tracker.module.databinding.ModuleFragmentLoadingBinding
class LoadingFragment : Fragment() {
private val binding:ModuleFragmentLoadingBinding by viewbind()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return binding.root
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/deck/DeckCodeParserActivity.kt
================================================
package com.ke.hs_tracker.module.ui.deck
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.DividerItemDecoration
import com.ke.hs_tracker.module.databinding.ModuleActivityDeckCodeParserBinding
import com.ke.hs_tracker.module.ui.common.CardAdapter
import com.ke.mvvm.base.data.ViewStatus
import com.ke.mvvm.base.ui.collectLoadingDialog
import com.ke.mvvm.base.ui.collectSnackbarFlow
import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
@AndroidEntryPoint
class DeckCodeParserActivity : AppCompatActivity() {
private val viewModel: DeckCodeParserViewModel by viewModels()
private lateinit var binding: ModuleActivityDeckCodeParserBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ModuleActivityDeckCodeParserBinding.inflate(layoutInflater)
setContentView(binding.root)
collectLoadingDialog(viewModel)
collectSnackbarFlow(viewModel)
binding.recyclerView.addItemDecoration(
DividerItemDecoration(
this,
DividerItemDecoration.VERTICAL
)
)
val adapter = CardAdapter()
binding.recyclerView.adapter = adapter
binding.start.setOnClickListener {
viewModel.start(
binding.code.text?.toString() ?: ""
)
}
binding.toolbar.setNavigationOnClickListener {
onBackPressed()
}
launchAndRepeatWithViewLifecycle {
viewModel.viewStatus.collect {
when (it) {
is ViewStatus.Loading -> {
}
is ViewStatus.Content -> {
adapter.setList(it.data.apply {
})
}
is ViewStatus.Error -> {
}
}
}
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/deck/DeckCodeParserViewModel.kt
================================================
package com.ke.hs_tracker.module.ui.deck
import androidx.lifecycle.viewModelScope
import com.ke.hs_tracker.module.domain.ParseDeckCodeUseCase
import com.ke.hs_tracker.module.entity.CardBean
import com.ke.mvvm.base.data.successOr
import com.ke.mvvm.base.ui.BaseContentViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class DeckCodeParserViewModel @Inject constructor(private val parseDeckCodeUseCase: ParseDeckCodeUseCase) :
BaseContentViewModel>() {
fun start(code: String) {
viewModelScope.launch {
showLoadingDialog("加载中")
val list = parseDeckCodeUseCase(code).successOr(emptyList())
dismissLoadingDialog()
showContent(list)
// when (result) {
// is Result.Success -> {
// showContent(result.data)
// }
// is Result.Error -> {
// result.exception.printStackTrace()
// }
// }
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/BattleRecordsFragment.kt
================================================
package com.ke.hs_tracker.module.ui.deckbattledetail
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.DividerItemDecoration
import com.hi.dhl.binding.viewbind
import com.ke.hs_tracker.module.ui.records.RecordAdapter
import com.ke.mvvm.base.databinding.KeMvvmLayoutBaseRefreshListRetryBinding
import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
@AndroidEntryPoint
class BattleRecordsFragment : Fragment() {
private val battleRecordsViewModel: BattleRecordsViewModel by activityViewModels()
private val binding: KeMvvmLayoutBaseRefreshListRetryBinding by viewbind()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return binding.root
}
private val adapter by lazy {
RecordAdapter()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.swipeRefreshLayout.isEnabled = false
binding.recyclerView.adapter = adapter
binding.recyclerView.addItemDecoration(
DividerItemDecoration(
requireContext(),
DividerItemDecoration.VERTICAL
)
)
launchAndRepeatWithViewLifecycle {
battleRecordsViewModel.games.collect {
adapter.setList(it)
}
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/BattleRecordsViewModel.kt
================================================
package com.ke.hs_tracker.module.ui.deckbattledetail
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.ke.hs_tracker.module.db.Game
import com.ke.mvvm.base.data.successOr
import com.ke.mvvm.base.ui.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class BattleRecordsViewModel @Inject constructor(
private val getGamesByDeckCodeAndNameUseCase: GetGamesByDeckCodeAndNameUseCase,
savedStateHandle: SavedStateHandle
) : BaseViewModel() {
private val _games = MutableStateFlow>(emptyList())
internal val games: StateFlow>
get() = _games
init {
viewModelScope.launch {
val code = savedStateHandle.get(DeckBattleDetailActivity.EXTRA_DECK_CODE)!!
val name = savedStateHandle.get(DeckBattleDetailActivity.EXTRA_DECK_NAME)!!
_games.value = getGamesByDeckCodeAndNameUseCase(code to name).successOr(emptyList())
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/DeckBattleDetailActivity.kt
================================================
package com.ke.hs_tracker.module.ui.deckbattledetail
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayoutMediator
import com.ke.hs_tracker.module.R
import com.ke.hs_tracker.module.databinding.ModuleActivityDeckBattleDetailBinding
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class DeckBattleDetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ModuleActivityDeckBattleDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.toolbar.setNavigationOnClickListener {
onBackPressed()
}
val fragmentList = listOf(
SummaryFragment(),
BattleRecordsFragment(),
DeckFragment(),
DeckDetailFragment()
)
val titles = listOf(
getString(R.string.module_summary),
getString(R.string.module_record),
getString(R.string.module_deck),
getString(
R.string.module_deck_detail
)
)
val adapter = object : FragmentStateAdapter(this) {
override fun getItemCount(): Int {
return fragmentList.size
}
override fun createFragment(position: Int): Fragment {
return fragmentList[position]
}
}
binding.viewPager.adapter = adapter
TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, index ->
tab.text = titles[index]
}.attach()
}
companion object {
fun createIntent(context: Context, deckCode: String, deckName: String): Intent {
return Intent(context, DeckBattleDetailActivity::class.java).apply {
putExtra(EXTRA_DECK_NAME, deckName)
putExtra(EXTRA_DECK_CODE, deckCode)
}
}
internal const val EXTRA_DECK_CODE = "EXTRA_DECK_CODE"
internal const val EXTRA_DECK_NAME = "EXTRA_DECK_NAME"
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/DeckDetailFragment.kt
================================================
package com.ke.hs_tracker.module.ui.deckbattledetail
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import com.github.mikephil.charting.charts.BarChart
import com.github.mikephil.charting.components.XAxis
import com.github.mikephil.charting.data.BarData
import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.BarEntry
import com.github.mikephil.charting.formatter.ValueFormatter
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import com.hi.dhl.binding.viewbind
import com.ke.hs_tracker.module.databinding.ModuleFragmentDeckBattleDetailDeckDetailBinding
import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
@AndroidEntryPoint
class DeckDetailFragment : Fragment() {
private val binding: ModuleFragmentDeckBattleDetailDeckDetailBinding by viewbind()
private val deckDetailViewModel: DeckDetailViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
launchAndRepeatWithViewLifecycle {
deckDetailViewModel.costList.collect {
fillChart(
binding.costChart, it
)
}
}
launchAndRepeatWithViewLifecycle {
deckDetailViewModel.attackList.collect {
fillChart(
binding.attackChart, it
)
}
}
launchAndRepeatWithViewLifecycle {
deckDetailViewModel.mechanics.collect { list ->
fillChips(
binding.mechanicsChips,
list.map {
it.first.name + it.second
}
)
}
}
launchAndRepeatWithViewLifecycle {
deckDetailViewModel.raceList.collect { list ->
fillChips(binding.raceChips, list.map {
getString(it.first.titleRes!!) + it.second
})
}
}
launchAndRepeatWithViewLifecycle {
deckDetailViewModel.spellSchoolList.collect { list ->
fillChips(
binding.spellSchoolChips,
list.map {
getString(it.first.titleRes) + it.second
}
)
}
}
}
private fun fillChips(chipGroup: ChipGroup, strings: List) {
chipGroup.removeAllViews()
strings.map {
Chip(requireContext()).apply {
text = it
}
}.forEach {
chipGroup.addView(it)
}
}
private fun fillChart(barChart: BarChart, list: List>) {
barChart.description.isEnabled = false
barChart.xAxis.apply {
setDrawGridLines(false)
position = XAxis.XAxisPosition.BOTTOM
labelCount = list.size
textColor = Color.WHITE
}
barChart.axisLeft.apply {
setDrawAxisLine(false)
}
barChart.legend.isEnabled = false
val entryList = list.map {
BarEntry(it.first.toFloat(), it.second.toFloat())
}
val barDataSet = BarDataSet(entryList, "")
// barDataSet.setValueTextColors(listOf(Color.WHITE))
barDataSet.valueTextColor = Color.WHITE
barDataSet.valueTextSize = 14f
barDataSet.valueFormatter = object : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
return value.toInt().toString()
}
}
barChart.data = BarData(barDataSet)
barChart.setFitBars(true)
barChart.invalidate()
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/DeckDetailViewModel.kt
================================================
package com.ke.hs_tracker.module.ui.deckbattledetail
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.ke.hs_tracker.module.domain.ParseDeckCodeUseCase
import com.ke.hs_tracker.module.entity.CardType
import com.ke.hs_tracker.module.entity.Mechanics
import com.ke.hs_tracker.module.entity.Race
import com.ke.hs_tracker.module.entity.SpellSchool
import com.ke.mvvm.base.data.successOr
import com.ke.mvvm.base.ui.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class DeckDetailViewModel
@Inject constructor(
private val parseDeckCodeUseCase: ParseDeckCodeUseCase,
savedStateHandle: SavedStateHandle
) : BaseViewModel() {
private val _costList = MutableStateFlow>>(emptyList())
/**
* 法力曲线
*/
internal val costList: StateFlow>>
get() = _costList
private val _attackList = MutableStateFlow>>(emptyList())
/**
* 攻击力曲线
*/
internal val attackList: StateFlow>>
get() = _attackList
private val _mechanics = MutableStateFlow>>(emptyList())
/**
* 卡牌效果
*/
internal val mechanics: StateFlow>>
get() = _mechanics
private val _raceList = MutableStateFlow>>(emptyList())
//随从种族
internal val raceList: StateFlow>>
get() = _raceList
private val _spellSchoolList = MutableStateFlow>>(emptyList())
internal val spellSchoolList: StateFlow>>
get() = _spellSchoolList
init {
viewModelScope.launch {
val code = savedStateHandle.get(DeckBattleDetailActivity.EXTRA_DECK_CODE)!!
val cardList = parseDeckCodeUseCase(code).successOr(emptyList())
_costList.value = cardList
.flatMap {
it.toCardList()
}
.groupBy {
val cost =
it.cost
if (cost > MAX_VALUE) MAX_VALUE else cost
}
.map {
it.key to it.value.size
}
_attackList.value = cardList
.flatMap {
it.toCardList()
}
.filter {
//必须是随从牌或武器牌
it.type == CardType.Minion || it.type == CardType.Weapon
}.groupBy {
if (
it.attack >= MAX_VALUE
) MAX_VALUE else it.attack
}.map {
it.key to it.value.size
}
_mechanics.value = cardList
.flatMap {
it.toCardList()
}
.flatMap {
it.mechanics
}.groupBy {
it
}.map {
it.key to it.value.size
}
val raceMap = mutableMapOf()
var allRace = 0
cardList.flatMap {
it.toCardList()
}.mapNotNull {
it.race
}.forEach {
if (it.tradition) {
if (it != Race.All) {
var count = raceMap[it] ?: 0
count += 1
raceMap[it] = count
} else if (it == Race.All) {
allRace += 1
}
}
}
_raceList.value = raceMap.map {
it.key to it.value + allRace
}
_spellSchoolList.value = cardList.flatMap {
it.toCardList()
}.mapNotNull {
it.spellSchool
}.groupBy {
it
}.map {
it.key to it.value.size
}
}
}
}
private const val MAX_VALUE = 7
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/DeckFragment.kt
================================================
package com.ke.hs_tracker.module.ui.deckbattledetail
import androidx.fragment.app.activityViewModels
import com.ke.hs_tracker.module.entity.CardBean
import com.ke.hs_tracker.module.ui.main.CardListFragment
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.StateFlow
@AndroidEntryPoint
class DeckFragment : CardListFragment() {
private val deckViewModel: DeckViewModel by activityViewModels()
override val cardList: StateFlow>
get() = deckViewModel.cardList
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/DeckViewModel.kt
================================================
package com.ke.hs_tracker.module.ui.deckbattledetail
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.ke.hs_tracker.module.domain.ParseDeckCodeUseCase
import com.ke.hs_tracker.module.entity.CardBean
import com.ke.mvvm.base.data.successOr
import com.ke.mvvm.base.ui.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class DeckViewModel @Inject constructor(
private val parseDeckCodeUseCase: ParseDeckCodeUseCase,
savedStateHandle: SavedStateHandle
) : BaseViewModel() {
private val _cardList = MutableStateFlow(emptyList())
internal val cardList: StateFlow>
get() = _cardList
init {
viewModelScope.launch {
val code = savedStateHandle.get(DeckBattleDetailActivity.EXTRA_DECK_CODE)!!
_cardList.value = parseDeckCodeUseCase(code).successOr(emptyList())
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/GetGamesByDeckCodeAndNameUseCase.kt
================================================
package com.ke.hs_tracker.module.ui.deckbattledetail
import com.ke.hs_tracker.module.db.Game
import com.ke.hs_tracker.module.db.GameDao
import com.ke.hs_tracker.module.di.IoDispatcher
import com.ke.mvvm.base.domian.UseCase
import kotlinx.coroutines.CoroutineDispatcher
import javax.inject.Inject
class GetGamesByDeckCodeAndNameUseCase @Inject constructor(
@IoDispatcher dispatcher: CoroutineDispatcher,
private val gameDao: GameDao
) : UseCase, List>(dispatcher) {
override suspend fun execute(parameters: Pair): List {
return gameDao.getByDeckCodeAndName(
parameters.first,
parameters.second
)
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/SummaryFragment.kt
================================================
package com.ke.hs_tracker.module.ui.deckbattledetail
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import com.github.mikephil.charting.charts.PieChart
import com.github.mikephil.charting.components.Legend
import com.github.mikephil.charting.data.PieData
import com.github.mikephil.charting.data.PieDataSet
import com.github.mikephil.charting.data.PieEntry
import com.github.mikephil.charting.formatter.PercentFormatter
import com.hi.dhl.binding.viewbind
import com.ke.hs_tracker.module.R
import com.ke.hs_tracker.module.databinding.ModuleFragmentDeckBattleDetailSummaryBinding
import com.ke.hs_tracker.module.ui.chart.PieChartData
import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
/**
* 总览
*/
@AndroidEntryPoint
internal class SummaryFragment : Fragment() {
private val binding: ModuleFragmentDeckBattleDetailSummaryBinding by viewbind()
private val summaryViewModel: SummaryViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
launchAndRepeatWithViewLifecycle {
summaryViewModel.heroBattleItems.collect { list ->
setupPieChart(
list.map {
PieChartData(
getString(it.first.titleRes) + it.second,
it.second,
it.first.color
)
},
binding.chart
)
}
}
}
private fun setupPieChart(list: List, pieChart: PieChart) {
pieChart.apply {
//设置成实心
holeRadius = 0f
transparentCircleRadius = 0f
//禁用描述
description.isEnabled = false
legend.isEnabled = true
legend.textColor = resources.getColor(R.color.module_grey500, null)
legend.orientation = Legend.LegendOrientation.VERTICAL
legend.horizontalAlignment = Legend.LegendHorizontalAlignment.LEFT
legend.verticalAlignment = Legend.LegendVerticalAlignment.TOP
setEntryLabelColor(resources.getColor(R.color.module_grey500, null))
setUsePercentValues(true)
val pieDataSet = PieDataSet(list.map {
return@map PieEntry(
it.value.toFloat(), it.label
)
}, "")
pieDataSet.valueTextSize = 16f
pieDataSet.valueTextColor = resources.getColor(R.color.module_grey500, null)
pieDataSet.colors = list.map {
it.color
}.map {
resources.getColor(it, null)
}
val pieData = PieData(pieDataSet)
// pieData.setDrawValues(true)
pieData.setValueFormatter(PercentFormatter(this))
// pieData.setValueFormatter(object : ValueFormatter() {
// override fun getFormattedValue(value: Float): String {
// return "$value %"
// }
// })
data = pieData
invalidate()
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/deckbattledetail/SummaryViewModel.kt
================================================
package com.ke.hs_tracker.module.ui.deckbattledetail
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.ke.hs_tracker.module.entity.CardClass
import com.ke.mvvm.base.data.successOr
import com.ke.mvvm.base.ui.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class SummaryViewModel @Inject constructor(
private val getGamesByDeckCodeAndNameUseCase: GetGamesByDeckCodeAndNameUseCase,
savedStateHandle: SavedStateHandle
) : BaseViewModel() {
private val _heroBattleItems = MutableStateFlow>>(emptyList())
internal val heroBattleItems: StateFlow>>
get() = _heroBattleItems
init {
viewModelScope.launch {
val code = savedStateHandle.get(DeckBattleDetailActivity.EXTRA_DECK_CODE)!!
val name = savedStateHandle.get(DeckBattleDetailActivity.EXTRA_DECK_NAME)!!
val list = getGamesByDeckCodeAndNameUseCase(code to name).successOr(emptyList())
_heroBattleItems.value = list.groupBy {
it.opponentHero!!
}.map {
it.key to it.value.count()
}
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/diagnose/DiagnoseActivity.kt
================================================
package com.ke.hs_tracker.module.ui.diagnose
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.ke.hs_tracker.module.R
import com.ke.hs_tracker.module.databinding.ModuleActivityDiagnoseBinding
import com.ke.hs_tracker.module.findHSDataFilesDir
/**
* 诊断
*/
class DiagnoseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.module_activity_diagnose)
val binding = ModuleActivityDiagnoseBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.toolbar.setNavigationOnClickListener {
onBackPressed()
}
binding.checkConfigFile.setOnClickListener {
var message = ""
val logsDir = findHSDataFilesDir("log.config")
if (logsDir == null) {
message = "无法找到log.config文件"
} else {
}
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/filter/FilterActivity.kt
================================================
package com.ke.hs_tracker.module.ui.filter
import android.content.Context
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import com.ke.hs_tracker.module.R
import com.ke.hs_tracker.module.databinding.ModuleActivityFilterBinding
import com.ke.hs_tracker.module.databinding.ModuleItemChipFilterBinding
import com.ke.hs_tracker.module.entity.Card
import com.ke.hs_tracker.module.entity.CardClass
import com.ke.hs_tracker.module.entity.CardType
class FilterActivity : AppCompatActivity() {
private lateinit var binding: ModuleActivityFilterBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ModuleActivityFilterBinding.inflate(layoutInflater)
setContentView(binding.root)
val cardList = intent.getParcelableArrayListExtra(EXTRA_CARD_LIST)?.toList()
?: throw RuntimeException("cardList 不能为空")
if (cardList.isEmpty()) {
finish()
}
binding.radioGroup.setOnCheckedChangeListener { _, id ->
binding.chipGroupClass.isVisible = id == R.id.rb_class
binding.chipGroupRarity.isVisible = id == R.id.rb_rarity
binding.chipGroupRace.isVisible = id == R.id.rb_minion
binding.chipGroupSpell.isVisible = id == R.id.rb_spell
binding.chipGroupWeapon.isVisible = id == R.id.rb_weapon
binding.chipGroupHero.isVisible = id == R.id.rb_hero
binding.chipGroupMechanics.isVisible = id == R.id.rb_mechanics
}
binding.toolbar.setNavigationOnClickListener {
onBackPressed()
}
// val cardClasses: List> =
cardList.map {
if (it.classes.isEmpty() && it.cardClass != null) {
listOf(it.cardClass)
} else if (it.classes.isNotEmpty()) {
it.classes
} else {
emptyList()
}
}.filter {
it.isNotEmpty()
}.groupBy {
it
}.map {
it.key to it.value.size
}.forEach {
val b = ModuleItemChipFilterBinding.inflate(layoutInflater)
b.root.setText(
"${getName(it.first, applicationContext)} ${it.second}"
)
b.root.tag = it.first
binding.chipGroupClass
.addView(b.root)
}
if (binding.chipGroupClass.childCount == 0) {
binding.rbClass.isEnabled = false
binding.rbClass.isChecked = false
} else {
binding.rbClass.isChecked = true
}
cardList.mapNotNull {
it.rarity
}.sortedBy {
it.ordinal
}
.groupBy {
it
}.map {
it.key to it.value.size
}.forEach {
val b = ModuleItemChipFilterBinding.inflate(layoutInflater)
b.root.setText(
"${getString(it.first.titleRes)} ${it.second}"
)
binding.chipGroupRarity
.addView(b.root)
}
cardList.mapNotNull {
it.race
}.groupBy {
it
}.map {
it.key to it.value.size
}.forEach {
val b = ModuleItemChipFilterBinding.inflate(layoutInflater)
b.root.setText(
"${getString(it.first.titleRes!!)} ${it.second}"
)
binding.chipGroupRace
.addView(b.root)
}
if (binding.chipGroupRace.childCount == 0) {
//没有随从
binding.rbMinion.isChecked = false
binding.rbMinion.isEnabled = false
}
cardList.mapNotNull {
it.spellSchool
}.groupBy {
it
}.map {
it.key to it.value.size
}.forEach {
val b = ModuleItemChipFilterBinding.inflate(layoutInflater)
b.root.setText(
"${getString(it.first.titleRes!!)} ${it.second}"
)
binding.chipGroupSpell
.addView(b.root)
}
if (binding.chipGroupSpell.childCount == 0) {
binding.rbSpell.isChecked = false
binding.rbSpell.isEnabled = false
}
val weaponCount = cardList.count {
it.type == CardType.Weapon
}
if (weaponCount == 0) {
binding.rbWeapon.isChecked = false
binding.rbWeapon.isEnabled = false
} else {
val b = ModuleItemChipFilterBinding.inflate(layoutInflater)
b.root.setText(
"${getString(R.string.module_weapon)} ${weaponCount}"
)
binding.chipGroupWeapon
.addView(b.root)
}
val heroCount = cardList.count {
it.type == CardType.Hero
}
if (heroCount == 0) {
binding.rbHero.isChecked = false
binding.rbHero.isEnabled = false
} else {
val b = ModuleItemChipFilterBinding.inflate(layoutInflater)
b.root.setText(
"${getString(R.string.module_hero)} ${heroCount}"
)
binding.chipGroupHero
.addView(b.root)
}
cardList.flatMap {
it.mechanics
}.groupBy { it }
.map {
it.key to it.value.size
}.forEach {
val b = ModuleItemChipFilterBinding.inflate(layoutInflater)
b.root.setText(
"${it.first.name} ${it.second}"
)
binding.chipGroupMechanics
.addView(b.root)
}
if (binding.chipGroupMechanics.childCount == 0) {
binding.rbMechanics.isChecked = false
binding.rbMechanics.isEnabled = false
}
// cardList.flatMap {
// it.classes
// }.filter { it.display }
// .groupBy {
// it
// }.map {
// it.key to it.value.size
// }
// cardList.groupBy {
// it.cardClass!!
// }.map {
// it.key to it.value.count()
// }
// CardClass.values()
// .filter {
// it.display
// }
// .forEach {
// val chip =
// ModuleItemChipFilterBinding.inflate(layoutInflater).root
//
// val isBlackTextColor = it.blackText
//
// if (isBlackTextColor) {
// chip.setTextColor(Color.BLACK)
// chip.setCheckedIconResource(R.drawable.module_baseline_done_black_24dp)
// } else {
// chip.setTextColor(Color.WHITE)
// chip.setCheckedIconResource(R.drawable.module_baseline_done_white_24dp)
// }
// chip.setChipBackgroundColorResource(it.color)
// chip.setText(it.titleRes)
// binding.chipGroupClass.addView(chip)
// }
//
// Rarity.values().forEach {
// val chip =
// ModuleItemChipFilterBinding.inflate(layoutInflater).root
// chip.setText(it.title)
// binding.chipGroupRarity.addView(chip)
// }
}
companion object {
const val EXTRA_CARD_LIST = "EXTRA_CARD_LIST"
private fun getName(list: List, context: Context): String {
if (list.isEmpty()) {
throw RuntimeException("list 不能为空")
}
if (list.size == 1) {
return context.getString(list.first().titleRes)
}
val stringBuilder = StringBuilder()
stringBuilder.append("(")
list.map {
context.getString(it.titleRes)
}.forEachIndexed { index, s ->
stringBuilder.append(s)
if (index != list.size - 1) {
stringBuilder.append(" ")
}
}
stringBuilder.append(")")
return stringBuilder.toString()
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/main/CardListFragment.kt
================================================
package com.ke.hs_tracker.module.ui.main
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.DividerItemDecoration
import com.hi.dhl.binding.viewbind
import com.ke.hs_tracker.module.databinding.ModuleFragmentCardListBinding
import com.ke.hs_tracker.module.databinding.ModuleItemCardBinding
import com.ke.hs_tracker.module.entity.CardBean
import com.ke.hs_tracker.module.ui.common.CardAdapter
import com.ke.mvvm.base.ui.BaseViewBindingAdapter
import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect
abstract class CardListFragment : Fragment() {
private val adapter by lazy {
CardAdapter()
}
private val binding: ModuleFragmentCardListBinding by viewbind()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.recyclerView.adapter = adapter
binding.recyclerView.addItemDecoration(
DividerItemDecoration(
requireContext(),
DividerItemDecoration.VERTICAL
)
)
launchAndRepeatWithViewLifecycle {
cardList.collect {
adapter.setList(it)
}
}
}
abstract val cardList: StateFlow>
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/main/DeckCardListFragment.kt
================================================
package com.ke.hs_tracker.module.ui.main
import androidx.fragment.app.activityViewModels
import com.ke.hs_tracker.module.entity.CardBean
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.StateFlow
@AndroidEntryPoint
class DeckCardListFragment : CardListFragment() {
private val mainViewModel: MainViewModel by activityViewModels()
override val cardList: StateFlow>
get() = mainViewModel.deckLeftCardList
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/main/GraveyardFragment.kt
================================================
package com.ke.hs_tracker.module.ui.main
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.DiffUtil
import com.hi.dhl.binding.viewbind
import com.ke.hs_tracker.module.R
import com.ke.hs_tracker.module.bindCard
import com.ke.hs_tracker.module.databinding.ModuleFragmentGraveyardBinding
import com.ke.hs_tracker.module.databinding.ModuleItemCardBinding
import com.ke.hs_tracker.module.entity.GraveyardCard
import com.ke.hs_tracker.module.showCardImageDialog
import com.ke.hs_tracker.module.ui.filter.FilterActivity
import com.ke.mvvm.base.ui.BaseViewBindingAdapter
import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
@AndroidEntryPoint
class GraveyardFragment : Fragment() {
private val binding: ModuleFragmentGraveyardBinding by viewbind()
private val mainViewModel: MainViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.sorted.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, index: Int, p3: Long) {
mainViewModel.setSort(SortBy.values()[index])
}
override fun onNothingSelected(p0: AdapterView<*>?) {
}
}
binding.filter.setOnClickListener {
val cardList = mainViewModel.graveyardCardList.value.map {
it.card
}
// listOf(
// "SCH_270",//始生研习
// "SCH_235",//衰变飞弹
// "EX1_610",//爆炸陷阱
// "TU5_CS2_029",//火球术
// "CFM_852",//玉莲帮密探
// "GIL_598",//苔丝
// "ULD_156t3",//暴龙王
// "GVG_021",//玛尔加尼斯
// "FP1_022",//空灵召唤者
// "CS2_024",//寒冰箭
// "SCH_427",//雷霆绽放
// "Story_09_BlastcrystalPotion",//爆晶药水
// "CORE_CS2_106",//炽炎战斧
// )
// .map {
// mainViewModel.allCard.find { card ->
// card.id == it
// }!!
// }
val intent = Intent(requireContext(), FilterActivity::class.java)
intent.putParcelableArrayListExtra(
FilterActivity.EXTRA_CARD_LIST,
arrayListOf().apply {
addAll(cardList)
})
startActivity(intent)
}
binding.toggle.addOnButtonCheckedListener { _, checkedId, isChecked ->
if (isChecked) {
mainViewModel.toggleShowUserGraveyard(checkedId == R.id.self)
}
}
binding.recyclerView.adapter = adapter
adapter.setDiffCallback(object : DiffUtil.ItemCallback() {
override fun areItemsTheSame(oldItem: GraveyardCard, newItem: GraveyardCard): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(
oldItem: GraveyardCard,
newItem: GraveyardCard
): Boolean {
return oldItem == newItem
}
})
adapter.setOnItemClickListener { _, _, position ->
showCardImageDialog(requireContext(), adapter.getItem(position).card.id)
}
// launchAndRepeatWithViewLifecycle {
// mainViewModel.sortBy.collect { sortBy ->
// sortList(sortBy)
// }
// }
launchAndRepeatWithViewLifecycle {
mainViewModel.graveyardCardList.collect {
adapter.setList(it)
binding.filter.isEnabled = it.isNotEmpty()
//更新数据后重新排序下
// sortList(mainViewModel.sortBy.value)
}
}
launchAndRepeatWithViewLifecycle {
mainViewModel.showUserGraveyardCardList.collect {
binding.toggle.check(if (it) R.id.self else R.id.opponent)
}
}
}
// private fun sortList(sortBy: SortBy) {
//
//
// binding.sorted.setSelection(SortBy.values().indexOf(sortBy))
// val list = adapter.data
// val result = when (sortBy) {
// SortBy.Cost -> list.sortedBy {
// it.card.cost
// }
// SortBy.CostReverse -> {
// list.sortedByDescending {
// it.card.cost
// }
// }
// SortBy.Time -> {
// list.sortedBy {
// it.time
// }
// }
// SortBy.TimeReverse -> {
// list.sortedByDescending { it.time }
// }
// }
//
// adapter.setList(result)
// }
private val adapter = object : BaseViewBindingAdapter() {
override fun bindItem(
item: GraveyardCard,
viewBinding: ModuleItemCardBinding,
viewType: Int,
position: Int
) {
viewBinding.bindCard(item.card)
}
override fun createViewBinding(
inflater: LayoutInflater,
parent: ViewGroup,
viewType: Int
): ModuleItemCardBinding {
return ModuleItemCardBinding.inflate(inflater, parent, false)
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/main/MainActivity.kt
================================================
package com.ke.hs_tracker.module.ui.main
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayoutMediator
import com.ke.hs_tracker.module.R
import com.ke.hs_tracker.module.databinding.ModuleActivityMainBinding
import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
// private val adapter =
// object : BaseViewBindingAdapter() {
// override fun bindItem(
// item: CardBean,
// viewBinding: ModuleItemCardBinding,
// viewType: Int,
// position: Int
// ) {
// viewBinding.name.text = item.cardEntity.name + " " + item.count
// viewBinding.cost.text = item.cardEntity.cost.toString()
// }
//
// override fun createViewBinding(
// inflater: LayoutInflater,
// parent: ViewGroup,
// viewType: Int
// ): ModuleItemCardBinding {
// return ModuleItemCardBinding.inflate(layoutInflater, parent, false)
// }
//
// }
private val mainViewModel: MainViewModel by viewModels()
private lateinit var binding: ModuleActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ModuleActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.toolbar.setNavigationOnClickListener {
onBackPressed()
}
val titleList = listOf(
R.string.module_deck,
R.string.module_graveyard,
R.string.module_opponent_hand_card
// R.string.module_opponent_graveyard
)
val fragmentList = listOf(
DeckCardListFragment(),
GraveyardFragment(),
OpponentHandCardsFragment()
// UserGraveyardFragment(),
// OpponentGraveyardFragment()
)
val adapter = object : FragmentStateAdapter(this) {
override fun getItemCount(): Int {
return fragmentList.size
}
override fun createFragment(position: Int): Fragment {
return fragmentList[position]
}
}
binding.viewPager.adapter = adapter
binding.viewPager.offscreenPageLimit = fragmentList.size
TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, index ->
tab.setText(titleList[index])
}.attach()
// val lines = assets
// .open("Power.log").reader()
// .readLines()
// .toMutableList().apply {
// removeFirstOrNull()
// }.toList()
// val mLines = mutableListOf()
// mLines.addAll(lines)
//
// val logsDir = getExternalFilesDir("Logs")!!
// if (logsDir.exists()) {
// val powerFile = File(logsDir, "Power.log")
// val writer = FileOutputStream(powerFile, true).writer()
//
// binding.next.setOnClickListener {
//
// repeat(100) {
// val line = mLines.removeFirstOrNull()
// if (line != null) {
// writer.append(line)
// writer.appendLine()
// writer.flush()
// }
// }
//
// }
// binding.clear.setOnClickListener {
// mLines.clear()
// mLines.addAll(lines)
// }
// }
// binding.recyclerView.adapter = adapter
// launchAndRepeatWithViewLifecycle {
// mainViewModel.deckLeftCardList.collect {
// adapter.setList(it)
// }
// }
launchAndRepeatWithViewLifecycle {
mainViewModel.title.collect {
binding.toolbar.title = it
}
}
}
}
internal enum class SortBy {
Cost,
CostReverse,
Time,
TimeReverse
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/main/MainViewModel.kt
================================================
package com.ke.hs_tracker.module.ui.main
import android.annotation.SuppressLint
import android.content.Context
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.ke.hs_tracker.module.db.*
import com.ke.hs_tracker.module.di.IoDispatcher
import com.ke.hs_tracker.module.domain.GetAllCardUseCase
import com.ke.hs_tracker.module.domain.GetRealLogDirUseCase
import com.ke.hs_tracker.module.domain.ParseDeckCodeUseCase
import com.ke.hs_tracker.module.domain.SaveLogFileUseCase
import com.ke.hs_tracker.module.entity.*
import com.ke.hs_tracker.module.log
import com.ke.hs_tracker.module.parser.DeckFileObserver
import com.ke.hs_tracker.module.parser.PowerFileObserver
import com.ke.hs_tracker.module.parser.PowerParser
import com.ke.mvvm.base.data.successOr
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.io.InputStream
import java.util.*
import javax.inject.Inject
@SuppressLint("StaticFieldLeak")
@HiltViewModel
class MainViewModel @Inject constructor(
@IoDispatcher private val dispatcher: CoroutineDispatcher,
@ApplicationContext private val context: Context,
private val parseDeckCodeUseCase: ParseDeckCodeUseCase,
private val getAllCardUseCase: GetAllCardUseCase,
private val getLogDirUseCase: GetRealLogDirUseCase,
private val gameDao: GameDao,
private val zonePositionChangedEventDao: ZonePositionChangedEventDao,
private val saveLogFileUseCase: SaveLogFileUseCase,
private val powerParser: PowerParser
) : ViewModel() {
private var user: PowerTag.GameState.PlayerMapping? = null
private var opponent: PowerTag.GameState.PlayerMapping? = null
private var currentTurn = 0
// private var logsDir: DocumentFile? = null
private suspend fun getLogsDir(): DocumentFile? {
return getLogDirUseCase(Unit).successOr(null)
}
private val _title = MutableStateFlow("标题")
val title: StateFlow
get() = _title
private val _deckLeftCardList = MutableStateFlow>(emptyList())
val deckLeftCardList: StateFlow>
get() = _deckLeftCardList
private val _graveyardCardList = MutableStateFlow>(emptyList())
val graveyardCardList: StateFlow>
get() = _graveyardCardList
/**
* 对手手牌
*/
private val currentOpponentHandCards = mutableListOf()
private val _opponentHandCards =
MutableStateFlow>>("" to emptyList())
/**
* 对手手牌
*/
val opponentHandCards: StateFlow>>
get() = _opponentHandCards
private val userGraveyardCardList = mutableListOf()
private val opponentGraveyardCardList = mutableListOf()
private val _showUserGraveyardCardList = MutableStateFlow(true)
/**
* 是否显示用户墓地
*/
internal val showUserGraveyardCardList: StateFlow
get() = _showUserGraveyardCardList
internal fun toggleShowUserGraveyard(showUser: Boolean) {
_showUserGraveyardCardList.value = showUser
// _graveyardCardList.value =
// if (showUser) userGraveyardCardList else opponentGraveyardCardList
updateGraveyardCardList()
}
private val _sortBy = MutableStateFlow(SortBy.Cost)
// internal val sortBy: StateFlow
// get() = _sortBy
/**
* 设置排序方式
*/
internal fun setSort(sortBy: SortBy) {
_sortBy.value = sortBy
updateGraveyardCardList()
}
private fun updateGraveyardCardList() {
val source =
if (_showUserGraveyardCardList.value) userGraveyardCardList else opponentGraveyardCardList
val result = when (_sortBy.value) {
SortBy.Cost -> source.sortedBy { it.card.cost }
SortBy.CostReverse -> source.sortedByDescending { it.card.cost }
SortBy.Time -> source.sortedBy { it.time }
SortBy.TimeReverse -> source.sortedByDescending { it.time }
}
_graveyardCardList.value = result
}
private val powerFileObserver: PowerFileObserver by lazy {
PowerFileObserver(1500) {
getFileStream("Power.log")
}
}
private val deckFileObserver: DeckFileObserver by lazy {
DeckFileObserver {
getFileStream("Decks.log")
}
}
private var currentDeck: CurrentDeck? = null
private var game: Game = Game()
// private val powerParser = PowerParserImpl()
private var allCard: List = emptyList()
private var deckCardList: List = emptyList()
private val zoneChangedEventList = mutableListOf()
private val entityIdAndCardIdMap = mutableMapOf()
init {
viewModelScope.launch {
clearPowerFile()
allCard = getAllCardUseCase(Unit).successOr(emptyList())
}
viewModelScope.launch {
_deckLeftCardList.collect {
}
}
// viewModelScope.launch {
// powerParser.powerTagFlow.collect {
// handlePowerTag(it)
// }
// }
powerParser.powerTagListener = {
handlePowerTag(it)
}
viewModelScope.launch {
delay(1000)
deckFileObserver
.start()
.flowOn(Dispatchers.IO)
.map {
it to parseDeckCodeUseCase(it.code).successOr(emptyList())
}
.collect {
_deckLeftCardList.value = it.second
_title.value = it.first.name
deckCardList = it.second
currentDeck = it.first
}
}
viewModelScope.launch {
powerFileObserver.start()
.flowOn(dispatcher)
.collect {
it.forEach { line ->
powerParser.parse(line)
}
}
}
}
private fun handlePowerTag(tag: PowerTag) {
when (tag) {
is PowerTag.GameState.BuildNumber -> {
game = Game(buildNumber = tag.number)
game.userDeckName = currentDeck?.name ?: ""
game.userDeckCode = currentDeck?.code ?: ""
}
is PowerTag.GameState.FormatType -> {
game.formatType = tag.type.toFormatType
}
is PowerTag.GameState.GameType -> {
game.gameType = tag.type.toGameType
}
is PowerTag.GameState.PlayerMapping -> {
if (tag.isUser) {
game.userName = tag.name
user = tag
// game.isUserFirst = tag.first
} else {
game.opponentName = tag.name
opponent = tag
}
}
is PowerTag.GameState.ScenarioID -> {
game.scenarioID = tag.id.toInt()
}
is PowerTag.PowerTaskList.Block -> {
tag.list.forEach {
handlePowerTag(it)
}
}
is PowerTag.PowerTaskList.CreateGame -> {
//初始化卡牌
onGameStarted()
}
is PowerTag.PowerTaskList.FullEntity -> {
// handleFullEntity(tag)
handleFullEntity(tag)
//FULL_ENTITY - Updating [entityName=萨尔 id=74 zone=PLAY zonePos=0 cardId=HERO_02 player=2] CardID=HERO_02
// tag=CONTROLLER value=2
// tag=CARDTYPE value=HERO
// tag=HEALTH value=30
// tag=ZONE value=PLAY
// tag=ENTITY_ID value=74
// tag=FACTION value=NEUTRAL
// tag=RARITY value=FREE
// tag=HERO_POWER value=687
// tag=SPAWN_TIME_COUNT value=1
tag.cardId?.apply {
entityIdAndCardIdMap[tag.entity.id] = this
}
}
is PowerTag.PowerTaskList.ShowEntity -> {
handleShowEntity(tag)
entityIdAndCardIdMap[tag.entity.id] = tag.cardId
}
is PowerTag.PowerTaskList.TagChange -> {
handleTagChange(tag)
}
}
// tag.toString().log()
(tag as? ZoneUpdatable)?.apply {
isUpdateZone(user?.id)?.let {
val cardId = it.cardId ?: entityIdAndCardIdMap[it.entityId]
// val event = ZonePositionChangedEvent(
// entityId = it.first.id,
// cardId = it.first.cardId,
// isUser = it.first.player == user?.id,
// currentZone = it.first.zone,
// newZone = it.second,
// currentPosition = it.first.zonePosition,
// newPosition = it.first.zonePosition
// )
zoneChangedEventList.add(it)
if (it.currentZone == Zone.Deck && it.newZone != Zone.Deck && it.isUser) {
//从牌库中抽取一张卡
//SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=48 zone=DECK zonePos=0 cardId= player=2]
// tag=CONTROLLER value=2
// tag=CARDTYPE value=SPELL
// tag=TAG_LAST_KNOWN_COST_IN_HAND value=1
// tag=COST value=1
// tag=PREMIUM value=1
// tag=ZONE value=HAND
// tag=ENTITY_ID value=48
// tag=ELITE value=1
// tag=CLASS value=SHAMAN
// tag=RARITY value=LEGENDARY
// tag=478 value=2
// tag=QUEST_PROGRESS_TOTAL value=3
// tag=676 value=1
// tag=839 value=1
// tag=1043 value=1
// tag=1068 value=0
// tag=QUEST_REWARD_DATABASE_ID value=64323
// tag=SPAWN_TIME_COUNT value=1
//或者 探底
//TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=1068 value=3
//TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=1068 value=0
//TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=1037 value=1
//TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=ZONE value=HAND
//TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] tag=ZONE_POSITION value=6
//探底抽上来的卡是没有cardId的
removeCardFromDeck(cardId)
} else if (it.newZone == Zone.Deck && it.currentZone != Zone.Deck && it.isUser) {
//当心探底
//SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=28 zone=DECK zonePos=0 cardId= player=1] CardID=DED_002
// tag=CONTROLLER value=1
// tag=CARDTYPE value=SPELL
// tag=TAG_LAST_KNOWN_COST_IN_HAND value=2
// tag=COST value=2
// tag=ZONE value=DECK
// tag=ENTITY_ID value=28
// tag=RARITY value=RARE
// tag=DISCOVER value=1
// tag=478 value=1
// tag=1043 value=1
// tag=1068 value=0
// tag=USE_DISCOVER_VISUALS value=1
// tag=SPAWN_TIME_COUNT value=1
// tag=SPELL_SCHOOL value=1
// tag=1711 value=1
// tag=MINI_SET value=1
//有牌插入到牌库
//TAG_CHANGE Entity=[entityName=冷风 id=70 zone=HAND zonePos=2 cardId=AV_266 player=2] tag=ZONE value=DECK
//会出现id为空的情况
//FULL_ENTITY - Updating [entityName=UNKNOWN ENTITY [cardType=INVALID] id=17 zone=DECK zonePos=0 cardId= player=1] CardID=
// tag=ZONE value=DECK
// tag=CONTROLLER value=1
// tag=ENTITY_ID value=17
insertCardToDeck(cardId)
} else if (it.currentZone == Zone.Play && it.newZone == Zone.Graveyard) {
onGraveyardCardsChanged(cardId, it.isUser)
} else if (it.newZone == Zone.Hand || it.currentZone == Zone.Hand) {
//手牌
if (!it.isUser) {
handleOpponentHandChanged(it)
}
}
}
}
}
private var lastZonePositionChangedEvent: ZonePositionChangedEvent? = null
/**
* 对手手牌发生变化
*/
private fun handleOpponentHandChanged(event: ZonePositionChangedEvent) {
if (lastZonePositionChangedEvent?.entityId == event.entityId && event.currentZone == event.newZone) {
return
}
if (event.newZone == Zone.Hand) {
//有卡牌入手 可能从牌库抽到手里 也可能改变了位置
val target = currentOpponentHandCards.firstOrNull {
it.entityId == event.entityId
}
if (target == null) {
//插入一张卡牌到list中
currentOpponentHandCards.add(
OpponentHandCard(
currentTurn, event.entityId, event.newPosition
)
)
} else {
target.position = event.newPosition
}
} else if (event.currentZone == Zone.Hand) {
//手牌出去了一张
val target = currentOpponentHandCards.firstOrNull {
it.entityId == event.entityId
}
if (target == null) {
//
} else {
// target.position = event.newPosition
currentOpponentHandCards.remove(target)
}
}
lastZonePositionChangedEvent = event
_opponentHandCards.value = UUID.randomUUID().toString() to currentOpponentHandCards
"对手手牌发生了变化 $event ,当前回合是 $currentTurn , 对手手牌 $currentOpponentHandCards".log()
}
private fun handleFullEntity(fullEntity: PowerTag.PowerTaskList.FullEntity) {
fullEntity.isUpdateHero()?.apply {
val cardClass = allCard.find {
it.id == second
}?.cardClass ?: return
if (first == user?.id) {
game.userHero = cardClass
} else if (first == opponent?.id) {
game.opponentHero = cardClass
}
}
}
private fun handleTagChange(tagChange: PowerTag.PowerTaskList.TagChange) {
tagChange.isTurnChanged()?.apply {
currentTurn = this
}
if (tagChange.entity.player == user?.id && tagChange.entity.zone == Zone.Hand && tagChange.tag == "ZONE" && tagChange.value == "DECK") {
//TAG_CHANGE Entity=[entityName=冷风 id=15 zone=HAND zonePos=3 cardId=AV_266 player=1] tag=ZONE value=DECK
// insertCardToDeck(tagChange.entity.cardId!!)
} else if (tagChange.tag == "ZONE" && tagChange.value == "GRAVEYARD" && tagChange.entity.zone == Zone.Play) {
//TAG_CHANGE Entity=[entityName=破霰元素 id=62 zone=PLAY zonePos=1 cardId=AV_260 player=2] tag=ZONE value=GRAVEYARD
//随从死亡后进入墓地
//TAG_CHANGE Entity=[entityName=始生研习 id=63 zone=PLAY zonePos=0 cardId=SCH_270 player=2] tag=ZONE value=GRAVEYARD
//打出法术
// onGraveyardCardsChanged(tagChange.entity)
} else if (tagChange.isGameComplete) {
"游戏结束了".log()
onGameOver()
}
val pair = tagChange.isPlayerWonOrLost
if (pair != null) {
"有玩家胜利或失败了 $pair $game".log()
if (pair.first == game.userName) {
game.isUserWin = pair.second
} else {
game.opponentName = pair.first
}
}
if (tagChange.tag == "FIRST_PLAYER" && tagChange.value == "1") {
game.isUserFirst = tagChange.entity.entityName == game.userName
}
}
/**
* 游戏开始
*/
private fun onGameStarted() {
"游戏开始了".log()
_deckLeftCardList.value = deckCardList
game.startTime = System.currentTimeMillis()
currentTurn = 0
currentOpponentHandCards.clear()
_opponentHandCards.value = UUID.randomUUID().toString() to currentOpponentHandCards
}
private fun onGameOver() {
viewModelScope.launch {
//保存游戏
game.endTime = System.currentTimeMillis()
gameDao.insert(game)
zonePositionChangedEventDao.insertAll(zoneChangedEventList.map {
it.cardId = entityIdAndCardIdMap[it.entityId]
it.gameId = game.id
it.cardName = allCard.find { card: Card ->
it.cardId == card.id
}?.name
it
})
entityIdAndCardIdMap.clear()
zoneChangedEventList.clear()
getFileStream("Power.log")?.apply {
saveLogFileUseCase(game.id to this)
}
delay(1000)
clearPowerFile()
}
_deckLeftCardList.value = deckCardList
// _userGraveyardCardList.value = emptyList()
// _opponentGraveyardCardList.value = emptyList()
userGraveyardCardList.clear()
opponentGraveyardCardList.clear()
_graveyardCardList.value = emptyList()
currentTurn = 0
_opponentHandCards.value = UUID.randomUUID().toString() to emptyList()
}
private fun handleShowEntity(showEntity: PowerTag.PowerTaskList.ShowEntity) {
if (showEntity.entity.player == user?.id && showEntity.entity.zone == Zone.Deck && showEntity.payloads["ZONE"].equals(
"hand",
true
)
) {
//起始手牌
//SHOW_ENTITY - Updating Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=15 zone=DECK zonePos=0 cardId= player=1] CardID=AV_266
// tag=CONTROLLER value=1
// tag=CARDTYPE value=SPELL
// tag=TAG_LAST_KNOWN_COST_IN_HAND value=1
// tag=COST value=1
// tag=PREMIUM value=1
// tag=ZONE value=HAND
// tag=ENTITY_ID value=15
// tag=RARITY value=COMMON
// tag=478 value=1
// tag=1043 value=1
// tag=1068 value=0
// tag=SPAWN_TIME_COUNT value=1
// tag=SPELL_SCHOOL value=3
// removeCardFromDeck(showEntity.cardId)
} else if (showEntity.entity.player == user?.id && showEntity.entity.zone == Zone.Deck && showEntity.payloads["ZONE"].equals(
"GRAVEYARD",
true
)
) {
//爆牌
// ShowEntity(entity=Entity(entityName=UNKNOWN ENTITY, gameCardType=Invalid, id=54, zone=Deck, zonePosition=0, cardId=null, player=2), cardId=OG_176, payloads={CONTROLLER=2, CARDTYPE=SPELL, TAG_LAST_KNOWN_COST_IN_HAND=3, COST=3, ZONE=GRAVEYARD, ENTITY_ID=54, RARITY=COMMON, 478=2, 1037=2, 1043=1, 1068=0, SPAWN_TIME_COUNT=1, SPELL_SCHOOL=6})
// removeCardFromDeck(showEntity.cardId)
}
}
private fun removeCardFromDeck(cardId: String?) {
onDeckCardsChanged(cardId, false)
}
private fun insertCardToDeck(cardId: String?) {
onDeckCardsChanged(cardId, true)
}
private fun onGraveyardCardsChanged(cardId: String?, isUser: Boolean) {
//TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=106 zone=PLAY zonePos=0 cardId= player=1] tag=ZONE value=GRAVEYARD
//TAG_CHANGE Entity=[entityName=UNKNOWN ENTITY [cardType=INVALID] id=5 zone=SECRET zonePos=0 cardId= player=1] tag=COST value=2
//如果对面打出一张奥秘拍 会直接进入墓地
// ?: throw RuntimeException("没有id $entity")
if (cardId == null) {
return
}
val card = findCardById(cardId)
if (card.type == CardType.Enchantment) {
// "衍生牌 $card 不能放到墓地去".log()
return
}
// "插入一张牌到墓地 $card $entity".log()
//TAG_CHANGE Entity=[entityName=破霰元素 id=62 zone=PLAY zonePos=1 cardId=AV_260 player=2] tag=ZONE value=GRAVEYARD
if (isUser) {
// viewModelScope.launch {
// updateCardList(
// cardEntity, _userGraveyardCardList, true
// )
// }
userGraveyardCardList.add(GraveyardCard(card))
// _graveyardCardList.value = userGraveyardCardList
} else {
// viewModelScope.launch {
// updateCardList(
// cardEntity, _opponentGraveyardCardList, true
// )
// }
opponentGraveyardCardList.add(GraveyardCard(card))
// _graveyardCardList.value = opponentGraveyardCardList
}
if (isUser && _showUserGraveyardCardList.value) {
//是当前用户的卡插入到墓地 并且显示是当前用户的墓地
// _graveyardCardList.value = userGraveyardCardList
updateGraveyardCardList()
} else if (!isUser && _showUserGraveyardCardList.value) {
// _graveyardCardList.value = opponentGraveyardCardList
updateGraveyardCardList()
}
}
private fun onDeckCardsChanged(cardId: String?, insert: Boolean) {
if (cardId == null) {
return
}
val card = findCardById(cardId)
updateCardList(card, _deckLeftCardList, insert)
}
private fun findCardById(cardId: String) =
allCard.find { it.id == cardId } ?: throw RuntimeException("id为 $cardId 没有这张牌")
private suspend fun clearPowerFile() {
withContext(dispatcher) {
val documentFile = getLogsDir()?.findFile(powerFileName)
documentFile?.apply {
context.contentResolver.openOutputStream(uri, "wt")?.use {
it.write("".encodeToByteArray())
it.flush()
it.close()
}
}
powerFileObserver.reset()
deckFileObserver.reset()
}
}
private suspend fun getFileStream(fileName: String): InputStream? = withContext(dispatcher) {
val documentFile = getLogsDir()?.findFile(fileName)
if (documentFile == null) {
"无法访问 $fileName 文件".log()
return@withContext null
}
context.contentResolver.openInputStream(documentFile.uri)
}
companion object {
fun updateCardList(
card: Card,
mutableStateFlow: MutableStateFlow>,
insert: Boolean,
) {
if (card.type == CardType.Enchantment) {
return
}
val list = mutableStateFlow.value.toMutableList()
val bean = list.find {
it.card.id == card.id
}
if (bean == null) {
list.add(CardBean(card, 1))
} else {
val newCount = if (!insert) bean.count - 1 else bean.count + 1
list[list.indexOf(bean)] = bean.updateCount(newCount)
}
if (bean?.count == 3) {
"插入了3张进去? ".log()
}
mutableStateFlow.value = list.sortedBy {
it.card.cost
}.filter {
it.count != 0
}
}
}
}
const val powerFileName = "Power.log"
data class OpponentHandCard(
/**
* 回合数
*/
val turn: Int,
/**
* 实体id
*/
val entityId: Int,
/**
* 位置
*/
var position: Int,
/**
* 时间
*/
val time: Long = System.currentTimeMillis()
)
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/main/OpponentGraveyardFragment.kt
================================================
package com.ke.hs_tracker.module.ui.main
import androidx.fragment.app.activityViewModels
import com.ke.hs_tracker.module.entity.CardBean
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.StateFlow
//@AndroidEntryPoint
//class OpponentGraveyardFragment : CardListFragment() {
//
// private val mainViewModel: MainViewModel by activityViewModels()
//
// override val cardList: StateFlow>
// get() = mainViewModel.opponentGraveyardCardList
//}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/main/OpponentHandCardsFragment.kt
================================================
package com.ke.hs_tracker.module.ui.main
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import com.hi.dhl.binding.viewbind
import com.ke.hs_tracker.module.databinding.ModuleFragmentOpponentHandCardsBinding
import com.ke.hs_tracker.module.databinding.ModuleItemOpponentHandCardBinding
import com.ke.mvvm.base.ui.BaseViewBindingAdapter
import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
@AndroidEntryPoint
class OpponentHandCardsFragment : Fragment() {
private val mainViewModel: MainViewModel by activityViewModels()
private val binding: ModuleFragmentOpponentHandCardsBinding by viewbind()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = binding.root
private val adapter =
object : BaseViewBindingAdapter() {
override fun bindItem(
item: OpponentHandCard,
viewBinding: ModuleItemOpponentHandCardBinding,
viewType: Int,
position: Int
) {
viewBinding.text.text = (item.turn).toString()
}
override fun createViewBinding(
inflater: LayoutInflater,
parent: ViewGroup,
viewType: Int
): ModuleItemOpponentHandCardBinding {
return ModuleItemOpponentHandCardBinding.inflate(inflater, parent, false)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.recyclerView.adapter = adapter
launchAndRepeatWithViewLifecycle {
mainViewModel.opponentHandCards.collect {
adapter.setList(
it.second
.sortedBy { card ->
card.position
}
.sortedBy { card ->
card.time
}
)
}
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/main/UserGraveyardFragment.kt
================================================
package com.ke.hs_tracker.module.ui.main
import androidx.fragment.app.activityViewModels
import com.ke.hs_tracker.module.entity.CardBean
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.StateFlow
//@AndroidEntryPoint
//class UserGraveyardFragment : CardListFragment() {
//
// private val mainViewModel: MainViewModel by activityViewModels()
//
// override val cardList: StateFlow>
// get() = mainViewModel.userGraveyardCardList
//}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/migrate/MigrateDataConvert.kt
================================================
package com.ke.hs_tracker.module.ui.migrate
import androidx.annotation.WorkerThread
import com.ke.hs_tracker.module.db.*
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import javax.inject.Inject
internal class MigrateDataConvert @Inject constructor(
private val gameDao: GameDao,
private val zonePositionChangedEventDao: ZonePositionChangedEventDao,
private val moshi: Moshi
) {
private val adapter = moshi.adapter(MigrateData::class.java)
@WorkerThread
suspend fun getJsonString(): String {
val migrateData =
MigrateData(DATABASE_VERSION, gameDao.getAll(), zonePositionChangedEventDao.getAll())
return adapter.toJson(migrateData)
}
suspend fun save(jsonString: String): Boolean {
val data = adapter.fromJson(jsonString) ?: return false
saveData(data)
return true
}
@WorkerThread
private suspend fun saveData(migrateData: MigrateData) {
gameDao.insert(migrateData.games)
zonePositionChangedEventDao.insertAll(migrateData.zonePositionChangedEvents)
}
}
@JsonClass(generateAdapter = true)
internal data class MigrateData(
val version: Int,
val games: List,
val zonePositionChangedEvents: List
)
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/migrate/MigrateMainActivity.kt
================================================
package com.ke.hs_tracker.module.ui.migrate
import android.content.Context
import android.content.Intent
import android.net.wifi.WifiManager
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.ke.hs_tracker.module.databinding.ModuleActivityMigrateMainBinding
class MigrateMainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ModuleActivityMigrateMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.toolbar.setNavigationOnClickListener {
onBackPressed()
}
binding.toThisPhone.setOnClickListener {
startActivity(Intent(this, SocketServerActivity::class.java))
finish()
}
binding.toAnotherPhone.setOnClickListener {
startActivity(Intent(this, SocketClientActivity::class.java))
finish()
}
}
}
internal fun getLocalIPAddress(context: Context): String {
val manager =
context.applicationContext.getSystemService(AppCompatActivity.WIFI_SERVICE) as WifiManager
return int2ip(manager.connectionInfo.ipAddress)
}
internal fun int2ip(ipInt: Int): String {
val sb = StringBuilder()
sb.append(ipInt and 0xFF).append(".")
sb.append(ipInt shr 8 and 0xFF).append(".")
sb.append(ipInt shr 16 and 0xFF).append(".")
sb.append(ipInt shr 24 and 0xFF)
return sb.toString()
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/migrate/SocketClientActivity.kt
================================================
package com.ke.hs_tracker.module.ui.migrate
import android.app.ProgressDialog
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.ke.hs_tracker.module.databinding.ModuleActivitySocketClientBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.net.Socket
import javax.inject.Inject
@AndroidEntryPoint
class SocketClientActivity : AppCompatActivity() {
@Inject
internal lateinit var migrateDataConvert: MigrateDataConvert
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.module_activity_socket_client)
val binding: ModuleActivitySocketClientBinding =
ModuleActivitySocketClientBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.toolbar.setNavigationOnClickListener {
onBackPressed()
}
// binding.ipAddress.setText(getLocalIPAddress(applicationContext))
binding.start.setOnClickListener {
val progressDialog = ProgressDialog(this)
progressDialog.show()
lifecycleScope.launch(Dispatchers.IO) {
val host = binding.ipAddress.text?.toString() ?: return@launch
val port = binding.ipPort.text?.toString()?.toInt() ?: return@launch
try {
val socket = Socket(host, port)
val text = migrateDataConvert.getJsonString()
socket.getOutputStream().apply {
write(
text.toByteArray()
)
flush()
close()
}
runOnUiThread {
progressDialog.dismiss()
finish()
}
} catch (e: Exception) {
runOnUiThread {
progressDialog.dismiss()
}
e.printStackTrace()
}
}
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/migrate/SocketServerActivity.kt
================================================
package com.ke.hs_tracker.module.ui.migrate
import android.app.ProgressDialog
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.ke.hs_tracker.module.databinding.ModuleActivitySocketServerBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.ServerSocket
import javax.inject.Inject
@AndroidEntryPoint
class SocketServerActivity : AppCompatActivity() {
@Inject
internal lateinit var migrateDataConvert: MigrateDataConvert
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.module_activity_socket_server)
val binding = ModuleActivitySocketServerBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.toolbar.setNavigationOnClickListener {
onBackPressed()
}
binding.ipAddress.setText(getLocalIPAddress(applicationContext))
binding.start.setOnClickListener {
val port = binding.ipPort.text.toString().toIntOrNull() ?: return@setOnClickListener
it.isEnabled = false
binding.loadingProgress.isVisible = true
start(port)
}
}
private fun start(port: Int) {
val progressDialog = ProgressDialog(this)
progressDialog.show()
lifecycleScope.launch {
withContext(Dispatchers.IO) {
val serverSocket = ServerSocket(port)
val socket = serverSocket.accept()
val text = socket.getInputStream().bufferedReader().readLine() ?: ""
// Logger.d("收到了数据 $text")
migrateDataConvert.save(text)
runOnUiThread {
finish()
}
}
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/permissions/PermissionsActivity.kt
================================================
package com.ke.hs_tracker.module.ui.permissions
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.DocumentsContract
import android.view.View
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile
import com.ke.hs_tracker.module.HS_DATA_FILE_DIR
import com.ke.hs_tracker.module.R
import com.ke.hs_tracker.module.canReadDataDir
import com.ke.hs_tracker.module.databinding.ModuleActivityPermissionsBinding
import com.ke.hs_tracker.module.hasAllPermissions
import com.ke.hs_tracker.module.ui.writeconfig.WriteConfigActivity
import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
@AndroidEntryPoint
class PermissionsActivity : AppCompatActivity() {
private lateinit var binding: ModuleActivityPermissionsBinding
private val viewModel: PermissionsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ModuleActivityPermissionsBinding.inflate(layoutInflater)
setContentView(binding.root)
setTitle(R.string.module_set_permissions)
initView()
launchAndRepeatWithViewLifecycle {
viewModel.navigationActions.collect {
// val intent = when (it) {
// PermissionsNavigationAction.NavigateToMain -> {
// Intent(this@PermissionsActivity, MainActivity::class.java)
// }
// PermissionsNavigationAction.NavigateToSync -> {
// Intent(this@PermissionsActivity, SyncCardDataActivity::class.java)
// }
// }
startActivity(Intent(this@PermissionsActivity, WriteConfigActivity::class.java))
finish()
}
}
}
private fun initView() {
// val requestPermissionLauncher =
// registerForActivityResult(ActivityResultContracts.RequestPermission()) {
//
// }
val requestAccessDataDirLauncher = registerForActivityResult(RequestAccessDataDir()) {
if (it != null) {
contentResolver.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
)
}
}
// binding.step3.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// val launcher = registerForActivityResult(RequestManageAllFilesAccessPermission()) {
//
// }
// binding.step3.setOnClickListener {
// launcher.launch(Unit)
// }
// }
val onClickListener = View.OnClickListener {
when (it) {
// binding.step1 -> {
// requestPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
// }
binding.step2 -> {
requestAccessDataDirLauncher.launch(Unit)
}
binding.next -> {
viewModel.next()
}
}
}
// binding.step1.setOnClickListener(onClickListener)
binding.step2.setOnClickListener(onClickListener)
binding.next.setOnClickListener(onClickListener)
}
override fun onResume() {
super.onResume()
binding.next.isEnabled = hasAllPermissions
// val hasPermission =
// checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
// if (hasPermission) {
// binding.step1.isEnabled = false
// binding.step1.setCompoundDrawablesRelativeWithIntrinsicBounds(
// 0,
// 0,
// R.drawable.module_baseline_done_green_500_24dp,
// 0
// )
// } else {
// binding.step1.isEnabled = true
// binding.step1.setCompoundDrawablesRelativeWithIntrinsicBounds(
// 0,
// 0,
// R.drawable.module_baseline_keyboard_arrow_right_grey_500_24dp,
// 0
// )
// }
if (canReadDataDir) {
binding.step2.isEnabled = false
binding.step2.setCompoundDrawablesRelativeWithIntrinsicBounds(
0,
0,
R.drawable.module_baseline_done_green_500_24dp,
0
)
} else {
binding.step2.isEnabled = true
binding.step2.setCompoundDrawablesRelativeWithIntrinsicBounds(
0,
0,
R.drawable.module_baseline_keyboard_arrow_right_grey_500_24dp,
0
)
}
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// if (isExternalStorageManager()) {
// binding.step3.isEnabled = false
// binding.step3.setCompoundDrawablesRelativeWithIntrinsicBounds(
// 0,
// 0,
// R.drawable.module_baseline_done_green_500_24dp,
// 0
// )
// } else {
// binding.step3.isEnabled = true
// binding.step3.setCompoundDrawablesRelativeWithIntrinsicBounds(
// 0,
// 0,
// R.drawable.module_baseline_keyboard_arrow_right_grey_500_24dp,
// 0
// )
// }
// }
}
}
class RequestAccessDataDir : ActivityResultContract() {
override fun createIntent(context: Context, input: Unit): Intent {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.flags = (Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
val documentFile = DocumentFile.fromTreeUri(context.applicationContext, HS_DATA_FILE_DIR)!!
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, documentFile.uri)
return intent
}
override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
return intent?.data
}
}
//@RequiresApi(Build.VERSION_CODES.R)
//class RequestManageAllFilesAccessPermission : ActivityResultContract() {
// override fun createIntent(context: Context, input: Unit): Intent {
// val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
// intent.data = Uri.parse("package:${context.packageName}")
// return intent
// }
//
// override fun parseResult(resultCode: Int, intent: Intent?): Boolean {
// return Environment.isExternalStorageManager()
// }
//
//}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/permissions/PermissionsViewModel.kt
================================================
package com.ke.hs_tracker.module.ui.permissions
import android.content.Context
import androidx.lifecycle.viewModelScope
import com.ke.hs_tracker.module.domain.GetDatabaseCardCountUseCase
import com.ke.hs_tracker.module.domain.WriteLogConfigFileUseCase
import com.ke.mvvm.base.data.successOr
import com.ke.mvvm.base.ui.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class PermissionsViewModel @Inject constructor(
private val getDatabaseCardCountUseCase: GetDatabaseCardCountUseCase,
private val writeLogConfigFileUseCase: WriteLogConfigFileUseCase
) : BaseViewModel() {
private val _navigationActions =
Channel(capacity = Channel.CONFLATED)
val navigationActions: Flow
get() = _navigationActions.receiveAsFlow()
internal fun next() {
viewModelScope.launch {
_navigationActions.send(PermissionsNavigationAction.NavigateToWriteConfig)
// writeLogConfigFileUseCase(true)
// if (getDatabaseCardCountUseCase(Unit).successOr(0) == 0) {
// _navigationActions.send(PermissionsNavigationAction.NavigateToSync)
// } else {
// _navigationActions.send(PermissionsNavigationAction.NavigateToMain)
// }
}
}
}
sealed interface PermissionsNavigationAction {
// data class ShowErrorDialog(
// val message: String
// ) : PermissionsNavigationAction
// object NavigateToMain : PermissionsNavigationAction
object NavigateToWriteConfig : PermissionsNavigationAction
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/records/RecordAdapter.kt
================================================
package com.ke.hs_tracker.module.ui.records
import android.view.LayoutInflater
import android.view.ViewGroup
import com.ke.hs_tracker.module.R
import com.ke.hs_tracker.module.databinding.ModuleItemRecordBinding
import com.ke.hs_tracker.module.db.Game
import com.ke.mvvm.base.ui.BaseViewBindingAdapter
import java.text.SimpleDateFormat
import java.util.*
class RecordAdapter : BaseViewBindingAdapter() {
override fun bindItem(
item: Game,
viewBinding: ModuleItemRecordBinding,
viewType: Int,
position: Int
) {
viewBinding.apply {
userHero.setImageResource(item.userHero!!.roundIcon!!)
opponentHero.setImageResource(item.opponentHero!!.roundIcon!!)
date.text = simpleDateFormat.format(Date(item.startTime))
state.isEnabled = item.isUserWin ?: true
val type =
root.context.getString(item.formatType.title) + root.context.getString(item.gameType.title)
gameType.text = type
state.setText(if (item.isUserWin == true) R.string.module_win else R.string.module_loss)
}
}
override fun createViewBinding(
inflater: LayoutInflater,
parent: ViewGroup,
viewType: Int
): ModuleItemRecordBinding {
return ModuleItemRecordBinding.inflate(inflater, parent, false)
}
}
private val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/records/RecordsActivity.kt
================================================
package com.ke.hs_tracker.module.ui.records
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.DividerItemDecoration
import com.ke.hs_tracker.module.databinding.ModuleActivityRecordsBinding
import com.ke.hs_tracker.module.ui.zoneevents.ZoneEventsActivity
import com.ke.mvvm.base.data.ViewStatus
import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
@AndroidEntryPoint
class RecordsActivity : AppCompatActivity() {
private val adapter by lazy {
RecordAdapter().apply {
setOnItemClickListener { _, _, position ->
startActivity(Intent(this@RecordsActivity, ZoneEventsActivity::class.java).apply {
putExtra(ZoneEventsActivity.EXTRA_KEY_ID, getItem(position).id)
})
}
}
}
private val recordsViewModel: RecordsViewModel by viewModels()
private lateinit var binding: ModuleActivityRecordsBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ModuleActivityRecordsBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.recyclerView.adapter = adapter
binding.recyclerView.addItemDecoration(
DividerItemDecoration(
this,
DividerItemDecoration.VERTICAL
)
)
binding.toolbar.setNavigationOnClickListener {
onBackPressed()
}
// binding.toolbar.menu.apply {
// clear()
// add(
// 0,
// 0,
// 0,
// R.string.module_settings
// ).setIcon(R.drawable.module_baseline_settings_white_24dp)
// .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
//
//
// }
// binding.toolbar.setOnMenuItemClickListener {
// startActivity(Intent(this, SettingsActivity::class.java))
// true
// }
binding.swipeRefreshLayout.setOnRefreshListener {
recordsViewModel.loadData()
}
launchAndRepeatWithViewLifecycle {
recordsViewModel.viewStatus.collect {
binding.swipeRefreshLayout.isRefreshing = it is ViewStatus.Loading
when (it) {
is ViewStatus.Loading -> {
}
is ViewStatus.Content -> {
adapter.setList(it.data)
}
is ViewStatus.Error -> {
}
}
}
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/records/RecordsViewModel.kt
================================================
package com.ke.hs_tracker.module.ui.records
import androidx.lifecycle.viewModelScope
import com.ke.hs_tracker.module.db.Game
import com.ke.hs_tracker.module.db.GameDao
import com.ke.mvvm.base.ui.BaseContentViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class RecordsViewModel @Inject constructor(private val gameDao: GameDao) :
BaseContentViewModel>() {
init {
loadData()
}
internal fun loadData() {
viewModelScope.launch {
showLoading()
showContent(gameDao.getAll().filter {
//掉线问题
it.opponentHero != null && it.isUserFirst != null
}.sortedByDescending {
it.endTime
})
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/settings/SettingsActivity.kt
================================================
package com.ke.hs_tracker.module.ui.settings
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.CompoundButton
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.ke.hs_tracker.module.R
import com.ke.hs_tracker.module.data.PreferenceStorage
import com.ke.hs_tracker.module.databinding.ModuleActivitySettingsBinding
import com.ke.hs_tracker.module.ui.deck.DeckCodeParserActivity
import com.ke.hs_tracker.module.ui.migrate.MigrateMainActivity
import com.ke.hs_tracker.module.ui.support.SupportActivity
import com.ke.hs_tracker.module.ui.sync.SyncCardDataActivity
import com.ke.hs_tracker.module.ui.test.TestActivity
import com.ke.hs_tracker.module.ui.theme.ThemeActivity
import com.ke.hs_tracker.module.ui.writeconfig.WriteConfigActivity
import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
import javax.inject.Inject
@AndroidEntryPoint
class SettingsActivity : AppCompatActivity(), CompoundButton.OnCheckedChangeListener {
private lateinit var binding: ModuleActivitySettingsBinding
private val settingsViewModel: SettingsViewModel by viewModels()
@Inject
lateinit var preferenceStorage: PreferenceStorage
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ModuleActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.floating.isChecked = preferenceStorage.floatingEnable
binding.floating.setOnCheckedChangeListener { _, isChecked ->
preferenceStorage.floatingEnable = isChecked
}
launchAndRepeatWithViewLifecycle {
settingsViewModel.saveLogFileEnable.collect {
binding.saveLogFile.setOnCheckedChangeListener(null)
binding.saveLogFile.isChecked = it
binding.saveLogFile.setOnCheckedChangeListener(this@SettingsActivity)
}
}
binding.toolbar.apply {
setNavigationOnClickListener {
onBackPressed()
}
this.menu.add(0, 0, 0, "测试")
this.setOnMenuItemClickListener {
startActivity(Intent(this@SettingsActivity, TestActivity::class.java))
true
}
}
binding.theme.setOnClickListener {
startActivity(Intent(this, ThemeActivity::class.java))
}
binding.codeParser.setOnClickListener {
startActivity(Intent(this, DeckCodeParserActivity::class.java))
}
binding.migrate.setOnClickListener {
startActivity(Intent(this, MigrateMainActivity::class.java))
}
binding.rewriteConfigFile.setOnClickListener {
val intent = Intent(this, WriteConfigActivity::class.java)
intent.putExtra(WriteConfigActivity.EXTRA_REWRITE, true)
startActivity(intent)
}
binding.sync.setOnClickListener {
val intent = Intent(this, SyncCardDataActivity::class.java)
intent.putExtra(SyncCardDataActivity.EXTRA_SHOW_BACK_BUTTON, true)
startActivity(intent)
}
//给作者发邮件
binding.llContactAuthor.setOnClickListener {
val email = getString(R.string.module_author_email)
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(email))
startActivity(Intent.createChooser(intent, "Send To"))
}
binding.llSource.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(getString(R.string.module_github_address))
startActivity(intent)
}
binding.support.setOnClickListener {
startActivity(Intent(this, SupportActivity::class.java))
}
}
override fun onCheckedChanged(p0: CompoundButton, checked: Boolean) {
settingsViewModel.setSaveLogFileEnable(checked)
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/settings/SettingsViewModel.kt
================================================
package com.ke.hs_tracker.module.ui.settings
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.ke.hs_tracker.module.domain.GetSaveLogFileEnableUseCase
import com.ke.hs_tracker.module.domain.SetSaveLogFileEnableUseCase
import com.ke.mvvm.base.data.successOr
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor(
private val getSaveLogFileEnableUseCase: GetSaveLogFileEnableUseCase,
private val setSaveLogFileEnableUseCase: SetSaveLogFileEnableUseCase
) : ViewModel() {
private val _saveLogFileEnable = MutableStateFlow(false)
internal val saveLogFileEnable: StateFlow
get() = _saveLogFileEnable
init {
viewModelScope.launch {
_saveLogFileEnable.value = getSaveLogFileEnableUseCase(Unit).successOr(false)
}
}
internal fun setSaveLogFileEnable(enable: Boolean) {
viewModelScope.launch {
_saveLogFileEnable.value = setSaveLogFileEnableUseCase(enable).successOr(enable)
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/splash/SplashActivity.kt
================================================
package com.ke.hs_tracker.module.ui.splash
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.ke.hs_tracker.module.ui.permissions.PermissionsActivity
import com.ke.hs_tracker.module.ui.summary.SummaryActivity
import com.ke.hs_tracker.module.ui.sync.SyncCardDataActivity
import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
@AndroidEntryPoint
@SuppressLint("CustomSplashScreen")
class SplashActivity : AppCompatActivity() {
private val splashViewModel: SplashViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// val intent = if (hasAllPermissions) {
// Intent(this, MainActivity::class.java)
// } else {
// Intent(this, PermissionsActivity::class.java)
// }
// startActivity(intent)
launchAndRepeatWithViewLifecycle {
// "写入log文件结果 ${writeLogFile()}".log()
// val result = writeLogFile()
//
// "写入文件结果 $result".log()
splashViewModel.navigationActions.collect {
val clazz = when (it) {
SplashNavigationAction.NavigateToMain -> SummaryActivity::class.java
SplashNavigationAction.NavigateToPermissions -> PermissionsActivity::class.java
SplashNavigationAction.NavigateToSync -> SyncCardDataActivity::class.java
}
val intent = Intent(this@SplashActivity, clazz)
startActivity(intent)
}
}
}
// private suspend fun writeLogFile(): Boolean {
// return withContext(Dispatchers.IO) {
//
//
// val documentFile = findHSDataFilesDir("Logs") ?: return@withContext false
//
// val fileName = "Power.log"
// documentFile?.findFile(fileName)?.delete()
// val configFile = documentFile.createFile("plain/text", fileName)
// ?: return@withContext false
//
// contentResolver.openOutputStream(configFile.uri)?.use {
// assets.open("Power.log")
// .copyTo(it)
// it.flush()
// }
//
// return@withContext true
// }
//
//
// }
override fun onStop() {
super.onStop()
finish()
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/splash/SplashViewModel.kt
================================================
package com.ke.hs_tracker.module.ui.splash
import android.content.Context
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.ke.hs_tracker.module.domain.GetDatabaseCardCountUseCase
import com.ke.hs_tracker.module.hasAllPermissions
import com.ke.mvvm.base.data.successOr
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class SplashViewModel @Inject constructor(
@ApplicationContext context: Context,
private val getDatabaseCardCountUseCase: GetDatabaseCardCountUseCase
) :
ViewModel() {
private val _navigationActions = Channel(capacity = Channel.CONFLATED)
val navigationActions: Flow
get() = _navigationActions.receiveAsFlow()
init {
viewModelScope.launch {
if (context.hasAllPermissions) {
if (getDatabaseCardCountUseCase(Unit).successOr(0) == 0) {
_navigationActions.send(SplashNavigationAction.NavigateToSync)
} else {
_navigationActions.send(SplashNavigationAction.NavigateToMain)
}
} else {
_navigationActions.send(SplashNavigationAction.NavigateToPermissions)
}
}
}
}
sealed interface SplashNavigationAction {
object NavigateToPermissions : SplashNavigationAction
object NavigateToMain : SplashNavigationAction
object NavigateToSync : SplashNavigationAction
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/summary/BattleRateItem.kt
================================================
package com.ke.hs_tracker.module.ui.summary
import androidx.annotation.IntRange
import com.ke.hs_tracker.module.entity.CardClass
/**
* 对局胜率
*/
sealed interface BattleRateItem {
/**
* 胜率局数
*/
val winCount: Int
/**
* 失败局数
*/
val lostCount: Int
/**
* 总局数
*/
val allCount: Int
/**
* 先手局数
*/
val firstHandCount: Int
/**
* 胜率
*/
@get:IntRange(from = 0, to = 100)
val rate: Int
/**
* 职业对战胜率
*/
data class ClassBattleRate(
override val winCount: Int,
override val lostCount: Int,
override val firstHandCount: Int,
val cardClass: CardClass
) : BattleRateItem {
override val allCount: Int
get() = winCount + lostCount
override val rate: Int
get() = if (allCount == 0) 0 else winCount * 100 / allCount
}
/**
* 卡组对战胜率
*/
data class DeckBattleRate(
override val winCount: Int,
override val lostCount: Int,
override val firstHandCount: Int,
val deckName: String,
val deckCode: String
) : BattleRateItem {
override val allCount: Int
get() = winCount + lostCount
override val rate: Int
get() = winCount * 100 / allCount
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/summary/BattleRateItemAdapter.kt
================================================
package com.ke.hs_tracker.module.ui.summary
import android.view.LayoutInflater
import android.view.ViewGroup
import com.ke.hs_tracker.module.R
import com.ke.hs_tracker.module.databinding.ModuleItemSummaryBattleBinding
import com.ke.hs_tracker.module.ui.classbattledetail.ClassBattleDetailActivity
import com.ke.hs_tracker.module.ui.deckbattledetail.DeckBattleDetailActivity
import com.ke.mvvm.base.ui.BaseViewBindingAdapter
internal class BattleRateItemAdapter :
BaseViewBindingAdapter() {
override fun bindItem(
item: BattleRateItem,
viewBinding: ModuleItemSummaryBattleBinding,
viewType: Int,
position: Int
) {
viewBinding.apply {
allCount.text = "总:" + (item.lostCount + item.winCount).toString()
winCount.text = "胜:" + item.winCount.toString()
lostCount.text = "负:" + item.lostCount.toString()
winRate.text = item.rate.toString() + "%"
when (item) {
is BattleRateItem.ClassBattleRate -> {
image.setImageResource(item.cardClass.roundIcon!!)
name.setText(item.cardClass.titleRes)
root.setOnClickListener {
it.context.startActivity(
ClassBattleDetailActivity.createIntent(
it.context,
item.cardClass
)
)
}
}
is BattleRateItem.DeckBattleRate -> {
image.setImageResource(R.drawable.module_image_round_demon_hunter)
name.text = item.deckName
root.setOnClickListener {
it.context.startActivity(
DeckBattleDetailActivity.createIntent(
it.context,
item.deckCode,
item.deckName
)
)
}
}
}
}
}
override fun createViewBinding(
inflater: LayoutInflater,
parent: ViewGroup,
viewType: Int
): ModuleItemSummaryBattleBinding {
return ModuleItemSummaryBattleBinding.inflate(inflater, parent, false)
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/summary/BattleRateListFragment.kt
================================================
package com.ke.hs_tracker.module.ui.summary
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.DividerItemDecoration
import com.hi.dhl.binding.viewbind
import com.ke.hs_tracker.module.R
import com.ke.mvvm.base.data.ViewStatus
import com.ke.mvvm.base.databinding.KeMvvmLayoutBaseRefreshListRetryBinding
import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle
import kotlinx.coroutines.flow.collect
abstract class BattleRateListFragment : Fragment() {
internal val adapter = BattleRateItemAdapter()
private val binding: KeMvvmLayoutBaseRefreshListRetryBinding by viewbind()
protected abstract val viewModel: BattleRateListViewModel<*>
final override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lifecycle.addObserver(viewModel)
binding.recyclerView.adapter = adapter
binding.recyclerView.addItemDecoration(
DividerItemDecoration(
requireContext(),
DividerItemDecoration.VERTICAL
)
)
adapter.addFooterView(
layoutInflater.inflate(R.layout.module_item_footer_with_fab, null)
)
binding.swipeRefreshLayout.setOnRefreshListener {
viewModel.refresh()
}
launchAndRepeatWithViewLifecycle {
viewModel.viewStatus.collect {
when (it) {
is ViewStatus.Loading -> {
binding.swipeRefreshLayout.isRefreshing = true
}
is ViewStatus.Content -> {
binding.swipeRefreshLayout.isRefreshing = false
adapter.setList(it.data.sortedByDescending { item ->
item.rate
})
}
is ViewStatus.Error -> throw IllegalArgumentException("不该出现错误的情况")
}
}
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/summary/BattleRateListViewModel.kt
================================================
package com.ke.hs_tracker.module.ui.summary
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.viewModelScope
import com.ke.mvvm.base.data.successOr
import com.ke.mvvm.base.domian.UseCase
import com.ke.mvvm.base.ui.BaseContentViewModel
import kotlinx.coroutines.launch
abstract class BattleRateListViewModel :
BaseContentViewModel>(), DefaultLifecycleObserver {
protected abstract val getBattleRateListUseCase: UseCase>
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
refresh()
}
internal fun refresh() {
viewModelScope.launch {
showLoading()
showContent(getBattleRateListUseCase(Unit).successOr(emptyList()))
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/summary/DeckBattleRateListViewModel.kt
================================================
package com.ke.hs_tracker.module.ui.summary
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class DeckBattleRateListViewModel @Inject constructor(override val getBattleRateListUseCase: GetDeckBattleRateListUseCase) :
BattleRateListViewModel()
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/summary/GetDeckBattleRateListUseCase.kt
================================================
package com.ke.hs_tracker.module.ui.summary
import com.ke.hs_tracker.module.db.GameDao
import com.ke.hs_tracker.module.di.IoDispatcher
import com.ke.mvvm.base.domian.UseCase
import kotlinx.coroutines.CoroutineDispatcher
import javax.inject.Inject
class GetDeckBattleRateListUseCase @Inject constructor(
private val gameDao: GameDao,
@IoDispatcher dispatcher: CoroutineDispatcher
) : UseCase>(dispatcher) {
override suspend fun execute(parameters: Unit): List {
val games = gameDao.getAll()
return games
.filter {
it.userDeckName.isNotEmpty() && it.userDeckCode.isNotEmpty()
}
.groupBy {
it.userDeckName to it.userDeckCode
}.map { map ->
BattleRateItem.DeckBattleRate(
map.value.count {
it.isUserWin == true
},
map.value.count {
it.isUserWin == false
},
map.value.count {
it.isUserFirst == false
},
map.key.first,
map.key.second
)
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/summary/GetHeroBattleRateListUseCase.kt
================================================
package com.ke.hs_tracker.module.ui.summary
import com.ke.hs_tracker.module.db.GameDao
import com.ke.hs_tracker.module.di.IoDispatcher
import com.ke.hs_tracker.module.entity.CardClass
import com.ke.mvvm.base.domian.UseCase
import kotlinx.coroutines.CoroutineDispatcher
import javax.inject.Inject
class GetHeroBattleRateListUseCase @Inject constructor(
@IoDispatcher dispatcher: CoroutineDispatcher,
private val gameDao: GameDao
) : UseCase>(dispatcher) {
override suspend fun execute(parameters: Unit): List {
val list = mutableListOf()
CardClass.values()
.filter {
it.isHero
}
.map {
it to gameDao.getByHero(it)
}.forEach { pair ->
val heroWinCount = pair.second.count {
it.isUserWin == true
}
val heroLostCount = pair.second.count {
it.isUserWin == false
}
val firstHandCount = pair.second.count {
it.isUserFirst == true
}
val item = BattleRateItem.ClassBattleRate(
heroWinCount,
heroLostCount,
firstHandCount,
pair.first
)
list.add(item)
}
return list
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/summary/HeroBattleRateListViewModel.kt
================================================
package com.ke.hs_tracker.module.ui.summary
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class HeroBattleRateListViewModel @Inject constructor(override val getBattleRateListUseCase: GetHeroBattleRateListUseCase) :
BattleRateListViewModel()
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/summary/RateByDeckFragment.kt
================================================
package com.ke.hs_tracker.module.ui.summary
import androidx.fragment.app.viewModels
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class RateByDeckFragment : BattleRateListFragment() {
override val viewModel: DeckBattleRateListViewModel by viewModels()
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/summary/RateByHeroFragment.kt
================================================
package com.ke.hs_tracker.module.ui.summary
import androidx.fragment.app.viewModels
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class RateByHeroFragment : BattleRateListFragment() {
override val viewModel: HeroBattleRateListViewModel by viewModels()
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/summary/SummaryActivity.kt
================================================
package com.ke.hs_tracker.module.ui.summary
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayoutMediator
import com.ke.hs_tracker.module.R
import com.ke.hs_tracker.module.data.PreferenceStorage
import com.ke.hs_tracker.module.databinding.ModuleActivitySummaryBinding
import com.ke.hs_tracker.module.service.WindowService
import com.ke.hs_tracker.module.ui.chart.SummaryChartActivity
import com.ke.hs_tracker.module.ui.main.MainActivity
import com.ke.hs_tracker.module.ui.records.RecordsActivity
import com.ke.hs_tracker.module.ui.settings.SettingsActivity
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class SummaryActivity : AppCompatActivity() {
@Inject
lateinit var preferenceStorage: PreferenceStorage
private lateinit var binding: ModuleActivitySummaryBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ModuleActivitySummaryBinding.inflate(layoutInflater)
setContentView(binding.root)
// request()
binding.toolbar.apply {
menu.clear()
menu.add(
0,
0,
0,
R.string.module_settings
).setIcon(R.drawable.module_baseline_settings_white_24dp)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
menu.add(
0,
2,
0,
R.string.module_pie_chart
).setIcon(R.drawable.module_baseline_pie_chart_white_24dp)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
menu.add(0, 1, 0, R.string.module_view_all_games)
setOnMenuItemClickListener {
when (it.itemId) {
0 -> {
startActivity(Intent(this@SummaryActivity, SettingsActivity::class.java))
}
1 -> {
startActivity(Intent(this@SummaryActivity, RecordsActivity::class.java))
}
2 -> {
startActivity(
Intent(
this@SummaryActivity,
SummaryChartActivity::class.java
)
)
}
}
true
}
}
binding.start.setOnClickListener {
// startActivity(Intent(this, MainActivity::class.java))
// startService(Intent(this, WindowService::class.java))
if (preferenceStorage.floatingEnable) {
//
if (Settings.canDrawOverlays(applicationContext)) {
startService(Intent(this, WindowService::class.java))
} else {
startActivityForResult(
Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:$packageName")
),
101
)
}
} else {
startActivity(Intent(this, MainActivity::class.java))
}
}
val fragments = listOf(RateByHeroFragment(), RateByDeckFragment())
val titles = listOf(R.string.module_by_class, R.string.module_by_deck)
val adapter = object : FragmentStateAdapter(this) {
override fun getItemCount() = fragments.size
override fun createFragment(position: Int) = fragments[position]
}
binding.viewPager.adapter = adapter
TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
tab.setText(titles[position])
}.attach()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == 101 && Settings.canDrawOverlays(applicationContext)) {
startService(Intent(this, WindowService::class.java))
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/support/SupportActivity.kt
================================================
package com.ke.hs_tracker.module.ui.support
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import com.ke.hs_tracker.module.R
class SupportActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.module_activity_support)
findViewById(R.id.toolbar).setNavigationOnClickListener {
onBackPressed()
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/sync/SyncCardDataActivity.kt
================================================
package com.ke.hs_tracker.module.ui.sync
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.ke.hs_tracker.module.R
import com.ke.hs_tracker.module.databinding.ModuleActivitySyncCardDataBinding
import com.ke.hs_tracker.module.ui.summary.SummaryActivity
import com.ke.mvvm.base.ui.collectLoadingDialog
import com.ke.mvvm.base.ui.collectSnackbarFlow
import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
@AndroidEntryPoint
class SyncCardDataActivity : AppCompatActivity() {
private lateinit var binding: ModuleActivitySyncCardDataBinding
private val viewModel: SyncCardDataViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ModuleActivitySyncCardDataBinding.inflate(layoutInflater)
setContentView(binding.root)
if (viewModel.showBackButton) {
binding.toolbar.setNavigationIcon(R.drawable.module_baseline_arrow_back_white_24dp)
binding.toolbar.setNavigationOnClickListener {
onBackPressed()
}
}
collectLoadingDialog(viewModel)
collectSnackbarFlow(viewModel)
binding.sync.setOnClickListener {
viewModel.sync(
binding.version.text?.toString() ?: "",
if (binding.chinese.isChecked) "zhCN" else "enUS"
)
}
launchAndRepeatWithViewLifecycle {
viewModel.navigationActions.collect {
val action: () -> Unit = when (it) {
SyncCardDataNavigationAction.NavigateToBack -> {
{
onBackPressed()
}
}
SyncCardDataNavigationAction.NavigateToMain -> {
{
val intent =
Intent(this@SyncCardDataActivity, SummaryActivity::class.java)
startActivity(intent)
finish()
}
}
}
AlertDialog.Builder(this@SyncCardDataActivity)
.setTitle(R.string.module_hint)
.setMessage(R.string.module_sync_success)
.setOnDismissListener {
action()
}.setPositiveButton(R.string.module_done, null)
.show()
}
}
}
companion object {
const val EXTRA_SHOW_BACK_BUTTON = "EXTRA_SHOW_BACK_BUTTON"
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/sync/SyncCardDataViewModel.kt
================================================
package com.ke.hs_tracker.module.ui.sync
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.ke.hs_tracker.module.domain.ClearCardTableUseCase
import com.ke.hs_tracker.module.domain.GetCardListUseCase
import com.ke.hs_tracker.module.domain.InsertCardListToDatabaseUseCase
import com.ke.mvvm.base.data.Result
import com.ke.mvvm.base.model.SnackbarAction
import com.ke.mvvm.base.ui.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class SyncCardDataViewModel @Inject constructor(
private val getCardListUseCase: GetCardListUseCase,
private val clearCardTableUseCase: ClearCardTableUseCase,
private val insertCardListToDatabaseUseCase: InsertCardListToDatabaseUseCase,
savedStateHandle: SavedStateHandle
) : BaseViewModel() {
internal val showBackButton =
savedStateHandle.get(SyncCardDataActivity.EXTRA_SHOW_BACK_BUTTON) ?: false
private val _navigationActions =
Channel(capacity = Channel.CONFLATED)
val navigationActions: Flow
get() = _navigationActions.receiveAsFlow()
fun sync(
versionCode: String,
region: String
) {
viewModelScope.launch {
showLoadingDialog("同步中")
val code = versionCode.ifEmpty { "latest" }
when (val result = getCardListUseCase(code to region)) {
is Result.Success -> {
clearCardTableUseCase(Unit)
insertCardListToDatabaseUseCase(result.data)
dismissLoadingDialog()
_navigationActions.send(if (showBackButton) SyncCardDataNavigationAction.NavigateToBack else SyncCardDataNavigationAction.NavigateToMain)
}
is Result.Error -> {
result.exception.printStackTrace()
dismissLoadingDialog()
showSnackbar(SnackbarAction("从服务器获取数据失败"))
// throw result.exception
}
}
}
}
}
sealed interface SyncCardDataNavigationAction {
/**
* 去首页
*/
object NavigateToMain : SyncCardDataNavigationAction
/**
* 返回上一个页面
*/
object NavigateToBack : SyncCardDataNavigationAction
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/test/CreateRecordActivity.kt
================================================
package com.ke.hs_tracker.module.ui.test
import android.content.DialogInterface
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import com.ke.hs_tracker.module.R
import com.ke.hs_tracker.module.databinding.ModuleActivityCreateRecordBinding
import com.ke.hs_tracker.module.db.Game
import com.ke.hs_tracker.module.db.GameDao
import com.ke.hs_tracker.module.entity.CardClass
import com.ke.hs_tracker.module.entity.FormatType
import com.ke.hs_tracker.module.entity.GameType
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class CreateRecordActivity : AppCompatActivity() {
@Inject
lateinit var gameDao: GameDao
private val heroClasses = CardClass.values().filter {
it.roundIcon != null
}
private var userClass = heroClasses[1]
private var opponentClass = heroClasses[3]
lateinit var binding: ModuleActivityCreateRecordBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ModuleActivityCreateRecordBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.toolbar.setNavigationOnClickListener {
onBackPressed()
}
refreshUserClass()
refreshOpponentClass()
binding.titleUserClass.setOnClickListener {
showClassPickerDialog { dialog, index ->
dialog.dismiss()
userClass = heroClasses[index]
refreshUserClass()
}
}
binding.titleOpponentClass.setOnClickListener {
showClassPickerDialog { dialog, index ->
dialog.dismiss()
opponentClass = heroClasses[index]
refreshOpponentClass()
}
}
binding.create.setOnClickListener {
val game = Game(
buildNumber = binding.buildNumber.text?.toString() ?: "",
gameType = getGameType(),
formatType = getFormatType(),
scenarioID = 2,
userName = binding.username.text?.toString() ?: "",
opponentName = binding.opponentName.text?.toString() ?: "",
isUserFirst = binding.isUserFirst.isChecked,
isUserWin = binding.isUserWon.isChecked,
userHero = userClass,
opponentHero = opponentClass,
userDeckCode = "AAECAbr5AwSJiwS4oASlrQSEsAQN5boD6LoD77oDm84D8NQDieADiuADpOED0eEDjOQDj+QDr4AEz6wEAA==",
userDeckName = "天胡",
startTime = System.currentTimeMillis() - 10000,
endTime = System.currentTimeMillis()
)
lifecycleScope.launch {
gameDao.insert(game)
AlertDialog.Builder(this@CreateRecordActivity)
.setTitle("提示")
.setMessage("写入成功")
.setPositiveButton("确定", null)
.show()
}
}
}
private fun showClassPickerDialog(onSelected: (DialogInterface, Int) -> Unit) {
AlertDialog.Builder(this)
.setTitle("选择职业")
.setSingleChoiceItems(heroClasses.map {
getString(it.titleRes)
}.toTypedArray(), heroClasses.indexOf(userClass)) { dialog, index ->
onSelected(dialog, index)
}.show()
}
private fun getGameType(): GameType {
return if (binding.rbGameType.checkedRadioButtonId == R.id.game_type_ranked) GameType.Ranked else GameType.Casual
}
private fun getFormatType(): FormatType {
return when (binding.rbFormatType.checkedRadioButtonId) {
R.id.format_type_standard -> FormatType.Standard
R.id.format_type_classic -> FormatType.Classic
R.id.format_type_wild -> FormatType.Wild
else -> FormatType.Unknown
}
}
private fun refreshUserClass() {
binding.userClass.setText(userClass.titleRes)
}
private fun refreshOpponentClass() {
binding.opponentClass.setText(opponentClass.titleRes)
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/test/LocalFileParserActivity.kt
================================================
package com.ke.hs_tracker.module.ui.test
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.ke.hs_tracker.module.R
class LocalFileParserActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.module_activity_local_file_parser)
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/test/TestActivity.kt
================================================
package com.ke.hs_tracker.module.ui.test
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.ke.hs_tracker.module.databinding.ModuleActivityTestBinding
import com.ke.hs_tracker.module.findHSDataFilesDir
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class TestActivity : AppCompatActivity() {
private lateinit var binding: ModuleActivityTestBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ModuleActivityTestBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.toolbar.setNavigationOnClickListener {
onBackPressed()
}
binding.createRecord.setOnClickListener {
startActivity(Intent(this, CreateRecordActivity::class.java))
}
binding.clearLog.setOnClickListener {
lifecycleScope.launch {
withContext(Dispatchers.IO) {
val documentFile = findHSDataFilesDir("Logs")?.findFile("Power.log")
// getLogsDir()?.findFile(powerFileName)
documentFile?.apply {
contentResolver.openOutputStream(uri, "wt")?.use {
it.write("".encodeToByteArray())
it.flush()
it.close()
}
}
}
AlertDialog.Builder(this@TestActivity)
.setTitle("提示")
.setMessage("清除成功")
.setPositiveButton("确定", null)
.show()
}
}
// val powerParser: PowerParser = PowerParserImpl()
// val lineList = mutableListOf()
//
// lineList.clear()
// lineList.addAll(
// assets.open("Power.log").reader().readLines().toMutableList()
// )
//
// val logDocument = findHSDataFilesDir("Logs")
//
//
// var document = logDocument?.findFile("Power.log")
// if (document == null) {
// document = logDocument?.createFile("plain/text", "Power.log")
// }
//
// var writer: OutputStreamWriter? = null
// document?.apply {
//
// writer = contentResolver.openOutputStream(uri, "wa")
// ?.writer()
// }
// binding.clear.setOnClickListener {
// document?.apply {
// contentResolver.openOutputStream(uri, "wt")?.writer()?.write("")
//
// }
//
// assets.open("Power.log").reader().apply {
// lineList.clear()
//
// lineList.addAll(readLines().toMutableList())
// close()
// }
//
//
// }
// powerParser.powerTagListener = {
//
//
// }
// var counter = 0
// binding.next.setOnClickListener {
//// val target = mutableListOf()
// repeat(1024) {
// counter++
// val line = lineList.removeFirstOrNull()
// if (line != null) {
//// target.add(line)
// writer?.appendLine(line)
//// powerParser.parse(line)
// }
// }
// writer?.flush()
//
//
// }
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/theme/ThemeActivity.kt
================================================
package com.ke.hs_tracker.module.ui.theme
import android.os.Bundle
import android.widget.CompoundButton
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import com.ke.hs_tracker.module.data.PreferenceStorage
import com.ke.hs_tracker.module.databinding.ModuleActivityThemeBinding
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class ThemeActivity : AppCompatActivity(), CompoundButton.OnCheckedChangeListener {
@Inject
lateinit var preferenceStorage: PreferenceStorage
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ModuleActivityThemeBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.toolbar.setNavigationOnClickListener {
onBackPressed()
}
binding.light.tag = AppCompatDelegate.MODE_NIGHT_NO
binding.dark.tag = AppCompatDelegate.MODE_NIGHT_YES
binding.system.tag = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
val buttonList = listOf(
binding.light,
binding.dark,
binding.system
)
buttonList.forEach {
it.isChecked = it.tag == preferenceStorage.theme
}
buttonList.forEach {
it.setOnCheckedChangeListener(this)
}
}
override fun onCheckedChanged(button: CompoundButton, checked: Boolean) {
if (checked) {
val theme = button.tag as? Int ?: return
preferenceStorage.theme = theme
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/writeconfig/WriteConfigActivity.kt
================================================
package com.ke.hs_tracker.module.ui.writeconfig
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.ke.hs_tracker.module.R
import com.ke.hs_tracker.module.databinding.ModuleActivityWriteConfigBinding
import com.ke.hs_tracker.module.ui.sync.SyncCardDataActivity
import com.ke.hs_tracker.module.writeLogConfigFile
import kotlinx.coroutines.launch
class WriteConfigActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.module_activity_write_config)
val binding = ModuleActivityWriteConfigBinding.inflate(layoutInflater)
setContentView(binding.root)
val rewrite = intent.getBooleanExtra(EXTRA_REWRITE, false)
if (rewrite) {
binding.forceWrite.isEnabled = false
binding.forceWrite.isChecked = true
binding.toolbar.apply {
setNavigationIcon(R.drawable.module_baseline_arrow_back_white_24dp)
setNavigationOnClickListener {
onBackPressed()
}
}
}
binding.content.text = assets.open("log.config").reader().readText()
binding.writeIn.setOnClickListener {
// findHSDataFilesDir("log.config")
lifecycleScope.launch {
if (writeLogConfigFile(
binding.forceWrite.isChecked
)
) {
if (rewrite) {
onBackPressed()
} else {
startActivity(
Intent(
this@WriteConfigActivity,
SyncCardDataActivity::class.java
)
)
finish()
}
} else {
AlertDialog.Builder(this@WriteConfigActivity)
.setTitle(R.string.module_hint)
.setMessage(R.string.module_write_config_failed)
.setPositiveButton(R.string.module_done, null)
.show()
}
}
}
}
companion object {
const val EXTRA_REWRITE = "EXTRA_REWRITE"
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/zonecards/ZoneCardsActivity.kt
================================================
package com.ke.hs_tracker.module.ui.zonecards
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.DividerItemDecoration
import com.bumptech.glide.Glide
import com.ke.hs_tracker.module.databinding.ModuleActivityZoneCardsBinding
import com.ke.hs_tracker.module.databinding.ModuleItemZoneCardBinding
import com.ke.hs_tracker.module.entity.ZoneCard
import com.ke.mvvm.base.ui.BaseViewBindingAdapter
class ZoneCardsActivity : AppCompatActivity() {
private val adapter = object : BaseViewBindingAdapter() {
override fun bindItem(
item: ZoneCard,
viewBinding: ModuleItemZoneCardBinding,
viewType: Int,
position: Int
) {
viewBinding.apply {
cost.text = item.card?.cost?.toString() ?: ""
name.text = item.card?.name ?: "未知卡牌"
this.position.text = item.position.toString()
item.card?.id?.let {
Glide.with(this@ZoneCardsActivity)
.load("https://art.hearthstonejson.com/v1/tiles/${it}.png")
.into(imageTile)
}
}
}
override fun createViewBinding(
inflater: LayoutInflater,
parent: ViewGroup,
viewType: Int
): ModuleItemZoneCardBinding {
return ModuleItemZoneCardBinding.inflate(inflater, parent, false)
}
}
private lateinit var binding: ModuleActivityZoneCardsBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ModuleActivityZoneCardsBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.recyclerView.adapter = adapter
binding.recyclerView.addItemDecoration(
DividerItemDecoration(
this,
DividerItemDecoration.VERTICAL
)
)
binding.toolbar.setNavigationOnClickListener {
onBackPressed()
}
val zoneCardList = intent.getParcelableArrayListExtra(EXTRA_KEY_ZONE_CARD_LIST)
adapter.setList(zoneCardList?.sortedBy {
it.position
})
}
companion object {
const val EXTRA_KEY_ZONE_CARD_LIST = "EXTRA_KEY_ZONE_CARD_LIST"
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/zoneevents/ListModeFragment.kt
================================================
package com.ke.hs_tracker.module.ui.zoneevents
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.DividerItemDecoration
import com.hi.dhl.binding.viewbind
import com.ke.hs_tracker.module.R
import com.ke.hs_tracker.module.databinding.ModuleFragmentListModeBinding
import com.ke.hs_tracker.module.databinding.ModuleItemZoneEventListModeBinding
import com.ke.hs_tracker.module.entity.Zone
import com.ke.hs_tracker.module.entity.ZoneCard
import com.ke.mvvm.base.ui.BaseViewBindingAdapter
import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
@AndroidEntryPoint
internal class ListModeFragment : Fragment() {
private val onChipClickListener = View.OnClickListener {
(it.tag as? List)?.apply {
if (isNotEmpty()) {
(activity as? ZoneEventsActivity)?.toZoneCardsActivity(this)
}
}
}
private val adapter by lazy {
object : BaseViewBindingAdapter() {
override fun bindItem(
item: GameCardCollections,
viewBinding: ModuleItemZoneEventListModeBinding,
viewType: Int,
position: Int
) {
viewBinding.apply {
userDeck.text = "玩家牌库 ${item.userDeckCardList.size}"
userHand.text = "玩家手牌 ${item.userHandCardList.size}"
userPlay.text = "玩家战场 ${item.userPlayCardList.size}"
userGraveyard.text = "玩家墓地 ${item.userGraveyardCardList.size}"
userSecret.text = "玩家奥秘 ${item.userSecretCardList.size}"
opponentDeck.text = "对手牌库 ${item.opponentDeckCardList.size}"
opponentHand.text = "对手手牌 ${item.opponentHandCardList.size}"
opponentPlay.text = "对手战场 ${item.opponentPlayCardList.size}"
opponentGraveyard.text = "对手墓地 ${item.opponentGraveyardCardList.size}"
opponentSecret.text = "对手奥秘 ${item.opponentSecretCardList.size}"
userDeck.setOnClickListener(onChipClickListener)
userHand.setOnClickListener(onChipClickListener)
userPlay.setOnClickListener(onChipClickListener)
userGraveyard.setOnClickListener(onChipClickListener)
userSecret.setOnClickListener(onChipClickListener)
opponentDeck.setOnClickListener(onChipClickListener)
opponentHand.setOnClickListener(onChipClickListener)
opponentPlay.setOnClickListener(onChipClickListener)
opponentGraveyard.setOnClickListener(onChipClickListener)
opponentSecret.setOnClickListener(onChipClickListener)
userDeck.tag = item.userDeckCardList
userHand.tag = item.userHandCardList
userPlay.tag = item.userPlayCardList
userGraveyard.tag = item.userGraveyardCardList
userSecret.tag = item.userSecretCardList
opponentDeck.tag = item.opponentDeckCardList
opponentHand.tag = item.opponentHandCardList
opponentPlay.tag = item.opponentPlayCardList
opponentGraveyard.tag = item.opponentGraveyardCardList
opponentSecret.tag = item.opponentSecretCardList
// userDeck.setOnClickListener {
// if (item.userDeckCardList.isNotEmpty()) {
// (activity as? ZoneEventsActivity)?.toZoneCardsActivity(item.userDeckCardList)
// }
// }
}
}
override fun createViewBinding(
inflater: LayoutInflater,
parent: ViewGroup,
viewType: Int
): ModuleItemZoneEventListModeBinding {
return ModuleItemZoneEventListModeBinding.inflate(inflater, parent, false)
}
}
}
private val binding: ModuleFragmentListModeBinding by viewbind()
private val zoneEventsViewModel: ZoneEventsViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.recyclerView.adapter = adapter
binding.recyclerView.addItemDecoration(
DividerItemDecoration(
requireContext(),
DividerItemDecoration.VERTICAL
)
)
launchAndRepeatWithViewLifecycle {
zoneEventsViewModel.collectionsList.collect {
adapter.setList(it)
}
}
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/zoneevents/ZoneEventsActivity.kt
================================================
package com.ke.hs_tracker.module.ui.zoneevents
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Parcelable
import androidx.activity.viewModels
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.ke.hs_tracker.module.R
import com.ke.hs_tracker.module.databinding.ModuleActivityZoneEventsBinding
import com.ke.hs_tracker.module.entity.ZoneCard
import com.ke.hs_tracker.module.ui.common.LoadingFragment
import com.ke.hs_tracker.module.ui.zonecards.ZoneCardsActivity
import com.ke.mvvm.base.ui.FragmentViewPager2Adapter
import com.ke.mvvm.base.ui.launchAndRepeatWithViewLifecycle
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
@AndroidEntryPoint
class ZoneEventsActivity : AppCompatActivity() {
private val zoneEventsViewModel: ZoneEventsViewModel by viewModels()
private lateinit var binding: ModuleActivityZoneEventsBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ModuleActivityZoneEventsBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.toolbar.setNavigationOnClickListener {
onBackPressed()
}
val fragmentList = listOf(LoadingFragment(), ListModeFragment(), ListModeFragment())
//禁止左右滑动
binding.viewPager.isUserInputEnabled = false
binding.viewPager.adapter = object : FragmentStateAdapter(this) {
override fun getItemCount(): Int {
return fragmentList.size
}
override fun createFragment(position: Int): Fragment {
return fragmentList[position]
}
}
launchAndRepeatWithViewLifecycle {
zoneEventsViewModel.currentFragmentIndex.collect {
binding.toggle.isVisible = it != 0
binding.viewPager.currentItem = it
}
}
}
internal fun toZoneCardsActivity(list: List) {
val intent = Intent(this, ZoneCardsActivity::class.java)
intent.putParcelableArrayListExtra(
ZoneCardsActivity.EXTRA_KEY_ZONE_CARD_LIST,
arrayListOf().apply {
addAll(list)
})
startActivity(intent)
}
companion object {
const val EXTRA_KEY_ID = "EXTRA_KEY_ID"
}
}
================================================
FILE: module/src/main/java/com/ke/hs_tracker/module/ui/zoneevents/ZoneEventsViewModel.kt
================================================
package com.ke.hs_tracker.module.ui.zoneevents
import android.os.Parcel
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.ke.hs_tracker.module.db.CardDao
import com.ke.hs_tracker.module.db.ZonePositionChangedEvent
import com.ke.hs_tracker.module.db.ZonePositionChangedEventDao
import com.ke.hs_tracker.module.entity.Card
import com.ke.hs_tracker.module.entity.Zone
import com.ke.hs_tracker.module.entity.ZoneCard
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import javax.inject.Inject
@HiltViewModel
class ZoneEventsViewModel @Inject constructor(
private val zonePositionChangedEventDao: ZonePositionChangedEventDao,
private val cardDao: CardDao,
savedStateHandle: SavedStateHandle
) :
ViewModel() {
private val _currentFragmentIndex = MutableStateFlow(0)
internal val currentFragmentIndex: StateFlow
get() = _currentFragmentIndex
private val gameId = savedStateHandle.get(ZoneEventsActivity.EXTRA_KEY_ID)!!
private val gameCardCollectionsList = mutableListOf().apply {
add(GameCardCollections())
}
private val _collectionsList = MutableStateFlow>(emptyList())
internal val collectionsList: StateFlow>
get() = _collectionsList
init {
viewModelScope.launch {
withContext(Dispatchers.IO) {
val allCard = cardDao.getAll()
val list = zonePositionChangedEventDao.getAllByGameId(gameId)
if (list.isEmpty()) {
return@withContext
}
val stack = mutableListOf()
list.forEach {
if (it.entityId == stack.lastOrNull()?.entityId) {
stack.add(it)
} else {
flushStack(stack, gameCardCollectionsList, allCard)
}
if (stack.isEmpty()) {
stack.add(it)
}
}
flushStack(stack, gameCardCollectionsList, allCard)
_currentFragmentIndex.value = 1
_collectionsList.value = gameCardCollectionsList
}
}
}
companion object {
private fun flushStack(
list: MutableList,
mutableList: MutableList,
allCardList: List
) {
val last = mutableList.last()
if (list.isEmpty()) {
return
}
when (list.size) {
1 -> {
//仅改变区域
val event = list.first()
mutableList.add(last.update(event, allCardList))
}
2 -> {
//改变区域和位置
val first = list[0]
val second = list[1]
mutableList.add(last.update(first.plus(second), allCardList))
}
3 -> {
val first = list[0]
val second = list[1]
val third = list[2]
mutableList.add(last.update(first.plusPlus(second, third), allCardList))
}
else -> {
list.forEach {
flushStack(
mutableListOf(it), mutableList, allCardList
)
}
}
}
list.clear()
}
}
}
@Parcelize
internal data class GameCardCollections(
private val _userDeckCardList: MutableList = mutableListOf(),
private val _opponentDeckCardList: MutableList = mutableListOf(),
private val _userHandCardList: MutableList = mutableListOf(),
private val _opponentHandCardList: MutableList = mutableListOf(),
private val _userPlayCardList: MutableList = mutableListOf(),
private val _opponentPlayCardList: MutableList