51. Cookbook 1: Creating a room-based MO game

When making an MO game using iFun Engine, you can use the engine’s event subsystem to create game sessions played by multitudes of people in real time.

This document will explain these concepts by making a simple MO room:

  • Asynchronous event handling

  • Unlocked synchronization

  • Implementing asynchronous functions (using C++ 11, 14, 17)

51.1. Setting up a room project

Use the following commands to create a room project:

$ funapi_initiator room
$ room-source/setup_build_environment --type=makefile

Modify the following files:

  • CMakeLists.txt

  • src/event_handlers.cc

  • src/object_model/example.json

Modify to use the latest C++

Change set(WANT_CXX11 false) to true in the CMakeLists.txt file in the source tree root. Ignore if this is already true.

# 중략

# Needs C++1x features? (Requires modern C++ compiler)
set(WANT_CXX11 true)
$ funapi_initiator room --csharp
$ room-source/setup_build_environment --type=makefile

Modify the following files:

  • CMakeLists.txt

  • mono/server.cs

  • src/object_model/example.json

51.2. Implementing the MO session

51.2.1. Implementing the rooms

First, add a simple ORM.

src/object_model/example.json

{
  "User": {
    "Name": "String KEY",
    "Level": "Integer",
    "CharacterId": "Integer"
  }
}

The following shows the implementation of a simple room.

Note

You can use a predefined ErrorCode when sending result messages, but success and failure have been handled simply here for the sake of giving a simple example.

src/event_handlers.cc

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
#include <funapi.h>

void SendMessage(const Ptr<Session> &session, const string &msg_type,
                 const bool result) {
  Json message;
  message["result"] = result;
  session->SendMessage(msg_type, message);
  return;
}


// 하나의 Room 을 관리하기 위한 클래스입니다.
class Room : public boost::enable_shared_from_this<Room> {
 public:
  DECLARE_CLASS_PTR(Room);

  typedef boost::unordered_map<Uuid, Ptr<Room> > RoomMap;

  // 유저를 관리하기 위한 struct 입니다.
  // 유저를 위한 데이터를 모두 DB 에 저장하지 않을 뿐더러,
  // DB 에 저장되지 않는 데이터 중에 방 관련 데이터를 기록해야되기 때문에,
  // 이 클래스는 ORM 의 User 와는 별개입니다.
  struct User {
    User(const Ptr<Session> &_session, const string &_name,
         const int64_t _character_id, const int64_t _level)
      : session(_session), name(_name),
        character_id(_character_id), level(_level) {
    }

    // 유저의 Session 입니다.
    const Ptr<Session> session;

    // 유저의 이름입니다.
    string name;

    // 유저의 캐릭터 id 입니다.
    int64_t character_id;

    // 유저의 레벨입니다.
    int64_t level;
  };

  static Ptr<Room> Create(const string &name,
                          const int64_t joinable_min_level,
                          const string &master_user_name) {
    Uuid id = RandomGenerator::GenerateUuid();
    Ptr<Room> room(new Room(id, name, joinable_min_level, master_user_name));

    boost::mutex::scoped_lock lock(the_mutex);
    the_rooms.insert(std::make_pair(id, room));

    return room;
  }


  static Ptr<Room> Find(const Uuid &id) {
    boost::mutex::scoped_lock lock(the_mutex);

    RoomMap::iterator it = the_rooms.find(id);
    if (it == the_rooms.end()) {
      return Room::kNullPtr;
    }

    return it->second;
  }


  static Json GetRooms() {
    Json rooms;
    {
      boost::mutex::scoped_lock lock(the_mutex);

      BOOST_FOREACH(const RoomMap::value_type &pair, the_rooms) {
        string room_id = boost::lexical_cast<string>(pair.first);
        rooms[room_id] = pair.second->GetInfo();
      }
    }

    return rooms;
  }


  ~Room() {
    sessions_.clear();
  }


  const Uuid &id() const { return id_; }
  const string &name() const { return name_; }
  int64_t joinable_min_level() const { return joinable_min_level_; }
  const string &master_user_name() const { return master_user_name_; }
  void SetMasterUserName(const string &master_user_name) {
    master_user_name_ = master_user_name;
  }


  // 유저가 Room 에 입장하는 것을 처리합니다.
  // 클라이언트로부터 "client_join" 이라는 메시지가 오면 호출됩니다.
  void HandleJoin(const Ptr<Session> &session, const string &name,
                  const int64_t character_id, const int64_t level) {
    // 유저가 Room 에 입장할 수 있는지 조건을 검사합니다.

    // 이미 입장했었는지 확인합니다.
    if (sessions_.count(session->id()) > 0) {
      SendMessage(session, "client_join", false);
      return;
    }

    // 이름이나 캐릭터 id 가 정상인지 확인합니다.
    if (name.empty() || character_id < 0) {
      SendMessage(session, "client_join", false);
      return;
    }

    // 입장 가능한 최소 레벨을 만족하는지 검사합니다.
    if (joinable_min_level() < level) {
      SendMessage(session, "client_join", false);
      return;
    }

    // 모든 조건에 만족하였습니다. 이제 Room 에 입장시키도록 하겠습니다.
    Ptr<User> user(new User(session, name, character_id, level));
    bool has_not_joined =
        sessions_.insert(std::make_pair(session->id(), user)).second;

    // 입장한 적이 없어야 합니다.
    BOOST_ASSERT(has_not_joined);

    // 성공적으로 입장했습니다.
    // session context 에 room id 를 저장하여
    // 메시지 핸들러에서 클라이언트가 room id 를 보내지 않아도
    // session context 로부터 room id 를 가져와 사용하도록 하겠습니다.
    session->AddToContext("room_id", boost::lexical_cast<string>(id()));

    // 입장하는 유저를 포함한 Room 내 유저들에게
    // 입장 메시지를 broadcasting 합니다.
    Json message;

    // 입장한 유저의 정보를 입력합니다.
    message["name"] = name;
    message["character_id"] = character_id;
    message["level"] = level;
    for (const auto &v: sessions_) {
      // server_user_join 이라는 msg type 으로 메시지를 전송합니다.
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (v.second->session->IsTransportAttached()) {
        v.second->session->SendMessage("server_user_join", message);
      }
    }
  }


  // Room 나가기를 처리합니다.
  void HandleLeave(const Ptr<Session> &session) {
    string user_name;
    {
      auto user = sessions_.find(session->id());

      // Room 에 존재하지 않는 session(유저) 입니다.
      if (user == sessions_.cend()) {
        SendMessage(session, "client_leave", false);
        return;
      }

      user_name = user->second->name;

      // Room 에서 유저를 삭제합니다.
      sessions_.erase(user);

      // 모든 유저가 Room 에서 나갔으므로 방을 빈방으로 처리하겠습니다.
      // 여기서는 간단하게 name 만 지우면 빈방이 되겠다고 하겠습니다.
      if (sessions_.size() == 0) {
        name_ = "";
      }

      // 떠나는 유저에게 메시지를 전송합니다.
      SendMessage(session, "client_leave", true);
    }

    // 떠나는 유저를 제외한 Room 내 유저들에게 메시지를 전송합니다.
    Json message;
    message["name"] = user_name;
    for (const auto &v: sessions_) {
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (v.second->session->IsTransportAttached()) {
        v.second->session->SendMessage("server_user_leave", message);
      }
    }
  }


  // Room 내 유저들과 채팅하는 것을 처리합니다.
  void HandleChat(const Ptr<Session> &session, const string &msg) {
    string user_name;
    {
      auto user = sessions_.find(session->id());

      // Room 에 존재하지 않는 session(유저) 입니다.
      if (user == sessions_.cend()) {
        SendMessage(session, "client_chat", false);
        return;
      }

      user_name = user->second->name;
    }

    // 유저 이름이 비어 있으면 안됩니다.
    BOOST_ASSERT(not user_name.empty());

    // Room 내 유저들에게 채팅 메시지를 전송합니다.
    Json message;
    message["name"] = user_name;
    message["msg"] = msg;
    for (const auto &v: sessions_) {
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (v.second->session->IsTransportAttached()) {
        v.second->session->SendMessage("server_user_chat", message);
      }
    }
  }


  // Room 에 모인 유저들과의 대전을 시작합니다.
  // 방장이 Start 버튼을 누르면 이 함수가 호출된다고 가정하겠습니다.
  void HandleStartMatch(const Ptr<Session> &session,
                        const string &user_name) {
    // 방장이 아니면 이상한 클라이언트라고 가정하겠습니다.
    if (user_name != master_user_name()) {
      session->Close();
      return;
    }

    Json message;

    // 대전을 진행할 맵을 선택합니다. 여기서는 간단하게
    // 100 ~ 200 사이의 값을 랜덤하게 선택하겠습니다.
    int64_t map_id = RandomGenerator::GenerateNumber(100, 200);

    // 대전 시간을 입력합니다. 여기서는 180 초라고 가정하겠습니다.
    message["match_time"] = 180;
    message["match_map_id"] = map_id;

    // 대전 정보를 전송합니다.
    for (const auto &v: sessions_) {
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (v.second->session->IsTransportAttached()) {
        v.second->session->SendMessage("server_start_match", message);
      }
    }

    // 여기서는 단순히 180 초가 지나면 대전이 끝난다고 가정하겠습니다.
    // 만약 중간에라도 대전이 끝날 수 있다면 match_timer_id_ 를
    // Timer::Cancel(match_timer_id) 함수를 호출하여
    // 아래에서 등록하는 timer 를 취소시킬 수 있습니다.

    // 180 초 이후에 대전을 종료시키는 timer 를 등록합니다.
    match_timer_id_ =
        Timer::ExpireAfter(
            WallClock::FromMsec(180 * 1000),
            bind(&Room::EndMatch, shared_from_this()));
  }


  // 대전을 종료합니다.
  void EndMatch() {
    // 승자와 패자 정보를 전송합니다.
    Json message;

    // ...

    for (const auto &v: sessions_) {
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (v.second->session->IsTransportAttached()) {
        v.second->session->SendMessage("server_end_match", message);
      }
    }
  }


 private:
  Room(const Uuid &id, const string &name, const int64_t joinable_min_level,
       const string &master_user_name)
    : id_(id), name_(name), joinable_min_level_(joinable_min_level),
      master_user_name_(master_user_name) {
  }


  // Room list 를 전달할 때 사용합니다. Json 으로 정보를 전달합니다.
  Json GetInfo() const {
    Json info;
    info["name"] = name();
    int64_t user_count = sessions_.size();
    info["user_count"] = user_count;
    info["joinable_min_level"] = joinable_min_level();
    info["master_user_name"] = master_user_name();

    return info;
  }


  // Room 의 고유 id 입니다.
  const Uuid id_;

  // Room 의 이름입니다. 이름은 변경 가능하다고 가정했습니다.
  string name_;

  // 방에 입장가능한 최소 레벨입니다. 최소 레벨은 변경 가능하다고 가정했습니다.
  int64_t joinable_min_level_;

  Timer::Id match_timer_id_;

  // 방장 이름입니다.
  string master_user_name_;

  typedef boost::unordered_map<
      SessionId, Ptr<User>, boost::hash<Uuid>> SessionMap;
  SessionMap sessions_;

  static boost::mutex the_mutex;
  static RoomMap the_rooms;
};


DEFINE_CLASS_PTR(Room);
boost::mutex Room::the_mutex;
Room::RoomMap Room::the_rooms;

This is what is needed to implement functions:

  • class Room id() member function (units to synchronize)

  • class Room HandleJoin(), HandleLeave(), HandleChat(), HandleStartMatch() member function (executing events)

There are two items. To synchronize without a lock, the method calling the Handle… function is limited as handled below.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
using funapi;

...

public class Room
{
  public static Dictionary<System.Guid, Room> TheRooms =
      new Dictionary<System.Guid, Room>();

  struct User
  {
    public User(Session session, string name, ulong character_id, ulong level)
    {
      Session = session;
      Name = name;
      CharacterId = character_id;
      Level = level;
    }

    public Session Session
    {
      get;set;
    }

    public string Name
    {
      get;set;
    }

    public ulong CharacterId
    {
      get;set;
    }

    public ulong Level
    {
      get;set;
    }
  }

  void SendMessage(Session session, string msg_type, bool result)
  {
    JObject message = new JObject ();
    message ["result"] = result;
    session.SendMessage (msg_type, message);
    return;
  }

  public static Room Create(
      string name, ulong joinable_min_level, string master_user_name)
  {
    System.Guid id = RandomGenerator.GenerateUuid ();

    Room room = new Room (id, name, joinable_min_level, master_user_name);

    lock (TheRooms)
    {
      TheRooms.Add (id, room);
    }

    return room;
  }

  public static bool Find(System.Guid id, out Room room)
  {
    lock (TheRooms)
    {
      return TheRooms.TryGetValue (id, out room);
    }
  }

  static JObject GetRooms()
  {
    JObject rooms = new JObject ();

    lock (TheRooms)
    {
      foreach (var room in TheRooms)
      {
        string room_id = room.Key.ToString ();
        rooms [room_id] = room.Value.GetInfo ();
      }
    }

    return rooms;
  }

  public void HandleJoin(Session session, string name,
                         ulong character_id, ulong level)
  {
    // 유저가 Room 에 입장할 수 있는지 조건을 검사합니다.

    // 이미 입장했었는지 확인합니다.
    if (Sessions.ContainsKey (session.Id))
    {
      SendMessage (session, "client_join", false);
      return;
    }

    // 이름이나 캐릭터 id 가 정상인지 확인합니다.
    if (name == String.Empty || character_id < 0) {
      SendMessage (session, "client_join", false);
      return;
    }

    // 입장 가능한 최소 레벨을 만족하는지 검사합니다.
    if (JoinableMinLevel < level) {
      SendMessage (session, "client_join", false);
      return;
    }

    // 모든 조건에 만족하였습니다. 이제 Room 에 입장시키도록 하겠습니다.
    User user = new User (session, name, character_id, level);
    Sessions.Add (session.Id, user);

    // 성공적으로 입장했습니다.
    // session context 에 room id 를 저장하여
    // 메시지 핸들러에서 클라이언트가 room id 를 보내지 않아도
    // session context 로부터 room id 를 가져와 사용하도록 하겠습니다.
    session.AddToContext ("room_id", Id.ToString ());

    foreach (var it in Sessions)
    {
      if (it.Value.Session.IsTransportAttached ())
      {
        it.Value.Session.SendMessage ("server_user_join", message, funapi.Session.Encryption.kDefault, funapi.Session.Transport.kTcp);
      }
    }
  }

  public void HandleLeave(Session session)
  {
    string user_name;

    User user;
    if (!Sessions.TryGetValue (session.Id, out user))
    {
      SendMessage (session, "client_leave", false);
      return;
    }

    user_name = user.Name;

    // Room 에서 유저를 삭제합니다.
    Sessions.Remove (session.Id);

    // 모든 유저가 Room 에서 나갔으므로 방을 빈방으로 처리하겠습니다.
    // 여기서는 간단하게 name 만 지우면 빈방이 되겠다고 하겠습니다.
    if (Sessions.Count == 0)
    {
      Name = "";
    }

    SendMessage (session, "client_leave", true);

    // 떠나는 유저를 제외한 Room 내 유저들에게 메시지를 전송합니다.
    JObject message = new JObject ();
    message ["name"] = user_name;
    foreach (var it in Sessions)
    {
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (it.Value.Session.IsTransportAttached ())
      {
        it.Value.Session.SendMessage ("server_user_leave", message);
      }
    }
  }

  // Room 내 유저들과 채팅하는 것을 처리합니다.
  public void HandleChat(Session session, string msg)
  {
    string user_name;

    User user;

    // Room 에 존재하지 않는 session(유저) 입니다.
    if (!Sessions.TryGetValue (session.Id, out user))
    {
      SendMessage (session, "client_chat", false);
      return;
    }

    user_name = user.Name;

    // 유저 이름이 비어 있으면 안됩니다.
    Log.Assert (user_name != String.Empty);

    // Room 내 유저들에게 채팅 메시지를 전송합니다.
    JObject message = new JObject ();
    message ["name"] = user_name;
    message ["msg"] = msg;
    foreach (var it in Sessions)
    {
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (it.Value.Session.IsTransportAttached ()) {
        it.Value.Session.SendMessage ("server_user_chat", message);
      }
    }
  }


  // Room 에 모인 유저들과의 대전을 시작합니다.
  // 방장이 Start 버튼을 누르면 이 함수가 호출된다고 가정하겠습니다.
  public void HandleStartMatch(Session session, string user_name)
  {
    // 방장이 아니면 이상한 클라이언트라고 가정하겠습니다.
    if (user_name != MasterUserName) {
      session.Close();
      return;
    }

    JObject message = new JObject();

    // 대전을 진행할 맵을 선택합니다. 여기서는 간단하게
    // 100 ~ 200 사이의 값을 랜덤하게 선택하겠습니다.
    ulong map_id = (ulong) RandomGenerator.GenerateNumber(100, 200);

    // 대전 시간을 입력합니다. 여기서는 180 초라고 가정하겠습니다.
    message ["match_time"] = 180;
    message ["match_map_id"] = map_id;

    // 대전 정보를 전송합니다.
    foreach (var it in Sessions)
    {
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (it.Value.Session.IsTransportAttached ()) {
        it.Value.Session.SendMessage ("server_start_match", message);
      }
    }

    // 여기서는 단순히 180 초가 지나면 대전이 끝난다고 가정하겠습니다.
    // 만약 중간에라도 대전이 끝날 수 있다면 match_timer_id_ 를
    // Timer::Cancel(match_timer_id) 함수를 호출하여
    // 아래에서 등록하는 timer 를 취소시킬 수 있습니다.

    // 180 초 이후에 대전을 종료시키는 timer 를 등록합니다.
    MatchTimerId =
        Timer.ExpireAfter(
            WallClock.FromMsec(180 * 1000), EndMatch);
  }


  // 대전을 종료합니다.
  public void EndMatch(UInt64 tid, DateTime value)
  {
    // 승자와 패자 정보를 전송합니다.
    JObject message = new JObject();

    // ...

    foreach (var it in Sessions)
    {
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (it.Value.Session.IsTransportAttached ()) {
        it.Value.Session.SendMessage ("server_end_match", message);
      }
    }
  }

  public ulong JoinableMinLevel
  {
    get;set;
  }

  public string MasterUserName
  {
    get;set;
  }

  public string Name
  {
    get;set;
  }

  public System.Guid Id
  {
    get;
  }

  Room(System.Guid id, string name,
       ulong joinable_min_level, string master_user_name)
  {
    Id = id;
    Name = name;
    JoinableMinLevel = joinable_min_level;
    MasterUserName = master_user_name;
  }

  JObject GetInfo()
  {
    JObject info = new JObject ();
    info ["name"] = Name;
    info ["user_count"] = Sessions.Count;
    info ["joinable_min_level"] = JoinableMinLevel;
    info ["master_user_name"] = MasterUserName;
    info ["id"] = Id.ToString ();
    return info;
  }

  Dictionary<System.Guid, User> Sessions =
      new Dictionary<System.Guid, User>();

  UInt64 MatchTimerId = 0;
}

51.2.2. Serializing events

Synchronize events to be handled in the room in each message handler.

src/event_handlers.cc

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
void OnSessionOpened(const Ptr<Session> &session) {
}


// session 이 닫히면 호출됩니다.
// 이 예제에서는 session 이 닫힐 때만 Room 에서 나가도록 처리하겠습니다.
// tcp 연결이 끊겼을 때 호출되는 OnTcpTransportDetached() 함수에서는
// 방에서 나가는 처리를 하지 않겠습니다.
void OnSessionClosed(const Ptr<Session> &session, SessionCloseReason reason) {
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager::FindLocalAccount(session);
  if (user_name.empty()) {
    return;
  }

  string room_id_str;
  // session context 에서 Room id 를 가져옵니다.
  if (not session->GetFromContext("room_id", &room_id_str)) {
    // 방에 접속하지 않은 세션입니다.
    return;
  }

  Uuid room_id = boost::lexical_cast<Uuid>(room_id_str);
  Ptr<Room> room = Room::Find(room_id);

  // 반드시 Room 이 존재한다고 가정했습니다.
  BOOST_ASSERT(room);

  // 유저를 로그아웃 처리합니다.
  bool is_user_logged_out = AccountManager::SetLoggedOut(user_name);

  // 반드시 로그아웃 처리된다고 가정하겠습니다.
  BOOST_ASSERT(is_user_logged_out);

  // Room id 로 이벤트를 직렬화합니다.
  Event::Invoke([room, session]() {
        room->HandleLeave(session);
      }, room_id);
}


// tcp 연결이 끊어지면 이 함수가 호출됩니다.
// session 이 닫히는 것과는 다릅니다.
// tcp 연결이 끊겼어도 session 은 session timeout 이 지나지 않으면
// 닫히지 않습니다.
void OnTcpTransportDetached(const Ptr<Session> &session) {
  // 여기서는 tcp 연결이 끊겨도 Room 에서 나가지 않도록 처리하겠습니다.
  // session timeout 으로 session 이 닫히기 전에
  // 재연결하게 될 경우 다시 게임을 진행할 수도 있기 때문입니다.
}


// 클라이언트 로그인을 처리합니다.
void OnClientLogin(const Ptr<Session> &session, const Json &message) {
  // 유저 이름이 메시지에 포함되어 있는지 확인합니다.
  if (not message.HasAttribute("user_name", Json::kString)) {
    SendMessage(session, "client_login", false);
    return;
  }

  // 유저 이름을 가져옵니다.
  string user_name = message["user_name"].GetString();

  // Object subsystem 을 이용하여
  // user_name 에 해당하는 object 를 가져올 수 있습니다.
  Ptr<User> user = User::FetchByName(user_name);

  // 만약 유저가 존재하지 않으면 생성하겠습니다.
  if (not user) {
    user = User::Create(user_name);

    // 이미 user_name 에 해당하는 User 가 생성되었다면 nullptr 를 반환합니다.
    if (not user) {
      SendMessage(session, "client_login", false);
      return;
    }

    // 기본 레벨 1로 설정
    user->SetLevel(1);

    // 기본 캐릭터 아이디 1로 설정
    user->SetCharacterId(1);
  }

  // user 가 반드시 존재한다고 가정했습니다.
  BOOST_ASSERT(user);

  // level 과 character id 를 가져옵니다.
  int64_t user_level = user->GetLevel();
  int64_t character_id = user->GetCharacterId();

  // 필요하다면 다른 정보도 가져옵니다.
  // ...

  // 이제 AccountManager 를 이용하여 로그인 처리를 합니다.
  // session 을 user_name 으로 mapping 할 수 있습니다.
  // 이미 로그인 처리되어 있다면 false 가 리턴됩니다.
  if (not AccountManager::CheckAndSetLoggedIn(user_name, session)) {
    SendMessage(session, "client_login", false);
    return;
  }

  // 유저에게 자신의 정보를 전달합니다.
  Json response;
  response["result"] = true;
  response["user_level"] = user_level;
  response["character_id"] = character_id;
  session->SendMessage("client_login", response);
}


void OnClientLogout(const Ptr<Session> &session, const Json &message) {
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager::FindLocalAccount(session);
  if (user_name.empty()) {
    SendMessage(session, "client_logout", false);
    return;
  }

  // Room 에 입장할 때 session context 에 저장한 room id 를 가져옵니다.
  string room_id_str;
  if (not session->GetFromContext("room_id", &room_id_str)) {
    // 만약 Room 에 입장하지 않았다면 room_id 가 없을 수 있습니다.
    // 이 경우에는 단순히 로그아웃되었다고 처리합니다.
    SendMessage(session, "client_logout", true);
    return;
  }

  // 반드시 room id 가 저장되었다고 가정했습니다.
  BOOST_ASSERT(not room_id_str.empty());

  // 입장한 Room 에서 유저를 나가게 합니다.
  Uuid room_id = boost::lexical_cast<Uuid>(room_id_str);
  Ptr<Room> room = Room::Find(room_id);

  // 반드시 Room 이 존재한다고 가정했습니다.
  BOOST_ASSERT(room);

  // 유저를 로그아웃 처리합니다.
  bool is_user_logged_out = AccountManager::SetLoggedOut(user_name);

  // 반드시 로그아웃 처리된다고 가정하겠습니다.
  BOOST_ASSERT(is_user_logged_out);

  // Room id 로 이벤트를 직렬화합니다.
  Event::Invoke([room, session]() {
        room->HandleLeave(session);
      }, room_id);
}


void OnClientJoinRoom(const Ptr<Session> &session, const Json &message) {
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager::FindLocalAccount(session);
  if (user_name.empty()) {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage(session, "client_join_room", false);
    return;
  }

  Ptr<User> user = User::FetchByName(user_name);
  // user object 가 존재한다고 가정하겠습니다.
  BOOST_ASSERT(user);

  // Room 입장 시 필요한 유저 정보를 가져옵니다.
  int64_t character_id = user->GetCharacterId();
  int64_t user_level = user->GetLevel();

  // 만약 Room 을 새로 만드는 것이라면 room_id 는 메시지에 없습니다.
  Ptr<Room> room;
  if (not message.HasAttribute("room_id", Json::kString)) {
    // 이 경우 Room 을 만들어야 하므로 room_name, joinable_min_level 이
    // 메시지에 있어야 합니다.
    if (not message.HasAttribute("room_name", Json::kString) ||
        not message.HasAttribute("joinable_min_level", Json::kInteger)) {
      // 필요한 값이 메시지에 없습니다.
      // 이상한 클라이언트라 판단하고 세션을 닫겠습니다.
      session->Close();
      return;
    }

    // Room 생성에 필요한 값을 가져오고 Room 을 생성합니다.
    const string room_name = message["room_name"].GetString();
    const int64_t joinable_min_level = message["joinable_min_level"].GetInteger();
    room = Room::Create(room_name, joinable_min_level, user_name);
    BOOST_ASSERT(room);
  } else {
    // 기존에 존재하는 Room 에 입장하는 것이면 room_id 는 메시지에 있어야 합니다.
    string room_id_str = message["room_id"].GetString();

    // room_id 값이 비어있지 않다고 가정했습니다.
    BOOST_ASSERT(not room_id_str.empty());

    Uuid room_id = boost::lexical_cast<Uuid>(room_id_str);
    room = Room::Find(room_id);

    // Room 은 반드시 존재해야 합니다.
    BOOST_ASSERT(room);
  }

  // Room 을 생성하거나 찾았습니다. 이제 Room 에 입장시키겠습니다.
  // Room id 로 이벤트를 직렬화합니다.
  Event::Invoke([room, session, user_name, character_id, user_level]() {
        room->HandleJoin(session, user_name, character_id, user_level);
      }, room->id());
}


void OnClientLeaveRoom(const Ptr<Session> &session, const Json &message) {
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager::FindLocalAccount(session);
  if (user_name.empty()) {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage(session, "client_leave_room", false);
    return;
  }

  // Room 에 입장할 때 session context 에 저장해둔 room id 를 가져오겠습니다.
  string room_id_str;
  if (not session->GetFromContext("room_id", &room_id_str)) {
    // 방에 입장하지 않은 세션입니다.
    // 이상한 클라이언트라고 가정하여 세션을 닫겠습니다.
    session->Close();
    return;
  }

  Uuid room_id = boost::lexical_cast<Uuid>(room_id_str);
  Ptr<Room> room = Room::Find(room_id);

  // 반드시 Room 이 존재한다고 가정하겠습니다.
  BOOST_ASSERT(room);

  // Room 에서 유저를 나가게 합니다.
  // Room id 로 이벤트를 직렬화합니다.
  Event::Invoke([room, session]() {
        room->HandleLeave(session);
      }, room_id);
}


void OnClientChat(const Ptr<Session> &session, const Json &message) {
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager::FindLocalAccount(session);
  if (user_name.empty()) {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage(session, "client_chat_room", false);
    return;
  }

  // chat msg 가 메시지에 없으면 이상한 클라이언트라고 가정하겠습니다.
  if (not message.HasAttribute("chat_msg", Json::kString)) {
    session->Close();
    return;
  }

  string chat_msg = message["chat_msg"].GetString();
  if (chat_msg.empty()) {
    // 비어있는 chat msg 도 이상한 클라이언트라고 가정하겠습니다.
    session->Close();
    return;
  }

  // Room 에 입장할 때 session context 에 저장해둔 room id 를 가져오겠습니다.
  string room_id_str;
  if (not session->GetFromContext("room_id", &room_id_str)) {
    // 방에 입장하지 않은 세션입니다.
    // 이상한 클라이언트라고 가정하여 세션을 닫겠습니다.
    session->Close();
    return;
  }

  Uuid room_id = boost::lexical_cast<Uuid>(room_id_str);
  Ptr<Room> room = Room::Find(room_id);

  // 반드시 Room 이 존재한다고 가정하겠습니다.
  BOOST_ASSERT(room);

  // Room 내 유저들에게 chat msg 를 전달합니다.
  // Room id 로 이벤트를 직렬화합니다.
  Event::Invoke([room, session, chat_msg]() {
        room->HandleChat(session, chat_msg);
      }, room_id);
}


// 대전을 시작합니다. 방장만이 이 메시지를 보낼 수 있습니다.
void OnClientStartMatch(const Ptr<Session> &session, const Json &message) {
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager::FindLocalAccount(session);
  if (user_name.empty()) {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage(session, "client_start_match", false);
    return;
  }

  // Room 에 입장할 때 session context 에 저장해둔 room id 를 가져오겠습니다.
  string room_id_str;
  if (not session->GetFromContext("room_id", &room_id_str)) {
    // 방에 입장하지 않은 세션입니다.
    // 이상한 클라이언트라고 가정하여 세션을 닫겠습니다.
    session->Close();
    return;
  }

  Uuid room_id = boost::lexical_cast<Uuid>(room_id_str);
  Ptr<Room> room = Room::Find(room_id);

  // 반드시 Room 이 존재한다고 가정하겠습니다.
  BOOST_ASSERT(room);

  // 대전 처리를 합니다.
  // Room id 로 이벤트를 직렬화합니다.
  Event::Invoke([room, session, user_name]() {
        // HandleStartMatch() 함수에서 방장 여부를 검사합니다.
        room->HandleStartMatch(session, user_name);
      }, room_id);
}


// 모든 Room 정보를 보냅니다.
void OnClientGetRoomList(const Ptr<Session> &session, const Json &message) {
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager::FindLocalAccount(session);
  if (user_name.empty()) {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage(session, "client_get_room_list", false);
    return;
  }

  // 모든 Room 들의 정보를 Json 으로 가져와서 메시지를 전송합니다.
  Json rooms = Room::GetRooms();
  session->SendMessage("client_get_room_list", rooms);
}


// 다음과 같이 RegisterEventHandlers() 함수에서 메시지 핸들러를 등록합니다.
void RegisterEventHandlers() {
  // OnSessionClosed() 함수가 호출되었을 때(즉 세션이 닫혔을 때)
  // Room 에서 나가도록 작업하였습니다.
  HandlerRegistry::Install2(OnSessionOpened, OnSessionClosed);

  HandlerRegistry::Register("client_login", OnClientLogin);
  HandlerRegistry::Register("client_logout", OnClientLogout);
  HandlerRegistry::Register("client_join_room", OnClientJoinRoom);
  HandlerRegistry::Register("client_leave_room", OnClientLeaveRoom);
  HandlerRegistry::Register("client_chat", OnClientChat);
  HandlerRegistry::Register("client_start_match", OnClientStartMatch);
  HandlerRegistry::Register("client_get_room_list", OnClientGetRoomList);

  // tcp 연결이 끊어지면 OnTcpTransportDetached 함수가 호출됩니다.
  // session 이 닫히는 것과는 다릅니다.
  // tcp 연결이 끊겼어도 session 은 session timeout 이 지나지 않으면
  // 닫히지 않습니다.
  // OnTcpTransportDetached() 함수가 호출되었을 땐 Room 에서 나가도록
  // 작업하지 않았습니다.
  HandlerRegistry::RegisterTcpTransportDetachedHandler(OnTcpTransportDetached);
}

Event::Invoke is invoked in the OnClient... message handler, but here C++ 11 lambda is passed as a callback. All events executed in the room are sent through Event::Invoke. Synchronization takes place as follows:

  • Units to synchronize: event groups such as room id are executed in the order Event::Invoke was invoked (later).

  • If all events to be executed in the room are executed through Event::Invoke, a lock is not necessary when changing variables within the room.

  • If room id differs (i.e. other rooms), all are executed concurrently.

  • Use C++11 lambda as a callback. Enter variables required for lambda capture syntax (e.g. room, session) to access later in the code inside Invoke . (You can use lambda capture syntax ([=]) if necessary.)

Note

If you used C++11 lambda here, you can use std::function or boost::function . However, if C++11 lambda can be used in the environment, it is better to have external variables referred to in code executed asynchronously. You can easily prevent bugs by designating whether to copy or refer inside the capture syntax.

For more details, see MSDN’s C++11 lambda page , See cppreference.com lambda page.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
public static void SendMessage(Session session, string msg_type, bool result)
{
  JObject message = new JObject ();
  message ["result"] = result;
  session.SendMessage (msg_type, message);
  return;
}

public static void OnSessionOpened(Session session)
{

}

// session 이 닫히면 호출됩니다.
// 이 예제에서는 session 이 닫힐 때만 Room 에서 나가도록 처리하겠습니다.
// tcp 연결이 끊겼을 때 호출되는 OnTcpTransportDetached() 함수에서는
// 방에서 나가는 처리를 하지 않겠습니다.
public static void OnSessionClosed(
    Session session, Session.CloseReason reason)
{
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager.FindLocalAccount (session);
  if (user_name == String.Empty)
  {
    return;
  }

  string room_id_str;

  // session context 에서 Room id 를 가져옵니다.
  if (!session.GetFromContext ("room_id", out room_id_str))
  {
    // 방에 접속하지 않은 세션입니다.
    return;
  }

  System.Guid room_id = new System.Guid (room_id_str);
  Room room;
  Room.Find (room_id, out room);

  // 반드시 Room 이 존재한다고 가정했습니다.
  Log.Assert (room != null);

  bool is_user_logged_out = AccountManager.SetLoggedOut (user_name);

  // 반드시 로그아웃 처리된다고 가정하겠습니다.
  Log.Assert (is_user_logged_out);

  // Room id 로 이벤트를 직렬화합니다.
  Event.Invoke (() => {
    room.HandleLeave(session);
  }, room_id);
}

// tcp 연결이 끊어지면 이 함수가 호출됩니다.
// session 이 닫히는 것과는 다릅니다.
// tcp 연결이 끊겼어도 session 은 session timeout 이 지나지 않으면
// 닫히지 않습니다.
public static void OnTcpTransportDetached(
    Session session)
{
  // 여기서는 tcp 연결이 끊겨도 Room 에서 나가지 않도록 처리하겠습니다.
  // session timeout 으로 session 이 닫히기 전에
  // 재연결하게 될 경우 다시 게임을 진행할 수도 있기 때문입니다.
}

public static void OnClientLogin(Session session, JObject message)
{
  if (message ["user_name"] == null)
  {
    SendMessage(session, "client_login", false);
    return;
  }

  string user_name = (string) message ["user_name"];
  User user = User.FetchByName (user_name);

  // 만약 유저가 존재하지 않으면 생성하겠습니다.
  if (user == null)
  {
    user = User.Create(user_name);

    // 이미 user_name 에 해당하는 User 가 생성되었다면 null 을 반환합니다.
    if (user == null)
    {
      SendMessage(session, "client_login", false);
      return;
    }

    // 기본 레벨 1로 설정
    user.SetLevel (1);

    // 기본 캐릭터 아이디 1로 설정
    user.SetCharacterId (1);

    // user 가 반드시 존재한다고 가정했습니다.
    Log.Assert (user != null);
  }

  // level 과 character id 를 가져옵니다.
  Int64 user_level = user.GetLevel ();
  Int64 character_id = user.GetCharacterId ();

  // 필요하다면 다른 정보도 가져옵니다.
  // ...

  // 이제 AccountManager 를 이용하여 로그인 처리를 합니다.
  // session 을 user_name 으로 mapping 할 수 있습니다.
  // 이미 로그인 처리되어 있다면 false 가 리턴됩니다.
  if (!AccountManager.CheckAndSetLoggedIn (user_name, session))
  {
    SendMessage (session, "client_login", false);
    return;
  }

  // 유저에게 자신의 정보를 전달합니다.
  JObject response = new JObject();
  response ["result"] = true;
  response ["user_level"] = user_level;
  response ["character_id"] = character_id;
  session.SendMessage ("client_login", response);
}

public static void OnClientLogout(Session session, JObject message)
{
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager.FindLocalAccount (session);
  if (user_name == String.Empty)
  {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage (session, "client_logout", false);
    return;
  }

  // Room 에 입장할 때 session context 에 저장한 room id 를 가져옵니다.
  string room_id_str;
  if (!session.GetFromContext ("room_id", out room_id_str))
  {
    // 만약 Room 에 입장하지 않았다면 room_id 가 없을 수 있습니다.
    // 이 경우에는 단순히 로그아웃되었다고 처리합니다.
    SendMessage (session, "client_logout", true);
    return;
  }

  Log.Assert (room_id_str != String.Empty);

  System.Guid room_id = new System.Guid (room_id_str);

  // 반드시 Room 이 존재한다고 가정했습니다.
  Room room = null;
  Log.Assert (Room.Find (room_id, out room));
  Log.Assert (room != null);

  // 유저를 로그아웃 처리합니다.
  bool is_user_logged_out = AccountManager.SetLoggedOut (user_name);

  // 반드시 로그아웃 처리된다고 가정하겠습니다.
  Log.Assert (is_user_logged_out);

  // Room id 로 이벤트를 직렬화합니다
  Event.Invoke (() => {
    room.HandleLeave (session);
  }, room_id);
}

public static void OnClientJoinRoom(Session session, JObject message)
{
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager.FindLocalAccount (session);
  if (user_name == String.Empty)
  {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage (session, "client_join_room", false);
    return;
  }

  User user = User.FetchByName(user_name);

  // user object 가 존재한다고 가정하겠습니다.
  Log.Assert (user != null);

  // Room 입장 시 필요한 유저 정보를 가져옵니다.
  ulong user_level = (ulong) user.GetLevel ();
  ulong character_id = (ulong) user.GetCharacterId ();

  // 만약 Room 을 새로 만드는 것이라면 room_id 는 메시지에 없습니다.
  Room room;
  if (message ["room_id"] == null)
  {
    // 이 경우 Room 을 만들어야 하므로 room_name, joinable_min_level 이
    // 메시지에 있어야 합니다.
    if (message ["room_name"] == null ||
        message ["joinable_min_level"] == null)
    {
      // 필요한 값이 메시지에 없습니다.
      // 이상한 클라이언트라 판단하고 세션을 닫겠습니다.
      session.Close();
      return;
    }

    // Room 생성에 필요한 값을 가져오고 Room 을 생성합니다.
    string room_name = (string) message ["room_name"];
    ulong joinable_min_level = (ulong) message ["joinable_min_level"];
    room = Room.Create(room_name, joinable_min_level, user_name);
    Log.Assert (room != null);
  }
  else
  {
    // 기존에 존재하는 Room 에 입장하는 것이면 room_id 는 메시지에 있어야 합니다.
    string room_id_str = (string) message ["room_id"];

    // room_id 값이 비어있지 않다고 가정했습니다.
    Log.Assert(room_id_str != String.Empty);

    System.Guid room_id = new System.Guid (room_id_str);

    room = null;

    // Room 은 반드시 존재해야 합니다.
    Log.Assert (Room.Find (room_id, out room));
    Log.Assert (room != null);
  }

   // Room 을 생성하거나 찾았습니다. 이제 Room 에 입장시키겠습니다.
   // Room id 로 이벤트를 직렬화합니다.
  Event.Invoke (() => {
      room.HandleJoin (session, user_name, character_id, user_level);
  }, room.Id);
}

public static void OnClientLeaveRoom(Session session, JObject message)
{
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager.FindLocalAccount (session);
  if (user_name == String.Empty)
  {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage (session, "client_leave_room", false);
    return;
  }

  // Room 에 입장할 때 session context 에 저장해둔 room id 를 가져오겠습니다.
  string room_id_str;
  if (!session.GetFromContext ("room_id", out room_id_str))
  {
    // 방에 입장하지 않은 세션입니다.
    // 이상한 클라이언트라고 가정하여 세션을 닫겠습니다.
    session.Close();
    return;
  }

  // room_id 값이 비어있지 않다고 가정했습니다.
  Log.Assert(room_id_str != String.Empty);

  System.Guid room_id = new System.Guid (room_id_str);
  Room room = null;

  // Room 은 반드시 존재해야 합니다.
  Log.Assert (Room.Find (room_id, out room));
  Log.Assert (room != null);

  // Room 에서 유저를 나가게 합니다.
  // Room id 로 이벤트를 직렬화합니다.
  Event.Invoke (() => {
    room.HandleLeave(session);
  }, room.Id);
}

public static void OnClientChat(Session session, JObject message)
{
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager.FindLocalAccount (session);
  if (user_name == String.Empty)
  {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage (session, "client_leave_room", false);
    return;
  }

  // chat msg 가 메시지에 없으면 이상한 클라이언트라고 가정하겠습니다.
  if (message ["chat_msg"] == null)
  {
    session.Close();
    return;
  }

  string chat_msg = (string) message ["chat_msg"];
  if (chat_msg == String.Empty)
  {
    // 비어있는 chat msg 도 이상한 클라이언트라고 가정하겠습니다.
    session.Close ();
    return;
  }

  // Room 에 입장할 때 session context 에 저장해둔 room id 를 가져오겠습니다.
  string room_id_str;
  if (!session.GetFromContext ("room_id", out room_id_str))
  {
    // 방에 입장하지 않은 세션입니다.
    // 이상한 클라이언트라고 가정하여 세션을 닫겠습니다.
    session.Close ();
    return;
  }

  Log.Assert (room_id_str != String.Empty);

  System.Guid room_id = new System.Guid (room_id_str);
  Room room = null;

  // Room 은 반드시 존재해야 합니다.
  Log.Assert (Room.Find (room_id, out room));
  Log.Assert (room != null);

  // Room 내 유저들에게 chat msg 를 전달합니다.
  // Room id 로 이벤트를 직렬화합니다.
  Event.Invoke (() => {
        room.HandleChat (session, chat_msg);
  }, room_id);
}

// 대전을 시작합니다. 방장만이 이 메시지를 보낼 수 있습니다.
public static void OnClientStartMatch(Session session, JObject message)
{
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager.FindLocalAccount(session);
  if (user_name == String.Empty) {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage(session, "client_start_match", false);
    return;
  }

  // Room 에 입장할 때 session context 에 저장해둔 room id 를 가져오겠습니다.
  string room_id_str;
  if (!session.GetFromContext("room_id", out room_id_str)) {
    // 방에 입장하지 않은 세션입니다.
    // 이상한 클라이언트라고 가정하여 세션을 닫겠습니다.
    session.Close();
    return;
  }

  System.Guid room_id = new System.Guid(room_id_str);

  Room room = null;

  // 반드시 Room 이 존재한다고 가정하겠습니다.
  Log.Assert (Room.Find(room_id, out room));
  Log.Assert (room != null);

  // 대전 처리를 합니다.
  // Room id 로 이벤트를 직렬화합니다.
  Event.Invoke (() => {
    // HandleStartMatch() 함수에서 방장 여부를 검사합니다.
    room.HandleStartMatch(session, user_name);
  }, room_id);
}


// 모든 Room 정보를 보냅니다.
public static void OnClientGetRoomList(Session session, JObject message)
{
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager.FindLocalAccount(session);
  if (user_name == String.Empty) {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage(session, "client_start_match", false);
    return;
  }

  // 모든 Room 들의 정보를 Json 으로 가져와서 메시지를 전송합니다.
  JObject rooms = Room.GetRooms();
  session.SendMessage("client_get_room_list", rooms);
}

public static void Install(ArgumentMap arguments)
{
  ...

  NetworkHandlerRegistry.RegisterMessageHandler ("client_login",
      new NetworkHandlerRegistry.JsonMessageHandler (OnClientLogin));

  NetworkHandlerRegistry.RegisterMessageHandler ("client_logout",
      new NetworkHandlerRegistry.JsonMessageHandler (OnClientLogout));

  NetworkHandlerRegistry.RegisterMessageHandler ("client_chat_room",
      new NetworkHandlerRegistry.JsonMessageHandler (OnClientChat));

  NetworkHandlerRegistry.RegisterMessageHandler ("client_join_room",
      new NetworkHandlerRegistry.JsonMessageHandler (OnClientJoinRoom));

  NetworkHandlerRegistry.RegisterMessageHandler ("client_leave_room",
      new NetworkHandlerRegistry.JsonMessageHandler (OnClientLeaveRoom));

  NetworkHandlerRegistry.RegisterMessageHandler ("client_start_match",
      new NetworkHandlerRegistry.JsonMessageHandler (OnClientStartMatch));

  NetworkHandlerRegistry.RegisterMessageHandler ("client_get_room_list",
      new NetworkHandlerRegistry.JsonMessageHandler (OnClientGetRoomList));

Warning

Here, we did not deal with leaving the room when ending a TCP connection. Depending on the situation, battles may be maintained when leaving a session and then reconnecting even if TCP was disconnected. (Handled so that when a session is closed, the room is only left in the invoked OnSessionClosed() function.)

If, when the TCP connection is also closed, room departure must be handled or AI must replace the user in battle, the HandlerRegistry::RegisterTcpTransportDetachedHandler() function can be added to handle TCP disconnection.