Exciting new information regarding Pixelmon!
User avatar
By Isi
#215362
IntroductionAs we all know, battling has suffered from the infamous "waiting glitch" for a long time now. To those who experience it, the term "waiting glitch" refers to a bug that occurs during battle, causing an error that locks the player into the battle UI. The server ends up in an unrecoverable state, leaving the player waiting for instructions from the server while the server thinks it has already sent those instructions. This creates a loop that is now known as the "waiting glitch." Although it is simple to explain, there are various ways the client can end up in this loop. The example below illustrates what it looks like, and we are sure it's not an unfamiliar sight for many of you.

Image

This is an issue that has unfortunately plagued Pixelmon for many years now. In fact, the earliest mention of it dates back to 2016, and the first bug report dates back to version 4.2.7. This is unsurprising, it has existed for so long due to the fact that the most infamous "glitch" in Pixelmon is actually a combination of hundreds, and potentially thousands, of bugs all leading to the same result. This makes fixing the issue particularly challenging since when someone talks about "the waiting glitch," they could be referring to any number of potential problems. So how do we go about trying to fix it once and for all?

Battle Log
Current Version

The first steps towards eradicating the waiting glitch were taken in 2015, by adding the first iteration of battle logging. Initially, the battle log was a simple system used to record a small number of interactions that occurred during battle. It's main goal was to provide our support staff with enough information to attempt bug replication. While imperfect, the system was an important first step. It provided previously omitted information on battle details, such as battle engine, and laid the groundwork for an ever-expanding logging system.

Since it's first appearance, the battle logging system has been improving gradually. Initially, it was just a series of logs to the server console and latest log file. Over time, it been improved and fleshed out, now providing an extremely accurate read of the battle events in its own logging file. See right below for an example.

Battle Log from Pixelmon 9.1.3
Code: Select allPixelmon Version 9.1.3
Battle data 2023-03-20T05:15:24.790527Z

Team #0 Trainer Marcus
   {EVSpeed:244s,StatsDefense:228s,IVAttack:31b,Growth:3b,Moveset:[{MoveID:"V-Create",MovePP:5b},{MoveID:"Double-Edge",MovePP:15b},{MoveID:"Zen Headbutt",MovePP:15b},{MoveID:"Fusion Bolt",MovePP:5b}],PersistentData:{},Gender:2b,GigantamaxFactor:0b,Friendship:79s,eggCycles:-1,EVAttack:252s,StatsSpecialAttack:218s,IVSpDef:27b,CaughtBall:"poke_ball",IVHP:29b,EVHP:12s,ndex:494,palette:"none",UUID:[I;-802439258,-57129610,-1145189544,2021589950],StatsSpecialDefense:239s,Nature:7b,Ability:"VictoryStar",StatsHP:342s,IVDefense:16b,Health:0,HeldItemStack:{id:"pixelmon:firium_z",Count:1b},SpecFlags:[],DoesLevel:1b,EVDefense:31s,DynamaxLevel:0,Variant:"",StatsSpeed:297s,EVSpecialAttack:31s,StatsAttack:328s,IVSpeed:31b,Level:100,IVSpAtt:31b,NBT_VERSION:2b,EVSpecialDefense:31s,EXP:0,RelrnMoves:[],ribbons2:[]}
   {EVSpeed:31s,StatsDefense:188s,IVAttack:31b,Growth:3b,Moveset:[{MoveID:"Synchronoise",MovePP:15b},{MoveID:"Thunderbolt",MovePP:15b},{MoveID:"Energy Ball",MovePP:10b},{MoveID:"Shadow Ball",MovePP:15b}],PersistentData:{},Gender:0b,GigantamaxFactor:0b,Friendship:70s,eggCycles:-1,EVAttack:31s,StatsSpecialAttack:383s,IVSpDef:31b,CaughtBall:"poke_ball",IVHP:31b,EVHP:248s,ndex:606,palette:"none",UUID:[I;-1493068646,-1156821198,-1686098395,-746438184],StatsSpecialDefense:233s,Nature:19b,Ability:"Analytic",StatsHP:353s,IVDefense:31b,Health:353,HeldItemStack:{id:"pixelmon:psychium_z",Count:1b},SpecFlags:[],DoesLevel:1b,EVDefense:8s,DynamaxLevel:0,Variant:"",StatsSpeed:110s,EVSpecialAttack:252s,StatsAttack:193s,IVSpeed:31b,Level:100,IVSpAtt:31b,NBT_VERSION:2b,EVSpecialDefense:31s,EXP:0,RelrnMoves:[],ribbons2:[]}
   {EVSpeed:31s,StatsDefense:275s,IVAttack:31b,Growth:3b,Moveset:[{MoveID:"Trick Room",MovePP:5b},{MoveID:"Gyro Ball",MovePP:2b},{MoveID:"Hypnosis",MovePP:20b},{MoveID:"Earthquake",MovePP:10b}],PersistentData:{},Gender:2b,GigantamaxFactor:0b,Friendship:49s,eggCycles:-1,EVAttack:252s,StatsSpecialAttack:201s,IVSpDef:31b,CaughtBall:"poke_ball",IVHP:31b,EVHP:252s,ndex:437,palette:"none",UUID:[I;343232573,-1482405026,-1763020549,1635543755],StatsSpecialDefense:269s,Nature:6b,Ability:"Levitate",StatsHP:338s,IVDefense:31b,Health:0,HeldItemStack:{id:"pixelmon:macho_brace",Count:1b},SpecFlags:[],DoesLevel:1b,EVDefense:31s,DynamaxLevel:0,Variant:"",StatsSpeed:98s,EVSpecialAttack:31s,StatsAttack:304s,IVSpeed:31b,Level:100,IVSpAtt:31b,NBT_VERSION:2b,EVSpecialDefense:4s,EXP:0,RelrnMoves:[],ribbons2:[]}
   {EVSpeed:252s,StatsDefense:213s,IVAttack:31b,Growth:3b,Moveset:[{MoveID:"Earthquake",MovePP:10b},{MoveID:"Flare Blitz",MovePP:15b},{MoveID:"Zen Headbutt",MovePP:15b},{MoveID:"Rock Slide",MovePP:10b}],PersistentData:{},Gender:2b,GigantamaxFactor:0b,Friendship:49s,eggCycles:-1,EVAttack:252s,StatsSpecialAttack:137s,IVSpDef:31b,CaughtBall:"poke_ball",IVHP:31b,EVHP:31s,ndex:338,palette:"none",UUID:[I;-387206736,-2085401303,-1163555311,-1021743869],StatsSpecialDefense:167s,Nature:7b,Ability:"Levitate",StatsHP:328s,IVDefense:31b,Health:0,HeldItemStack:{id:"pixelmon:groundium_z",Count:1b},SpecFlags:[],DoesLevel:1b,EVDefense:31s,DynamaxLevel:0,Variant:"",StatsSpeed:239s,EVSpecialAttack:31s,StatsAttack:317s,IVSpeed:31b,Level:100,IVSpAtt:31b,NBT_VERSION:2b,EVSpecialDefense:4s,EXP:0,RelrnMoves:[],ribbons2:[]}
   {EVSpeed:164s,StatsDefense:184s,IVAttack:31b,Growth:3b,Moveset:[{MoveID:"Draco Meteor",MovePP:2b},{MoveID:"Psychic",MovePP:10b},{MoveID:"Energy Ball",MovePP:10b},{MoveID:"Surf",MovePP:15b}],PersistentData:{},Gender:0b,GigantamaxFactor:0b,Friendship:49s,eggCycles:-1,EVAttack:31s,StatsSpecialAttack:394s,IVSpDef:28b,CaughtBall:"poke_ball",IVHP:31b,EVHP:92s,ndex:381,palette:"none",UUID:[I;-1648258871,-1104264084,-1460113823,987270784],StatsSpecialDefense:260s,Nature:17b,Ability:"Levitate",StatsHP:324s,IVDefense:12b,Health:0,HeldItemStack:{id:"pixelmon:soul_dew",Count:1b},SpecFlags:[],DoesLevel:1b,EVDefense:31s,DynamaxLevel:0,Variant:"",StatsSpeed:293s,EVSpecialAttack:252s,StatsAttack:200s,IVSpeed:27b,Level:100,IVSpAtt:31b,NBT_VERSION:2b,EVSpecialDefense:31s,EXP:0,RelrnMoves:[],ribbons2:[]}
   {EVSpeed:180s,StatsDefense:169s,IVAttack:31b,Growth:3b,Moveset:[{MoveID:"Moonblast",MovePP:14b},{MoveID:"Psychic",MovePP:10b},{MoveID:"Energy Ball",MovePP:10b},{MoveID:"Thunderbolt",MovePP:15b}],PersistentData:{},Gender:2b,GigantamaxFactor:0b,Friendship:0s,eggCycles:-1,EVAttack:31s,StatsSpecialAttack:359s,IVSpDef:20b,CaughtBall:"poke_ball",IVHP:31b,EVHP:76s,ndex:786,palette:"none",UUID:[I;-377919340,1578585221,-2083563578,1747168896],StatsSpecialDefense:262s,Nature:13b,Ability:"PsychicSurge",StatsHP:300s,IVDefense:7b,Health:282,HeldItemStack:{id:"pixelmon:terrain_extender",Count:1b},SpecFlags:[],DoesLevel:1b,EVDefense:31s,DynamaxLevel:0,Variant:"",StatsSpeed:270s,EVSpecialAttack:252s,StatsAttack:191s,IVSpeed:6b,Level:100,IVSpAtt:31b,NBT_VERSION:2b,EVSpecialDefense:31s,EXP:0,RelrnMoves:[],ribbons2:[]}
Team #1 Player playerA
   {EVSpeed:252s,StatsDefense:213s,IVAttack:31b,Growth:3b,Moveset:[{MoveID:"Dragon Tail",MovePP:10b},{MoveID:"Aerial Ace",MovePP:20b},{MoveID:"Bolt Beak",MovePP:7b},{MoveID:"Dragon Rush",MovePP:10b}],PersistentData:{},Gender:2b,originalTrainerUUID:[I;1958116753,-741785370,-1500661654,1355589333],GigantamaxFactor:0b,Friendship:70s,eggCycles:-1,EVAttack:252s,StatsSpecialAttack:176s,IVSpDef:25b,CaughtBall:"poke_ball",IVHP:7b,EVHP:0s,ndex:880,palette:"shiny",UUID:[I;1813242995,381634496,-1313621995,1010972518],StatsSpecialDefense:170s,Nickname:"Bolt Beak",Nature:7b,Ability:"Hustle",StatsHP:297s,IVDefense:27b,Health:297,HeldItemStack:{id:"pixelmon:choice_scarf",Count:1b},originalTrainer:"orEgrown43",SpecFlags:["unbreedable"],DoesLevel:1b,EVDefense:6s,DynamaxLevel:0,Variant:"",StatsSpeed:249s,EVSpecialAttack:0s,StatsAttack:328s,IVSpeed:31b,Level:100,IVSpAtt:31b,NBT_VERSION:2b,EVSpecialDefense:0s,EXP:759,RelrnMoves:[],ribbons2:[]}
   {EVSpeed:252s,StatsDefense:157s,IVAttack:9b,Growth:3b,Moveset:[{MoveID:"Psychic",MovePP:10b},{MoveID:"Dream Eater",MovePP:15b},{MoveID:"Moonblast",MovePP:15b},{MoveID:"Hypnosis",MovePP:20b}],PersistentData:{ot_copy:[I;446931056,-1108523974,-1075870883,482821422],ot_name_copy:"Boatman09"},Gender:0b,originalTrainerUUID:[I;446931056,-1108523974,-1075870883,482821422],GigantamaxFactor:0b,Friendship:152s,eggCycles:-1,EVAttack:0s,StatsSpecialAttack:348s,IVSpDef:27b,CaughtBall:"master_ball",IVHP:30b,EVHP:6s,ndex:282,palette:"none",UUID:[I;-1393535308,648954013,-1106730996,1899907988],StatsSpecialDefense:262s,Nickname:"Fem Boy",Nature:2b,Ability:"Trace",StatsHP:277s,IVDefense:22b,Health:277,HeldItemStack:{id:"pixelmon:gardevoirite",Count:1b},originalTrainer:"Boatman09",SpecFlags:[],DoesLevel:1b,EVDefense:0s,DynamaxLevel:0,Variant:"",StatsSpeed:259s,EVSpecialAttack:252s,StatsAttack:144s,IVSpeed:31b,Level:100,IVSpAtt:30b,NBT_VERSION:2b,EVSpecialDefense:0s,EXP:1710,RelrnMoves:[],ribbons2:[]}
   {EVSpeed:252s,StatsDefense:166s,IVAttack:31b,Growth:2b,Moveset:[{MoveID:"Flare Blitz",MovePP:15b},{MoveID:"Flamethrower",MovePP:15b},{MoveID:"Head Smash",MovePP:5b},{MoveID:"Hammer Arm",MovePP:10b}],PersistentData:{},Gender:0b,originalTrainerUUID:[I;1958116753,-741785370,-1500661654,1355589333],GigantamaxFactor:0b,Friendship:127s,eggCycles:-1,EVAttack:252s,StatsSpecialAttack:200s,IVSpDef:25b,CaughtBall:"premier_ball",IVHP:10b,EVHP:6s,ndex:500,palette:"shiny",UUID:[I;452984287,1441612886,-1924398740,1216041628],StatsSpecialDefense:160s,Nickname:"petpig",Nature:15b,Ability:"Blaze",StatsHP:341s,IVDefense:31b,Health:341,HeldItemStack:{id:"pixelmon:hard_stone",Count:1b},originalTrainer:"orEgrown43",SpecFlags:[],DoesLevel:1b,EVDefense:0s,DynamaxLevel:0,Variant:"",StatsSpeed:251s,EVSpecialAttack:0s,StatsAttack:345s,IVSpeed:31b,Level:100,IVSpAtt:18b,NBT_VERSION:2b,EVSpecialDefense:0s,EXP:1258,RelrnMoves:[],ribbons2:[{receiver:'{"text":""}',received:1677970149334L,type:"destiny"}]}
   {EVSpeed:252s,StatsDefense:216s,IVAttack:27b,Growth:7b,Moveset:[{MoveID:"Dynamax Cannon",MovePP:8b,MovePPLevel:3b},{MoveID:"Earth Power",MovePP:10b},{MoveID:"Nasty Plot",MovePP:20b},{MoveID:"Fire Blast",MovePP:5b}],PersistentData:{},Gender:1b,originalTrainerUUID:[I;1153850581,523323166,-1844284862,1920071412],GigantamaxFactor:0b,Friendship:195s,eggCycles:-1,EVAttack:0s,StatsSpecialAttack:383s,IVSpDef:31b,CaughtBall:"poke_ball",IVHP:20b,EVHP:6s,ndex:635,palette:"none",UUID:[I;907616676,335564601,-1708247319,-1359285417],StatsSpecialDefense:216s,Nickname:"A RARWRUH",Nature:17b,Ability:"Levitate",StatsHP:315s,IVDefense:31b,Health:308,HeldItemStack:{id:"pixelmon:dragon_fang",Count:1b},originalTrainer:"Grendel541",SpecFlags:["unbreedable"],DoesLevel:1b,EVDefense:0s,DynamaxLevel:6,Variant:"alpha",StatsSpeed:266s,EVSpecialAttack:252s,StatsAttack:217s,IVSpeed:2b,Level:100,IVSpAtt:31b,NBT_VERSION:2b,EVSpecialDefense:0s,EXP:21201,RelrnMoves:[],ribbons2:[]}
   {EVSpeed:252s,StatsDefense:237s,IVAttack:31b,Growth:7b,Moveset:[{MoveID:"Hydro Pump",MovePP:5b},{MoveID:"Aura Sphere",MovePP:20b},{MoveID:"Power Gem",MovePP:20b},{MoveID:"Spacial Rend",MovePP:3b}],PersistentData:{},Gender:2b,originalTrainerUUID:[I;1958116753,-741785370,-1500661654,1355589333],GigantamaxFactor:0b,Friendship:151s,eggCycles:-1,EVAttack:0s,StatsSpecialAttack:399s,IVSpDef:16b,CaughtBall:"master_ball",IVHP:5b,EVHP:2s,ndex:484,palette:"none",UUID:[I;-146564322,-969979663,-2138985116,-1156665176],StatsSpecialDefense:261s,Nature:2b,Ability:"Pressure",StatsHP:295s,IVDefense:31b,Health:0,originalTrainer:"orEgrown43",SpecFlags:[],DoesLevel:1b,EVDefense:4s,DynamaxLevel:0,Variant:"",StatsSpeed:275s,EVSpecialAttack:252s,StatsAttack:276s,IVSpeed:7b,Level:100,IVSpAtt:31b,NBT_VERSION:2b,EVSpecialDefense:0s,EXP:165,RelrnMoves:[],ribbons2:[]}
   {EVSpeed:0s,StatsDefense:282s,IVAttack:27b,Growth:4b,Moveset:[{MoveID:"Stone Edge",MovePP:5b},{MoveID:"Crunch",MovePP:14b},{MoveID:"Thunder Fang",MovePP:15b},{MoveID:"Earthquake",MovePP:10b}],PersistentData:{},Gender:1b,originalTrainerUUID:[I;1958116753,-741785370,-1500661654,1355589333],GigantamaxFactor:0b,Friendship:236s,eggCycles:-1,EVAttack:252s,StatsSpecialAttack:243s,IVSpDef:24b,CaughtBall:"ultra_ball",IVHP:28b,EVHP:2s,ndex:248,palette:"none",UUID:[I;-1012978465,868305276,-2105354763,1024252689],StatsSpecialDefense:261s,Nature:19b,Ability:"SandStream",StatsHP:338s,IVDefense:25b,Health:0,HeldItemStack:{id:"pixelmon:scope_lens",Count:1b},originalTrainer:"orEgrown43",SpecFlags:[],DoesLevel:1b,EVDefense:128s,DynamaxLevel:0,Variant:"",StatsSpeed:135s,EVSpecialAttack:0s,StatsAttack:363s,IVSpeed:23b,Level:100,IVSpAtt:26b,NBT_VERSION:2b,EVSpecialDefense:128s,EXP:3536,RelrnMoves:[],ribbons2:[]}

Turn #0
 - Player playerA pokemon=Dracozolt, action=ATTACK, attack=AttackBase{attackName='Bolt Beak'}, targets=Latios , results=[MoveResults{damage=342, fullDamage=455, accuracy=0, priority=0.0, result=killed, weightMod=0.0}]
Turn #1
 - Player playerA pokemon=Dracozolt, action=ATTACK, attack=AttackBase{attackName='Bolt Beak'}, targets=Latios , results=[MoveResults{damage=328, fullDamage=438, accuracy=0, priority=0.0, result=killed, weightMod=0.0}]
Turn #2
 - Player playerA pokemon=Dracozolt, action=ATTACK, attack=AttackBase{attackName='Bolt Beak'}, targets=Latios , results=[MoveResults{damage=279, fullDamage=279, accuracy=0, priority=0.0, result=hit, weightMod=0.0}]
 - Trainer Marcus pokemon=Latios, action=ATTACK, attack=AttackBase{attackName='Draco Meteor'}, targets=Palkia , results=[MoveResults{damage=297, fullDamage=826, accuracy=0, priority=0.0, result=succeeded, weightMod=0.0}]
Turn #3
 - Trainer Marcus pokemon=Latios, action=ATTACK, attack=AttackBase{attackName='Draco Meteor'}, targets=Palkia , results=[MoveResults{damage=261, fullDamage=261, accuracy=0, priority=0.0, result=succeeded, weightMod=0.0}]
 - Player playerA pokemon=Palkia, action=ATTACK, attack=AttackBase{attackName='Spacial Rend'}, targets=Bronzong , results=[MoveResults{damage=45, fullDamage=345, accuracy=0, priority=0.0, result=killed, weightMod=0.0}]
Turn #4
 - Player playerA pokemon=Palkia, action=ATTACK, attack=AttackBase{attackName='Spacial Rend'}, targets=Bronzong , results=[MoveResults{damage=0, fullDamage=0, accuracy=0, priority=0.0, result=missed, weightMod=0.0}]
 - Trainer Marcus pokemon=Bronzong, action=ATTACK, attack=AttackBase{attackName='Gyro Ball'}, targets=Tyranitar , results=[MoveResults{damage=34, fullDamage=108, accuracy=0, priority=0.0, result=killed, weightMod=0.0}]
Turn #5
 - Player playerA pokemon=Tyranitar, action=BAG, item=pixelmon:max_revive
 - Trainer Marcus pokemon=Bronzong, action=ATTACK, attack=AttackBase{attackName='Gyro Ball'}, targets=Tyranitar , results=[MoveResults{damage=266, fullDamage=266, accuracy=0, priority=0.0, result=hit, weightMod=0.0}]
Turn #6
 - Player playerA pokemon=Tyranitar, action=ATTACK, attack=AttackBase{attackName='Crunch'}, targets=TapuLele , results=[MoveResults{damage=338, fullDamage=371, accuracy=0, priority=0.0, result=succeeded, weightMod=0.0}]
Turn #7
 - Trainer Marcus pokemon=TapuLele, action=ATTACK, attack=AttackBase{attackName='Moonblast'}, targets=Dracozolt , results=[MoveResults{damage=72, fullDamage=196, accuracy=0, priority=0.0, result=succeeded, weightMod=0.0}]


As shown above, battle logs have come a long way from a crude list of events. Currently, battle logs are segmented according to battle proceedings. First, they provide information about the teams involved in the battle by printing the NBT tags of the player's parties at the top. Second, it describes the moves that were selected in each turn, along with their attack result, indicating how much damage was done and whether the attack was successful or missed. Though useful, this information is concernedly incomplete. Battle logs, in their current iteration, do not describe the inner workings of the battle or either allow us to replicate the battle. While this is to be expected due to the large amount of randomness is involved in battling, it does make it impossible for us to know exactly what happened during the battle.

New Version

Due to the flaws of the current system, we have decided to rework the battle log as the first step towards a more organized effort to fixing all waiting glitches. Initially, this was an intimidating and overwhelming task - a large number of potential places would require logging. Moreover, it needed to be easy to read and understand, while including all relevant information for effective bug fixing.

The first and most important part of the rework was to dismantle an old system, which prevented battles from ending due to errors. This system would capture fatal errors that occurred during battle logic, record the error as a “crash” while discarding the error, and then wait for two further fatal errors before ending the battle. This was a significant oversight in the old system. While it prevented a lot of crashes, it also meant that some battle errors were being lost, potentially causing additional waiting glitch situations by leaving the battle in an unrecoverable state. As of version 9.1.4, if a battle encounters a fatal error, it will immediately end and log the error to the “logs/battle/” directory of Minecraft. A message will appear in chat telling the user to report the bug to our bug tracker, or Discord.

Next, we decided that more information was needed in between turns. This information would allow us to replicate battles more accurately because we could see the state of the two parties at the end and beginning of each turn. It may seem redundant to log this information between turns; however, it's quite the opposite. It in fact allows us to ensure that no logic modifies the state of the battle between turns. In addition, it provides a clean break between turns, making it easier to read and decipher when scrolling through the log.

We also decided that it was important to include the battle messages from the player-visible log in the battle log. This way, even when the actual logging system failed to capture an event, we could at least get an idea of the player's perspective. This also allows us to reduce the number of required actions being logged, offering information about the "random" events that might occur.

Then it was time to add logs to actions during the battle. When deciding what we should be logging, we tried to look at the points of the battle that are likely to cause the most problems. For example, we added a log for when the player's 'select attack packet' is received. This is particularly important as it allows the log to indicate if the packet was lost or not. As a result, we added all of the following to the log:

New battle log actions for 9.1.4
Code: Select allSelect move
Battle message
Change ability
Change type
Damage pokemon
Enter dynamax
Exit dynamax
Global status add
Global status remove
Heal pokemon
Mega evolve
Player command
Stat change
Status add
Status remove
Terrain change
Turn begin
Turn end
Ultra burst
Weather change


Finally, after a couple of rounds of play-testing and bug fixing, the support team decided that the team descriptions at the top of the log were unfriendly and made it very difficult to understand the teams at the start of the battle. The initial discussion focused on reducing irrelevant information from the NBT data; however, this would lead to potentially erroneous attempts at copying the initial party state. So, it was decided that the best approach would be to upload the teams to PokePaste and include the link to the team underneath. This allows for a readable format with a much faster method of replicating the team while also keeping the more precise NBT method.

All of this resulted in a new battle log that looks like this:

9.1.4 Battle Log
Code: Select allPixelmon Version 9.1.3
Battle data 2023-03-27T14:46:49.690281600Z

Team #0 Player Dev pixelmon:ultra_space (8623.861299180464, 72.0, 14601.598592660941)
   Alomomola {EVSpeed:0s,StatsDefense:6s,IVAttack:18b,Growth:4b,Moveset:[{MoveID:"Helping Hand",MovePP:20b},{MoveID:"Hydro Pump",MovePP:5b},{MoveID:"Play Nice",MovePP:18b},{MoveID:"Wide Guard",MovePP:10b}],PersistentData:{},Gender:1b,originalTrainerUUID:[I;940439953,-167562164,-1601161573,-1389718966],GigantamaxFactor:0b,Friendship:70s,eggCycles:-1,EVAttack:0s,StatsSpecialAttack:5s,IVSpDef:16b,CaughtBall:"poke_ball",IVHP:5b,EVHP:0s,ndex:594,palette:"none",UUID:[I;-631063276,674778972,-1171883285,-408996263],StatsSpecialDefense:6s,Nature:23b,Ability:"Healer",StatsHP:14s,IVDefense:27b,Health:14,originalTrainer:"Dev",SpecFlags:[],DoesLevel:1b,EVDefense:0s,DynamaxLevel:0,Variant:"",StatsSpeed:5s,EVSpecialAttack:0s,StatsAttack:6s,IVSpeed:21b,Level:1,IVSpAtt:8b,NBT_VERSION:2b,EVSpecialDefense:0s,EXP:0,RelrnMoves:[],ribbons2:[]}
   Buneary {EVSpeed:0s,StatsDefense:4s,IVAttack:22b,Growth:3b,Moveset:[{MoveID:"Pound",MovePP:35b},{MoveID:"Splash",MovePP:40b}],PersistentData:{},Gender:1b,originalTrainerUUID:[I;940439953,-167562164,-1601161573,-1389718966],GigantamaxFactor:0b,Friendship:0s,eggCycles:-1,EVAttack:0s,StatsSpecialAttack:5s,IVSpDef:15b,CaughtBall:"poke_ball",IVHP:26b,EVHP:0s,ndex:427,palette:"none",UUID:[I;1222943274,-1970516950,-1989001930,1331607483],StatsSpecialDefense:6s,Nature:14b,Ability:"RunAway",Status:130s,StatsHP:12s,IVDefense:11b,Health:12,originalTrainer:"Dev",SpecFlags:[],DoesLevel:1b,EVDefense:0s,DynamaxLevel:0,Variant:"",StatsSpeed:6s,EVSpecialAttack:0s,StatsAttack:6s,IVSpeed:20b,Level:1,IVSpAtt:2b,NBT_VERSION:2b,EVSpecialDefense:0s,EXP:0,RelrnMoves:[],ribbons2:[]}
   Nidoranmale {EVSpeed:0s,StatsDefense:17s,IVAttack:15b,Growth:1b,Moveset:[{MoveID:"Poison Sting",MovePP:35b},{MoveID:"Peck",MovePP:35b},{MoveID:"Focus Energy",MovePP:30b},{MoveID:"Fury Attack",MovePP:20b}],PersistentData:{},Gender:0b,originalTrainerUUID:[I;940439953,-167562164,-1601161573,-1389718966],GigantamaxFactor:0b,Friendship:71s,eggCycles:-1,EVAttack:0s,StatsSpecialAttack:18s,IVSpDef:18b,CaughtBall:"poke_ball",IVHP:15b,EVHP:0s,ndex:32,palette:"none",UUID:[I;-1880043947,21315839,-1087399627,1238826516],StatsSpecialDefense:20s,Nature:15b,Ability:"PoisonPoint",Status:130s,StatsHP:43s,IVDefense:0b,Health:43,originalTrainer:"Dev",SpecFlags:[],DoesLevel:1b,EVDefense:0s,DynamaxLevel:0,Variant:"",StatsSpeed:27s,EVSpecialAttack:0s,StatsAttack:25s,IVSpeed:25b,Level:16,IVSpAtt:25b,NBT_VERSION:2b,EVSpecialDefense:0s,EXP:0,RelrnMoves:[],ribbons2:[]}
 URL: https://pokepast.es//48c76891608e5964

Team #1 WildPokemon Kirlia pixelmon:ultra_space (8619.43523789586, 74.0, 14597.395792256508)
   Kirlia {EVSpeed:0s,StatsDefense:22s,IVAttack:26b,Growth:5b,Moveset:[{MoveID:"Double Team",MovePP:15b},{MoveID:"Hypnosis",MovePP:19b},{MoveID:"Teleport",MovePP:20b},{MoveID:"Psybeam",MovePP:20b}],PersistentData:{},Gender:0b,GigantamaxFactor:0b,Friendship:35s,eggCycles:-1,EVAttack:0s,StatsSpecialAttack:29s,IVSpDef:20b,CaughtBall:"poke_ball",IVHP:6b,EVHP:0s,ndex:281,palette:"none",UUID:[I;1746548599,1826770630,-1345556839,-885903729],StatsSpecialDefense:34s,Nature:24b,Ability:"Trace",StatsHP:46s,IVDefense:16b,Health:46,SpecFlags:[],DoesLevel:1b,EVDefense:0s,DynamaxLevel:0,Variant:"",StatsSpeed:29s,EVSpecialAttack:0s,StatsAttack:24s,IVSpeed:24b,Level:20,IVSpAtt:10b,NBT_VERSION:2b,EVSpecialDefense:0s,EXP:0,RelrnMoves:[],ribbons2:[]}
 URL: https://pokepast.es//8c2f142b0eba973d

Turn #0 has begun
 Terrain: NONE[5 turns remaining]
 Weather: none
 Pokemon:
 Alomomola[escape attempts 0 damage taken this turn 0 priority 0.0 can attack true will try flee false switching false evolve false moveset Moveset{pokemon=Pokemon{Alomomola}, attacks=[Helping Hand[pp 20, ppLevel 0, movePower 0, moveAccuracy 0, cantMiss false, disabled false, savedAttack null, savedPower 0, savedAccuracy 0, overridePPMax null, overrideAttackCategory null, overrideType null, isZ false, isMax false, originalMove null], Hydro Pump[pp 5, ppLevel 0, movePower 110, moveAccuracy 0, cantMiss false, disabled false, savedAttack null, savedPower 0, savedAccuracy 0, overridePPMax null, overrideAttackCategory null, overrideType null, isZ false, isMax false, originalMove null], Play Nice[pp 18, ppLevel 0, movePower 0, moveAccuracy -1, cantMiss true, disabled false, moveResult MoveResults{damage=0, fullDamage=0, accuracy=0, priority=0.0, result=succeeded, weightMod=0.0}, damageResult 0.0, didCrit false, savedAttack AttackBase{attackName='Play Nice'}, savedPower 0, savedAccuracy -1, overridePPMax null, overrideAttackCategory null, overrideType null, isZ false, isMax false, originalMove null], Wide Guard[pp 10, ppLevel 0, movePower 0, moveAccuracy 0, cantMiss false, disabled false, savedAttack null, savedPower 0, savedAccuracy 0, overridePPMax null, overrideAttackCategory null, overrideType null, isZ false, isMax false, originalMove null]], ability=com.pixelmonmod.pixelmon.api.pokemon.ability.abilities.Healer@8099ecb5, reminderMoves=[]} status com.pixelmonmod.pixelmon.battles.status.Drowsy@4ceea848]
 Kirlia[escape attempts 0 damage taken this turn 0 priority 0.0 can attack true will try flee false switching false evolve false moveset Moveset{pokemon=Pokemon{Kirlia}, attacks=[Double Team[pp 15, ppLevel 0, movePower 0, moveAccuracy 0, cantMiss false, disabled false, savedAttack null, savedPower 0, savedAccuracy 0, overridePPMax null, overrideAttackCategory null, overrideType null, isZ false, isMax false, originalMove null], Hypnosis[pp 19, ppLevel 0, movePower 0, moveAccuracy 60, cantMiss false, disabled false, moveResult MoveResults{damage=0, fullDamage=0, accuracy=0, priority=0.0, result=missed, weightMod=0.0}, damageResult -1.0, didCrit false, savedAttack AttackBase{attackName='Hypnosis'}, savedPower 0, savedAccuracy 60, overridePPMax null, overrideAttackCategory null, overrideType null, isZ false, isMax false, originalMove null], Teleport[pp 20, ppLevel 0, movePower 0, moveAccuracy 0, cantMiss false, disabled false, savedAttack null, savedPower 0, savedAccuracy 0, overridePPMax null, overrideAttackCategory null, overrideType null, isZ false, isMax false, originalMove null], Psybeam[pp 20, ppLevel 0, movePower 65, moveAccuracy 0, cantMiss false, disabled false, savedAttack null, savedPower 0, savedAccuracy 0, overridePPMax null, overrideAttackCategory null, overrideType null, isZ false, isMax false, originalMove null]], ability=com.pixelmonmod.pixelmon.api.pokemon.ability.abilities.Trace@4d50125, reminderMoves=[]} status ]

Alomomola stats changed from [ACCURACY=100, EVASION=100, ATTACK=0, DEFENSE=0, SPECIAL_ATTACK=0, SPECIAL_DEFENSE=0, SPEED=0] to [ACCURACY=100, EVASION=100, ATTACK=6, DEFENSE=6, SPECIAL_ATTACK=5, SPECIAL_DEFENSE=6, SPEED=5] delta: ATTACK=-6, DEFENSE=-6, SPECIAL_ATTACK=-5, SPECIAL_DEFENSE=-6, SPEED=-5
Kirlia stats changed from [ACCURACY=100, EVASION=100, ATTACK=0, DEFENSE=0, SPECIAL_ATTACK=0, SPECIAL_DEFENSE=0, SPEED=0] to [ACCURACY=100, EVASION=100, ATTACK=24, DEFENSE=22, SPECIAL_ATTACK=29, SPECIAL_DEFENSE=34, SPEED=29] delta: ATTACK=-24, DEFENSE=-22, SPECIAL_ATTACK=-29, SPECIAL_DEFENSE=-34, SPEED=-29
Battle Message: Kirlia traced Alomomola's Healer!
Kirlia changed abilities from Trace to Healer
Kirlia selected attack Hypnosis targeting Alomomola and opted to mega evolve
Dev selected attack Play Nice targeting Kirlia
Battle Message: Kirlia used Hypnosis!
Battle Message: Alomomola avoided the attack!
Kirlia used Hypnosis against Alomomola with results MoveResults{damage=0, fullDamage=0, accuracy=0, priority=0.0, result=missed, weightMod=0.0}
Battle Message: Alomomola is drowsy!
Battle Message: Alomomola used Play Nice!
Battle Message: Kirlia's Attack was decreased!
Dev told Alomomola to use Play Nice against Kirlia with results MoveResults{damage=0, fullDamage=0, accuracy=0, priority=0.0, result=succeeded, weightMod=0.0}

Turn #0 has ended
 Terrain: NONE[5 turns remaining]
 Weather: none
 Pokemon:
 Alomomola[escape attempts 0 damage taken this turn 0 priority 0.0 can attack true will try flee false switching false evolve false moveset Moveset{pokemon=Pokemon{Alomomola}, attacks=[Helping Hand[pp 20, ppLevel 0, movePower 0, moveAccuracy 0, cantMiss false, disabled false, savedAttack null, savedPower 0, savedAccuracy 0, overridePPMax null, overrideAttackCategory null, overrideType null, isZ false, isMax false, originalMove null], Hydro Pump[pp 5, ppLevel 0, movePower 110, moveAccuracy 0, cantMiss false, disabled false, savedAttack null, savedPower 0, savedAccuracy 0, overridePPMax null, overrideAttackCategory null, overrideType null, isZ false, isMax false, originalMove null], Play Nice[pp 18, ppLevel 0, movePower 0, moveAccuracy -1, cantMiss true, disabled false, moveResult MoveResults{damage=0, fullDamage=0, accuracy=0, priority=0.0, result=succeeded, weightMod=0.0}, damageResult 0.0, didCrit false, savedAttack AttackBase{attackName='Play Nice'}, savedPower 0, savedAccuracy -1, overridePPMax null, overrideAttackCategory null, overrideType null, isZ false, isMax false, originalMove null], Wide Guard[pp 10, ppLevel 0, movePower 0, moveAccuracy 0, cantMiss false, disabled false, savedAttack null, savedPower 0, savedAccuracy 0, overridePPMax null, overrideAttackCategory null, overrideType null, isZ false, isMax false, originalMove null]], ability=com.pixelmonmod.pixelmon.api.pokemon.ability.abilities.Healer@8099ecb5, reminderMoves=[]} status com.pixelmonmod.pixelmon.battles.status.Drowsy@4ceea848]
 Kirlia[escape attempts 0 damage taken this turn 0 priority 0.0 can attack true will try flee false switching false evolve false moveset Moveset{pokemon=Pokemon{Kirlia}, attacks=[Double Team[pp 15, ppLevel 0, movePower 0, moveAccuracy 0, cantMiss false, disabled false, savedAttack null, savedPower 0, savedAccuracy 0, overridePPMax null, overrideAttackCategory null, overrideType null, isZ false, isMax false, originalMove null], Hypnosis[pp 19, ppLevel 0, movePower 0, moveAccuracy 60, cantMiss false, disabled false, moveResult MoveResults{damage=0, fullDamage=0, accuracy=0, priority=0.0, result=missed, weightMod=0.0}, damageResult -1.0, didCrit false, savedAttack AttackBase{attackName='Hypnosis'}, savedPower 0, savedAccuracy 60, overridePPMax null, overrideAttackCategory null, overrideType null, isZ false, isMax false, originalMove null], Teleport[pp 20, ppLevel 0, movePower 0, moveAccuracy 0, cantMiss false, disabled false, savedAttack null, savedPower 0, savedAccuracy 0, overridePPMax null, overrideAttackCategory null, overrideType null, isZ false, isMax false, originalMove null], Psybeam[pp 20, ppLevel 0, movePower 65, moveAccuracy 0, cantMiss false, disabled false, savedAttack null, savedPower 0, savedAccuracy 0, overridePPMax null, overrideAttackCategory null, overrideType null, isZ false, isMax false, originalMove null]], ability=com.pixelmonmod.pixelmon.api.pokemon.ability.abilities.Trace@4d50125, reminderMoves=[]} status ]

Turn #1 has begun
 Terrain: NONE[5 turns remaining]
 Weather: none
 Pokemon:
 Alomomola[escape attempts 0 damage taken this turn 0 priority 0.0 can attack true will try flee false switching false evolve false moveset Moveset{pokemon=Pokemon{Alomomola}, attacks=[Helping Hand[pp 20, ppLevel 0, movePower 0, moveAccuracy 0, cantMiss false, disabled false, savedAttack null, savedPower 0, savedAccuracy 0, overridePPMax null, overrideAttackCategory null, overrideType null, isZ false, isMax false, originalMove null], Hydro Pump[pp 5, ppLevel 0, movePower 110, moveAccuracy 0, cantMiss false, disabled false, savedAttack null, savedPower 0, savedAccuracy 0, overridePPMax null, overrideAttackCategory null, overrideType null, isZ false, isMax false, originalMove null], Play Nice[pp 18, ppLevel 0, movePower 0, moveAccuracy -1, cantMiss true, disabled false, moveResult MoveResults{damage=0, fullDamage=0, accuracy=0, priority=0.0, result=succeeded, weightMod=0.0}, damageResult 0.0, didCrit false, savedAttack AttackBase{attackName='Play Nice'}, savedPower 0, savedAccuracy -1, overridePPMax null, overrideAttackCategory null, overrideType null, isZ false, isMax false, originalMove null], Wide Guard[pp 10, ppLevel 0, movePower 0, moveAccuracy 0, cantMiss false, disabled false, savedAttack null, savedPower 0, savedAccuracy 0, overridePPMax null, overrideAttackCategory null, overrideType null, isZ false, isMax false, originalMove null]], ability=com.pixelmonmod.pixelmon.api.pokemon.ability.abilities.Healer@8099ecb5, reminderMoves=[]} status com.pixelmonmod.pixelmon.battles.status.Drowsy@4ceea848]
 Kirlia[escape attempts 0 damage taken this turn 0 priority 0.0 can attack true will try flee false switching false evolve false moveset Moveset{pokemon=Pokemon{Kirlia}, attacks=[Double Team[pp 15, ppLevel 0, movePower 0, moveAccuracy 0, cantMiss false, disabled false, savedAttack null, savedPower 0, savedAccuracy 0, overridePPMax null, overrideAttackCategory null, overrideType null, isZ false, isMax false, originalMove null], Hypnosis[pp 19, ppLevel 0, movePower 0, moveAccuracy 60, cantMiss false, disabled false, moveResult MoveResults{damage=0, fullDamage=0, accuracy=0, priority=0.0, result=missed, weightMod=0.0}, damageResult -1.0, didCrit false, savedAttack AttackBase{attackName='Hypnosis'}, savedPower 0, savedAccuracy 60, overridePPMax null, overrideAttackCategory null, overrideType null, isZ false, isMax false, originalMove null], Teleport[pp 20, ppLevel 0, movePower 0, moveAccuracy 0, cantMiss false, disabled false, savedAttack null, savedPower 0, savedAccuracy 0, overridePPMax null, overrideAttackCategory null, overrideType null, isZ false, isMax false, originalMove null], Psybeam[pp 20, ppLevel 0, movePower 65, moveAccuracy 0, cantMiss false, disabled false, savedAttack null, savedPower 0, savedAccuracy 0, overridePPMax null, overrideAttackCategory null, overrideType null, isZ false, isMax false, originalMove null]], ability=com.pixelmonmod.pixelmon.api.pokemon.ability.abilities.Trace@4d50125, reminderMoves=[]} status ]

Kirlia selected attack Psybeam targeting Alomomola and opted to mega evolve
Dev executed command /battlelog
Dev executed command /endbattle


As you may notice in upcoming 9.1.4, battle logging now records a much wider scope of information, and it initially appeared to be much more useful for debugging battles. However, the appearance alone was not enough, and we needed some real waiting glitches to put it through its paces.

Server Testing

It was at this point that we realized that our small team alone was not capable of covering enough battle conditions to determine if the new battle log was sufficient. Therefore, we contacted several of the largest and most competitive servers in the community with a copy of the new battle log system and a sidemod that posted all battle logs to a Discord WebHook. Thanks to these servers, our team gained the insight of a cumulative number of battles reaching roughly 100,000 per day, which allowed us to gain a much more in-depth view of battling.

Initially, there were hundreds, if not thousands, of battle logs coming in from all the servers every day, which was overwhelming and certainly a difficult place to start from. However, in all of this, it was clear that removing the "rule of three" (crashes) had made a clear difference. It allowed us to find issues that had previously never been reported, seen, or heard of.

Image

We then worked diligently to fix as many of the errors as possible, using mixins that were then fed back into the mod itself, to reduce the initial error logging spam and ultimately, to find more niche errors. We also managed to catch a couple of verified waiting glitches and fix them, confirming that the new logging was providing a much better level of detail. At the time of writing, we are at a stage where we only get a couple of battle logs per day from these servers, averaging one battle crash or log per 5,500 battles. We have also seen a notable downtick in waiting bug complaints from these targeted playerbases. The numbers in the messages below represent the crash count, battle ID, and then total battle count. Note, the battle ID won't always equal the battle count as the report is only sent after the battle ends. So, if a battle were to begin after the battle that crashes, then the ID would not equal the total.

Image

The success of the battle logging system was a positive first step and got us excited to plan for our next strategies to continue addressing battle errors and 'waiting glitches'.



Future GoalsWith all of this in mind, we can confidently say that these will not be the last steps we take towards improving the battling experience in Pixelmon. We are committed to continued development on new systems, ensuring that our battling experience grows increasingly more stable. We hope that one day, we may be able to say that it is perfect.

Unit Testing

The next step that we will take towards achieving this goal is to introduce standardized testing on the battle engine, known as _unit testing_ in the software industry. This will involve breaking down the battle engine into individual components, such as a specific move. By testing it thoroughly through a wide variety of scenarios, we would be able to ensure that a move always results in its desired outcome. The benefit of this is that when we add, modify, or update the battle engine in the future, it is less likely to break due to a simple mistake made by one of us, as there will be tests checking battle scenarios and alerting us if anything doesn’t give the correct outcome. For example, let’s take the most basic example of a Magikarp using Splash. This is perhaps the simplest unit test that can be written as this move should never do any damage or change any stats except its move PP, and should just display a message in the battle log.

While this might sound like a very simple task to begin with, it will come with its complexities. For example, checking that Poison Fangcorrectly applies poison at the correct probability is a more complicated scenario. We cannot simply check that the target was badly poisoned every time the move is used. We need to run a series of tests to ensure it averages out to a 50% chance. In addition, we also need to check how much damage was dealt by the user to the target, which will vary based on typings, EVs, IVs, and form.

Unfortunately, the complications don’t stop there. Currently, the API for battling in Pixelmon isn’t designed for this level of control from an outside source. As most sidemod developers can attest, and as is perhaps evident from the lack of battling side mods, the API around battling is currently unfriendly and admittedly obscure. This is something we would need to improve going forward, so that we may be able to effectively and efficiently write battle tests, as otherwise, we will end up with unit tests that are repeating hundreds of lines of battle logic.

Moreover, we will need to figure out a way to integrate these tests into our developmental pipeline effectively, so that effectuating these tests isn't tedious and doesn't slow us down when producing updates. Hopefully, this may become easier with a future version update as Forge has introduced an unit testing framework from 1.18 onwards. However, Forge has commented that it’s too large of a change to backport. Therefore, we will need to come up with our own testing framework that can then be refitted to work with, and wrap around, the Forge Game Testing system whenever we move forward to the next Minecraft version.

Battle State

The ultimate goal for us as a team, and what we believe to be the best strategy for solving this problem, is to create a "transactional" battle engine. This means that when something changes, it will be recorded in a log that will allow us to revert the change in the future. This makes the most sense for testing battles, as it would allow us to perfectly replicate a battle state and then infinitely repeat the given situation until we've figured out exactly what's going wrong and how to solve it. This would give us 100% confidence in every fix and also allow for easy fixing mid-battle.

We have taken our first steps towards this system with the current battle log, which attempts to take copies of a vast amount of the battle engine whenever a log is created. Currently, this is just to ensure that the data being logged is accurate and not polluted by future battle actions. However, we hope that this will act as a springboard into extensible and replicable battle states.



ConclusionAll in all, why would this be useful for the average user? Well, with a transactional battle engine comes the ability to replay battles, which we believe would be a very entertaining feature and a step forward for the average player. Additionally, it could extend under-used features such as the Camera item so that it can record a battle to Film, allowing the user to playback the battle. While this may seem inconsequential, it could extend educational opportunities in competitive battling, and even provide opportunities to create content within Pixelmon.

While this is not the last time we may end up discussing battle logging, battle bugs and their respective challenges, we hope that our efforts will address current concerns with the 'waiting glitch', as well as any other future battle issues we will surely find along the way.
Attachments
6c9bd20f4265f231b016f02aab7e9b21.jpg
878061e1f0ec7cb762f341b95180906d.png
60a3d57f2142c25462d0ce73e0980dec.png

JOIN THE TEAM