蟻地獄

Twitterに書ききれない長めの文とか書くよ

avatar.enchant.jsでアニメーションパターンを追加する方法

こんにちは!

avatar.enchant.jsって便利だよね!絵が描けなくてもこんなゲームがさくっと作れちゃいます。

今あるアニメーションパターンは、

  • stop (立ってる)
  • run (走ってる)
  • attack (攻撃)
  • special (なんか光る攻撃)
  • damage (痛そう)
  • dead (うずくまってる)

の6種類。

これだけでもゲームは作れるけど、どうせならいろんな攻撃パターン作ったり防御とかジャンプとかしたいよね!
実は、avatar.enchant.jsはアニメーションパターンを簡単に追加できるんです!

やりかた

Avatarクラスのオブジェクトは、内部にこんな感じの16パターンの画像を持っています。

(実際の画像はライセンスが微妙なのでイメージ画像)

で、こんな感じでanimPattermにフレーム番号の配列を設定することで、actionに指定して使うことができます。

var avatar = new Avatar('2:2:0:2031:21310:0');
avatar.animPattern['ride'] = [13,4];
avatar.animPattern['attack2'] = [6,5,10,11,10,5,6,6,-1];

avatar.action = 'ride';

サンプルゲーム

こんなん作ってみたよ!
9leap : Error - どこでも遊べる、投稿型ゲームサイト

Node.js + Socket.IO でアメピグっぽいアバターチャットを作った


こんにちは!

1年ほど前についったライフっていうアバターチャットをWebGL+WebSocketで作ったんですよ。本業のほうではMMOゲームサーバとか作ってる生粋のC++erなもんで、同じような感じで作ればいいやと思ってサーバはC++で書いてたんですね。でもほら、WebSocketってあれじゃないですか。仕様がぐりんぐりん変わったじゃないですか。使ってたC++のWebSocketライブラリが動かなくなっちゃったんですね。
そこでSocket.IOですよ。
C++ではprotobufとかnineとか使って頑張ってたんですけど、Socket.IOだとJavaScriptのオブジェクトそのまま送れるわ非同期コールバック簡単に書けるわハートビートやらコネクション管理やらもいい感じにやってくれるわで、チートレベルでお手軽にゲームサーバが書けちゃうわけですよ。C++なんていらんかったんや!
というわけで、Socket.IOのおかげでFirefoxでも動くようになったついったライフでみんな遊んでね!
ついったライフ | Twitterアカウントで遊べる3D仮想空間


え、サーバのソースが見たい?じゃあまるっと晒しちゃうよ!

//----------------------
// 定数定義
//----------------------
var LISTEN_PORT = 80;
var LOG_LEVEL = 1;

var DB_CON_STRING = 'tcp://user:pass@localhost:5432/twlife';

var ROOM_NAME_LENGTH_LIMIT = 24
var CHAT_LENGTH_LIMIT = 140;
var ROOM_OBJECT_NUM_LIMIT = 140;
var GET_ROOM_NUM_LIMIT = 100;
var ROOM_SAVE_INTERVAL_MSEC = 60000;
var SESSION_TIMEOUT_MSEC = 30000;

var CHARACTER = 'abcdefghijklmnopqrstuvwxyz0123456789'.split('');

var ResultCode = {
  OK: 0,
  NOT_LOGIN: 1,
  NOT_PERMITTED: 2,
  ROOM_NOT_FOUND: 3,
  ACTOR_NOT_FOUND: 4,
  API_COUNT_LIMIT: 5,
  DB_ERROR: 6
};

var ActorType = {
  AVATAR: 1,
  IMAGE: 2,
  USTREAM: 3
};

var EditType = {
  OWNER_ONLY: 1,
  ALL: 2
};

//--------------------
// 変数定義
//--------------------
var sessions = {};
var rooms = {};

var express = require('express')
  , app = express.createServer()
  , io = require('socket.io').listen(app)
  , pg = require('pg')
  , fs = require('fs')
  , Schema = require('protobuf').Schema
  , schema = new Schema(fs.readFileSync('TL.desc'))
  , RoomSaveData = schema['TL.RoomSaveData']
  , log4js = require('log4js');

var logger = log4js.getLogger();

//----------------------
// class Session
//----------------------
var Session = function(sessionId, userId, name, url, description){
  this.sessionId = sessionId;
  this.userId = userId;
  this.name = name;
  this.url = url;
  this.description = description;
  this.actorId = 0;
  this.socket = null;
  this.room = null;
  this.authTime = (new Date()).getTime();
};

//----------------------
// class ActorInfo
//----------------------
var ActorInfo = function(type, userId, name, url, description){
  this.actorId = 0;
  this.type = type;
  this.userId = userId;
  this.name = name;
  this.url = url;
  this.description = description;
  this.pos = {x:0, y:0, z:0};
  this.rot = {x:0, y:0, z:0};
};

//----------------------
// class Room
//----------------------
var Room = function(roomId, roomName, ownerId, ownerName, editType, isPrivate){
  this.roomId = roomId;
  this.roomName = roomName;
  this.ownerId = ownerId;
  this.ownerName = ownerName;
  this.editType = editType;
  this.isPrivate = isPrivate;
  this.actors = {};
  this.sessions = {};
  this.lastId = 0;
  this.saveTime = 0;
  this.saving = false;
  this.modify = false;
};

// ルーム情報更新
Room.prototype.SetRoomInfo = function(roomName, editType, isPrivate){
  this.roomName = roomName;
  this.editType = editType;
  this.isPrivate = isPrivate;

  var data = {
    room_id: this.roomId,
    room_name: this.roomName,
    owner_id: this.ownerId,
    owner_name: this.ownerName,
    edit_type: this.editType,
    is_private: this.isPrivate,
    avatar_num: 0
  };
  for(var i in this.sessions){
    this.sessions[i].socket.emit('RoomInfo', data);
  }

  // DBに保存
  var room = this;
  pg.connect(DB_CON_STRING, function(err, client){
    if(err){
      logger.error(err);
      return;
    }
    client.query(
      'UPDATE room SET room_name=$1,edit_type=$2,is_private=$3,update_at=now() WHERE room_id=$4',
      [room.roomName, room.editType, room.isPrivate, room.roomId], function(err){
      if(err){
        logger.error(err);
      }
    });
  });
}

// アクター追加
Room.prototype.AddActor = function(actor, session){
  ++this.lastId;
  actor.actorId = this.lastId;
  this.actors[actor.actorId] = actor;
  if(session){
    if(session.room){
      session.room.RemoveSession(session);
    }
    session.actorId = actor.actorId;
    session.room = this;
    this.sessions[session.sessionId] = session;
  }
  if(actor.type != ActorType.AVATAR){
    this.modify = true;
  }

  for(var i in this.sessions){
    if(this.sessions[i].actorId == actor.actorId) continue;
    this.sessions[i].socket.emit('ActorAdd', actor);
  }
}

// アクター削除
Room.prototype.RemoveActor = function(id){
  if(id in this.actors){
    if(this.actors[id].type != ActorType.AVATAR){
      this.modify = true;
    }
    delete this.actors[id];
  
    for(var i in this.sessions){
      this.sessions[i].socket.emit('ActorRemove', id);
    }
  }
}

// セッション削除
Room.prototype.RemoveSession = function(session){
  this.RemoveActor(session.actorId);
  delete this.sessions[session.sessionId];
  session.actorId = 0;
  session.room = null;
  
  if(this.IsEmpty()){
    this.SaveData();
  }
}

// アクター移動
Room.prototype.MoveActor = function(id, pos, rot, exceptId){
  if(id in this.actors){
    var actor = this.actors[id];
    if(actor.type != ActorType.AVATAR){
      this.modify = true;
    }
    
    actor.pos = pos;
    actor.rot = rot;

    for(var i in this.sessions){
      if(this.sessions[i].actorId == exceptId) continue;
      this.sessions[i].socket.emit('ActorMove', id, pos, rot);
    }
  }
}

// ルームにログインしている人がいなければtrue
Room.prototype.IsEmpty = function(){
  for(var i in this.sessions){
    return false;
  }
  return true;
}

// データ保存
Room.prototype.SaveData = function(){
  if(!this.modify || this.saving) return;

  logger.info('SaveData: @' + this.roomId);

  // DBに保存
  var room = this;
  pg.connect(DB_CON_STRING, function(err, client){
    if(err){
      logger.error(err);
      return;
    }
    // セーブデータ作成
    var saveData = {actor:[]};
    for(var i in room.actors){
      var actor = room.actors[i];
      if(actor.type == ActorType.AVATAR) continue;
      saveData.actor.push({
        type: actor.type,
        userId: actor.userId,
        name: actor.name,
        url: actor.url,
        description: actor.description,
        posX: actor.pos.x,
        posY: actor.pos.y,
        posZ: actor.pos.z,
        rotX: actor.rot.x,
        rotY: actor.rot.y,
        rotZ: actor.rot.z
      });
    }
    var data = new Buffer(RoomSaveData.serialize(saveData));
    client.query('UPDATE room SET data=$1,update_at=now() WHERE room_id=$2',
      [data.toString('base64'),room.roomId], function(err){
      if(err){
        logger.error(err);
      }
      room.saving = false;
    });
  });

  this.saving = true;
  this.modify = false;
}

//--------------------
// main
//--------------------

io.set('log level', LOG_LEVEL);
app.use(express.bodyParser());
app.listen(LISTEN_PORT);

// 認証
app.post('/auth', function(req, res){
  /* 実際はここで認証やってるけど省略 */
  if(req.body.sid in sessions){
    var session = sessions[req.body.sid];
    if(session.socket){
      session.socket.emit('ForceLogout');
    }
    if(session.room){
      session.room.RemoveSession(session);
    }
  }
  sessions[req.body.sid] = new Session(req.body.sid, req.body.uid, req.body.name, decodeURI(req.body.url), decodeURI(req.body.desc));
  logger.info('Auth: #' + req.body.sid + ' ' + req.body.name);
  res.send('');
});

// ゲームセッション
io.sockets.on('connection', function (socket) {
  var logger = log4js.getLogger();
  var session = null;

  // ログイン判定
  function CheckLogin(fn){
    if(!session || !session.room){
      socket.disconnect();
      return false;
    }
    return true;
  }

  // 切断
  socket.on('disconnect', function(){
    if(session && session.room){
      logger.info('Disconnect: #' + session.sessionId);
      session.authTime = (new Date()).getTime(); // ルーム切り替えの間にセッション削除されないように
      session.room.RemoveSession(session);
    }
  });

  // ログイン
  socket.on('Login', function(sessionId, roomId, fn){
    function Login(){
      var room = rooms[roomId];
      var actor = new ActorInfo(
        ActorType.AVATAR, 
        session.userId, 
        session.name, 
        session.url, 
        session.description);
      actor.pos.z = 10; // 上から落ちてくる演出
      room.AddActor(actor, session);
      fn(ResultCode.OK, actor, {
        room_id: room.roomId,
        room_name: room.roomName,
        owner_id: room.ownerId,
        owner_name: room.ownerName,
        edit_type: room.editType,
        is_private: room.isPrivate,
        avatar_num: 0
      });  
      logger = log4js.getLogger('[' + session.name + '@' + roomId + ']');
      logger.info('Login');

      var  data = [];
      for(var actorId in room.actors){
        if(actorId == session.actorId) continue;
        data.push(room.actors[actorId]);
      }
      socket.emit('ActorAddList', data);
    }

    if(sessionId in sessions){
      session = sessions[sessionId];
      if(session.socket){
        session.socket.emit('ForceLogout');
      }
      session.socket = socket;
      if(roomId in rooms){
        Login();
      }else{
        // DB検索
        pg.connect(DB_CON_STRING, function(err, client){
          if(err){
            logger.error(err);
            fn(ResultCode.DB_ERROR);
            return;
          }
          client.query(
            'SELECT room_id,room_name,owner_id,owner_name,edit_type,is_private,data FROM room WHERE room_id = $1',
            [roomId], function(err, result){
            if(err){
              logger.error(err);
              fn(ResultCode.DB_ERROR);
              return;
            }
            if(result.rows.length == 0){
              fn(ResultCode.ROOM_NOT_FOUND);
              return;
            }
            if(!(roomId in rooms)){
              // ルーム読み込み
              var row = result.rows[0];
              var room = new Room(
                row.room_id, 
                row.room_name, 
                row.owner_id,
                row.owner_name,
                row.edit_type,
                row.is_private);
              var buffer = new Buffer(row.data, 'base64');
              var data = RoomSaveData.parse(buffer);
              if(data.actor){
                for(var i = 0; i < data.actor.length; ++i){
                  var actor = data.actor[i];
                  var actorInfo = new ActorInfo(
                    actor.type,
                    actor.userId,
                    actor.name,
                    actor.url,
                    actor.description);
                  actorInfo.pos = {x:actor.posX, y:actor.posY, z:actor.posZ};
                  actorInfo.rot = {x:actor.rotX, y:actor.rotY, z:actor.rotZ};
                  room.AddActor(actorInfo);
                }
              }
              rooms[room.roomId] = room;
              logger.info('Load: @' + room.roomId);
            }
            Login();
          });
        });
      }
    }else{
      fn(ResultCode.NOT_LOGIN);
    }
  });

  // ログアウト
  socket.on('Logout', function(fn){
    if(!CheckLogin(fn)){
      return;
    }

    logger.info('Logout');
    delete sessions[session.sessionId];
    session.room.RemoveSession(session);
    session = null;
    fn(ResultCode.OK);
  });

  // アバター移動
  socket.on('ActorMove', function(actorId, pos, rot){
    if(!CheckLogin()){
      return;
    }

    if(actorId in session.room.actors){
      var actor = session.room.actors[actorId];
      if(actor.type == ActorType.AVATAR){
        if(actor.userId == session.userId){
          session.room.MoveActor(actorId, pos, rot, session.actorId);
        }
      }else{
        if(session.room.ownerId == session.userId || session.room.editType == EditType.ALL){
          session.room.MoveActor(actorId, pos, rot, session.actorId);
        }
      }
    }
  });

  // チャット
  socket.on('Chat', function(actorId, content){
    if(!CheckLogin()){
      return;
    }

    if(content.length <= CHAT_LENGTH_LIMIT){
      logger.info('Chat: ' + content);
      if(actorId in session.room.actors){
        var actor = session.room.actors[actorId];
        if(actor.userId == session.userId){
          for(var i in session.room.sessions){
            session.room.sessions[i].socket.emit('Chat', actorId, content);
          }
        }
      }
    }
  });

  // アクター追加
  socket.on('AddActor', function(type, url, description, pos, rot, fn){
    if(!CheckLogin(fn)){
      return;
    }

    if(type == ActorType.AVATAR){
      fn(ResultCode.NOT_PERMITTED);
    }else if(session.room.ownerId != session.userId && session.room.editType != EditType.ALL){
      fn(ResultCode.NOT_PERMITTED);
    }else{
      var count = 0;
      for(var i in session.room.actors){
        ++count;
      }
      if(count < ROOM_OBJECT_NUM_LIMIT){
        var actor = new ActorInfo(
          type, 
          session.userId, 
          session.name, 
          url, 
          description);
        actor.pos = pos;
        actor.rot = rot;
        session.room.AddActor(actor);
        logger.info("AddActor: '" + actor.actorId + ' ' + url);
        fn(ResultCode.OK);
      }else{
        fn(ResultCode.NOT_PERMITTED);
      }
    }
  });

  // アクター削除
  socket.on('RemoveActor', function(actorId, fn){
    if(!CheckLogin(fn)){
      return;
    }

    if(actorId in session.room.actors){
      var actor = session.room.actors[actorId];
      logger.info("RemoveActor: '" + actor.actorId);
      if(actor.type == ActorType.AVATAR){
        if(actor.userId == session.userId){
          session.room.RemoveActor(actorId);
          fn(ResultCode.OK);
        }else{
          fn(ResultCode.NOT_PERMITTED);
        }
      }else{
        if(session.room.ownerId == session.userId || session.room.editType == EditType.ALL){
          session.room.RemoveActor(actorId);
          fn(ResultCode.OK);
        }else{
          fn(ResultCode.NOT_PERMITTED);
        }
      }
    }else{
      fn(ResultCode.ACTOR_NOT_FOUND);
    }
  });    

  // ルーム追加
  socket.on('AddRoom', function(roomName, editType, isPrivate, fn){
    if(!CheckLogin(fn)){
      return;
    }
    if(roomName.length > ROOM_NAME_LENGTH_LIMIT){
      fn(ResultCode.NOT_PERMITTED);
      return;
    }

    // DBに保存
    pg.connect(DB_CON_STRING, function(err, client){
      if(err){
        logger.error(err);
        fn(ResultCode.DB_ERROR);
        return;
      }
      // ルームID生成
      var roomId = '';
      for(var i = 0; i < 12; ++i){
        roomId += CHARACTER[Math.floor(Math.random() * CHARACTER.length)];
      }
      // 空のルームデータ生成
      var data = new Buffer(RoomSaveData.serialize({actor:[]}));
      client.query(
        'INSERT INTO room (room_id,room_name,owner_id,owner_name,edit_type,is_private,data,update_at) VALUES ($1,$2,$3,$4,$5,$6,$7,now())',
        [roomId,roomName,session.userId,session.name,editType,isPrivate,data.toString('base64')], function(err){
        if(err){
          logger.error(err);
          fn(ResultCode.DB_ERROR);
          return;
        }
        rooms[roomId] = new Room(
          roomId, 
          roomName, 
          session.userId, 
          session.name, 
          editType, 
          isPrivate);
        logger.info('AddRoom: @' + roomId);
        fn(ResultCode.OK, roomId);
      });
    });
  });

  // ルーム一覧取得
  socket.on('GetRoomList', function(fn){
    if(!CheckLogin(fn)){
      return;
    }

    logger.info('GetRoomList');
    var data = [];
    for(var i in rooms){
      var room = rooms[i];
      if(room.isPrivate) continue;
      var avatarNum = 0;
      for(var sid in room.sessions){
        ++avatarNum;
      }
      data.push({
        room_id: room.roomId,
        room_name: room.roomName,
        owner_id: room.ownerId,
        owner_name: room.ownerName,
        edit_type: room.editType,
        is_private: room.isPrivate,
        avatar_num: avatarNum
      });
      if(data.length >= GET_ROOM_NUM_LIMIT) break;  
    }
    data.sort(function(a,b){return b.avatar_num - a.avatar_num});
    fn(ResultCode.OK, data);
  });

  // ユーザーIDでルーム検索
  socket.on('GetUserRoomList', function(ownerId, fn){
    if(!CheckLogin(fn)){
      return;
    }

    logger.info('GetUserRoomList');
    // DB検索
    pg.connect(DB_CON_STRING, function(err, client){
      if(err){
        logger.error(err);
        fn(ResultCode.DB_ERROR);
        return;
      }
      var query = 'SELECT room_id,room_name,owner_id,owner_name,edit_type,is_private FROM room WHERE owner_id = $1';
      if(ownerId != session.userId){
        query += ' AND is_private = FALSE';
      }
      query += ' ORDER BY update_at DESC LIMIT ' + GET_ROOM_NUM_LIMIT;
      client.query(query, [ownerId], function(err, result){
        if(err){
          logger.error(err);
          fn(ResultCode.DB_ERROR);
          return;
        }
        fn(ResultCode.OK, result.rows);
      });
    });
  });

  // ルーム情報更新
  socket.on('UpdateRoomInfo', function(roomName, editType, isPrivate, fn){
    if(!CheckLogin(fn)){
      return;
    }

    if(session.room.ownerId == session.userId && roomName.length <= ROOM_NAME_LENGTH_LIMIT){
      logger.info('UpdateRoomInfo');
      session.room.SetRoomInfo(roomName, editType, isPrivate);
      fn(ResultCode.OK);
    }else{
      fn(ResultCode.NOT_PERMITTED);
    }
  });
});

// 定期処理
setInterval(function(){
  var now = (new Date()).getTime();

  for(var i in sessions){
    var session = sessions[i];

    // セッションタイムアウト
    if(!session.room && now - session.authTime > SESSION_TIMEOUT_MSEC){
      logger.info('Timeout: #' + session.sessionId);
      delete sessions[i];
    }
  }

  for(var i in rooms){
    var room = rooms[i];

    // 自動セーブ
    if(now - room.saveTime > ROOM_SAVE_INTERVAL_MSEC){
      room.SaveData();
      room.saveTime = now;
    }

    // 誰もいないルームをアンロード
    if(room.IsEmpty() && !room.saving){
      logger.info('Unload: @' + room.roomId);
      delete rooms[i];
    }
  }
}, 1000);

WebGLで作った3DネトゲにjQueryで吹き出しチャットを付けてみた

ネトゲっていうかまだスタンドアロンなんだけど、WebGLで作った3D空間の任意の位置にDOM要素の吹き出し重ねたりできるかな?ってやってみたら結構簡単にできました。

動かし方

前回の記事と同様に、WebGLが動くブラウザを用意して下さい。
最新のFirefox開発版で動作確認済みです。Chromiumでも動くけど、最新版だとなんかオクルージョンが変だったり動作が怪しいのでFirefoxのほうがいいかも。
で、ブラウザの準備ができたら下記のURLにアクセス!チャットを入力するとねこさんが喋ります。
http://meengr.sakura.ne.jp/demo2/

解説

吹き出しはWebGLのテクスチャではなく、DOM要素を動的に追加してCanvasに絶対位置指定で重ねています。毎フレームねこさんの位置からウィンドウ座標を計算し、適切な位置に移動させています。詳しくはソースを見てね。
でもこれってパフォーマンス的にどうなんだろう?けっこう重い気がする・・・

WebGLでヌルヌル動く3Dネトゲを作ってみた

タイトルは釣りだけど、WebGLで3DネトゲっぽいUIを作ってみたよ!

動かし方

まだWebGLに正式対応したブラウザが無いから、開発版のブラウザをインストールする必要があります。めんどくさい!
詳しくはここらへんを見てね。
Getting a WebGL Implementation - WebGL Public Wiki

ここでは一番簡単に使えるChromiumのインストールのしかたを説明します。

  1. ダウンロードページ(http://build.chromium.org/buildbot/continuous/win/LATEST/)から最新版をダウンロード
  2. 以下のようにコマンドラインオプションを付けて起動

chrome.exe --no-sandbox --enable-webgl

コマンドラインオプションを付けないとWebGLが有効にならないので注意してください。
あと、OpenGL2.0に対応したグラフィックボードが必要だから、ノートPCとかじゃできないかもよ。

で、ブラウザの準備ができたら下記のURLにアクセス!
http://meengr.sakura.ne.jp/demo/

作り方

ソースを見てね☆
使ったライブラリは以下の通り。

GLGE WebGL Library/Framework

WebGLのラッパー。ライティングとかピッキングとかこれ一発でできて便利。

jQuery UI

チャットウィンドウとかをDraggableにするのに使用。
こういう既存のライブラリが使えるから、WebGLでゲーム作るとUIの実装がすごく簡単です。

http://www.kelvinluck.com/assets/jquery/jScrollPane/jScrollPane.html

デフォルトのスクロールバーだと見栄えが悪かったので変えるのに使用。

タイヤ買取ナンバーワン、ホイール買取

イコン画像を使わせていただきました。ありがとう!

Cometeoのサーバ構成を晒してみる

スケールアウトからスケールアップへの回帰:Kenn's Clairvoyance - CNET Japanに対抗して、
Lingrのクローンというかパクりの超高速無料レンタルチャットCometeoのサーバ構成を晒しちゃうよ!


サーバ: さくらインターネット 専用サーバ エントリープラン
CPU: Atom 1.6GHz
メモリ: 1GB
HDD: 160GB
回線: 10M共有
料金: 月額7,800円


これにMySQLフルスクラッチhttpd(C++で書いた!)が乗ってる。
DBのバックアップは毎晩AmazonS3に転送して直近7日分を保管。S3の月額は1ドル未満。

で、サービス状況はというと、


月間PV: 8万くらい
同時接続数: ピーク時で350〜400くらい
総チャットルーム数: 5500くらい
総発言数: 340万くらい
CPU: スカスカ
HDD: スカスカ
メモリ: 余裕
回線: 1ルームに200人とか来た日は辛いけど普段はスカスカ


で、ビジネス的にはどうかというと、


アフィリエイト収入: 月3,000円くらい

全然赤字だよ☆

GoでCometチャットサーバを書いてみた

最近流行りのGoでCometなチャットサーバを書いてみたよ!
いちおう動いてるっぽいけど正しいかは知らないよ!

http://www.cometeo.com:8080/

package main

import (
	"flag";
	"http";
	"log";
	"container/list";
	"strconv";
	"bytes";
	"json";
	"time";
)

type post struct {
	id uint64;
	name string;
	comment string;
	at *time.Time;
}

type chatData struct {
	lastId uint64;
	posts list.List;
	waits list.List;
}

type request interface {
	execute(data *chatData);
}

type addRequest struct {
	name string;
	comment string;
}

type getRequest struct {
	id uint64;
	resCh chan []post;
}

func (req addRequest) execute(data *chatData) {
	data.lastId++;
	p := post{data.lastId, req.name, req.comment, time.LocalTime()};
	data.posts.PushBack(p);
	if data.posts.Len() > 100 {
		data.posts.Remove(data.posts.Front());
	}

	for ch := range data.waits.Iter() {
		res := make([]post, 1);
		res[0] = p;
		ch.(chan []post) <- res;
	}
	data.waits.Init();
}

func (req getRequest) execute(data *chatData) {
	if req.id == data.lastId {
		data.waits.PushBack(req.resCh);
	}else{
		i := 0;
		e := data.posts.Front();
		for ; e != nil; i,e = i+1,e.Next() {
			if e.Value.(post).id > req.id {
				break;
			}
		}
		res := make([]post, data.posts.Len()-i);
		for i = 0; e != nil; i,e = i+1,e.Next() {
			res[i] = e.Value.(post);
		}
		req.resCh <- res;
	}
}

var requestCh = make(chan request)
var addr = flag.String("addr", ":8080", "http service address")

func backend() {
	var data chatData;
	for {
		req := <- requestCh;
		req.execute(&data);
	}
}

func add(c *http.Conn, req *http.Request) {
	requestCh <- addRequest{req.FormValue("name"), req.FormValue("c")};
	c.WriteHeader(http.StatusOK);
}

func get(c *http.Conn, req *http.Request){
	id,err := strconv.Atoui64(req.FormValue("id"));
	if err != nil {
		c.WriteHeader(http.StatusBadRequest);
	}else{
		q := getRequest{id, make(chan []post)};
		requestCh <- q;
		res := <- q.resCh;

		c.SetHeader("Content-Type", "application/json; charset=utf-8");
		var buf bytes.Buffer;
		buf.WriteString("{\"post\":[");
		last := len(res)-1;
		for i,val := range res {
			buf.WriteString("{\"id\":" + strconv.Uitoa64(val.id) 
				+ ",\"name\":" + json.Quote(val.name) 
				+ ",\"c\":" + json.Quote(val.comment) 
				+ ",\"at\":"+ json.Quote(val.at.String()) + "}");
			if(i != last){
				buf.WriteString(",");
			}
		}
		buf.WriteString("]}");
		c.Write(buf.Bytes());
	}
}

func main() {
	flag.Parse();

	http.Handle("/", http.FileServer("www", ""));
	http.Handle("/add", http.HandlerFunc(add));
	http.Handle("/get", http.HandlerFunc(get));

	go backend();

	err := http.ListenAndServe(*addr, nil);
	if err != nil {
		log.Exit("ListenAndServe:", err);
	}
}