@@ -20,25 +20,206 @@ export type MargonemCharacter = {
2020 world ?: string ;
2121} ;
2222
23+ const toNumberOrNull = ( value : unknown ) => {
24+ if ( typeof value === "number" && Number . isFinite ( value ) ) {
25+ return value ;
26+ }
27+
28+ if ( typeof value === "string" && value . trim ( ) !== "" ) {
29+ const parsedValue = Number ( value ) ;
30+
31+ if ( Number . isFinite ( parsedValue ) ) {
32+ return parsedValue ;
33+ }
34+ }
35+
36+ return null ;
37+ } ;
38+
39+ const toStringOrNull = ( value : unknown ) => {
40+ if ( typeof value === "string" ) {
41+ return value ;
42+ }
43+
44+ if ( typeof value === "number" && Number . isFinite ( value ) ) {
45+ return String ( value ) ;
46+ }
47+
48+ return null ;
49+ } ;
50+
51+ const findValueByAliases = (
52+ characterData : Record < string , unknown > ,
53+ aliases : string [ ] ,
54+ ) => {
55+ for ( const alias of aliases ) {
56+ const directValue = characterData [ alias ] ;
57+
58+ if ( directValue !== undefined ) {
59+ return directValue ;
60+ }
61+ }
62+
63+ const loweredAliases = new Set ( aliases . map ( ( alias ) => alias . toLowerCase ( ) ) ) ;
64+
65+ for ( const [ key , value ] of Object . entries ( characterData ) ) {
66+ if ( loweredAliases . has ( key . toLowerCase ( ) ) ) {
67+ return value ;
68+ }
69+ }
70+
71+ return undefined ;
72+ } ;
73+
74+ const unwrapCharacterData = ( characterData : Record < string , unknown > ) => {
75+ const nestedCharacterCandidates = [
76+ characterData . character ,
77+ characterData . char ,
78+ characterData . hero ,
79+ characterData . data ,
80+ characterData . d ,
81+ characterData . value ,
82+ ] ;
83+
84+ for ( const nestedCharacterCandidate of nestedCharacterCandidates ) {
85+ if (
86+ typeof nestedCharacterCandidate === "object" &&
87+ nestedCharacterCandidate !== null &&
88+ ! Array . isArray ( nestedCharacterCandidate )
89+ ) {
90+ return nestedCharacterCandidate as Record < string , unknown > ;
91+ }
92+ }
93+
94+ return characterData ;
95+ } ;
96+
97+ const normalizeCharacter = ( character : unknown ) : MargonemCharacter | null => {
98+ if ( Array . isArray ( character ) ) {
99+ const [ id , nick , world , lvl , prof , gender , icon , last , clan , clan_rank ] =
100+ character ;
101+
102+ return normalizeCharacter ( {
103+ clan,
104+ clan_rank,
105+ gender,
106+ icon,
107+ id,
108+ last,
109+ lvl,
110+ nick,
111+ prof,
112+ world,
113+ } ) ;
114+ }
115+
116+ if ( typeof character !== "object" || character === null ) {
117+ return null ;
118+ }
119+
120+ const characterData = unwrapCharacterData (
121+ character as Record < string , unknown > ,
122+ ) ;
123+ const normalizedId = toNumberOrNull (
124+ findValueByAliases ( characterData , [ "id" , "charId" , "characterId" ] ) ,
125+ ) ;
126+ const normalizedIcon = toStringOrNull (
127+ findValueByAliases ( characterData , [
128+ "icon" ,
129+ "image" ,
130+ "imageUrl" ,
131+ "iconUrl" ,
132+ "avatar" ,
133+ ] ) ,
134+ ) ;
135+ const normalizedLevel = toNumberOrNull (
136+ findValueByAliases ( characterData , [ "lvl" , "level" ] ) ,
137+ ) ;
138+ const normalizedNick = toStringOrNull (
139+ findValueByAliases ( characterData , [ "nick" , "nickname" , "name" ] ) ,
140+ ) ;
141+ const normalizedProfession = toStringOrNull (
142+ findValueByAliases ( characterData , [ "prof" , "profession" , "class" ] ) ,
143+ ) ;
144+ const normalizedWorld = toStringOrNull (
145+ findValueByAliases ( characterData , [
146+ "world" ,
147+ "server" ,
148+ "worldName" ,
149+ "worldname" ,
150+ "serverName" ,
151+ ] ) ,
152+ ) ;
153+
154+ if (
155+ normalizedId === null ||
156+ normalizedIcon === null ||
157+ normalizedLevel === null ||
158+ normalizedNick === null ||
159+ normalizedProfession === null ||
160+ normalizedWorld === null
161+ ) {
162+ return null ;
163+ }
164+
165+ const normalizedClan = toNumberOrNull (
166+ findValueByAliases ( characterData , [ "clan" , "clanId" ] ) ,
167+ ) ;
168+ const normalizedClanRank = toNumberOrNull (
169+ findValueByAliases ( characterData , [ "clan_rank" , "clanRank" ] ) ,
170+ ) ;
171+ const normalizedLast = toNumberOrNull (
172+ findValueByAliases ( characterData , [ "last" , "lastSeen" , "lastLogin" ] ) ,
173+ ) ;
174+ const rawGender = findValueByAliases ( characterData , [ "gender" , "sex" ] ) ;
175+ const normalizedGender =
176+ rawGender === "m" || rawGender === "f" ? rawGender : undefined ;
177+
178+ return {
179+ clan : normalizedClan ?? undefined ,
180+ clan_rank : normalizedClanRank ?? undefined ,
181+ gender : normalizedGender ,
182+ icon : normalizedIcon ,
183+ id : normalizedId ,
184+ last : normalizedLast ?? undefined ,
185+ lvl : normalizedLevel ,
186+ nick : normalizedNick ,
187+ prof : normalizedProfession ,
188+ world : normalizedWorld ,
189+ } ;
190+ } ;
191+
23192export const normalizeCharacterList = (
24193 characters : unknown ,
25194) : MargonemCharacter [ ] => {
26195 if ( ! Array . isArray ( characters ) ) {
27196 return [ ] ;
28197 }
29198
30- return characters . filter ( ( character ) : character is MargonemCharacter => {
31- return (
32- typeof character === "object" &&
33- character !== null &&
34- typeof character . id === "number" &&
35- typeof character . icon === "string" &&
36- typeof character . lvl === "number" &&
37- typeof character . nick === "string" &&
38- typeof character . prof === "string" &&
39- typeof character . world === "string"
199+ return characters . reduce < MargonemCharacter [ ] > (
200+ ( normalizedCharacters , character ) => {
201+ const normalizedCharacter = normalizeCharacter ( character ) ;
202+
203+ if ( normalizedCharacter ) {
204+ normalizedCharacters . push ( normalizedCharacter ) ;
205+ }
206+
207+ return normalizedCharacters ;
208+ } ,
209+ [ ] ,
210+ ) ;
211+ } ;
212+
213+ const filterCharactersByWorld = (
214+ characters : MargonemCharacter [ ] ,
215+ world : string | undefined ,
216+ ) => {
217+ return characters
218+ . filter ( ( character ) => character . world === world )
219+ . sort (
220+ ( firstCharacter , secondCharacter ) =>
221+ secondCharacter . lvl - firstCharacter . lvl ,
40222 ) ;
41- } ) ;
42223} ;
43224
44225type FetchCharacterListOptions = {
@@ -53,21 +234,32 @@ export async function fetchCharacterList({
53234 languageVersion,
54235} : FetchCharacterListOptions ) : Promise < MargonemCharacter [ ] > {
55236 const margonemEntry = window . localStorage ?. getItem ( "Margonem" ) ;
56- const parsed = margonemEntry ? JSON . parse ( margonemEntry ) : null ;
237+ const accountIdKey = String ( accountId ) ;
238+
239+ let parsed : unknown = null ;
57240
241+ if ( margonemEntry ) {
242+ try {
243+ parsed = JSON . parse ( margonemEntry ) ;
244+ } catch ( error ) {
245+ throw error ;
246+ }
247+ }
248+
249+ // @ts -expect-error `get` accepts runtime data here; `parsed` intentionally stays `unknown`.
58250 const charlist = get ( parsed , "charlist" , null ) as Record <
59251 string ,
60252 MargonemCharacter [ ]
61253 > | null ;
254+ const rawCachedCharacters = accountId
255+ ? ( charlist ?. [ accountIdKey ] ?? null )
256+ : null ;
62257
63- const cached = accountId
64- ? normalizeCharacterList ( charlist ?. [ accountId ] ?? null )
65- : [ ] ;
258+ const cached = accountId ? normalizeCharacterList ( rawCachedCharacters ) : [ ] ;
259+ const filteredCached = filterCharactersByWorld ( cached , world ) ;
66260
67- if ( cached . length > 0 ) {
68- return cached
69- . filter ( ( character ) => character . world === world )
70- . sort ( ( a , b ) => b . lvl - a . lvl ) ;
261+ if ( filteredCached . length > 0 ) {
262+ return filteredCached ;
71263 }
72264
73265 const hs3 = window . getCookie ?.( "hs3" ) ;
@@ -84,12 +276,15 @@ export async function fetchCharacterList({
84276 const response = await client . get < MargonemCharacter [ ] > ( `${ url } ?hs3=${ hs3 } ` , {
85277 withCredentials : true ,
86278 } ) ;
279+ const normalizedCharacters = normalizeCharacterList ( response . data ) ;
280+ const filteredCharacters = filterCharactersByWorld (
281+ normalizedCharacters ,
282+ world ,
283+ ) ;
87284
88285 if ( ! response . data || response . data . length === 0 ) {
89286 throw new Error ( "Empty character list received from API" ) ;
90287 }
91288
92- return normalizeCharacterList ( response . data )
93- . filter ( ( character ) => character . world === world )
94- . sort ( ( a , b ) => b . lvl - a . lvl ) ;
289+ return filteredCharacters ;
95290}
0 commit comments