DEV Community

idk
idk

Posted on

PySupercell Core Guide: Building Your Own Supercell Game Server

So you've decided to create your own server for a Supercell game and Python caught your eye. Among countless questionable projects, you stumbled upon PySupercell Core. What now?

Why PySupercell Core?

Supercell games (Clash of Clans, Brawl Stars etc.) share similar architecture—they’re built on a core (server foundation). PySupercell Core (PSC) is a fresh Python core that:

  • Implements Supercell’s base server architecture
  • Easily adapts to any SC game
  • But isn’t a ready-made server—you’ll write the logic yourself

Most other Python servers/cores are slow and outdated. PSC is fast and user-friendly

Installing PSC

  1. Clone the repo:

    git clone https://github.com/REtard-1337/pysupercell-core
    cd pysupercell-core
    
  2. Install dependencies:

    pip install -r requirements.txt
    

Picking Ur Game

Navigate to logic/messages/ and find logic_magic_message_factory.py
Here magic is Clash of Clans' codename. Swap it for your game:

Game Codename
Clash of Clans magic
Brawl Stars laser
Clash Royale scroll
Hay Day soil
Boom Beach reef
Squad Busters squad

Server Configuration

Open config.toml and set game parameters. Example for Brawl Stars v52.13.77:

[game]
major_version = 52
build = 13
content_version = 77
environment = "int"
Enter fullscreen mode Exit fullscreen mode

Now your server seems ready... but when you launch it...

— Wait, why is the client stuck at "Connecting to server..."? That means PSC is broken!!!
— Nope, it works! PSC is a core, not a full server. You must implement all packets yourself


Writing Messages

Let’s take LoginMessage as an example.

  1. Create logic/messages/auth/login_message.py
  2. Code the packet:

    from titan.message.piranha_message import PiranhaMessage
    from titan.math.logic_long import LogicLong
    
    class LoginMessage(PiranhaMessage):
        def __init__(self):
            super().__init__()
            self.account_id = LogicLong() 
            self.pass_token = ""
            self.major_version = 0
    
        def decode(self):
            super().decode()
            self.account_id = self.stream.read_long()
            self.pass_token = self.stream.read_string() 
            self.major_version = self.stream.read_int()
            # ...
    
        def get_message_type(self) -> int:
            return 10101
    

But this seems complicated, so let’s break it down.

First, imports:

from titan.message.piranha_message import PiranhaMessage
from titan.math.logic_long import LogicLong
Enter fullscreen mode Exit fullscreen mode

Then create LoginMessage class—it must inherit from PiranhaMessage:

class LoginMessage(PiranhaMessage):
    ...
Enter fullscreen mode Exit fullscreen mode

PiranhaMessage is the base class for all packets. It provides stream (like Classic-Brawl’s Reader/Writer)

Initialize fields in the constructor:

    def __init__(self):
        super().__init__()
        self.account_id = LogicLong()
        self.pass_token = ""
        self.major_version = 0
Enter fullscreen mode Exit fullscreen mode

Now the decode:

    def decode(self):
        super().decode()
        self.account_id = self.stream.read_long()
        self.pass_token = self.stream.read_string()
        self.major_version = self.stream.read_int()
Enter fullscreen mode Exit fullscreen mode

And message type:

    def get_message_type(self) -> int:
        return 10101
Enter fullscreen mode Exit fullscreen mode

Fascinating! But what about server responses?
Create login_ok_message.py next to login_message.py:

from titan.math.logic_long import LogicLong
from titan.message.piranha_message import PiranhaMessage  

class LoginOkMessage(PiranhaMessage):  
    def __init__(self) -> None:  
        super().__init__()  
        self.account_id = LogicLong()  
        self.home_id = LogicLong()  
        self.pass_token = ""  
        ...  

    def encode(self):
        super().encode()
        self.stream.write_long(self.account_id)
        self.stream.write_long(self.home_id)
        self.stream.write_string(self.pass_token)
        # ...

    def get_message_type(self) -> int:
        return 20104
Enter fullscreen mode Exit fullscreen mode

Not much to explain—since this is a server packet, we implement encode() using fields from the constructor


Handling Packets

Notice no process(), execute(), or handle() in message classes? Supercell uses a different approach—all packets are handled via MessageManager.receive_message
Example for LoginMessage:

# server/protocol/message_manager.py
from logic.messages.auth import LoginMessage, LoginOkMessage
from titan.math import LogicLong

class MessageManager:  
    def __init__(self, connection):
        self.connection = connection

    async def receive_message(self, message):
        match message.get_message_type():
            case 10101:
                await self.on_login(message)
            case _:
                ...   # other stuff

    async def on_login(self, message: LoginMessage):       
        response = LoginOkMessage()
        response.account_id = ...
        response.pass_token = ...
        # ...
        await self.connection.send_message(response)
Enter fullscreen mode Exit fullscreen mode

What’s happening?
First, message_manager.py isn’t empty—it has base structure (see screenshot below)
Image description

We added a case to handle login_message when its ID arrives:

    async def receive_message(self, message):
        ...
            case 10101:
                await self.on_login(message)  # await 'cause async
Enter fullscreen mode Exit fullscreen mode

Now observe on_login:

    async def on_login(self, message: LoginMessage):
        response = LoginOkMessage()
        response.account_id = ...
        response.pass_token = ...
        # ...
        await self.connection.send_message(response)
Enter fullscreen mode Exit fullscreen mode

We pass the incoming packet:

    async def on_login(self, message: LoginMessage):
Enter fullscreen mode Exit fullscreen mode

Create a response packet:

        response = LoginOkMessage()
Enter fullscreen mode Exit fullscreen mode

Fill its fields:

        response.account_id = ...
        response.pass_token = ...
Enter fullscreen mode Exit fullscreen mode

Then send it back:

        await self.connection.send_message(response)
Enter fullscreen mode Exit fullscreen mode

Got questions? DM:

  • t.me/TheBladewise1337
  • t.me/user_with_username

Top comments (0)