1+ using System . Collections . Concurrent ;
2+ using System . Globalization ;
13using DirectorPrompt . Domain . Configurations ;
24using DirectorPrompt . Domain . Enums ;
35using DirectorPrompt . Domain . Models ;
@@ -14,6 +16,8 @@ public sealed class SystemStateTransformer
1416 IConditionEngine conditionEngine
1517) : ISystemStateTransformer
1618{
19+ private readonly ConcurrentDictionary < ( long SessionID , long AttributeID , long ? CharacterID , string Option ) , bool > onceTriggered = new ( ) ;
20+
1721 public async Task ExecuteAsync
1822 (
1923 long projectID ,
@@ -33,11 +37,11 @@ public async Task ExecuteAsync
3337 ) ;
3438
3539 var attributes = await stateRepository . GetAttributesAsync ( projectID , null , cancellationToken ) ;
36- var systemAttrs = attributes . Where ( a => a . Driver == Driver . System ) . ToList ( ) ;
40+ var systemAttrs = attributes . Where ( a => a . Driver == Driver . System || a . ValueType == StateValueType . Enum ) . ToList ( ) ;
3741
3842 if ( systemAttrs . Count == 0 )
3943 {
40- Log . Debug ( "无 system 驱动的状态属性 , 跳过" ) ;
44+ Log . Debug ( "无系统驱动的状态属性 , 跳过" ) ;
4145 return ;
4246 }
4347
@@ -78,24 +82,24 @@ private async Task TransformGlobalAttributeAsync
7882 CancellationToken cancellationToken
7983 )
8084 {
81- if ( attr . ValueType == StateValueType . Enum )
82- {
83- var value = await stateRepository . GetStateValueAsync ( attr . ID , sessionID , cancellationToken ) ;
84- var currentValue = value ? . Value ?? string . Empty ;
85+ if ( attr . ValueType != StateValueType . Enum )
86+ return ;
8587
86- await TransformEnumAttributeAsync
87- (
88- attr ,
89- sessionID ,
90- sceneID ,
91- roundID ,
92- trigger ,
93- currentValue ,
94- globalStateValues ,
95- null ,
96- cancellationToken
97- ) ;
98- }
88+ var value = await stateRepository . GetStateValueAsync ( attr . ID , sessionID , cancellationToken ) ;
89+ var currentValue = value ? . Value ?? string . Empty ;
90+
91+ await TransformEnumAttributeAsync
92+ (
93+ attr ,
94+ sessionID ,
95+ sceneID ,
96+ roundID ,
97+ trigger ,
98+ currentValue ,
99+ globalStateValues ,
100+ null ,
101+ cancellationToken
102+ ) ;
99103 }
100104
101105 private async Task TransformCategoryAttributeAsync
@@ -111,6 +115,9 @@ private async Task TransformCategoryAttributeAsync
111115 CancellationToken cancellationToken
112116 )
113117 {
118+ if ( attr . ValueType != StateValueType . Enum )
119+ return ;
120+
114121 foreach ( var character in characters )
115122 {
116123 var charValues = await characterRepository . GetCharacterStateValuesAsync ( character . ID , cancellationToken ) ;
@@ -122,23 +129,20 @@ CancellationToken cancellationToken
122129 v => v . Value
123130 ) ;
124131
125- if ( attr . ValueType == StateValueType . Enum )
126- {
127- var currentValue = charValues . FirstOrDefault ( v => v . AttributeID == attr . ID ) ? . Value ?? string . Empty ;
128-
129- await TransformEnumAttributeAsync
130- (
131- attr ,
132- sessionID ,
133- sceneID ,
134- roundID ,
135- trigger ,
136- currentValue ,
137- charContext ,
138- character . ID ,
139- cancellationToken
140- ) ;
141- }
132+ var currentValue = charValues . FirstOrDefault ( v => v . AttributeID == attr . ID ) ? . Value ?? string . Empty ;
133+
134+ await TransformEnumAttributeAsync
135+ (
136+ attr ,
137+ sessionID ,
138+ sceneID ,
139+ roundID ,
140+ trigger ,
141+ currentValue ,
142+ charContext ,
143+ character . ID ,
144+ cancellationToken
145+ ) ;
142146 }
143147 }
144148
@@ -166,7 +170,7 @@ CancellationToken cancellationToken
166170 if ( string . IsNullOrEmpty ( currentValue ) )
167171 currentValue = config . Options . FirstOrDefault ( ) ?? string . Empty ;
168172
169- var newValue = ResolveEnumTransition ( currentValue , config , stateValues ) ;
173+ var newValue = ResolveEnumTransition ( attr , sessionID , characterID , currentValue , config , stateValues ) ;
170174
171175 if ( newValue == currentValue )
172176 return ;
@@ -200,47 +204,142 @@ await stateRepository.SetStateValueAsync
200204
201205 private string ResolveEnumTransition
202206 (
207+ StateAttribute attr ,
208+ long sessionID ,
209+ long ? characterID ,
203210 string currentValue ,
204211 EnumAttributeConfig config ,
205212 Dictionary < string , string > stateValues
206213 )
207214 {
208- if ( config . Conditions . Count > 0 )
215+ if ( config . Transitions . Count == 0 )
216+ return currentValue ;
217+
218+ var alwaysMet = new List < ( string Option , float Weight ) > ( ) ;
219+
220+ foreach ( var t in config . Transitions )
209221 {
210- var context = new ConditionContext ( stateValues ) ;
222+ if ( t . Method != EnumTransitionMethod . Expression || t . SwitchMode != EnumSwitchMode . Always )
223+ continue ;
211224
212- foreach ( var cond in config . Conditions )
225+ if ( EvaluateTransitionExpression ( t , stateValues ) )
226+ alwaysMet . Add ( ( t . Option , t . Weight ) ) ;
227+ }
228+
229+ if ( alwaysMet . Count > 0 )
230+ {
231+ var best = alwaysMet . MaxBy ( x => x . Weight ) ;
232+ return best . Option ;
233+ }
234+
235+ var onceFirstTime = new List < ( string Option , float Weight ) > ( ) ;
236+
237+ foreach ( var t in config . Transitions )
238+ {
239+ if ( t . Method != EnumTransitionMethod . Expression || t . SwitchMode != EnumSwitchMode . Once )
240+ continue ;
241+
242+ var key = ( sessionID , attr . ID , characterID , t . Option ) ;
243+ var isMet = EvaluateTransitionExpression ( t , stateValues ) ;
244+
245+ if ( ! isMet )
213246 {
214- if ( conditionEngine . Evaluate ( cond . When , context ) )
215- return PickWeighted ( cond . Transition ) ;
247+ onceTriggered [ key ] = false ;
248+ continue ;
216249 }
250+
251+ if ( onceTriggered . TryGetValue ( key , out var triggered ) && triggered )
252+ continue ;
253+
254+ onceTriggered [ key ] = true ;
255+ onceFirstTime . Add ( ( t . Option , t . Weight ) ) ;
217256 }
218257
219- if ( config . TransitionRules . TryGetValue ( currentValue , out var rules ) )
220- return PickWeighted ( rules ) ;
258+ if ( onceFirstTime . Count > 0 )
259+ {
260+ var best = onceFirstTime . MaxBy ( x => x . Weight ) ;
261+ return best . Option ;
262+ }
263+
264+ var pool = new List < ( string Option , float Weight ) > ( ) ;
265+
266+ foreach ( var t in config . Transitions )
267+ {
268+ switch ( t . Method )
269+ {
270+ case EnumTransitionMethod . Random :
271+ pool . Add ( ( t . Option , t . Weight ) ) ;
272+ break ;
273+
274+ case EnumTransitionMethod . Expression when t . SwitchMode == EnumSwitchMode . Once :
275+ {
276+ var key = ( sessionID , attr . ID , characterID , t . Option ) ;
277+ var isMet = EvaluateTransitionExpression ( t , stateValues ) ;
278+
279+ if ( isMet && onceTriggered . TryGetValue ( key , out var triggered ) && triggered )
280+ pool . Add ( ( t . Option , t . Weight ) ) ;
281+
282+ break ;
283+ }
284+ }
285+ }
286+
287+ if ( pool . Count > 0 )
288+ return PickWeighted ( pool ) ;
221289
222290 return currentValue ;
223291 }
224292
225- private static string PickWeighted ( Dictionary < string , float > weights )
293+ private bool EvaluateTransitionExpression
294+ (
295+ EnumTransitionConfig transition ,
296+ Dictionary < string , string > stateValues
297+ )
226298 {
227- var total = weights . Values . Sum ( ) ;
299+ if ( string . IsNullOrWhiteSpace ( transition . Expression ) || string . IsNullOrWhiteSpace ( transition . AttributeName ) )
300+ return false ;
301+
302+ if ( ! stateValues . TryGetValue ( transition . AttributeName , out var value ) )
303+ return false ;
304+
305+ var isNumeric = float . TryParse ( value , NumberStyles . Float , CultureInfo . InvariantCulture , out _ ) ;
306+ var valReplacement = isNumeric ?
307+ value :
308+ $ "\" { value } \" ";
309+
310+ var expr = transition . Expression . Replace ( "{val}" , valReplacement ) ;
311+ expr = expr . Replace ( " AND " , " && " ) . Replace ( " OR " , " || " ) ;
312+
313+ try
314+ {
315+ return conditionEngine . Evaluate ( expr , new ConditionContext ( new Dictionary < string , string > ( ) ) ) ;
316+ }
317+ catch ( Exception ex )
318+ {
319+ Log . Warning ( ex , "表达式求值失败: {Expression}" , transition . Expression ) ;
320+ return false ;
321+ }
322+ }
323+
324+ private static string PickWeighted ( List < ( string Option , float Weight ) > pool )
325+ {
326+ var total = pool . Sum ( x => x . Weight ) ;
228327
229328 if ( total <= 0 )
230- return weights . Keys . FirstOrDefault ( ) ?? string . Empty ;
329+ return pool [ 0 ] . Option ;
231330
232331 var roll = ( float ) Random . Shared . NextDouble ( ) * total ;
233332 var cumulative = 0f ;
234333
235- foreach ( var ( key , weight ) in weights )
334+ foreach ( var ( option , weight ) in pool )
236335 {
237336 cumulative += weight ;
238337
239338 if ( roll <= cumulative )
240- return key ;
339+ return option ;
241340 }
242341
243- return weights . Keys . Last ( ) ;
342+ return pool [ ^ 1 ] . Option ;
244343 }
245344
246345 private async Task < Dictionary < string , string > > BuildGlobalStateContextAsync
@@ -265,11 +364,7 @@ private static bool IsTriggerMatch(SystemTrigger configTrigger, SystemTrigger ac
265364 configTrigger == actualTrigger ;
266365
267366 private static SystemTrigger ParseTrigger ( string ? value ) =>
268- value switch
269- {
270- "scene_change" => SystemTrigger . SceneChange ,
271- "round_end" => SystemTrigger . RoundEnd ,
272- "custom" => SystemTrigger . Custom ,
273- _ => SystemTrigger . RoundEnd
274- } ;
367+ Enum . TryParse ( value , true , out SystemTrigger trigger ) ?
368+ trigger :
369+ SystemTrigger . RoundEnd ;
275370}
0 commit comments