diff --git a/games/Pong/Classes/GGSNetwork.m b/games/Pong/Classes/GGSNetwork.m index a436f13..ecc00d0 100644 --- a/games/Pong/Classes/GGSNetwork.m +++ b/games/Pong/Classes/GGSNetwork.m @@ -11,7 +11,7 @@ @implementation GGSNetwork -#define GGS_HOST @"jeena.net" +#define GGS_HOST @"localhost" #define GGS_PORT 9000 #define NO_TIMEOUT -1 diff --git a/src/ggs_dispatcher.erl b/src/ggs_dispatcher.erl index 11dd729..fdd28c6 100644 --- a/src/ggs_dispatcher.erl +++ b/src/ggs_dispatcher.erl @@ -58,7 +58,8 @@ handle_info({tcp_closed, Socket}, State) -> %% and when it does, we accept the connection. handle_info(timeout, LSock) -> {ok, Sock} = gen_tcp:accept(LSock), - spawn(ggs_player, start_link, [Sock]), + ggs_player:start(Sock), + erlang:display("handle_info"), {noreply, LSock, 0}. terminate(normal, _State) -> diff --git a/src/ggs_player.erl b/src/ggs_player.erl index d6e939e..e80aeac 100644 --- a/src/ggs_player.erl +++ b/src/ggs_player.erl @@ -1,48 +1,66 @@ --module(ggs_player). --export([start_link/1, notify/3, get_token/1, stop/2]). --record(pl_state, - {token, % Player's token - socket, % Player's socket - table}). % Player's table - %% @doc This module handles communication between a player and GGS. This module is -%%responsible for: -%% * The storage of the player socket, player token and a table token. -%% * Ability to fetch a player token. -%% * Forwarding messages from players to the game -%% * Remove a player from GGS +%% responsible for: +%% * The storage of the player socket, player token and a table token. +%% * Ability to fetch a player token. +%% * Forwarding messages from players to the game +%% * Remove a player from GGS + +-module(ggs_player). +-behaviour(gen_server). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-export([start/1, notify/3, notify_game/2, get_token/1, stop/1]). + +-vsn(1.0). + +-record(state, { + token, + socket, + table, + protocol }). %% @doc Spawns a process representing the player in GGS. Takes the player socket as %% an argument for storage and later usage. Creates a unique player token %% identifying the player. %% @spec start_link(Socket::socket()) -> {ok, Pid} | {error, Reason} -start_link(Socket) -> - % The socket is in 'active' mode, and that means we are pushed any data - % that arrives on it, we do not need to recv() manually. Since the socket - % was opened in our parent process, we need to change the owner of it to - % us, otherwise these messages end up in our parent. - erlang:port_connect(Socket, self()), +start(Socket) -> + erlang:display("start_link"), + gen_server:start(?MODULE, [Socket], []). + +init([Socket]) -> + {ok, Protocol} = ggs_protocol:start_link(Socket, self()), {ok, Token} = ggs_coordinator:join_lobby(), - TableStatus = ggs_coordinator:join_table("1337"), - case TableStatus of - {ok, Table} -> - notify(self(), self(), {"hello", Token}), - loop(#pl_state{socket = Socket, token = Token, table = Table}); + + case ggs_coordinator:join_table("1337") of + {ok, T} -> + Table = T; {error, no_such_table} -> ggs_coordinator:create_table({force, "1337"}), - {ok, Table} = ggs_coordinator:join_table("1337"), - notify(self(), self(), {"hello", Token}), - loop(#pl_state{socket = Socket, token = Token, table = Table}) - end. + {ok, T} = ggs_coordinator:join_table("1337"), + Table = T + end, + + State = #state{ + token = Token, + socket = Socket, + table = Table, + protocol = Protocol + }, + erlang:display(State), + {ok, State}. %% @doc Handles incoming messages from the GGS and forwards them through the player %% socket to the player. %% @spec notify(Player::Pid(), From::Pid(), %% {Command::String(), Message::string()}) -> ok -notify(Player, From, Message) -> - {Cmd, Data} = Message, - Parsed = ggs_protocol:create_message(Cmd, "text","text", Data), - Player ! {notify, From, Parsed}. +notify(Player, _From, Message) -> + gen_server:cast(Player, {notify, Message}). + +%% @doc Handles incomming messages form a client and forwards them +%% through to the game_vm +notify_game(Player, Message) -> + gen_server:cast(Player, Message). %% @doc Get the player token uniquely representing the player. %% @spec get_token() -> string() @@ -53,32 +71,33 @@ get_token(_Player) -> %% together with the table token. It should also close the player socket and the %% process should return in the end. %% @spec stop(Table::pid()) -> Reason::string() -stop(_Player,_Table) -> - ggs_logger:not_implemented(). +stop(Player) -> + gen_server:cast(Player, stop). %% Internals -loop(#pl_state{token = _Token, socket = Socket, table = Table} = State) -> - receive - {tcp, Socket, Data} -> % Just echo for now.. - Parsed = ggs_protocol:parse(Data), - self() ! Parsed, - loop(State); - {notify, _From, Message} -> - gen_tcp:send(Socket, Message), - loop(State); - % Below are messages generated by the parser - {game_cmd,Cmd, _Headers, Data} -> - ggs_table:notify(Table, self(), {game, Cmd, Data}), - loop(State); - {srv_cmd,"define", _Headers, Data} -> - ggs_table:notify(Table, self(), {server, define, Data}), - loop(State); - {tcp_closed, _Socket} -> - io:format("Client disconnected, but THIS IS NOT SUPPORTED YET!~n"), - loop(State); - Other -> - io:format("Got UNKNOWN message: "), - erlang:display(Other), - io:format("~n") - end. +handle_call({notify, Message}, _From, #state { protocol = Protocol } = State) -> + ggs_protocol:send_command(Protocol, Message), + {noreplay, State}; + +handle_call({game_cmd, Command, _Headers, Data}, _From, #state { table = Table } = State) -> + ggs_table:notify(Table, self(), {game, Command, Data}), + {noreplay, State}; + +handle_call({srv_cmd, "define", _Headers, Data}, _From, #state { table = Table } = State) -> + ggs_table:notify(Table, self(), {server, define, Data}), + {noreplay, State}; + +handle_call(_Request, _From, St) -> {stop, unimplemented, St}. +handle_cast(_Request, St) -> {stop, unimplemented, St}. + +handle_info(_Info, St) -> {stop, unimplemented, St}. + +terminate(_Reason, State) -> + ggs_protocol:stop(State#state.protocol), + ggs_table:remove_player(State#state.table, self()), + % ggs_coordinator:remove_player(self(), self()), % not implemented yet + % TODO: release Socket + ok. + +code_change(_OldVsn, St, _Extra) -> {ok, St}. diff --git a/src/ggs_protocol.erl b/src/ggs_protocol.erl index 11cb2d0..f561ff7 100644 --- a/src/ggs_protocol.erl +++ b/src/ggs_protocol.erl @@ -1,11 +1,111 @@ +%%% @doc This module handles TCP incomming and outcommint. + -module(ggs_protocol). --export([parse/1, getToken/1, create_message/4]). +-export([start_link/2,stop/1]). +-behaviour(gen_server). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). +%% Old +-export([parse/1, getToken/1, create_message/4, send_command/2]). + +-vsn(1.0). + +-record(state, { + player, + socket, + header_string, + header_list, + body, + content_length}). + +start_link(Socket, Player) -> + gen_server:start_link(?MODULE, {Socket, Player}, []). + +stop(Protocol) -> + gen_server:cast(Protocol, stop). + +send_command(Protocol, {Command, Data}) -> + gen_server:cast(Protocol, {send, Command, Data}). + +init({Socket, Player}) -> + erlang:port_connect(Socket, self()), + State = #state{ + socket = Socket, + player = Player, + header_list = [], + header_string = "", + body = "", + content_length = -1 + }, + {ok, State}. + +handle_cast({tcp, _Socket, Data}, State) -> + case State#state.content_length of + -1 -> % its a header + TmpHeader = State#state.header_string ++ Data, + case string:str(TmpHeader, "\n\n") of + 0 -> % still in header + {reply, ok, State # state {header_string = TmpHeader}}; + _ -> % we left the header + {Header, Body} = parse(TmpHeader), + {_, ContentLengthString} = lists:keyfind(content_len, 1, Header), % find Content-Length + {ContentLength, []} = string:to_integer(ContentLengthString), + {reply, ok, State#state{ + header_list = Header, + header_string = "", + body = Body, + content_length = ContentLength}} + end; + Length -> % its a body + LBody = string:len(State#state.body), + LData = string:len(Data), + NewLength = LBody + LData, + if + NewLength < Length -> % not enough data + Body = State#state.body ++ Data, + {reply, ok, State#state {body = Body}}; + NewLength > Length -> % too much data + EndOfMessagePos = LBody + LData - Length, + Body = State#state.body ++ string:substr(Data, 0, EndOfMessagePos), + NextHeader = string:substr(Data, EndOfMessagePos, LData), + Message = prettify(State#state.header_list, Body), + gen_player:notify_game(State#state.player, Message), + {reply, ok, State#state { + header_string = NextHeader, + header_list = [], + body = "", + content_length = -1}}; + NewLength == Length -> % end of message + Message = prettify(State#state.header_list, State#state.body ++ Data), + gen_player:notify_game(State#state.player, Message), + {reply, ok, State#state { + header_string = "", + header_list = [], + body = "", + content_length = -1}} + end + end; + +handle_cast({send, Command, Data}, State) -> + Message = create_message(Command, "text", "text", Data), + gen_tcp:send(State#state.socket, Message), + {noreply, State}; + +handle_cast(_Request, St) -> {stop, unimplemented, St}. +handle_call(_Request, _From, St) -> {stop, unimplemented, St}. + +handle_info(_Info, St) -> {stop, unimplemented, St}. + + +terminate(_Reason, _St) -> ok. +code_change(_OldVsn, St, _Extra) -> {ok, St}. + + %% API Functions parse(Data) -> - Parsed = do_parse(Data, []), - prettify(Parsed). - + do_parse(Data, []). + getToken(Parsed) -> case lists:keyfind(token, 1, Parsed) of {_, Value} -> @@ -13,6 +113,8 @@ getToken(Parsed) -> false -> false end. + + create_message(Cmd, Enc, Acc, Data) -> Length = integer_to_list(string:len(Data)), @@ -26,18 +128,15 @@ create_message(Cmd, Enc, Acc, Data) -> Msg. %% Internal helpers -do_parse(Data, ParsedMessage) -> +do_parse(Data, Headers) -> NewLinePos = string:chr(Data, $\n), Line = string:substr(Data, 1, NewLinePos-1), Tokens = re:split(Line, ": ", [{return, list}]), case handle(Tokens) of {Command, more} -> - do_parse(string:substr(Data, NewLinePos+1), ParsedMessage ++ [Command]); + do_parse(string:substr(Data, NewLinePos+1), Headers ++ [Command]); {separator, data_next} -> - {_, Value} = lists:keyfind(content_len, 1, ParsedMessage), - {ContentLength, []} = string:to_integer(Value), - {data, ArgumentData} = handle_data(string:substr(Data, NewLinePos+1), ContentLength), - {ParsedMessage, ArgumentData} + {Headers, Data} end. handle([[]]) -> @@ -53,11 +152,11 @@ handle(["Token", Param]) -> handle(["Content-Type", Param]) -> {{content_type, Param}, more}. -handle_data(Data, Length) -> - {data, string:substr(Data,1,Length)}. +%handle_data(Data, Length) -> +% {data, string:substr(Data,1,Length)}. -prettify({Args, Data}) -> +prettify(Args, Data) -> case lists:keyfind(srv_cmd, 1, Args) of {_, Value} -> {srv_cmd, Value, Args, Data}; diff --git a/tests/ggs_player_test.erl b/tests/ggs_player_test.erl index efe7530..1782842 100644 --- a/tests/ggs_player_test.erl +++ b/tests/ggs_player_test.erl @@ -4,7 +4,7 @@ %% @doc start_link should always return ok for any valid socket. A valid socket %% should always return {ok, Pid} and {error, Reason} otherwise. start_link_test() -> - ggs_logger:not_implemented(). + {ok, Player} = ggs_player:start_link(Sock). %% @doc Given that start_link returned {ok, Player}. Notify shall always return ok and %% deliver a specified message through the socket.