From b4ecb80b23fc596108262610624111151df95a08 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Fri, 23 Oct 2026 00:05:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(state):=20=E8=BF=81=E7=A7=BB=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E9=82=80=E8=AF=B7=E5=88=87=E7=89=87=20reducer?= =?UTF-8?q?=EF=BC=88InviteState=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1) 动作在 Kotlin 侧不再要求公共基类:原包动作也没有基类,Store/Gateway 改用 Any,817 个生成类无需实现接口。 2) 新增 App/State/Reducers/InviteReducer.kt,逐条对照 module_10032:状态 aD=InviteState(isInviteNeed, invite, status, existsUid, inviteAccepted)、枚举 lD=InviteStatus(UNKNOWN/FREE/NOT_FOUND/ALREADY_ACTIVATED)、reducer pD.er5 的三条分支。默认值取自构造函数(isInviteNeed 默认 true、invite 默认空串、status 默认 UNKNOWN、inviteAccepted 默认 false)。 3) 自检 19 增到 26 项:默认值、三条动作分支(含 FREE 推导 inviteAccepted、ApplyInvite 重置 status/existsUid)、无关动作不动、接 Store 派发后归约。 结果:TANKIOFFLINE_STATE_TEST=1 全通过。 --- .../App/State/Reducers/InviteReducer.kt | 67 +++++++++++++++++++ .../app/src/jsMain/kotlin/App/StateCheck.kt | 52 +++++++++++++- .../state/src/jsMain/kotlin/State/Redux.kt | 27 ++++---- 3 files changed, 128 insertions(+), 18 deletions(-) create mode 100644 TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/InviteReducer.kt diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/InviteReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/InviteReducer.kt new file mode 100644 index 0000000..9556637 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/InviteReducer.kt @@ -0,0 +1,67 @@ +// InviteReducer.kt —— 登录邀请切片(对应 module_10032 的 aD 状态 + pD.er5 reducer) +// +// 证据链: +// 状态类 aD:`function aD(t, n, i, r, e) { t = t === cd || t; n = n === cd ? "" : n; +// i = i === cd ? wD() : i; r = r === cd ? null : r; e = e !== cd && e; … }` +// toString:`InviteState(isInviteNeed=…, invite=…, status=…, existsUid=…, inviteAccepted=…)` +// 枚举 lD(cD()):UNKNOWN / FREE / NOT_FOUND / ALREADY_ACTIVATED +// reducer pD.er5: +// SetInviteNeed → copy(inviteNeed) +// SetInviteStatus → copy(cd, cd, inviteStatus, existUid, inviteStatus.equals(FREE)) +// ApplyInvite → copy(cd, invite, UNKNOWN, null) +// 其它 → 原状态 +package App.State.Reducers + +import App.State.ApplyInvite +import App.State.SetInviteNeed +import App.State.SetInviteStatus + +/** 原 `lD`(module_10032 的邀请状态枚举)。 */ +enum class InviteStatus { + UNKNOWN, + FREE, + NOT_FOUND, + ALREADY_ACTIVATED, + ; + + companion object { + /** + * 动作字段在运行期是 JS 枚举对象,toString 就是成员名(`SetInviteStatus(inviteStatus=UNKNOWN…)`)。 + * 认不出时按原逻辑落到默认值 UNKNOWN。 + */ + fun FromText(Text: Any?): InviteStatus = + entries.firstOrNull { it.name == Text?.toString() } ?: UNKNOWN + } +} + +/** + * 原 `aD`:`InviteState(isInviteNeed, invite, status, existsUid, inviteAccepted)`。 + * 字段类型只在有证据处收紧:isInviteNeed / inviteAccepted 由 reducer 逻辑确定为 Boolean, + * invite / existsUid 运行时还没采到,保持宽松。 + */ +data class InviteStateSlice( + val IsInviteNeed: Boolean = true, + val Invite: Any? = "", + val Status: InviteStatus = InviteStatus.UNKNOWN, + val ExistsUid: Any? = null, + val InviteAccepted: Boolean = false, +) + +/** 原 `pD.er5`。 */ +fun ReduceInvite(State0: InviteStateSlice, A: Any): InviteStateSlice = when (A) { + is SetInviteNeed -> State0.copy(IsInviteNeed = A.inviteNeed) + is SetInviteStatus -> { + val Status = InviteStatus.FromText(A.inviteStatus) + State0.copy( + Status = Status, + ExistsUid = A.existUid, + InviteAccepted = Status == InviteStatus.FREE, + ) + } + is ApplyInvite -> State0.copy( + Invite = A.invite, + Status = InviteStatus.UNKNOWN, + ExistsUid = null, + ) + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt b/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt index a9c91c5..63f0099 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt @@ -10,16 +10,21 @@ // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) package App -import State.Action import State.Middleware import State.ReduxGateway import State.ReduxStore import State.StoreListener import App.State.ActionCatalog import App.State.ActionSamples +import App.State.ApplyInvite +import App.State.Reducers.InviteStateSlice +import App.State.Reducers.InviteStatus +import App.State.Reducers.ReduceInvite +import App.State.SetInviteNeed +import App.State.SetInviteStatus -private data class AddFrags(val fragsCount: Int) : Action -private data class SetScreen(val screen: String) : Action +private data class AddFrags(val fragsCount: Int) +private data class SetScreen(val screen: String) private data class UiState(val Frags: Int = 0, val Screen: String = "Preloader") @@ -213,6 +218,47 @@ object StateCheck { Check("网关:全目录 ${ActionCatalog.All.size} 个动作类可注册且只路由命中项", Hits == 1) } + // ⑨ 切片 reducer:InviteState(原 module_10032 的 aD / pD.er5) + run { + val S0 = InviteStateSlice() + Check( + "切片 Invite:默认值与原构造一致", + S0.IsInviteNeed && S0.Invite == "" && S0.Status == InviteStatus.UNKNOWN && + S0.ExistsUid == null && !S0.InviteAccepted + ) + + val S1 = ReduceInvite(S0, SetInviteNeed(false)) + Check("切片 Invite:SetInviteNeed 只改 isInviteNeed", !S1.IsInviteNeed && S1.Status == S0.Status) + + val S2 = ReduceInvite(S1, SetInviteStatus("FREE", 12345L)) + Check( + "切片 Invite:SetInviteStatus 更新 status/existsUid/inviteAccepted", + S2.Status == InviteStatus.FREE && S2.ExistsUid == 12345L && S2.InviteAccepted + ) + + val S3 = ReduceInvite(S2, SetInviteStatus("NOT_FOUND", null)) + Check("切片 Invite:非 FREE 状态 inviteAccepted 归 false", !S3.InviteAccepted && S3.Status == InviteStatus.NOT_FOUND) + + val S4 = ReduceInvite(S3, ApplyInvite("INVITE-CODE")) + Check( + "切片 Invite:ApplyInvite 写入邀请码并重置 status/existsUid", + S4.Invite == "INVITE-CODE" && S4.Status == InviteStatus.UNKNOWN && S4.ExistsUid == null && + !S4.InviteAccepted + ) + + val S5 = ReduceInvite(S4, AddFrags(1)) + Check("切片 Invite:无关动作不改状态", S5 == S4) + + val Store = ReduxStore(S0, { S, A -> ReduceInvite(S, A) }) + var Notified = false + Store.Subscribe(StoreListener { Notified = true }) + Store.Dispatch(SetInviteStatus("ALREADY_ACTIVATED", 7L)) + Check( + "切片 Invite:接 Store 派发后状态已归约", + Store.State.Status == InviteStatus.ALREADY_ACTIVATED && Store.State.ExistsUid == 7L && Notified + ) + } + val Failed = Results.filter { !it.second } println("TankiOffline 状态层自检:${Results.size} 项") for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name) diff --git a/TankiOffline/state/src/jsMain/kotlin/State/Redux.kt b/TankiOffline/state/src/jsMain/kotlin/State/Redux.kt index 1fbdce9..92d0d95 100644 --- a/TankiOffline/state/src/jsMain/kotlin/State/Redux.kt +++ b/TankiOffline/state/src/jsMain/kotlin/State/Redux.kt @@ -9,22 +9,19 @@ // ft/Ct = 派生值(selector)与组件订阅,S3 暂不移植(UI 层才需要) // // 这里的实现保持同样语义: -// · 动作以 Kotlin 类为键(等价原包的构造函数作 Map key) +// · 动作没有公共基类(原包同样没有),以 Kotlin 类作键(等价原包的构造函数作 Map key) // · 中间件顺序与原包一致:列表里靠后的中间件包在更外层 // · 监听器在状态更新之后被通知;网关的按动作监听器在状态更新之前被通知 package State import kotlin.reflect.KClass -/** 动作基类:与原包一致,动作是不可变数据类(Kotlin data class)。 */ -interface Action - /** * 原 module_10849 的 `ct`:`InitState(type, action)`。 * 证据:`(p(ct).toString = ... "InitState(type=" + d(this.zap_1) + ", action=" + d(this.aaq_1) + ")")`。 * 启动时用它把初始状态灌进 Store。 */ -data class InitState(val type: Any?, val action: Any?) : Action +data class InitState(val type: Any?, val action: Any?) /** Store 订阅者:状态变化后回调(对应原 `vt.maq_1` 里那些对象的 `kaq`)。 */ fun interface StoreListener { @@ -33,7 +30,7 @@ fun interface StoreListener { /** 中间件:签名等价原包的 `o(store, next) -> dispatch`。 */ fun interface Middleware { - fun Invoke(Store: ReduxStore, Next: (Action) -> Action): (Action) -> Action + fun Invoke(Store: ReduxStore, Next: (Any) -> Any): (Any) -> Any } /** @@ -45,18 +42,18 @@ fun interface Middleware { */ class ReduxStore( InitialState: S, - private val Reducer: (S, Action) -> S, + private val Reducer: (S, Any) -> S, Middlewares: List> = emptyList(), ) { var State: S = InitialState private set private val Listeners = ArrayList() - private val DispatchChain: (Action) -> Action + private val DispatchChain: (Any) -> Any init { // 原 `vt` 构造里的 invokeReducer:先归约,再逐个通知监听器,最后把动作原样返回 - var Chain: (Action) -> Action = { A -> + var Chain: (Any) -> Any = { A -> State = Reducer(State, A) for (L in Listeners.toList()) L.OnStoreChanged() A @@ -65,7 +62,7 @@ class ReduxStore( DispatchChain = Chain } - fun Dispatch(A: Action): Action = DispatchChain(A) + fun Dispatch(A: Any): Any = DispatchChain(A) fun Subscribe(L: StoreListener) { Listeners.add(L) @@ -82,7 +79,7 @@ class ReduxStore( class ModelActionScope internal constructor(private val Gateway: ReduxGateway<*>) { internal val Removers = ArrayList<() -> Unit>() - fun Listen(Type: KClass<*>, Listener: (Action) -> Unit) { + fun Listen(Type: KClass<*>, Listener: (Any) -> Unit) { Removers.add(Gateway.OnAction(Type, Listener)) } } @@ -99,7 +96,7 @@ class ReduxGateway( private val Store: ReduxStore, private val Scheduler: (() -> Unit) -> Unit, ) { - private val ActionListeners = LinkedHashMap, MutableList<(Action) -> Unit>>() + private val ActionListeners = LinkedHashMap, MutableList<(Any) -> Unit>>() private val ModelScopes = LinkedHashMap Unit>>() private val KeyScopes = LinkedHashMap Unit>>() private val GameObjectScopes = LinkedHashMap Unit>>() @@ -109,19 +106,19 @@ class ReduxGateway( val StoreState: S get() = Store.State /** 原 `uar`:把派发排进调度器(调度器负责错误隔离)。 */ - fun Dispatch(A: Action) { + fun Dispatch(A: Any) { Scheduler { StoreDispatch(A) } } /** 原 `gas` 返回的派发包装:先按动作类通知,再交给 Store。 */ - fun StoreDispatch(A: Action) { + fun StoreDispatch(A: Any) { val List = ActionListeners[A::class] if (List != null) for (L in List.toList()) L(A) Store.Dispatch(A) } /** 注册按动作类的监听器,返回摘除函数(原 `sar` / `tar`)。 */ - fun OnAction(Type: KClass<*>, Listener: (Action) -> Unit): () -> Unit { + fun OnAction(Type: KClass<*>, Listener: (Any) -> Unit): () -> Unit { ActionListeners.getOrPut(Type) { ArrayList() }.add(Listener) return { ActionListeners[Type]?.remove(Listener) } }