feat: Add prefecture modification support (#170)

This commit is contained in:
alexay7 2025-08-21 22:19:25 +02:00 committed by GitHub
parent 15412911a9
commit 3d95a84739
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 294 additions and 24 deletions

View File

@ -4,6 +4,7 @@
import GameSettingFields from "./GameSettingFields.svelte";
import { t, ts } from "../../libs/i18n";
import useLocalStorage from "../../libs/hooks/useLocalStorage.svelte";
import RegionSelector from "./RegionSelector.svelte";
const rounding = useLocalStorage("rounding", true);
</script>
@ -22,6 +23,11 @@
</label>
</div>
</div>
<div class="divider"></div>
<blockquote>
{ts("settings.regionNotice")}
</blockquote>
<RegionSelector/>
</div>
<style lang="sass">
@ -44,19 +50,10 @@
.desc
opacity: 0.6
.field
display: flex
flex-direction: column
label
max-width: max-content
> div:not(.bool)
display: flex
align-items: center
gap: 1rem
margin-top: 0.5rem
> input
flex: 1
.divider
width: 100%
height: 0.5px
background: white
opacity: 0.2
margin: 0.4rem 0
</style>

View File

@ -0,0 +1,59 @@
<script lang="ts">
import { USER} from "../../libs/sdk";
import { ts } from "../../libs/i18n";
import StatusOverlays from "../StatusOverlays.svelte";
let regionId = 0;
let submitting = ""
let error: string;
const prefectures = ["None","Aichi","Aomori","Akita","Ishikawa","Ibaraki","Iwate","Ehime","Oita","Osaka","Okayama","Okinawa","Kagawa","Kagoshima","Kanagawa","Gifu","Kyoto","Kumamoto","Gunma","Kochi","Saitama","Saga","Shiga","Shizuoka","Shimane","Chiba","Tokyo","Tokushima","Tochigi","Tottori","Toyama","Nagasaki","Nagano","Nara","Niigata","Hyogo","Hiroshima","Fukui","Fukuoka","Fukushima","Hokkaido","Mie","Miyagi","Miyazaki","Yamagata","Yamaguchi","Yamanashi","Wakayama"]
USER.me().then(user => {
const parsedRegion = parseInt(user.region);
if (!isNaN(parsedRegion) && parsedRegion > 0) {
regionId = parsedRegion - 1;
} else {
regionId = 0;
}
})
async function saveNewRegion() {
if (submitting) return false
submitting = "region"
await USER.changeRegion(regionId+1).catch(e => error = e.message).finally(() => submitting = "")
return true
}
</script>
<div class="fields">
<label for="rounding">
<span class="name">{ts(`settings.regionSelector.title`)}</span>
<span class="desc">{ts(`settings.regionSelector.desc`)}</span>
</label>
<select bind:value={regionId} on:change={saveNewRegion}>
<option value={0} disabled selected>{ts("settings.regionSelector.select")}</option>
{#each prefectures.filter(p=>p!=="None") as prefecture, index}
<option value={index}>{prefecture}</option>
{/each}
</select>
</div>
<StatusOverlays {error} loading={!!submitting}/>
<style lang="sass">
@use "../../vars"
.fields
display: flex
flex-direction: column
gap: 12px
label
display: flex
flex-direction: column
.desc
opacity: 0.6
</style>

View File

@ -19,6 +19,7 @@ export interface AquaNetUser {
email: string
displayName: string
country: string
region:string
lastLogin: number
regTime: number
profileLocation: string

View File

@ -195,7 +195,11 @@ export const EN_REF_SETTINGS = {
'settings.export': 'Export Player Data',
'settings.batchManualExport': "Export in Batch Manual (for Tachi)",
'settings.cabNotice': "Note: These settings will only affect your own cab/setup. If you're playing on someone else's setup, please contact them to change these settings.",
'settings.gameNotice': "These only apply to Mai and Wacca."
'settings.gameNotice': "These only apply to Mai and Wacca.",
'settings.regionNotice': "These only apply to Mai, Ongeki and Chuni.",
'settings.regionSelector.title': "Prefecture Selector",
'settings.regionSelector.desc': "Select the region where you want the game to think you are playing",
'settings.regionSelector.select': "Select Prefecture",
}
export const EN_REF_USERBOX = {

View File

@ -208,6 +208,14 @@ const zhSettings: typeof EN_REF_SETTINGS = {
'settings.batchManualExport': "导出 Batch Manual 格式(用于 Tachi",
'settings.cabNotice': '注意:下面这些设置只会影响你自己的机器,如果你是在其他人的机器上玩的话,请联系机主来改设置',
'settings.gameNotice': "这些设置仅对舞萌和华卡生效。",
// AI
'settings.regionNotice': "这些设置仅适用于舞萌、音击和中二。",
// AI
'settings.regionSelector.title': "地区选择器",
// AI
'settings.regionSelector.desc': "选择游戏中显示的地区",
// AI
'settings.regionSelector.select': "选择地区",
}
export const zhUserbox: typeof EN_REF_USERBOX = {

View File

@ -196,6 +196,8 @@ export const USER = {
},
isLoggedIn,
ensureLoggedIn,
changeRegion: (regionId: number) =>
post('/api/v2/user/change-region', { regionId }),
}
export const USERBOX = {

View File

@ -161,7 +161,7 @@ class UserRegistrar(
// Check if user exists, treat as email / username
val user = async { userRepo.findByEmailIgnoreCase(email) ?: userRepo.findByUsernameIgnoreCase(email) }
?: return SUCCESS // obviously dont tell them if the email exists or not
// Check if email is verified
if (!user.emailConfirmed && emailProps.enable) 400 - "Email not verified"
@ -179,7 +179,7 @@ class UserRegistrar(
// Send a password reset email
emailService.sendPasswordReset(user)
return SUCCESS
}
@ -189,7 +189,7 @@ class UserRegistrar(
@RP token: Str, @RP password: Str,
request: HttpServletRequest
) : Any {
// Find the reset token
val reset = async { resetPasswordRepo.findByToken(token) }
@ -302,4 +302,17 @@ class UserRegistrar(
SUCCESS
}
@API("/change-region")
@Doc("Change the region of the user.", "Success message")
suspend fun changeRegion(@RP token: Str, @RP regionId: Str) = jwt.auth(token) { u ->
// Check if the region is valid (between 1 and 47)
val r = regionId.toIntOrNull() ?: (400 - "Invalid region")
if (r !in 1..47) 400 - "Invalid region"
async {
userRepo.save(u.apply { region = r.toString() })
}
SUCCESS
}
}

View File

@ -43,6 +43,10 @@ class AquaNetUser(
@Column(length = 3)
var country: String = "",
// Region code at most 2 characters
@Column(length = 2)
var region: String = "",
// Last login time
var lastLogin: Long = 0L,

View File

@ -103,6 +103,7 @@ class AllNet(
// encode UTF-8, format_ver 3, hops 1 token 2010451813
val reqMap = decodeAllNet(dataStream.readAllBytes())
val serial = reqMap["serial"] ?: ""
var region = props.map.mut["region0"] ?: "1"
logger.info("AllNet /PowerOn : $reqMap")
var session: String? = null
@ -114,6 +115,10 @@ class AllNet(
if (u != null) {
// Create a new session for the user
logger.info("> Keychip authenticated: ${u.auId} ${u.computedName}")
// If the user defined its own region apply it
if (u.region.isNotBlank()) {
region = u.region
}
session = keychipSessionService.new(u, reqMap["game_id"] ?: "").token
}
@ -140,6 +145,7 @@ class AllNet(
val resp = props.map.mut + mapOf(
"uri" to switchUri(here, localPort, gameId, ver, session),
"host" to props.host.ifBlank { here },
"region0" to region
)
// Different responses for different versions

View File

@ -288,6 +288,12 @@ fun ChusanController.chusanInit() {
)
}
"GetUserRegion" {
db.userRegions.findByUser_Card_ExtId(uid)
.map { mapOf("regionId" to it.regionId, "playCount" to it.playCount) }
.let { mapOf("userId" to uid, "userRegionList" to it) }
}
// Game settings
"GetGameSetting" {
val version = data["version"].toString()

View File

@ -29,6 +29,25 @@ fun ChusanController.upsertApiInit() {
userNameEx = ""
}.also { db.userData.saveAndFlush(it) }
// Only save if it is a valid region and the user has played at least a song
if (req.userPlaylogList?.isNotEmpty() == true) {
val region = req.userPlaylogList!![0].regionId
val userRegion = db.userRegions.findByUserIdAndRegionId(u.id, region)
if (userRegion.isPresent) {
userRegion.get().apply {
playCount += 1
db.userRegions.save(this)
}
} else {
db.userRegions.save(UserRegions().apply {
user = u
regionId = region
playCount = 1
})
}
}
versionHelper[u.lastClientId] = u.lastDataVersion
// Set users

View File

@ -174,6 +174,10 @@ interface Chu3GameLoginBonusRepo : JpaRepository<GameLoginBonus, Int> {
fun findByRequiredDays(version: Int, presetId: Int, requiredDays: Int): Optional<GameLoginBonus>
}
interface Chu3UserRegionsRepo: Chu3UserLinked<UserRegions> {
fun findByUserIdAndRegionId(userId: Long, regionId: Int): Optional<UserRegions>
}
@Component
class Chu3Repos(
val userLoginBonus: Chu3UserLoginBonusRepo,
@ -191,6 +195,7 @@ class Chu3Repos(
val userMap: Chu3UserMapRepo,
val userMusicDetail: Chu3UserMusicDetailRepo,
val userPlaylog: Chu3UserPlaylogRepo,
val userRegions: Chu3UserRegionsRepo,
val userCMission: Chu3UserCMissionRepo,
val userCMissionProgress: Chu3UserCMissionProgressRepo,
val netBattleLog: Chu3NetBattleLogRepo,

View File

@ -0,0 +1,14 @@
package icu.samnyan.aqua.sega.chusan.model.userdata
import jakarta.persistence.Entity
import jakarta.persistence.Table
import jakarta.persistence.UniqueConstraint
import java.time.LocalDate
@Entity(name = "ChusanUserRegions")
@Table(name = "chusan_user_regions", uniqueConstraints = [UniqueConstraint(columnNames = ["user_id", "region_id"])])
class UserRegions : Chu3UserEntity() {
var regionId = 0
var playCount = 0
var created: String = LocalDate.now().toString()
}

View File

@ -7,6 +7,7 @@ import icu.samnyan.aqua.sega.general.model.CardStatus
import icu.samnyan.aqua.sega.maimai2.model.UserRivalMusic
import icu.samnyan.aqua.sega.maimai2.model.UserRivalMusicDetail
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserKaleidx
import icu.samnyan.aqua.sega.maimai2.model.userdata.UserRegions
import java.time.LocalDate
fun Maimai2ServletController.initApis() {
@ -134,6 +135,31 @@ fun Maimai2ServletController.initApis() {
res["returnCode"] = 0
}
// Get regionId from request
val region = data["regionId"] as? Int
// Only save if it is a valid region and the user has played at least a song
if (region!=null && region > 0 && d != null) {
val userRegion = db.userRegions.findByUserIdAndRegionId(uid, region)
if (userRegion.isPresent) {
userRegion.get().apply {
playCount += 1
db.userRegions.save(this)
}
} else {
logger().info("user: $d")
logger().info("region: $region")
// Create a new user region row
// Crea una nueva fila de región de usuario
db.userRegions.save(UserRegions().apply {
user = d
regionId = region
playCount = 1
})
}
}
res
}
@ -178,13 +204,19 @@ fun Maimai2ServletController.initApis() {
mapOf("userId" to uid, "rivalId" to rivalId, "nextIndex" to 0, "userRivalMusicList" to res.values)
}
"GetUserRegion" {
logger().info("Getting user regions for user $uid")
db.userRegions.findByUser_Card_ExtId(uid)
.map { mapOf("regionId" to it.regionId, "playCount" to it.playCount) }
.let { mapOf("userId" to uid, "userRegionList" to it) }
}
"GetUserIntimate".unpaged {
val u = db.userData.findByCardExtId(uid)() ?: (404 - "User not found")
db.userIntimate.findByUser(u)
}
// Empty List Handlers
"GetUserRegion".unpaged { empty }
"GetUserGhost".unpaged { empty }
"GetUserFriendBonus" { mapOf("userId" to uid, "returnCode" to 0, "getMiles" to 0) }
"GetTransferFriend" { mapOf("userId" to uid, "transferFriendList" to empty) }

View File

@ -127,6 +127,10 @@ interface Mai2GameEventRepo : JpaRepository<Mai2GameEvent, Int> {
interface Mai2GameSellingCardRepo : JpaRepository<Mai2GameSellingCard, Long>
interface Mai2UserRegionsRepo: Mai2UserLinked<UserRegions> {
fun findByUserIdAndRegionId(userId: Long, regionId: Int): Optional<UserRegions>
}
@Component
class Mai2Repos(
val mapEncountNpc: Mai2MapEncountNpcRepo,
@ -152,5 +156,6 @@ class Mai2Repos(
val userIntimate: MAi2UserIntimateRepo,
val gameCharge: Mai2GameChargeRepo,
val gameEvent: Mai2GameEventRepo,
val gameSellingCard: Mai2GameSellingCardRepo
val gameSellingCard: Mai2GameSellingCardRepo,
val userRegions: Mai2UserRegionsRepo,
)

View File

@ -21,6 +21,7 @@ import java.time.format.DateTimeFormatter
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.core.JsonGenerator
import java.time.LocalDate
@MappedSuperclass
open class Mai2UserEntity : BaseEntity(), IUserEntity<Mai2UserDetail> {
@ -451,9 +452,9 @@ class Mai2UserPlaylog : Mai2UserEntity(), IGenericGamePlaylog {
get() = maxCombo == totalCombo
override val isAllPerfect: Boolean
get() = tapMiss + tapGood + tapGreat == 0 &&
holdMiss + holdGood + holdGreat == 0 &&
slideMiss + slideGood + slideGreat == 0 &&
get() = tapMiss + tapGood + tapGreat == 0 &&
holdMiss + holdGood + holdGreat == 0 &&
slideMiss + slideGood + slideGreat == 0 &&
touchMiss + touchGood + touchGreat == 0 &&
breakMiss + breakGood + breakGreat == 0
}
@ -551,6 +552,17 @@ class Mai2UserIntimate : Mai2UserEntity() {
var intimateCountRewarded = 0;
}
@Entity(name = "Maimai2UserRegions")
@Table(
name = "maimai2_user_regions",
uniqueConstraints = [UniqueConstraint(columnNames = ["user_id", "region_id"])]
)
class UserRegions : Mai2UserEntity() {
var regionId = 0
var playCount = 0
var created: String = LocalDate.now().toString()
}
val MAIMAI_DATETIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0")
class MaimaiDateSerializer : JsonSerializer<LocalDateTime>() {
override fun serialize(v: LocalDateTime, j: JsonGenerator, s: SerializerProvider) {

View File

@ -69,4 +69,10 @@ fun OngekiController.ongekiInit() {
"GetClientTestmode" {
empty.staticLst("clientTestmodeList") + mapOf("placeId" to data["placeId"])
}
"GetUserRegion" {
db.regions.findByUser_Card_ExtId(uid)
.map { mapOf("regionId" to it.regionId, "playCount" to it.playCount) }
.staticLst("userRegionList") + mapOf("userId" to uid)
}
}

View File

@ -147,6 +147,10 @@ interface OgkUserTrainingRoomRepo : OngekiUserLinked<UserTrainingRoom> {
fun findByUserAndRoomId(user: UserData, roomId: Int): Optional<UserTrainingRoom>
}
interface OgkUserRegionsRepo: OngekiUserLinked<UserRegions> {
fun findByUserIdAndRegionId(userId: Long, regionId: Int): Optional<UserRegions>
}
// Re:Fresh
interface OgkUserEventMapRepo : OngekiUserLinked<UserEventMap>
interface OgkUserSkinRepo : OngekiUserLinked<UserSkin>
@ -190,6 +194,7 @@ class OngekiUserRepos(
val trainingRoom: OgkUserTrainingRoomRepo,
val eventMap: OgkUserEventMapRepo,
val skin: OgkUserSkinRepo,
val regions: OgkUserRegionsRepo,
)
@Component

View File

@ -1,11 +1,13 @@
package icu.samnyan.aqua.sega.ongeki
import ext.int
import ext.invoke
import ext.mapApply
import ext.minus
import icu.samnyan.aqua.sega.ongeki.model.OngekiUpsertUserAll
import icu.samnyan.aqua.sega.ongeki.model.UserData
import icu.samnyan.aqua.sega.ongeki.model.UserGeneralData
import icu.samnyan.aqua.sega.ongeki.model.UserRegions
fun OngekiController.initUpsertAll() {
@ -33,6 +35,26 @@ fun OngekiController.initUpsertAll() {
db.data.save(this)
} ?: oldUser ?: return@api null
// User region
val region = data["regionId"]?.int ?: 0
// Only save if it is a valid region and the user has played at least a song
if (region > 0 && all.userPlaylogList?.isNotEmpty() == true) {
val userRegion = db.regions.findByUserIdAndRegionId(u.id, region)
if (userRegion.isPresent) {
userRegion.get().apply {
playCount += 1
db.regions.save(this)
}
} else {
db.regions.save(UserRegions().apply {
user = u
regionId = region
playCount = 1
})
}
}
all.run {
// Set users
listOfNotNull(

View File

@ -7,6 +7,7 @@ import icu.samnyan.aqua.net.games.*
import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.util.jackson.AccessCodeSerializer
import jakarta.persistence.*
import java.time.LocalDate
@MappedSuperclass
class OngekiUserEntity : BaseEntity(), IUserEntity<UserData> {
@ -511,4 +512,15 @@ class UserSkin : OngekiUserEntity() {
var cardId1 = 0
var cardId2 = 0
var cardId3 = 0
}
@Entity(name = "OngekiUserRegions")
@Table(
name = "ongeki_user_regions",
uniqueConstraints = [UniqueConstraint(columnNames = ["user_id", "regionId"])]
)
class UserRegions : OngekiUserEntity() {
var regionId = 0
var playCount = 0
var created: String = LocalDate.now().toString()
}

View File

@ -0,0 +1,38 @@
CREATE TABLE chusan_user_regions
(
id BIGINT AUTO_INCREMENT NOT NULL,
user_id BIGINT NULL,
region_id INT NOT NULL,
play_count INT NOT NULL DEFAULT 1,
created VARCHAR(355),
PRIMARY KEY (id),
CONSTRAINT fk_chusanregions_on_chusan_user_Data FOREIGN KEY (user_id) REFERENCES chusan_user_data (id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT unq_chusanregions_on_region_user UNIQUE (user_id, region_id)
);
CREATE TABLE ongeki_user_regions
(
id BIGINT AUTO_INCREMENT NOT NULL,
user_id BIGINT NULL,
region_id INT NOT NULL,
play_count INT NOT NULL DEFAULT 1,
created VARCHAR(355),
PRIMARY KEY (id),
CONSTRAINT fk_ongekiregions_on_aqua_net_user FOREIGN KEY (user_id) REFERENCES aqua_net_user (au_id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT unq_ongekiregions_on_region_user UNIQUE (user_id, region_id)
);
CREATE TABLE maimai2_user_regions
(
id BIGINT AUTO_INCREMENT NOT NULL,
user_id BIGINT NULL,
region_id INT NOT NULL,
play_count INT NOT NULL DEFAULT 1,
created VARCHAR(355),
PRIMARY KEY (id),
CONSTRAINT fk_maimai2regions_on_user_Details FOREIGN KEY (user_id) REFERENCES maimai2_user_detail (id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT unq_maimai2regions_on_region_user UNIQUE (user_id, region_id)
);
ALTER TABLE aqua_net_user
ADD COLUMN region VARCHAR(2) NOT NULL DEFAULT '1';