Soulless

Unofficial Mahjong Soul game API client for elixir.

This is an early prototype and should not be used for anything serious

It provides:

Installation

The package can be installed by adding the following to your list of dependencies in mix.exs:

def deps do
  [
    {:soulless, "~> 0.3.0"}
  ]
end

Getting started

See hexdocs for a list of generated fetch functions.

Authentication

Before you can connect, you'll need to get yourself a set of necessary credentials. Which credentials you'll need depends on the server you want to connect to and the method you chose.

Supported flows are:

Connecting

Mahjong Soul is a bit weird and makes connections to up to four different servers, each with a different protocol. To that end, this library implements each of them as a separate module. Currently we have: Soulless.LobbyClient, Soulless.GameClient, Soulless.SpectatorClient, Soulless.ChatClient.

# see the "authentication" section for information on what to put here
opts = [
  user_id: "123456789",
  token: "effeffeffeffeffeffeffeffeffeff",
  login_method: :yostar_session,
  server: :en
]

# start the client
{:ok, client} = Soulless.LobbyClient.start_link(opts)

# you will find relevant fetch functions in the same module
# these are automatically generated from the SoullessProto.descriptor/1 along with typespecs
# check the documentation for a full list

# simple request that do not require a payload can just be called with the client pid
{:ok, %Soulless.Game.Lq.ResFetchInfo{}} = Soulless.LobbyClient.fetch_info(client)

# some request may require the game version to be included in the payload
# you can retrieve it from the client like so
version = Soulless.LobbyClient.version(client)

{:ok, %Soulless.Game.Lq.ResGameRecord{}} =
  Soulless.LobbyClient.fetch_game_record(
    client,
    %Soulless.Game.Lq.ReqGameRecord{
      client_version_string: "web-#{version}", 
      game_uuid: "260413-133c2058-a806-4122-9b89-2350bbb83c29"
    }
  )

Note: there are currently no plans to document every generated function and their arguments. Figuring them out is a bit of a challenge and left as an exercise for the reader. The easiest approach would be to proxy the real game traffic to observe what is being sent for which actions.

Certain messages, known as notices, are sent unconditionally by the server. This includes things like: friends logging on, decoration changes while in a friendly room, actions that happened in a mahjong game. In order to do something with them, you'll need to implement the Soulless.Handler behaviour and pass it to start/1 or start_link/1. You almost certainly need it to use anything but the Soulless.LobbyClient effectively (and it's useful even for that one).

defmodule ExampleHandler do
  use Soulless.Handler

  @impl Soulless.Handler
  # called with either :connected or :authenticated `state`
  # in case you need to know when the connection is ready to use
  def handle_ready(_client, _state) do
    :ok
  end

  @impl Soulless.Handler
  # this handler is executed in a spawned task, meaning it is safe to make requests to the client
  def handle_notice(_client, %Soulless.Game.Lq.NotifyFriendStateChange{} = message) do
    message_prefix = "Our friend UID #{message.active_state.account_id} has"

    case {message.active_state.is_online, message.active_state.playing} do
      {false, _} -> IO.puts(message_prefix <> " logged out")
      {true, nil} -> IO.puts(message_prefix <> " is online")
      {true, _} -> IO.puts(message_prefix <> " joined a match")
    end
  end

  # don&#39;t forget about the default clause!
  def handle_notice(_client, _notice) do
    :ok
  end
end

# use it like so
{:ok, client} = Soulless.LobbyClient.start_link([handler: ExampleHandler, ...])

While hopefully not necessary, you can alter the lower level aspects of the client by providing your own implementation of the Soulless.Websocket.Implementation behaviour. That's what encodes and decodes messages, performs endpoint discovery, runs the authentication flow and decides what to do after connecting and disconnecting. You can find the default implementations in lib/websocket/implementation/.

{:ok, client} = Soulless.LobbyClient.start_link([implementation: MyOwn.Websocket.Implementation, ...])