• Do not use Discord to host any images you post, these links expire quickly! You can learn how to add images to your posts here.
  • The Eevee Expo Game Jam has concluded! 🎉 Head on over to the game jam forum to play through the games.
    Don't forget to come back September 21st to vote for your favorites!
  • Reminder: AI-generated content is not allowed on the forums per the Rules and Regulations. Please contact us if you have any questions!
Resource icon

Resource [v16/v17/v18/v19/v20/v21] Cable Club 3.7

Hello, how are you trainers? Vendily, a report, on the python server POKEMON_MAX_NAME_SIZE is set to 10. But there are already Pokemon with names bigger than that.
Corviknight, Slither Wing, Roaring Moon are examples of this.
Hope this helps!
 
Hello, in version 21 the level adjustment is not working as it should, because the
LevelAdjustment class received a change in the variables
BothTeams is now BOTH_TEAMS and you would need to add the level recalculator again in self.do_battle.

We are getting better and better!
 
Could you please provide the code to enter in the self.do_battle function to recalculate the level? I am experiencing the same issue. Thank you!


Hello, in version 21 the level adjustment is not working as it should, because the
LevelAdjustment class received a change in the variables
BothTeams is now BOTH_TEAMS and you would need to add the level recalculator again in self.do_battle.

We are getting better and better!
 
Could you please provide the code to enter in the self.do_battle function to recalculate the level? I am experiencing the same issue. Thank you!
Hello! I made a quick adjustment as follows:
First, I saved the levels in oldlevels and then applied the level in:
battle_rules.adjustLevels(player_party, partner_party), then returned the levels in: battle_rules.unadjustLevels(player_party,partner_party,oldlevels)

But I'm still facing some problems, like the logic of returning Pokémon items for battles where the player, when choosing his team, selects the first, second, third out of the sequence of $player.party. Soon I should post here the way I solved this problem.

Ruby:
Expand Collapse Copy
def self.do_battle(connection, client_id, seed, battle_rules, player_party, partner, partner_party)
    $player.heal_party # Avoids having to transmit damaged state.
    partner_party.each{|pkmn| pkmn.heal} # back to back battles desync without it.
    oldlevels = battle_rules.adjustLevels(player_party, partner_party)
    olditems  = player_party.transform { |p| p.item_id }
    olditems2 = partner_party.transform { |p| p.item_id }
    if !DISABLE_SKETCH_ONLINE
      oldmoves  = player_party.transform { |p| p.moves.dup }
      oldmoves2 = partner_party.transform { |p| p.moves.dup }
    end
    if PluginManager.installed?("Terastal Phenomenon") ||
       PluginManager.installed?("[DBK] Terastallization")
      old_tera = $player.tera_charged?
      $player.tera_charged = true
    end
    scene = BattleCreationHelperMethods.create_battle_scene
    battle = Battle_CableClub.new(connection, client_id, scene, player_party, partner_party, partner, seed)
    battle.items = []
    battle.internalBattle = false
    battle_rules.applyBattleRules(battle)
    battle_rules.adjustLevels(player_party, partner_party)
    trainerbgm = pbGetTrainerBattleBGM(partner)
    EventHandlers.trigger(:on_start_battle)
    # XXX: Configuring Online Battle Rules
    setBattleRule("environment", :None)
    setBattleRule("weather", :None)
    setBattleRule("terrain", :None)
    setBattleRule("backdrop", "indoor1")
    BattleCreationHelperMethods.prepare_battle(battle)
    $game_temp.clear_battle_rules
    battle.time = 0
    exc = nil
    outcome = 0
    pbBattleAnimation(trainerbgm, (battle.singleBattle?) ? 1 : 3, [partner]) {
      pbSceneStandby {
        begin
          outcome = battle.pbStartBattle
        rescue Connection::Disconnected
          scene.pbEndBattle(0)
          exc = $!
        ensure
          battle_rules.unadjustLevels(player_party,partner_party,oldlevels)
          if PluginManager.installed?("Terastal Phenomenon") ||
             PluginManager.installed?("[DBK] Terastallization")
            $player.tera_charged = old_tera
          end
          player_party.each_with_index do |pkmn, i|
            pkmn.heal
            pkmn.makeUnmega
            pkmn.makeUnprimal
            pkmn.item = olditems[i]
            if !DISABLE_SKETCH_ONLINE
              pkmn.moves.clear
              oldmoves[i].each_with_index {|move,i| pkmn.moves[i]=move}
            end
            pkmn.unmax if PluginManager.installed?("ZUD Mechanics")
            if PluginManager.installed?("[DBK] Dynamax")
               if pkmn.dynamax?
                 pkmn.makeUndynamaxForm
                 pkmn.makeUndynamax
                 pkmn.calc_stats
               end
            end
            if PluginManager.installed?("Terastal Phenomenon") ||
               PluginManager.installed?("[DBK] Terastallization")
              pkmn.terastallized = false if pkmn&.tera?
            end
          end
          partner_party.each_with_index do |pkmn, i|
            pkmn.heal
            pkmn.makeUnmega
            pkmn.makeUnprimal
            pkmn.item = olditems2[i]
            if !DISABLE_SKETCH_ONLINE
              pkmn.moves.clear
              oldmoves2[i].each_with_index {|move,i| pkmn.moves[i]=move}
            end
            pkmn.unmax if PluginManager.installed?("ZUD Mechanics")
            if PluginManager.installed?("[DBK] Dynamax")
               if pkmn.dynamax?
                 pkmn.makeUndynamaxForm
                 pkmn.makeUndynamax
                 pkmn.calc_stats
               end
            end
            if PluginManager.installed?("Terastal Phenomenon") ||
               PluginManager.installed?("[DBK] Terastallization")
              pkmn.terastallized = false if pkmn&.tera?
            end
          end
        end
      }
    }
    raise exc if exc
    case outcome
    when 1
      $stats.online_battles_wins+=1
    when 2
      $stats.online_battles_lost+=1
    end
  end
 
Last edited:
The code you provided works perfectly for me. Thank you so much for your help!




Hello! I made a quick adjustment as follows:
First, I saved the levels in oldlevels and then applied the level in:
battle_rules.adjustLevels(player_party, partner_party), then returned the levels in: battle_rules.unadjustLevels(player_party,partner_party,oldlevels)

But I'm still facing some problems, like the logic of returning Pokémon items for battles where the player, when choosing his team, selects the first, second, third out of the sequence of $player.party. Soon I should post here the way I solved this problem.

Ruby:
Expand Collapse Copy
def self.do_battle(connection, client_id, seed, battle_rules, player_party, partner, partner_party)
    $player.heal_party # Avoids having to transmit damaged state.
    partner_party.each{|pkmn| pkmn.heal} # back to back battles desync without it.
    oldlevels = battle_rules.adjustLevels(player_party, partner_party)
    olditems  = player_party.transform { |p| p.item_id }
    olditems2 = partner_party.transform { |p| p.item_id }
    if !DISABLE_SKETCH_ONLINE
      oldmoves  = player_party.transform { |p| p.moves.dup }
      oldmoves2 = partner_party.transform { |p| p.moves.dup }
    end
    if PluginManager.installed?("Terastal Phenomenon") ||
       PluginManager.installed?("[DBK] Terastallization")
      old_tera = $player.tera_charged?
      $player.tera_charged = true
    end
    scene = BattleCreationHelperMethods.create_battle_scene
    battle = Battle_CableClub.new(connection, client_id, scene, player_party, partner_party, partner, seed)
    battle.items = []
    battle.internalBattle = false
    battle_rules.applyBattleRules(battle)
    battle_rules.adjustLevels(player_party, partner_party)
    trainerbgm = pbGetTrainerBattleBGM(partner)
    EventHandlers.trigger(:on_start_battle)
    # XXX: Configuring Online Battle Rules
    setBattleRule("environment", :None)
    setBattleRule("weather", :None)
    setBattleRule("terrain", :None)
    setBattleRule("backdrop", "indoor1")
    BattleCreationHelperMethods.prepare_battle(battle)
    $game_temp.clear_battle_rules
    battle.time = 0
    exc = nil
    outcome = 0
    pbBattleAnimation(trainerbgm, (battle.singleBattle?) ? 1 : 3, [partner]) {
      pbSceneStandby {
        begin
          outcome = battle.pbStartBattle
        rescue Connection::Disconnected
          scene.pbEndBattle(0)
          exc = $!
        ensure
          battle_rules.unadjustLevels(player_party, partner_party, oldlevels)
          if PluginManager.installed?("Terastal Phenomenon") ||
             PluginManager.installed?("[DBK] Terastallization")
            $player.tera_charged = old_tera
          end
          player_party.each_with_index do |pkmn, i|
            pkmn.heal
            pkmn.makeUnmega
            pkmn.makeUnprimal
            pkmn.item = olditems[i]
            if !DISABLE_SKETCH_ONLINE
              pkmn.moves.clear
              oldmoves[i].each_with_index {|move,i| pkmn.moves[i]=move}
            end
            pkmn.unmax if PluginManager.installed?("ZUD Mechanics")
            if PluginManager.installed?("[DBK] Dynamax")
               if pkmn.dynamax?
                 pkmn.makeUndynamaxForm
                 pkmn.makeUndynamax
                 pkmn.calc_stats
               end
            end
            if PluginManager.installed?("Terastal Phenomenon") ||
               PluginManager.installed?("[DBK] Terastallization")
              pkmn.terastallized = false if pkmn&.tera?
            end
          end
          partner_party.each_with_index do |pkmn, i|
            pkmn.heal
            pkmn.makeUnmega
            pkmn.makeUnprimal
            pkmn.item = olditems2[i]
            if !DISABLE_SKETCH_ONLINE
              pkmn.moves.clear
              oldmoves2[i].each_with_index {|move,i| pkmn.moves[i]=move}
            end
            pkmn.unmax if PluginManager.installed?("ZUD Mechanics")
            if PluginManager.installed?("[DBK] Dynamax")
               if pkmn.dynamax?
                 pkmn.makeUndynamaxForm
                 pkmn.makeUndynamax
                 pkmn.calc_stats
               end
            end
            if PluginManager.installed?("Terastal Phenomenon") ||
               PluginManager.installed?("[DBK] Terastallization")
              pkmn.terastallized = false if pkmn&.tera?
            end
          end
        end
      }
    }
    raise exc if exc
    case outcome
    when 1
      $stats.online_battles_wins+=1
    when 2
      $stats.online_battles_lost+=1
    end
  end
 
MOVE COMPATIBILITIES
Hello trainers, today I'm sharing something that might be useful to you.

I know that the tutorial contains the necessary information for this, but when creating server_pokemon.txt, you should opt for shared mode if you want to have high compatibility with this resource.

When using propagate mode, it ends up not recognizing moves from an evolutionary line that is defined later in the standard PBS. Clefairy and Clefable were not able to allow the moves of Cleffa. (Wish specifically in Gen 8). However, shared mode can even assign compatibilities from baby species that come later in the PBS.

I hope I've helped someone, see you soon!
 
Last edited:

ESSENTIALS V21.1 UPDATES WITH UNOFFICIAL EBDX.​

  • This is the version I managed to develop after a while for the unofficial EBDX in version 21.1, which I made over time in a single post. Let's look at the first changes:
  • The first is the correction of the order of the targets for different situations. In EBDX the order of the @battlers is initialized differently, different from the standard Essentials, so the position of the codes must confuse the Cable Club - EBDX interactions, but this was easily solved without having to mess with the EBDX code which is much larger.
Ruby:
Expand Collapse Copy
module CableClub
  # EDBX COMPAT
  def self.pokemon_order(client_id)
    case client_id
    when 0..1; [0, 1, 2, 3, 4, 5]
    else; raise "Unknown client_id: #{client_id}"
    end
  end

  def self.pokemon_order_priority(client_id)
    case client_id
    when 0; [0, 1, 2, 3, 4, 5]
    when 1; [1, 0, 3, 2, 4, 5]
    else; raise "Unknown client_id: #{client_id}"
    end
  end

  # Original target order
  def self.pokemon_target_order(client_id)
    case client_id
    when 0..1; [1, 0, 3, 2, 5, 4]
    else; raise "Unknown client_id: #{client_id}"
    end
  end
end

class Battle_CableClub < Battle
  def pbRandom(x)
    ret = nil
    pbWithSynchronizedOrder do
      ret = @battleRNG.rand(x)
    end
    PBDebug.log_message("Chamada de pbRandom class Battle_CableClub = #{ret}.")
    return ret
  end
  def pbWithSynchronizedOrder
    battlers = @battlers.dup
    order = CableClub::pokemon_order_priority(@client_id)
    order.each_with_index { |o, i| @battlers[i] = battlers[o] }
    yield
  ensure
    original = CableClub::pokemon_order(@client_id)
    original.each_with_index { |o, i| @battlers[i] = battlers[o] }
  end

  def pbCalculatePriority(*args)
    battlers = @battlers.dup
    begin
      order = CableClub::pokemon_order_priority(@client_id)
      order.each_with_index do |o,i|
        @battlers[i] = battlers[o]
      end
      return super(*args)
    ensure
      order = CableClub::pokemon_order(@client_id)
      order.each_with_index do |o,i|
        @battlers[i] = battlers[o]
      end
      @battlers = battlers
    end
  end
 
  def pbEORSwitch(favorDraws=false)
    return if @decision>0 && !favorDraws
    return if @decision==5 && favorDraws
    pbJudge
    return if @decision>0
    # Check through each fainted battler to see if that spot can be filled.
    switched = []
    loop do
      switched.clear
      # check in same order
      battlers = []
      order = CableClub::pokemon_order(@client_id)
      order.each_with_index do |o,i|
        battlers[i] = @battlers[o]
      end
      battlers.each do |b|
        next if !b || !b.fainted?
        idxBattler = b.index
        next if !pbCanChooseNonActive?(idxBattler)
        if !pbOwnedByPlayer?(idxBattler)   # Opponent/ally is switching in
          idxPartyNew = pbSwitchInBetween(idxBattler)
          opponent = pbGetOwnerFromBattlerIndex(idxBattler)
          pbRecallAndReplace(idxBattler,idxPartyNew)
          switched.push(idxBattler)
        else
          idxPlayerPartyNew = pbGetReplacementPokemonIndex(idxBattler)   # Owner chooses
          pbRecallAndReplace(idxBattler,idxPlayerPartyNew)
          switched.push(idxBattler)
        end
      end
      break if switched.length==0
      pbOnBattlerEnteringBattle(switched)
    end
  end
end

class Battle
  class AI_CableClub < AI
    def pbDefaultChooseEnemyCommand(index)
      # Hurray for default methods. have to reverse it to show the expected order.
      our_indices = @battle.pbGetOpposingIndicesInOrder(1).reverse
      their_indices = @battle.pbGetOpposingIndicesInOrder(0).reverse
      # Sends our choices after they have all been locked in.
      if index == their_indices.last
        # TODO: patch this up to be index agnostic.
        # Would work fine if restricted to single/double battles
        target_order = CableClub::pokemon_target_order(@battle.client_id)
        @battle.connection.send do |writer|
          writer.sym(:battle_data)
          # Send Seed
          cur_seed=@battle.battleRNG.srand
          @battle.battleRNG.srand(cur_seed)
          writer.sym(:seed)
          writer.int(cur_seed)
          # Send Extra Battle Mechanics
          writer.sym(:mechanic)
          # Mega Evolution
          mega=@battle.megaEvolution[0][0]
          mega^=1 if mega>=0
          writer.int(mega)
          # ZUD / [DBK] Z-Power
          if PluginManager.installed?("ZUD Mechanics") ||
             PluginManager.installed?("[DBK] Z-Power")
            zmove = @battle.zMove[0][0]
            zmove^=1 if zmove>=0
            writer.int(zmove)
            ultra = @battle.ultraBurst[0][0]
            ultra^=1 if ultra>=0
            writer.int(ultra)
          end
          if PluginManager.installed?("ZUD Mechanics") ||
             PluginManager.installed?("[DBK] Dynamax")
            dmax = @battle.dynamax[0][0]
            dmax^=1 if dmax>=0
            writer.int(dmax)
          end
          # PLA Styles
          if PluginManager.installed?("PLA Battle Styles")
            style = @battle.battleStyle[0][0]
            style_trigger = 0
            if style>=0
              style_trigger = @battle.battlers[style].style_trigger
              style^=1
            end
            writer.int(style)
            writer.int(style_trigger)
          end
          if PluginManager.installed?("Terastal Phenomenon") ||
             PluginManager.installed?("[DBK] Terastallization")
            tera = @battle.terastallize[0][0]
            tera^=1 if tera>=0
            writer.int(tera)
          end
          # Focus
          if PluginManager.installed?("Focus Meter System")
            focus = @battle.focusMeter[0][0]
            focus^=1 if focus>=0
            writer.int(focus)
          end
          # Send Choices for Player's Mons
          for our_index in our_indices
            pkmn = @battle.battlers[our_index]
            writer.sym(:choice)
            # choice picked was changed to be a symbol now.
            writer.sym(@battle.choices[our_index][0])
            writer.int(@battle.choices[our_index][1])
            move = !!@battle.choices[our_index][2]
            writer.nil_or(:bool, move)
            # Target from their POV.
            our_target = @battle.choices[our_index][3]
            their_target = target_order[our_target] rescue our_target
            writer.int(their_target)
          end
        end
        frame = 0
        @battle.scene.pbShowWindow(Battle::Scene::MESSAGE_BOX)
        cw = @battle.scene.sprites["messageWindow"]
        cw.letterbyletter = false
        begin
          loop do
            frame += 1
            cw.text = _INTL("Waiting" + "." * (1 + ((frame / 8) % 3)))
            @battle.scene.pbFrameUpdate(cw)
            Graphics.update
            Input.update
            raise Connection::Disconnected.new("disconnected") if Input.trigger?(Input::BACK) && pbConfirmMessageSerious("Você gostaria de se desconectar?")
            @battle.connection.update do |record|
              case (type = record.sym)
              when :forfeit
                pbSEPlay("Battle flee")
                @battle.pbDisplay(_INTL("{1} perdeu a partida!", @battle.opponent[0].full_name))
                @battle.decision = 1
                @battle.pbAbort
              when :battle_data
                loop do
                  case (t = record.sym)
                  when :seed
                    seed=record.int()
                    @battle.battleRNG.srand(seed)
                  when :mechanic
                    @battle.megaEvolution[1][0] = record.int
                    if PluginManager.installed?("ZUD Mechanics") ||
                       PluginManager.installed?("[DBK] Z-Power")
                      @battle.zMove[1][0] = record.int
                      @battle.ultraBurst[1][0] = record.int
                    end
                    if PluginManager.installed?("ZUD Mechanics") ||
                       PluginManager.installed?("[DBK] Dynamax")
                      @battle.dynamax[1][0] = record.int
                    end
                    if PluginManager.installed?("PLA Battle Styles")
                      focus_index = record.int
                      @battle.battleStyle[1][0] = focus_index
                      style_trigger = record.int
                      @battle.battlers[focus_index].style_trigger = style_trigger if focus_index >= 0
                    end
                    if PluginManager.installed?("Terastal Phenomenon") ||
                       PluginManager.installed?("[DBK] Terastallization")
                      @battle.terastallize[1][0] = record.int
                    end
                    if PluginManager.installed?("Focus Meter System")
                      @battle.focusMeter[1][0] = record.int
                    end
                  when :choice
                    their_index = their_indices.shift
                    partner_pkmn = @battle.battlers[their_index]
                    @battle.choices[their_index][0] = record.sym
                    @battle.choices[their_index][1] = record.int
                    if PluginManager.installed?("[DBK] Z-Power")
                      partner_pkmn.display_zmoves if @battle.zMove[1][0] == their_index
                    end
                    if PluginManager.installed?("[DBK] Dynamax")
                      partner_pkmn.display_dynamax_moves if @battle.dynamax[1][0] == their_index
                    end
                    if PluginManager.installed?("ZUD Mechanics")
                      if @battle.zMove[1][0] == their_index
                        partner_pkmn.display_power_moves(1)
                        partner_pkmn.power_trigger = true
                      elsif @battle.ultraBurst[1][0] == their_index
                        partner_pkmn.power_trigger = true
                      elsif @battle.dynamax[1][0] == their_index
                        partner_pkmn.display_power_moves(2)
                        partner_pkmn.power_trigger = true
                      end
                    end
                    if PluginManager.installed?("PLA Battle Styles")
                      if @battle.battleStyle[1][0] == their_index
                        partner_pkmn.toggle_style_moves(partner_pkmn.style_trigger)
                      end
                    end
                    move = record.nil_or(:bool)
                    if move
                      move = (@battle.choices[their_index][1]<0) ? @battle.struggle : partner_pkmn.moves[@battle.choices[their_index][1]]
                    end
                    @battle.choices[their_index][2] = move
                    @battle.choices[their_index][3] = record.int
                    break if their_indices.empty?
                  else
                    raise "Unknown message: #{t}"
                  end
                end
                return
              else
                raise "Unknown message: #{type}"
              end
            end
          end
        ensure
          cw.letterbyletter = true
        end
      end
    end
end
That's not all, for some reason EBDX v21 messages are not being received decoded in UTF8, so you need to force this decoding. Soon I'll post how to do this in each part of the code.

Authenticator improvements:
  • Now the next step is a standard fix for the authenticator.
POKEMON_MAX_NAME_SIZE inside cable_club.py is set to 10, but several Pokémon like Corviknight, Slither Wing, Roaring Moon have more than 10, so if you don't want the game to crash in these cases just increase it to a higher value.
  • To generate server_pokemon.txt use the shared method that has the highest compatibility rate, the others give different problems and I explain this better in this topic if you want details: https://eeveeexpo.com/threads/3737/post-82037
  • There is important information here for specific situations. There are Pokémon like Rotom that learn moves when changing form, moves that are not defined in PBS so if you don't want it to get stuck during authentication, add the other moves it can learn to TutorMoves. This applies to others that have a similar function.
Level adjustment rule compatibility
  • In v21 some variables change but it is easy to adjust.
Ruby:
Expand Collapse Copy
module CableClub
  def self.do_battle(connection, client_id, seed, battle_rules, player_party, partner, partner_party)
    $player.heal_party # Avoids having to transmit damaged state.
    partner_party.each{|pkmn| pkmn.heal} # back to back battles desync without it.
    oldlevels = battle_rules.adjustLevels(player_party, partner_party)
    olditems  = player_party.transform { |p| p.item_id }
    olditems2 = partner_party.transform { |p| p.item_id }
    if !DISABLE_SKETCH_ONLINE
      oldmoves  = player_party.transform { |p| p.moves.dup }
      oldmoves2 = partner_party.transform { |p| p.moves.dup }
    end
    if PluginManager.installed?("Terastal Phenomenon") ||
       PluginManager.installed?("[DBK] Terastallization")
      old_tera = $player.tera_charged?
      $player.tera_charged = true
    end
    scene = BattleCreationHelperMethods.create_battle_scene
    battle = Battle_CableClub.new(connection, client_id, scene, player_party, partner_party, partner, seed)
    battle.items = []
    battle.internalBattle = false
    battle_rules.applyBattleRules(battle)
    battle_rules.adjustLevels(player_party, partner_party)
    trainerbgm = pbGetTrainerBattleBGM(partner)
    EventHandlers.trigger(:on_start_battle)
    # XXX: Configuring Online Battle Rules
    setBattleRule("environment", :None)
    setBattleRule("weather", :None)
    setBattleRule("terrain", :None)
    setBattleRule("backdrop", "indoor1")
    BattleCreationHelperMethods.prepare_battle(battle)
    $game_temp.clear_battle_rules
    battle.time = 0
    exc = nil
    outcome = 0
    pbBattleAnimation(trainerbgm, (battle.singleBattle?) ? 1 : 3, [partner]) {
      pbSceneStandby {
        begin
          outcome = battle.pbStartBattle
        rescue Connection::Disconnected
          scene.pbEndBattle(0)
          exc = $!
        ensure
          battle_rules.unadjustLevels(player_party,partner_party,oldlevels)
          if PluginManager.installed?("Terastal Phenomenon") ||
             PluginManager.installed?("[DBK] Terastallization")
            $player.tera_charged = old_tera
          end
          player_party.each_with_index do |pkmn, i|
            pkmn.heal
            pkmn.makeUnmega
            pkmn.makeUnprimal
            pkmn.item = olditems[i]
            if !DISABLE_SKETCH_ONLINE
              pkmn.moves.clear
              oldmoves[i].each_with_index {|move,i| pkmn.moves[i]=move}
            end
            pkmn.unmax if PluginManager.installed?("ZUD Mechanics")
            if PluginManager.installed?("[DBK] Dynamax")
               if pkmn.dynamax?
                 pkmn.makeUndynamaxForm
                 pkmn.makeUndynamax
                 pkmn.calc_stats
               end
            end
            if PluginManager.installed?("Terastal Phenomenon") ||
               PluginManager.installed?("[DBK] Terastallization")
              pkmn.terastallized = false if pkmn&.tera?
            end
          end
          partner_party.each_with_index do |pkmn, i|
            pkmn.heal
            pkmn.makeUnmega
            pkmn.makeUnprimal
            pkmn.item = olditems2[i]
            if !DISABLE_SKETCH_ONLINE
              pkmn.moves.clear
              oldmoves2[i].each_with_index {|move,i| pkmn.moves[i]=move}
            end
            pkmn.unmax if PluginManager.installed?("ZUD Mechanics")
            if PluginManager.installed?("[DBK] Dynamax")
               if pkmn.dynamax?
                 pkmn.makeUndynamaxForm
                 pkmn.makeUndynamax
                 pkmn.calc_stats
               end
            end
            if PluginManager.installed?("Terastal Phenomenon") ||
               PluginManager.installed?("[DBK] Terastallization")
              pkmn.terastallized = false if pkmn&.tera?
            end
          end
        end
      }
    }
    raise exc if exc
    case outcome
    when 1
      $stats.online_battles_wins+=1
    when 2
      $stats.online_battles_lost+=1
    end
  end
end

Ruby:
Expand Collapse Copy
class PokemonOnlineRules
  def adjustLevels(party1,party2)
    if @levelAdjustment && @levelAdjustment.type==LevelAdjustment::BOTH_TEAMS
      return @levelAdjustment.adjustLevels(party1,party2)
    else
      return nil
    end
  end

  def unadjustLevels(party1,party2,adjusts)
    if @levelAdjustment && adjusts && @levelAdjustment.type==LevelAdjustment::BOTH_TEAMS
      @levelAdjustment.unadjustLevels(party1,party2,adjusts)
    end
  end
end
  • I did this a while ago so if there is any missing information let me know.

SYNCHRONIZATION SUPPORT
  • In def pbDefaultChooseEnemyCommand(index) I removed the following lines after fixing all the synchronization issues, including these lines that desynchronize the battle.
Code:
Expand Collapse Copy
# -1 invokes the RNG, out of order (somehow?!) which causes desync
# But this is a single battle, so the only possible choice is the foe.
if @battle.singleBattle? && @battle.choices[our_index][3] == -1
  @battle.choices[our_index][3] = their_indices[0]
end

PREVENTING ERRORS
  • If you happen to create a new attribute for the Pokemon class, you should register these attributes in def self.write_pkmn(writer, pkmn) and def self.parse_pkmn(record), in addition to adding the reading in cable_club.py itself.
    For example, something as simple as Independent Hidden Power from DemICE https://eeveeexpo.com/threads/6300/ creates an attribute called hptype that if not read the system data will conflict and a crash will occur.

UPDATES
1.0
Posted with this support.
1.0.1 Updated 07/14/2025 with support for full synchronization for duo battles.
1.0.2 Updated 08/08/2025 with support for Revival Blessing and a synchronization fix for double battle targets.
Codes for version v21.1 Essentials with unofficial EBDX.
 
Last edited:
DEALING WITH UNEXPECTED DISCONNECTION
  • If you are battling normally and one of the two players disconnects, the battle will not end automatically and when you try to progress you will enter an infinite loop waiting for the other player to act. This happens because, if you lose connection even for 1 second, the server is not able to reconnect and return to where it left off, @socket is undone. So, reporting the disconnection is the simplest way in this case, until a reconnection system is made.
  • There is a very rare situation where the other player disconnects and this information is triggered while you are sending. So there is this second exception handling in rescue Errno::EPIPE
On Joiplay people can play on 4G, 5G and a simple drop of one second is enough to crash the battle, so this is the best way for now, unlike the PC where you have a direct connection cable.

Ruby:
Expand Collapse Copy
class Connection
  def send
    # XXX: Non-blocking send.
    # but note we don't update often so we need some sort of drained?
    # for the send buffer so that we can delay starting the battle.
    writer = RecordWriter.new
    yield writer
    begin
      @socket.write_nonblock(writer.line!)
    rescue Errno::ECONNABORTED, Errno::ECONNRESET
      raise Connection::Disconnected.new("internet disconnection")
   rescue Errno::EPIPE
      raise Connection::Disconnected.new("peer disconnected")
    end
  end
end

class CableClubScreen
  def pbAttemptConnection
    if $player.party_count == 0
      pbDisplay(_INTL("I'm sorry, you must have a Pokémon to enter the Cable Club."))
      return false
    end
    begin
      msg = _ISPRINTF("What's the ID of the trainer you're searching for? (Your ID: {1:05d})",$player.public_ID($player.id))
      partner_id = ""
      loop do
        partner_id = pbEnterText(msg, partner_id, false, 5)
        return false if partner_id.empty?
        break if partner_id =~ /^[0-9]{5}$/
      end
      pbConnectServer(partner_id)
      raise Connection::Disconnected.new("disconnected")
    rescue Connection::Disconnected => e
      case e.message
      when "disconnected"
        pbDisplay(_I
[/SPOILER]
NTL("Thank you for using the Cable Club. We hope to see you again soon."))
        return true
      when "invalid party"
        pbDisplay(_INTL("I'm sorry, your party contains Pokémon not allowed in the Cable Club."))
        return false
      when "peer disconnected"
        pbDisplay(_INTL("I'm sorry, the other trainer has disconnected."))
        return true
      when "invalid version"
        pbDisplay(_INTL("I'm sorry, your game version is out of date compared to the Cable Club."))
        return false
      when "internet disconnection"
        pbDisplay(_INTL("I'm sorry, there was an unexpected disconnection by your client."))
        return true
      else
        pbDisplay(_INTL("I'm sorry, the Cable Club server has malfunctioned!"))
        return false
      end
    rescue Errno::ECONNABORTED
      pbDisplay(_INTL("I'm sorry, the other trainer has disconnected."))
      return true
    rescue Errno::ECONNREFUSED
      pbDisplay(_INTL("I'm sorry, the Cable Club server is down at the moment."))
      return false
    rescue
      pbPrintException($!)
      pbDisplay(_INTL("I'm sorry, the Cable Club has malfunctioned!"))
      return false
    ensure
      pbHideMessageBox
    end
  end
end
 
Last edited:
JOIPLAY COMPATIBILITY
I have separated some steps to make it compatible with Joiplay, let's go!
First step:
  • Last year we got compatibility for Socket and necessary functions, they met us and this allowed Cable Club to be compatible, so: Somewhere in your code, you will need to check the compatible RPGM Plugin version, and I have already separated the ideal version.
Ruby:
Expand Collapse Copy
if $joiplay
  if (MKXP.plugin_version()).to_i < 12053
    # This is the ideal check, your player version has to be greater than 12053
  end
end
Second step:
  • For some reason require causes Joiplay to crash, all requires are loaded otherwise so you should just handle this in: 001_Connection_Communication.rb with:
Ruby:
Expand Collapse Copy
begin
  require 'socket'
  require 'io/wait'
rescue => e
  # Joiplay
end
Third step:
  • The last step is to search for a player! The Input module is loaded differently in Joiplay, each non-standard Pokémon Essentials method is gradually inserted into the Plugin and one that is not yet fully compatible with all devices is Input.text_input of pbEnterText when searching for an ID, so I prepared an alternative solution.
  • Of course you can adapt pbEnterText for Joiplay somehow.
In def pbAttemptConnection, modify partner_id:
Ruby:
Expand Collapse Copy
if !$joiplay
   partner_id = pbEnterText(msg, partner_id, false, 5)
  else
   partner_id = pbEnterTextID(msg, 0, 5, "", 1)
end

So I made a very quick adaptation of the default Essentials text input that works in Joiplay smoothly.
Ruby:
Expand Collapse Copy
class PokemonEntrySceneID
  @@Characters = [
    [("0123456789   " + "             " + "             " + "             " + "             ").scan(/./), _INTL("UPPER")],
    [("             " + "             " + "             " + "             " + "             ").scan(/./), _INTL("lower")],
    [("             " + "             " + "             " + "             " + "             ").scan(/./), _INTL("accents")],
    [("             " + "             " + "             " + "             " + "             ").scan(/./), _INTL("other")]
  ]
 
  ROWS    = 13
  COLUMNS = 5
  MODE1   = -6
  MODE2   = -5
  MODE3   = -4
  MODE4   = -3
  BACK    = -2
  OK      = -1

  class NameEntryCursor
    def initialize(viewport)
      @sprite = Sprite.new(viewport)
      @cursortype = 0
      @cursor1 = AnimatedBitmap.new("Graphics/UI/Naming/cursor_1")
      @cursor2 = AnimatedBitmap.new("Graphics/UI/Naming/cursor_2")
      @cursor3 = AnimatedBitmap.new("Graphics/UI/Naming/cursor_3")
      @cursorPos = 0
      updateInternal
    end

    def setCursorPos(value)
      @cursorPos = value
    end

    def updateCursorPos
      value = @cursorPos
      case value
      when PokemonEntryScene2::MODE1   # Upper case
        @sprite.x = 44
        @sprite.y = 120
        @cursortype = 1
      when PokemonEntryScene2::MODE2   # Lower case
        @sprite.x = 106
        @sprite.y = 120
        @cursortype = 1
      when PokemonEntryScene2::MODE3   # Accents
        @sprite.x = 168
        @sprite.y = 120
        @cursortype = 1
      when PokemonEntryScene2::MODE4   # Other symbols
        @sprite.x = 230
        @sprite.y = 120
        @cursortype = 1
      when PokemonEntryScene2::BACK   # Back
        @sprite.x = 314
        @sprite.y = 120
        @cursortype = 2
      when PokemonEntryScene2::OK   # OK
        @sprite.x = 394
        @sprite.y = 120
        @cursortype = 2
      else
        if value >= 0
          @sprite.x = 52 + (32 * (value % PokemonEntryScene2::ROWS))
          @sprite.y = 180 + (38 * (value / PokemonEntryScene2::ROWS))
          @cursortype = 0
        end
      end
    end

    def visible=(value)
      @sprite.visible = value
    end

    def visible
      @sprite.visible
    end

    def color=(value)
      @sprite.color = value
    end

    def color
      @sprite.color
    end

    def disposed?
      @sprite.disposed?
    end

    def updateInternal
      @cursor1.update
      @cursor2.update
      @cursor3.update
      updateCursorPos
      case @cursortype
      when 0 then @sprite.bitmap = @cursor1.bitmap
      when 1 then @sprite.bitmap = @cursor2.bitmap
      when 2 then @sprite.bitmap = @cursor3.bitmap
      end
    end

    def update
      updateInternal
    end

    def dispose
      @cursor1.dispose
      @cursor2.dispose
      @cursor3.dispose
      @sprite.dispose
    end
  end

  def pbStartSceneID(helptext, minlength, maxlength, initialText, subject = 0, pokemon = nil)
    @viewport = Viewport.new(0, 0, Graphics.width, Graphics.height)
    @viewport.z = 99999
    @helptext = helptext
    @helper = CharacterEntryHelper.new(initialText)
    # Create bitmaps
    @bitmaps = []
    @@Characters.length.times do |i|
      @bitmaps[i] = AnimatedBitmap.new(sprintf("Graphics/UI/Naming/overlay_tab_%d", i + 1))
      b = @bitmaps[i].bitmap.clone
      pbSetSystemFont(b)
      textPos = []
      COLUMNS.times do |y|
        ROWS.times do |x|
          pos = (y * ROWS) + x
          textPos.push([@@Characters[i][0][pos], 44 + (x * 32), 24 + (y * 38), :center,
                        Color.new(16, 24, 32), Color.new(160, 160, 160)])
        end
      end
      pbDrawTextPositions(b, textPos)
      @bitmaps[@@Characters.length + i] = b
    end
    underline_bitmap = Bitmap.new(24, 6)
    underline_bitmap.fill_rect(2, 2, 22, 4, Color.new(168, 184, 184))
    underline_bitmap.fill_rect(0, 0, 22, 4, Color.new(16, 24, 32))
    @bitmaps.push(underline_bitmap)
    # Create sprites
    @sprites = {}
    @sprites["bg"] = IconSprite.new(0, 0, @viewport)
    @sprites["bg"].setBitmap("Graphics/UI/Naming/bg")
    case subject
    when 1   # Player
      meta = GameData::PlayerMetadata.get($player.character_ID)
      if meta
        @sprites["shadow"] = IconSprite.new(0, 0, @viewport)
        @sprites["shadow"].setBitmap("Graphics/UI/Naming/icon_shadow")
        @sprites["shadow"].x = 66
        @sprites["shadow"].y = 64
        filename = pbGetPlayerCharset(meta.walk_charset, nil, true)
        @sprites["subject"] = TrainerWalkingCharSprite.new(filename, @viewport)
        charwidth = @sprites["subject"].bitmap.width
        charheight = @sprites["subject"].bitmap.height
        @sprites["subject"].x = 88 - (charwidth / 8)
        @sprites["subject"].y = 76 - (charheight / 4)
      end
    when 2   # Pokémon
      if pokemon
        @sprites["shadow"] = IconSprite.new(0, 0, @viewport)
        @sprites["shadow"].setBitmap("Graphics/UI/Naming/icon_shadow")
        @sprites["shadow"].x = 66
        @sprites["shadow"].y = 64
        @sprites["subject"] = PokemonIconSprite.new(pokemon, @viewport)
        @sprites["subject"].setOffset(PictureOrigin::CENTER)
        @sprites["subject"].x = 88
        @sprites["subject"].y = 54
        @sprites["gender"] = BitmapSprite.new(32, 32, @viewport)
        @sprites["gender"].x = 430
        @sprites["gender"].y = 54
        @sprites["gender"].bitmap.clear
        pbSetSystemFont(@sprites["gender"].bitmap)
        textpos = []
        if pokemon.male?
          textpos.push([_INTL("♂"), 0, 6, :left, Color.new(0, 128, 248), Color.new(168, 184, 184)])
        elsif pokemon.female?
          textpos.push([_INTL("♀"), 0, 6, :left, Color.new(248, 24, 24), Color.new(168, 184, 184)])
        end
        pbDrawTextPositions(@sprites["gender"].bitmap, textpos)
      end
    when 3   # NPC
      @sprites["shadow"] = IconSprite.new(0, 0, @viewport)
      @sprites["shadow"].setBitmap("Graphics/UI/Naming/icon_shadow")
      @sprites["shadow"].x = 66
      @sprites["shadow"].y = 64
      @sprites["subject"] = TrainerWalkingCharSprite.new(pokemon.to_s, @viewport)
      charwidth = @sprites["subject"].bitmap.width
      charheight = @sprites["subject"].bitmap.height
      @sprites["subject"].x = 88 - (charwidth / 8)
      @sprites["subject"].y = 76 - (charheight / 4)
    when 4   # Storage box
      @sprites["subject"] = TrainerWalkingCharSprite.new(nil, @viewport)
      @sprites["subject"].altcharset = "Graphics/UI/Naming/icon_storage"
      @sprites["subject"].anim_duration = 0.4
      charwidth = @sprites["subject"].bitmap.width
      charheight = @sprites["subject"].bitmap.height
      @sprites["subject"].x = 88 - (charwidth / 8)
      @sprites["subject"].y = 52 - (charheight / 2)
    end
    @sprites["bgoverlay"] = BitmapSprite.new(Graphics.width, Graphics.height, @viewport)
    pbDoUpdateOverlay
    @blanks = []
    @mode = 0
    @minlength = minlength
    @maxlength = maxlength
    @maxlength.times do |i|
      @sprites["blank#{i}"] = Sprite.new(@viewport)
      @sprites["blank#{i}"].x = 160 + (24 * i)
      @sprites["blank#{i}"].bitmap = @bitmaps[@bitmaps.length - 1]
      @blanks[i] = 0
    end
    @sprites["bottomtab"] = Sprite.new(@viewport)   # Current tab
    @sprites["bottomtab"].x = 22
    @sprites["bottomtab"].y = 162
    @sprites["bottomtab"].bitmap = @bitmaps[@@Characters.length]
    @sprites["toptab"] = Sprite.new(@viewport)   # Next tab
    @sprites["toptab"].x = 22 - 504
    @sprites["toptab"].y = 162
    @sprites["toptab"].bitmap = @bitmaps[@@Characters.length + 1]
    @sprites["controls"] = IconSprite.new(0, 0, @viewport)
    @sprites["controls"].x = 16
    @sprites["controls"].y = 96
    @sprites["controls"].setBitmap(_INTL("Graphics/UI/Naming/overlay_controls"))
    @sprites["overlay"] = BitmapSprite.new(Graphics.width, Graphics.height, @viewport)
    pbDoUpdateOverlay2
    @sprites["cursor"] = NameEntryCursor.new(@viewport)
    @cursorpos = 0
    @refreshOverlay = true
    @sprites["cursor"].setCursorPos(@cursorpos)
    pbFadeInAndShow(@sprites) { pbUpdate }
  end
 
  def pbUpdateOverlay
    @refreshOverlay = true
  end

  def pbDoUpdateOverlay2
    overlay = @sprites["overlay"].bitmap
    overlay.clear
    modeIcon = [[_INTL("Graphics/UI/Naming/icon_mode"), 44 + (@mode * 62), 120, @mode * 60, 0, 60, 44]]
    pbDrawImagePositions(overlay, modeIcon)
  end

  def pbDoUpdateOverlay
    return if !@refreshOverlay
    @refreshOverlay = false
    bgoverlay = @sprites["bgoverlay"].bitmap
    bgoverlay.clear
    pbSetSystemFont(bgoverlay)
    textPositions = [
      [@helptext, 160, 18, :left, Color.new(16, 24, 32), Color.new(160, 160, 160)]
    ]
    chars = @helper.textChars
    x = 172
    chars.each do |ch|
      textPositions.push([ch, x, 54, :center, Color.new(0, 0, 0), Color.new(160, 160, 160)])
      x += 24
    end
    pbDrawTextPositions(bgoverlay, textPositions)
  end

  def pbChangeTab(newtab = @mode + 1)
    pbSEPlay("GUI naming tab swap start")
    @sprites["cursor"].visible = false
    @sprites["toptab"].bitmap = @bitmaps[(newtab % @@Characters.length) + @@Characters.length]
    # Move bottom (old) tab down off the screen, and move top (new) tab right
    # onto the screen
    timer_start = System.uptime
    loop do
      @sprites["bottomtab"].y = lerp(162, 414, 0.5, timer_start, System.uptime)
      @sprites["toptab"].x = lerp(22 - 504, 22, 0.5, timer_start, System.uptime)
      Graphics.update
      Input.update
      pbUpdate
      break if @sprites["toptab"].x >= 22 && @sprites["bottomtab"].y >= 414
    end
    # Swap top and bottom tab around
    @sprites["toptab"].x, @sprites["bottomtab"].x = @sprites["bottomtab"].x, @sprites["toptab"].x
    @sprites["toptab"].y, @sprites["bottomtab"].y = @sprites["bottomtab"].y, @sprites["toptab"].y
    @sprites["toptab"].bitmap, @sprites["bottomtab"].bitmap = @sprites["bottomtab"].bitmap, @sprites["toptab"].bitmap
    Graphics.update
    Input.update
    pbUpdate
    # Set the current mode
    @mode = newtab % @@Characters.length
    # Set the top tab up to be the next tab
    newtab = @bitmaps[((@mode + 1) % @@Characters.length) + @@Characters.length]
    @sprites["cursor"].visible = true
    @sprites["toptab"].bitmap = newtab
    @sprites["toptab"].x = 22 - 504
    @sprites["toptab"].y = 162
    pbSEPlay("GUI naming tab swap end")
    pbDoUpdateOverlay2
  end

  def pbUpdate
    @@Characters.length.times do |i|
      @bitmaps[i].update
    end
    # Update which inputted text's character's underline is lowered to indicate
    # which character is selected
    cursorpos = @helper.cursor.clamp(0, @maxlength - 1)
    @maxlength.times do |i|
      @blanks[i] = (i == cursorpos) ? 1 : 0
      @sprites["blank#{i}"].y = [78, 82][@blanks[i]]
    end
    pbDoUpdateOverlay
    pbUpdateSpriteHash(@sprites)
  end

  def pbColumnEmpty?(m)
    return false if m >= ROWS - 1
    chset = @@Characters[@mode][0]
    COLUMNS.times do |i|
      return false if chset[(i * ROWS) + m] != " "
    end
    return true
  end

  def wrapmod(x, y)
    result = x % y
    result += y if result < 0
    return result
  end

  def pbMoveCursor
    oldcursor = @cursorpos
    cursordiv = @cursorpos / ROWS   # The row the cursor is in
    cursormod = @cursorpos % ROWS   # The column the cursor is in
    cursororigin = @cursorpos - cursormod
    if Input.repeat?(Input::LEFT)
      if @cursorpos < 0   # Controls
        @cursorpos -= 1
        @cursorpos = OK if @cursorpos < MODE1
      else
        loop do
          cursormod = wrapmod(cursormod - 1, ROWS)
          @cursorpos = cursororigin + cursormod
          break unless pbColumnEmpty?(cursormod)
        end
      end
    elsif Input.repeat?(Input::RIGHT)
      if @cursorpos < 0   # Controls
        @cursorpos += 1
        @cursorpos = MODE1 if @cursorpos > OK
      else
        loop do
          cursormod = wrapmod(cursormod + 1, ROWS)
          @cursorpos = cursororigin + cursormod
          break unless pbColumnEmpty?(cursormod)
        end
      end
    elsif Input.repeat?(Input::UP)
      if @cursorpos < 0         # Controls
        case @cursorpos
        when MODE1 then @cursorpos = ROWS * (COLUMNS - 1)
        when MODE2 then @cursorpos = (ROWS * (COLUMNS - 1)) + 2
        when MODE3 then @cursorpos = (ROWS * (COLUMNS - 1)) + 4
        when MODE4 then @cursorpos = (ROWS * (COLUMNS - 1)) + 6
        when BACK  then @cursorpos = (ROWS * (COLUMNS - 1)) + 9
        when OK    then @cursorpos = (ROWS * (COLUMNS - 1)) + 11
        end
      elsif @cursorpos < ROWS   # Top row of letters
        case @cursorpos
        when 0, 1     then @cursorpos = MODE1
        when 2, 3     then @cursorpos = MODE2
        when 4, 5     then @cursorpos = MODE3
        when 6, 7     then @cursorpos = MODE4
        when 8, 9, 10 then @cursorpos = BACK
        when 11, 12   then @cursorpos = OK
        end
      else
        cursordiv = wrapmod(cursordiv - 1, COLUMNS)
        @cursorpos = (cursordiv * ROWS) + cursormod
      end
    elsif Input.repeat?(Input::DOWN)
      if @cursorpos < 0                      # Controls
        case @cursorpos
        when MODE1 then @cursorpos = 0
        when MODE2 then @cursorpos = 2
        when MODE3 then @cursorpos = 4
        when MODE4 then @cursorpos = 6
        when BACK  then @cursorpos = 9
        when OK    then @cursorpos = 11
        end
      elsif @cursorpos >= ROWS * (COLUMNS - 1)   # Bottom row of letters
        case cursormod
        when 0, 1     then @cursorpos = MODE1
        when 2, 3     then @cursorpos = MODE2
        when 4, 5     then @cursorpos = MODE3
        when 6, 7     then @cursorpos = MODE4
        when 8, 9, 10 then @cursorpos = BACK
        else               @cursorpos = OK
        end
      else
        cursordiv = wrapmod(cursordiv + 1, COLUMNS)
        @cursorpos = (cursordiv * ROWS) + cursormod
      end
    end
    if @cursorpos != oldcursor   # Cursor position changed
      @sprites["cursor"].setCursorPos(@cursorpos)
      pbPlayCursorSE
      return true
    end
    return false
  end

  def pbEntry
    ret = ""
    loop do
      Graphics.update
      Input.update
      pbUpdate
      next if pbMoveCursor
      if Input.trigger?(Input::SPECIAL)
        #pbChangeTab
      elsif Input.trigger?(Input::ACTION)
        @cursorpos = OK
        @sprites["cursor"].setCursorPos(@cursorpos)
      elsif Input.trigger?(Input::BACK)
        @helper.delete
        pbPlayCancelSE
        pbUpdateOverlay
      elsif Input.trigger?(Input::USE)
        case @cursorpos
        when BACK   # Backspace
          @helper.delete
          pbPlayCancelSE
          pbUpdateOverlay
        when OK     # Done
          pbSEPlay("GUI naming confirm")
          if @helper.length >= @minlength
            ret = @helper.text
            break
          end
        when MODE1
          #pbChangeTab(0) if @mode != 0
        when MODE2
          #pbChangeTab(1) if @mode != 1
        when MODE3
          #pbChangeTab(2) if @mode != 2
        when MODE4
          #pbChangeTab(3) if @mode != 3
        else
          cursormod = @cursorpos % ROWS
          cursordiv = @cursorpos / ROWS
          charpos = (cursordiv * ROWS) + cursormod
          chset = @@Characters[@mode][0]
          @helper.delete if @helper.length >= @maxlength
          @helper.insert(chset[charpos])
          pbPlayCursorSE
          if @helper.length >= @maxlength
            @cursorpos = OK
            @sprites["cursor"].setCursorPos(@cursorpos)
          end
          pbUpdateOverlay
          # Auto-switch to lowercase letters after the first uppercase letter is selected
          #pbChangeTab(1) if @mode == 0 && @helper.cursor == 1
        end
      end
    end
    Input.update
    return ret
  end

  def pbEndScene
    pbFadeOutAndHide(@sprites) { pbUpdate }
    @bitmaps.each do |bitmap|
      bitmap&.dispose
    end
    @bitmaps.clear
    pbDisposeSpriteHash(@sprites)
    @viewport.dispose
  end
end

class PokemonEntry
  def pbStartScreenID(helptext, minlength, maxlength, initialText, mode = -1, pokemon = nil)
    @scene.pbStartSceneID(helptext, minlength, maxlength, initialText, mode, pokemon)
    ret = @scene.pbEntry
    @scene.pbEndScene
    return ret
  end
end

def pbEnterTextID(helptext, minlength, maxlength, initialText = "", mode = 0, pokemon = nil, nofadeout = false)
  ret = ""
  pbFadeOutIn(99999, nofadeout) do
    sscene = PokemonEntrySceneID.new
    sscreen = PokemonEntry.new(sscene)
    ret = sscreen.pbStartScreenID(helptext, minlength, maxlength, initialText, mode, pokemon)
  end
  return ret
end
Yes, this is just a simple copy of the PokemonEntryScene class, you can create your own anyway, this is just a quick fix to get you started testing the system. But I removed all the other characters and made it allow you to add only numbers in a quick way!
With this, you can run Cable Club on Joiplay. Good Game!
Codes for version v21.1 Essentials with unofficial EBDX.
 
Last edited:

GEN 9 PACK

REVIVAL BLESSING SYNC
  • Information exchange is different in Online battle so this is necessary, add this to your code.
Ruby:
Expand Collapse Copy
class Battle_CableClub < Battle
  def pbReviveInParty(idxBattler, canCancel = false)
    pbWithSynchronizedOrder do
      party_index = -1
      if pbOwnedByPlayer?(idxBattler)
        @scene.pbPartyScreen(idxBattler, canCancel, 2) { |idxParty, partyScene|
          party_index = idxParty
          next true
        }
        @connection.send do |writer|
          writer.sym(:revive)
          writer.int(party_index)
        end
      else
        frame = 0
        @scene.pbShowWindow(Battle::Scene::MESSAGE_BOX)
        cw = @scene.sprites["messageWindow"]
        cw.letterbyletter = false
        begin
          revive_info = false
          loop do
            frame += 1
            cw.text = _INTL("Esperando o adversário escolher um Pokémon para ser revivido" + "." * (1 + ((frame / 8) % 3)))
            @scene.pbFrameUpdate(cw)
            Graphics.update
            Input.update
            raise Connection::Disconnected.new("disconnected") if Input.trigger?(Input::BACK) && pbConfirmMessageSerious("Você gostaria de se desconectar?")
            @connection.update do |record|
              type = record.sym
              if type == :forfeit
                pbSEPlay("Battle flee")
                pbDisplay(_INTL("{1} perdeu a partida!", @opponent[0].full_name))
                @decision = 1
                pbAbort
              elsif type == :revive
                party_index = record.int
                revive_info = true
              else
                raise "Unknown message: #{type}"
              end
            end
            break if revive_info
          end
        ensure
          cw.letterbyletter = false
        end
      end
      return if party_index < 0
      party = pbParty(idxBattler)
      pkmn = party[party_index]
      pkmn.hp = [1, (pkmn.totalhp / 2).floor].max
      pkmn.heal_status
      displayname = (pbOwnedByPlayer?(idxBattler)) ? pkmn.name : _INTL("O oponente {1}", pkmn.name)
      pbDisplay(_INTL("{1} foi revivido e está pronto para lutar novamente!", displayname))
    end
  end
end

LOADED DICE SYNC FIX
  • The code for version 3.3.1 of the Gen 9 Pack currently contains a use of rand() instead of @battle.pbRandom. This will desynchronize your battle because seed changes work within it. The report has been submitted and the fix will be coming soon, but pay attention to the classes Battle::Move::HitTenTimes and Battle::Move::HitTwoToFiveTimes. They will contain rand.

DESYNCHRONIZATION FIX


Today, we will work on def pb Attempt Connection in the CableClubScreen class.

STEPS
1.
We need to store the connection created during the match search in a support variable. So, edit def pbConnectServer(partner_id).
Ruby:
Expand Collapse Copy
  def pbConnectServer(partner_id)
    host,port = CableClub::get_server_info
    Connection.open(host,port) do |connection|
      @load_connection = connection
      await_server(@load_connection,partner_id)
    end
  end
2. Now, in def pbAttemptConnection, just below the line rescue Connection::Disconnected =>, add:
@load_connection&.dispose

EXPLANATION

Once this is done, it will force the socket and connection to close, so that the message played in pbDisplay is displayed when everything is finished, instead of waiting for the message confirmation to close the connection.
This is necessary because our connected friend will continue to receive irregular information as if you were still in the room since @load_connection still exists, which should cause a desynchronization failure in a short space of time. With this, we will have our character disconnected in real time, regardless of whether or not we wait for the pbDisplay message to finish.

Codes for version v21.1 Essentials with unofficial EBDX.
 
Last edited:
Is there any YouTube or video tutorial showing step by step how to make this work? I have a lot of questions and I'm not very good with VPNs and network stuff, but I really want to implement trades and online battles in my game
 
Is there any YouTube or video tutorial showing step by step how to make this work? I have a lot of questions and I'm not very good with VPNs and network stuff, but I really want to implement trades and online battles in my game
In fact, the documentation that Vendily left in the download is sufficient. You don't need a video for this because it's very simple. Just follow the steps calmly. Mainly because I don't know anything about it, I had never done it before and it worked very well.
But I still haven't been able to fix all the problems in this script, Vendily seems to be very busy too.
So there's a second option here:
 
Not sure if anyone is still around, but if someone sees this I'm having issues getting this to work as when I enter the user id it throws the following error "invalid literal for int() with base 10: 'FIRE'". whereas "FIRE" is the type of the Pokemon I have
Server Log: https://bin.bloerg.net/nRClNXb+vGl
Game Log: https://bin.bloerg.net/qCxEazhUMwb

If anyone can help I'd greatly appreciate it.
It's expecting an integer, and it looks like the typing is being sent. It's likely that the writing of Pokémon data and the reading of cable_club.py aren't synchronized.

Have you enabled support for the plugins you installed? Like terastal? This is done in:
TERA_INSTALLED = true
You need to check the other plugins as well. This will ensure proper data synchronization.
 
It's expecting an integer, and it looks like the typing is being sent. It's likely that the writing of Pokémon data and the reading of cable_club.py aren't synchronized.

Have you enabled support for the plugins you installed? Like terastal? This is done in:
TERA_INSTALLED = true
You need to check the other plugins as well. This will ensure proper data synchronization.
I was talking on the discord and I figuered it out. For whatever reason the plugin breaks when the Dynamax plugin is installed, even when the value it enabled. Seems that on line 385 on the CableClub class "pkmn.gmax_factor = record.bool" keeps getting returned null
 
I was talking on the discord and I figuered it out. For whatever reason the plugin breaks when the Dynamax plugin is installed, even when the value it enabled. Seems that on line 385 on the CableClub class "pkmn.gmax_factor = record.bool" keeps getting returned null
Very good! I haven't tested this script on v21.1 yet! Just the Z-Moves and they worked great!
Support for EBDX v21.1 updated on 07/14/2025 https://eeveeexpo.com/threads/3737/post-82729
 
I am having a time trying to get everything working, and I almost am, but every time I try to connect it tells me my party is invalid, no matter what permutation I give.

Errors: ["invalid literal for int() with base 10: '0...3'"]

That is the only error I'm receiving, or the '0...3' is changed to different numbers, and it's coming from the server log. Does anyone have any ideas? I should clarify, I am using v21.1 and have added a custom type, if that does anything.
 
Last edited:
I am having a time trying to get everything working, and I almost am, but every time I try to connect it tells me my party is invalid, no matter what permutation I give.

Errors: ["invalid literal for int() with base 10: '0...3'"]

That is the only error I'm receiving, or the '0...3' is changed to different numbers, and it's coming from the server log. Does anyone have any ideas? I should clarify, I am using v21.1 and have added a custom type, if that does anything.
I recommend you install the script in a standard essentials, test its efficiency, then add your configuration according to the system's compatibility and eliminate what may be wrong.
 
Back
Top