diff options
Diffstat (limited to 'learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_serv.erl')
-rw-r--r-- | learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_serv.erl | 154 |
1 files changed, 154 insertions, 0 deletions
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_serv.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_serv.erl new file mode 100644 index 0000000..24aed16 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_serv.erl @@ -0,0 +1,154 @@ +%%% Handles socket connections, and bridges a remote server +%%% With a progressquest game. +-module(sockserv_serv). +-behaviour(gen_server). + +-record(state, {name, % player's name + next, % next step, used when initializing + socket}). % the current socket + +-export([start_link/1]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + code_change/3, terminate/2]). + +-define(SOCK(Msg), {tcp, _Port, Msg}). +-define(TIME, 800). +-define(EXP, 50). + +%% The socket is passed in from sockserv_sup. +%% It's a listen socket, as started by gen_tcp:listen/2. +%% +%% In Erlang, a TCP socket must be started as a listening socket first. +%% The listening socket can then be used to listen for a connection, +%% meant to be accepted. To do so, use gen_tcp:accept/1-2, as it is done +%% later in this module. +%% +%% A single listen socket can be used by many processes, each accepting +%% a communication. When a communication is accepted with accept/1-2, +%% a new socket, called accept socket, is returned. This accept socket +%% is the one that may be used to communicate with a client. +start_link(Socket) -> + gen_server:start_link(?MODULE, Socket, []). + +init(Socket) -> + %% properly seeding the process + <<A:32, B:32, C:32>> = crypto:rand_bytes(12), + random:seed({A,B,C}), + %% Because accepting a connection is a blocking function call, + %% we can not do it in here. Forward to the server loop! + gen_server:cast(self(), accept), + {ok, #state{socket=Socket}}. + +handle_call(_E, _From, State) -> + {noreply, State}. + +%% Accepting a connection +handle_cast(accept, S = #state{socket=ListenSocket}) -> + %% this is the socket acceptance mentioned earlier + {ok, AcceptSocket} = gen_tcp:accept(ListenSocket), + %% Remember that thou art dust, and to dust thou shalt return. + %% We want to always keep a given number of children in this app. + sockserv_sup:start_socket(), % a new acceptor is born, praise the lord + send(AcceptSocket, "What's your character's name?", []), + {noreply, S#state{socket=AcceptSocket, next=name}}; +%% The player has given us his name (in handle_info) +%% so we now roll stats that might or might not satisfy +%% said player. +handle_cast(roll_stats, S = #state{socket=Socket}) -> + Roll = pq_stats:initial_roll(), + send(Socket, + "Stats for your character:~n" + " Charisma: ~B~n" + " Constitution: ~B~n" + " Dexterity: ~B~n" + " Intelligence: ~B~n" + " Strength: ~B~n" + " Wisdom: ~B~n~n" + "Do you agree to these? y/n~n", + [Points || {_Name, Points} <- lists:sort(Roll)]), + {noreply, S#state{next={stats, Roll}}}; +%% The player has accepted the stats! Start the game! +handle_cast(stats_accepted, S = #state{name=Name, next={stats, Stats}}) -> + processquest:start_player(Name, [{stats,Stats},{time,?TIME}, + {lvlexp, ?EXP}]), + processquest:subscribe(Name, sockserv_pq_events, self()), + {noreply, S#state{next=playing}}; +%% Events coming in from process quest +%% We know this because all these events' tuples start with the +%% name of the player. +handle_cast(Event, S = #state{name=N, socket=Sock}) when element(1, Event) =:= N -> + [case E of + {wait, Time} -> timer:sleep(Time); + IoList -> send(Sock, IoList, []) + end || E <- sockserv_trans:to_str(Event)], % translate to a string + {noreply, S}. + +%% The TCP client sends the string "quit". We close the connection. +handle_info(?SOCK("quit"++_), S) -> + processquest:stop_player(S#state.name), + gen_tcp:close(S#state.socket), + {stop, normal, S}; +%% We receive a string while looking for a name -- we assume that hte +%% string is the name. +handle_info(?SOCK(Str), S = #state{next=name}) -> + Name = line(Str), + gen_server:cast(self(), roll_stats), + {noreply, S#state{name=Name, next=stats}}; +%% The user might or might not accept the stats we rolled in handle_cast +handle_info(?SOCK(Str), S = #state{socket=Socket, next={stats, _}}) -> + case line(Str) of + "y" -> + gen_server:cast(self(), stats_accepted); + "n" -> + gen_server:cast(self(), roll_stats); + _ -> % ask again because we didn't get what we wanted + send(Socket, "Answer with y (yes) or n (no)", []) + end, + {noreply, S}; +handle_info(?SOCK(E), S = #state{socket=Socket}) -> + send(Socket, "Unexpected input: ~p~n", [E]), + {noreply, S}; +handle_info({tcp_closed, _Socket}, S) -> + {stop, normal, S}; +handle_info({tcp_error, _Socket, _}, S) -> + {stop, normal, S}; +handle_info(E, S) -> + io:format("unexpected: ~p~n", [E]), + {noreply, S}. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +terminate(normal, _State) -> + ok; +terminate(_Reason, _State) -> + io:format("terminate reason: ~p~n", [_Reason]). + +%% Send a message through a socket, then make it active again. +%% The difference between an active and a passive socket is that +%% an active socket will send incoming data as Erlang messages, while +%% passive sockets will require to be polled with gen_tcp:recv/2-3. +%% +%% Depending on the context, you might want one or the other. I chose +%% to have active sockets because they feel somewhat easier to work +%% with. However, one problem with active sockets is that all input +%% is blindly changed into messages and makes it so the Erlang VM +%% is somewhat more subject to overload. Passive sockets push this +%% responsibility to the underlying implementation and the OS and are +%% somewhat safer. +%% +%% A middle ground exists, with sockets that are 'active once'. +%% The {active, once} option (can be set with inet:setopts or +%% when creating the listen socket) makes it so only *one* message +%% will be sent in active mode, and then the socket is automatically +%% turned back to passive mode. On each message reception, we turn +%% the socket back to {active once} as to achieve rate limiting. +send(Socket, Str, Args) -> + ok = gen_tcp:send(Socket, io_lib:format(Str++"~n", Args)), + ok = inet:setopts(Socket, [{active, once}]), + ok. + +%% Let's get rid of the whitespace and ignore whatever's after. +%% makes it simpler to deal with telnet. +line(Str) -> + hd(string:tokens(Str, "\r\n ")). |