diff options
author | Trygve Laugstøl <trygvis@inamo.no> | 2024-02-23 07:08:18 +0100 |
---|---|---|
committer | Trygve Laugstøl <trygvis@inamo.no> | 2024-02-23 07:08:18 +0100 |
commit | 5a9cdd3cc89507d4d74f8bded56ce5e037b3b56e (patch) | |
tree | 982ca2e7f9ac4e8c350dfb5c4f60bcfdfff5afaf /learn-you-some-erlang/processquest/apps/sockserv-1.0.0 | |
parent | 05ae56e5e89abf2993f84e6d52b250131f247c35 (diff) | |
download | erlang-workshop-5a9cdd3cc89507d4d74f8bded56ce5e037b3b56e.tar.gz erlang-workshop-5a9cdd3cc89507d4d74f8bded56ce5e037b3b56e.tar.bz2 erlang-workshop-5a9cdd3cc89507d4d74f8bded56ce5e037b3b56e.tar.xz erlang-workshop-5a9cdd3cc89507d4d74f8bded56ce5e037b3b56e.zip |
wip
Diffstat (limited to 'learn-you-some-erlang/processquest/apps/sockserv-1.0.0')
7 files changed, 294 insertions, 0 deletions
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/Emakefile b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/Emakefile new file mode 100644 index 0000000..b203ea3 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/Emakefile @@ -0,0 +1,2 @@ +{["src/*", "test/*"], + [{i,"include"}, {outdir, "ebin"}]}. diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/ebin/sockserv.app b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/ebin/sockserv.app new file mode 100644 index 0000000..dd6d8c7 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/ebin/sockserv.app @@ -0,0 +1,11 @@ +{application, sockserv, + [{description, "Socket server to forward ProcessQuest messages to a client"}, + {vsn, "1.0.0"}, + {mod, {sockserv, []}}, + {registered, [sockserv_sup]}, + {modules, [sockserv, sockserv_sup, sockserv_serv, sockserv_trans, + sockserv_pq_events]}, + {applications, [stdlib, kernel, processquest]}, + {env, + [{port, 8082}]} + ]}. diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv.erl new file mode 100644 index 0000000..bd82b6d --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv.erl @@ -0,0 +1,13 @@ +%%% Starting the sockserv application. +%%% The sockserv application is a lightweight +%%% Raw socket server that can be used with telnet +%%% to follow updates on the process quest game. +%%% The port is defined in the app's env +-module(sockserv). +-behaviour(application). +-export([start/2, stop/1]). + +start(normal, []) -> + sockserv_sup:start_link(). + +stop(_) -> ok. diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_pq_events.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_pq_events.erl new file mode 100644 index 0000000..c3c6705 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_pq_events.erl @@ -0,0 +1,25 @@ +%%% Converts events from a player's event manager into a +%%% cast sent to the sockserv socket gen_server. +-module(sockserv_pq_events). +-behaviour(gen_event). +-export([init/1, handle_event/2, handle_call/2, handle_info/2, + terminate/2, code_change/3]). + +init(Parent) -> {ok, Parent}. + +handle_event(E, Pid) -> + gen_server:cast(Pid, E), + {ok, Pid}. + +handle_call(Req, Pid) -> + Pid ! Req, + {ok, ok, Pid}. + +handle_info(E, Pid) -> + Pid ! E, + {ok, Pid}. + +terminate(_, _) -> ok. + +code_change(_OldVsn, Pid, _Extra) -> + {ok, Pid}. diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_serv.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_serv.erl new file mode 100644 index 0000000..cabc608 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_serv.erl @@ -0,0 +1,153 @@ +%%% 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), + {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{socket=S}) -> + gen_tcp:close(S); +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 ")). diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_sup.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_sup.erl new file mode 100644 index 0000000..0e8cbdd --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_sup.erl @@ -0,0 +1,32 @@ +%%% The supervisor in charge of all the socket acceptors. +-module(sockserv_sup). +-behaviour(supervisor). + +-export([start_link/0, start_socket/0]). +-export([init/1]). + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +init([]) -> + {ok, Port} = application:get_env(port), + %% Set the socket into {active_once} mode. + %% See sockserv_serv comments for more details + {ok, ListenSocket} = gen_tcp:listen(Port, [{active,once}, {packet,line}]), + spawn_link(fun empty_listeners/0), + {ok, {{simple_one_for_one, 60, 3600}, + [{socket, + {sockserv_serv, start_link, [ListenSocket]}, % pass the socket! + temporary, 1000, worker, [sockserv_serv]} + ]}}. + +start_socket() -> + supervisor:start_child(?MODULE, []). + +%% Start with 20 listeners so that many multiple connections can +%% be started at once, without serialization. In best circumstances, +%% a process would keep the count active at all times to insure nothing +%% bad happens over time when processes get killed too much. +empty_listeners() -> + [start_socket() || _ <- lists:seq(1,20)], + ok. diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_trans.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_trans.erl new file mode 100644 index 0000000..23a70c0 --- /dev/null +++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_trans.erl @@ -0,0 +1,58 @@ +%%% Translates the process quest events to iolists +%%% that can be sent over a socket. +%%% +%%% IO lists are lists of bytes (0..255, ASCII), +%%% binaries and other iolists. They allow to append, +%%% prepend and insert data in strings without re-writing +%%% the fragments that compose them. Erlang's drivers and +%%% IO modules accept them without an issue and are a quick, +%%% somewhat elegant solution to immutable data structures +%%% requiring many changes. +-module(sockserv_trans). +-export([to_str/1]). + +%% The player killed something +to_str({_User, killed, Time, {EnemyName, Props}}) -> + {Drop, _} = proplists:get_value(drop, Props), + [["Executing a ",EnemyName, "..."], + {wait, Time}, % take a pause between the output values + ["Obtained ", Drop, "."]]; +%% Changing locations +to_str({_Name, heading, _Time, Loc}) -> + [["Heading to ", + case Loc of + market -> "the marketplace to sell loot..."; + killing -> "the killing fields..." + end]]; +%% Leveling up +to_str({_Name, lvl_up, _, NewStats, NewLvl, _NewExp}) -> + [["Leveled up to level ", integer_to_list(NewLvl), + " Here are your new stats:", $\n, + io_lib:format( + " Charisma: ~B~n" + " Constitution: ~B~n" + " Dexterity: ~B~n" + " Intelligence: ~B~n" + " Strength: ~B~n" + " Wisdom: ~B~n~n", + [Points || {_, Points} <- lists:sort(NewStats)])]]; +%% Bought an item +to_str({_Name, buy, Time, Slot, {Item, _, _, _}}) -> + SlotTxt = case Slot of + armor -> " armor"; + weapon -> ""; + helmet -> " helmet"; + shield -> " shield" + end, + [["Negotiating purchase of better equipment..."], + {wait, Time}, + ["Bought a ", Item, SlotTxt]]; +%% Sold an item +to_str({_Name, sell, Time, {Item, Val}}) -> + [["Selling ", Item], + {wait, Time}, + ["Got ", integer_to_list(Val), " bucks."]]; +%% Completed a quest +to_str({_Name, quest, 0, Completed, New}) -> + [["Completed quest: ", Completed, "..."], + ["Obtained new quest: ", New, "."]]. |