')
195 | print(f'{Fore.GREEN}We create base-backups on every login and further backups every time you start or choose so manually.')
196 | print(f'{Fore.GREEN}When changes do not seem to apply, refresh without cache / use a private tab.')
197 | print(f'{Fore.GREEN}Otherwise please visit {repoURL} and report the issue.')
198 | print(f'{Fore.YELLOW}Please refer to SECURITY.md if you have security concerns.')
199 | print('------------------------------------------------------------')
200 | print(f'{Fore.MAGENTA}{Style.BRIGHT}1: Using no browser with requests.')
201 | print(f'{Fore.MAGENTA}{Style.BRIGHT}2: Using own browser with requests.')
202 | print(f'{Fore.MAGENTA}{Style.BRIGHT}3: Using own browser with JavaScript.')
203 | print(f'{Fore.MAGENTA}{Style.BRIGHT}4: Just edit an existing trainer.json')
204 |
205 | def f_printHelp() -> None:
206 | """
207 | Print helpful information for the user.
208 |
209 | This function prints various helpful messages for the user, including information
210 | about manual JSON editing, assistance through the program's GitHub page, release
211 | version details, and cautions about account safety and program authenticity.
212 |
213 | Usage Example:
214 | >>> print_help()
215 |
216 | Modules/Librarys used and for what purpose exactly in each function:
217 | - utilities.cFormatter: Prints colored console output for help messages.
218 | """
219 | print(f'{Fore.YELLOW}You can always edit your JSON manually as well.')
220 | print(f'{Fore.YELLOW}If you need assistance, please refer to the program\'s GitHub page.')
221 | print(f'{Fore.YELLOW}{repoURL}')
222 | print(f'{Fore.YELLOW}This is release version {version} - please include that in your issue or question report.')
223 | print(f'{Fore.YELLOW}This version now also features a log file.')
224 | print(f'{Fore.YELLOW}We do not take responsibility if your accounts get flagged or banned, and')
225 | print(f'{Fore.YELLOW}you never know if there is a clone of this program. If you are not sure, please')
226 | print(f'{Fore.YELLOW}calculate the checksum of this binary and visit {repoURL}')
227 | print(f'{Fore.YELLOW}to see the value it should have to know it\'s original from source.')
228 |
229 | def f_anonymizeName(username):
230 | if len(username) < 3: # If username length is less than 3, return as is (minimum 2 characters)
231 | return username
232 |
233 | visibleChars = max(int(len(username) * 0.2), 1) # Calculate how many characters to leave visible
234 | startVisible = max(visibleChars // 2, 1) # At least 1 character visible from the start
235 | endVisible = visibleChars - startVisible # Remaining visible characters from the end
236 |
237 | # Construct the masked username
238 | maskedUsername = username[:startVisible] + '*' * (len(username) - startVisible - endVisible)
239 |
240 | return maskedUsername
241 |
--------------------------------------------------------------------------------
/src/modules/data/__init__.py:
--------------------------------------------------------------------------------
1 | from . import dataParser
2 |
3 | __all__ = [
4 | 'dataParser'
5 | ]
--------------------------------------------------------------------------------
/src/modules/data/dataParser.py:
--------------------------------------------------------------------------------
1 | # Authors https://github.com/JulianStiebler/
2 | # Organization: https://github.com/rogueEdit/
3 | # Repository: https://github.com/rogueEdit/OnlineRogueEditor
4 | # Contributors: None except Authors
5 | # Date of release: 06.06.2024
6 | # Last Edited: 28.06.2024
7 |
8 | # Unlike the other code, reusing this in your own project is forbidden.
9 |
10 | import json
11 | from enum import Enum
12 | from dataclasses import dataclass, field, asdict
13 | from typing import List, Optional, Any, Dict, Union
14 | from modules.config import dataDirectory
15 | from colorama import Fore, Style
16 |
17 | def __createEnum(name, values):
18 | return Enum(name, values)
19 |
20 | def __modifiySpeciesName(name):
21 | return name.replace('_', ' ').title()
22 |
23 | with open(f'{dataDirectory}/hasForms.json') as f:
24 | hasForms = json.load(f)["hasForms"]
25 |
26 | with open(f'{dataDirectory}/species.json') as f:
27 | dexEnum = json.load(f)["dex"]
28 |
29 | with open(f'{dataDirectory}/noPassive.json') as f:
30 | noPassive = json.load(f)["noPassive"]
31 |
32 | with open(f'{dataDirectory}/starter.json') as f:
33 | startersList = json.load(f)["dex"]
34 |
35 | Dex = __createEnum('Dex', {name.capitalize(): id_ for name, id_ in dexEnum.items()})
36 | noPassiveSet = {int(id_) for id_ in noPassive.keys()}
37 | startersSet = {dexEnum[name] for name in startersList}
38 |
39 | class DexAttr(Enum):
40 | NON_SHINY = 1
41 | SHINY = 2
42 | MALE = 4
43 | FEMALE = 8
44 | VARIANT_1 = 16
45 | VARIANT_2 = 32
46 | VARIANT_3 = 64
47 | DEFAULT_FORM = 128
48 |
49 | @dataclass
50 | class Modifier:
51 | args: List[Optional[Union[int, bool]]]
52 | className: Optional[str]
53 | player: Optional[bool]
54 | stackCount: Optional[int]
55 | typeId: Optional[str]
56 | typePregenArgs: Optional[List[Optional[int]]]
57 |
58 | def toDict(self):
59 | return asdict(self)
60 |
61 | @dataclass
62 | class ModifierEnemyData:
63 | modifiers: List[Optional[Modifier]]
64 |
65 | def toDict(self):
66 | return asdict(self)
67 |
68 | @dataclass
69 | class ModifierPlayerData:
70 | modifiers: List[Optional[Modifier]]
71 |
72 | @dataclass
73 | class Move:
74 | moveId: Optional[int] = None
75 | ppUp: Optional[int] = None
76 | ppUsed: Optional[int] = None
77 | virtual: Optional[bool] = None
78 |
79 | def toDict(self):
80 | return asdict(self)
81 |
82 | @dataclass
83 | class PartySummonData:
84 | abilitiesApplied: Optional[Any] = None
85 | ability: Optional[int] = None
86 | abilitySuppressed: Optional[bool] = None
87 | battleStats: List[Optional[int]] = field(default_factory=lambda: [None, None, None, None, None, None, None])
88 | disabledMove: Optional[int] = None
89 | disabledTurns: Optional[int] = None
90 | moveQueue: Optional[Any] = None
91 | tags: Optional[Any] = None
92 | types: Optional[Any] = None
93 |
94 | def toDict(self):
95 | return asdict(self)
96 |
97 | @dataclass
98 | class PartyDetails:
99 | abilityIndex: Optional[int] = None
100 | boss: Optional[bool] = None
101 | exp: Optional[int] = None
102 | formIndex: Optional[int] = None
103 | friendship: Optional[int] = None
104 | fusionFormIndex: Optional[int] = None
105 | fusionLuck: Optional[int] = None
106 | fusionShiny: Optional[int] = None
107 | fusionSpecies: Optional[int] = None
108 | fusionVariant: Optional[int] = None
109 | gender: Optional[int] = None
110 | hp: Optional[int] = None
111 | id: Optional[int] = None
112 | ivs: List[Optional[int]] = field(default_factory=lambda: [None, None, None, None, None, None])
113 | level: Optional[int] = None
114 | levelExp: Optional[int] = None
115 | luck: Optional[int] = None
116 | metBiome: Optional[int] = None
117 | metLevel: Optional[int] = None
118 | moveset: List[Move] = field(default_factory=lambda: [Move(), Move(), Move(), Move()])
119 | nature: Optional[int] = None
120 | natureOverride: Optional[int] = None
121 | passive: Optional[bool] = None
122 | pauseEvolutions: Optional[bool] = None
123 | player: Optional[bool] = None
124 | pokeball: Optional[int] = None
125 | pokerus: Optional[bool] = None
126 | shiny: Optional[bool] = None
127 | species: Optional[int] = None
128 | stats: List[Optional[int]] = field(default_factory=lambda: [None, None, None, None, None, None])
129 | summonData: PartySummonData = field(default_factory=PartySummonData)
130 | variant: Optional[int] = None
131 |
132 | def toDict(self):
133 | return asdict(self)
134 |
135 | @dataclass
136 | class PartyPlayer:
137 | partyInfo: Optional[PartyDetails] = field(default_factory=PartyDetails)
138 | partyData: Optional[PartySummonData] = field(default_factory=PartySummonData)
139 |
140 | def toDict(self):
141 | return asdict(self)
142 |
143 | @dataclass
144 | class PartyEnemy:
145 | partyInfo: Optional[PartyDetails] = field(default_factory=PartyDetails)
146 | partyData: Optional[PartySummonData] = field(default_factory=PartySummonData)
147 |
148 | def toDict(self):
149 | return asdict(self)
150 |
151 | @dataclass
152 | class SpeciesDexData:
153 | seenAttr: int = 0
154 | caughtAttr: int = 0
155 | natureAttr: int = 0
156 | seenCount: int = 0
157 | caughtCount: int = 0
158 | hatchedCount: int = 0
159 | ivs: List[int] = field(default_factory=lambda: [0, 0, 0, 0, 0, 0])
160 |
161 | def toDict(self):
162 | return asdict(self)
163 |
164 | @dataclass
165 | class SpeciesStarterData:
166 | moveset: Optional[List[int]] = field(default_factory=lambda: [])
167 | eggMoves: Optional[int] = None
168 | candyCount: Optional[int] = 0
169 | friendship: Optional[int] = 0
170 | abilityAttr: Optional[int] = 0
171 | passiveAttr: Optional[int] = 0
172 | valueReduction: Optional[int] = 0
173 | classicWinCount: Optional[int] = 0
174 |
175 | def toDict(self):
176 | return asdict(self)
177 |
178 | @dataclass
179 | class SpeciesForm:
180 | name: str
181 | variant1: Optional[int] = None
182 | variant2: Optional[int] = None
183 | variant3: Optional[int] = None
184 | nonShiny: Optional[int] = None
185 | index: int = 0
186 |
187 | def toDict(self):
188 | return asdict(self)
189 |
190 | @dataclass
191 | class Species:
192 | name: str
193 | dex: int
194 | forms: List[SpeciesForm]
195 | hasPassive: bool
196 | isStarter: bool
197 | isNormalForm: bool
198 |
199 | # User Data
200 | # put starterData and dexData here
201 |
202 | def __post_init__(self):
203 | self.formMap = {form.name: form for form in self.forms}
204 |
205 | def getFormAttribute(self, formName: str, attribute: str) -> Optional[int]:
206 | form = self.formMap.get(formName)
207 | if form:
208 | return getattr(form, attribute, None)
209 | else:
210 | return None
211 |
212 | def toDict(self):
213 | return asdict(self)
214 |
215 | def computeVariant(speciesData, variantFlag, defaultFlag, variantAdjustment):
216 | caughtAttr = {}
217 |
218 | for speciesID, speciesInfo in speciesData.items():
219 | caughtAttr[speciesID] = {}
220 |
221 | for speciesName, forms in speciesInfo.items():
222 | if speciesName == "isNormalForm":
223 | continue
224 |
225 | formAttributes = {}
226 | combinedCaughtAttr = {
227 | "variant1": 31,
228 | "variant2": 63,
229 | "variant3": 127,
230 | "nonShiny": 255
231 | }
232 |
233 | for index, formName in enumerate(forms):
234 | formFlag = defaultFlag | (1 << (index + 7))
235 | formCaughtAttr = 255 | formFlag | variantFlag
236 |
237 | adjustedFormCaughtAttr = formCaughtAttr - variantAdjustment
238 | formAttributes[formName] = adjustedFormCaughtAttr
239 |
240 | combinedCaughtAttr["variant1"] |= adjustedFormCaughtAttr
241 | combinedCaughtAttr["variant2"] |= adjustedFormCaughtAttr
242 | combinedCaughtAttr["variant3"] |= adjustedFormCaughtAttr
243 | combinedCaughtAttr["nonShiny"] |= adjustedFormCaughtAttr
244 |
245 | caughtAttr[speciesID][speciesName] = formAttributes
246 |
247 | caughtAttr[speciesID]["Combined"] = {
248 | "variant1": combinedCaughtAttr["variant1"] + 128,
249 | "variant2": combinedCaughtAttr["variant2"] + 128,
250 | "variant3": combinedCaughtAttr["variant3"] + 128,
251 | "nonShiny": combinedCaughtAttr["nonShiny"] + 128
252 | }
253 |
254 | return caughtAttr
255 |
256 | variant1CaughtAttr = computeVariant(hasForms, DexAttr.VARIANT_1.value, DexAttr.DEFAULT_FORM.value, 224)
257 | variant2CaughtAttr = computeVariant(hasForms, DexAttr.VARIANT_2.value, DexAttr.DEFAULT_FORM.value, 192)
258 | variant3CaughtAttr = computeVariant(hasForms, DexAttr.VARIANT_3.value, DexAttr.DEFAULT_FORM.value, 128)
259 | nonShinyCaughtAttr = computeVariant(hasForms, DexAttr.NON_SHINY.value, DexAttr.DEFAULT_FORM.value, 255)
260 |
261 | specieses = []
262 | speciesDict = {}
263 |
264 | for speciesName, speciesId in dexEnum.items():
265 | modifiedName = __modifiySpeciesName(speciesName)
266 | hasPassive = int(speciesId) not in noPassiveSet
267 | isStarter = speciesId in startersSet
268 | speciesIdString = str(speciesId)
269 |
270 | forms = []
271 | isNormalForm = False
272 | if speciesIdString in hasForms:
273 | formNames = hasForms[speciesIdString].get(modifiedName, [])
274 | isNormalForm = hasForms[speciesIdString].get("isNormalForm", False)
275 | combinedCaughtAttr = {
276 | "variant1": 31,
277 | "variant2": 63,
278 | "variant3": 127,
279 | "nonShiny": 255
280 | }
281 | for index, formName in enumerate(formNames):
282 | form = SpeciesForm(
283 | name=formName,
284 | variant1=variant1CaughtAttr[speciesIdString][modifiedName][formName],
285 | variant2=variant2CaughtAttr[speciesIdString][modifiedName][formName],
286 | variant3=variant3CaughtAttr[speciesIdString][modifiedName][formName],
287 | nonShiny=nonShinyCaughtAttr[speciesIdString][modifiedName][formName],
288 | index=index
289 | )
290 | forms.append(form)
291 | combinedCaughtAttr["variant1"] |= form.variant1
292 | combinedCaughtAttr["variant2"] |= form.variant2
293 | combinedCaughtAttr["variant3"] |= form.variant3
294 | combinedCaughtAttr["nonShiny"] |= form.nonShiny
295 |
296 | # Add the imaginary "Combined" form with proper variant values
297 | forms.append(SpeciesForm(
298 | name="Combined",
299 | variant1=combinedCaughtAttr["variant1"] + 128,
300 | variant2=combinedCaughtAttr["variant2"] + 128,
301 | variant3=combinedCaughtAttr["variant3"] + 128,
302 | nonShiny=combinedCaughtAttr["nonShiny"] + 128,
303 | index=len(forms)
304 | ))
305 |
306 | species = Species(
307 | name=modifiedName,
308 | dex=int(speciesId),
309 | forms=forms,
310 | hasPassive=hasPassive,
311 | isStarter=isStarter,
312 | isNormalForm=isNormalForm
313 | )
314 | specieses.append(species)
315 | speciesDict[speciesName] = species
316 | speciesDict[speciesId] = species
317 |
318 | @dataclass
319 | class SessionData:
320 | seed: Optional[str] = None
321 | playTime: Optional[int] = 0
322 | gameMode: Optional[int] = 0
323 | party: Optional[PartyPlayer] = field(default_factory=PartyPlayer)
324 | enemyParty: Optional[PartyEnemy] = field(default_factory=PartyEnemy)
325 | playerModifier: Optional[ModifierPlayerData] = field(default_factory=ModifierPlayerData)
326 | enemyModifier: Optional[ModifierEnemyData] = field(default_factory=ModifierEnemyData)
327 | arena: Optional[Dict[str, Union[int, None]]] = field(default_factory=lambda: {"biome": 0, "tags": None})
328 | pokeballCounts: Optional[Dict[str, Optional[int]]] = field(default_factory=lambda: {"0": 5, "1": 0, "2": 0, "3": 0, "4": 0})
329 | money: Optional[int] = None
330 | score: Optional[int] = None
331 | victoryCount: Optional[int] = None
332 | faintCount: Optional[int] = None
333 | reviveCount: Optional[int] = None
334 | waveIndex: Optional[int] = None
335 | battleType: Optional[int] = None
336 | trainer: Optional[int] = None
337 | gameVersion: Optional[str] = None
338 | timestamp: Optional[int] = None
339 | challenges: Optional[Dict[str, None]] = None
340 |
341 | def toDict(self):
342 | return asdict(self)
343 |
344 |
345 | @dataclass
346 | class TrainerData:
347 | trainerId: Optional[int] = None
348 | secretId: Optional[int] = None
349 | gender: Optional[int] = None
350 | dexData: Optional[SpeciesDexData] = field(default_factory=SpeciesDexData)
351 | starterData: Optional[SpeciesStarterData] = field(default_factory=SpeciesStarterData)
352 | starterMoveData: Optional[List[int]] = field(default_factory=lambda: [])
353 | starterEggMoveData: Optional[List[int]] = field(default_factory=lambda: [])
354 | gameStats: Optional[Dict[str, Optional[int]]] = None
355 | unlocks: Optional[Dict[str, Optional[bool]]] = None
356 | achvUnlocks: Optional[Dict[str, Optional[int]]] = None
357 | voucherUnlocks: Optional[Dict[str, Optional[int]]] = None
358 | voucherCounts: Optional[Dict[str, Optional[int]]] = None
359 | eggs: Optional[Dict[str, None]] = None
360 | eggPity: Optional[List[int]] = field(default_factory=lambda: [0, 0, 0, 0])
361 | unlockPity: Optional[List[int]] = field(default_factory=lambda: [0, 0, 0, 0])
362 | gameVersion: Optional[int] = None
363 | timestamp: Optional[int] = None
364 |
365 | def toDict(self):
366 | return asdict(self)
367 |
368 |
369 |
370 | def fh_getCombinedIDs(includeStarter=True, onlyNormalForms=True):
371 | combinedFormIds = []
372 |
373 | for species in specieses:
374 | if (includeStarter or not species.isStarter) and (not onlyNormalForms or species.isNormalForm):
375 | for form in species.forms:
376 | if form.name == "Combined":
377 | combinedFormIds.append({
378 | "speciesID": species.dex,
379 | "speciesName": species.name,
380 | "formName": form.name,
381 | "caughtAttr": form.variant3,
382 | "formIndex": form.index
383 | })
384 |
385 | return combinedFormIds
386 |
387 | @staticmethod
388 | def data_iterateParty(slotData, speciesNameByIDHelper, moveNamesByIDHelper, natureNamesByIDHelper):
389 | currentParty = []
390 | pokeballList = {
391 | 0: "Pokeball",
392 | 1: "Great Ball",
393 | 2: "Hyper Ball",
394 | 3: "Rogue Ball",
395 | 4: "Master Ball",
396 | 5: "Luxury Ball"
397 | }
398 | for object in slotData.get("party", []): # Use .get() to avoid KeyError if "party" doesn't exist
399 | # Define IDs and indices
400 | speciesDexID = str(object.get('species', 1))
401 | speciesFusionID = str(object.get('fusionSpecies', 0))
402 | speciesFormIndex = int(object.get('formIndex', 0))
403 | speciesFusionFormIndex = int(object.get('fusionFormIndex', 0))
404 |
405 | # Get base names
406 | speciesDexName = speciesNameByIDHelper.get(speciesDexID, f'Unknown Dex ID {speciesDexID}')
407 | speciesFusionName = speciesNameByIDHelper.get(speciesFusionID, f'Unknown Fuse ID {speciesFusionID}')
408 |
409 | # Modify names based on form index
410 | if speciesFormIndex > 0 and int(speciesDexID) in speciesDict and len(speciesDict[int(speciesDexID)].forms) > speciesFormIndex:
411 | speciesFormName = speciesDict[int(speciesDexID)].forms[speciesFormIndex].name
412 | speciesDexName = f'{speciesFormName} {speciesDexName}'
413 |
414 | if speciesFusionFormIndex > 0 and speciesFusionID != '0' and int(speciesFusionID) in speciesDict and len(speciesDict[int(speciesFusionID)].forms) > speciesFusionFormIndex:
415 | speciesFusionFormName = speciesDict[int(speciesFusionID)].forms[speciesFusionFormIndex].name
416 | speciesFusionName = f'{speciesFusionFormName} {speciesFusionName}'
417 |
418 | # General fusion info
419 | speciesFusionLuck = object.get('fusionLuck', None)
420 | speciesFusionisShiny = object.get('fusionShiny', None)
421 | speciesFusionVariant = object.get('fusionVariant', None)
422 |
423 | # General species info
424 | speciesIsShiny = object.get('shiny', False)
425 | speciesShinyVariant = object.get('variant', 0)
426 | speciesLuck = object.get('luck', 1)
427 | speciesLevel = object.get('level', 1)
428 |
429 | # Ensure moveset is a list and handle missing move IDs
430 | speciesMoves = [moveNamesByIDHelper.get(str(move.get("moveId", 0)), "Unknown Move") for move in object.get("moveset", [])]
431 |
432 | speciesNatureID = str(object.get('nature', 0))
433 | speciesNatureName = natureNamesByIDHelper.get(speciesNatureID, "None")
434 | speciesIVs = object.get('ivs', 1)
435 | speciesHP = object.get('hp', 1)
436 | speciesPassive = object.get('passive', False)
437 |
438 | # Check if the species has a passive ability
439 | if int(speciesDexID) in speciesDict and hasattr(speciesDict[int(speciesDexID)], 'hasPassive') and speciesDict[int(speciesDexID)].hasPassive:
440 | speciesPassiveStatus = object.get('passive', False)
441 | else:
442 | speciesPassiveStatus = 'Not available'
443 |
444 | speciesPokerus = object.get('pokerus', False)
445 | speciesPokeball = object.get('pokeball', 0)
446 |
447 | # Handle missing Pokeball types
448 | speciesPokeballStatus = pokeballList.get(speciesPokeball, "Unknown Pokeball")
449 |
450 | speciesInfo = {
451 | 'id': speciesDexID,
452 | 'fusionID': speciesFusionID,
453 | 'formIndex': speciesFormIndex,
454 | 'fusionFormIndex': speciesFusionFormIndex,
455 | 'name': speciesDexName.title(),
456 |
457 | 'fusion': speciesFusionName.title(),
458 | 'fusionLuck': speciesFusionLuck,
459 | 'fusionIsShiny': speciesFusionisShiny,
460 | 'fusionVariant': speciesFusionVariant,
461 |
462 | 'shiny': speciesIsShiny,
463 | 'variant': speciesShinyVariant,
464 | 'luck': speciesLuck,
465 | 'level': speciesLevel,
466 | 'moves': speciesMoves,
467 | 'natureID': speciesNatureID,
468 | 'nature': speciesNatureName,
469 | 'ivs': speciesIVs,
470 | 'hp': speciesHP,
471 | 'passive': speciesPassive,
472 | 'passiveStatus': speciesPassiveStatus,
473 | 'pokerus': speciesPokerus,
474 | 'pokeball': speciesPokeball,
475 |
476 | 'pokeballStatus': speciesPokeballStatus,
477 | 'fusionStatus': '' if speciesFusionID == '0' else f'Fused with {Fore.YELLOW}{speciesFusionName.title()}{Style.RESET_ALL}',
478 | 'shinyStatus': f'Shiny {speciesShinyVariant + 1}' if speciesIsShiny else 'Not Shiny',
479 | 'luckCount': speciesLuck if speciesFusionID == '0' else (speciesLuck + (speciesFusionLuck if speciesFusionLuck is not None else 0)),
480 | 'data_ref': object
481 | }
482 |
483 | currentParty.append(speciesInfo)
484 |
485 | return currentParty
--------------------------------------------------------------------------------
/src/modules/handler/__init__.py:
--------------------------------------------------------------------------------
1 | from .httpResponseHandling import dec_handleHTTPExceptions, HTTPEmptyResponse
2 |
3 | from .operationResponseHandling import dec_handleOperationExceptions
4 | from .operationResponseHandling import OperationCancel, OperationError, OperationSuccessful, PropagateResponse, OperationSoftCancel
5 | from .inputHandler import fh_getChoiceInput, fh_getCompleterInput, fh_getIntegerInput
6 |
7 | __all__ = [
8 | 'dec_handleOperationExceptions',
9 | 'OperationCancel', 'OperationError', 'OperationSuccessful', 'PropagateResponse', 'OperationSoftCancel',
10 |
11 | 'dec_handleHTTPExceptions', 'HTTPEmptyResponse',
12 | 'fh_getChoiceInput', 'fh_getCompleterInput', 'fh_getIntegerInput'
13 | ]
--------------------------------------------------------------------------------
/src/modules/handler/httpResponseHandling.py:
--------------------------------------------------------------------------------
1 | # Authors https://github.com/JulianStiebler/
2 | # Organization: https://github.com/rogueEdit/
3 | # Repository: https://github.com/rogueEdit/OnlineRogueEditor
4 | # Contributors: None except Author
5 | # Date of release: 24.06.2024
6 | # Last Edited: 28.06.2024
7 |
8 | from functools import wraps
9 | from utilities import cFormatter, Color
10 |
11 | class HTTPEmptyResponse(Exception):
12 | def __init__(self, message='Response content is empty.'):
13 | self.message = message
14 | super().__init__(self.message)
15 |
16 | # Custom status messages for specific HTTP status codes
17 | statusMessages = {
18 | 200: (Color.BRIGHT_GREEN, 'Response 200 - That seemed to have worked!'),
19 | 400: (Color.WARNING, 'Response 400 - Bad Request: The server could not understand the request due to invalid syntax. This is usually related to wrong credentials.'),
20 | 401: (Color.BRIGHT_RED, 'Response 401 - Unauthorized: Authentication is required and has failed or has not yet been provided.'),
21 | 403: (Color.BRIGHT_RED, 'Response 403 - Forbidden. We have no authorization to access the resource.'),
22 | 404: (Color.BRIGHT_RED, 'Response 404 - Not Found: The server can not find the requested resource.'),
23 | 405: (Color.BRIGHT_RED, 'Response 405 - Method Not Allowed: The request method is known by the server but is not supported by the target resource.'),
24 | 406: (Color.BRIGHT_RED, 'Response 406 - Not Acceptable: The server cannot produce a response matching the list of acceptable values defined in the request\'s proactive content negotiation headers.'),
25 | 407: (Color.BRIGHT_RED, 'Response 407 - Proxy Authentication Required: The client must first authenticate itself with the proxy.'),
26 | 408: (Color.BRIGHT_RED, 'Response 408 - Request Timeout: The server would like to shut down this unused connection.'),
27 | 413: (Color.BRIGHT_RED, 'Response 413 - Payload Too Large: The request entity is larger than limits defined by server.'),
28 | 429: (Color.BRIGHT_RED, 'Response 429 - Too Many Requests: The user has sent too many requests in a given amount of time ("rate limiting").'),
29 | 500: (Color.CRITICAL, 'Error 500 - Internal Server Error: The server has encountered a situation it does not know how to handle.'),
30 | 502: (Color.CRITICAL, 'Error 502 - Bad Gateway: The server was acting as a gateway or proxy and received an invalid response from the upstream server.'),
31 | 503: (Color.CRITICAL, 'Error 503 - Service Temporarily Unavailable: The server is not ready to handle the request.'),
32 | 504: (Color.CRITICAL, 'Error 504 - Gateway Timeout: The server is acting as a gateway or proxy and did not receive a timely response from the upstream server.'),
33 | 520: (Color.CRITICAL, 'Error 520 - Web Server Returns an Unknown Error: The server has returned an unknown error.'),
34 | 521: (Color.CRITICAL, 'Error 521 - Web Server Is Down: The server is not responding to Cloudflare requests.'),
35 | 522: (Color.CRITICAL, 'Error 522 - Connection Timed Out: Cloudflare was able to complete a TCP connection to the origin server, but the origin server did not reply with an HTTP response.'),
36 | 523: (Color.CRITICAL, 'Error 523 - Origin Is Unreachable: Cloudflare could not reach the origin server.'),
37 | 524: (Color.CRITICAL, 'Error 524 - A Timeout Occurred: Cloudflare was able to complete a TCP connection to the origin server, but the origin server did not reply with an HTTP response.')
38 | }
39 |
40 | def dec_handleHTTPExceptions(func, requests):
41 | @wraps(func)
42 | def wrapper(*args, **kwargs):
43 | try:
44 | return func(*args, **kwargs)
45 | except requests.exceptions.HTTPError as http_err:
46 | cFormatter.print(Color.CRITICAL, f'HTTP error occurred: {http_err}')
47 | if isinstance(args[0], requests.Response) and args[0].status_code in statusMessages:
48 | color, message = statusMessages[args[0].status_code]
49 | cFormatter.print(color, message, isLogging=True)
50 | else:
51 | cFormatter.print(Color.CRITICAL, 'Unexpected HTTP error occurred.', isLogging=True)
52 | except requests.exceptions.RequestException as req_err:
53 | cFormatter.print(Color.CRITICAL, f'Request error occurred: {req_err}')
54 | except Exception as e:
55 | cFormatter.print(Color.CRITICAL, f'Other error occurred: {e}')
56 | return wrapper
57 |
--------------------------------------------------------------------------------
/src/modules/handler/inputHandler.py:
--------------------------------------------------------------------------------
1 | # Authors https://github.com/JulianStiebler/
2 | # Organization: https://github.com/rogueEdit/
3 | # Repository: https://github.com/rogueEdit/OnlineRogueEditor
4 | # Contributors: None except Author
5 | # Date of release: 25.06.2024
6 | # Last Edited: 28.06.2024
7 |
8 | from modules.handler import OperationCancel, OperationSoftCancel
9 | from enum import Enum
10 | from prompt_toolkit import prompt
11 | from prompt_toolkit.completion import WordCompleter
12 | from utilities import cFormatter, Color
13 | from typing import Optional
14 |
15 |
16 | @staticmethod
17 | def fh_getChoiceInput(promptMesage: str, choices: dict, renderMenu: bool = False, zeroCancel: bool=False, softCancel:bool = False) -> str:
18 | """
19 | Args:
20 | - promptMesage (str): The prompt message to display.
21 | - choices (dict): The dictionary containing choice options.
22 | - renderMenu (bool): If True, render the menu with line breaks for readability.
23 | - zeroCancel (bool): If True, allow raise cancellation with '0' interrupting the operation and save.
24 | - softCancel (bool): If True, allow soft cancellation with '0' interrupting the operation but allow saving.
25 |
26 | Helper method to get a validated choice input from the user.
27 |
28 | Raises:
29 | - OperationCancel()
30 | - OperationSoftCancel()
31 | - ValueError()
32 |
33 | Returns:
34 | - str: The validated choice key.
35 | - or any Raise depending on setup.
36 | """
37 | if renderMenu:
38 | actions = "\n".join([f'{idx + 1}: {desc}' for idx, desc in enumerate(choices.values())])
39 | fullPrompt = f'{promptMesage}\n{actions}\nSelect a option (0: Cancel): '
40 | else:
41 | actions = " | ".join([f'{idx + 1}: {desc}' for idx, desc in enumerate(choices.values())])
42 | if zeroCancel or softCancel:
43 | fullPrompt = f'{promptMesage} (0: Cancel | {actions}): '
44 | else:
45 | fullPrompt = f'{promptMesage} ({actions}): '
46 |
47 | while True:
48 | userInput = input(fullPrompt).strip()
49 | if userInput.lower() == 'exit' or userInput.lower() == 'cancel' or userInput == '':
50 | raise OperationCancel()
51 | if userInput == '0':
52 | if zeroCancel:
53 | raise OperationCancel()
54 | if softCancel:
55 | raise OperationSoftCancel()
56 |
57 | # If no cancel or skip is requested
58 | if userInput.isdigit():
59 | idx = int(userInput) - 1
60 | if 0 <= idx < len(choices):
61 | return list(choices.keys())[idx]
62 |
63 | print(f'{userInput}')
64 |
65 | @staticmethod
66 | def fh_getIntegerInput(promptMessage: str, minBound: int, maxBound: int, zeroCancel: bool=False, softCancel: bool=False, allowSkip: bool=False) -> int:
67 | """
68 | Args:
69 | - prompt (str): The prompt message to display.
70 | - minBound (int): The minimum valid value.
71 | - maxBound (int): The maximum valid value.
72 | - zeroCancel (bool): If True, allow raise cancellation with '0' interrupting the operation and save.
73 | - softCancel (bool): If True, allow soft cancellation with '0' interrupting the operation but allow saving.
74 | - allowSkip (bool): If True, returns 'skip'
75 | Helper method to get a validated integer input from the user.
76 |
77 |
78 | Raises:
79 | - OperationCancel()
80 | - OperationSoftCancel()
81 | - ValueError()
82 |
83 | Returns:
84 | - int: The validated integer input.
85 | - or any Raise depending on setup.
86 | """
87 | if zeroCancel:
88 | minBound = 0
89 | fullPrompt = f'{promptMessage} (0: Cancel | 1 - {maxBound} | "skip"): ' if allowSkip else f'{promptMessage} (0: Cancel | 1 - {maxBound}): '
90 | if softCancel:
91 | minBound = 0
92 | fullPrompt = f'{promptMessage} (0: Save & Cancel | 1 - {maxBound} | "skip"): ' if allowSkip else f'{promptMessage} (0: Save & Cancel | 1 - {maxBound}): '
93 | else:
94 | fullPrompt = f'{promptMessage} ({minBound} - {maxBound}): '
95 |
96 | while True:
97 | userInput = input(fullPrompt).strip()
98 | if userInput.lower() == 'exit' or userInput.lower() == 'cancel' or userInput == '' or userInput == ' ' or userInput is None:
99 | raise OperationCancel()
100 | if userInput == '0':
101 | if zeroCancel:
102 | raise OperationCancel()
103 | elif softCancel:
104 | raise OperationSoftCancel()
105 | if allowSkip and userInput.lower() == 'skip':
106 | return 'skip'
107 |
108 | # If no cancel or skip is requested
109 | if userInput.isdigit():
110 | value = int(userInput)
111 | if minBound <= value <= maxBound:
112 | return str(value)
113 |
114 | cFormatter.print(Color.INFO, f'Invalid input: "{userInput}" - must be between {minBound} - {maxBound}')
115 |
116 | @staticmethod
117 | def fh_getCompleterInput(promptMessage: str, choices: dict, zeroCancel: bool = False, softCancel: bool = False, allowSkip: bool = False) -> str:
118 | """
119 | Args:
120 | - prompt_message (str): The prompt message to display.
121 | - choices (dict): A dictionary mapping input choices to their corresponding values.
122 | - zeroCancel (bool): If True, allow raise cancellation with '0' interrupting the operation and save.
123 | - softCancel (bool): If True, allow soft cancellation with '0' interrupting the operation but allow saving.
124 |
125 | Helper method to get input from the user with auto-completion support.
126 |
127 | Raises:
128 | - OperationSoftCancel()
129 | - OperationCancel()
130 | - ValueError()
131 |
132 | Returns:
133 | - str: The value corresponding to the validated input choice, or raises OperationCancel if the user cancels.
134 | - or any Raise depending on setup.
135 | """
136 | fullPrompt = f'{promptMessage}: '
137 | if zeroCancel or softCancel:
138 | fullPrompt = f'{promptMessage} (0: Cancel): '
139 |
140 | # Create a WordCompleter from the keys of choices dictionary
141 | completer = WordCompleter(choices.keys(), ignore_case=True)
142 |
143 | while True:
144 | try:
145 | userInput = prompt(fullPrompt, completer=completer).strip() # Ensure prompt is the correct callable
146 |
147 | if userInput.lower() == 'exit' or userInput.lower() == 'cancel' or userInput == '':
148 | raise OperationCancel()
149 | if userInput == '0':
150 | if softCancel:
151 | raise OperationSoftCancel()
152 | if zeroCancel:
153 | raise OperationCancel()
154 | if allowSkip and userInput.lower() == 'skip':
155 | return 'skip'
156 |
157 | ## Validate the input
158 | if userInput in choices:
159 | return choices[userInput]
160 |
161 | # Ensure inputValue is a string
162 | inputValue = str(userInput).strip().lower()
163 | enumMember: Optional[Enum] = None
164 | if inputValue.isdigit():
165 | # Input is an ID
166 | enumMember = next((member for member in choices.values() if isinstance(member, Enum) and member.value == int(inputValue)))
167 | else:
168 | # Input is a name
169 | enumMember = next((member for member in choices.values() if isinstance(member, Enum) and member.name() == inputValue))
170 |
171 | if enumMember is not None:
172 | return enumMember
173 | # only except that here, this indicates invalid input for choicecompleter
174 | except StopIteration:
175 | cFormatter.print(Color.INFO, 'Invalid input.')
176 |
--------------------------------------------------------------------------------
/src/modules/handler/operationResponseHandling.py:
--------------------------------------------------------------------------------
1 | # Authors https://github.com/JulianStiebler/
2 | # Organization: https://github.com/rogueEdit/
3 | # Repository: https://github.com/rogueEdit/OnlineRogueEditor
4 | # Contributors: None except Author
5 | # Date of release: 25.06.2024
6 | # Last Edited: 28.06.2024
7 |
8 | from utilities import Color
9 | from json import JSONDecodeError
10 | from modules.config import debugEnableTraceback
11 | from utilities import fh_appendMessageBuffer
12 |
13 | def dec_handleOperationExceptions(func):
14 | def wrapper(*args, **kwargs):
15 | try:
16 | return func(*args, **kwargs)
17 |
18 | except OperationSuccessful as os:
19 | funcName = func.__name__
20 | customMessage = os.args[0] if os.args else ""
21 | fh_appendMessageBuffer(Color.DEBUG, f'Operation {funcName} finished. {customMessage}')
22 |
23 | except OperationError as oe:
24 | fh_appendMessageBuffer(Color.DEBUG, str(oe), isLogging=True)
25 |
26 | except OperationCancel as oc:
27 | fh_appendMessageBuffer(Color.DEBUG, f'{str(oc)}') # need \n cause it breaks on new lines
28 |
29 | except OperationSoftCancel as sc:
30 | funcName = func.__name__
31 | customMessage = sc.args[0] if sc.args else ""
32 | fh_appendMessageBuffer(Color.DEBUG, f'Soft-cancelling {funcName}. {customMessage}')
33 |
34 | except KeyboardInterrupt:
35 | fh_appendMessageBuffer(Color.DEBUG, 'Keyboard-interrupt detected.')
36 |
37 | except JSONDecodeError as jde:
38 | funcName = func.__name__
39 | customMessage = f'JSON decoding error in function {funcName}: {jde}'
40 | fh_appendMessageBuffer(Color.CRITICAL, customMessage, isLogging=True)
41 |
42 | except IOError as ioe:
43 | funcName = func.__name__
44 | customMessage = f'{funcName}: {ioe}'
45 | fh_appendMessageBuffer(Color.CRITICAL, f'Error loading data: {customMessage}', isLogging=True)
46 |
47 | except Exception as e:
48 | funcName = func.__name__
49 | customMessage = f'Error in function {funcName}: {e}'
50 | fh_appendMessageBuffer(Color.CRITICAL, customMessage, isLogging=True)
51 | # This should forward any exception not handled to our main stack
52 | if debugEnableTraceback:
53 | raise Exception()
54 | return wrapper
55 |
56 | # ==============================================
57 | # = Custom Exception for Operation Cancelling. =
58 | # = Will be catched by our main routine. =
59 | # ==============================================
60 | class OperationCancel(Exception):
61 |
62 | def __init__(self, message: str = None):
63 | if message:
64 | self.message = message
65 | else:
66 | self.message = 'Operation canceled.'
67 | super().__init__(self.message)
68 |
69 | # ==============================================
70 | # = Custom Exception for Operation Feedback =
71 | # = Will be catched by our main routine. =
72 | # ==============================================
73 | class OperationSuccessful(Exception):
74 | def __init__(self, message: str = None):
75 | if message:
76 | self.message = message
77 | else:
78 | self.message = 'Operation succesful.'
79 | super().__init__(self.message)
80 |
81 | # ==============================================
82 | # = Custom Exception for Operation Errors . =
83 | # = Will be catched by our main routine. =
84 | # ==============================================
85 | class OperationError(Exception):
86 | def __init__(self, originalTraceback: Exception = None, message: str = None):
87 | self.original_exception = originalTraceback
88 | if message:
89 | self.message = message
90 | elif originalTraceback:
91 | self.message = f'Operation encountered an error: {str(originalTraceback)}'
92 | else:
93 | self.message = 'Operation encountered an error.'
94 | super().__init__(self.message)
95 |
96 | # ==============================================================
97 | # = Custom Exception for Propagating messages to main routine. =
98 | # = Will be catched by our main routine. =
99 | # ==============================================================
100 | class PropagateResponse(Exception):
101 | def __init__(self, message: str = None):
102 | if message:
103 | self.message = message
104 | else:
105 | self.message = ''
106 | super().__init__(self.message)
107 |
108 | # ======================================================================================
109 | # = Custom Exception for Operation Softcancelling. Doesnt cancel the function raising. =
110 | # = Will be catched by our main routine. =
111 | # ======================================================================================
112 | class OperationSoftCancel(Exception):
113 | def __init__(self, message=""):
114 | super().__init__(message)
--------------------------------------------------------------------------------
/src/modules/requestsLogic.py:
--------------------------------------------------------------------------------
1 | # Authors https://github.com/JulianStiebler/
2 | # Organization: https://github.com/rogueEdit/
3 | # Repository: https://github.com/rogueEdit/OnlineRogueEditor
4 | # Contributors: https://github.com/claudiunderthehood
5 | # Date of release: 13.06.2024
6 | # Last Edited: 28.06.2024
7 |
8 | """
9 | This script provides functionality for handling HTTP requests, including error handling and login logic.
10 |
11 | Features:
12 | - Sends HTTP POST requests to a specified login URL.
13 | - Handles various HTTP response status codes and logs corresponding messages.
14 | - Generates random user agent headers for requests.
15 | - Implements rate limiting to prevent excessive requests.
16 |
17 | Modules:
18 | - requests: Sends HTTP requests and handles responses.
19 | - random: Generates random integers for implementing rate limiting and adding delays.
20 | - typing: Provides support for type hints.
21 | - time.sleep: Introduces delays between requests to simulate human behavior.
22 | - utilities.limiter.Limiter: Limits the frequency of requests to avoid rate limiting issues.
23 | - utilities.cFormatter: Formats console output for displaying error and debug messages.
24 | - pyuseragents: Generates random user agent strings for diverse HTTP requests.
25 | - string: Provides character manipulation functions for generating random session IDs.
26 |
27 | Workflow:
28 | 1. Define error handling for various HTTP response status codes.
29 | 2. Generate random user agent headers for HTTP requests.
30 | 3. Implement rate limiting to prevent excessive requests.
31 | 4. Send HTTP POST request to the login URL with provided credentials.
32 | 5. Log login status and HTTP response details upon successful or failed login attempts.
33 |
34 | Usage:
35 | - Initialize an instance of requestsLogic with username and password.
36 | - Call login() method to attempt login and retrieve login status.
37 |
38 | Output examples:
39 | - Displays formatted error messages for various HTTP status codes.
40 | - Logs successful login with HTTP status code, response URL, and headers.
41 |
42 | Modules/Librarys used and for what purpose exactly in each function at the end of the docstring:
43 | - requests: Sends HTTP POST requests to login URL and handles server responses.
44 | - random: Generates random integers for rate limiting and adding delays.
45 | - typing: Provides type hints for function parameters and return types.
46 | - time.sleep: Delays execution to simulate natural behavior during HTTP requests.
47 | - utilities.limiter.Limiter: Implements rate limiting to prevent excessive login attempts.
48 | - utilities.cFormatter: Formats console output for displaying debug and error messages during login process.
49 | - pyuseragents: Generates diverse user agent strings to mimic different browsers and operating systems.
50 | - string: Provides character manipulation functions for generating random session IDs.
51 | """
52 |
53 | import requests
54 | import random
55 | from typing import Dict, Optional
56 | from time import sleep
57 | from utilities import Limiter, cFormatter, Color
58 | import pyuseragents
59 | from user_agents import parse
60 | import string
61 | from modules.config import useCaCert
62 | limiter = Limiter()
63 |
64 |
65 | def fh_handleErrorResponse(response: requests.Response) -> Dict[str, str]:
66 | """
67 | Handle error responses from the server.
68 |
69 | Args:
70 | response (requests.Response): The HTTP response object.
71 |
72 | Returns:
73 | dict: Empty dictionary.
74 |
75 | This method handles various HTTP response status codes and prints corresponding
76 | messages using the cFormatter class. It covers common client and server error
77 | codes, information from cloudflare docs.
78 |
79 | Example:
80 | >>> response = requests.get("https://example.com")
81 | >>> fh_handleErrorResponse(response)
82 | 'Response 404 - Not Found: The server can not find the requested resource.'
83 |
84 | Modules/Librarys used and for what purpose exactly in each function:
85 | - requests: Handles HTTP response objects to log error messages based on status codes.
86 | - utilities.cFormatter: Formats console output for displaying error messages with color coding.
87 | """
88 |
89 | if response.status_code == 200:
90 | cFormatter.print(Color.BRIGHT_GREEN, 'Response 200 - That seemed to have worked!')
91 | cFormatter.print(Color.BRIGHT_GREEN, 'If it doesn\'t apply in-game, refresh without cache or try a private tab!')
92 | else:
93 | cFormatter.print(Color.CRITICAL, f'Response {response.status_code} - {response.reason}: {response.text}', isLogging=True)
94 |
95 | return {}
96 |
97 | class HeaderGenerator:
98 | """
99 | Generates random user agent headers for HTTP requests.
100 |
101 | This class generates random user agent strings using pyuseragents and parses them
102 | using pyuseragents library to extract browser and operating system information.
103 |
104 | :arguments:
105 | - isAuthHeader (bool): Whether to generate authentication headers.
106 |
107 | :params:
108 | - browserFamily: Browser family extracted from the user agent string.
109 | - browserVersion: Browser version extracted from the user agent string.
110 | - osFamily: Operating system family extracted from the user agent string.
111 | - osVersion: Operating system version extracted from the user agent string.
112 | - isMobile: Boolean indicating if the user agent represents a mobile device.
113 |
114 | Usage:
115 | Generate user agent headers:
116 | >>> headers = HeaderGenerator.generateHeaders()
117 |
118 | Output examples:
119 | - User agent headers with randomly generated browser and operating system information.
120 |
121 | Modules/Librarys used and for what purpose exactly in each function:
122 | - pyuseragents: Generates diverse user agent strings to mimic different browsers and operating systems.
123 | """
124 | @classmethod
125 | def fh_generateHeaders(cls, isAuthHeader: bool = False) -> Dict[str, str]:
126 | userAgentString = pyuseragents.random()
127 | userAgent = parse(userAgentString)
128 |
129 | browserFamily = userAgent.browser.family
130 | browserVersion = userAgent.browser.version_string
131 | osFamily = userAgent.os.family
132 | osVersion = userAgent.os.version_string
133 | isMobile = userAgent.is_mobile
134 |
135 | headers = {
136 | "Accept": "application/x-www-form-urlencoded",
137 | "Content-Type": "application/x-www-form-urlencoded",
138 | "Origin": "https://pokerogue.net",
139 | "Referer": "https://pokerogue.net/",
140 | "Sec-CH-UA-Mobile": "?1" if isMobile else "?0",
141 | "Sec-Fetch-Dest": "empty",
142 | "Sec-Fetch-Mode": "cors",
143 | "Sec-Fetch-Site": "same-site",
144 | "User-Agent": userAgentString,
145 | }
146 |
147 | # Define the optional headers
148 | optionalHeader = {
149 | "Sec-CH-UA": f'"{browserFamily}";v="{browserVersion}"',
150 | "Sec-CH-UA-Platform": osFamily,
151 | "Sec-CH-UA-Platform-Version": osVersion,
152 | }
153 |
154 | # Randomly decide to add some or all of the optional headers
155 | for header, value in optionalHeader.items():
156 | if random.choice([True, False]):
157 | headers[header] = value
158 |
159 | return headers
160 |
161 | class requestsLogic:
162 | """
163 | Handles HTTP requests for logging in to a specified URL.
164 |
165 | This class initializes a session, generates random user agent headers using HeaderGenerator,
166 | implements rate limiting with Limiter, and handles various HTTP response status codes using
167 | fh_handleErrorResponse.
168 |
169 | :arguments:
170 | - username (str): The username for logging in.
171 | - password (str): The password for logging in.
172 |
173 | :params:
174 | - token (Optional[str]): Authentication token retrieved after successful login.
175 | - sessionId (Optional[str]): Randomly generated session ID.
176 | - session (requests.Session): Session object for managing HTTP requests.
177 |
178 | Usage:
179 | Initialize requestsLogic instance with username and password:
180 | >>> login = requestsLogic('user', 'pass')
181 |
182 | Attempt login using HTTP POST request:
183 | >>> success = login.login()
184 | >>> print(success)
185 |
186 | Output examples:
187 | - Logs successful login with HTTP status code, response URL, and headers.
188 | - Displays formatted error messages for various HTTP status codes.
189 |
190 | Modules/Librarys used and for what purpose exactly in each function:
191 | - requests: Sends HTTP POST requests and manages session for logging in.
192 | - random: Generates random integers for implementing rate limiting and adding delays.
193 | - typing: Provides type hints for function parameters and return types.
194 | - time.sleep: Delays execution to simulate natural behavior during HTTP requests.
195 | - utilities.limiter.Limiter: Limits the frequency of HTTP requests to avoid rate limiting issues.
196 | - utilities.cFormatter: Formats console output for displaying debug and error messages during login process.
197 | - pyuseragents: Generates diverse user agent strings to mimic different browsers and operating systems.
198 | - user_agents.parse: Extracts browser and operating system details from user agent strings.
199 | - string: Provides character manipulation functions for generating random session IDs.
200 | """
201 |
202 | LOGIN_URL = 'https://api.pokerogue.net/account/login'
203 |
204 | def __init__(self, username: str, password: str) -> None:
205 | """
206 | Initialize requestsLogic with username and password.
207 |
208 | Args:
209 | username (str): The username for logging in.
210 | password (str): The password for logging in.
211 |
212 | Example:
213 | >>> login = requestsLogic('user', 'pass')
214 | """
215 | self.username = username
216 | self.password = password
217 | self.token: Optional[str] = None
218 | self.sessionId: Optional[str] = None
219 | self.session = requests.Session()
220 |
221 | def calcSessionId(self) -> str:
222 | """
223 | Calculate a randomly generated session ID.
224 |
225 | Returns:
226 | str: Randomly generated session ID.
227 |
228 | Usage:
229 | Generate a session ID:
230 | >>> session_id = self.calcSessionId()
231 | """
232 | characters = string.ascii_letters + string.digits
233 | result = []
234 | for _ in range(32):
235 | randomIndex = random.randint(0, len(characters) - 1)
236 | result.append(characters[randomIndex])
237 |
238 | return ''.join(result)
239 |
240 | @limiter.lockout
241 | def login(self) -> bool:
242 | """
243 | Attempt login using HTTP POST request and handle responses.
244 |
245 | Returns:
246 | bool: True if login is successful, False otherwise.
247 |
248 | Usage:
249 | Attempt login and check success:
250 | >>> success = self.login()
251 | >>> print(success)
252 | """
253 | data = {'username': self.username, 'password': self.password}
254 | try:
255 | headers = HeaderGenerator.fh_generateHeaders()
256 | cFormatter.print(Color.DEBUG, 'Adding delay to appear more natural to the server. Please stand by...')
257 | cFormatter.print(Color.DEBUG, '(If it takes longer than 5 Seconds its not on us.)')
258 | response = self.session.post(self.LOGIN_URL, headers=headers, data=data, verify=useCaCert)
259 | del data, self.username, self.password
260 | sleep(random.randint(3, 5))
261 | response.raise_for_status()
262 |
263 | loginResponse = response.json()
264 | self.token = loginResponse.get('token')
265 | cFormatter.fh_printSeperators(30, '-')
266 | self.sessionId = self.calcSessionId()
267 | cFormatter.print(Color.GREEN, 'Login successful.')
268 | formattedStatusCode = Color.BRIGHT_GREEN if response.status_code == 200 else Color.BRIGHT_RED
269 | cFormatter.print(formattedStatusCode, f'HTTP Status Code: {response.status_code}')
270 | cFormatter.print(Color.CYAN, f'Response URL: {response.request.url}', isLogging=True)
271 | filteredHeaders = {key: value for key, value in response.headers.items() if key != 'Report-To'}
272 | cFormatter.print(Color.CYAN, f'Response Headers: {filteredHeaders}', isLogging=True)
273 | cFormatter.fh_printSeperators(30, '-')
274 | return True
275 |
276 | except requests.RequestException:
277 | fh_handleErrorResponse(response)
278 | return False
279 |
--------------------------------------------------------------------------------
/src/modules/seleniumLogic.py:
--------------------------------------------------------------------------------
1 | # Authors https://github.com/JulianStiebler/
2 | # Organization: https://github.com/rogueEdit/
3 | # Repository: https://github.com/rogueEdit/OnlineRogueEditor
4 | # Contributors: https://github.com/claudiunderthehood
5 | # Date of release: 06.06.2024
6 | # Last Edited: 28.06.2024
7 |
8 | """
9 | This script provides a Selenium-based login process for pyRogue, enabling automated login
10 | and retrieval of session ID, token, and headers from a specified website.
11 |
12 | Features:
13 | - Automates login using Selenium with username and password.
14 | - Retrieves session ID and authentication token from the logged responses.
15 | - Supports handling of browser performance logs to extract relevant data.
16 |
17 | Modules:
18 | - selenium.webdriver: Provides browser automation capabilities for interacting with web pages.
19 | - selenium.webdriver.common.by: Defines methods for locating elements in the web page.
20 | - selenium.webdriver.common.keys: Provides keys like RETURN for simulating user inputs.
21 | - selenium.webdriver.support.ui: Implements WebDriverWait for waiting until certain conditions are met.
22 | - selenium.webdriver.support.expected_conditions: Defines expected conditions for WebDriverWait.
23 | - selenium.common.exceptions: Handles exceptions that may occur during browser interactions.
24 | - json: Provides methods for parsing JSON data received from the web server.
25 | - time: Offers time-related functions, used here for adding a randomized wait time.
26 | - typing: Supports type hints for Python functions and variables.
27 | - utilities.CustomLogger: Handles custom logging settings to control log outputs.
28 | - random: Generates random integers for adding variability in the login process.
29 |
30 | Workflow:
31 | 1. Initialize the SeleniumLogic instance with username, password, and optional timeout.
32 | 2. Use the logic() method to perform automated login and retrieve session ID, token, and headers.
33 | 3. Handle browser performance logs to extract necessary data for authentication.
34 |
35 | Usage Example:
36 | >>> seleniumLogic = SeleniumLogic(username='yourUsername', password='yourPassword', timeout=120)
37 | >>> sessionId, token, driver = seleniumLogic.logic()
38 | >>> print(f'Session ID: {sessionId}')
39 | >>> print(f'Token: {token}')
40 | >>> # driver can be further used for additional operations if needed
41 |
42 | Expected Output Example:
43 | >> Session ID: abc123clientSessionId
44 | >> Token: abc123token
45 | >> # Additional headers: {'Content-Type': 'application/json', 'User-Agent': '...'}
46 | """
47 |
48 | from selenium import webdriver
49 | from selenium.webdriver.common.by import By
50 | from selenium.webdriver.common.keys import Keys
51 | from selenium.webdriver.support.ui import WebDriverWait
52 | from selenium.webdriver.support import expected_conditions as EC
53 | from selenium.common.exceptions import TimeoutException
54 | import json
55 | import time
56 | from typing import Optional, Tuple, Dict, Any
57 | from utilities import CustomLogger
58 | import random
59 | from modules.config import useCaCert
60 |
61 | class SeleniumLogic:
62 | """
63 | Handles the Selenium-based login process for pyRogue.
64 |
65 | Attributes:
66 | username (str): The username for login.
67 | password (str): The password for login.
68 | timeout (int): The timeout duration for the login process.
69 | useScripts (Optional[bool]): Specifies if additional scripts are used during login.
70 | """
71 |
72 | def __init__(self, username: str, password: str, timeout: int = 120, useScripts: Optional[bool] = None) -> None:
73 | """
74 | Initializes the SeleniumLogic instance.
75 |
76 | Args:
77 | username (str): The username for login.
78 | password (str): The password for login.
79 | timeout (int): The timeout duration for the login process.
80 | useScripts (Optional[bool]): Specifies if additional scripts are used during login.
81 | """
82 | self.timeout = timeout
83 | self.username = username
84 | self.password = password
85 | self.useScripts = useScripts
86 |
87 | def _processBrowserLogs(self, entry: Dict[str, Any]) -> Dict[str, Any]:
88 | """
89 | Processes a single browser log entry to extract the relevant response data.
90 |
91 | Args:
92 | entry (Dict[str, Any]): A log entry from the browser.
93 |
94 | Returns:
95 | Dict[str, Any]: The processed response data.
96 | """
97 | response = json.loads(entry["message"])["message"]
98 | return response
99 |
100 | def logic(self) -> Tuple[Optional[str], Optional[str], Optional[webdriver.Chrome]]:
101 | """
102 | Handles the login logic using Selenium and retrieves the session ID, token, and headers.
103 |
104 | Returns:
105 | Tuple[Optional[str], Optional[str], Optional[webdriver.Chrome]]:
106 | The session ID, token, and WebDriver instance if available, otherwise None.
107 | """
108 | # Deactivate logging because selenium clutters it extremely
109 | CustomLogger.fh_deactivateLogging()
110 |
111 | # Set Browser options
112 | options = webdriver.ChromeOptions()
113 | options.set_capability('goog:loggingPrefs', {'performance': 'ALL'}) # All performance logs
114 | options.add_argument('--disable-blink-features=AutomationControlled') # Avoid detection
115 | options.add_argument('--no-sandbox') # Overcome limited resource problems
116 | options.add_argument('--disable-dev-shm-usage') # Overcome limited resource problems
117 | options.add_argument('--disable-infobars') # Disable infobars
118 | options.add_argument('--enable-javascript') # enable javascript explicitly
119 | if useCaCert:
120 | options.add_argument(f'--ca-certificate={useCaCert}')
121 |
122 | driver = webdriver.Chrome(options=options)
123 | url = 'https://www.pokerogue.net/'
124 | driver.get(url)
125 |
126 | sessionID = None
127 | token = None
128 |
129 | try:
130 | # Wait for the username field to be visible and input the username
131 | usernameInput = WebDriverWait(driver, self.timeout).until(
132 | EC.visibility_of_element_located((By.CSS_SELECTOR, 'input[type="text"]'))
133 | )
134 | usernameInput.send_keys(self.username)
135 |
136 | # Wait for the password field to be visible and input the password
137 | passwordInput = WebDriverWait(driver, self.timeout).until(
138 | EC.visibility_of_element_located((By.CSS_SELECTOR, 'input[type="password"]'))
139 | )
140 | passwordInput.send_keys(self.password)
141 |
142 | # Send RETURN key
143 | passwordInput.send_keys(Keys.RETURN)
144 |
145 | print('Waiting for login data...')
146 | time.sleep(random.randint(8,12)) # Fixed wait time to ensure data is there
147 |
148 | # Process the browser log
149 | browserLogs = driver.get_log('performance')
150 | events = [self._processBrowserLogs(entry) for entry in browserLogs]
151 |
152 | # Extract session data such as sessionId, auth-token or headers etc
153 | for event in events:
154 | # Extract the clientSessionId
155 | if 'response' in event["params"]:
156 | response = event["params"]["response"]
157 | if 'url' in response:
158 | url = response["url"]
159 | if 'clientSessionId' in url:
160 | sessionID = url.split('clientSessionId=')[1]
161 | # Extract the authorization token
162 | if 'method' in event and event["method"] == 'Network.responseReceived':
163 | response = event["params"]["response"]
164 | if response["url"] == 'https://api.pokerogue.net/account/login':
165 | requestId = event["params"]["requestId"]
166 | result = driver.execute_cdp_cmd('Network.getResponseBody', {'requestId': requestId})
167 | responseBody = result.get('body', '')
168 | if responseBody:
169 | tokenData = json.loads(responseBody)
170 | token = tokenData.get('token')
171 |
172 | except TimeoutException as e:
173 | print(f'Timeout occurred: {e}')
174 |
175 | finally:
176 | CustomLogger.fh_reactivateLogging()
177 | # If we are not using login method 3 we should close the driver already
178 | if not self.useScripts:
179 | driver.close()
180 |
181 | del self.username, self.password
182 |
183 | return sessionID, token, driver
--------------------------------------------------------------------------------
/src/offlineSaveConverter/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AES Encryption/Decryption
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/offlineSaveConverter/js/script.js:
--------------------------------------------------------------------------------
1 | window.onload = function() {
2 | document.getElementById("decryptBtn").addEventListener('click', function() {
3 | const fileInput = document.getElementById("file");
4 | const file = fileInput.files[0];
5 |
6 | if (!file) {
7 | alert("Please select a file.");
8 | return;
9 | }
10 |
11 | const reader = new FileReader();
12 | reader.onload = function(event) {
13 | const fileContent = event.target.result;
14 |
15 | try {
16 | const decrypted = CryptoJS.AES.decrypt(fileContent, "x0i2O7WRiANTqPmZ");
17 | const plaintext = decrypted.toString(CryptoJS.enc.Utf8);
18 |
19 | const jsonContent = JSON.parse(plaintext);
20 |
21 | const nullCheckbox = document.getElementById("nullCheckbox");
22 | if (nullCheckbox.checked) {
23 | for (const key in jsonContent.starterData) {
24 | if (jsonContent.starterData.hasOwnProperty(key)) {
25 | jsonContent.starterData[key]["$m"] = null;
26 | }
27 | }
28 | }
29 |
30 | const binaryCheckbox = document.getElementById("binaryCheckbox");
31 | if (binaryCheckbox.checked) {
32 | for (const key in jsonContent.dexData) {
33 | if (jsonContent.dexData.hasOwnProperty(key)) {
34 | jsonContent.dexData[key]["$sa"] = (jsonContent.dexData[key]["$sa"]).toString(2);
35 | jsonContent.dexData[key]["$ca"] = (jsonContent.dexData[key]["$ca"]).toString(2);
36 | jsonContent.dexData[key]["$na"] = (jsonContent.dexData[key]["$na"]).toString(2);
37 | }
38 | }
39 | }
40 |
41 | const blob = new Blob([JSON.stringify(jsonContent, null, 2)], { type: "application/json" });
42 |
43 | const filename = file.name.startsWith('data') ? 'data_Guest.json' : (file.name.startsWith('sessionData') ? 'sessionData_Guest.json' : 'decrypted_data.json');
44 |
45 | const downloadLink = document.createElement("a");
46 | downloadLink.href = window.URL.createObjectURL(blob);
47 | downloadLink.download = filename;
48 | downloadLink.click();
49 | } catch (e) {
50 | alert("Error: Failed to decrypt or parse JSON.");
51 | }
52 | };
53 |
54 | reader.readAsText(file);
55 | });
56 |
57 | document.getElementById("encryptBtn").addEventListener('click', function() {
58 | const fileInput = document.getElementById("file");
59 | const file = fileInput.files[0];
60 |
61 | if (!file) {
62 | alert("Please select a file.");
63 | return;
64 | }
65 |
66 | const reader = new FileReader();
67 | reader.onload = function(event) {
68 | const fileContent = event.target.result;
69 |
70 | try {
71 |
72 | const ciphertext = CryptoJS.AES.encrypt(CryptoJS.enc.Latin1.parse(fileContent), "x0i2O7WRiANTqPmZ").toString();
73 |
74 | const blob = new Blob([ciphertext], { type: "application/octet-stream" });
75 |
76 | const extension = file.name.endsWith('.json') ? 'prsv' : 'json';
77 | const filename = file.name.replace(/\.[^/.]+$/, `.${extension}`);
78 |
79 | const downloadLink = document.createElement("a");
80 | downloadLink.href = window.URL.createObjectURL(blob);
81 | downloadLink.download = filename;
82 | downloadLink.click();
83 | } catch (e) {
84 | alert("Error: Failed to encrypt.");
85 | }
86 | };
87 |
88 | // Read the file as binary data
89 | reader.readAsBinaryString(file);
90 | });
91 | };
--------------------------------------------------------------------------------
/src/offlineSaveConverter/readme.md:
--------------------------------------------------------------------------------
1 | # Pretty self explanatory, use to convert offline save files.
2 |
3 | From PRSV -> .json (Decrypt)
4 | From .json -> PRSV (Encrypt)
--------------------------------------------------------------------------------
/src/requirements.txt:
--------------------------------------------------------------------------------
1 | altgraph==0.17.4
2 | attrs==23.2.0
3 | Brotli==1.1.0
4 | certifi==2024.7.4
5 | cffi==1.16.0
6 | charset-normalizer==3.3.2
7 | colorama==0.4.6
8 | h11==0.14.0
9 | idna==3.7
10 | outcome==1.3.0.post0
11 | packaging==24.1
12 | pefile==2023.2.7
13 | prompt_toolkit==3.0.46
14 | pycparser==2.22
15 | pyinstaller==6.8.0
16 | pyinstaller-hooks-contrib==2024.7
17 | PySocks==1.7.1
18 | pyuseragents==1.0.5
19 | pywin32-ctypes==0.2.2
20 | requests==2.32.3
21 | selenium==4.21.0
22 | setuptools==70.1.0
23 | sniffio==1.3.1
24 | sortedcontainers==2.4.0
25 | trio==0.25.1
26 | trio-websocket==0.11.1
27 | typing_extensions==4.12.1
28 | ua-parser==0.18.0
29 | urllib3==2.2.2
30 | user-agents==2.2.0
31 | wcwidth==0.2.13
32 | wsproto==1.2.0
33 | zstandard==0.22.0
34 |
--------------------------------------------------------------------------------
/src/utilities/__init__.py:
--------------------------------------------------------------------------------
1 | from .cFormatter import cFormatter, Color, format
2 | from .logger import CustomLogger, CustomFilter
3 | from .enumLoader import EnumLoader
4 | from .generator import Vouchers, Generator, Nature, NatureSlot, NoPassive
5 | from .limiter import Limiter
6 | from .propagateMessage import messageBuffer, fh_appendMessageBuffer, fh_clearMessageBuffer, fh_printMessageBuffer, fh_redundantMesage
7 | from . import eggLogic
8 |
9 |
10 | __all__ = [
11 | 'cFormatter', 'Color', 'CustomLogger', 'CustomFilter', 'format',
12 | 'Vouchers', 'Generator', 'Nature', 'NatureSlot', 'NoPassive',
13 | 'Limiter', 'EnumLoader', 'eggLogic',
14 | 'messageBuffer', 'fh_appendMessageBuffer', 'fh_clearMessageBuffer', 'fh_printMessageBuffer', 'fh_redundantMesage'
15 | ]
--------------------------------------------------------------------------------
/src/utilities/cFormatter.py:
--------------------------------------------------------------------------------
1 | # Authors https://github.com/JulianStiebler/
2 | # Organization: https://github.com/rogueEdit/
3 | # Repository: https://github.com/rogueEdit/OnlineRogueEditor
4 | # Contributors: None except Author
5 | # Date of release: 06.06.2024
6 | # Last Edited: 28.06.2024
7 |
8 | """
9 | This module provides a custom logging formatter and various utility functions for enhanced console output.
10 | It includes:
11 | - An enumeration for ANSI color codes to allow colored logging output.
12 | - A custom logging formatter (cFormatter) that supports colored console output and other formatting utilities.
13 | - Functions for printing colored text, separators, and formatted text lines.
14 | - A function for initializing and displaying a menu with numbered choices.
15 |
16 | Modules:
17 | - colorama: Provides ANSI escape sequences for colored terminal text.
18 | - enum: Allows the creation of enumerations, a set of symbolic names bound to unique, constant values.
19 | - logging: Provides a flexible framework for emitting log messages from Python programs.
20 | - shutil: Includes high-level file operations such as copying and removal.
21 | - typing: Provides support for type hints, enabling optional type checking.
22 | - re: Provides support for regular expressions, allowing pattern matching in strings.
23 |
24 | Workflow:
25 | 1. Define the Color enum for ANSI color codes.
26 | 2. Define the cFormatter class for custom logging formatting.
27 | 3. Implement static methods in cFormatter for printing colored text, separators, and formatted lines.
28 | 4. Implement a method for initializing and displaying a menu with numbered choices.
29 | """
30 |
31 | from colorama import Fore, Style
32 | # Provides ANSI escape sequences for colored terminal text, used for coloring console output.
33 |
34 | from enum import Enum
35 | # Allows the creation of enumerations, used here for defining color codes.
36 |
37 | import logging
38 | # Provides a flexible framework for emitting log messages, used for custom logging formatting.
39 |
40 | import shutil
41 | # Includes high-level file operations, used here for getting terminal width.
42 |
43 | from typing import Optional, List, Tuple, Union
44 | # Provides support for type hints, used for optional type checking and clarity.
45 |
46 | import re
47 | # Provides support for regular expressions, used for stripping ANSI color codes from text.
48 |
49 | @staticmethod
50 | def format(entry, color=Fore.YELLOW):
51 | return f'{color}{entry}{Style.RESET_ALL}'
52 |
53 | class Color(Enum):
54 | """
55 | Enum defining ANSI color codes for console output.
56 |
57 | Attributes:
58 | > These also trigger the corresponding logging level.
59 | CRITICAL (str): Bright red color for critical messages.
60 | DEBUG (str): Bright blue color for debug messages.
61 | ERROR (str): Red color for error messages.
62 | WARNING (str): Yellow color for warning messages.
63 | INFO (str): Bright light yellow color for informational messages.
64 |
65 | BLACK (str): Black color.
66 | RED (str): Red color.
67 | GREEN (str): Green color.
68 | YELLOW (str): Yellow color.
69 | BLUE (str): Blue color.
70 | MAGENTA (str): Magenta color.
71 | CYAN (str): Cyan color.
72 | WHITE (str): White color.
73 | BRIGHT_BLACK (str): Bright black color.
74 | BRIGHT_RED (str): Bright red color.
75 | BRIGHT_GREEN (str): Bright green color.
76 | BRIGHT_YELLOW (str): Bright yellow color.
77 | BRIGHT_BLUE (str): Bright blue color.
78 | BRIGHT_MAGENTA (str): Bright magenta color.
79 | BRIGHT_CYAN (str): Bright cyan color.
80 | BRIGHT_WHITE (str): Bright white color.
81 | """
82 | CRITICAL = Style.BRIGHT + Fore.RED
83 | DEBUG = Style.BRIGHT + Fore.BLUE
84 | ERROR = Fore.RED
85 | WARNING = Fore.YELLOW
86 | INFO = Style.BRIGHT + Fore.LIGHTYELLOW_EX
87 |
88 | BLACK = Fore.BLACK
89 | RED = Fore.RED
90 | GREEN = Fore.GREEN
91 | YELLOW = Fore.YELLOW
92 | BLUE = Fore.BLUE
93 | MAGENTA = Fore.MAGENTA
94 | CYAN = Fore.CYAN
95 | WHITE = Fore.WHITE
96 | BRIGHT_BLACK = Style.BRIGHT + Fore.BLACK
97 | BRIGHT_RED = Style.BRIGHT + Fore.RED
98 | BRIGHT_GREEN = Style.BRIGHT + Fore.GREEN
99 | BRIGHT_YELLOW = Style.BRIGHT + Fore.YELLOW
100 | BRIGHT_BLUE = Style.BRIGHT + Fore.BLUE
101 | BRIGHT_MAGENTA = Style.BRIGHT + Fore.MAGENTA
102 | BRIGHT_CYAN = Style.BRIGHT + Fore.CYAN
103 | BRIGHT_WHITE = Style.BRIGHT + Fore.WHITE
104 |
105 | class cFormatter(logging.Formatter):
106 | """
107 | A custom formatter for logging with colored console output.
108 | """
109 | LOG_LEVELS = {
110 | logging.CRITICAL: Color.CRITICAL,
111 | logging.DEBUG: Color.DEBUG,
112 | logging.ERROR: Color.ERROR,
113 | logging.WARNING: Color.WARNING,
114 | logging.INFO: Color.INFO,
115 | }
116 |
117 | @staticmethod
118 | def print(color: Color, text: str, isLogging: bool = False) -> None:
119 | """
120 | Logs the text to the console with specified color and optionally to a file.
121 |
122 | Args:
123 | color (Color): The color to use for formatting the text.
124 | text (str): The text to log.
125 | isLogging (bool, optional): Specifies whether the text is for logging. Defaults to False.
126 |
127 | Usage Example:
128 | cFormatter.print(Color.INFO, 'This is an informational message', isLogging=True)
129 | cFormatter.print(Color.DEBUG, 'This is a debug message')
130 |
131 | Example Output:
132 | [LIGHTYELLOW_EX]This is an informational message[RESET]
133 | [BLUE]This is a debug message[RESET]
134 |
135 | Modules/Librarys used:
136 | - logging: Used to log messages.
137 | - colorama: Used to apply color to text.
138 | """
139 | logger = logging.getLogger("root")
140 |
141 | if isLogging:
142 | # Determine the logging level based on color
143 | logLevel = logging.INFO
144 | for level, col in cFormatter.LOG_LEVELS.items():
145 | if col == color:
146 | logLevel = level
147 | break
148 | logger.log(logLevel, text)
149 |
150 | # Format text with ANSI color codes and print to console
151 | colorCode = color.value
152 | formatted_text = f'{colorCode}{text}{Style.RESET_ALL}'
153 | print(formatted_text)
154 |
155 | @staticmethod
156 | def fh_printSeperators(numSeperator: Optional[int] = None, separator: str = '-', color: Optional[Color] = None) -> None:
157 | """
158 | Prints separators with the specified color.
159 |
160 | Args:
161 | numSeperator (int, optional): The number of separator characters to print. If None, uses terminal width. Defaults to None.
162 | separator (str, optional): The character to use for separators. Defaults to '-'.
163 | color (Color, optional): The color to use for formatting separators. Defaults to None.
164 |
165 | Usage Example:
166 | cFormatter.fh_printSeparators(10, '-', Color.GREEN)
167 | cFormatter.fh_printSeparators(separator='-', color=Color.GREEN)
168 |
169 | Example Output:
170 | [GREEN]----------[RESET]
171 | [GREEN]---------------------------------------------[RESET]
172 |
173 | Modules/Librarys used:
174 | - shutil: Used to get the terminal width.
175 | - colorama: Used to apply color to text.
176 | """
177 | if numSeperator is None:
178 | terminalWidth = shutil.get_terminal_size().columns
179 | numSeperator = terminalWidth
180 |
181 | colorCode = color.value if color else ''
182 | formattedSeperator = f'{colorCode}{separator * numSeperator}{Style.RESET_ALL}'
183 | print(formattedSeperator)
184 |
185 | @staticmethod
186 | def fh_stripColorCodes(text: str) -> str:
187 | """
188 | Strips ANSI color codes from the text for accurate length calculations.
189 |
190 | Args:
191 | text (str): The text from which to strip ANSI color codes.
192 |
193 | Returns:
194 | str: The text without ANSI color codes.
195 |
196 | Usage Example:
197 | strippedText = cFormatter.fh_stripColorCodes('[GREEN]Text[RESET]')
198 | print(strippedText)
199 |
200 | Example Output:
201 | Text
202 |
203 | Modules/Librarys used:
204 | - re: Used to strip ANSI color codes from text.
205 | """
206 | ansiEscape = re.compile(r'\x1b\[.*?m')
207 | return ansiEscape.sub('', text)
208 |
209 | @staticmethod
210 | def fh_lineFill(line: str, helperText: str = '', length: int = 55, fill_char: str = ' ', truncate: bool = False) -> str:
211 | """
212 | Args:
213 | line (str): The main text line to format.
214 | helperText (str, optional): Additional text to append. Defaults to ''.
215 | length (int, optional): The total length of the formatted line. Defaults to 55.
216 | fillChar (str, optional): The character used for filling empty space. Defaults to ' '.
217 | truncate (bool, optional): Whether to truncate the line if it exceeds the specified length. Defaults to False.
218 |
219 | Formats a line of text to a fixed length by adding fill characters.
220 |
221 | Returns:
222 | str: The formatted line of text.
223 |
224 | Usage Example:
225 | formatedLine = cFormatter.fh_lineFill('Main text', 'Helper text', 80, '-')
226 | print(formattedLine)
227 |
228 | Example Output:
229 | Main text-----------------------------------Helper text
230 |
231 | Modules/Librarys used:
232 | - re: Used to strip ANSI color codes from text.
233 | """
234 | strippedLine = cFormatter.fh_stripColorCodes(line)
235 | strippedHelperText = cFormatter.fh_stripColorCodes(helperText)
236 |
237 | totalLength = len(strippedLine) + len(strippedHelperText)
238 |
239 | if truncate and totalLength > length:
240 | truncated_length = length - len(strippedLine) - 3 # 3 characters for "..."
241 | line = line[:truncated_length] + '...'
242 | strippedLine = cFormatter.fh_stripColorCodes(line)
243 | totalLength = len(strippedLine) + len(strippedHelperText)
244 |
245 | fillLength = length - totalLength
246 | fill = fill_char * fillLength
247 | return f'{Style.RESET_ALL}{line}{fill}{helperText}'
248 |
249 | @staticmethod
250 | def fh_centerText(text: str, length: int = 55, fillChar: str = ' ') -> str:
251 | """
252 | Args:
253 | text (str): The text to center.
254 | length (int, optional): The total length of the centered text. Defaults to 55.
255 | fillChar (str, optional): The character used for filling empty space. Defaults to ' '.
256 |
257 | Centers a text within a given length, filling with the specified character.
258 |
259 | Returns:
260 | str: The centered text.
261 |
262 | Usage Example:
263 | centeredText = cFormatter.fh_centerText('Centered Text', 80, '-')
264 | print(centeredText)
265 |
266 | Example Output:
267 | --------------Centered Text---------------
268 |
269 | Modules/Librarys used:
270 | - re: Used to strip ANSI color codes from text.
271 | """
272 | strippedText = cFormatter.fh_stripColorCodes(text)
273 | totalLength = len(strippedText)
274 | if totalLength >= length:
275 | return text[:length]
276 |
277 | fillLength = length - totalLength
278 | frontFill = fillChar * (fillLength // 2)
279 | backFill = fillChar * (fillLength - (fillLength // 2))
280 | if fillChar == '>':
281 | backFill = '<' * (fillLength - (fillLength // 2))
282 |
283 |
284 | return f'{frontFill}{text}{backFill}'
285 |
286 | @staticmethod
287 | def m_initializeMenu(term: List[Union[str, Tuple[str, str, Optional[str]], Tuple[str, callable]]], length: Optional[int] = 55) -> List[Tuple[int, callable]]:
288 | """
289 | Args:
290 | term (List[Union[str, Tuple[str, str, Optional[str]], Tuple[str, callable]]]): A list containing tuples and strings representing menu items.
291 |
292 | Initializes and prints a menu based on the provided term list.
293 |
294 | Returns:
295 | List[Tuple[int, callable]]: A list of tuples containing valid numbered choices and their associated functions.
296 |
297 | Usage Example:
298 | term = [
299 | (title, 'title'),
300 | (('Option 1', 'Description for option 1'), function1),
301 | (('Option 2', ''), function2),
302 | ('Helper text', 'helper'),
303 | ('Helper text', 'category'),
304 | ]
305 | validChoices = cFormatter.m_initializeMenu(term)
306 |
307 | Example Output:
308 | * --------------------- pyRogue ---------------------- *
309 | 1: Option 1 Description for option 1
310 | 2: Option 2
311 | * ----------------------- Helper text ----------------------- *
312 |
313 | Returns [(1, function1), (2, function2)]
314 |
315 | Modules/Librarys used:
316 | - colorama: Used to apply color to text.
317 | """
318 | validChoices = []
319 | actualIndex = 1
320 | for item in term:
321 | if isinstance(item, tuple):
322 | if item[1] == 'helper':
323 | print(Fore.GREEN + '* ' + cFormatter.fh_centerText(f' {item[0]} ', length, '-') + f' {Fore.GREEN}*' + Style.RESET_ALL)
324 | elif item[1] == 'title':
325 | print(Fore.GREEN + '* ' + cFormatter.fh_centerText(f' {item[0]} ', length, '*') + f' {Fore.GREEN}*' + Style.RESET_ALL)
326 | elif item[1] == 'category':
327 | print(Fore.LIGHTYELLOW_EX + '* ' + cFormatter.fh_centerText(f' {item[0]} ', length, '>') + ' *' + Style.RESET_ALL)
328 | else:
329 | text, func = item
330 | line = f'{actualIndex}: {text[0]}'
331 | formatted_line = cFormatter.fh_lineFill(line, text[1], length, ' ', True)
332 | print(Fore.GREEN + '* ' + formatted_line + f' {Fore.GREEN}*' + Style.RESET_ALL)
333 | validChoices.append((actualIndex, func))
334 | actualIndex += 1
335 | else:
336 | print(Fore.YELLOW + '* ' + cFormatter.fh_centerText(item, length, '*') + ' *' + Style.RESET_ALL)
337 |
338 | return validChoices
--------------------------------------------------------------------------------
/src/utilities/eggLogic.py:
--------------------------------------------------------------------------------
1 | # Authors https://github.com/JulianStiebler/
2 | # Organization: https://github.com/rogueEdit/
3 | # Repository: https://github.com/rogueEdit/OnlineRogueEditor
4 | # Contributors: None except Authors
5 | # Date of release: 13.06.2024
6 | # Last Edited: 28.06.2024
7 | # Based on: https://github.com/pagefaultgames/pokerogue/
8 |
9 | """
10 | Source Code from https://github.com/pagefaultgames/pokerogue/ multiple files
11 | Tier and Source Type Initialization:
12 | using eggOptions.tier to determine the _tier of the egg. If not provided, it falls back to Overrides.EGG_TIER_OVERRIDE or rolls a random tier using this.rollEggTier().
13 | _sourceType is set to eggOptions.sourceType or undefined.
14 |
15 | Pulled Eggs:
16 | If eggOptions.pulled is true, you check for eggOptions.scene to potentially override the _tier using this.checkForPityTierOverrides().
17 |
18 | ID Generation:
19 | _id is generated using eggOptions.id if provided or by calling Utils.randInt(EGG_SEED, EGG_SEED * this._tier).
20 |
21 | Timestamp and Hatch Waves:
22 | _timestamp defaults to the current time if not provided in eggOptions.
23 | _hatchWaves is determined by eggOptions.hatchWaves or defaults to a tier-specific default using this.getEggTierDefaultHatchWaves().
24 |
25 | Shiny, Variant, Species, and Hidden Ability:
26 | _isShiny is set based on eggOptions.isShiny, Overrides.EGG_SHINY_OVERRIDE, or randomly rolled.
27 | _variantTier is set similarly for variants.
28 | _species is determined by eggOptions.species or randomly rolled using this.rollSpecies().
29 | _overrideHiddenAbility is set to eggOptions.overrideHiddenAbility or defaults to false.
30 |
31 | Species-Specific Handling:
32 | If eggOptions.species is provided, it overrides _tier and _hatchWaves. If the species has no variants, _variantTier is set to VariantTier.COMMON.
33 |
34 | Egg Move Index:
35 | _eggMoveIndex defaults to a random value using this.rollEggMoveIndex() unless specified in eggOptions.
36 |
37 | Pulled Egg Actions:
38 | If eggOptions.pulled is true, you increase pull statistics and add the egg to game data using this.increasePullStatistic() and this.addEggToGameData().
39 |
40 | constructor(eggOptions?: IEggOptions) {
41 | //if (eggOptions.tier && eggOptions.species) throw Error("Error egg can't have species and tier as option. only choose one of them.")
42 |
43 | this._tier = eggOptions.tier ?? (Overrides.EGG_TIER_OVERRIDE ?? this.rollEggTier());
44 | this._sourceType = eggOptions.sourceType ?? undefined;
45 | // If egg was pulled, check if egg pity needs to override the egg tier
46 | if (eggOptions.pulled) {
47 | // Needs this._tier and this._sourceType to work
48 | this.checkForPityTierOverrides(eggOptions.scene);
49 | }
50 |
51 | this._id = eggOptions.id ?? Utils.randInt(EGG_SEED, EGG_SEED * this._tier);
52 |
53 | this._sourceType = eggOptions.sourceType ?? undefined;
54 | this._hatchWaves = eggOptions.hatchWaves ?? this.getEggTierDefaultHatchWaves();
55 | this._timestamp = eggOptions.timestamp ?? new Date().getTime();
56 |
57 | // First roll shiny and variant so we can filter if species with an variant exist
58 | this._isShiny = eggOptions.isShiny ?? (Overrides.EGG_SHINY_OVERRIDE || this.rollShiny());
59 | this._variantTier = eggOptions.variantTier ?? (Overrides.EGG_VARIANT_OVERRIDE ?? this.rollVariant());
60 | this._species = eggOptions.species ?? this.rollSpecies(eggOptions.scene);
61 |
62 | this._overrideHiddenAbility = eggOptions.overrideHiddenAbility ?? false;
63 |
64 | // Override egg tier and hatchwaves if species was given
65 | if (eggOptions.species) {
66 | this._tier = this.getEggTierFromSpeciesStarterValue();
67 | this._hatchWaves = eggOptions.hatchWaves ?? this.getEggTierDefaultHatchWaves();
68 | // If species has no variant, set variantTier to common. This needs to
69 | // be done because species with no variants get filtered at rollSpecies but since the
70 | // species is set the check never happens
71 | if (!getPokemonSpecies(this.species).hasVariants()) {
72 | this._variantTier = VariantTier.COMMON;
73 | }
74 | }
75 | // Needs this._tier so it needs to be generated afer the tier override if bought from same species
76 | this._eggMoveIndex = eggOptions.eggMoveIndex ?? this.rollEggMoveIndex();
77 | if (eggOptions.pulled) {
78 | this.increasePullStatistic(eggOptions.scene);
79 | this.addEggToGameData(eggOptions.scene);
80 | }
81 | }
82 |
83 |
84 | export const EGG_SEED = 1073741824;
85 |
86 | // Rates for specific random properties in 1/x
87 | const DEFAULT_SHINY_RATE = 128;
88 | const GACHA_SHINY_UP_SHINY_RATE = 64;
89 | const SAME_SPECIES_EGG_SHINY_RATE = 32;
90 | const SAME_SPECIES_EGG_HA_RATE = 16;
91 | const MANAPHY_EGG_MANAPHY_RATE = 8;
92 |
93 | // 1/x for legendary eggs, 1/x*2 for epic eggs, 1/x*4 for rare eggs, and 1/x*8 for common eggs
94 | const DEFAULT_RARE_EGGMOVE_RATE = 6;
95 | const SAME_SPECIES_EGG_RARE_EGGMOVE_RATE = 3;
96 | const GACHA_MOVE_UP_RARE_EGGMOVE_RATE = 3;
97 |
98 |
99 | /** Egg options to override egg properties */
100 | export interface IEggOptions {
101 | /** Id. Used to check if egg type will be manaphy (id % 204 === 0) */
102 | id?: number;
103 | /** Timestamp when this egg got created */
104 | timestamp?: number;
105 | /** Defines if the egg got pulled from a gacha or not. If true, egg pity and pull statistics will be applyed.
106 | * Egg will be automaticly added to the game data.
107 | * NEEDS scene eggOption to work.
108 | */
109 | pulled?: boolean;
110 | /** Defines where the egg comes from. Applies specific modifiers.
111 | * Will also define the text displayed in the egg list.
112 | */
113 | sourceType?: EggSourceType;
114 | /** Needs to be defined if eggOption pulled is defined or if no species or isShiny is degined since this will be needed to generate them. */
115 | export enum EggSourceType {
116 | GACHA_MOVE,
117 | GACHA_LEGENDARY,
118 | GACHA_SHINY,
119 | SAME_SPECIES_EGG,
120 | EVENT
121 | }
122 | scene?: BattleScene;
123 | /** Sets the tier of the egg. Only species of this tier can be hatched from this egg.
124 | * Tier will be overriden if species eggOption is set.
125 | */
126 | tier?: EggTier;
127 | /** Sets how many waves it will take till this egg hatches. */
128 | hatchWaves?: number;
129 | /** Sets the exact species that will hatch from this egg.
130 | * Needs scene eggOption if not provided.
131 | */
132 | species?: Species;
133 | /** Defines if the hatched pokemon will be a shiny. */
134 | isShiny?: boolean;
135 | /** Defines the variant of the pokemon that will hatch from this egg. If no variantTier is given the normal variant rates will apply. */
136 | variantTier?: VariantTier;
137 | /** Defines which egg move will be unlocked. 3 = rare egg move. */
138 | eggMoveIndex?: number;
139 | /** Defines if the egg will hatch with the hidden ability of this species.
140 | * If no hidden ability exist, a random one will get choosen.
141 | */
142 | overrideHiddenAbility?: boolean
143 | }
144 | """
145 |
146 | import random
147 | import time
148 | from typing import List, Tuple, Dict, Optional
149 |
150 | # Constant from game source code
151 | EGG_SEED: int = 1073741824
152 | GACHA_TYPES: List[str] = ['MoveGacha', 'LegendaryGacha', 'ShinyGacha', 'SAME_SPECIES_EGG', 'EVENT']
153 | EGG_TIERS: List[str] = ['Common', 'Rare', 'Epic', 'Legendary', 'Manaphy']
154 |
155 | def getIDBoundarys(tier: int) -> Tuple[int, int]:
156 | """
157 | Get the ID boundaries for a given tier.
158 |
159 | Args:
160 | tier (int): The tier index.
161 |
162 | Returns:
163 | Tuple[int, int]: A tuple containing the start and end IDs.
164 |
165 | Example:
166 | start, end = getIDBoundarys(2)
167 | print(start, end) # Output: 2147483648 3221225471
168 | """
169 | # Calculate the start and end IDs for the given tier
170 | start: int = tier * EGG_SEED
171 | end: int = (tier + 1) * EGG_SEED - 1
172 | return max(start, 255), end
173 |
174 | def generateRandomID(start: int, end: int, manaphy: bool = False) -> int:
175 | """
176 | Generate a random ID within the given range.
177 |
178 | Args:
179 | start (int): The start of the ID range.
180 | end (int): The end of the ID range.
181 | manaphy (bool): Whether the ID is for a Manaphy egg.
182 |
183 | Returns:
184 | int: The random ID.
185 |
186 | Example:
187 | random_id = generateRandomID(0, 1073741823)
188 | print(random_id) # Output: 564738291 (example)
189 | """
190 | if manaphy:
191 | # Generate a random ID that is divisible by 204 within the specified range based on tier
192 | return random.randrange(start // 204 * 204, (end // 204 + 1) * 204, 204)
193 |
194 | # Generate a regular random ID within the specified range
195 | result: int = random.randint(start, end)
196 |
197 | if result % 204 == 0:
198 | result -= 1
199 |
200 | return max(result, 1)
201 |
202 | def __getRandomSpeciesForShiny(tier: int, eggTypesData) -> Optional[int]:
203 | speciesMatch = []
204 | for member in eggTypesData:
205 | species = member.value
206 | if species["isEgg"] is not None:
207 | # If the tier is 6, match by name substrings
208 | if tier == 6:
209 | if any(substring in species['name'].lower() for substring in ["alola_", "galar_", "hisui_", "paldea_"]):
210 | speciesMatch.append(member)
211 | print(f"Matched species: {species['name']} with index {member.name}")
212 | elif tier == 7:
213 | paradoxIDs = [984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 1005, 1006, 1009, 1010, 1020, 1021, 1022, 1023]
214 | if paradoxIDs and int(member.name) in paradoxIDs:
215 | speciesMatch.append(member)
216 | print(f"Matched species: {species['name']} with index {member.name}")
217 | # For other tiers, match by eggType
218 | elif species["isEgg"]["eggType"] == tier:
219 | speciesMatch.append(member)
220 | #print(f"Matched species: {species['name']} with index {member.name}")
221 |
222 | if not speciesMatch:
223 | # No matching species found
224 | return None
225 |
226 | rnd = random.choice(speciesMatch)
227 | print(f"Randomly chosen species: {rnd.value['name']} with index {rnd.name}")
228 | return rnd.name, rnd.value["isEgg"]["eggType"]
229 |
230 | def constructEggs(tier: int, gachaType: int, hatchWaveCount: int, eggAmount: int, eggTypesData, isShiny: bool = False, variantTier: int = 0) -> List[Dict[str, int]]:
231 | """
232 | Generate eggs with the given properties.
233 |
234 | Args:
235 | tier (int): The tier of the eggs.
236 | gachaType (int): The gacha type.
237 | hatchWaveCount (int): The number of hatch waves.
238 | eggAmount (int): The number of eggs to generate.
239 | isShiny (bool): Whether the egg is shiny. Defaults to False.
240 | variantTier (int): The variant tier for shiny eggs. Defaults to 0.
241 |
242 | Returns:
243 | List[Dict[str, int]]: A list of generated eggs, each represented as a dictionary with egg properties.
244 |
245 | Example:
246 | eggs = constructEggs(1, 2, 3, 10, True, 1)
247 | print(eggs)
248 | # Output: [{'id': 123456789, 'gachaType': 2, 'hatchWaves': 3, 'timestamp': 1625247123456, 'tier': 0, 'isShiny': True, 'variantTier': 1}, ...]
249 | """
250 | isManaphy: bool = tier == 4
251 | start, end = getIDBoundarys(0 if isManaphy else tier)
252 |
253 | eggs: List[Dict[str, int]] = []
254 | for _ in range(eggAmount):
255 | eggID: int = generateRandomID(start, end, isManaphy)
256 | timestamp: int = int(time.time() * 1000)
257 |
258 | egg: Dict[str, int] = {
259 | 'id': eggID,
260 | 'gachaType': gachaType,
261 | 'hatchWaves': int(hatchWaveCount),
262 | 'timestamp': timestamp,
263 | }
264 |
265 | shinyRoll = random.randint(1, 64)
266 | if isShiny or shinyRoll == 1:
267 | egg["isShiny"] = True
268 | egg["variantTier"] = int(variantTier-1) if isShiny else 0
269 | egg["sourceType"] = 1
270 |
271 | # Find a random species with matching eggType
272 | speciesID, speciesEggType = __getRandomSpeciesForShiny(tier+1, eggTypesData)
273 | if speciesID is not None:
274 | egg["species"] = int(speciesID)
275 | egg["tier"] = int(speciesEggType)-1
276 | print(f'EggType: {speciesEggType}')
277 |
278 | else:
279 | egg["isShiny"] = False
280 | egg["variantTier"] = 0
281 | egg["sourceType"] = gachaType
282 | egg["tier"] = tier
283 |
284 |
285 | eggs.append(egg)
286 |
287 | return eggs
--------------------------------------------------------------------------------
/src/utilities/enumLoader.py:
--------------------------------------------------------------------------------
1 | # Authors: https://github.com/JulianStiebler https://github.com/claudiunderthehood
2 | # Organization: https://github.com/rogueEdit/
3 | # Repository: https://github.com/rogueEdit/OnlineRogueEditor
4 | # Contributors: None except Authors
5 | # Date of release: 06.06.2024
6 | # Last Edited: 28.06.2024
7 | # Based on: https://github.com/pagefaultgames/pokerogue/
8 |
9 | """
10 | This script provides functionalities to load data from JSON files and convert them into Enums.
11 | It includes the capability to handle Pokemon IDs, biomes, moves, natures, vouchers, and nature slots.
12 |
13 | Modules:
14 | - utilities: Custom module for colored printing and logging functionalities (cFormatter and Color).
15 | - modules: Contains configuration settings (config).
16 | - json: Provides functionalities to work with JSON data.
17 | - enum: Provides support for enumerations, a set of symbolic names bound to unique, constant values.
18 |
19 | Workflow:
20 | 1. Initialize the EnumLoader class.
21 | 2. Load data from JSON files located in a specified directory.
22 | 3. Convert loaded data to Enums.
23 | 4. Return the created Enums.
24 | """
25 |
26 | from utilities import cFormatter, Color
27 | # Custom module for colored printing and logging functionalities.
28 |
29 | from modules import config
30 | # Contains configuration settings, specifically for directory paths.
31 |
32 | import json
33 | # Provides functionalities to work with JSON data for reading and writing.
34 |
35 | from enum import Enum
36 | # Provides support for enumerations, a set of symbolic names bound to unique, constant values.
37 |
38 | from typing import Optional, Tuple, Dict
39 | # Provides type hints for better code clarity and type checking.
40 |
41 | class EnumLoader:
42 | def __init__(self) -> None:
43 | """
44 | Initialize the EnumLoader object.
45 |
46 | Attributes:
47 | starterNameByID (Optional[Dict[str, int]]): Dictionary for starter IDs by name.
48 | biomesByID (Optional[Dict[str, int]]): Dictionary for biomes by ID.
49 | movesByID (Optional[Dict[str, int]]): Dictionary for moves by ID.
50 | natureData (Optional[Dict[str, int]]): Dictionary for natures data.
51 | voucherData (Optional[Dict[str, int]]): Dictionary for vouchers data.
52 | natureDataSlots (Optional[Dict[str, int]]): Dictionary for nature slot data.
53 | noPassiveIDs (Optional[Dict[str, int]]): Dictionary for no passive IDs.
54 | hasFormIDs (Optional[Dict[str, int]]): Dictionary for IDs that have forms.
55 | speciesNameByID (Optional[Dict[str, int]]): Dictionary for species names by ID.
56 | achievementsData (Optional[Dict[str, int]]): Dictionary for achievements data.
57 | """
58 | self.starterNameByID: Optional[Dict[str, int]] = None
59 | self.biomesByID: Optional[Dict[str, int]] = None
60 | self.movesByID: Optional[Dict[str, int]] = None
61 | self.natureData: Optional[Dict[str, int]] = None
62 | self.voucherData: Optional[Dict[str, int]] = None
63 | self.natureDataSlots: Optional[Dict[str, int]] = None
64 | self.noPassiveIDs: Optional[Dict[str, int]] = None
65 | self.hasFormIDs: Optional[Dict[str, int]] = None
66 | self.speciesNameByID: Optional[Dict[str, int]] = None
67 | self.achievementsData: Optional[Dict[str, int]] = None
68 | self.eggTypesData: Optional[Dict[str, int]] = None
69 |
70 | def __f_loadData(self) -> None:
71 | """
72 | Load data from JSON files located in the directory specified by config.dataDirectory.
73 |
74 | Raises:
75 | Exception: If there is an error loading the data files.
76 |
77 | Example:
78 | loader = EnumLoader()
79 | loader.__f_loadData()
80 | """
81 | try:
82 | dataDir: str = config.dataDirectory
83 |
84 | with open(f'{dataDir}/starter.json') as f:
85 | self.starterNameByID: Dict[str, int] = json.load(f)
86 |
87 | with open(f'{dataDir}/biomes.json') as f:
88 | self.biomesByID: Dict[str, int] = json.load(f)
89 |
90 | with open(f'{dataDir}/moves.json') as f:
91 | self.movesByID: Dict[str, int] = json.load(f)
92 |
93 | with open(f'{dataDir}/natures.json') as f:
94 | self.natureData: Dict[str, int] = json.load(f)
95 |
96 | with open(f'{dataDir}/vouchers.json') as f:
97 | self.voucherData: Dict[str, int] = json.load(f)
98 |
99 | with open(f'{dataDir}/natureSlot.json') as f:
100 | self.natureDataSlots: Dict[str, int] = json.load(f)
101 |
102 | with open(f'{dataDir}/achievements.json') as f:
103 | self.achievementsData: Dict[str, int] = json.load(f)
104 |
105 | with open(f'{dataDir}/species.json') as f:
106 | self.speciesNameByID: Dict[str, int] = json.load(f)
107 |
108 | with open(f'{dataDir}/noPassive.json') as f:
109 | self.noPassiveIDs: Dict[str, int] = json.load(f)
110 |
111 | with open(f'{dataDir}/hasForms.json') as f:
112 | self.hasFormIDs: Dict[str, int] = json.load(f)
113 |
114 | with open(f'{dataDir}/eggTypes.json') as f:
115 | self.eggTypesData: Dict[str, int] = json.load(f)
116 |
117 | except Exception as e:
118 | cFormatter.print(Color.CRITICAL, f'Error in enumLoader.__f_loadData(). {e}', isLogging=True)
119 |
120 | def __f_createENUMFromDict(self, dataDict: Dict[str, int], enumName: str) -> Enum:
121 | """
122 | Create an Enum from a dictionary.
123 |
124 | Args:
125 | dataDict (Dict[str, int]): The dictionary to convert to an Enum.
126 | enumName (str): The name of the Enum.
127 |
128 | Returns:
129 | Enum: The created Enum.
130 |
131 | Example:
132 | loader = EnumLoader()
133 | speciesEnum = loader.__f_createENUMFromDict({'PIKACHU': 25}, 'SpeciesEnum')
134 | """
135 | enumClass: Enum = Enum(enumName, {key: value for key, value in dataDict.items()})
136 | return enumClass
137 |
138 | def f_convertToEnums(self) -> Tuple[Enum, Enum, Enum, Enum, Enum, Enum, Enum, Enum, Enum, Enum]:
139 | """
140 | Convert loaded data to Enums.
141 |
142 | Returns:
143 | Tuple[Enum, Enum, Enum, Enum, Enum, Enum, Enum, Enum, Enum, Enum]:
144 | A tuple containing the created Enums for starter names, biomes, moves, vouchers, natures, nature slots,
145 | achievements, species names, no passive IDs, and IDs with forms.
146 |
147 | Example:
148 | loader = EnumLoader()
149 | enums = loader.f_convertToEnums()
150 | StarterEnum = enums[0] # Access StarterEnum
151 | """
152 | self.__f_loadData()
153 |
154 | self.starterNameByID: Dict[str, int] = self.__f_createENUMFromDict(self.starterNameByID["dex"], 'StarterEnum')
155 | self.biomesByID: Dict[str, int] = self.__f_createENUMFromDict(self.biomesByID["biomes"], 'BiomesEnum')
156 | self.movesByID: Dict[str, int] = self.__f_createENUMFromDict(self.movesByID["moves"], 'MovesEnum')
157 | self.voucherData: Dict[str, int] = self.__f_createENUMFromDict(self.voucherData["vouchers"], 'VouchersEnum')
158 | self.natureData: Dict[str, int] = self.__f_createENUMFromDict(self.natureData["natures"], 'NaturesEnum')
159 | self.natureDataSlots: Dict[str, int] = self.__f_createENUMFromDict(self.natureDataSlots["natureSlot"], 'NaturesSlotEnum')
160 | self.achievementsData: Dict[str, int] = self.__f_createENUMFromDict(self.achievementsData["achvUnlocks"], 'AchievementsEnum')
161 | self.speciesNameByID: Dict[str, int] = self.__f_createENUMFromDict(self.speciesNameByID["dex"], 'PokemonEnum')
162 | self.noPassiveIDs: Dict[str, int] = self.__f_createENUMFromDict(self.noPassiveIDs["noPassive"], 'NoPassiveEnum')
163 | self.hasFormIDs: Dict[str, int] = self.__f_createENUMFromDict(self.hasFormIDs["hasForms"], 'HasFormsEnum')
164 | self.eggTypesData: Dict[str, int] = self.__f_createENUMFromDict(self.eggTypesData["eggTypes"], 'eggtypeEnum')
165 |
166 |
167 | return (self.starterNameByID, self.biomesByID, self.movesByID, self.voucherData,
168 | self.natureData, self.natureDataSlots, self.achievementsData, self.speciesNameByID,
169 | self.noPassiveIDs, self.hasFormIDs, self.eggTypesData)
170 |
--------------------------------------------------------------------------------
/src/utilities/limiter.py:
--------------------------------------------------------------------------------
1 | # Authors: https://github.com/JulianStiebler/
2 | # Organization: https://github.com/rogueEdit/
3 | # Repository: https://github.com/rogueEdit/OnlineRogueEditor
4 | # Contributors: None except Author
5 | # Date of release: 06.06.2024
6 | # Last Edited: 28.06.2024
7 |
8 | """
9 | This script provides a lockout mechanism to limit the frequency of function executions. It includes functionality to
10 | persistently store the last execution timestamps of functions and prevent re-execution within a specified lockout period.
11 |
12 | Features:
13 | - Limit function execution frequency with a lockout period.
14 | - Persistent storage of execution timestamps.
15 | - Colored output for log messages.
16 |
17 | Modules:
18 | - os: Module for interacting with the operating system.
19 | - json: Module for working with JSON data.
20 | - time: Module for time-related functions.
21 | - functools: Module for higher-order functions.
22 | - utilities.cFormatter: Custom formatter for colored printing and logging.
23 |
24 | Workflow:
25 | 1. Initialize the Limiter class with a lockout period and optional timestamp file path.
26 | 2. Decorate functions with the lockout decorator to enforce execution limits.
27 | 3. Use the decorated functions as usual, with lockout limits applied.
28 | """
29 |
30 | import os
31 | import json
32 | import time
33 | from functools import wraps
34 | from utilities import cFormatter, Color
35 | from modules.config import timestampFile
36 |
37 | class Limiter:
38 | """
39 | A class to handle lockout mechanism for functions to limit their execution frequency.
40 |
41 | Attributes:
42 | lockoutPeriod (int): The lockout period in seconds.
43 |
44 | Usage:
45 | Initialize the limiter and decorate functions to limit their execution frequency:
46 | >>> limiter = Limiter(lockoutPeriod=60)
47 | >>> @limiter.lockout
48 | >>> def my_function():
49 | >>> print("Function executed.")
50 |
51 | Modules:
52 | - os: Module for interacting with the operating system.
53 | - json: Module for working with JSON data.
54 | - time: Module for time-related functions.
55 | - functools: Module for higher-order functions.
56 | - utilities.cFormatter: Custom formatter for colored printing and logging.
57 | """
58 |
59 | def __init__(self, lockoutPeriod: int = 40) -> None:
60 | """
61 | Initialize the Limiter object.
62 |
63 | Args:
64 | lockoutPeriod (int): The lockout period in seconds.
65 | timestampFile (str, optional): The file path to store the timestamps. Default is './data/extra.json'.
66 |
67 | Modules:
68 | - os: Provides a way to interact with the operating system, particularly for file and directory operations.
69 | - json: Provides functionalities to work with JSON data for reading and writing timestamps.
70 | """
71 |
72 | self.lockoutPeriod = lockoutPeriod
73 | self.timestampFile = timestampFile
74 | if not os.path.exists(os.path.dirname(self.timestampFile)):
75 | os.makedirs(os.path.dirname(self.timestampFile))
76 | if not os.path.exists(self.timestampFile):
77 | with open(self.timestampFile, 'w') as f:
78 | json.dump({}, f)
79 |
80 | def lockout(self, func):
81 | """
82 | Decorator function to enforce the lockout mechanism on the decorated function.
83 |
84 | Args:
85 | func (function): The function to be decorated.
86 |
87 | Returns:
88 | function: The decorated function.
89 |
90 | Usage:
91 | Decorate a function with the lockout decorator to limit its execution frequency:
92 | >>> limiter = Limiter(lockoutPeriod=60)
93 | >>> @limiter.lockout
94 | >>> def my_function():
95 | >>> print("Function executed.")
96 |
97 | Modules:
98 | - functools: Provides utilities for higher-order functions, particularly for creating decorators.
99 | - time: Provides time-related functions, particularly for getting the current time.
100 | - utilities.cFormatter: Custom formatter for colored printing and logging.
101 | """
102 | @wraps(func)
103 | def wrapper(*args, **kwargs):
104 | funcName = func.__name__
105 | lastExecTime = self._fh_getLastExecTime(funcName)
106 | currentTime = time.time()
107 | if currentTime - lastExecTime < self.lockoutPeriod:
108 | cFormatter.print(Color.RED, f'{funcName} is rate limited. You can only do this every {self.lockoutPeriod} seconds!', isLogging=True)
109 | return None
110 | else:
111 | result = func(*args, **kwargs)
112 | self._fh_updateLastExecTime(funcName, currentTime)
113 | return result
114 | return wrapper
115 |
116 | def _fh_getLastExecTime(self, function: str) -> float:
117 | """
118 | Get the timestamp of the last execution of a function.
119 |
120 | Args:
121 | func_name (str): The name of the function.
122 |
123 | Returns:
124 | float: The timestamp of the last execution.
125 |
126 | Usage:
127 | Get the last execution time of a function:
128 | >>> lastExecTime = limiter._fh_getLastExecTime('my_function')
129 |
130 | Modules:
131 | - json: Provides functionalities to work with JSON data for reading and writing timestamps.
132 | """
133 | with open(self.timestampFile, 'r') as f:
134 | timestamps = json.load(f)
135 | return timestamps.get(function, 0)
136 |
137 | def _fh_updateLastExecTime(self, function: str, timestamp: float) -> None:
138 | """
139 | Update the timestamp of the last execution of a function.
140 |
141 | Args:
142 | func_name (str): The name of the function.
143 | timestamp (float): The timestamp of the last execution.
144 |
145 | Usage:
146 | Update the last execution time of a function:
147 | >>> limiter._fh_updateLastExecTime('my_function', time.time())
148 |
149 | Modules:
150 | - json: Provides functionalities to work with JSON data for reading and writing timestamps.
151 | """
152 | with open(self.timestampFile, 'r+') as f:
153 | try:
154 | timestamps = json.load(f)
155 | except json.decoder.JSONDecodeError:
156 | timestamps = {}
157 | timestamps[function] = timestamp
158 | f.seek(0)
159 | json.dump(timestamps, f, indent=4)
160 | f.truncate()
161 |
--------------------------------------------------------------------------------
/src/utilities/logger.py:
--------------------------------------------------------------------------------
1 | # Authors: https://github.com/JulianStiebler/
2 | # Organization: https://github.com/rogueEdit/
3 | # Repository: https://github.com/rogueEdit/OnlineRogueEditor
4 | # Contributors: None except Author
5 | # Date of release: 06.06.2024
6 | # Last Edited: 28.06.2024
7 |
8 | """
9 | This script provides a custom logger that logs messages to a weekly rotating log file. It includes functionality to
10 | exclude specific log messages and to temporarily deactivate and reactivate logging.
11 |
12 | Features:
13 | - Weekly rotating log file creation.
14 | - Custom log message filtering.
15 | - Temporary deactivation and reactivation of logging.
16 |
17 | Modules:
18 | - logging: Python's built-in logging module.
19 | - os: Module for interacting with the operating system.
20 | - logging.handlers: Module for logging handler classes.
21 | - datetime: Module for manipulating dates and times.
22 |
23 | Workflow:
24 | 1. Initialize the custom logger with a weekly rotating file handler.
25 | 2. Provide methods to deactivate and reactivate logging.
26 | """
27 | from modules import config
28 |
29 | import logging
30 | import os
31 | from logging.handlers import TimedRotatingFileHandler
32 | from datetime import datetime
33 |
34 | class CustomFilter(logging.Filter):
35 | """
36 | Custom filter to exclude log messages containing specific text.
37 |
38 | :param record: The log record to filter.
39 | :type record: logging.LogRecord
40 | :return: Whether the log record should be included.
41 | :rtype: bool
42 | """
43 | def filter(self, record):
44 | return 'data={"value":' not in record.getMessage()
45 |
46 | class CustomLogger:
47 | """
48 | A custom logger class that logs messages to a weekly log file.
49 |
50 | This logger initializes a TimedRotatingFileHandler that creates a new log file
51 | every week. It also includes a custom filter to exclude specific log messages.
52 |
53 | Usage:
54 | Initialize the custom logger in your script to start logging:
55 | >>> logger = CustomLogger()
56 |
57 | Temporarily deactivate logging:
58 | >>> CustomLogger.fh_deactivateLogging()
59 |
60 | Reactivate logging:
61 | >>> CustomLogger.fh_reactivateLogging()
62 |
63 | Output examples:
64 | - Log file created at logs/YYYY-WW.log with formatted log messages.
65 | - Log messages filtered based on custom criteria.
66 |
67 | Modules:
68 | - logging: Provides logging capabilities for creating log messages and managing log levels.
69 | - os: Interacts with the operating system for file and directory operations.
70 | - logging.handlers: Provides a handler that rotates log files at specified intervals.
71 | - datetime: Manipulates dates and times for timestamping log files.
72 | """
73 | def __init__(self):
74 | # Create and configure file handler
75 | logsDirectory = config.logsDirectory
76 |
77 | formatterFile = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
78 |
79 | # Create file handler and set level to DEBUG for file output
80 | logFilename = os.path.join(logsDirectory, f'{datetime.now().strftime("%Y-%W")}.log')
81 | fh = TimedRotatingFileHandler(logFilename, when='W0', backupCount=52)
82 | fh.setLevel(logging.DEBUG)
83 | fh.setFormatter(formatterFile)
84 |
85 | # Add custom filter to file handler
86 | fh.addFilter(CustomFilter())
87 |
88 | # Add file handler to the root logger
89 | rootLogger = logging.getLogger()
90 | rootLogger.propagate = False
91 | rootLogger.setLevel(logging.DEBUG) # Ensure root logger level is set to DEBUG
92 | if not any(isinstance(handler, TimedRotatingFileHandler) for handler in rootLogger.handlers):
93 | rootLogger.addHandler(fh)
94 |
95 | # Remove default console handler to avoid outputs since we want to display them colored with less information
96 | for handler in rootLogger.handlers:
97 | if isinstance(handler, logging.StreamHandler):
98 | rootLogger.removeHandler(handler)
99 |
100 | @staticmethod
101 | def fh_deactivateLogging():
102 | """
103 | Temporarily deactivate logging.
104 |
105 | This method sets the logging level of TimedRotatingFileHandler to NOTSET,
106 | effectively silencing logging output temporarily.
107 |
108 | Usage:
109 | Temporarily deactivate logging:
110 | >>> CustomLogger.deactivate_logging()
111 | """
112 | rootLogger = logging.getLogger()
113 | for handler in rootLogger.handlers:
114 | if isinstance(handler, TimedRotatingFileHandler):
115 | handler.setLevel(logging.NOTSET)
116 |
117 | @staticmethod
118 | def fh_reactivateLogging():
119 | """
120 | Reactivate logging.
121 |
122 | This method sets the logging level of TimedRotatingFileHandler back to DEBUG,
123 | re-enabling logging output.
124 |
125 | Usage:
126 | Reactivate logging:
127 | >>> CustomLogger.reactivate_logging()
128 | """
129 | rootLogger = logging.getLogger()
130 | for handler in rootLogger.handlers:
131 | if isinstance(handler, TimedRotatingFileHandler):
132 | handler.setLevel(logging.DEBUG)
--------------------------------------------------------------------------------
/src/utilities/propagateMessage.py:
--------------------------------------------------------------------------------
1 | # Authors https://github.com/JulianStiebler/
2 | # Organization: https://github.com/rogueEdit/
3 | # Repository: https://github.com/rogueEdit/OnlineRogueEditor
4 | # Contributors: None except Author
5 | # Date of release: 25.06.2024
6 | # Last Edited: 28.06.2024
7 |
8 | from utilities import cFormatter, Color
9 |
10 | # Initialize a global message buffer list
11 | messageBuffer = []
12 |
13 | # Function to clear the message buffer
14 | def fh_clearMessageBuffer():
15 | global messageBuffer
16 | messageBuffer = []
17 |
18 | # Function to append messages to the message buffer
19 | def fh_appendMessageBuffer(type, message, isLogging=False):
20 | global messageBuffer
21 | messageBuffer.append((type, message, isLogging))
22 |
23 | # Function to print messages from the message buffer
24 | def fh_printMessageBuffer():
25 | global messageBuffer
26 | for color, text, isLogging in messageBuffer:
27 | if isinstance(color, Color): # Check if color is a valid Color enum
28 | cFormatter.fh_centerText(text, length=55, fillChar='>')
29 | cFormatter.print(color, f'{text}', isLogging)
30 | else:
31 | print(text)
32 | messageBuffer = [] # Clear buffer after printing
33 |
34 | def fh_redundantMesage(changedItems, message, target=None, propagate=True):
35 | if target:
36 | changedItems.append(f'{target}: {message}')
37 | cFormatter.print(Color.INFO, f'{target}: {message}')
38 | if propagate:
39 | fh_appendMessageBuffer(Color.INFO, f'{target}: {message}')
40 | else:
41 | changedItems.append(f'{message}')
42 | cFormatter.print(Color.INFO, f'{message}')
43 | if propagate:
44 | fh_appendMessageBuffer(Color.INFO, f'{message}')
--------------------------------------------------------------------------------