Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,3 @@
{enet, ".*", {git, "https://github.com/flambard/enet", {branch, "master"}}},
{lager, ".*", {git, "https://github.com/erlang-lager/lager", {branch, "adt/lager_use_logger-option"}}}
]}.

2 changes: 1 addition & 1 deletion rebar.lock
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
{<<"iso8601">>,{pkg,<<"iso8601">>,<<"1.3.4">>},0},
{<<"lager">>,
{git,"https://github.com/erlang-lager/lager",
{ref,"20ee7596867705d8ec9f6194bc937cc8250c6e33"}},
{ref,"c97cf1512f976583f632158a96afa9e92713bac0"}},
0},
{<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},1},
{<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.3.0">>},1},
Expand Down
177 changes: 163 additions & 14 deletions src/openomf_lobby_client.erl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
-define(PACKET_REFRESH, 8).
-define(PACKET_ANNOUNCEMENT, 9).
-define(PACKET_RELAY, 10).
-define(PACKET_SPECTATE, 11).

-define(CHALLENGE_OFFER, 1).
-define(CHALLENGE_ACCEPT, 1).
Expand All @@ -35,6 +36,9 @@
-define(CHALLENGE_DONE, 4).
-define(CHALLENGE_ERROR, 5).

-define(SPECTATE_ACCEPT, 1).
-define(SPECTATE_ERROR, 2).

-define(JOIN_ERROR_NAME_USED, 1).
-define(JOIN_ERROR_NAME_INVALID, 2).
-define(JOIN_ERROR_UNSUPPORTED_PROTOCOL, 3).
Expand Down Expand Up @@ -71,7 +75,7 @@ init(PeerInfo) ->
handle_call(_, _, State) ->
{reply, error, State}.

handle_cast({get_presence, From}, State = #state{name=Name, version=Version}) when is_binary(Name) ->
handle_cast({get_presence, From}, State = #state{name=Name}) when is_binary(Name) ->
gen_server:cast(From, {presence, encode_peer_to_presence(State#state.peer_info, 0)}),
{noreply, State};

Expand All @@ -83,7 +87,7 @@ handle_cast({challenge, From, ChallengerID, Version}, State = #state{name=Name,
enet:send_reliable(Channel, Packet),
PeerInfo2 = maps:put(status, ?PRESENCE_PONDERING, PeerInfo),
enet:broadcast_reliable(2098, 0, encode_peer_to_presence(PeerInfo2, 0)),
%% TODO
%% TODO
{noreply, State#state{match_pid=From, peer_info=PeerInfo2}};

handle_cast({challenge, From, _ChallengerID, _Version}, State = #state{name=Name, match_pid=MatchPid}) when is_binary(Name), is_pid(MatchPid) ->
Expand All @@ -93,7 +97,7 @@ handle_cast({challenge, From, _ChallengerID, _Version}, State = #state{name=Name

handle_cast({challenge, From, _ChallengerID, TheirVersion}, State = #state{name=Name, version=OurVersion, match_pid=undefined}) when is_binary(Name) andalso TheirVersion /= OurVersion ->
gen_server:cast(From, {incompatible, self()}),
%% TODO
%% TODO
{noreply, State};

handle_cast(accepted, State = #state{peer_info=PeerInfo, match_pid = MatchPid}) when is_pid(MatchPid) ->
Expand Down Expand Up @@ -168,6 +172,8 @@ handle_info({'EXIT', PeerPid, _Reason}, State = #state{name=Name, peer_pid=PeerP
RFU = 0,
ConnectID = maps:get(connect_id, State#state.peer_info),
user_leave_event(Name),
%% Record last player activity
openomf_lobby_client_sup:record_last_activity(Name),
enet:broadcast_reliable(2098, 0, <<?PACKET_DISCONNECT:4/integer, RFU:4/integer, ConnectID:32/integer-unsigned-big>>),
{stop, normal, State};

Expand Down Expand Up @@ -262,6 +268,8 @@ handle_info({enet, ChannelID, #reliable{ data = <<?PACKET_JOIN:4/integer, LobbyV
lager:info("motd is ~p", [Message]),
enet:send_reliable(Channel, <<?PACKET_ANNOUNCEMENT:4/integer, 0:4/integer, Message/binary, 0>>)
end,
%% Send last match and activity info
send_activity_info(Channel),
{noreply ,State#state{name = Name, version = Version, protocol_version = LobbyVersion, peer_info = PeerInfo2}}
catch _:_ ->
%% user already registered
Expand Down Expand Up @@ -306,6 +314,14 @@ handle_info({enet, _ChannelID, #reliable{ data = <<?PACKET_YELL:4/integer, 0:4/i
end,
lager:info("client ~p yelled ~s", [State#state.name, Yell]),
enet:broadcast_reliable(2098, 0, <<?PACKET_YELL:4/integer, 0:4/integer, Name/binary, ": ", Yell/binary, 0>>),
%% Bridge chat message to Discord
case application:get_env(discord_callback) of
undefined -> ok;
{ok, URL} ->
ChatMessage = iolist_to_binary(io_lib:format("**~s**: ~s", [Name, Yell])),
DiscordPayload = <<"{\"content\": \"", ChatMessage/binary, "\" }">>,
hackney:request(post, URL, [{<<"Content-Type">>, <<"application/json">>}], DiscordPayload, [])
end,
{noreply ,State};

handle_info({enet, _ChannelID, #reliable{ data = <<?PACKET_CHALLENGE:4/integer, ?CHALLENGE_ACCEPT:4/integer>> }}, State = #state{match_pid=MatchPid}) when MatchPid /= undefined ->
Expand Down Expand Up @@ -335,14 +351,22 @@ handle_info({enet, ChannelID, #reliable{ data = <<?PACKET_CHALLENGE:4/integer, 0
Channel = maps:get(ChannelID, Channels),
case gproc:lookup_pids({n, l, {connect_id, ID}}) of
[Pid] ->
case openomf_lobby_match_sup:start_match(self(), PeerInfo, Pid) of
{ok, MatchPid} ->
PeerInfo2 = maps:put(status, ?PRESENCE_CHALLENGING, PeerInfo),
enet:broadcast_reliable(2098, 0, encode_peer_to_presence(PeerInfo2, 0)),
{noreply, State#state{match_pid =MatchPid, peer_info=PeerInfo2}};
{error, Reason} ->
lager:info("failed to challenge ~p", [Reason]),
ErrorString = list_to_binary(io_lib:format("Failed to challenge user: ~p", [Reason])),
case find_match_processes(ID) of
[] ->
case openomf_lobby_match_sup:start_match(self(), PeerInfo, Pid, ID) of
{ok, MatchPid} ->
PeerInfo2 = maps:put(status, ?PRESENCE_CHALLENGING, PeerInfo),
enet:broadcast_reliable(2098, 0, encode_peer_to_presence(PeerInfo2, 0)),
{noreply, State#state{match_pid =MatchPid, peer_info=PeerInfo2}};
{error, Reason} ->
lager:info("failed to challenge ~p", [Reason]),
ErrorString = list_to_binary(io_lib:format("Failed to challenge user: ~p", [Reason])),
enet:send_reliable(Channel, <<?PACKET_CHALLENGE:4/integer, ?CHALLENGE_ERROR:4/integer, ErrorString/binary>>),
{noreply, State}
end;
_ ->
lager:info("failed to challenge: user busy"),
ErrorString = <<"Failed to challenge user: user busy">>,
enet:send_reliable(Channel, <<?PACKET_CHALLENGE:4/integer, ?CHALLENGE_ERROR:4/integer, ErrorString/binary>>),
{noreply, State}
end;
Expand All @@ -352,6 +376,39 @@ handle_info({enet, ChannelID, #reliable{ data = <<?PACKET_CHALLENGE:4/integer, 0
{noreply, State}
end;

handle_info({enet, ChannelID, #reliable{ data = <<?PACKET_SPECTATE:4/unsigned-integer, 0:4/integer, ID:32/integer-unsigned-big>> }}, State = #state{peer_info=PeerInfo}) ->
Channels = maps:get(channels, PeerInfo),
LobbyChannel = maps:get(ChannelID, Channels),
case find_match_processes(ID) of
[Pid] ->
lager:info("sending spectate request to ~p", [Pid]),
Channel = maps:get(2, Channels),
OkFun = fun() ->
enet:send_reliable(LobbyChannel, <<?PACKET_SPECTATE:4/integer, ?SPECTATE_ACCEPT:4/integer>>)
end,
try gen_statem:call(Pid, {subscribe, Channel, self(), OkFun}) of
ok ->
PeerInfo2 = maps:put(status, ?PRESENCE_WATCHING, PeerInfo),
enet:broadcast_reliable(2098, 0, encode_peer_to_presence(PeerInfo2, 0)),
{noreply, State#state{peer_info=PeerInfo2}};
Error ->
lager:warning("unexpected spectate result ~p", [Error]),
ErrorString = list_to_binary(io_lib:format("Failed to spectate match: ~p", [Error])),
enet:send_reliable(LobbyChannel, <<?PACKET_SPECTATE:4/integer, ?SPECTATE_ERROR:4/integer, ErrorString/binary>>),
{noreply, State}
catch What:Why ->
lager:info("failed to spectate ~p", [{What, Why}]),
ErrorString = list_to_binary(io_lib:format("Failed to spectate match: ~p", [{What, Why}])),
enet:send_reliable(LobbyChannel, <<?PACKET_SPECTATE:4/integer, ?SPECTATE_ERROR:4/integer, ErrorString/binary>>),
{noreply, State}
end;
_ ->
lager:info("failed to spectate: no such match"),
ErrorString = <<"Failed to spectate match: no such match">>,
enet:send_reliable(LobbyChannel, <<?PACKET_SPECTATE:4/integer, ?SPECTATE_ERROR:4/integer, ErrorString/binary>>),
{noreply, State}
end;

handle_info({enet, _ChannelID, #reliable{ data = <<?PACKET_CONNECTED:4/integer, 0:4/integer>> }}, State = #state{match_pid = MatchPid}) when is_pid(MatchPid) ->
gen_statem:cast(MatchPid, {connected, self()}),
lager:info("~p connected to peer", [State#state.name]),
Expand All @@ -368,7 +425,7 @@ handle_info({enet, _ChannelID, #reliable{ data = <<?PACKET_CONNECTED:4/integer,
%% TODO relay the game packets via the server once both sides have failed to connect 2x
{noreply, State};

handle_info({enet, ChannelID, #reliable{ data = <<?PACKET_REFRESH:4/integer, _:4/integer>> }}, State = #state{peer_info=PeerInfo}) ->
handle_info({enet, ChannelID, #reliable{ data = <<?PACKET_REFRESH:4/integer, Rest:4/integer>> }}, State = #state{peer_info=PeerInfo}) ->

Channels = maps:get(channels, PeerInfo),
Channel = maps:get(ChannelID, Channels),
Expand All @@ -385,8 +442,18 @@ handle_info({enet, ChannelID, #reliable{ data = <<?PACKET_REFRESH:4/integer, _:4
ok
end,

enet:send_reliable(Channel, encode_peer_to_presence(PeerInfo, 0)),
{noreply, State};
PeerInfo2 = case Rest of
?PRESENCE_AVAILABLE ->
PI2 = maps:put(status, ?PRESENCE_AVAILABLE, State#state.peer_info),
enet:broadcast_reliable(2098, 0, encode_peer_to_presence(PI2, 0)),
PI2;
_ ->
%% normal refresh
PeerInfo
end,

enet:send_reliable(Channel, encode_peer_to_presence(PeerInfo2, 0)),
{noreply, State#state{peer_info=PeerInfo2}};

handle_info({enet, _ChannelID, #reliable{ data = Packet }}, State) ->
lager:info("got reliable packet ~p", [Packet]),
Expand Down Expand Up @@ -430,4 +497,86 @@ user_leave_event(Name) ->
hackney:request(post, URL, [{<<"Content-Type">>, <<"application/json">>}], <<"{\"content\": \"'", Name/binary, "' has left the arena\" }">>, [])
end.

find_match_processes(KnownID) ->
%% Construct a match specification to check both ID positions
MatchSpec = [{
%% Match the key pattern {n, l, {match, '$1', '$2'}}
{ {n, l, {match, '$1', '$2'}}, '_', '_' },
%% Guard: Check if either '$1' or '$2' equals KnownID
[{'orelse', {'=:=', '$1', KnownID}, {'=:=', '$2', KnownID}}],
%% Return all matched entries
['$_']
}],
%% Execute the select query on the local registry
case gproc:select(n, MatchSpec) of
[] ->
[]; %% No processes found
Entries ->
%% Extract PIDs from the matched entries
[Pid || {_, Pid, _} <- Entries]
end.

%% Format relative time from timestamp
format_relative_time(Timestamp) ->
Now = erlang:system_time(second),
Diff = Now - Timestamp,

if
Diff < 60 ->
"just now";
Diff < 3600 ->
Minutes = Diff div 60,
case Minutes of
1 -> "1 minute ago";
_ -> io_lib:format("~p minutes ago", [Minutes])
end;
Diff < 86400 ->
Hours = Diff div 3600,
case Hours of
1 -> "1 hour ago";
_ -> io_lib:format("~p hours ago", [Hours])
end;
Diff < 604800 ->
Days = Diff div 86400,
case Days of
1 -> "1 day ago";
_ -> io_lib:format("~p days ago", [Days])
end;
true ->
Weeks = Diff div 604800,
case Weeks of
1 -> "1 week ago";
_ -> io_lib:format("~p weeks ago", [Weeks])
end
end.

%% Send activity information to a newly joined client
send_activity_info(Channel) ->
%% Check if there are other players online
Children = supervisor:which_children(openomf_lobby_client_sup),
ActivePlayerCount = length([Pid || {_Id, Pid, _Type, _Modules} <- Children, Pid /= self()]),

%% Send last match info
case openomf_lobby_client_sup:get_last_match() of
{ok, {Timestamp, Winner, Loser}} ->
RelativeTime = format_relative_time(Timestamp),
Message = iolist_to_binary(io_lib:format("Last match: ~s defeated ~s ~s", [Winner, Loser, RelativeTime])),
enet:send_reliable(Channel, <<?PACKET_ANNOUNCEMENT:4/integer, 0:4/integer, Message/binary, 0>>);
undefined ->
ok
end,

%% Send last activity info only if no other players are online
case ActivePlayerCount of
0 ->
case openomf_lobby_client_sup:get_last_activity() of
{ok, {ActivityTimestamp, PlayerName}} ->
ActivityRelativeTime = format_relative_time(ActivityTimestamp),
ActivityMessage = iolist_to_binary(io_lib:format("Last player online: ~s ~s", [PlayerName, ActivityRelativeTime])),
enet:send_reliable(Channel, <<?PACKET_ANNOUNCEMENT:4/integer, 0:4/integer, ActivityMessage/binary, 0>>);
undefined ->
ok
end;
_ ->
ok
end.
26 changes: 25 additions & 1 deletion src/openomf_lobby_client_sup.erl
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
-behaviour(supervisor).

-export([start_link/0,
start_client/1, client_presence/0, announce/1]).
start_client/1, client_presence/0, announce/1,
record_last_match/2, record_last_activity/1,
get_last_match/0, get_last_activity/0]).

-export([init/1]).

start_link() ->
% Create ETS table for tracking activity
ets:new(lobby_activity, [named_table, public, set]),
supervisor:start_link({local, ?MODULE}, ?MODULE, []).

init(_Args) ->
Expand All @@ -32,3 +36,23 @@ announce(Message) ->
Children = supervisor:which_children(?MODULE),
[ openomf_lobby_client:announce(Pid, Message) || {_Id, Pid, _Type, _Modules} <- Children].

%% Activity tracking functions
record_last_match(Winner, Loser) ->
Timestamp = erlang:system_time(second),
ets:insert(lobby_activity, {last_match, {Timestamp, Winner, Loser}}).

record_last_activity(PlayerName) ->
Timestamp = erlang:system_time(second),
ets:insert(lobby_activity, {last_activity, {Timestamp, PlayerName}}).

get_last_match() ->
case ets:lookup(lobby_activity, last_match) of
[{last_match, Data}] -> {ok, Data};
[] -> undefined
end.

get_last_activity() ->
case ets:lookup(lobby_activity, last_activity) of
[{last_activity, Data}] -> {ok, Data};
[] -> undefined
end.
Loading