Compare commits
213 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ceea04c3b4 | ||
![]() |
9adab0ea87 | ||
![]() |
6579c26f98 | ||
![]() |
368bb0f7b4 | ||
![]() |
807510f669 | ||
![]() |
c69513073d | ||
![]() |
35b86b58ba | ||
![]() |
ac0c3c551b | ||
![]() |
289ce855ad | ||
![]() |
bc47fcb049 | ||
![]() |
408e7e99e6 | ||
![]() |
c42b1a85f8 | ||
![]() |
e2274666b8 | ||
![]() |
6fd1119777 | ||
![]() |
eab53bc3a7 | ||
![]() |
2325c7cf63 | ||
![]() |
8889ca8332 | ||
![]() |
23e781f498 | ||
![]() |
60b8009382 | ||
![]() |
325c1a4c10 | ||
![]() |
8f3db6480a | ||
![]() |
8363741b3a | ||
![]() |
935b490461 | ||
![]() |
948873ddec | ||
![]() |
72726bdf6c | ||
![]() |
0a399238f4 | ||
![]() |
c957d9a8b9 | ||
![]() |
d9cde30b0b | ||
![]() |
c7cb5ab589 | ||
![]() |
dc26bb82f7 | ||
![]() |
d8906226a5 | ||
![]() |
730f47d837 | ||
![]() |
d2f7c9e83f | ||
![]() |
6466f05728 | ||
![]() |
3821a1e7bf | ||
![]() |
a1bf3e43f3 | ||
![]() |
3290aba95d | ||
![]() |
72989f5332 | ||
![]() |
6cbb987ab0 | ||
![]() |
9c8ad80a07 | ||
![]() |
b5d6b3934b | ||
![]() |
cdc2c56d5c | ||
![]() |
5b255b211b | ||
![]() |
edb061662f | ||
![]() |
9268f3f7ae | ||
![]() |
dd4a228b01 | ||
![]() |
34b4a21ce7 | ||
![]() |
2a7383b9a1 | ||
![]() |
3f1d1d2fb0 | ||
![]() |
18429b8d76 | ||
![]() |
82aca844f2 | ||
![]() |
1b36ff2589 | ||
![]() |
4ff28e0939 | ||
![]() |
f21f23a92f | ||
![]() |
979e99f6a5 | ||
![]() |
58cfb8a61e | ||
![]() |
9aca48a697 | ||
![]() |
722bcf7c9e | ||
![]() |
dc77b8e804 | ||
![]() |
eaafdc213f | ||
![]() |
f8524ca9c4 | ||
![]() |
8c95aab709 | ||
![]() |
87c256edba | ||
![]() |
9c7f8f6eaa | ||
![]() |
5c234257dc | ||
![]() |
a571b9a56b | ||
![]() |
dfa5634adb | ||
![]() |
53a4b3dbbe | ||
![]() |
4faabbda8f | ||
![]() |
023214793c | ||
![]() |
250a22dea8 | ||
![]() |
f40ba44364 | ||
![]() |
d0e15316b8 | ||
![]() |
4848b3c218 | ||
![]() |
7415da1c89 | ||
![]() |
e8c1c98824 | ||
![]() |
fde0f8b3fe | ||
![]() |
82e82503fa | ||
![]() |
fde008c7a8 | ||
![]() |
564ab8749f | ||
![]() |
6dc9fb52dc | ||
![]() |
0b02718faa | ||
![]() |
333e898bb4 | ||
![]() |
64d8bf7c25 | ||
![]() |
a44fc4274f | ||
![]() |
5f0bd73303 | ||
![]() |
19350ba1ff | ||
![]() |
998263b417 | ||
![]() |
b6c1bf277d | ||
![]() |
86bb5c81b3 | ||
![]() |
d69fe073a8 | ||
![]() |
6c8b831fd2 | ||
![]() |
953da28a3e | ||
![]() |
344c534812 | ||
![]() |
3280e79743 | ||
![]() |
6e922a7ec9 | ||
![]() |
823ee73e7d | ||
![]() |
40d2eed01b | ||
![]() |
ac8d6858cd | ||
![]() |
e409241a50 | ||
![]() |
25c9548ec3 | ||
![]() |
0b8c4dbd85 | ||
![]() |
8eae404797 | ||
![]() |
840db6b7b3 | ||
![]() |
d5b5afa0a7 | ||
![]() |
344b88eec4 | ||
![]() |
69a07dfad2 | ||
![]() |
57e4e91187 | ||
![]() |
722e0a53f4 | ||
![]() |
0f64bea72d | ||
![]() |
b380fe9d23 | ||
![]() |
ecee1226aa | ||
![]() |
378e9a9927 | ||
![]() |
2de4359c32 | ||
![]() |
b728731830 | ||
![]() |
1be3c4f5c6 | ||
![]() |
e7246271ad | ||
![]() |
ad7071a61b | ||
![]() |
50c2a2615f | ||
![]() |
afb888e566 | ||
![]() |
326b356d02 | ||
![]() |
9d10c28504 | ||
![]() |
06a032652a | ||
![]() |
a4d9c5a35d | ||
![]() |
526f281e2b | ||
![]() |
bf7fa44897 | ||
![]() |
11a03f3a96 | ||
![]() |
7e42b9b26a | ||
![]() |
a1b8b1909e | ||
![]() |
8f069e72d8 | ||
![]() |
9247e51fb7 | ||
![]() |
ea25d1bd74 | ||
![]() |
ffd27bda46 | ||
![]() |
fdfd49179f | ||
![]() |
0989664035 | ||
![]() |
74f54e7e71 | ||
![]() |
a484de77e0 | ||
![]() |
098ad5243a | ||
![]() |
05149483c8 | ||
![]() |
4dcaa79371 | ||
![]() |
4171f2eba4 | ||
![]() |
bd4a296b0a | ||
![]() |
d022584300 | ||
![]() |
1957ad0f1d | ||
![]() |
3a2bb34cbe | ||
![]() |
cedc2f50d7 | ||
![]() |
8526b5ab8f | ||
![]() |
d043ab4d3d | ||
![]() |
d5178a313e | ||
![]() |
411763faf5 | ||
![]() |
f867dfbb72 | ||
![]() |
207b0b04cc | ||
![]() |
c378397181 | ||
![]() |
3eb3d4dc4d | ||
![]() |
67b38a3316 | ||
![]() |
92f3c2fbc5 | ||
![]() |
6335c39403 | ||
![]() |
c161f8c48a | ||
![]() |
8504352ef7 | ||
![]() |
9b501e998b | ||
![]() |
9f3f815be8 | ||
![]() |
3a45b43354 | ||
![]() |
3b05e4dfe3 | ||
![]() |
401ffe5194 | ||
![]() |
48728337c7 | ||
![]() |
d7f41a8ee5 | ||
![]() |
bd6b5632f1 | ||
![]() |
c23c7e56fa | ||
![]() |
9efb734c63 | ||
![]() |
7fadf362b3 | ||
![]() |
71772a58fb | ||
![]() |
1e83a98d22 | ||
![]() |
681d61e73c | ||
![]() |
e9fc339bc2 | ||
![]() |
ccf3c6f55e | ||
![]() |
2ce9ec0857 | ||
![]() |
4e12f5bd5f | ||
![]() |
f6819a2f95 | ||
![]() |
eafeb744af | ||
![]() |
fc6c424536 | ||
![]() |
1e264d3e1b | ||
![]() |
35bcf4595e | ||
![]() |
dc2968cbce | ||
![]() |
9528c521b3 | ||
![]() |
01c02b24ef | ||
![]() |
3fa3d27915 | ||
![]() |
0a14def994 | ||
![]() |
1c8dbd0a69 | ||
![]() |
8c0683c07b | ||
![]() |
79e9e39fba | ||
![]() |
6f13319790 | ||
![]() |
03ec8a4562 | ||
![]() |
3f97b24a1d | ||
![]() |
9e36068382 | ||
![]() |
103fe3f8a9 | ||
![]() |
71d2c22028 | ||
![]() |
814d4348f5 | ||
![]() |
a0d6e67e0e | ||
![]() |
d6b4b24388 | ||
![]() |
22278250b9 | ||
![]() |
26d5763df3 | ||
![]() |
a86e4caf0d | ||
![]() |
31dc7ffb7f | ||
![]() |
94ce201941 | ||
![]() |
4ff129224f | ||
![]() |
c91880be1f | ||
![]() |
a6563c7378 | ||
![]() |
94e902d07e | ||
![]() |
b9f407bb1e | ||
![]() |
8bcf1ba3c8 | ||
![]() |
393ee96012 | ||
![]() |
af29f8a460 | ||
![]() |
d99c5388b9 |
7
.gitignore
vendored
7
.gitignore
vendored
@ -1 +1,6 @@
|
|||||||
Mnesia*
|
apps/*/c_src/*.o
|
||||||
|
apps/*/ebin
|
||||||
|
apps/*/priv/*.so
|
||||||
|
apps/egs/src/egs_script_lexer.erl
|
||||||
|
apps/egs/src/egs_script_parser.erl
|
||||||
|
deps
|
||||||
|
32
Makefile
32
Makefile
@ -1,5 +1,5 @@
|
|||||||
# EGS: Erlang Game Server
|
# EGS: Erlang Game Server
|
||||||
# Copyright (C) 2010 Loic Hoguin
|
# Copyright (C) 2010-2011 Loic Hoguin
|
||||||
#
|
#
|
||||||
# This file is part of EGS.
|
# This file is part of EGS.
|
||||||
#
|
#
|
||||||
@ -16,29 +16,23 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
# along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
ERL ?= erl
|
REBAR = rebar
|
||||||
ERLC ?= erlc
|
|
||||||
APP := egs
|
|
||||||
|
|
||||||
all: clean missions server
|
all: app
|
||||||
|
|
||||||
server:
|
app: deps
|
||||||
@./rebar compile
|
@$(REBAR) compile
|
||||||
|
|
||||||
missions:
|
deps:
|
||||||
$(ERLC) src/psu/psu_parser.erl
|
@$(REBAR) get-deps
|
||||||
$(ERL) -noshell -noinput -sname missions -pa ebin -run psu_parser run -run init stop
|
|
||||||
rm psu_parser.beam
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@./rebar clean
|
@$(REBAR) clean
|
||||||
rm -f erl_crash.dump
|
rm -f erl_crash.dump
|
||||||
|
|
||||||
fclean: clean
|
tests:
|
||||||
rm -rf Mnesia.egs*
|
@$(REBAR) eunit
|
||||||
|
@$(REBAR) ct
|
||||||
|
|
||||||
run:
|
dialyze:
|
||||||
@echo "EGS is free software available under the GNU GPL version 3"
|
@$(REBAR) dialyze
|
||||||
@echo "Copyright (C) 2010 Loic Hoguin"
|
|
||||||
@echo
|
|
||||||
$(ERL) -ssl protocol_version '{sslv3}' -sname egs -pa ebin -boot start_sasl -s reloader -s egs
|
|
||||||
|
2
README
2
README
@ -10,4 +10,6 @@ This is a very early work in progress. It does not include any data files
|
|||||||
yet, meaning one cannot at this point run a server without using the
|
yet, meaning one cannot at this point run a server without using the
|
||||||
proprietary files from AOTI.
|
proprietary files from AOTI.
|
||||||
|
|
||||||
|
The server requires Erlang R14B to compile and run.
|
||||||
|
|
||||||
Use 'make' to compile, 'make run' to run and 'make fclean' to cleanup.
|
Use 'make' to compile, 'make run' to run and 'make fclean' to cleanup.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
%% @copyright 2010 Loïc Hoguin.
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
%% @doc Project-wide Erlang records.
|
%% @doc Project-wide Erlang records.
|
||||||
%%
|
%%
|
||||||
%% This file is part of EGS.
|
%% This file is part of EGS.
|
||||||
@ -17,13 +17,67 @@
|
|||||||
%% You should have received a copy of the GNU Affero General Public License
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
%% @doc Per-process state used by the various EGS modules.
|
%% Records.
|
||||||
-record(state, {socket, gid, slot, lid=16#ffff, areanb=0}).
|
|
||||||
|
|
||||||
%% @doc Accounts. So far only used for storing temporary information.
|
%% @doc Client state. One per connected client.
|
||||||
%% @todo Hash the password.
|
-record(egs_net, {
|
||||||
%% @todo Add email, password_salt, is_ingame, register_time, last_login_time, etc.
|
socket :: ssl:sslsocket(),
|
||||||
-record(accounts, {gid, username, password, auth_state}).
|
transport :: module(),
|
||||||
|
handler :: module(),
|
||||||
|
buffer = <<>> :: binary(),
|
||||||
|
keepalive = false :: boolean(),
|
||||||
|
gid = 0 :: egs:gid(),
|
||||||
|
lid = 16#ffff :: egs:lid(),
|
||||||
|
slot = 0 :: 0..3,
|
||||||
|
areanb = 0 :: non_neg_integer()
|
||||||
|
}).
|
||||||
|
|
||||||
|
%% @doc Table containing the users currently logged in.
|
||||||
|
%% @todo Probably can use a "param" or "extra" field to store the game-specific information (for things that don't need to be queried).
|
||||||
|
-record(users, {
|
||||||
|
%% General information.
|
||||||
|
gid :: egs:gid(),
|
||||||
|
lid = 16#ffff :: egs:lid(),
|
||||||
|
pid :: pid(),
|
||||||
|
%% Character information.
|
||||||
|
%% @todo Specs it.
|
||||||
|
type = white,
|
||||||
|
slot,
|
||||||
|
npcid = 16#ffff,
|
||||||
|
name,
|
||||||
|
race,
|
||||||
|
gender,
|
||||||
|
class,
|
||||||
|
level = 1,
|
||||||
|
exp = 0,
|
||||||
|
currenthp = 100,
|
||||||
|
maxhp = 100,
|
||||||
|
stats = {stats, 1000, 2000, 3000, 4000, 5000, 6000, 7000},
|
||||||
|
money = 1000000,
|
||||||
|
blastbar = 0,
|
||||||
|
luck = 3,
|
||||||
|
appearance,
|
||||||
|
onlinestatus = 0,
|
||||||
|
options = {options, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0},
|
||||||
|
inventory = [],
|
||||||
|
%% Location/state related information.
|
||||||
|
uni :: integer(),
|
||||||
|
questpid :: pid(),
|
||||||
|
zonepid :: pid(),
|
||||||
|
partypid :: pid(),
|
||||||
|
areatype :: counter | mission | lobby | myroom,
|
||||||
|
area :: egs:area(),
|
||||||
|
entryid :: egs:entryid(),
|
||||||
|
pos = {0.0, 0.0, 0.0, 0.0} :: egs:position(),
|
||||||
|
shopid :: integer(),
|
||||||
|
prev_area = {0, 0, 0} :: egs:area(),
|
||||||
|
prev_entryid = 0 :: egs:entryid(),
|
||||||
|
%% To be moved or deleted later on.
|
||||||
|
instancepid :: pid(),
|
||||||
|
char
|
||||||
|
}).
|
||||||
|
|
||||||
|
%% Past this point needs to be reviewed.
|
||||||
|
|
||||||
%% @doc NPC configuration data.
|
%% @doc NPC configuration data.
|
||||||
%% @todo Add inventory, AI parameters.
|
%% @todo Add inventory, AI parameters.
|
||||||
@ -57,36 +111,6 @@
|
|||||||
faceboxx=65535, faceboxy=65535
|
faceboxx=65535, faceboxy=65535
|
||||||
}).
|
}).
|
||||||
|
|
||||||
%% Past this point needs to be reviewed.
|
|
||||||
|
|
||||||
%% @doc Table containing counters current values.
|
|
||||||
-record(counters, {name, id}).
|
|
||||||
|
|
||||||
%% @doc Character position data structure.
|
|
||||||
|
|
||||||
-record(pos, {x, y, z, dir}).
|
|
||||||
|
|
||||||
%% @doc Character area location data structure.
|
|
||||||
|
|
||||||
-record(psu_area, {questid, zoneid, mapid}).
|
|
||||||
|
|
||||||
%% @doc Table containing the users currently logged in.
|
|
||||||
%% @todo Probably can use a "param" or "extra" field to store the game-specific information (for things that don't need to be queried).
|
|
||||||
|
|
||||||
-record(egs_user_model, {
|
|
||||||
%% General information.
|
|
||||||
id, lid, pid, time, character,
|
|
||||||
%% Location/state related information.
|
|
||||||
instancepid, partypid, areatype, area, entryid, pos=#pos{x=0.0, y=0.0, z=0.0, dir=0.0}, shopid,
|
|
||||||
prev_area=#psu_area{questid=0, zoneid=0, mapid=0}, prev_entryid=0, %% universeid
|
|
||||||
%% To be moved or deleted later on.
|
|
||||||
setid=0 %% @todo Current area's set number. Move that to psu_instance probably.
|
|
||||||
}).
|
|
||||||
|
|
||||||
%% @doc Character main or class level data structure.
|
|
||||||
|
|
||||||
-record(level, {number, exp}).
|
|
||||||
|
|
||||||
%% @doc Character stats data structure.
|
%% @doc Character stats data structure.
|
||||||
|
|
||||||
-record(stats, {atp, ata, tp, dfp, evp, mst, sta}).
|
-record(stats, {atp, ata, tp, dfp, evp, mst, sta}).
|
||||||
@ -97,38 +121,6 @@
|
|||||||
cutindisplay, mainmenucursorposition, camera3y, camera3x, camera1y, camera1x, controller, weaponswap,
|
cutindisplay, mainmenucursorposition, camera3y, camera3x, camera1y, camera1x, controller, weaponswap,
|
||||||
lockon, brightness, functionkeysetting, buttondetaildisplay}).
|
lockon, brightness, functionkeysetting, buttondetaildisplay}).
|
||||||
|
|
||||||
%% @doc Characters data structure.
|
|
||||||
%% @todo Make a disk table for storing characters permanently. Also keep the current character in #users.
|
|
||||||
|
|
||||||
-record(characters, {
|
|
||||||
gid,
|
|
||||||
type=white,
|
|
||||||
slot,
|
|
||||||
npcid=16#ffff,
|
|
||||||
name,
|
|
||||||
race,
|
|
||||||
gender,
|
|
||||||
class,
|
|
||||||
mainlevel={level, 1, 0},
|
|
||||||
classlevels,
|
|
||||||
currenthp=100,
|
|
||||||
maxhp=100,
|
|
||||||
stats={stats, 1000, 2000, 3000, 4000, 5000, 6000, 7000},
|
|
||||||
se=[],
|
|
||||||
money=1000000,
|
|
||||||
blastbar=0,
|
|
||||||
luck=3,
|
|
||||||
playtime=0,
|
|
||||||
appearance,
|
|
||||||
onlinestatus=0,
|
|
||||||
options={options, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0},
|
|
||||||
inventory=[]
|
|
||||||
}). % also: shortcuts partnercards blacklist npcs flags...
|
|
||||||
|
|
||||||
%% @doc Table containing all mission objects.
|
|
||||||
|
|
||||||
-record(psu_object, {id, instancepid, type, args}).
|
|
||||||
|
|
||||||
%% @doc Hit response data.
|
%% @doc Hit response data.
|
||||||
|
|
||||||
-record(hit_response, {type, user, exp, damage, targethp, targetse, events}).
|
-record(hit_response, {type, user, exp, damage, targethp, targetse, events}).
|
6
apps/egs/rebar.config
Normal file
6
apps/egs/rebar.config
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{deps, [
|
||||||
|
{erlson, ".*", {git, "https://github.com/alavrik/erlson.git", "HEAD"}},
|
||||||
|
{ex_reloader, ".*", {git, "git://github.com/extend/ex_reloader.git", "HEAD"}},
|
||||||
|
{cowboy, ".*", {git, "git://github.com/extend/cowboy.git", "HEAD"}}
|
||||||
|
]}.
|
||||||
|
{plugins, [erlson_rebar_plugin]}.
|
16
apps/egs/src/egs.app.src
Normal file
16
apps/egs/src/egs.app.src
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
%%-*- mode: erlang -*-
|
||||||
|
{application, egs, [
|
||||||
|
{description, "EGS online action-RPG game server"},
|
||||||
|
{vsn, "0.14"},
|
||||||
|
{modules, []},
|
||||||
|
{registered, []},
|
||||||
|
{applications, [
|
||||||
|
kernel,
|
||||||
|
stdlib,
|
||||||
|
crypto,
|
||||||
|
ssl,
|
||||||
|
cowboy
|
||||||
|
]},
|
||||||
|
{mod, {egs_app, []}},
|
||||||
|
{env, []}
|
||||||
|
]}.
|
85
apps/egs/src/egs.erl
Normal file
85
apps/egs/src/egs.erl
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
|
%% @doc EGS startup code and utility functions.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs).
|
||||||
|
-export([start/0, stop/0, global/1, warp/4, warp/5]). %% API.
|
||||||
|
|
||||||
|
%% Player and account-related types.
|
||||||
|
|
||||||
|
-type gid() :: 0..16#ffffffff.
|
||||||
|
-type lid() :: 0..1023 | 16#ffff.
|
||||||
|
-type character_slot() :: 0..3.
|
||||||
|
-export_type([gid/0, lid/0, character_slot/0]).
|
||||||
|
|
||||||
|
%% Location related types.
|
||||||
|
|
||||||
|
-type uniid() :: 21 | 26..254 | 16#ffffffff.
|
||||||
|
-type questid() :: 0..16#ffffffff. %% @todo What's the real max?
|
||||||
|
-type zoneid() :: 0..16#ffff. %% @todo What's the real max?
|
||||||
|
-type mapid() :: 0..9999.
|
||||||
|
-type entryid() :: 0..16#ffff. %% @todo What's the real max?
|
||||||
|
-type area() :: {questid(), zoneid(), mapid()}. %% @todo Probably remove later.
|
||||||
|
-type position() :: {X::float(), Y::float(), Z::float(), Dir::float()}.
|
||||||
|
-export_type([uniid/0, questid/0, zoneid/0, mapid/0, entryid/0,
|
||||||
|
area/0, position/0]).
|
||||||
|
|
||||||
|
%% API.
|
||||||
|
|
||||||
|
-spec start() -> ok.
|
||||||
|
start() ->
|
||||||
|
ensure_started(crypto),
|
||||||
|
ensure_started(public_key),
|
||||||
|
ensure_started(ssl),
|
||||||
|
ensure_started(cowboy),
|
||||||
|
application:start(egs).
|
||||||
|
|
||||||
|
-spec stop() -> ok.
|
||||||
|
stop() ->
|
||||||
|
Res = application:stop(egs),
|
||||||
|
ok = application:stop(cowboy),
|
||||||
|
ok = application:stop(ssl),
|
||||||
|
ok = application:stop(public_key),
|
||||||
|
ok = application:stop(crypto),
|
||||||
|
Res.
|
||||||
|
|
||||||
|
%% @doc Send a global message.
|
||||||
|
-spec global(string()) -> ok.
|
||||||
|
global(Message) when length(Message) > 511 ->
|
||||||
|
io:format("global: message too long~n");
|
||||||
|
global(Message) ->
|
||||||
|
egs_users:broadcast_all({egs, notice, top, Message}).
|
||||||
|
|
||||||
|
%% @doc Warp all players to a new map.
|
||||||
|
-spec warp(questid(), zoneid(), mapid(), entryid()) -> ok.
|
||||||
|
warp(QuestID, ZoneID, MapID, EntryID) ->
|
||||||
|
egs_users:broadcast_all({egs, warp, QuestID, ZoneID, MapID, EntryID}).
|
||||||
|
|
||||||
|
%% @doc Warp one player to a new map.
|
||||||
|
-spec warp(gid(), questid(), zoneid(), mapid(), entryid()) -> ok.
|
||||||
|
warp(GID, QuestID, ZoneID, MapID, EntryID) ->
|
||||||
|
egs_users:broadcast({egs, warp, QuestID, ZoneID, MapID, EntryID}, [GID]).
|
||||||
|
|
||||||
|
%% Internal.
|
||||||
|
|
||||||
|
-spec ensure_started(module()) -> ok.
|
||||||
|
ensure_started(App) ->
|
||||||
|
case application:start(App) of
|
||||||
|
ok -> ok;
|
||||||
|
{error, {already_started, App}} -> ok
|
||||||
|
end.
|
143
apps/egs/src/egs_accounts.erl
Normal file
143
apps/egs/src/egs_accounts.erl
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
|
%% @doc Accounts handling.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_accounts).
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
-export([start_link/0, stop/0, get_folder/1, key_auth/2, key_auth_init/1, key_auth_timeout/1, login_auth/2, tmp_gid/0]). %% API.
|
||||||
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
||||||
|
|
||||||
|
-define(SERVER, ?MODULE).
|
||||||
|
|
||||||
|
%% @todo Make accounts permanent.
|
||||||
|
%% @todo Hash the password.
|
||||||
|
%% @todo Add email, password_salt, is_ingame, register_time, last_login_time, etc.
|
||||||
|
-record(accounts, {
|
||||||
|
gid :: egs:gid(),
|
||||||
|
username :: string(),
|
||||||
|
password :: string(),
|
||||||
|
auth_state :: undefined | {wait_for_authentication, binary(), any()}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-record(state, {
|
||||||
|
accounts = [] :: list({egs:gid(), #accounts{}}),
|
||||||
|
next_gid = 10000001 :: integer(),
|
||||||
|
tmp_gid = 16#ff000001 :: integer()
|
||||||
|
}).
|
||||||
|
|
||||||
|
%% API.
|
||||||
|
|
||||||
|
-spec start_link() -> {ok, Pid::pid()}.
|
||||||
|
start_link() ->
|
||||||
|
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||||||
|
|
||||||
|
-spec stop() -> stopped.
|
||||||
|
stop() ->
|
||||||
|
gen_server:call(?SERVER, stop).
|
||||||
|
|
||||||
|
%% @todo Temporary code until we properly save the player data.
|
||||||
|
get_folder(GID) ->
|
||||||
|
gen_server:call(?SERVER, {get_folder, GID}).
|
||||||
|
|
||||||
|
-spec key_auth(egs:gid(), AuthKey::binary()) -> ok | {error, badarg}.
|
||||||
|
%% @doc Authenticate using the given key.
|
||||||
|
key_auth(GID, AuthKey) ->
|
||||||
|
gen_server:call(?SERVER, {key_auth, GID, AuthKey}).
|
||||||
|
|
||||||
|
-spec key_auth_init(egs:gid()) -> {ok, AuthKey::binary()}.
|
||||||
|
%% @doc Initialize key authentication. Obtain a key for a subsequent re-authentication on a different connection.
|
||||||
|
key_auth_init(GID) ->
|
||||||
|
gen_server:call(?SERVER, {key_auth_init, GID}).
|
||||||
|
|
||||||
|
-spec key_auth_timeout(egs:gid()) -> ok.
|
||||||
|
%% @doc Key authentication timeout handling.
|
||||||
|
%% @todo Probably handle the authentication in a gen_fsm properly.
|
||||||
|
key_auth_timeout(GID) ->
|
||||||
|
gen_server:cast(?SERVER, {key_auth_timeout, GID}).
|
||||||
|
|
||||||
|
-spec login_auth(Username::binary(), Password::binary()) -> {ok, GID::integer()}.
|
||||||
|
%% @doc Authenticate using the given username and password.
|
||||||
|
%% @todo Properly handle login authentication when accounts are saved.
|
||||||
|
login_auth(Username, Password) ->
|
||||||
|
gen_server:call(?SERVER, {login_auth, Username, Password}).
|
||||||
|
|
||||||
|
-spec tmp_gid() -> egs:gid().
|
||||||
|
%% @doc Return an unused temporary GID for initial connection and APC characters.
|
||||||
|
tmp_gid() ->
|
||||||
|
gen_server:call(?SERVER, tmp_gid).
|
||||||
|
|
||||||
|
%% gen_server.
|
||||||
|
|
||||||
|
init([]) ->
|
||||||
|
{ok, #state{}}.
|
||||||
|
|
||||||
|
handle_call({get_folder, GID}, _From, State) ->
|
||||||
|
{_, #accounts{username=Username, password=Password}} = lists:keyfind(GID, 1, State#state.accounts),
|
||||||
|
{reply, << Username/binary, "-", Password/binary >>, State};
|
||||||
|
|
||||||
|
handle_call({key_auth, GID, AuthKey}, _From, State) ->
|
||||||
|
{_, Account = #accounts{auth_state=AuthState}} = lists:keyfind(GID, 1, State#state.accounts),
|
||||||
|
case AuthState of
|
||||||
|
{wait_for_authentication, AuthKey, TRef} ->
|
||||||
|
timer:cancel(TRef),
|
||||||
|
Accounts = lists:delete({GID, Account}, State#state.accounts),
|
||||||
|
{reply, ok, State#state{accounts=[{GID, Account#accounts{auth_state=undefined}}|Accounts]}};
|
||||||
|
undefined ->
|
||||||
|
{reply, {error, badarg}, State}
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_call({key_auth_init, GID}, _From, State) ->
|
||||||
|
AuthKey = crypto:rand_bytes(4),
|
||||||
|
TRef = timer:apply_after(10000, ?MODULE, key_auth_timeout, [GID]),
|
||||||
|
{_, Account} = lists:keyfind(GID, 1, State#state.accounts),
|
||||||
|
Accounts = lists:delete({GID, Account}, State#state.accounts),
|
||||||
|
{reply, {ok, AuthKey}, State#state{accounts=
|
||||||
|
[{GID, Account#accounts{auth_state={wait_for_authentication, AuthKey, TRef}}}|Accounts]}};
|
||||||
|
|
||||||
|
handle_call({login_auth, Username, Password}, _From, State) ->
|
||||||
|
GID = State#state.next_gid,
|
||||||
|
Account = #accounts{gid=GID, username=Username, password=Password},
|
||||||
|
{reply, {ok, GID}, State#state{next_gid=GID + 1, accounts=[{GID, Account}|State#state.accounts]}};
|
||||||
|
|
||||||
|
handle_call(tmp_gid, _From, State) ->
|
||||||
|
GID = State#state.tmp_gid,
|
||||||
|
{reply, GID, State#state{tmp_gid=GID + 1}};
|
||||||
|
|
||||||
|
handle_call(stop, _From, State) ->
|
||||||
|
{stop, normal, stopped, State};
|
||||||
|
|
||||||
|
handle_call(_Request, _From, State) ->
|
||||||
|
{reply, ignored, State}.
|
||||||
|
|
||||||
|
handle_cast({key_auth_timeout, GID}, State) ->
|
||||||
|
{_, Account} = lists:keyfind(GID, 1, State#state.accounts),
|
||||||
|
Accounts = lists:delete({GID, Account}, State#state.accounts),
|
||||||
|
{noreply, State#state{accounts= [{GID, Account#accounts{auth_state=undefined}}|Accounts]}};
|
||||||
|
|
||||||
|
handle_cast(_Msg, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
handle_info(_Info, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
terminate(_Reason, _State) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
code_change(_OldVsn, State, _Extra) ->
|
||||||
|
{ok, State}.
|
58
apps/egs/src/egs_app.erl
Normal file
58
apps/egs/src/egs_app.erl
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
|
%% @doc Callbacks for the egs application.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_app).
|
||||||
|
-behaviour(application).
|
||||||
|
-export([start/2, stop/1]). %% API.
|
||||||
|
|
||||||
|
-type application_start_type()
|
||||||
|
:: normal | {takeover, node()} | {failover, node()}.
|
||||||
|
|
||||||
|
-define(SSL_OPTIONS, [{certfile, "priv/ssl/servercert.pem"},
|
||||||
|
{keyfile, "priv/ssl/serverkey.pem"}, {password, "alpha"}]).
|
||||||
|
|
||||||
|
%% API.
|
||||||
|
|
||||||
|
-spec start(application_start_type(), term()) -> {ok, pid()}.
|
||||||
|
start(_Type, _StartArgs) ->
|
||||||
|
{ok, Pid} = egs_sup:start_link(),
|
||||||
|
application:set_env(egs_patch, patch_ports, egs_conf:read(patch_ports)),
|
||||||
|
application:start(egs_patch),
|
||||||
|
application:start(egs_store),
|
||||||
|
start_login_listeners(egs_conf:read(login_ports)),
|
||||||
|
{_ServerIP, GamePort} = egs_conf:read(game_server),
|
||||||
|
{ok, _GamePid} = cowboy:start_listener({game, GamePort}, 10,
|
||||||
|
cowboy_ssl_transport, [{port, GamePort}] ++ ?SSL_OPTIONS,
|
||||||
|
egs_game_protocol, []),
|
||||||
|
{ok, Pid}.
|
||||||
|
|
||||||
|
-spec stop(term()) -> ok.
|
||||||
|
stop(_State) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% Internal.
|
||||||
|
|
||||||
|
-spec start_login_listeners([inet:ip_port()]) -> ok.
|
||||||
|
start_login_listeners([]) ->
|
||||||
|
ok;
|
||||||
|
start_login_listeners([Port|Tail]) ->
|
||||||
|
{ok, _Pid} = cowboy:start_listener({login, Port}, 10,
|
||||||
|
cowboy_ssl_transport, [{port, Port}] ++ ?SSL_OPTIONS,
|
||||||
|
egs_login_protocol, []),
|
||||||
|
start_login_listeners(Tail).
|
64
apps/egs/src/egs_char.erl
Normal file
64
apps/egs/src/egs_char.erl
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
-module(egs_char).
|
||||||
|
|
||||||
|
-export([new/6]).
|
||||||
|
|
||||||
|
-include_lib("erlson/include/erlson.hrl").
|
||||||
|
|
||||||
|
%% @todo Add the current location for backtopreviousfield
|
||||||
|
new(Slot, Name, Race, Gender, Class, Appearance) ->
|
||||||
|
#{
|
||||||
|
type=player,
|
||||||
|
|
||||||
|
slot=Slot,
|
||||||
|
name=Name,
|
||||||
|
race=Race,
|
||||||
|
gender=Gender,
|
||||||
|
class=Class,
|
||||||
|
appearance=Appearance,
|
||||||
|
|
||||||
|
level=1,
|
||||||
|
exp=0,
|
||||||
|
|
||||||
|
hunter_level=1,
|
||||||
|
hunter_exp=0,
|
||||||
|
ranger_level=1,
|
||||||
|
ranger_exp=0,
|
||||||
|
force_level=1,
|
||||||
|
force_exp=0,
|
||||||
|
acro_level=1,
|
||||||
|
acro_level=0,
|
||||||
|
|
||||||
|
blast_bar=0,
|
||||||
|
luck=3,
|
||||||
|
playtime=0,
|
||||||
|
|
||||||
|
%% current_uni,
|
||||||
|
%% current location + entryid
|
||||||
|
%% previous location + entryid
|
||||||
|
%% pids for such
|
||||||
|
|
||||||
|
money=1000000,
|
||||||
|
inventory=[],
|
||||||
|
|
||||||
|
card_comment= <<>>,
|
||||||
|
|
||||||
|
options=#{
|
||||||
|
brightness=4,
|
||||||
|
buttonhelp=0,
|
||||||
|
cam1stx=0,
|
||||||
|
cam1sty=0,
|
||||||
|
cam3rdx=0,
|
||||||
|
cam3rdy=0,
|
||||||
|
controller=0,
|
||||||
|
cursorpos=0,
|
||||||
|
cutin=0,
|
||||||
|
fnkeys=0,
|
||||||
|
lockon=0,
|
||||||
|
musicvolume=0,
|
||||||
|
radarmap=0,
|
||||||
|
sfxvolume=0,
|
||||||
|
sound=0,
|
||||||
|
textspeed=0,
|
||||||
|
vibration=0,
|
||||||
|
weaponswap=0
|
||||||
|
}}.
|
86
apps/egs/src/egs_char_select.erl
Normal file
86
apps/egs/src/egs_char_select.erl
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
|
%% @doc Character selection callback module.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_char_select).
|
||||||
|
-export([info/2, cast/3, event/2]).
|
||||||
|
|
||||||
|
-include_lib("erlson/include/erlson.hrl").
|
||||||
|
-include("include/records.hrl").
|
||||||
|
|
||||||
|
%% @doc We don't expect any message here.
|
||||||
|
info(_Msg, _Client) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% @doc Nothing to broadcast.
|
||||||
|
cast(_Command, _Data, _Client) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% Events.
|
||||||
|
|
||||||
|
%% @doc Character screen selection request and delivery.
|
||||||
|
event(account_characters_request, Client) ->
|
||||||
|
{ok, Characters}
|
||||||
|
= egs_store:load_characters(egs_net:get_gid(Client), [0, 1, 2, 3]),
|
||||||
|
egs_net:account_characters_response(Characters, Client);
|
||||||
|
|
||||||
|
%% @todo Don't forget to check for the character's name (in egs_net).
|
||||||
|
%% 00F7 is the RGBA color control character.
|
||||||
|
%% 03F7 is the RGB color control character.
|
||||||
|
event({account_create_character, Slot, Name, Race, Gender, Class, Appearance},
|
||||||
|
Client) ->
|
||||||
|
Character = egs_char:new(Slot, Name, Race, Gender, Class, Appearance),
|
||||||
|
egs_store:save_character(egs_net:get_gid(Client), Slot, Character);
|
||||||
|
|
||||||
|
%% @doc Load the selected character into the game's default universe.
|
||||||
|
%% @todo Isn't very pretty to call egs_game from here but that will do for now.
|
||||||
|
event({system_character_select, Slot, _BackToPreviousField}, Client) ->
|
||||||
|
GID = egs_net:get_gid(Client),
|
||||||
|
{ok, 1, Char} = egs_store:load_character(GID, Slot),
|
||||||
|
UniID = egs_universes:defaultid(),
|
||||||
|
egs_universes:enter(UniID),
|
||||||
|
%% @todo Handle users properly, just giving Character directly.
|
||||||
|
Name = Char.name,
|
||||||
|
NameBin = << Name/binary, 0:(512 - bit_size(Name)) >>,
|
||||||
|
Race = Char.race,
|
||||||
|
Gender = Char.gender,
|
||||||
|
Class = Char.class,
|
||||||
|
Appearance = Char.appearance,
|
||||||
|
Options = Char.options,
|
||||||
|
User = #users{gid=GID, pid=self(), uni=UniID, slot=Slot,
|
||||||
|
name=NameBin, race=Race, gender=Gender, class=Class,
|
||||||
|
appearance=Appearance, options=Options,
|
||||||
|
area={1100000, 0, 4}, entryid=0, char=Char},
|
||||||
|
egs_users:write(User),
|
||||||
|
egs_users:item_add(GID, 16#11010000, #psu_special_item_variables{}),
|
||||||
|
egs_users:item_add(GID, 16#11020000, #psu_special_item_variables{}),
|
||||||
|
egs_users:item_add(GID, 16#11020100, #psu_special_item_variables{}),
|
||||||
|
egs_users:item_add(GID, 16#11020200, #psu_special_item_variables{}),
|
||||||
|
egs_users:item_add(GID, 16#01010900, #psu_striking_weapon_item_variables{
|
||||||
|
current_pp=99, max_pp=100, element=#psu_element{type=1, percent=50}}),
|
||||||
|
egs_users:item_add(GID, 16#01010a00, #psu_striking_weapon_item_variables{
|
||||||
|
current_pp=99, max_pp=100, element=#psu_element{type=2, percent=50}}),
|
||||||
|
egs_users:item_add(GID, 16#01010b00, #psu_striking_weapon_item_variables{
|
||||||
|
current_pp=99, max_pp=100, element=#psu_element{type=3, percent=50}}),
|
||||||
|
{ok, User2} = egs_users:read(GID),
|
||||||
|
egs_game:char_load(User2, Client),
|
||||||
|
Client2 = egs_net:set_handler(egs_game, Client),
|
||||||
|
{ok, Client2};
|
||||||
|
|
||||||
|
event({client_hardware, GPU, CPU}, Client) ->
|
||||||
|
ok.
|
@ -1,5 +1,5 @@
|
|||||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
%% @copyright 2010 Loïc Hoguin.
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
%% @doc EGS configuration gen_server.
|
%% @doc EGS configuration gen_server.
|
||||||
%%
|
%%
|
||||||
%% This file is part of EGS.
|
%% This file is part of EGS.
|
||||||
@ -19,41 +19,35 @@
|
|||||||
|
|
||||||
-module(egs_conf).
|
-module(egs_conf).
|
||||||
-behavior(gen_server).
|
-behavior(gen_server).
|
||||||
-export([start_link/0, stop/0, read/1, reload/0]). %% API.
|
|
||||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
|
||||||
|
|
||||||
%% Use the module name for the server's name.
|
-export([start_link/0, stop/0, read/1, reload/0]). %% API.
|
||||||
|
-export([init/1, handle_call/3, handle_cast/2,
|
||||||
|
handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
||||||
|
|
||||||
-define(SERVER, ?MODULE).
|
-define(SERVER, ?MODULE).
|
||||||
|
|
||||||
%% API.
|
%% API.
|
||||||
|
|
||||||
%% @spec start_link() -> {ok,Pid::pid()}
|
-spec start_link() -> {ok, pid()}.
|
||||||
start_link() ->
|
start_link() ->
|
||||||
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||||||
|
|
||||||
%% @spec stop() -> stopped
|
-spec stop() -> stopped.
|
||||||
stop() ->
|
stop() ->
|
||||||
gen_server:call(?SERVER, stop).
|
gen_server:call(?SERVER, stop).
|
||||||
|
|
||||||
%% @spec read(Key) -> Value | undefined
|
-spec read(atom()) -> undefined | any().
|
||||||
read(Key) ->
|
read(Key) ->
|
||||||
gen_server:call(?SERVER, {read, Key}).
|
gen_server:call(?SERVER, {read, Key}).
|
||||||
|
|
||||||
%% @spec reload() -> ok
|
-spec reload() -> ok.
|
||||||
reload() ->
|
reload() ->
|
||||||
gen_server:cast(?SERVER, reload).
|
gen_server:cast(?SERVER, reload).
|
||||||
|
|
||||||
%% gen_server.
|
%% gen_server.
|
||||||
|
|
||||||
init([]) ->
|
init([]) ->
|
||||||
case file:consult("priv/egs.conf") of
|
{ok, _Terms} = file:consult("priv/egs.conf").
|
||||||
{ok, Terms} ->
|
|
||||||
error_logger:info_report("egs_conf started"),
|
|
||||||
{ok, Terms};
|
|
||||||
Error ->
|
|
||||||
error_logger:error_report(["An error occurred when trying to load the configuration file:", Error]),
|
|
||||||
Error
|
|
||||||
end.
|
|
||||||
|
|
||||||
handle_call({read, Key}, _From, State) ->
|
handle_call({read, Key}, _From, State) ->
|
||||||
{reply, proplists:get_value(Key, State), State};
|
{reply, proplists:get_value(Key, State), State};
|
106
apps/egs/src/egs_counters_db.erl
Normal file
106
apps/egs/src/egs_counters_db.erl
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
|
%% @doc EGS counters database and cache manager.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_counters_db).
|
||||||
|
-behavior(gen_server).
|
||||||
|
|
||||||
|
-export([start_link/0, stop/0, bg/1, opts/1, pack/1, reload/0]). %% API.
|
||||||
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
||||||
|
|
||||||
|
%% Use the module name for the server's name.
|
||||||
|
-define(SERVER, ?MODULE).
|
||||||
|
|
||||||
|
%% API.
|
||||||
|
|
||||||
|
%% @spec start_link() -> {ok,Pid::pid()}
|
||||||
|
start_link() ->
|
||||||
|
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||||||
|
|
||||||
|
%% @spec stop() -> stopped
|
||||||
|
stop() ->
|
||||||
|
gen_server:call(?SERVER, stop).
|
||||||
|
|
||||||
|
%% @spec bg(CounterID) -> integer()
|
||||||
|
bg(CounterID) ->
|
||||||
|
gen_server:call(?SERVER, {bg, CounterID}).
|
||||||
|
|
||||||
|
%% @spec opts(CounterID) -> binary()
|
||||||
|
opts(CounterID) ->
|
||||||
|
gen_server:call(?SERVER, {opts, CounterID}).
|
||||||
|
|
||||||
|
%% @spec pack(CounterID) -> binary()
|
||||||
|
pack(CounterID) ->
|
||||||
|
gen_server:call(?SERVER, {pack, CounterID}).
|
||||||
|
|
||||||
|
%% @spec reload() -> ok
|
||||||
|
reload() ->
|
||||||
|
gen_server:cast(?SERVER, reload).
|
||||||
|
|
||||||
|
%% gen_server.
|
||||||
|
|
||||||
|
init([]) ->
|
||||||
|
{ok, []}.
|
||||||
|
|
||||||
|
%% @doc Possible keys: bg, opts, pack.
|
||||||
|
handle_call({Key, CounterID}, _From, State) ->
|
||||||
|
{Counter, State2} = get_counter(CounterID, State),
|
||||||
|
{reply, proplists:get_value(Key, Counter), State2};
|
||||||
|
|
||||||
|
handle_call(stop, _From, State) ->
|
||||||
|
{stop, normal, stopped, State};
|
||||||
|
|
||||||
|
handle_call(_Request, _From, State) ->
|
||||||
|
{reply, ignored, State}.
|
||||||
|
|
||||||
|
handle_cast(reload, _State) ->
|
||||||
|
{noreply, []};
|
||||||
|
|
||||||
|
handle_cast(_Msg, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
handle_info(_Info, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
terminate(_Reason, _State) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
code_change(_OldVsn, State, _Extra) ->
|
||||||
|
{ok, State}.
|
||||||
|
|
||||||
|
%% Internal.
|
||||||
|
|
||||||
|
%% @doc Return a counter information either from the cache or from the configuration file,
|
||||||
|
%% in which case it gets added to the cache for subsequent attempts.
|
||||||
|
get_counter(CounterID, Cache) ->
|
||||||
|
case proplists:get_value(CounterID, Cache) of
|
||||||
|
undefined ->
|
||||||
|
Dir = io_lib:format("priv/counters/~b/", [CounterID]),
|
||||||
|
ConfFilename = Dir ++ "counter.conf",
|
||||||
|
{TableRelData, TableRelPtrs} = egs_files:load_table_rel(ConfFilename),
|
||||||
|
TextBinData = egs_files:load_text_bin(Dir ++ "text.bin.en_US.txt"),
|
||||||
|
CounterNbl = egs_files:nbl_pack([{files, [
|
||||||
|
{data, "table.rel", TableRelData, TableRelPtrs},
|
||||||
|
{data, "text.bin", TextBinData, []}
|
||||||
|
]}]),
|
||||||
|
Counter = egs_files:load_counter_pack(ConfFilename, CounterNbl),
|
||||||
|
Cache2 = [{CounterID, Counter}|Cache],
|
||||||
|
{Counter, Cache2};
|
||||||
|
Counter ->
|
||||||
|
{Counter, Cache}
|
||||||
|
end.
|
559
apps/egs/src/egs_files.erl
Normal file
559
apps/egs/src/egs_files.erl
Normal file
@ -0,0 +1,559 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
|
%% @doc EGS file creation functions.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_files).
|
||||||
|
-export([load_counter_pack/2, load_quest_xnr/1, load_script_bin/1, load_set_rel/4, load_table_rel/1,
|
||||||
|
load_text_bin/1, load_unit_title_table_rel/2, nbl_pack/1, nbl_padded_size/1]).
|
||||||
|
|
||||||
|
%% @doc Build a counter's pack file, options and return them along with the background value.
|
||||||
|
load_counter_pack(ConfFilename, CounterNbl) ->
|
||||||
|
{ok, Settings} = file:consult(ConfFilename),
|
||||||
|
Groups = proplists:get_value(groups, Settings),
|
||||||
|
{FilesBin, PosList, SizeList} = load_counter_pack_groups(Groups, [0], [byte_size(CounterNbl)]),
|
||||||
|
NbFiles = length(PosList),
|
||||||
|
PosBin = iolist_to_binary([<< P:32/little >> || P <- PosList]),
|
||||||
|
SizeBin = iolist_to_binary([<< S:32/little >> || S <- SizeList]),
|
||||||
|
Padding = 8 * (512 - byte_size(PosBin)),
|
||||||
|
Pack = << PosBin/binary, 0:Padding, SizeBin/binary, 0:Padding, NbFiles:32/little, CounterNbl/binary, FilesBin/binary >>,
|
||||||
|
Opts = case length(Groups) of
|
||||||
|
0 -> << >>;
|
||||||
|
L ->
|
||||||
|
OptsList = lists:seq(1, L + length(PosList)),
|
||||||
|
iolist_to_binary([<< 3:8 >> || _N <- OptsList])
|
||||||
|
end,
|
||||||
|
Opts2 = if byte_size(Opts) rem 2 =:= 0 -> Opts; true -> << Opts/binary, 0 >> end,
|
||||||
|
[{bg, proplists:get_value(bg, Settings, 255)}, {opts, Opts2}, {pack, Pack}].
|
||||||
|
|
||||||
|
load_counter_pack_groups(Groups, PosList, SizeList) ->
|
||||||
|
load_counter_pack_groups(Groups, PosList, SizeList, []).
|
||||||
|
load_counter_pack_groups([], PosList, SizeList, Acc) ->
|
||||||
|
GroupsBin = iolist_to_binary(lists:reverse(Acc)),
|
||||||
|
{GroupsBin, lists:reverse(PosList), lists:reverse(SizeList)};
|
||||||
|
load_counter_pack_groups([Group|Tail], PosList, SizeList, Acc) ->
|
||||||
|
Quests = proplists:get_value(quests, Group),
|
||||||
|
{QuestsBin, PosList2, SizeList2} = load_counter_pack_quests(Quests, PosList, SizeList),
|
||||||
|
load_counter_pack_groups(Tail, PosList2, SizeList2, [QuestsBin|Acc]).
|
||||||
|
|
||||||
|
load_counter_pack_quests(Quests, PosList, SizeList) ->
|
||||||
|
load_counter_pack_quests(Quests, PosList, SizeList, []).
|
||||||
|
load_counter_pack_quests([], PosList, SizeList, Acc) ->
|
||||||
|
QuestsBin = iolist_to_binary(lists:reverse(Acc)),
|
||||||
|
{QuestsBin, PosList, SizeList};
|
||||||
|
load_counter_pack_quests([{_QuestID, Filename}|Tail], PosList, SizeList, Acc) ->
|
||||||
|
{ok, File} = file:read_file("data/missions/" ++ Filename),
|
||||||
|
Pos = lists:sum(SizeList),
|
||||||
|
Size = byte_size(File),
|
||||||
|
load_counter_pack_quests(Tail, [Pos|PosList], [Size|SizeList], [File|Acc]).
|
||||||
|
|
||||||
|
%% @doc Load a quest configuration file and return a quest.xnr binary along with its pointers array.
|
||||||
|
load_quest_xnr(Settings) ->
|
||||||
|
QuestID = proplists:get_value(questid, Settings),
|
||||||
|
%% Temp flags.
|
||||||
|
TmpFlagsList = proplists:get_value(temp_flags, Settings),
|
||||||
|
TmpFlagsBin = load_quest_xnr_flags(TmpFlagsList),
|
||||||
|
%% Value flags.
|
||||||
|
ValFlagsList = proplists:get_value(value_flags, Settings),
|
||||||
|
ValFlagsBin = load_quest_xnr_flags(ValFlagsList),
|
||||||
|
%% Bool flags.
|
||||||
|
BoolFlagsList = proplists:get_value(bool_flags, Settings),
|
||||||
|
BoolFlagsBin = load_quest_xnr_flags(BoolFlagsList),
|
||||||
|
%% Items.
|
||||||
|
ItemsPos = 16 + byte_size(TmpFlagsBin) + byte_size(ValFlagsBin) + byte_size(BoolFlagsBin),
|
||||||
|
ItemsList = proplists:get_value(items, Settings),
|
||||||
|
ItemsBin = load_quest_xnr_items(ItemsList),
|
||||||
|
NbItems = length(ItemsList),
|
||||||
|
%% Item pointers.
|
||||||
|
ItemsPtrsPos = ItemsPos + byte_size(ItemsBin),
|
||||||
|
ItemsPtrs = << ItemsPos:32/little, NbItems:32/little >>,
|
||||||
|
%% Zones.
|
||||||
|
ZonesBasePos = ItemsPtrsPos + byte_size(ItemsPtrs),
|
||||||
|
ZonesList = proplists:get_value(zones, Settings),
|
||||||
|
{SetsBin, SetsPtrsList, ZonesBin} = load_quest_xnr_zones(ZonesList, ZonesBasePos),
|
||||||
|
ZonesPos = ZonesBasePos + byte_size(SetsBin),
|
||||||
|
NbZones = length(ZonesList),
|
||||||
|
%% Warps.
|
||||||
|
WarpsPos = ZonesPos + byte_size(ZonesBin),
|
||||||
|
WarpsList = proplists:get_value(warps, Settings),
|
||||||
|
WarpsBin = load_quest_xnr_warps(QuestID, WarpsList),
|
||||||
|
NbWarps = length(WarpsList),
|
||||||
|
%% Temp flag pointers.
|
||||||
|
TmpFlagsPtrsPos = WarpsPos + byte_size(WarpsBin),
|
||||||
|
{NbTmpFlags, TmpFlagsPtrs} = load_quest_xnr_flag_ptrs(TmpFlagsList, 0),
|
||||||
|
TmpFlagsPtrsPos2 = if NbTmpFlags =/= 0 -> TmpFlagsPtrsPos; true -> 0 end,
|
||||||
|
%% Value flag pointers.
|
||||||
|
ValFlagsPtrsPos = TmpFlagsPtrsPos + byte_size(TmpFlagsPtrs),
|
||||||
|
{NbValFlags, ValFlagsPtrs} = load_quest_xnr_flag_ptrs(ValFlagsList, NbTmpFlags),
|
||||||
|
ValFlagsPtrsPos2 = if NbValFlags =/= 0 -> ValFlagsPtrsPos; true -> 0 end,
|
||||||
|
%% Bool flag pointers.
|
||||||
|
BoolFlagsPtrsPos = ValFlagsPtrsPos + byte_size(ValFlagsPtrs),
|
||||||
|
{NbBoolFlags, BoolFlagsPtrs} = load_quest_xnr_flag_ptrs(BoolFlagsList, NbTmpFlags + NbValFlags),
|
||||||
|
BoolFlagsPtrsPos2 = if NbBoolFlags =/= 0 -> BoolFlagsPtrsPos; true -> 0 end,
|
||||||
|
%% Main pointers.
|
||||||
|
MainPos = BoolFlagsPtrsPos + byte_size(BoolFlagsPtrs),
|
||||||
|
NbNPCs = 0, %% @todo
|
||||||
|
NPCsPos = MainPos + 260, %% @todo if NbNPCs =/= 0 -> todo; true -> MainPos + 260 end,
|
||||||
|
NPCsBin = << 0:64 >>,
|
||||||
|
%% Main options.
|
||||||
|
EnterWarp = load_quest_xnr_warp(QuestID, proplists:get_value(enter_warp, Settings)),
|
||||||
|
ExitWarp = load_quest_xnr_warp(QuestID, proplists:get_value(exit_warp, Settings)),
|
||||||
|
FailWarp = load_quest_xnr_warp(QuestID, proplists:get_value(fail_warp, Settings)),
|
||||||
|
MissionOpts = 0, %% @todo
|
||||||
|
NbCustomNPCs = 0, %% @todo
|
||||||
|
Icon = proplists:get_value(icon, Settings),
|
||||||
|
{PartySizeMin, PartySizeMax} = proplists:get_value(party_size, Settings),
|
||||||
|
{CursorX, CursorY} = proplists:get_value(cursor, Settings),
|
||||||
|
UnixTime = calendar:datetime_to_gregorian_seconds(calendar:now_to_universal_time(now()))
|
||||||
|
- calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}),
|
||||||
|
MainBin = << 16#16000600:32, UnixTime:32/little, QuestID:32/little, ZonesPos:32/little, NbZones:32/little,
|
||||||
|
WarpsPos:32/little, NbWarps:32/little, EnterWarp/binary, ExitWarp/binary, MissionOpts:8, 16#040200:24, 0:288,
|
||||||
|
Icon:16/little, 0:16, PartySizeMin:8, PartySizeMax:8, CursorX:16/little, CursorY:16/little, 0:16, ItemsPtrsPos:32/little,
|
||||||
|
FailWarp/binary, 0:160, TmpFlagsPtrsPos2:32/little, ValFlagsPtrsPos2:32/little, BoolFlagsPtrsPos2:32/little, NbTmpFlags:8,
|
||||||
|
NbValFlags:8, NbBoolFlags:8, NbNPCs:8, NPCsPos:32/little, 16#00000100:32, 16#00000100:32, NbCustomNPCs:32/little, 0:704 >>,
|
||||||
|
%% Wrapping it up.
|
||||||
|
Data = << MainPos:32/little, 0:32, TmpFlagsBin/binary, ValFlagsBin/binary, BoolFlagsBin/binary,
|
||||||
|
ItemsBin/binary, ItemsPtrs/binary, SetsBin/binary, ZonesBin/binary, WarpsBin/binary,
|
||||||
|
TmpFlagsPtrs/binary, ValFlagsPtrs/binary, BoolFlagsPtrs/binary, MainBin/binary, NPCsBin/binary >>,
|
||||||
|
Size = 8 + byte_size(Data),
|
||||||
|
Data2 = << $N, $X, $R, 0, Size:32/little, Data/binary >>,
|
||||||
|
%% Calculate the pointers and return.
|
||||||
|
L0 = [ItemsPtrsPos] ++ SetsPtrsList,
|
||||||
|
L1 = L0 ++ lists:seq(ZonesPos + 16, ZonesPos + 16 + 64 * (NbZones - 1), 64),
|
||||||
|
L2 = L1 ++ lists:seq(TmpFlagsPtrsPos, TmpFlagsPtrsPos + 4 * (NbTmpFlags + NbValFlags + NbBoolFlags - 1), 4),
|
||||||
|
L3 = L2 ++ [MainPos + 12, MainPos + 20, MainPos + 104],
|
||||||
|
L4 = if TmpFlagsPtrsPos2 =/= 0 -> L3 ++ [MainPos + 140]; true -> L3 end,
|
||||||
|
L5 = if ValFlagsPtrsPos2 =/= 0 -> L4 ++ [MainPos + 144]; true -> L4 end,
|
||||||
|
L6 = if BoolFlagsPtrsPos2 =/= 0 -> L5 ++ [MainPos + 148]; true -> L5 end,
|
||||||
|
{Data2, L6 ++ [MainPos + 156]}.
|
||||||
|
|
||||||
|
load_quest_xnr_flag_ptrs([], _N) ->
|
||||||
|
{0, << >>};
|
||||||
|
load_quest_xnr_flag_ptrs(FlagsList, N) ->
|
||||||
|
NbFlags = length(FlagsList),
|
||||||
|
L1 = lists:seq(16 + N * 16, 16 + N * 16 + (NbFlags - 1) * 16, 16),
|
||||||
|
L2 = [<< X:32/little >> || X <- L1],
|
||||||
|
{NbFlags, iolist_to_binary(L2)}.
|
||||||
|
|
||||||
|
load_quest_xnr_flags(FlagsList) ->
|
||||||
|
load_quest_xnr_flags(FlagsList, []).
|
||||||
|
load_quest_xnr_flags([], Acc) ->
|
||||||
|
iolist_to_binary(lists:reverse(Acc));
|
||||||
|
load_quest_xnr_flags([Flag|Tail], Acc) ->
|
||||||
|
L = length(Flag),
|
||||||
|
Padding = 8 * (16 - L),
|
||||||
|
FlagBin = list_to_binary(Flag),
|
||||||
|
Bin = << FlagBin/binary, 0:Padding >>,
|
||||||
|
load_quest_xnr_flags(Tail, [Bin|Acc]).
|
||||||
|
|
||||||
|
load_quest_xnr_items(ItemsList) ->
|
||||||
|
load_quest_xnr_items(ItemsList, []).
|
||||||
|
load_quest_xnr_items([], Acc) ->
|
||||||
|
iolist_to_binary(lists:reverse(Acc));
|
||||||
|
load_quest_xnr_items([Item|Tail], Acc) ->
|
||||||
|
Index = proplists:get_value(index, Item),
|
||||||
|
ItemID = proplists:get_value(itemid, Item),
|
||||||
|
NbItems = proplists:get_value(nb_items, Item),
|
||||||
|
Type = proplists:get_value(type, Item),
|
||||||
|
Money = proplists:get_value(money, Item),
|
||||||
|
Bin = << Index:8, ItemID:32, 0:16, NbItems:8, Type:32/little, Money:32/little >>,
|
||||||
|
load_quest_xnr_items(Tail, [Bin|Acc]).
|
||||||
|
|
||||||
|
load_quest_xnr_warp(QuestID, {WarpQuestID, WarpZoneID, WarpMapID, WarpEntryID}) ->
|
||||||
|
WarpQuestID2 = if WarpQuestID =:= QuestID -> 16#ffffffff; true -> WarpQuestID end,
|
||||||
|
<< WarpQuestID2:32/little, WarpZoneID:16/little, WarpMapID:16/little, WarpEntryID:16/little, 0:16 >>.
|
||||||
|
|
||||||
|
load_quest_xnr_warps(QuestID, WarpsList) ->
|
||||||
|
load_quest_xnr_warps(QuestID, WarpsList, []).
|
||||||
|
load_quest_xnr_warps(_QuestID, [], Acc) ->
|
||||||
|
iolist_to_binary(lists:reverse(Acc));
|
||||||
|
load_quest_xnr_warps(QuestID, [Warp|Tail], Acc) ->
|
||||||
|
{CurrentWarp, NextWarp} = Warp,
|
||||||
|
Bin1 = load_quest_xnr_warp(QuestID, CurrentWarp),
|
||||||
|
Bin2 = load_quest_xnr_warp(QuestID, NextWarp),
|
||||||
|
load_quest_xnr_warps(QuestID, Tail, [<< Bin1/binary, Bin2/binary >>|Acc]).
|
||||||
|
|
||||||
|
%% @todo Counter(16#7fffffff) has ffff before EnemyLevel, why?
|
||||||
|
%% @todo Spaceport(1104000) and counter(16#7fffffff) has 04010000 be 00010000, why?
|
||||||
|
load_quest_xnr_zones(ZonesList, BasePos) ->
|
||||||
|
load_quest_xnr_zones(ZonesList, BasePos, [], [], []).
|
||||||
|
load_quest_xnr_zones([], _BasePos, SetsAcc, SetsPtrsAcc, ZonesAcc) ->
|
||||||
|
SetsBin = iolist_to_binary(lists:reverse(SetsAcc)),
|
||||||
|
SetsPtrsList = lists:flatten(lists:reverse(SetsPtrsAcc)),
|
||||||
|
ZonesBin = iolist_to_binary(lists:reverse(ZonesAcc)),
|
||||||
|
{SetsBin, SetsPtrsList, ZonesBin};
|
||||||
|
load_quest_xnr_zones([{ZoneID, ZoneParams}|Tail], BasePos, SetsAcc, SetsPtrsAcc, ZonesAcc) ->
|
||||||
|
AreaID = proplists:get_value(areaid, ZoneParams),
|
||||||
|
EnemyLevel = proplists:get_value(enemy_level, ZoneParams),
|
||||||
|
SetList = proplists:get_value(sets, ZoneParams),
|
||||||
|
NbSets = length(SetList),
|
||||||
|
SetsBin = iolist_to_binary([<< Set:32/little >> || Set <- SetList]),
|
||||||
|
SetsBin2 = << SetsBin/binary, BasePos:32/little, NbSets:32/little >>,
|
||||||
|
SetPos = BasePos + NbSets * 4,
|
||||||
|
ZoneBin = << ZoneID:16/little, AreaID:16/little, AreaID:32/little, 0:16, EnemyLevel:8, 16#ff:8, 16#04010000:32, SetPos:32/little, 0:352 >>,
|
||||||
|
load_quest_xnr_zones(Tail, BasePos + byte_size(SetsBin2), [SetsBin2|SetsAcc], [SetPos|SetsPtrsAcc], [ZoneBin|ZonesAcc]).
|
||||||
|
|
||||||
|
%% @doc Load a script file and compile it into the bytecode used by the game.
|
||||||
|
load_script_bin(ScriptFilename) ->
|
||||||
|
{ok, Script} = file:read_file(ScriptFilename),
|
||||||
|
{ok, Tokens, _NbLines} = egs_script_lexer:string(binary_to_list(Script)),
|
||||||
|
{ok, ParseTree} = egs_script_parser:parse(Tokens),
|
||||||
|
egs_script_compiler:compile(ParseTree).
|
||||||
|
|
||||||
|
%% @doc Load a set configuration file and return a set_r*.rel binary.
|
||||||
|
load_set_rel(ConfFilename, AreaID, Maps, FilePos) ->
|
||||||
|
{ok, Settings} = file:consult(ConfFilename),
|
||||||
|
NbMaps = length(Maps),
|
||||||
|
AreaPtr = FilePos + 16,
|
||||||
|
MapsPtr = FilePos + 24,
|
||||||
|
{MapsBin, Ptrs} = load_set_rel_maps(Settings, Maps, MapsPtr, MapsPtr + NbMaps * 12),
|
||||||
|
Ptrs2 = [FilePos + 20|Ptrs],
|
||||||
|
Size = byte_size(MapsBin) + 24,
|
||||||
|
{<< $N, $X, $R, 0, Size:32/little, AreaPtr:32/little, 0:32,
|
||||||
|
AreaID:16/little, NbMaps:16/little, MapsPtr:32/little,
|
||||||
|
MapsBin/binary >>, Ptrs2}.
|
||||||
|
|
||||||
|
load_set_rel_maps(Settings, Maps, MapsPos, GroupsPos) ->
|
||||||
|
load_set_rel_maps(Settings, Maps, MapsPos, GroupsPos, [], [], []).
|
||||||
|
load_set_rel_maps(_Settings, [], _MapsPos, _GroupsPos, Ptrs, GroupsAcc, MapsAcc) ->
|
||||||
|
ObjectsBin = iolist_to_binary(lists:reverse(GroupsAcc)),
|
||||||
|
MapsBin = iolist_to_binary(lists:reverse(MapsAcc)),
|
||||||
|
{<< MapsBin/binary, ObjectsBin/binary >>, lists:sort(Ptrs)};
|
||||||
|
load_set_rel_maps(Settings, [MapID|Tail], MapsPos, GroupsPos, Ptrs, GroupsAcc, MapsAcc) ->
|
||||||
|
Map = proplists:get_value({map, MapID}, Settings),
|
||||||
|
NbGroups = length(Map),
|
||||||
|
{GroupsBin, Ptrs2} = load_set_rel_groups(Map, GroupsPos, GroupsPos + NbGroups * 40, Ptrs),
|
||||||
|
MapBin = << MapID:16/little, NbGroups:16/little, GroupsPos:32/little, 0:32 >>,
|
||||||
|
load_set_rel_maps(Settings, Tail, MapsPos + 12, GroupsPos + byte_size(GroupsBin), [MapsPos + 4|Ptrs2], [GroupsBin|GroupsAcc], [MapBin|MapsAcc]).
|
||||||
|
|
||||||
|
%% @todo 0:144, 16#ffff:16 0:32 can have some values.
|
||||||
|
load_set_rel_groups(Groups, GroupsPos, ObjectsPos, Ptrs) ->
|
||||||
|
load_set_rel_groups(Groups, GroupsPos, ObjectsPos, Ptrs, 0, [], []).
|
||||||
|
load_set_rel_groups([], _GroupsPos, _ObjectsPos, Ptrs, _N, ObjectsAcc, GroupsAcc) ->
|
||||||
|
ObjectsBin = iolist_to_binary(lists:reverse(ObjectsAcc)),
|
||||||
|
GroupsBin = iolist_to_binary(lists:reverse(GroupsAcc)),
|
||||||
|
{<< GroupsBin/binary, ObjectsBin/binary >>, Ptrs};
|
||||||
|
load_set_rel_groups([Group|Tail], GroupsPos, ObjectsPos, Ptrs, N, ObjectsAcc, GroupsAcc) ->
|
||||||
|
NbObjects = length(Group),
|
||||||
|
{ObjectsBin, Ptrs2} = load_set_rel_objects(Group, ObjectsPos, ObjectsPos + NbObjects * 52, Ptrs),
|
||||||
|
GroupBin = << 16#ffffffff:32, 0:144, 16#ffff:16, 0:32, N:32/little, 0:16, NbObjects:16/little, ObjectsPos:32/little >>,
|
||||||
|
load_set_rel_groups(Tail, GroupsPos + 40, ObjectsPos + byte_size(ObjectsBin), [GroupsPos + 36|Ptrs2], N + 1, [ObjectsBin|ObjectsAcc], [GroupBin|GroupsAcc]).
|
||||||
|
|
||||||
|
load_set_rel_objects(Objects, StdPos, ParamsPos, Ptrs) ->
|
||||||
|
load_set_rel_objects(Objects, StdPos, ParamsPos, Ptrs, [], []).
|
||||||
|
load_set_rel_objects([], _StdPos, _ParamsPos, Ptrs, StdAcc, ParamsAcc) ->
|
||||||
|
StdBin = iolist_to_binary(lists:reverse(StdAcc)),
|
||||||
|
ParamsBin = iolist_to_binary(lists:reverse(ParamsAcc)),
|
||||||
|
{<< StdBin/binary, ParamsBin/binary >>, Ptrs};
|
||||||
|
load_set_rel_objects([{ObjType, ObjPos, ObjRot, ObjParams}|Tail], StdPos, ParamsPos, Ptrs, StdAcc, ParamsAcc) ->
|
||||||
|
ParamsBin = load_set_rel_object_params(ObjType, ObjParams),
|
||||||
|
ParamsSize = byte_size(ParamsBin),
|
||||||
|
{ClassID, TypeID} = load_set_rel_object_id(ObjType),
|
||||||
|
{PosX, PosY, PosZ} = ObjPos,
|
||||||
|
{RotX, RotY, RotZ} = ObjRot,
|
||||||
|
StdBin = << 16#ffffffff:32, ClassID:32/little, 16#ffffffff:32, 16#ffff:16, TypeID:16/little, 0:32,
|
||||||
|
PosX:32/little-float, PosY:32/little-float, PosZ:32/little-float,
|
||||||
|
RotX:32/little-float, RotY:32/little-float, RotZ:32/little-float,
|
||||||
|
ParamsSize:32/little, ParamsPos:32/little >>,
|
||||||
|
load_set_rel_objects(Tail, StdPos + 52, ParamsPos + ParamsSize, [StdPos + 48|Ptrs], [StdBin|StdAcc], [ParamsBin|ParamsAcc]).
|
||||||
|
|
||||||
|
load_set_rel_object_id(static_model) -> {4, 4};
|
||||||
|
load_set_rel_object_id(sensor) -> {4, 9};
|
||||||
|
load_set_rel_object_id(invisible_block) -> {1, 10};
|
||||||
|
load_set_rel_object_id(npc) -> {2, 18};
|
||||||
|
load_set_rel_object_id(door) -> {5, 20};
|
||||||
|
load_set_rel_object_id(entrance) -> {2, 26};
|
||||||
|
load_set_rel_object_id(exit) -> {6, 27};
|
||||||
|
load_set_rel_object_id(type_counter) -> {1, 47};
|
||||||
|
load_set_rel_object_id(label) -> {2, 53};
|
||||||
|
load_set_rel_object_id(chair) -> {2, 56};
|
||||||
|
load_set_rel_object_id(uni_cube) -> {0, 60};
|
||||||
|
load_set_rel_object_id(pp_cube) -> {0, 62};
|
||||||
|
load_set_rel_object_id({raw, ClassID, TypeID}) -> {ClassID, TypeID}.
|
||||||
|
|
||||||
|
load_set_rel_object_params(static_model, Params) ->
|
||||||
|
Model = proplists:get_value(model, Params),
|
||||||
|
Size = proplists:get_value(size, Params, 1.0),
|
||||||
|
<< Model:32/little, Size:32/little-float, 16#0000ff00:32,
|
||||||
|
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32,
|
||||||
|
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32 >>;
|
||||||
|
%% @todo If 00010000 has a 01 at the end, the sensor doesn't have an associated function in the script.
|
||||||
|
%% @todo Not sure about the box. It's probably wrong.
|
||||||
|
load_set_rel_object_params(sensor, Params) ->
|
||||||
|
ID = proplists:get_value(id, Params),
|
||||||
|
{Rad, X, Y, Z} = proplists:get_value(box, Params),
|
||||||
|
<< Rad:32/little-float, X:32/little-float, Y:32/little-float, Z:32/little-float, 0:64, ID:32/little, 16#00010000:32, 0:96,
|
||||||
|
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32 >>;
|
||||||
|
load_set_rel_object_params(invisible_block, Params) ->
|
||||||
|
{Width, Height, Depth} = proplists:get_value(dimension, Params),
|
||||||
|
<< Width:32/little-float, Height:32/little-float, Depth:32/little-float, 16#ffff0000:32,
|
||||||
|
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, %% First ffffffff can be events required to enable (each being a 16-bit integer).
|
||||||
|
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32 >>; %% First ffffffff can be events required to disable.
|
||||||
|
load_set_rel_object_params(npc, Params) ->
|
||||||
|
Model = proplists:get_value(model, Params),
|
||||||
|
ID = proplists:get_value(id, Params),
|
||||||
|
{CanTalk, TalkRadius} = case proplists:get_value(talk_radius, Params) of undefined -> {0, 0.0}; T -> {1, T} end,
|
||||||
|
{CanWalk, WalkRadius} = case proplists:get_value(walk_radius, Params) of undefined -> {0, 0.0}; W -> {1, W} end,
|
||||||
|
<< Model:16/little, ID:16/little, TalkRadius:32/little-float, WalkRadius:32/little-float,
|
||||||
|
CanWalk:8, 0:24, CanTalk:8, 0:24, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32 >>;
|
||||||
|
%% @todo The byte right after the first ffff can take the value 1.
|
||||||
|
%% @todo The 2 bytes starting the many ffffffff at the end can take an unknown value.
|
||||||
|
load_set_rel_object_params(door, Params) ->
|
||||||
|
Model = proplists:get_value(model, Params),
|
||||||
|
Variant = proplists:get_value(variant, Params, 255),
|
||||||
|
IsClosed = case proplists:get_value(closed, Params, false) of true -> 1; false -> 0 end,
|
||||||
|
<< Model:32/little, IsClosed:8, 0:8, 16#ffff:16, 0:16, Variant:8, 0:8,
|
||||||
|
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32 >>;
|
||||||
|
%% @todo 16#2041 is unknown (10.0 float). Other unknown values following can be non-0. Probably entrance and camera movements.
|
||||||
|
load_set_rel_object_params(entrance, Params) ->
|
||||||
|
EntryID = proplists:get_value(entryid, Params),
|
||||||
|
<< EntryID:16/little, 0:32, 16#2041:16, 0:1088 >>;
|
||||||
|
%% @todo The 2 bytes after CameraMovZ can have unknown values.
|
||||||
|
load_set_rel_object_params(exit, Params) ->
|
||||||
|
EntryID = proplists:get_value(entryid, Params),
|
||||||
|
Animation = proplists:get_value(animation, Params),
|
||||||
|
AnimationID = case Animation of stop -> 0; walk -> 1; run -> 2 end,
|
||||||
|
{ExitBoxRad, ExitBoxW, ExitBoxH, ExitBoxD} = proplists:get_value(exit_box, Params),
|
||||||
|
{ExitMovX, ExitMovY, ExitMovZ} = proplists:get_value(exit_movement, Params),
|
||||||
|
{CameraBoxRad, CameraBoxW, CameraBoxH, CameraBoxD} = proplists:get_value(camera_box, Params),
|
||||||
|
{CameraMovX, CameraMovY, CameraMovZ} = proplists:get_value(camera_movement, Params),
|
||||||
|
CommonBin = << ExitBoxRad:32/little-float, ExitBoxW:32/little-float, ExitBoxH:32/little-float, ExitBoxD:32/little-float,
|
||||||
|
ExitMovX:32/little-float, ExitMovY:32/little-float, ExitMovZ:32/little-float, 0:192,
|
||||||
|
CameraBoxRad:32/little-float, CameraBoxW:32/little-float, CameraBoxH:32/little-float, CameraBoxD:32/little-float,
|
||||||
|
CameraMovX:32/little-float, CameraMovY:32/little-float, CameraMovZ:32/little-float,
|
||||||
|
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32 >>,
|
||||||
|
case proplists:get_value(type, Params) of
|
||||||
|
counter ->
|
||||||
|
CounterID = proplists:get_value(counterid, Params),
|
||||||
|
<< CounterID:16/little, 1:16/little, 0:16, AnimationID:8, 1:8, EntryID:8, 0:16, 1:8, CommonBin/binary >>;
|
||||||
|
map ->
|
||||||
|
<< EntryID:16/little, 0:32, AnimationID:8, 1:8, 255:8, 0:16, 1:8, CommonBin/binary >>
|
||||||
|
end;
|
||||||
|
%% @todo Mostly unknown values!
|
||||||
|
load_set_rel_object_params(type_counter, Params) ->
|
||||||
|
TalkRadius = proplists:get_value(talk_radius, Params),
|
||||||
|
<< TalkRadius:32/little-float, 0:384, 16#ffffffff:32 >>;
|
||||||
|
%% @todo Not sure about the box. It's probably wrong.
|
||||||
|
%% @todo Can only have up to 10 different label ids.
|
||||||
|
load_set_rel_object_params(label, Params) ->
|
||||||
|
ID = proplists:get_value(id, Params),
|
||||||
|
{Rad, X, Y, Z} = proplists:get_value(box, Params),
|
||||||
|
<< Rad:32/little-float, X:32/little-float, Y:32/little-float, Z:32/little-float, ID:8, 1:8, 0:16,
|
||||||
|
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32 >>;
|
||||||
|
%% @todo Many unknown values...
|
||||||
|
load_set_rel_object_params(chair, Params) ->
|
||||||
|
ID = proplists:get_value(id, Params),
|
||||||
|
ActionRadius = 1.0,
|
||||||
|
UnknownA = -20.0,
|
||||||
|
UnknownB = 30.0,
|
||||||
|
UnknownC = 110.0,
|
||||||
|
UnknownD = -6.0,
|
||||||
|
%% If it looks like the player is going to get stuck in an object, the game won't allow the player to leave its chair.
|
||||||
|
StandUpMoveDistance = proplists:get_value(stand_dist, Params, 13.0),
|
||||||
|
UnknownE = 94.0,
|
||||||
|
<< ID:32/little, ActionRadius:32/little-float, 0, 1, 1, 0, UnknownA:32/little-float, UnknownB:32/little-float,
|
||||||
|
UnknownC:32/little-float, 0:32, UnknownD:32/little-float, StandUpMoveDistance:32/little-float, UnknownE:32/little-float >>;
|
||||||
|
%% @todo First ffff sometimes is 0, find out why.
|
||||||
|
load_set_rel_object_params(uni_cube, Params) ->
|
||||||
|
I = proplists:get_value(i, Params),
|
||||||
|
EntryID = proplists:get_value(entryid, Params),
|
||||||
|
<< I:8, EntryID:16/little, 0:8,
|
||||||
|
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32,
|
||||||
|
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32 >>;
|
||||||
|
%% @todo First ffff can be that same old unknown type of value.
|
||||||
|
%% @todo First byte set to 1 = hidden?
|
||||||
|
load_set_rel_object_params(pp_cube, _Params) ->
|
||||||
|
<< 0:32, 16#ffffffff:32, 16#ffffffff:32 >>;
|
||||||
|
%% @doc Raw binary, used for testing.
|
||||||
|
load_set_rel_object_params({raw, _ClassID, _TypeID}, ParamsBin) ->
|
||||||
|
ParamsBin.
|
||||||
|
%% @todo ghosts: NbGhosts:32, Width:32/float, Unknown:32/float
|
||||||
|
|
||||||
|
%% @doc Load a counter configuration file and return a table.rel binary along with its pointers array.
|
||||||
|
load_table_rel(ConfFilename) ->
|
||||||
|
{ok, Settings} = file:consult(ConfFilename),
|
||||||
|
TName = proplists:get_value(t_name, Settings),
|
||||||
|
TDesc = proplists:get_value(t_desc, Settings),
|
||||||
|
{CursorX, CursorY} = proplists:get_value(cursor, Settings),
|
||||||
|
{NbQuests, QuestsBin, NbGroups, GroupsBin} = load_table_rel_groups_to_bin(proplists:get_value(groups, Settings)),
|
||||||
|
QuestsPos = 16,
|
||||||
|
GroupsPos = 16 + byte_size(QuestsBin),
|
||||||
|
UnixTime = calendar:datetime_to_gregorian_seconds(calendar:now_to_universal_time(now()))
|
||||||
|
- calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}),
|
||||||
|
MainBin = << 16#00000100:32, UnixTime:32/little, GroupsPos:32/little, QuestsPos:32/little,
|
||||||
|
NbGroups:16/little, NbQuests:16/little, TName:16/little, TDesc:16/little, CursorX:16/little, CursorY:16/little,
|
||||||
|
0:32, 0:16, 16#ffff:16 >>,
|
||||||
|
MainPos = GroupsPos + byte_size(GroupsBin),
|
||||||
|
{CityPos, CityBin} = case proplists:get_value(city, Settings) of
|
||||||
|
undefined -> {0, << >>};
|
||||||
|
{QuestID, ZoneID, MapID, EntryID} ->
|
||||||
|
{MainPos + byte_size(MainBin) + 4, << QuestID:32/little, ZoneID:16/little, MapID:16/little, EntryID:16/little >>}
|
||||||
|
end,
|
||||||
|
Data = << MainPos:32/little, 0:32, QuestsBin/binary, GroupsBin/binary, MainBin/binary, CityPos:32/little, CityBin/binary >>,
|
||||||
|
Size = byte_size(Data),
|
||||||
|
Data2 = << $N, $X, $R, 0, Size:32/little, Data/binary >>,
|
||||||
|
case CityPos of
|
||||||
|
0 -> {Data2, [MainPos + 8, MainPos + 12]};
|
||||||
|
_ -> {Data2, [MainPos + 8, MainPos + 12, MainPos + 36]}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc Convert groups of quests to their binary equivalent for load_table_rel.
|
||||||
|
load_table_rel_groups_to_bin(Groups) ->
|
||||||
|
load_table_rel_groups_to_bin(Groups, 0, [], []).
|
||||||
|
load_table_rel_groups_to_bin([], N, QAcc, GAcc) ->
|
||||||
|
NbGroups = length(GAcc),
|
||||||
|
Quests = iolist_to_binary(lists:reverse(QAcc)),
|
||||||
|
Groups = iolist_to_binary(lists:reverse(GAcc)),
|
||||||
|
{N, Quests, NbGroups, Groups};
|
||||||
|
load_table_rel_groups_to_bin([Settings|Tail], N, QAcc, GAcc) ->
|
||||||
|
TName = proplists:get_value(t_name, Settings),
|
||||||
|
TDesc = proplists:get_value(t_desc, Settings),
|
||||||
|
Quests = proplists:get_value(quests, Settings),
|
||||||
|
QuestsBin = [<< Q:32/little >> || {Q, _Filename} <- Quests],
|
||||||
|
L = length(Quests),
|
||||||
|
GroupBin = << N:16/little, L:16/little, TName:16/little, TDesc:16/little, 0:16, 16#ff03:16, 0:160 >>,
|
||||||
|
load_table_rel_groups_to_bin(Tail, N + L, [QuestsBin|QAcc], [GroupBin|GAcc]).
|
||||||
|
|
||||||
|
%% @doc Load a text.bin file from its UCS-2 txt file equivalent.
|
||||||
|
load_text_bin(TextFilename) ->
|
||||||
|
{ok, << 16#fffe:16, File/binary >>} = file:read_file(TextFilename),
|
||||||
|
Strings = re:split(File, "\n."),
|
||||||
|
TextBin = load_text_bin_strings(Strings),
|
||||||
|
Size = 12 + byte_size(TextBin),
|
||||||
|
<< Size:32/little, 8:32/little, 12:32/little, TextBin/binary >>.
|
||||||
|
|
||||||
|
load_text_bin_strings(Strings) ->
|
||||||
|
load_text_bin_strings(Strings, 0, [], []).
|
||||||
|
load_text_bin_strings([], _Pos, PosList, Acc) ->
|
||||||
|
L = length(PosList) * 4,
|
||||||
|
PosList2 = [P + L + 12 || P <- lists:reverse(PosList)],
|
||||||
|
PosBin = iolist_to_binary([<< P:32/little >> || P <- PosList2]),
|
||||||
|
StringsBin = iolist_to_binary(lists:reverse(Acc)),
|
||||||
|
<< PosBin/binary, StringsBin/binary >>;
|
||||||
|
%% empty line at the end of a text.bin.txt file.
|
||||||
|
load_text_bin_strings([<< >>], Pos, PosList, Acc) ->
|
||||||
|
load_text_bin_strings([], Pos, PosList, Acc);
|
||||||
|
load_text_bin_strings([String|Tail], Pos, PosList, Acc) ->
|
||||||
|
String2 = re:replace(String, "~.n.", "\n\0", [global, {return, binary}]),
|
||||||
|
String3 = << String2/binary, 0, 0 >>,
|
||||||
|
load_text_bin_strings(Tail, Pos + byte_size(String3), [Pos|PosList], [String3|Acc]).
|
||||||
|
|
||||||
|
%% @doc Create the unit_title_table.rel file based on a quest configuration file and a starting file position.
|
||||||
|
%% The file position depends on the previously built quest.xnr.
|
||||||
|
load_unit_title_table_rel(ConfFilename, FilePos) ->
|
||||||
|
{ok, Settings} = file:consult(ConfFilename),
|
||||||
|
{Titles, NbTitles} = load_unit_title_table_rel_zones(proplists:get_value(zones, Settings)),
|
||||||
|
MainPos = 16 + byte_size(Titles),
|
||||||
|
Size = 8 + MainPos,
|
||||||
|
TitlesPtr = FilePos + 16,
|
||||||
|
Data = << $N, $X, $R, 0, Size:32/little, MainPos:32/little, 0:32, Titles/binary, TitlesPtr:32/little, NbTitles:32/little >>,
|
||||||
|
{Data, [FilePos + 16 + byte_size(Titles)]}.
|
||||||
|
|
||||||
|
load_unit_title_table_rel_maps(ZoneID, Maps) ->
|
||||||
|
load_unit_title_table_rel_maps(ZoneID, Maps, 0, []).
|
||||||
|
load_unit_title_table_rel_maps(_ZoneID, [], N, Acc) ->
|
||||||
|
{iolist_to_binary(lists:reverse(Acc)), N};
|
||||||
|
load_unit_title_table_rel_maps(ZoneID, [MapID|Tail], N, Acc) ->
|
||||||
|
Bin = << ZoneID:16/little, MapID:16/little, N:32/little >>,
|
||||||
|
load_unit_title_table_rel_maps(ZoneID, Tail, N + 1, [Bin|Acc]).
|
||||||
|
|
||||||
|
load_unit_title_table_rel_zones(Zones) ->
|
||||||
|
load_unit_title_table_rel_zones(Zones, 0, []).
|
||||||
|
load_unit_title_table_rel_zones([], N, Acc) ->
|
||||||
|
{iolist_to_binary(lists:reverse(Acc)), N};
|
||||||
|
load_unit_title_table_rel_zones([{ZoneID, ZoneParams}|Tail], N, Acc) ->
|
||||||
|
Maps = proplists:get_value(maps, ZoneParams),
|
||||||
|
{Bin, N2} = load_unit_title_table_rel_maps(ZoneID, Maps),
|
||||||
|
load_unit_title_table_rel_zones(Tail, N + N2, [Bin|Acc]).
|
||||||
|
|
||||||
|
%% @doc Pack an nbl file according to the given Options.
|
||||||
|
%% Example usage: nbl:pack([{files, [{file, "table.rel", [16#184, 16#188, 16#1a0]}, {file, "text.bin", []}]}]).
|
||||||
|
%% The data will be automatically compressed if the file is big enough to make it worth it.
|
||||||
|
%% @todo The 0010 value is unknown. If it was too low it would crash the client when it cleans up the nbl.
|
||||||
|
nbl_pack(Options) ->
|
||||||
|
Files = proplists:get_value(files, Options),
|
||||||
|
{Header, Data, DataSize, CompressedDataSize, PtrArray, PtrArraySize} = nbl_pack_files(Files),
|
||||||
|
NbFiles = length(Files),
|
||||||
|
HeaderSize = 16#30 + 16#60 * NbFiles,
|
||||||
|
EncryptSeed = 0,
|
||||||
|
<< $N, $M, $L, $L, 2:16/little, 16#0010:16, HeaderSize:32/little, NbFiles:32/little,
|
||||||
|
DataSize:32/little, CompressedDataSize:32/little, PtrArraySize:32/little, EncryptSeed:32/little,
|
||||||
|
0:128, Header/binary, Data/binary, PtrArray/binary >>.
|
||||||
|
|
||||||
|
%% @doc Pack a list of files and return the header, data and pointer array parts.
|
||||||
|
nbl_pack_files(Files) ->
|
||||||
|
nbl_pack_files(Files, {[], [], [], 0, 0}).
|
||||||
|
nbl_pack_files([], {AccH, AccD, AccP, _FilePos, _PtrIndex}) ->
|
||||||
|
BinH = iolist_to_binary(lists:reverse(AccH)),
|
||||||
|
PaddingH = 8 * (16#7d0 - (byte_size(BinH) rem 16#800)),
|
||||||
|
PaddingH2 = if PaddingH =< 0 -> 16#800 + PaddingH; true -> PaddingH end,
|
||||||
|
BinD = iolist_to_binary(lists:reverse(AccD)),
|
||||||
|
BinDSize = byte_size(BinD),
|
||||||
|
{BinD3, CompressedDataSize} = if BinDSize < 16#800 ->
|
||||||
|
{BinD, 0};
|
||||||
|
true ->
|
||||||
|
BinD2 = prs:compress(BinD),
|
||||||
|
BinD2Size = byte_size(BinD2),
|
||||||
|
{BinD2, BinD2Size}
|
||||||
|
end,
|
||||||
|
PaddingD = 8 * (16#800 - (byte_size(BinD3) rem 16#800)),
|
||||||
|
PaddingD2 = if PaddingD =:= 8 * 16#800 -> 0; true -> PaddingD end,
|
||||||
|
BinP = iolist_to_binary(lists:reverse(AccP)),
|
||||||
|
PtrSize = byte_size(BinP),
|
||||||
|
PtrArray = case PtrSize of
|
||||||
|
0 -> << >>;
|
||||||
|
_ ->
|
||||||
|
PaddingP = 8 * (16#800 - (byte_size(BinP) rem 16#800)),
|
||||||
|
<< BinP/binary, 0:PaddingP >>
|
||||||
|
end,
|
||||||
|
{<< BinH/binary, 0:PaddingH2 >>, << BinD3/binary, 0:PaddingD2 >>, BinDSize, CompressedDataSize, PtrArray, PtrSize};
|
||||||
|
nbl_pack_files([{data, Filename, Data, PtrList}|Tail], {AccH, AccD, AccP, FilePos, PtrIndex}) ->
|
||||||
|
ID = case filename:extension(Filename) of
|
||||||
|
".bin" -> << $S, $T, $D, 0 >>;
|
||||||
|
[$.|String] -> list_to_binary(string:to_upper(String ++ [0]))
|
||||||
|
end,
|
||||||
|
FilenameBin = iolist_to_binary(Filename),
|
||||||
|
FilenamePaddingBits = 8 * (32 - byte_size(FilenameBin)),
|
||||||
|
DataSize = byte_size(Data),
|
||||||
|
PaddedSize = nbl_padded_size(DataSize),
|
||||||
|
DataPaddingBits = 8 * (PaddedSize - DataSize),
|
||||||
|
PtrSize = 4 * length(PtrList),
|
||||||
|
BinD = << Data/binary, 0:DataPaddingBits >>,
|
||||||
|
BinH = << ID/binary, 16#60000000:32, 0:64, FilenameBin/binary, 0:FilenamePaddingBits,
|
||||||
|
FilePos:32/little, DataSize:32/little, PtrIndex:32/little, PtrSize:32/little >>,
|
||||||
|
NXIF = case filename:extension(Filename) of
|
||||||
|
".bin" -> << 0:256 >>;
|
||||||
|
_ -> nbl_pack_nxif(DataSize, PtrSize)
|
||||||
|
end,
|
||||||
|
BinH2 = << BinH/binary, NXIF/binary >>,
|
||||||
|
BinP = iolist_to_binary([ << Ptr:32/little >> || Ptr <- PtrList]),
|
||||||
|
nbl_pack_files(Tail, {[BinH2|AccH], [BinD|AccD], [BinP|AccP], FilePos + PaddedSize, PtrIndex + PtrSize});
|
||||||
|
nbl_pack_files([{file, Filename, PtrList}|Tail], Acc) ->
|
||||||
|
io:format("~p~n", [Filename]),
|
||||||
|
{ok, Data} = file:read_file(Filename),
|
||||||
|
nbl_pack_files([{data, Filename, Data, PtrList}|Tail], Acc).
|
||||||
|
|
||||||
|
%% @doc Return an NXIF chunk data for a specific data and pointer array size.
|
||||||
|
nbl_pack_nxif(DataSize, PtrSize) ->
|
||||||
|
DataSize2 = DataSize + 16#20,
|
||||||
|
PtrSize2 = PtrSize + 16#20 - (PtrSize rem 16#20),
|
||||||
|
<< $N, $X, $I, $F, 16#18000000:32, 16#01000000:32, 16#20000000:32,
|
||||||
|
DataSize:32/little, DataSize2:32/little, PtrSize2:32/little, 16#01000000:32 >>.
|
||||||
|
|
||||||
|
%% @doc Return the padded size of a file to be packed in an nbl archive.
|
||||||
|
nbl_padded_size(Size) ->
|
||||||
|
Size + 16#20 - (Size rem 16#20).
|
857
apps/egs/src/egs_game.erl
Normal file
857
apps/egs/src/egs_game.erl
Normal file
@ -0,0 +1,857 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
|
%% @doc Game callback module.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_game).
|
||||||
|
-export([info/2, cast/2, raw/3, event/2]).
|
||||||
|
-export([char_load/2]). %% Hopefully temporary export.
|
||||||
|
|
||||||
|
-include("include/records.hrl").
|
||||||
|
|
||||||
|
%% @doc Forward the broadcasted command to the client.
|
||||||
|
info({egs, cast, Command}, Client=#egs_net{gid=GID}) ->
|
||||||
|
<< A:64/bits, _:32, B:96/bits, _:64, C/bits >> = Command,
|
||||||
|
egs_proto:packet_send(Client, << A/binary, 16#00011300:32, B/binary, 16#00011300:32, GID:32/little, C/binary >>);
|
||||||
|
|
||||||
|
%% @doc Forward the chat message to the client.
|
||||||
|
info({egs, chat, FromGID, ChatTypeID, ChatGID, ChatName, ChatModifiers, ChatMessage}, Client) ->
|
||||||
|
egs_proto:send_0304(FromGID, ChatTypeID, ChatGID, ChatName, ChatModifiers, ChatMessage, Client);
|
||||||
|
|
||||||
|
info({egs, notice, Type, Message}, Client) ->
|
||||||
|
egs_proto:send_0228(Type, 2, Message, Client);
|
||||||
|
|
||||||
|
%% @doc Inform the client that a player has spawn.
|
||||||
|
%% @todo Not sure what IsSeasonal or the AreaNb in 0205 should be for other spawns.
|
||||||
|
info({egs, player_spawn, Player}, Client) ->
|
||||||
|
egs_proto:send_0111(Player, 6, Client),
|
||||||
|
egs_proto:send_010d(Player, Client),
|
||||||
|
egs_proto:send_0205(Player, 0, Client),
|
||||||
|
egs_proto:send_0203(Player, Client),
|
||||||
|
egs_proto:send_0201(Player, Client);
|
||||||
|
|
||||||
|
%% @doc Inform the client that a player has unspawn.
|
||||||
|
info({egs, player_unspawn, Player}, Client) ->
|
||||||
|
egs_proto:send_0204(Player, Client);
|
||||||
|
|
||||||
|
%% @doc Warp the player to the given location.
|
||||||
|
info({egs, warp, QuestID, ZoneID, MapID, EntryID}, Client) ->
|
||||||
|
event({area_change, QuestID, ZoneID, MapID, EntryID}, Client).
|
||||||
|
|
||||||
|
%% Broadcasts.
|
||||||
|
|
||||||
|
%% @todo Handle broadcasting better than that. Review the commands at the same time.
|
||||||
|
%% @doc Position change. Save the position and then dispatch it.
|
||||||
|
cast({16#0503, Data}, Client=#egs_net{gid=GID}) ->
|
||||||
|
<< _:424, Dir:24/little, _PrevCoords:96, X:32/little-float, Y:32/little-float, Z:32/little-float,
|
||||||
|
QuestID:32/little, ZoneID:32/little, MapID:32/little, EntryID:32/little, _:32 >> = Data,
|
||||||
|
FloatDir = Dir / 46603.375,
|
||||||
|
{ok, User} = egs_users:read(GID),
|
||||||
|
NewUser = User#users{pos={X, Y, Z, FloatDir}, area={QuestID, ZoneID, MapID}, entryid=EntryID},
|
||||||
|
egs_users:write(NewUser),
|
||||||
|
cast({valid, Data}, Client);
|
||||||
|
|
||||||
|
%% @doc Stand still. Save the position and then dispatch it.
|
||||||
|
cast({16#0514, Data}, Client=#egs_net{gid=GID}) ->
|
||||||
|
<< _:424, Dir:24/little, X:32/little-float, Y:32/little-float, Z:32/little-float,
|
||||||
|
QuestID:32/little, ZoneID:32/little, MapID:32/little, EntryID:32/little, _/bits >> = Data,
|
||||||
|
FloatDir = Dir / 46603.375,
|
||||||
|
{ok, User} = egs_users:read(GID),
|
||||||
|
NewUser = User#users{pos={X, Y, Z, FloatDir}, area={QuestID, ZoneID, MapID}, entryid=EntryID},
|
||||||
|
egs_users:write(NewUser),
|
||||||
|
cast({valid, Data}, Client);
|
||||||
|
|
||||||
|
%% @doc Default broadcast handler. Dispatch the command to everyone.
|
||||||
|
%% We clean up the command and use the real GID and LID of the user, disregarding what was sent and possibly tampered with.
|
||||||
|
%% Only a handful of commands are allowed to broadcast. An user tampering with it would get disconnected instantly.
|
||||||
|
%% @todo Don't query the user data everytime! Keep the needed information in the Client.
|
||||||
|
cast({Command, Data}, #egs_net{gid=GID, lid=LID})
|
||||||
|
when Command =:= 16#0101;
|
||||||
|
Command =:= 16#0102;
|
||||||
|
Command =:= 16#0104;
|
||||||
|
Command =:= 16#0107;
|
||||||
|
Command =:= 16#010f;
|
||||||
|
Command =:= 16#050f;
|
||||||
|
Command =:= valid ->
|
||||||
|
<< _:32, A:64/bits, _:64, B:192/bits, _:64, C/bits >> = Data,
|
||||||
|
{ok, User} = egs_users:read(GID),
|
||||||
|
Packet = << A/binary, 16#00011300:32, GID:32/little, B/binary, GID:32/little, LID:32/little, C/binary >>,
|
||||||
|
egs_zones:broadcast(User#users.zonepid, GID, Packet).
|
||||||
|
|
||||||
|
%% Raw commands.
|
||||||
|
|
||||||
|
%% @todo Handle this packet properly.
|
||||||
|
%% @todo Spawn cleared response event shouldn't be handled following this packet but when we see the spawn actually dead HP-wise.
|
||||||
|
%% @todo Type shouldn't be :32 but it seems when the later 16 have something it's not a spawn event.
|
||||||
|
raw(16#0402, << _:352, Data/bits >>, Client=#egs_net{gid=GID}) ->
|
||||||
|
<< SpawnID:32/little, _:64, Type:32/little, _:64 >> = Data,
|
||||||
|
case Type of
|
||||||
|
7 -> % spawn cleared @todo 1201 sent back with same values apparently, but not always
|
||||||
|
io:format("~p: cleared spawn ~b~n", [GID, SpawnID]),
|
||||||
|
{ok, User} = egs_users:read(GID),
|
||||||
|
{BlockID, EventID} = psu_instance:spawn_cleared_event(User#users.instancepid, element(2, User#users.area), SpawnID),
|
||||||
|
if EventID =:= false -> ignore;
|
||||||
|
true -> egs_proto:send_1205(EventID, BlockID, 0, Client)
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
ignore
|
||||||
|
end;
|
||||||
|
|
||||||
|
%% @todo Handle this packet.
|
||||||
|
%% @todo 3rd Unsafe Passage C, EventID 10 BlockID 2 = mission cleared?
|
||||||
|
raw(16#0404, << _:352, Data/bits >>, Client) ->
|
||||||
|
<< EventID:8, BlockID:8, _:16, Value:8, _/bits >> = Data,
|
||||||
|
io:format("~p: unknown command 0404: eventid ~b blockid ~b value ~b~n", [Client#egs_net.gid, EventID, BlockID, Value]),
|
||||||
|
egs_proto:send_1205(EventID, BlockID, Value, Client);
|
||||||
|
|
||||||
|
%% @todo Used in the tutorial. Not sure what it does. Give an item (the PA) maybe?
|
||||||
|
%% @todo Probably should ignore that until more is known.
|
||||||
|
raw(16#0a09, _Data, Client=#egs_net{gid=GID}) ->
|
||||||
|
egs_proto:packet_send(Client, << 16#0a090300:32, 0:32, 16#00011300:32, GID:32/little, 0:64, 16#00011300:32, GID:32/little, 0:64, 16#00003300:32, 0:32 >>);
|
||||||
|
|
||||||
|
%% @todo Figure out this command.
|
||||||
|
raw(16#0c11, << _:352, A:32/little, B:32/little >>, Client=#egs_net{gid=GID}) ->
|
||||||
|
io:format("~p: 0c11 ~p ~p~n", [GID, A, B]),
|
||||||
|
egs_proto:packet_send(Client, << 16#0c120300:32, 0:160, 16#00011300:32, GID:32/little, 0:64, A:32/little, 1:32/little >>);
|
||||||
|
|
||||||
|
%% @doc Set flag handler. Associate a new flag with the character.
|
||||||
|
%% Just reply with a success value for now.
|
||||||
|
%% @todo God save the flags.
|
||||||
|
raw(16#0d04, << _:352, Data/bits >>, Client=#egs_net{gid=GID}) ->
|
||||||
|
<< Flag:128/bits, A:16/bits, _:8, B/bits >> = Data,
|
||||||
|
io:format("~p: flag handler for ~s~n", [GID, re:replace(Flag, "\\0+", "", [global, {return, binary}])]),
|
||||||
|
egs_proto:packet_send(Client, << 16#0d040300:32, 0:160, 16#00011300:32, GID:32/little, 0:64, Flag/binary, A/binary, 1, B/binary >>);
|
||||||
|
|
||||||
|
%% @doc Initialize a vehicle object.
|
||||||
|
%% @todo Find what are the many values, including the odd Whut value (and whether it's used in the reply).
|
||||||
|
%% @todo Separate the reply.
|
||||||
|
raw(16#0f00, << _:352, Data/bits >>, Client=#egs_net{gid=GID}) ->
|
||||||
|
<< A:32/little, 0:16, B:16/little, 0:16, C:16/little, 0, Whut:8, D:16/little, 0:16,
|
||||||
|
E:16/little, 0:16, F:16/little, G:16/little, H:16/little, I:32/little >> = Data,
|
||||||
|
io:format("~p: init vehicle: ~b ~b ~b ~b ~b ~b ~b ~b ~b ~b~n", [GID, A, B, C, Whut, D, E, F, G, H, I]),
|
||||||
|
egs_proto:packet_send(Client, << 16#12080300:32, 0:160, 16#00011300:32, GID:32/little, 0:64,
|
||||||
|
A:32/little, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32,
|
||||||
|
0:16, B:16/little, 0:16, C:16/little, 0:16, D:16/little, 0:112,
|
||||||
|
E:16/little, 0:16, F:16/little, H:16/little, 1, 0, 100, 0, 10, 0, G:16/little, 0:16 >>);
|
||||||
|
|
||||||
|
%% @doc Enter vehicle.
|
||||||
|
%% @todo Separate the reply.
|
||||||
|
raw(16#0f02, << _:352, Data/bits >>, Client=#egs_net{gid=GID}) ->
|
||||||
|
<< A:32/little, B:32/little, C:32/little >> = Data,
|
||||||
|
io:format("~p: enter vehicle: ~b ~b ~b~n", [GID, A, B, C]),
|
||||||
|
HP = 100,
|
||||||
|
egs_proto:packet_send(Client, << 16#120a0300:32, 0:160, 16#00011300:32, GID:32/little, 0:64, A:32/little, B:32/little, C:32/little, HP:32/little >>);
|
||||||
|
|
||||||
|
%% @doc Sent right after entering the vehicle. Can't move without it.
|
||||||
|
%% @todo Separate the reply.
|
||||||
|
raw(16#0f07, << _:352, Data/bits >>, Client=#egs_net{gid=GID}) ->
|
||||||
|
<< A:32/little, B:32/little >> = Data,
|
||||||
|
io:format("~p: after enter vehicle: ~b ~b~n", [GID, A, B]),
|
||||||
|
egs_proto:packet_send(Client, << 16#120f0300:32, 0:160, 16#00011300:32, GID:32/little, 0:64, A:32/little, B:32/little >>);
|
||||||
|
|
||||||
|
%% @todo Not sure yet.
|
||||||
|
raw(16#1019, _Data, _Client) ->
|
||||||
|
ignore;
|
||||||
|
%~ egs_proto:packet_send(Client, << 16#10190300:32, 0:160, 16#00011300:32, GID:32/little, 0:64, 0:192, 16#00200000:32, 0:32 >>);
|
||||||
|
|
||||||
|
%% @todo Not sure about that one though. Probably related to 1112 still.
|
||||||
|
raw(16#1106, << _:352, Data/bits >>, Client) ->
|
||||||
|
egs_proto:send_110e(Data, Client);
|
||||||
|
|
||||||
|
%% @doc Probably asking permission to start the video (used for syncing?).
|
||||||
|
raw(16#1112, << _:352, Data/bits >>, Client) ->
|
||||||
|
egs_proto:send_1113(Data, Client);
|
||||||
|
|
||||||
|
%% @todo Not sure yet. Value is probably a TargetID. Used in Airboard Rally. Replying with the same value starts the race.
|
||||||
|
raw(16#1216, << _:352, Data/bits >>, Client) ->
|
||||||
|
<< Value:32/little >> = Data,
|
||||||
|
io:format("~p: command 1216 with value ~b~n", [Client#egs_net.gid, Value]),
|
||||||
|
egs_proto:send_1216(Value, Client);
|
||||||
|
|
||||||
|
%% @doc Dismiss all unknown raw commands with a log notice.
|
||||||
|
%% @todo Have a log event handler instead.
|
||||||
|
raw(Command, _Data, Client) ->
|
||||||
|
io:format("~p (~p): dismissed command ~4.16.0b~n", [?MODULE, Client#egs_net.gid, Command]).
|
||||||
|
|
||||||
|
%% Events.
|
||||||
|
|
||||||
|
%% @doc Load the given map as a standard lobby.
|
||||||
|
%% @todo When changing lobby to the room, or room to lobby, we must perform an universe change.
|
||||||
|
%% @todo Handle area_change event for APCs in story missions (characters with PartyPos defined).
|
||||||
|
event({area_change, QuestID, ZoneID, MapID, EntryID}, Client) ->
|
||||||
|
event({area_change, QuestID, ZoneID, MapID, EntryID, 16#ffffffff}, Client);
|
||||||
|
event({area_change, QuestID, ZoneID, MapID, EntryID, PartyPos=16#ffffffff}, Client) ->
|
||||||
|
io:format("~p: area change (~b,~b,~b,~b,~b)~n", [Client#egs_net.gid, QuestID, ZoneID, MapID, EntryID, PartyPos]),
|
||||||
|
{ok, OldUser} = egs_users:read(Client#egs_net.gid),
|
||||||
|
{OldQuestID, OldZoneID, _OldMapID} = OldUser#users.area,
|
||||||
|
QuestChange = OldQuestID /= QuestID,
|
||||||
|
ZoneChange = if OldQuestID =:= QuestID, OldZoneID =:= ZoneID -> false; true -> true end,
|
||||||
|
AreaType = egs_quests_db:area_type(QuestID, ZoneID),
|
||||||
|
AreaShortName = "dammy", %% @todo Load the short name from egs_quests_db.
|
||||||
|
{IsSeasonal, SeasonID} = egs_seasons:read(QuestID),
|
||||||
|
User = OldUser#users{areatype=AreaType, area={QuestID, ZoneID, MapID}, entryid=EntryID},
|
||||||
|
egs_users:write(User), %% @todo Booh ugly! But temporary.
|
||||||
|
%% Load the quest.
|
||||||
|
User2 = if QuestChange ->
|
||||||
|
egs_proto:send_0c00(User, Client),
|
||||||
|
egs_proto:send_020e(egs_quests_db:quest_nbl(QuestID), Client),
|
||||||
|
User#users{questpid=egs_universes:lobby_pid(User#users.uni, QuestID)};
|
||||||
|
true -> User
|
||||||
|
end,
|
||||||
|
%% Load the zone.
|
||||||
|
Client1 = if ZoneChange ->
|
||||||
|
ZonePid = egs_quests:zone_pid(User2#users.questpid, ZoneID),
|
||||||
|
egs_zones:leave(User2#users.zonepid, User2#users.gid),
|
||||||
|
NewLID = egs_zones:enter(ZonePid, User2#users.gid),
|
||||||
|
NewClient = Client#egs_net{lid=NewLID},
|
||||||
|
{ok, User3} = egs_users:read(User2#users.gid),
|
||||||
|
egs_proto:send_0a05(NewClient),
|
||||||
|
egs_proto:send_0111(User3, 6, NewClient),
|
||||||
|
egs_proto:send_010d(User3, NewClient),
|
||||||
|
egs_proto:send_0200(ZoneID, AreaType, NewClient),
|
||||||
|
egs_proto:send_020f(egs_quests_db:zone_nbl(QuestID, ZoneID), egs_zones:setid(ZonePid), SeasonID, NewClient),
|
||||||
|
NewClient;
|
||||||
|
true ->
|
||||||
|
User3 = User2,
|
||||||
|
Client
|
||||||
|
end,
|
||||||
|
%% Save the user.
|
||||||
|
egs_users:write(User3),
|
||||||
|
%% Load the player location.
|
||||||
|
Client2 = Client1#egs_net{areanb=Client#egs_net.areanb + 1},
|
||||||
|
egs_proto:send_0205(User3, IsSeasonal, Client2),
|
||||||
|
egs_proto:send_100e(User3#users.area, User3#users.entryid, AreaShortName, Client2),
|
||||||
|
%% Load the zone objects.
|
||||||
|
if ZoneChange ->
|
||||||
|
egs_proto:send_1212(Client2); %% @todo Only sent if there is a set file.
|
||||||
|
true -> ignore
|
||||||
|
end,
|
||||||
|
%% Load the player.
|
||||||
|
egs_proto:send_0201(User3, Client2),
|
||||||
|
if ZoneChange ->
|
||||||
|
egs_proto:send_0a06(User3, Client2),
|
||||||
|
%% Load the other players in the zone.
|
||||||
|
OtherPlayersGID = egs_zones:get_all_players(User3#users.zonepid, User3#users.gid),
|
||||||
|
if OtherPlayersGID =:= [] -> ignore;
|
||||||
|
true ->
|
||||||
|
OtherPlayers = egs_users:select(OtherPlayersGID),
|
||||||
|
egs_proto:send_0233(OtherPlayers, Client)
|
||||||
|
end;
|
||||||
|
true -> ignore
|
||||||
|
end,
|
||||||
|
%% End of loading.
|
||||||
|
Client3 = Client2#egs_net{areanb=Client2#egs_net.areanb + 1},
|
||||||
|
egs_proto:send_0208(Client3),
|
||||||
|
egs_proto:send_0236(Client3),
|
||||||
|
%% @todo Load APC characters.
|
||||||
|
{ok, Client3};
|
||||||
|
event({area_change, QuestID, ZoneID, MapID, EntryID, PartyPos}, Client) ->
|
||||||
|
io:format("~p: area change (~b,~b,~b,~b,~b)~n", [Client#egs_net.gid, QuestID, ZoneID, MapID, EntryID, PartyPos]),
|
||||||
|
ignore;
|
||||||
|
|
||||||
|
%% @doc After the character has been (re)loaded, change the area he's in.
|
||||||
|
%% @todo The area_change event should probably not change the user's values.
|
||||||
|
%% @todo Remove that ugly code when the above is done.
|
||||||
|
event(system_character_load_complete, Client=#egs_net{gid=GID}) ->
|
||||||
|
{ok, User=#users{area={QuestID, ZoneID, MapID}, entryid=EntryID}} = egs_users:read(GID),
|
||||||
|
egs_users:write(User#users{area={0, 0, 0}, entryid=0}),
|
||||||
|
event({area_change, QuestID, ZoneID, MapID, EntryID}, Client);
|
||||||
|
|
||||||
|
%% @doc Chat broadcast handler. Dispatch the message to everyone (for now).
|
||||||
|
%% Disregard the name sent by the server. Use the name saved in memory instead, to prevent client-side editing.
|
||||||
|
%% @todo Only broadcast to people in the same map.
|
||||||
|
%% @todo In the case of NPC characters, when FromTypeID is 00001d00, check that the NPC is in the party and broadcast only to the party (probably).
|
||||||
|
%% @todo When the game doesn't find an NPC (probably) and forces it to talk like in the tutorial mission it seems FromTypeID, FromGID and Name are all 0.
|
||||||
|
%% @todo Make sure modifiers have correct values.
|
||||||
|
event({chat, _, FromGID, _, Modifiers, _, ChatMsg}, #egs_net{gid=UserGID}) ->
|
||||||
|
[BcastTypeID, BcastGID, BcastName] = case FromGID of
|
||||||
|
0 -> %% This probably shouldn't happen. Just make it crash on purpose.
|
||||||
|
io:format("~p: chat FromGID=0~n", [UserGID]),
|
||||||
|
ignore;
|
||||||
|
UserGID -> %% player chat: disregard whatever was sent except modifiers and message.
|
||||||
|
{ok, User} = egs_users:read(UserGID),
|
||||||
|
[16#00001200, User#users.gid, User#users.name];
|
||||||
|
NPCGID -> %% npc chat: @todo Check that the player is the party leader and this npc is in his party.
|
||||||
|
{ok, User} = egs_users:read(NPCGID),
|
||||||
|
[16#00001d00, FromGID, User#users.name]
|
||||||
|
end,
|
||||||
|
%% log the message as ascii to the console
|
||||||
|
[LogName|_] = re:split(BcastName, "\\0\\0", [{return, binary}]),
|
||||||
|
[TmpMessage|_] = re:split(ChatMsg, "\\0\\0", [{return, binary}]),
|
||||||
|
LogMessage = re:replace(TmpMessage, "\\n", " ", [global, {return, binary}]),
|
||||||
|
io:format("~p: chat from ~s: ~s~n", [UserGID, [re:replace(LogName, "\\0", "", [global, {return, binary}])], [re:replace(LogMessage, "\\0", "", [global, {return, binary}])]]),
|
||||||
|
egs_users:broadcast_all({egs, chat, UserGID, BcastTypeID, BcastGID, BcastName, Modifiers, ChatMsg});
|
||||||
|
|
||||||
|
%% @todo There's at least 9 different sets of locations. Handle all of them correctly.
|
||||||
|
event(counter_background_locations_request, Client) ->
|
||||||
|
egs_proto:send_170c(Client);
|
||||||
|
|
||||||
|
%% @todo Make sure non-mission counters follow the same loading process.
|
||||||
|
%% @todo Probably validate the From* values, to not send the player back inside a mission.
|
||||||
|
%% @todo Handle the LID change when entering counters.
|
||||||
|
event({counter_enter, CounterID, FromZoneID, FromMapID, FromEntryID}, Client=#egs_net{gid=GID}) ->
|
||||||
|
io:format("~p: counter load ~b~n", [GID, CounterID]),
|
||||||
|
{ok, OldUser} = egs_users:read(GID),
|
||||||
|
FromArea = {element(1, OldUser#users.area), FromZoneID, FromMapID},
|
||||||
|
egs_zones:leave(OldUser#users.zonepid, OldUser#users.gid),
|
||||||
|
User = OldUser#users{questpid=undefined, zonepid=undefined, areatype=counter,
|
||||||
|
area={16#7fffffff, 0, 0}, entryid=0, prev_area=FromArea, prev_entryid=FromEntryID},
|
||||||
|
egs_users:write(User),
|
||||||
|
QuestData = egs_quests_db:quest_nbl(0),
|
||||||
|
ZoneData = << 0:16000 >>, %% Doing like official just in case.
|
||||||
|
%% load counter
|
||||||
|
egs_proto:send_0c00(User, Client),
|
||||||
|
egs_proto:send_020e(QuestData, Client),
|
||||||
|
egs_proto:send_0a05(Client),
|
||||||
|
egs_proto:send_010d(User, Client),
|
||||||
|
egs_proto:send_0200(0, mission, Client),
|
||||||
|
egs_proto:send_020f(ZoneData, 0, 255, Client),
|
||||||
|
Client2 = Client#egs_net{areanb=Client#egs_net.areanb + 1},
|
||||||
|
egs_proto:send_0205(User, 0, Client2),
|
||||||
|
egs_proto:send_100e(CounterID, "Counter", Client2),
|
||||||
|
egs_proto:send_0215(0, Client2),
|
||||||
|
egs_proto:send_0215(0, Client2),
|
||||||
|
egs_proto:send_020c(Client2),
|
||||||
|
egs_proto:send_1202(Client2),
|
||||||
|
egs_proto:send_1204(Client2),
|
||||||
|
egs_proto:send_1206(Client2),
|
||||||
|
egs_proto:send_1207(Client2),
|
||||||
|
egs_proto:send_1212(Client2),
|
||||||
|
egs_proto:send_0201(User, Client2),
|
||||||
|
egs_proto:send_0a06(User, Client2),
|
||||||
|
case User#users.partypid of
|
||||||
|
undefined -> ignore;
|
||||||
|
_ -> egs_proto:send_022c(0, 16#12, Client)
|
||||||
|
end,
|
||||||
|
Client3 = Client2#egs_net{areanb=Client2#egs_net.areanb + 1},
|
||||||
|
egs_proto:send_0208(Client3),
|
||||||
|
egs_proto:send_0236(Client3),
|
||||||
|
{ok, Client3};
|
||||||
|
|
||||||
|
%% @todo Handle parties to join.
|
||||||
|
event(counter_join_party_request, Client) ->
|
||||||
|
egs_proto:send_1701(Client);
|
||||||
|
|
||||||
|
%% @doc Leave mission counter handler.
|
||||||
|
event(counter_leave, Client=#egs_net{gid=GID}) ->
|
||||||
|
{ok, User} = egs_users:read(GID),
|
||||||
|
PrevArea = User#users.prev_area,
|
||||||
|
event({area_change, element(1, PrevArea), element(2, PrevArea), element(3, PrevArea), User#users.prev_entryid}, Client);
|
||||||
|
|
||||||
|
%% @doc Send the code for the background image to use. But there's more that should be sent though.
|
||||||
|
%% @todo Apparently background values 1 2 3 are never used on official servers. Find out why.
|
||||||
|
%% @todo Rename to counter_bg_request.
|
||||||
|
event({counter_options_request, CounterID}, Client) ->
|
||||||
|
io:format("~p: counter options request ~p~n", [Client#egs_net.gid, CounterID]),
|
||||||
|
egs_proto:send_1711(egs_counters_db:bg(CounterID), Client);
|
||||||
|
|
||||||
|
%% @todo Handle when the party already exists! And stop doing it wrong.
|
||||||
|
event(counter_party_info_request, Client=#egs_net{gid=GID}) ->
|
||||||
|
{ok, User} = egs_users:read(GID),
|
||||||
|
egs_proto:send_1706(User#users.name, Client);
|
||||||
|
|
||||||
|
%% @todo Item distribution is always set to random for now.
|
||||||
|
event(counter_party_options_request, Client) ->
|
||||||
|
egs_proto:send_170a(Client);
|
||||||
|
|
||||||
|
%% @doc Request the counter's quest files.
|
||||||
|
event({counter_quest_files_request, CounterID}, Client) ->
|
||||||
|
io:format("~p: counter quest files request ~p~n", [Client#egs_net.gid, CounterID]),
|
||||||
|
egs_proto:send_0c06(egs_counters_db:pack(CounterID), Client);
|
||||||
|
|
||||||
|
%% @doc Counter available mission list request handler.
|
||||||
|
event({counter_quest_options_request, CounterID}, Client) ->
|
||||||
|
io:format("~p: counter quest options request ~p~n", [Client#egs_net.gid, CounterID]),
|
||||||
|
egs_proto:send_0c10(egs_counters_db:opts(CounterID), Client);
|
||||||
|
|
||||||
|
%% @todo A and B are mostly unknown. Like most of everything else from the command 0e00...
|
||||||
|
event({hit, FromTargetID, ToTargetID, A, B}, Client=#egs_net{gid=GID}) ->
|
||||||
|
{ok, User} = egs_users:read(GID),
|
||||||
|
%% hit!
|
||||||
|
#hit_response{type=Type, user=NewUser, exp=HasEXP, damage=Damage, targethp=TargetHP, targetse=TargetSE, events=Events} = psu_instance:hit(User, FromTargetID, ToTargetID),
|
||||||
|
case Type of
|
||||||
|
box ->
|
||||||
|
%% @todo also has a hit sent, we should send it too
|
||||||
|
events(Events, Client);
|
||||||
|
_ ->
|
||||||
|
PlayerHP = NewUser#users.currenthp,
|
||||||
|
case lists:member(death, TargetSE) of
|
||||||
|
true -> SE = 16#01000200;
|
||||||
|
false -> SE = 16#01000000
|
||||||
|
end,
|
||||||
|
egs_proto:packet_send(Client, << 16#0e070300:32, 0:160, 16#00011300:32, GID:32/little, 0:64,
|
||||||
|
1:32/little, 16#01050000:32, Damage:32/little,
|
||||||
|
A/binary, 0:64, PlayerHP:32/little, 0:32, SE:32,
|
||||||
|
0:32, TargetHP:32/little, 0:32, B/binary,
|
||||||
|
16#04320000:32, 16#80000000:32, 16#26030000:32, 16#89068d00:32, 16#0c1c0105:32, 0:64 >>)
|
||||||
|
% after TargetHP is SE-related too?
|
||||||
|
end,
|
||||||
|
%% exp
|
||||||
|
if HasEXP =:= true ->
|
||||||
|
egs_proto:send_0115(NewUser, ToTargetID, Client);
|
||||||
|
true -> ignore
|
||||||
|
end,
|
||||||
|
%% save
|
||||||
|
egs_users:write(NewUser);
|
||||||
|
|
||||||
|
event({hits, Hits}, Client) ->
|
||||||
|
events(Hits, Client);
|
||||||
|
|
||||||
|
event({item_description_request, ItemID}, Client) ->
|
||||||
|
egs_proto:send_0a11(ItemID, egs_items_db:desc(ItemID), Client);
|
||||||
|
|
||||||
|
%% @todo A and B are unknown.
|
||||||
|
%% Melee uses a format similar to: AAAA--BBCCCC----DDDDDDDDEE----FF with
|
||||||
|
%% AAAA the attack sound effect, BB the range, CCCC and DDDDDDDD unknown but related to angular range or similar, EE number of targets and FF the model.
|
||||||
|
%% Bullets and tech weapons formats are unknown but likely use a slightly different format.
|
||||||
|
%% @todo Others probably want to see that you changed your weapon.
|
||||||
|
%% @todo Apparently B is always ItemID+1. Not sure why.
|
||||||
|
%% @todo Currently use a separate file for the data sent for the weapons.
|
||||||
|
%% @todo TargetGID and TargetLID must be validated, they're either the player's or his NPC characters.
|
||||||
|
%% @todo Handle NPC characters properly.
|
||||||
|
event({item_equip, ItemIndex, TargetGID, TargetLID, A, B}, Client=#egs_net{gid=GID}) ->
|
||||||
|
case egs_users:item_nth(GID, ItemIndex) of
|
||||||
|
{ItemID, Variables} when element(1, Variables) =:= psu_special_item_variables ->
|
||||||
|
<< Category:8, _:24 >> = << ItemID:32 >>,
|
||||||
|
egs_proto:packet_send(Client, << 16#01050300:32, 0:64, TargetGID:32/little, 0:64, 16#00011300:32, GID:32/little, 0:64,
|
||||||
|
TargetGID:32/little, TargetLID:32/little, ItemIndex:8, 1:8, Category:8, A:8, B:32/little >>);
|
||||||
|
{ItemID, Variables} when element(1, Variables) =:= psu_striking_weapon_item_variables ->
|
||||||
|
#psu_item{data=Constants} = egs_items_db:read(ItemID),
|
||||||
|
#psu_striking_weapon_item{attack_sound=Sound, hitbox_a=HitboxA, hitbox_b=HitboxB,
|
||||||
|
hitbox_c=HitboxC, hitbox_d=HitboxD, nb_targets=NbTargets, effect=Effect, model=Model} = Constants,
|
||||||
|
<< Category:8, _:24 >> = << ItemID:32 >>,
|
||||||
|
{SoundInt, SoundType} = case Sound of
|
||||||
|
{default, Val} -> {Val, 0};
|
||||||
|
{custom, Val} -> {Val, 8}
|
||||||
|
end,
|
||||||
|
egs_proto:packet_send(Client, << 16#01050300:32, 0:64, TargetGID:32/little, 0:64, 16#00011300:32, GID:32/little, 0:64,
|
||||||
|
TargetGID:32/little, TargetLID:32/little, ItemIndex:8, 1:8, Category:8, A:8, B:32/little,
|
||||||
|
SoundInt:32/little, HitboxA:16, HitboxB:16, HitboxC:16, HitboxD:16, SoundType:4, NbTargets:4, 0:8, Effect:8, Model:8 >>);
|
||||||
|
{ItemID, Variables} when element(1, Variables) =:= psu_trap_item_variables ->
|
||||||
|
#psu_item{data=#psu_trap_item{effect=Effect, type=Type}} = egs_items_db:read(ItemID),
|
||||||
|
<< Category:8, _:24 >> = << ItemID:32 >>,
|
||||||
|
Bin = case Type of
|
||||||
|
damage -> << Effect:8, 16#0c0a05:24, 16#20140500:32, 16#0001c800:32, 16#10000000:32 >>;
|
||||||
|
damage_g -> << Effect:8, 16#2c0505:24, 16#0c000600:32, 16#00049001:32, 16#10000000:32 >>;
|
||||||
|
trap -> << Effect:8, 16#0d0a05:24, 16#61140000:32, 16#0001c800:32, 16#10000000:32 >>;
|
||||||
|
trap_g -> << Effect:8, 16#4d0505:24, 16#4d000000:32, 16#00049001:32, 16#10000000:32 >>;
|
||||||
|
trap_ex -> << Effect:8, 16#490a05:24, 16#4500000f:32, 16#4b055802:32, 16#10000000:32 >>
|
||||||
|
end,
|
||||||
|
egs_proto:packet_send(Client, << 16#01050300:32, 0:64, TargetGID:32/little, 0:64, 16#00011300:32, GID:32/little, 0:64,
|
||||||
|
TargetGID:32/little, TargetLID:32/little, ItemIndex:8, 1:8, Category:8, A:8, B:32/little, Bin/binary >>);
|
||||||
|
undefined ->
|
||||||
|
%% @todo Shouldn't be needed later when NPCs are handled correctly.
|
||||||
|
ignore
|
||||||
|
end;
|
||||||
|
|
||||||
|
event({item_set_trap, ItemIndex, TargetGID, TargetLID, A, B}, Client=#egs_net{gid=GID}) ->
|
||||||
|
{ItemID, _Variables} = egs_users:item_nth(GID, ItemIndex),
|
||||||
|
egs_users:item_qty_add(GID, ItemIndex, -1),
|
||||||
|
<< Category:8, _:24 >> = << ItemID:32 >>,
|
||||||
|
egs_proto:packet_send(Client, << 16#01050300:32, 0:64, TargetGID:32/little, 0:64, 16#00011300:32, GID:32/little, 0:64,
|
||||||
|
TargetGID:32/little, TargetLID:32/little, ItemIndex:8, 9:8, Category:8, A:8, B:32/little >>);
|
||||||
|
|
||||||
|
%% @todo A and B are unknown.
|
||||||
|
%% @see item_equip
|
||||||
|
event({item_unequip, ItemIndex, TargetGID, TargetLID, A, B}, Client=#egs_net{gid=GID}) ->
|
||||||
|
Category = case ItemIndex of
|
||||||
|
% units would be 8, traps would be 12
|
||||||
|
19 -> 2; % armor
|
||||||
|
Y when Y =:= 5; Y =:= 6; Y =:= 7 -> 0; % clothes
|
||||||
|
_ -> 1 % weapons
|
||||||
|
end,
|
||||||
|
egs_proto:packet_send(Client, << 16#01050300:32, 0:64, GID:32/little, 0:64, 16#00011300:32, GID:32/little,
|
||||||
|
0:64, TargetGID:32/little, TargetLID:32/little, ItemIndex, 2, Category, A, B:32/little >>);
|
||||||
|
|
||||||
|
%% @todo Just ignore the meseta price for now and send the player where he wanna be!
|
||||||
|
event(lobby_transport_request, Client) ->
|
||||||
|
egs_proto:send_0c08(Client);
|
||||||
|
|
||||||
|
event(lumilass_options_request, Client=#egs_net{gid=GID}) ->
|
||||||
|
{ok, User} = egs_users:read(GID),
|
||||||
|
egs_proto:send_1a03(User, Client);
|
||||||
|
|
||||||
|
%% @todo Probably replenish the player HP when entering a non-mission area rather than when aborting the mission?
|
||||||
|
event(mission_abort, Client=#egs_net{gid=GID}) ->
|
||||||
|
egs_proto:send_1006(11, Client),
|
||||||
|
{ok, User} = egs_users:read(GID),
|
||||||
|
%% delete the mission
|
||||||
|
if User#users.instancepid =:= undefined -> ignore;
|
||||||
|
true -> psu_instance:stop(User#users.instancepid)
|
||||||
|
end,
|
||||||
|
%% full hp
|
||||||
|
User2 = User#users{currenthp=User#users.maxhp, instancepid=undefined},
|
||||||
|
egs_users:write(User2),
|
||||||
|
%% map change
|
||||||
|
if User2#users.areatype =:= mission ->
|
||||||
|
PrevArea = User2#users.prev_area,
|
||||||
|
event({area_change, element(1, PrevArea), element(2, PrevArea), element(3, PrevArea), User2#users.prev_entryid}, Client);
|
||||||
|
true -> ignore
|
||||||
|
end;
|
||||||
|
|
||||||
|
%% @todo Forward the mission start to other players of the same party, whatever their location is.
|
||||||
|
event({mission_start, QuestID}, Client) ->
|
||||||
|
io:format("~p: mission start ~b~n", [Client#egs_net.gid, QuestID]),
|
||||||
|
egs_proto:send_1020(Client),
|
||||||
|
egs_proto:send_1015(QuestID, Client),
|
||||||
|
egs_proto:send_0c02(Client);
|
||||||
|
|
||||||
|
%% @doc Force the invite of an NPC character while inside a mission. Mostly used by story missions.
|
||||||
|
%% Note that the NPC is often removed and reinvited between block/cutscenes.
|
||||||
|
event({npc_force_invite, NPCid}, Client=#egs_net{gid=GID}) ->
|
||||||
|
{ok, User} = egs_users:read(GID),
|
||||||
|
%% Create NPC.
|
||||||
|
io:format("~p: npc force invite ~p~n", [GID, NPCid]),
|
||||||
|
TmpNPCUser = egs_npc_db:create(NPCid, User#users.level),
|
||||||
|
%% Create and join party.
|
||||||
|
case User#users.partypid of
|
||||||
|
undefined ->
|
||||||
|
{ok, PartyPid} = psu_party:start_link(GID);
|
||||||
|
PartyPid ->
|
||||||
|
ignore
|
||||||
|
end,
|
||||||
|
{ok, PartyPos} = psu_party:join(PartyPid, npc, TmpNPCUser#users.gid),
|
||||||
|
#users{instancepid=InstancePid, area=Area, entryid=EntryID, pos=Pos} = User,
|
||||||
|
NPCUser = TmpNPCUser#users{lid=PartyPos, partypid=PartyPid, instancepid=InstancePid, areatype=mission, area=Area, entryid=EntryID, pos=Pos},
|
||||||
|
egs_users:write(NPCUser),
|
||||||
|
egs_users:write(User#users{partypid=PartyPid}),
|
||||||
|
%% Send stuff.
|
||||||
|
egs_proto:send_010d(NPCUser, Client),
|
||||||
|
egs_proto:send_0201(NPCUser, Client),
|
||||||
|
egs_proto:send_0215(0, Client),
|
||||||
|
egs_proto:send_0a04(NPCUser#users.gid, Client),
|
||||||
|
egs_proto:send_022c(0, 16#12, Client),
|
||||||
|
egs_proto:send_1004(npc_mission, NPCUser, PartyPos, Client),
|
||||||
|
egs_proto:send_100f(NPCUser#users.npcid, PartyPos, Client),
|
||||||
|
egs_proto:send_1601(PartyPos, Client);
|
||||||
|
|
||||||
|
%% @todo Also at the end send a 101a (NPC:16, PartyPos:16, ffffffff). Not sure about PartyPos.
|
||||||
|
event({npc_invite, NPCid}, Client=#egs_net{gid=GID}) ->
|
||||||
|
{ok, User} = egs_users:read(GID),
|
||||||
|
%% Create NPC.
|
||||||
|
io:format("~p: invited npcid ~b~n", [GID, NPCid]),
|
||||||
|
TmpNPCUser = egs_npc_db:create(NPCid, User#users.level),
|
||||||
|
%% Create and join party.
|
||||||
|
case User#users.partypid of
|
||||||
|
undefined ->
|
||||||
|
{ok, PartyPid} = psu_party:start_link(GID),
|
||||||
|
egs_proto:send_022c(0, 16#12, Client);
|
||||||
|
PartyPid ->
|
||||||
|
ignore
|
||||||
|
end,
|
||||||
|
{ok, PartyPos} = psu_party:join(PartyPid, npc, TmpNPCUser#users.gid),
|
||||||
|
NPCUser = TmpNPCUser#users{lid=PartyPos, partypid=PartyPid},
|
||||||
|
egs_users:write(NPCUser),
|
||||||
|
egs_users:write(User#users{partypid=PartyPid}),
|
||||||
|
%% Send stuff.
|
||||||
|
egs_proto:send_1004(npc_invite, NPCUser, PartyPos, Client),
|
||||||
|
egs_proto:send_101a(NPCid, PartyPos, Client);
|
||||||
|
|
||||||
|
%% @todo Should be 0115(money) 010a03(confirm sale).
|
||||||
|
event({npc_shop_buy, ShopItemIndex, QuantityOrColor}, Client=#egs_net{gid=GID}) ->
|
||||||
|
ShopID = egs_users:shop_get(GID),
|
||||||
|
ItemID = egs_shops_db:nth(ShopID, ShopItemIndex + 1),
|
||||||
|
io:format("~p: npc shop ~p buy itemid ~8.16.0b quantity/color+1 ~p~n", [GID, ShopID, ItemID, QuantityOrColor]),
|
||||||
|
#psu_item{name=Name, rarity=Rarity, buy_price=BuyPrice, sell_price=SellPrice, data=Constants} = egs_items_db:read(ItemID),
|
||||||
|
{Quantity, Variables} = case element(1, Constants) of
|
||||||
|
psu_clothing_item ->
|
||||||
|
if QuantityOrColor >= 1, QuantityOrColor =< 10 ->
|
||||||
|
{1, #psu_clothing_item_variables{color=QuantityOrColor - 1}}
|
||||||
|
end;
|
||||||
|
psu_consumable_item ->
|
||||||
|
{QuantityOrColor, #psu_consumable_item_variables{quantity=QuantityOrColor}};
|
||||||
|
psu_parts_item ->
|
||||||
|
{1, #psu_parts_item_variables{}};
|
||||||
|
psu_special_item ->
|
||||||
|
{1, #psu_special_item_variables{}};
|
||||||
|
psu_striking_weapon_item ->
|
||||||
|
#psu_striking_weapon_item{pp=PP, shop_element=Element} = Constants,
|
||||||
|
{1, #psu_striking_weapon_item_variables{current_pp=PP, max_pp=PP, element=Element}};
|
||||||
|
psu_trap_item ->
|
||||||
|
{QuantityOrColor, #psu_trap_item_variables{quantity=QuantityOrColor}}
|
||||||
|
end,
|
||||||
|
egs_users:money_add(GID, -1 * BuyPrice * Quantity),
|
||||||
|
ItemUUID = egs_users:item_add(GID, ItemID, Variables),
|
||||||
|
{ok, User} = egs_users:read(GID),
|
||||||
|
egs_proto:send_0115(User, Client), %% @todo This one is apparently broadcast to everyone in the same zone.
|
||||||
|
%% @todo Following command isn't done 100% properly.
|
||||||
|
UCS2Name = << << X:8, 0:8 >> || X <- Name >>,
|
||||||
|
NamePadding = 8 * (46 - byte_size(UCS2Name)),
|
||||||
|
<< Category:8, _:24 >> = << ItemID:32 >>,
|
||||||
|
RarityInt = Rarity - 1,
|
||||||
|
egs_proto:packet_send(Client, << 16#010a0300:32, 0:64, GID:32/little, 0:64, 16#00011300:32, GID:32/little, 0:64,
|
||||||
|
GID:32/little, 0:32, 2:16/little, 0:16, (egs_proto:build_item_variables(ItemID, ItemUUID, Variables))/binary,
|
||||||
|
UCS2Name/binary, 0:NamePadding, RarityInt:8, Category:8, SellPrice:32/little, (egs_proto:build_item_constants(Constants))/binary >>);
|
||||||
|
|
||||||
|
%% @todo Currently send the normal items shop for all shops, differentiate.
|
||||||
|
event({npc_shop_enter, ShopID}, Client=#egs_net{gid=GID}) ->
|
||||||
|
io:format("~p: npc shop enter ~p~n", [GID, ShopID]),
|
||||||
|
egs_users:shop_enter(GID, ShopID),
|
||||||
|
egs_proto:send_010a(egs_shops_db:read(ShopID), Client);
|
||||||
|
|
||||||
|
event({npc_shop_leave, ShopID}, Client=#egs_net{gid=GID}) ->
|
||||||
|
io:format("~p: npc shop leave ~p~n", [GID, ShopID]),
|
||||||
|
egs_users:shop_leave(GID),
|
||||||
|
egs_proto:packet_send(Client, << 16#010a0300:32, 0:64, GID:32/little, 0:64, 16#00011300:32,
|
||||||
|
GID:32/little, 0:64, GID:32/little, 0:32 >>);
|
||||||
|
|
||||||
|
%% @todo Should be 0115(money) 010a03(confirm sale).
|
||||||
|
event({npc_shop_sell, InventoryItemIndex, Quantity}, Client) ->
|
||||||
|
io:format("~p: npc shop sell itemindex ~p quantity ~p~n", [Client#egs_net.gid, InventoryItemIndex, Quantity]);
|
||||||
|
|
||||||
|
%% @todo First 1a02 value should be non-0.
|
||||||
|
%% @todo Could the 2nd 1a02 parameter simply be the shop type or something?
|
||||||
|
%% @todo Although the values replied should be right, they seem mostly ignored by the client.
|
||||||
|
event({npc_shop_request, ShopID}, Client) ->
|
||||||
|
io:format("~p: npc shop request ~p~n", [Client#egs_net.gid, ShopID]),
|
||||||
|
case ShopID of
|
||||||
|
80 -> egs_proto:send_1a02(17, 17, 3, 9, Client); %% lumilass
|
||||||
|
90 -> egs_proto:send_1a02(5, 1, 4, 5, Client); %% parum weapon grinding
|
||||||
|
91 -> egs_proto:send_1a02(5, 5, 4, 7, Client); %% tenora weapon grinding
|
||||||
|
92 -> egs_proto:send_1a02(5, 8, 4, 0, Client); %% yohmei weapon grinding
|
||||||
|
93 -> egs_proto:send_1a02(5, 18, 4, 0, Client); %% kubara weapon grinding
|
||||||
|
_ -> egs_proto:send_1a02(0, 1, 0, 0, Client)
|
||||||
|
end;
|
||||||
|
|
||||||
|
%% @todo Not sure what are those hardcoded values.
|
||||||
|
event({object_boss_gate_activate, ObjectID}, Client) ->
|
||||||
|
egs_proto:send_1213(ObjectID, 0, Client),
|
||||||
|
egs_proto:send_1215(2, 16#7008, Client),
|
||||||
|
%% @todo Following sent after the warp?
|
||||||
|
egs_proto:send_1213(37, 0, Client),
|
||||||
|
%% @todo Why resend this?
|
||||||
|
egs_proto:send_1213(ObjectID, 0, Client);
|
||||||
|
|
||||||
|
event({object_boss_gate_enter, ObjectID}, Client) ->
|
||||||
|
egs_proto:send_1213(ObjectID, 1, Client);
|
||||||
|
|
||||||
|
%% @todo Do we need to send something back here?
|
||||||
|
event({object_boss_gate_leave, _ObjectID}, _Client) ->
|
||||||
|
ignore;
|
||||||
|
|
||||||
|
event({object_box_destroy, ObjectID}, Client) ->
|
||||||
|
egs_proto:send_1213(ObjectID, 3, Client);
|
||||||
|
|
||||||
|
%% @todo Second send_1211 argument should be User#users.lid. Fix when it's correctly handled.
|
||||||
|
event({object_chair_sit, ObjectTargetID}, Client) ->
|
||||||
|
egs_proto:send_1211(ObjectTargetID, 0, 8, 0, Client);
|
||||||
|
|
||||||
|
%% @todo Second send_1211 argument should be User#users.lid. Fix when it's correctly handled.
|
||||||
|
event({object_chair_stand, ObjectTargetID}, Client) ->
|
||||||
|
egs_proto:send_1211(ObjectTargetID, 0, 8, 2, Client);
|
||||||
|
|
||||||
|
event({object_crystal_activate, ObjectID}, Client) ->
|
||||||
|
egs_proto:send_1213(ObjectID, 1, Client);
|
||||||
|
|
||||||
|
%% @doc Server-side event.
|
||||||
|
event({object_event_trigger, BlockID, EventID}, Client) ->
|
||||||
|
egs_proto:send_1205(EventID, BlockID, 0, Client);
|
||||||
|
|
||||||
|
event({object_goggle_target_activate, ObjectID}, Client=#egs_net{gid=GID}) ->
|
||||||
|
{ok, User} = egs_users:read(GID),
|
||||||
|
{BlockID, EventID} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
|
||||||
|
egs_proto:send_1205(EventID, BlockID, 0, Client),
|
||||||
|
egs_proto:send_1213(ObjectID, 8, Client);
|
||||||
|
|
||||||
|
%% @todo Make NPC characters heal too.
|
||||||
|
event({object_healing_pad_tick, [_PartyPos]}, Client=#egs_net{gid=GID}) ->
|
||||||
|
{ok, User} = egs_users:read(GID),
|
||||||
|
if User#users.currenthp =:= User#users.maxhp -> ignore;
|
||||||
|
true ->
|
||||||
|
NewHP = User#users.currenthp + User#users.maxhp div 10,
|
||||||
|
NewHP2 = if NewHP > User#users.maxhp -> User#users.maxhp; true -> NewHP end,
|
||||||
|
User2 = User#users{currenthp=NewHP2},
|
||||||
|
egs_users:write(User2),
|
||||||
|
egs_proto:send_0117(User2, Client),
|
||||||
|
egs_proto:send_0111(User2, 4, Client)
|
||||||
|
end;
|
||||||
|
|
||||||
|
event({object_key_console_enable, ObjectID}, Client=#egs_net{gid=GID}) ->
|
||||||
|
{ok, User} = egs_users:read(GID),
|
||||||
|
{BlockID, [EventID|_]} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
|
||||||
|
egs_proto:send_1205(EventID, BlockID, 0, Client),
|
||||||
|
egs_proto:send_1213(ObjectID, 1, Client);
|
||||||
|
|
||||||
|
event({object_key_console_init, ObjectID}, Client=#egs_net{gid=GID}) ->
|
||||||
|
{ok, User} = egs_users:read(GID),
|
||||||
|
{BlockID, [_, EventID, _]} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
|
||||||
|
egs_proto:send_1205(EventID, BlockID, 0, Client);
|
||||||
|
|
||||||
|
event({object_key_console_open_gate, ObjectID}, Client=#egs_net{gid=GID}) ->
|
||||||
|
{ok, User} = egs_users:read(GID),
|
||||||
|
{BlockID, [_, _, EventID]} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
|
||||||
|
egs_proto:send_1205(EventID, BlockID, 0, Client),
|
||||||
|
egs_proto:send_1213(ObjectID, 1, Client);
|
||||||
|
|
||||||
|
%% @todo Now that it's separate from object_key_console_enable, handle it better than that, don't need a list of events.
|
||||||
|
event({object_key_enable, ObjectID}, Client=#egs_net{gid=GID}) ->
|
||||||
|
{ok, User} = egs_users:read(GID),
|
||||||
|
{BlockID, [EventID|_]} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
|
||||||
|
egs_proto:send_1205(EventID, BlockID, 0, Client),
|
||||||
|
egs_proto:send_1213(ObjectID, 1, Client);
|
||||||
|
|
||||||
|
%% @todo Some switch objects apparently work differently, like the light switch in Mines in MAG'.
|
||||||
|
event({object_switch_off, ObjectID}, Client=#egs_net{gid=GID}) ->
|
||||||
|
{ok, User} = egs_users:read(GID),
|
||||||
|
{BlockID, EventID} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
|
||||||
|
egs_proto:send_1205(EventID, BlockID, 1, Client),
|
||||||
|
egs_proto:send_1213(ObjectID, 0, Client);
|
||||||
|
|
||||||
|
event({object_switch_on, ObjectID}, Client=#egs_net{gid=GID}) ->
|
||||||
|
{ok, User} = egs_users:read(GID),
|
||||||
|
{BlockID, EventID} = psu_instance:std_event(User#users.instancepid, element(2, User#users.area), ObjectID),
|
||||||
|
egs_proto:send_1205(EventID, BlockID, 0, Client),
|
||||||
|
egs_proto:send_1213(ObjectID, 1, Client);
|
||||||
|
|
||||||
|
event({object_vehicle_boost_enable, ObjectID}, Client) ->
|
||||||
|
egs_proto:send_1213(ObjectID, 1, Client);
|
||||||
|
|
||||||
|
event({object_vehicle_boost_respawn, ObjectID}, Client) ->
|
||||||
|
egs_proto:send_1213(ObjectID, 0, Client);
|
||||||
|
|
||||||
|
%% @todo Second send_1211 argument should be User#users.lid. Fix when it's correctly handled.
|
||||||
|
event({object_warp_take, BlockID, ListNb, ObjectNb}, Client=#egs_net{gid=GID}) ->
|
||||||
|
{ok, User} = egs_users:read(GID),
|
||||||
|
Pos = psu_instance:warp_event(User#users.instancepid, element(2, User#users.area), BlockID, ListNb, ObjectNb),
|
||||||
|
NewUser = User#users{pos=Pos},
|
||||||
|
egs_users:write(NewUser),
|
||||||
|
egs_proto:send_0503(User#users.pos, Client),
|
||||||
|
egs_proto:send_1211(16#ffffffff, 0, 14, 0, Client);
|
||||||
|
|
||||||
|
%% @todo Don't send_0204 if the player is removed from the party while in the lobby I guess.
|
||||||
|
event({party_remove_member, PartyPos}, Client=#egs_net{gid=GID}) ->
|
||||||
|
io:format("~p: party remove member ~b~n", [GID, PartyPos]),
|
||||||
|
{ok, DestUser} = egs_users:read(GID),
|
||||||
|
{ok, RemovedGID} = psu_party:get_member(DestUser#users.partypid, PartyPos),
|
||||||
|
psu_party:remove_member(DestUser#users.partypid, PartyPos),
|
||||||
|
{ok, RemovedUser} = egs_users:read(RemovedGID),
|
||||||
|
case RemovedUser#users.type of
|
||||||
|
npc -> egs_users:delete(RemovedGID);
|
||||||
|
_ -> ignore
|
||||||
|
end,
|
||||||
|
egs_proto:send_1006(8, PartyPos, Client),
|
||||||
|
egs_proto:send_0204(RemovedUser, Client),
|
||||||
|
egs_proto:send_0215(0, Client);
|
||||||
|
|
||||||
|
event({player_options_change, Options}, #egs_net{gid=GID, slot=Slot}) ->
|
||||||
|
Folder = egs_accounts:get_folder(GID),
|
||||||
|
file:write_file(io_lib:format("save/~s/~b-character.options", [Folder, Slot]), Options);
|
||||||
|
|
||||||
|
%% @todo If the player has a scape, use it! Otherwise red screen.
|
||||||
|
%% @todo Right now we force revive with a dummy HP value.
|
||||||
|
event(player_death, Client=#egs_net{gid=GID}) ->
|
||||||
|
% @todo send_0115(GID, 16#ffffffff, LV=1, EXP=idk, Money=1000), % apparently sent everytime you die...
|
||||||
|
%% use scape:
|
||||||
|
NewHP = 10,
|
||||||
|
{ok, User} = egs_users:read(GID),
|
||||||
|
User2 = User#users{currenthp=NewHP},
|
||||||
|
egs_users:write(User2),
|
||||||
|
egs_proto:send_0117(User2, Client),
|
||||||
|
egs_proto:send_1022(User2, Client);
|
||||||
|
%% red screen with return to lobby choice:
|
||||||
|
%~ egs_proto:send_0111(User2, 3, 1, Client);
|
||||||
|
|
||||||
|
%% @todo Refill the player's HP to maximum, remove SEs etc.
|
||||||
|
event(player_death_return_to_lobby, Client=#egs_net{gid=GID}) ->
|
||||||
|
{ok, User} = egs_users:read(GID),
|
||||||
|
PrevArea = User#users.prev_area,
|
||||||
|
event({area_change, element(1, PrevArea), element(2, PrevArea), element(3, PrevArea), User#users.prev_entryid}, Client);
|
||||||
|
|
||||||
|
event(player_type_availability_request, Client) ->
|
||||||
|
egs_proto:send_1a07(Client);
|
||||||
|
|
||||||
|
event(character_type_capabilities_request, Client) ->
|
||||||
|
egs_proto:send_0113(Client);
|
||||||
|
|
||||||
|
event(ppcube_request, Client) ->
|
||||||
|
egs_proto:send_1a04(Client);
|
||||||
|
|
||||||
|
event(unicube_request, Client) ->
|
||||||
|
egs_proto:send_021e(egs_universes:all(), Client);
|
||||||
|
|
||||||
|
%% @todo When selecting 'Your room', don't load a default room that's not yours.
|
||||||
|
event({unicube_select, cancel, _EntryID}, _Client) ->
|
||||||
|
ignore;
|
||||||
|
event({unicube_select, Selection, EntryID}, Client=#egs_net{gid=GID}) ->
|
||||||
|
{ok, User} = egs_users:read(GID),
|
||||||
|
case Selection of
|
||||||
|
16#ffffffff ->
|
||||||
|
UniID = egs_universes:myroomid(),
|
||||||
|
User2 = User#users{uni=UniID, area={1120000, 0, 100}, entryid=0};
|
||||||
|
_ ->
|
||||||
|
UniID = Selection,
|
||||||
|
User2 = User#users{uni=UniID, entryid=EntryID}
|
||||||
|
end,
|
||||||
|
egs_proto:send_0230(Client),
|
||||||
|
%% 0220
|
||||||
|
case User#users.partypid of
|
||||||
|
undefined -> ignore;
|
||||||
|
PartyPid ->
|
||||||
|
%% @todo Replace stop by leave when leaving stops the party correctly when nobody's there anymore.
|
||||||
|
%~ psu_party:leave(User#users.partypid, User#users.gid)
|
||||||
|
{ok, NPCList} = psu_party:get_npc(PartyPid),
|
||||||
|
[egs_users:delete(NPCGID) || {_Spot, NPCGID} <- NPCList],
|
||||||
|
psu_party:stop(PartyPid)
|
||||||
|
end,
|
||||||
|
egs_users:write(User2),
|
||||||
|
egs_universes:leave(User#users.uni),
|
||||||
|
egs_universes:enter(UniID),
|
||||||
|
char_load(User2, Client).
|
||||||
|
|
||||||
|
%% Internal.
|
||||||
|
|
||||||
|
%% @doc Trigger many events.
|
||||||
|
events(Events, Client) ->
|
||||||
|
[event(Event, Client) || Event <- Events],
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% @doc Load and send the character information to the client.
|
||||||
|
char_load(User, Client) ->
|
||||||
|
egs_net:account_character(User#users.char, Client),
|
||||||
|
%% 0246
|
||||||
|
egs_proto:send_0a0a(User#users.inventory, Client),
|
||||||
|
egs_proto:send_1006(5, 0, Client), %% @todo The 0 here is PartyPos, save it in User.
|
||||||
|
egs_proto:send_1005(User, Client),
|
||||||
|
egs_proto:send_1006(12, Client),
|
||||||
|
egs_proto:send_0210(Client),
|
||||||
|
egs_proto:send_0222(User#users.uni, Client),
|
||||||
|
egs_net:comm_own_card(User#users.char, Client),
|
||||||
|
egs_proto:send_1501(Client),
|
||||||
|
egs_proto:send_1512(Client),
|
||||||
|
%% 0303
|
||||||
|
egs_proto:send_1602(Client),
|
||||||
|
egs_proto:send_021b(Client).
|
||||||
|
|
||||||
|
%% @todo Don't change the NPC info unless you are the leader!
|
||||||
|
npc_load(_Leader, [], _Client) ->
|
||||||
|
ok;
|
||||||
|
npc_load(Leader, [{PartyPos, NPCGID}|NPCList], Client) ->
|
||||||
|
{ok, OldNPCUser} = egs_users:read(NPCGID),
|
||||||
|
#users{instancepid=InstancePid, area=Area, entryid=EntryID, pos=Pos} = Leader,
|
||||||
|
NPCUser = OldNPCUser#users{lid=PartyPos, instancepid=InstancePid, areatype=mission, area=Area, entryid=EntryID, pos=Pos},
|
||||||
|
%% @todo This one on mission end/abort?
|
||||||
|
%~ OldNPCUser#users{lid=PartyPos, instancepid=undefined, areatype=AreaType, area={0, 0, 0}, entryid=0, pos={0.0, 0.0, 0.0, 0}}
|
||||||
|
egs_users:write(NPCUser),
|
||||||
|
egs_proto:send_010d(NPCUser, Client),
|
||||||
|
egs_proto:send_0201(NPCUser, Client),
|
||||||
|
egs_proto:send_0215(0, Client),
|
||||||
|
egs_proto:send_0a04(NPCUser#users.gid, Client),
|
||||||
|
egs_proto:send_1004(npc_mission, NPCUser, PartyPos, Client),
|
||||||
|
egs_proto:send_100f(NPCUser#users.npcid, PartyPos, Client),
|
||||||
|
egs_proto:send_1601(PartyPos, Client),
|
||||||
|
egs_proto:send_1016(PartyPos, Client),
|
||||||
|
npc_load(Leader, NPCList, Client).
|
62
apps/egs/src/egs_game_protocol.erl
Normal file
62
apps/egs/src/egs_game_protocol.erl
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
|
%% @doc Cowboy protocol module for the game server.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_game_protocol).
|
||||||
|
-export([start_link/4, init/2]).
|
||||||
|
|
||||||
|
-include("include/records.hrl").
|
||||||
|
|
||||||
|
-spec start_link(pid(), ssl:sslsocket(), module(), []) -> {ok, pid()}.
|
||||||
|
start_link(_ListenerPid, Socket, Transport, []) ->
|
||||||
|
Pid = spawn_link(?MODULE, init, [Socket, Transport]),
|
||||||
|
{ok, Pid}.
|
||||||
|
|
||||||
|
-spec init(ssl:sslsocket(), module()) -> ok.
|
||||||
|
%% @todo Handle keepalive messages globally?
|
||||||
|
init(Socket, Transport) ->
|
||||||
|
{ok, _TRef} = timer:send_interval(5000, {egs, keepalive}),
|
||||||
|
Client = egs_net:init(Socket, Transport, egs_login,
|
||||||
|
egs_accounts:tmp_gid()),
|
||||||
|
egs_net:system_hello(Client),
|
||||||
|
catch egs_net:loop(Client),
|
||||||
|
terminate().
|
||||||
|
|
||||||
|
-spec terminate() -> ok.
|
||||||
|
%% @todo Just use monitors to handle cleanups.
|
||||||
|
%% @todo Cleanup the instance process if there's nobody in it anymore.
|
||||||
|
%% @todo Leave party instead of stopping it.
|
||||||
|
%% @todo Fix the crash when user isn't in egs_users yet.
|
||||||
|
terminate() ->
|
||||||
|
case egs_users:find_by_pid(self()) of
|
||||||
|
undefined -> ok;
|
||||||
|
User ->
|
||||||
|
case User#users.partypid of
|
||||||
|
undefined ->
|
||||||
|
ignore;
|
||||||
|
PartyPid ->
|
||||||
|
{ok, NPCList} = psu_party:get_npc(PartyPid),
|
||||||
|
lists:foreach(fun({_Spot, NPCGID}) ->
|
||||||
|
egs_users:delete(NPCGID) end, NPCList),
|
||||||
|
psu_party:stop(PartyPid)
|
||||||
|
end,
|
||||||
|
egs_zones:leave(User#users.zonepid, User#users.gid),
|
||||||
|
egs_universes:leave(User#users.uni),
|
||||||
|
egs_users:delete(User#users.gid),
|
||||||
|
io:format("game (~p): quit~n", [User#users.gid])
|
||||||
|
end.
|
@ -1,5 +1,5 @@
|
|||||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
%% @copyright 2010 Loïc Hoguin.
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
%% @doc EGS items database.
|
%% @doc EGS items database.
|
||||||
%%
|
%%
|
||||||
%% This file is part of EGS.
|
%% This file is part of EGS.
|
||||||
@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
-module(egs_items_db).
|
-module(egs_items_db).
|
||||||
-behavior(gen_server).
|
-behavior(gen_server).
|
||||||
|
|
||||||
-export([start_link/0, stop/0, desc/1, read/1, reload/0]). %% API.
|
-export([start_link/0, stop/0, desc/1, read/1, reload/0]). %% API.
|
||||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
||||||
|
|
||||||
@ -26,7 +27,7 @@
|
|||||||
-define(SERVER, ?MODULE).
|
-define(SERVER, ?MODULE).
|
||||||
|
|
||||||
-include("include/records.hrl").
|
-include("include/records.hrl").
|
||||||
-include("priv/items.hrl").
|
-include("../../priv/items.hrl").
|
||||||
|
|
||||||
%% API.
|
%% API.
|
||||||
|
|
81
apps/egs/src/egs_login.erl
Normal file
81
apps/egs/src/egs_login.erl
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
|
%% @doc Log in and authentication callback module.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_login).
|
||||||
|
-export([info/2, cast/3, event/2]).
|
||||||
|
|
||||||
|
-include("include/records.hrl").
|
||||||
|
|
||||||
|
%% @doc We don't expect any message here.
|
||||||
|
info(_Msg, _Client) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% @doc Nothing to broadcast.
|
||||||
|
cast(_Command, _Data, _Client) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% Events.
|
||||||
|
|
||||||
|
%% @doc Reject version < 2.0009.2.
|
||||||
|
%% @todo Reject wrong platforms too.
|
||||||
|
%% @todo Put the URL in a configuration file.
|
||||||
|
event({client_version, _Entrance, _Language, _Platform, Version}, Client)
|
||||||
|
when Version < 2009002 ->
|
||||||
|
egs_net:system_open_url(<<"http://psumods.co.uk/forums/comments.php?DiscussionID=40#Item_1">>, Client),
|
||||||
|
{ok, Error} = file:read_file("priv/login/error_version.txt"),
|
||||||
|
egs_net:system_auth_error(Error, Client),
|
||||||
|
egs_net:terminate(Client),
|
||||||
|
closed;
|
||||||
|
event({client_version, _Entrance, _Language, _Platform, _Version}, _Client) ->
|
||||||
|
ok;
|
||||||
|
|
||||||
|
%% @doc Game server info request handler.
|
||||||
|
event(system_game_server_request, Client) ->
|
||||||
|
{ServerIP, ServerPort} = egs_conf:read(game_server),
|
||||||
|
egs_net:system_game_server_response(ServerIP, ServerPort, Client),
|
||||||
|
egs_net:terminate(Client),
|
||||||
|
closed;
|
||||||
|
|
||||||
|
%% @doc Authenticate the user by pattern matching its saved state against the key received.
|
||||||
|
%% If the user is authenticated, send him the character flags list.
|
||||||
|
event({system_key_auth, AuthGID, AuthKey}, Client) ->
|
||||||
|
egs_accounts:key_auth(AuthGID, AuthKey),
|
||||||
|
Client2 = egs_net:set_gid(AuthGID, Client),
|
||||||
|
ValueFlags = egs_conf:read(value_flags),
|
||||||
|
BoolFlags = egs_conf:read(bool_flags),
|
||||||
|
TempFlags = egs_conf:read(temp_flags),
|
||||||
|
egs_net:account_flags(ValueFlags, BoolFlags, TempFlags, Client2),
|
||||||
|
Client3 = egs_net:set_handler(egs_char_select, Client2),
|
||||||
|
Client4 = egs_net:set_keepalive(Client3),
|
||||||
|
{ok, Client4};
|
||||||
|
|
||||||
|
%% @doc Authentication request handler. Currently always succeed.
|
||||||
|
%% @todo Handle real GIDs whenever there's real authentication. GID is the second SessionID in the reply.
|
||||||
|
%% @todo Apparently it's possible to ask a question in the reply here. Used for free course on JP.
|
||||||
|
event({system_login_auth, Username, Password}, Client) ->
|
||||||
|
{ok, AuthGID} = egs_accounts:login_auth(Username, Password),
|
||||||
|
{ok, AuthKey} = egs_accounts:key_auth_init(AuthGID),
|
||||||
|
io:format("auth success for ~s ~s~n", [Username, Password]),
|
||||||
|
egs_net:system_key_auth_info(AuthGID, AuthKey, Client);
|
||||||
|
|
||||||
|
%% @doc MOTD request handler. Page number starts at 0.
|
||||||
|
%% @todo Currently ignore the language and send the same MOTD file to everyone.
|
||||||
|
event({system_motd_request, Page, _Language}, Client) ->
|
||||||
|
{ok, MOTD} = file:read_file("priv/login/motd.txt"),
|
||||||
|
egs_net:system_motd_response(MOTD, Page, Client).
|
@ -1,6 +1,6 @@
|
|||||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
%% @copyright 2010 Loïc Hoguin.
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
%% @doc Login server module.
|
%% @doc Cowboy protocol module for the login server.
|
||||||
%%
|
%%
|
||||||
%% This file is part of EGS.
|
%% This file is part of EGS.
|
||||||
%%
|
%%
|
||||||
@ -17,25 +17,19 @@
|
|||||||
%% You should have received a copy of the GNU Affero General Public License
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
-module(egs_login_server).
|
-module(egs_login_protocol).
|
||||||
-export([start_link/1, on_exit/1, init/1]).
|
-export([start_link/4, init/2]).
|
||||||
|
|
||||||
-include("include/records.hrl").
|
-include("include/records.hrl").
|
||||||
|
|
||||||
%% @spec start_link(Port) -> {ok,Pid::pid()}
|
-spec start_link(pid(), ssl:sslsocket(), module(), []) -> {ok, pid()}.
|
||||||
%% @doc Start the login server.
|
start_link(_ListenerPid, Socket, Transport, []) ->
|
||||||
start_link(Port) ->
|
Pid = spawn_link(?MODULE, init, [Socket, Transport]),
|
||||||
Pid = spawn(egs_network, listen, [Port, ?MODULE]),
|
|
||||||
{ok, Pid}.
|
{ok, Pid}.
|
||||||
|
|
||||||
%% @spec on_exit(Pid) -> ok
|
-spec init(ssl:sslsocket(), module()) -> ok | closed.
|
||||||
%% @doc Nothing to do for the login server.
|
init(Socket, Transport) ->
|
||||||
on_exit(_Pid) ->
|
Client = egs_net:init(Socket, Transport, egs_login,
|
||||||
ok.
|
egs_accounts:tmp_gid()),
|
||||||
|
egs_net:system_hello(Client),
|
||||||
%% @doc Initialize the game state and start receiving messages.
|
egs_net:loop(Client).
|
||||||
init(Socket) ->
|
|
||||||
TmpGID = 16#ff000000 + mnesia:dirty_update_counter(counters, tmpgid, 1),
|
|
||||||
State = #state{socket=Socket, gid=TmpGID},
|
|
||||||
psu_proto:send_0202(State),
|
|
||||||
egs_network:recv(<< >>, egs_login, State).
|
|
@ -1,5 +1,5 @@
|
|||||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
%% @copyright 2010 Loïc Hoguin.
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
%% @doc EGS NPC database.
|
%% @doc EGS NPC database.
|
||||||
%%
|
%%
|
||||||
%% This file is part of EGS.
|
%% This file is part of EGS.
|
||||||
@ -19,14 +19,15 @@
|
|||||||
|
|
||||||
-module(egs_npc_db).
|
-module(egs_npc_db).
|
||||||
-behavior(gen_server).
|
-behavior(gen_server).
|
||||||
-export([start_link/0, stop/0, all/0, count/0, create/2, reload/0]). %% API.
|
|
||||||
|
-export([start_link/0, stop/0, all/0, create/2, reload/0]). %% API.
|
||||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
||||||
|
|
||||||
%% Use the module name for the server's name.
|
%% Use the module name for the server's name.
|
||||||
-define(SERVER, ?MODULE).
|
-define(SERVER, ?MODULE).
|
||||||
|
|
||||||
-include("include/records.hrl").
|
-include("include/records.hrl").
|
||||||
-include("priv/npc.hrl").
|
-include("../../priv/npc.hrl").
|
||||||
|
|
||||||
%% API.
|
%% API.
|
||||||
|
|
||||||
@ -42,10 +43,6 @@ stop() ->
|
|||||||
all() ->
|
all() ->
|
||||||
gen_server:call(?SERVER, all).
|
gen_server:call(?SERVER, all).
|
||||||
|
|
||||||
%% @spec count() -> integer()
|
|
||||||
count() ->
|
|
||||||
gen_server:call(?SERVER, count).
|
|
||||||
|
|
||||||
%% @spec read(NPCid, BaseLevel) -> term()
|
%% @spec read(NPCid, BaseLevel) -> term()
|
||||||
create(NPCid, BaseLevel) ->
|
create(NPCid, BaseLevel) ->
|
||||||
gen_server:call(?SERVER, {create, NPCid, BaseLevel}).
|
gen_server:call(?SERVER, {create, NPCid, BaseLevel}).
|
||||||
@ -63,19 +60,17 @@ init([]) ->
|
|||||||
handle_call(all, _From, State) ->
|
handle_call(all, _From, State) ->
|
||||||
{reply, ?NPC, State};
|
{reply, ?NPC, State};
|
||||||
|
|
||||||
handle_call(count, _From, State) ->
|
|
||||||
{reply, length(?NPC), State};
|
|
||||||
|
|
||||||
%% @todo Handle stats, experience, based on level.
|
%% @todo Handle stats, experience, based on level.
|
||||||
handle_call({create, NPCid, BaseLevel}, _From, State) ->
|
handle_call({create, NPCid, BaseLevel}, _From, State) ->
|
||||||
NPCGID = 16#ff000000 + mnesia:dirty_update_counter(counters, tmpgid, 1),
|
NPCGID = egs_accounts:tmp_gid(),
|
||||||
#npc{name=Name, race=Race, gender=Gender, class=Class, level_diff=LevelDiff, appearance=Appearance} = proplists:get_value(NPCid, ?NPC),
|
#npc{name=Name, race=Race, gender=Gender, class=Class, level_diff=LevelDiff, appearance=Appearance} = proplists:get_value(NPCid, ?NPC),
|
||||||
TmpUCS2Name = << << X:8, 0:8 >> || X <- Name >>,
|
TmpUCS2Name = << << X:8, 0:8 >> || X <- Name >>,
|
||||||
Padding = 8 * (64 - byte_size(TmpUCS2Name)),
|
Padding = 8 * (64 - byte_size(TmpUCS2Name)),
|
||||||
UCS2Name = << TmpUCS2Name/binary, 0:Padding >>,
|
UCS2Name = << TmpUCS2Name/binary, 0:Padding >>,
|
||||||
Character = #characters{gid=NPCGID, slot=0, type=npc, npcid=NPCid, name=UCS2Name, race=Race, gender=Gender, class=Class, appearance=Appearance,
|
User = #users{gid=NPCGID, slot=0, type=npc, npcid=NPCid, name=UCS2Name, race=Race, gender=Gender, class=Class, appearance=Appearance,
|
||||||
mainlevel={level, calc_level(BaseLevel, LevelDiff), 0}, blastbar=0, luck=2, money=0, playtime=0, stats={stats, 0, 0, 0, 0, 0, 0, 0}, se=[], currenthp=100, maxhp=100},
|
level=calc_level(BaseLevel, LevelDiff), blastbar=0, luck=2, money=0,
|
||||||
User = #egs_user_model{id=NPCGID, character=Character, areatype=lobby, area={psu_area, 0, 0, 0}, entryid=0},
|
stats={stats, 0, 0, 0, 0, 0, 0, 0}, currenthp=100, maxhp=100,
|
||||||
|
areatype=lobby, area={0, 0, 0}, entryid=0},
|
||||||
{reply, User, State};
|
{reply, User, State};
|
||||||
|
|
||||||
handle_call(stop, _From, State) ->
|
handle_call(stop, _From, State) ->
|
726
apps/egs/src/egs_proto.erl
Normal file
726
apps/egs/src/egs_proto.erl
Normal file
@ -0,0 +1,726 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
|
%% @doc Independent implementation of the PSU protocol.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_proto).
|
||||||
|
-compile(export_all).
|
||||||
|
|
||||||
|
-include("include/records.hrl").
|
||||||
|
|
||||||
|
%% @spec assert() -> ok
|
||||||
|
%% @doc Log a detailed message when the function is called.
|
||||||
|
-define(ASSERT(), io:format("assert error in module ~p on line ~p~n", [?MODULE, ?LINE])).
|
||||||
|
|
||||||
|
%% @spec assert(A, B) -> ok
|
||||||
|
%% @doc Log a detailed message when the assertion A =:= B fails.
|
||||||
|
-define(ASSERT_EQ(A, B), if A =:= B -> ok; true -> io:format("assert error in module ~p on line ~p~n", [?MODULE, ?LINE]) end).
|
||||||
|
|
||||||
|
%% @doc Send a shop listing.
|
||||||
|
%% @todo This packet (and its build_010a_list function) hasn't been reviewed at all yet.
|
||||||
|
send_010a(ItemsList, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
NbItems = length(ItemsList),
|
||||||
|
ItemsBin = build_010a_list(ItemsList, []),
|
||||||
|
packet_send(Client, << 16#010a0300:32, 0:64, DestGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64,
|
||||||
|
DestGID:32/little, 0:32, 1:16/little, NbItems:8, 2:8, 0:32, ItemsBin/binary >>).
|
||||||
|
|
||||||
|
%% @todo The values set to 0 are unknown.
|
||||||
|
build_010a_list([], Acc) ->
|
||||||
|
iolist_to_binary(lists:reverse(Acc));
|
||||||
|
build_010a_list([ItemID|Tail], Acc) ->
|
||||||
|
#psu_item{name=Name, rarity=Rarity, buy_price=SellPrice, data=Data} = egs_items_db:read(ItemID),
|
||||||
|
UCS2Name = << << X:8, 0:8 >> || X <- Name >>,
|
||||||
|
NamePadding = 8 * (46 - byte_size(UCS2Name)),
|
||||||
|
RarityBin = Rarity - 1,
|
||||||
|
DataBin = build_item_constants(Data),
|
||||||
|
BinItemID = case element(1, Data) of
|
||||||
|
psu_clothing_item -> %% Change the ItemID to enable all colors.
|
||||||
|
<< A:8, _:4, B:12, _:8 >> = << ItemID:32 >>,
|
||||||
|
<< A:8, 3:4, B:12, 16#ff:8 >>;
|
||||||
|
_Any ->
|
||||||
|
<< ItemID:32 >>
|
||||||
|
end,
|
||||||
|
Bin = << UCS2Name/binary, 0:NamePadding, RarityBin:8, 0:8, BinItemID/binary, SellPrice:32/little, DataBin/binary >>,
|
||||||
|
build_010a_list(Tail, [Bin|Acc]).
|
||||||
|
|
||||||
|
%% @doc Send character appearance and other information.
|
||||||
|
%% @todo Probably don't pattern match the data like this...
|
||||||
|
send_010d(CharUser, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
|
||||||
|
CharGID = CharUser#users.gid,
|
||||||
|
CharLID = CharUser#users.lid,
|
||||||
|
<< _:640, CharBin/bits >> = psu_characters:character_user_to_binary(CharUser),
|
||||||
|
packet_send(Client, << 16#010d0300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little,
|
||||||
|
0:64, 1:32/little, 0:32, 16#00000300:32, 16#ffff0000:32, 0:32, CharGID:32/little,
|
||||||
|
0:192, CharGID:32/little, CharLID:32/little, 16#ffffffff:32, CharBin/binary >>).
|
||||||
|
|
||||||
|
%% @doc Trigger a character-related event.
|
||||||
|
send_0111(CharUser, EventID, Client) ->
|
||||||
|
send_0111(CharUser, EventID, 0, Client).
|
||||||
|
send_0111(#users{gid=CharGID, lid=CharLID}, EventID, Param, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
|
||||||
|
packet_send(Client, << 16#01110300:32, DestLID:16/little, 0:48, CharGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64,
|
||||||
|
CharGID:32/little, CharLID:32/little, EventID:32/little, Param:32/little >>).
|
||||||
|
|
||||||
|
%% @todo Types capability list.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_0113(Client=#egs_net{gid=DestGID}) ->
|
||||||
|
{ok, File} = file:read_file("p/typesinfo.bin"),
|
||||||
|
packet_send(Client, << 16#01130300:32, 0:64, DestGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64, DestGID:32/little, File/binary >>).
|
||||||
|
|
||||||
|
%% @doc Update the character level, blastbar, luck and money information.
|
||||||
|
send_0115(User, Client) ->
|
||||||
|
send_0115(User, 16#ffffffff, Client).
|
||||||
|
send_0115(User=#users{gid=CharGID, lid=CharLID}, EnemyTargetID, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
|
||||||
|
packet_send(Client, << 16#01150300:32, DestLID:16/little, 0:48, CharGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64,
|
||||||
|
CharGID:32/little, CharLID:32/little, EnemyTargetID:32/little, (build_char_level(User))/binary >>).
|
||||||
|
|
||||||
|
%% @doc Revive player with optional SEs.
|
||||||
|
%% @todo SEs.
|
||||||
|
send_0117(#users{gid=CharGID, lid=CharLID, currenthp=HP}, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
|
||||||
|
SE = << 0:64 >>,
|
||||||
|
packet_send(Client, << 16#01170300:32, DestLID:16/little, 0:48, CharGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64,
|
||||||
|
CharGID:32/little, CharLID:32/little, SE/binary, HP:32/little, 0:32 >>).
|
||||||
|
|
||||||
|
%% @doc Send the zone initialization command.
|
||||||
|
%% @todo Handle NbPlayers properly. There's more than 1 player!
|
||||||
|
send_0200(ZoneID, ZoneType, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
|
||||||
|
Var = case ZoneType of
|
||||||
|
mission -> << 16#06000500:32, 16#01000000:32, 0:64, 16#00040000:32, 16#00010000:32, 16#00140000:32 >>;
|
||||||
|
myroom -> << 16#06000000:32, 16#02000000:32, 0:64, 16#40000000:32, 16#00010000:32, 16#00010000:32 >>;
|
||||||
|
_ -> << 16#00040000:32, 0:160, 16#00140000:32 >>
|
||||||
|
end,
|
||||||
|
packet_send(Client, << 16#02000300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64,
|
||||||
|
DestLID:16/little, ZoneID:16/little, 1:32/little, 16#ffffffff:32, Var/binary, 16#ffffffff:32, 16#ffffffff:32 >>).
|
||||||
|
|
||||||
|
%% @doc Send character location, appearance and other information.
|
||||||
|
send_0201(CharUser, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
|
||||||
|
[CharTypeID, GameVersion] = case CharUser#users.type of
|
||||||
|
npc -> [16#00001d00, 255];
|
||||||
|
_ -> [16#00001200, 0]
|
||||||
|
end,
|
||||||
|
CharGID = CharUser#users.gid,
|
||||||
|
CharBin = psu_characters:character_user_to_binary(CharUser),
|
||||||
|
IsGM = 0,
|
||||||
|
OnlineStatus = 0,
|
||||||
|
packet_send(Client, << 16#02010300:32, DestLID:16/little, 0:16, CharTypeID:32, CharGID:32/little,
|
||||||
|
0:64, 16#00011300:32, DestGID:32/little, 0:64, CharBin/binary, IsGM:8, 0:8, OnlineStatus:8, GameVersion:8, 0:608 >>).
|
||||||
|
|
||||||
|
%% @doc Spawn a player with the given GID and LID.
|
||||||
|
send_0203(#users{gid=CharGID, lid=CharLID}, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
|
||||||
|
packet_send(Client, << 16#02030300:32, DestLID:16/little, 0:144, 16#00011300:32,
|
||||||
|
DestGID:32/little, 0:64, CharGID:32/little, CharLID:32/little >>).
|
||||||
|
|
||||||
|
%% @doc Unspawn the given character.
|
||||||
|
%% @todo The last 4 bytes are probably the number of players remaining in the zone.
|
||||||
|
send_0204(User, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
|
||||||
|
CharTypeID = case User#users.type of
|
||||||
|
npc -> 16#00001d00;
|
||||||
|
_ -> 16#00001200
|
||||||
|
end,
|
||||||
|
#users{gid=CharGID, lid=CharLID} = User,
|
||||||
|
packet_send(Client, << 16#02040300:32, DestLID:16/little, 0:16, CharTypeID:32, CharGID:32/little, 0:64,
|
||||||
|
16#00011300:32, DestGID:32/little, 0:64, CharGID:32/little, CharLID:32/little, 100:32/little >>).
|
||||||
|
|
||||||
|
%% @doc Make the client load a new map.
|
||||||
|
send_0205(CharUser, IsSeasonal, Client=#egs_net{gid=DestGID, lid=DestLID, areanb=AreaNb}) ->
|
||||||
|
#users{lid=CharLID, area={_QuestID, ZoneID, MapID}, entryid=EntryID} = CharUser,
|
||||||
|
packet_send(Client, << 16#02050300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64,
|
||||||
|
16#ffffffff:32, ZoneID:32/little, MapID:32/little, EntryID:32/little, AreaNb:32/little, CharLID:16/little, 0:8, IsSeasonal:8 >>).
|
||||||
|
|
||||||
|
%% @doc Indicate to the client that loading should finish.
|
||||||
|
send_0208(Client=#egs_net{gid=DestGID, lid=DestLID, areanb=AreaNb}) ->
|
||||||
|
packet_send(Client, << 16#02080300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64, AreaNb:32/little >>).
|
||||||
|
|
||||||
|
%% @todo No idea what this one does. For unknown reasons it uses channel 2.
|
||||||
|
%% @todo Handle the DestLID properly?
|
||||||
|
send_020c(Client) ->
|
||||||
|
packet_send(Client, << 16#020c0200:32, 16#ffff0000:32, 0:256 >>).
|
||||||
|
|
||||||
|
%% @doc Send the quest file to be loaded by the client.
|
||||||
|
%% @todo Handle the DestLID properly?
|
||||||
|
send_020e(QuestData, Client) ->
|
||||||
|
Size = byte_size(QuestData),
|
||||||
|
packet_send(Client, << 16#020e0300:32, 16#ffff:16, 0:272, Size:32/little, 0:32, QuestData/binary, 0:32 >>).
|
||||||
|
|
||||||
|
%% @doc Send the zone file to be loaded.
|
||||||
|
send_020f(ZoneData, SetID, SeasonID, Client) ->
|
||||||
|
Size = byte_size(ZoneData),
|
||||||
|
packet_send(Client, << 16#020f0300:32, 16#ffff:16, 0:272, SetID, SeasonID, 0:16, Size:32/little, ZoneData/binary >>).
|
||||||
|
|
||||||
|
%% @doc Send the current UNIX time.
|
||||||
|
send_0210(Client=#egs_net{gid=DestGID, lid=DestLID}) ->
|
||||||
|
{M, S, _} = erlang:now(),
|
||||||
|
UnixTime = M * 1000000 + S,
|
||||||
|
packet_send(Client, << 16#02100300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:96, UnixTime:32/little >>).
|
||||||
|
|
||||||
|
%% @todo No idea what this is doing.
|
||||||
|
send_0215(UnknownValue, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
|
||||||
|
packet_send(Client, << 16#02150300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64, UnknownValue:32/little >>).
|
||||||
|
|
||||||
|
%% @doc End of character loading.
|
||||||
|
send_021b(Client=#egs_net{gid=DestGID, lid=DestLID}) ->
|
||||||
|
packet_send(Client, << 16#021b0300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64 >>).
|
||||||
|
|
||||||
|
%% @doc Send the list of available universes.
|
||||||
|
send_021e(Universes, Client) ->
|
||||||
|
NbUnis = length(Universes),
|
||||||
|
UnisBin = build_021e_uni(Universes, []),
|
||||||
|
packet_send(Client, << 16#021e0300:32, 0:288, NbUnis:32/little, UnisBin/binary >>).
|
||||||
|
|
||||||
|
build_021e_uni([], Acc) ->
|
||||||
|
iolist_to_binary(lists:reverse(Acc));
|
||||||
|
build_021e_uni([{_UniID, {myroom, Name, NbPlayers, _MaxPlayers}}|Tail], Acc) ->
|
||||||
|
Padding = 8 * (44 - byte_size(Name)),
|
||||||
|
Bin = << 16#ffffffff:32, NbPlayers:16/little, 0:16, Name/binary, 0:Padding >>,
|
||||||
|
build_021e_uni(Tail, [Bin|Acc]);
|
||||||
|
build_021e_uni([{UniID, {universe, Name, NbPlayers, _MaxPlayers}}|Tail], Acc) ->
|
||||||
|
Padding = 8 * (32 - byte_size(Name)),
|
||||||
|
PopString = lists:flatten(io_lib:format("~5b", [NbPlayers])),
|
||||||
|
PopString2 = << << X:8, 0:8 >> || X <- PopString >>,
|
||||||
|
Bin = << UniID:32/little, NbPlayers:16/little, 643:16/little, Name/binary, 0:Padding, PopString2/binary, 0:16 >>,
|
||||||
|
build_021e_uni(Tail, [Bin|Acc]).
|
||||||
|
|
||||||
|
%% @doc Send the current universe info along with the current level cap.
|
||||||
|
send_0222(UniID, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
{_Type, Name, NbPlayers, MaxPlayers} = egs_universes:read(UniID),
|
||||||
|
Padding = 8 * (44 - byte_size(Name)),
|
||||||
|
LevelCap = egs_conf:read(level_cap),
|
||||||
|
packet_send(Client, << 16#02220300:32, 16#ffff:16, 0:16, 16#00001200:32, DestGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64,
|
||||||
|
UniID:32/little, NbPlayers:16/little, MaxPlayers:16/little, Name/binary, 0:Padding, LevelCap:32/little >>).
|
||||||
|
|
||||||
|
%% @doc Display a notice on the player's screen.
|
||||||
|
%% There are four types of notices: dialog, top, scroll and timeout.
|
||||||
|
%% * dialog: A dialog in the center of the screen, which can be OK'd by players.
|
||||||
|
%% * top: Horizontal scroll on top of the screen, traditionally used for server-wide messages.
|
||||||
|
%% * scroll: Vertical scroll on the right of the screen, traditionally used for rare missions obtention messages.
|
||||||
|
%% * timeout: A dialog in the center of the screen that disappears after Duration seconds.
|
||||||
|
send_0228(Type, Duration, Message, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
TypeInt = case Type of dialog -> 0; top -> 1; scroll -> 2; timeout -> 3 end,
|
||||||
|
UCS2Message = << << X:8, 0:8 >> || X <- Message >>,
|
||||||
|
packet_send(Client, << 16#02280300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64,
|
||||||
|
TypeInt:32/little, Duration:32/little, UCS2Message/binary, 0:16 >>).
|
||||||
|
|
||||||
|
%% @todo No idea!
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_022c(A, B, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#022c0300:32, 0:160, 16#00011300:32, DestGID:32/little, 0:64, A:16/little, B:16/little >>).
|
||||||
|
|
||||||
|
%% @todo Not sure. Sent when going to or from room. Possibly when changing universes too?
|
||||||
|
send_0230(Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#02300300:32, 16#ffff:16, 0:16, 16#00011300:32, DestGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64 >>).
|
||||||
|
|
||||||
|
%% @doc Send the list of players already spawned in the zone when entering it.
|
||||||
|
send_0233(Users, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
|
||||||
|
NbUsers = length(Users),
|
||||||
|
Bin = build_0233_users(Users, []),
|
||||||
|
packet_send(Client, << 16#02330300:32, DestLID:16/little, 0:16, 16#00001200:32, DestGID:32/little, 0:64,
|
||||||
|
16#00011300:32, DestGID:32/little, 0:64, NbUsers:32/little, Bin/binary, 0:608 >>).
|
||||||
|
|
||||||
|
build_0233_users([], Acc) ->
|
||||||
|
iolist_to_binary(lists:reverse(Acc));
|
||||||
|
build_0233_users([User|Tail], Acc) ->
|
||||||
|
Bin = psu_characters:character_user_to_binary(User),
|
||||||
|
build_0233_users(Tail, [<< Bin/binary, 0:32 >>|Acc]).
|
||||||
|
|
||||||
|
%% @doc Start the zone handling: load the zone file and the objects sent separately.
|
||||||
|
send_0236(Client=#egs_net{gid=DestGID, lid=DestLID}) ->
|
||||||
|
packet_send(Client, << 16#02360300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64 >>).
|
||||||
|
|
||||||
|
%% @doc Chat message.
|
||||||
|
send_0304(FromGID, ChatTypeID, ChatGID, ChatName, ChatModifiers, ChatMessage, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
|
||||||
|
{chat_modifiers, ChatType, ChatCutIn, ChatCutInAngle, ChatMsgLength, ChatChannel, ChatCharacterType} = ChatModifiers,
|
||||||
|
packet_send(Client, << 16#03040300:32, DestLID:16/little, 0:16, 16#00011300:32, FromGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64,
|
||||||
|
ChatTypeID:32, ChatGID:32/little, 0:64, ChatType:8, ChatCutIn:8, ChatCutInAngle:8, ChatMsgLength:8,
|
||||||
|
ChatChannel:8, ChatCharacterType:8, 0:16, ChatName/binary, ChatMessage/binary >>).
|
||||||
|
|
||||||
|
%% @todo Force send a new player location. Used for warps.
|
||||||
|
%% @todo The value before IntDir seems to be the player's current animation. 01 stand up, 08 ?, 17 normal sit
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_0503({PrevX, PrevY, PrevZ, _AnyDir}, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
{ok, User} = egs_users:read(DestGID),
|
||||||
|
#users{pos={X, Y, Z, Dir}, area={QuestID, ZoneID, MapID}, entryid=EntryID} = User,
|
||||||
|
IntDir = trunc(Dir * 182.0416),
|
||||||
|
packet_send(Client, << 16#05030300:32, 0:64, DestGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64, DestGID:32/little, 0:32,
|
||||||
|
16#1000:16, IntDir:16/little, PrevX:32/little-float, PrevY:32/little-float, PrevZ:32/little-float, X:32/little-float, Y:32/little-float, Z:32/little-float,
|
||||||
|
QuestID:32/little, ZoneID:32/little, MapID:32/little, EntryID:32/little, 1:32/little >>).
|
||||||
|
|
||||||
|
%% @todo NPC inventory. Guessing it's only for NPC characters...
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_0a04(NPCGID, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
{ok, Bin} = file:read_file("p/packet0a04.bin"),
|
||||||
|
packet_send(Client, << 16#0a040300:32, 0:32, 16#00001d00:32, NPCGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64, Bin/binary >>).
|
||||||
|
|
||||||
|
%% @todo Inventory related. Doesn't seem to do anything.
|
||||||
|
send_0a05(Client=#egs_net{gid=DestGID, lid=DestLID}) ->
|
||||||
|
packet_send(Client, << 16#0a050300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64 >>).
|
||||||
|
|
||||||
|
%% @doc Send the list of ItemUUID for the items in the inventory.
|
||||||
|
send_0a06(CharUser, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
|
||||||
|
Len = length(CharUser#users.inventory),
|
||||||
|
UUIDs = lists:seq(1, Len),
|
||||||
|
Bin = iolist_to_binary([ << N:32/little >> || N <- UUIDs]),
|
||||||
|
Blanks = lists:seq(1, 60 - Len),
|
||||||
|
Bin2 = iolist_to_binary([ << 16#ffffffff:32 >> || _N <- Blanks]),
|
||||||
|
packet_send(Client, << 16#0a060300:32, DestLID:16/little, 0:48, DestGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64, Bin/binary, Bin2/binary >>).
|
||||||
|
|
||||||
|
%% @todo Handle more than just goggles.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_0a0a(Inventory, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
{ok, << _:68608/bits, Rest/bits >>} = file:read_file("p/packet0a0a.bin"),
|
||||||
|
NbItems = length(Inventory),
|
||||||
|
ItemVariables = build_0a0a_item_variables(Inventory, 1, []),
|
||||||
|
ItemConstants = build_0a0a_item_constants(Inventory, []),
|
||||||
|
packet_send(Client, << 16#0a0a0300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64,
|
||||||
|
NbItems:8, 0:8, 6:8, 0:72, 0:192, 0:2304, ItemVariables/binary, ItemConstants/binary, 0:13824, Rest/binary >>).
|
||||||
|
|
||||||
|
build_0a0a_item_variables([], _N, Acc) ->
|
||||||
|
Bin = iolist_to_binary(lists:reverse(Acc)),
|
||||||
|
Padding = 17280 - 8 * byte_size(Bin),
|
||||||
|
<< Bin/binary, 0:Padding >>;
|
||||||
|
build_0a0a_item_variables([{ItemID, Variables}|Tail], N, Acc) ->
|
||||||
|
build_0a0a_item_variables(Tail, N + 1, [build_item_variables(ItemID, N, Variables)|Acc]).
|
||||||
|
|
||||||
|
build_0a0a_item_constants([], Acc) ->
|
||||||
|
Bin = iolist_to_binary(lists:reverse(Acc)),
|
||||||
|
Padding = 34560 - 8 * byte_size(Bin),
|
||||||
|
<< Bin/binary, 0:Padding >>;
|
||||||
|
build_0a0a_item_constants([{ItemID, _Variables}|Tail], Acc) ->
|
||||||
|
#psu_item{name=Name, rarity=Rarity, sell_price=SellPrice, data=Data} = egs_items_db:read(ItemID),
|
||||||
|
UCS2Name = << << X:8, 0:8 >> || X <- Name >>,
|
||||||
|
NamePadding = 8 * (46 - byte_size(UCS2Name)),
|
||||||
|
<< Category:8, _:24 >> = << ItemID:32 >>,
|
||||||
|
DataBin = build_item_constants(Data),
|
||||||
|
RarityInt = Rarity - 1,
|
||||||
|
Bin = << UCS2Name/binary, 0:NamePadding, RarityInt:8, Category:8, SellPrice:32/little, DataBin/binary >>,
|
||||||
|
build_0a0a_item_constants(Tail, [Bin|Acc]).
|
||||||
|
|
||||||
|
%% @doc Send an item's description.
|
||||||
|
send_0a11(ItemID, ItemDesc, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
|
||||||
|
Length = 1 + byte_size(ItemDesc) div 2,
|
||||||
|
packet_send(Client, << 16#0a110300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64,
|
||||||
|
ItemID:32, Length:32/little, ItemDesc/binary, 0:16 >>).
|
||||||
|
|
||||||
|
%% @doc Quest init.
|
||||||
|
%% @todo When first entering a zone it seems LID should be set to ffff apparently.
|
||||||
|
send_0c00(CharUser, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
|
||||||
|
#users{area={QuestID, _ZoneID, _MapID}} = CharUser,
|
||||||
|
packet_send(Client, << 16#0c000300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64, QuestID:32/little,
|
||||||
|
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32,
|
||||||
|
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32,
|
||||||
|
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32,
|
||||||
|
16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32 >>).
|
||||||
|
|
||||||
|
%% @todo Figure out last 4 bytes!
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_0c02(Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#0c020300:32, 0:160, 16#00011300:32, DestGID:32/little, 0:64, 0:32 >>).
|
||||||
|
|
||||||
|
%% @doc Send the huge pack of quest files available in the counter.
|
||||||
|
send_0c06(Pack, Client) ->
|
||||||
|
packet_send(Client, << 16#0c060300:32, 0:288, 1:32/little, Pack/binary >>).
|
||||||
|
|
||||||
|
%% @doc Reply that the player is allowed to use the lobby transport. Always allow.
|
||||||
|
send_0c08(Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#0c080300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:96 >>).
|
||||||
|
|
||||||
|
%% @doc Send the trial start notification.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_0c09(Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#0c090300:32, 0:160, 16#00011300:32, DestGID:32/little, 0:64, 0:64 >>).
|
||||||
|
|
||||||
|
%% @doc Send the counter's mission options (0 = invisible, 2 = disabled, 3 = available).
|
||||||
|
send_0c10(Options, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
|
||||||
|
Size = byte_size(Options),
|
||||||
|
packet_send(Client, << 16#0c100300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64, 1, 0, Size:16/little, Options/binary >>).
|
||||||
|
|
||||||
|
%% @todo Add a character (NPC or real) to the party members on the right of the screen.
|
||||||
|
%% @todo NPCid is 65535 for normal characters.
|
||||||
|
%% @todo Apparently the 4 location ids are set to 0 when inviting an NPC in the lobby - NPCs have their location set to 0 when in lobby; also odd value before PartyPos related to missions
|
||||||
|
%% @todo Not sure about LID. But seems like it.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_1004(Type, User, PartyPos, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
[TypeID, LID, SomeFlag] = case Type of
|
||||||
|
npc_mission -> [16#00001d00, PartyPos, 2];
|
||||||
|
npc_invite -> [0, 16#ffffffff, 3];
|
||||||
|
_ -> 1 %% seems to be for players
|
||||||
|
end,
|
||||||
|
#users{gid=GID, npcid=NPCid, name=Name, level=Level, area={QuestID, ZoneID, MapID}, entryid=EntryID} = User,
|
||||||
|
packet_send(Client, << 16#10040300:32, 16#ffff0000:32, 0:128, 16#00011300:32, DestGID:32/little, 0:64,
|
||||||
|
TypeID:32, GID:32/little, 0:64, Name/binary,
|
||||||
|
Level:16/little, 16#ffff:16,
|
||||||
|
SomeFlag, 1, PartyPos:8, 1,
|
||||||
|
NPCid:16/little, 0:16,
|
||||||
|
%% Odd unknown values. PA related? No idea. Values on invite, 0 in-mission.
|
||||||
|
%~ 16#00001f08:32, 0:32, 16#07000000:32,
|
||||||
|
%~ 16#04e41f08:32, 0:32, 16#01000000:32,
|
||||||
|
%~ 16#64e41f08:32, 0:32, 16#02000000:32,
|
||||||
|
%~ 16#64e41f08:32, 0:32, 16#03000000:32,
|
||||||
|
%~ 16#64e41f08:32, 0:32, 16#12000000:32,
|
||||||
|
%~ 16#24e41f08:32,
|
||||||
|
0:512,
|
||||||
|
QuestID:32/little, ZoneID:32/little, MapID:32/little, EntryID:32/little,
|
||||||
|
LID:32/little,
|
||||||
|
0:64,
|
||||||
|
16#01000000:32, 16#01000000:32, %% @todo first is current hp, second is max hp
|
||||||
|
0:608 >>).
|
||||||
|
|
||||||
|
%% @doc Send the client's own player's party information, on the bottom left of the screen.
|
||||||
|
%% @todo Location and the 20 bytes following sometimes have values, not sure why; when joining a party maybe?
|
||||||
|
send_1005(User, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
#users{name=Name, level=Level, currenthp=CurrentHP, maxhp=MaxHP} = User,
|
||||||
|
Location = << 0:512 >>,
|
||||||
|
packet_send(Client, << 16#10050300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64,
|
||||||
|
16#00000100:32, 0:32, 16#ffffffff:32, 0:32, 16#00011200:32, DestGID:32/little, 0:64,
|
||||||
|
Name/binary, Level:8, 0:16, 1:8, 16#01010000:32, 0:32, Location/binary,
|
||||||
|
16#ffffffff:32, 0:96, 16#ffffffff:32, 0:64, CurrentHP:32/little, MaxHP:32/little, 0:640,
|
||||||
|
16#0100ffff:32, 16#0000ff00:32, 16#ffff0000:32, 0:640, 16#ffffffff:32, 0:768,
|
||||||
|
16#0100ffff:32, 16#0000ff00:32, 16#ffff0000:32, 0:640, 16#ffffffff:32, 0:768,
|
||||||
|
16#0100ffff:32, 16#0000ff00:32, 16#ffff0000:32, 0:640, 16#ffffffff:32, 0:768,
|
||||||
|
16#0100ffff:32, 16#0000ff00:32, 16#ffff0000:32, 0:640, 16#ffffffff:32, 0:768,
|
||||||
|
16#0100ffff:32, 16#0000ff00:32, 16#ffff0000:32, 0:640, 16#ffffffff:32, 0:448,
|
||||||
|
16#ffffffff:32, 0:32, 16#ff020000:32, 16#ffff0000:32, 16#ffff0000:32, 16#ffff0000:32,
|
||||||
|
16#ffff0000:32, 16#ffff0000:32, 16#ffff0000:32, 0:3680 >>).
|
||||||
|
|
||||||
|
%% @doc Party-related events.
|
||||||
|
send_1006(EventID, Client) ->
|
||||||
|
send_1006(EventID, 0, Client).
|
||||||
|
send_1006(EventID, PartyPos, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#10060300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, EventID:8, PartyPos:8, 0:16 >>).
|
||||||
|
|
||||||
|
%% @doc Send the player's current location.
|
||||||
|
%% @todo Handle PartyPos.
|
||||||
|
%% @todo Receive the AreaName as UCS2 directly to allow for color codes and the like.
|
||||||
|
%% @todo Handle TargetLID probably (right after the padding).
|
||||||
|
%% @todo Do counters even have a name?
|
||||||
|
send_100e(CounterID, AreaName, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
PartyPos = 0,
|
||||||
|
UCS2Name = << << X:8, 0:8 >> || X <- AreaName >>,
|
||||||
|
Padding = 8 * (64 - byte_size(UCS2Name)),
|
||||||
|
CounterType = if CounterID =:= 16#ffffffff -> 2; true -> 1 end,
|
||||||
|
packet_send(Client, << 16#100e0300:32, 16#ffffffbf:32, 0:128, 16#00011300:32, DestGID:32, 0:64,
|
||||||
|
1, PartyPos, 0:48, 16#ffffff7f:32, UCS2Name/binary, 0:Padding, 0:32, CounterID:32/little, CounterType:32/little >>).
|
||||||
|
send_100e({QuestID, ZoneID, MapID}, EntryID, AreaName, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
PartyPos = 0,
|
||||||
|
UCS2Name = << << X:8, 0:8 >> || X <- AreaName >>,
|
||||||
|
Padding = 8 * (64 - byte_size(UCS2Name)),
|
||||||
|
packet_send(Client, << 16#100e0300:32, 16#ffffffbf:32, 0:128, 16#00011300:32, DestGID:32, 0:64,
|
||||||
|
1, PartyPos, ZoneID:16/little, MapID:16/little, EntryID:16/little, QuestID:32/little,
|
||||||
|
UCS2Name/binary, 0:Padding, 0:32, 16#ffffffff:32, 0:32 >>).
|
||||||
|
|
||||||
|
%% @todo No idea. Also the 2 PartyPos in the built packet more often than not match, but sometimes don't? That's probably because one is PartyPos and the other is LID or something.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_100f(NPCid, PartyPos, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#100f0300:32, 0:160, 16#00011300:32, DestGID:32/little, 0:64, NPCid:16/little, 1, PartyPos:8, PartyPos:32/little >>).
|
||||||
|
|
||||||
|
%% @doc Send the mission's quest file when starting a new mission.
|
||||||
|
%% @todo Handle correctly. 0:32 is actually a missing value. Value before that is unknown too.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_1015(QuestID, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
QuestData = egs_quests_db:quest_nbl(QuestID),
|
||||||
|
Size = byte_size(QuestData),
|
||||||
|
packet_send(Client, << 16#10150300:32, 0:160, 16#00011300:32, DestGID:32/little, 0:64, QuestID:32/little, 16#01010000:32, 0:32, Size:32/little, QuestData/binary >>).
|
||||||
|
|
||||||
|
%% @todo No idea.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_1016(PartyPos, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#10160300:32, 16#ffff0000:32, 0:128, 16#00011300:32, DestGID:32/little, 0:64, PartyPos:32/little >>).
|
||||||
|
|
||||||
|
%% @todo No idea.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_101a(NPCid, PartyPos, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#101a0300:32, 0:160, 16#00011300:32, DestGID:32/little, 0:64, NPCid:16/little, PartyPos:16/little, 16#ffffffff:32 >>).
|
||||||
|
|
||||||
|
%% @doc Mission start related.
|
||||||
|
send_1020(Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#10200300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64 >>).
|
||||||
|
|
||||||
|
%% @doc Update HP in the party members information on the left.
|
||||||
|
%% @todo Handle PartyPos. Probably only pass HP later.
|
||||||
|
send_1022(#users{currenthp=HP}, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
PartyPos = 0,
|
||||||
|
packet_send(Client, << 16#10220300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, HP:32/little, PartyPos:32/little >>).
|
||||||
|
|
||||||
|
%% @todo Boss related command.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_110e(Data, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#110e0300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, Data/binary, 0:32, 5:16/little, 12:16/little, 0:32, 260:32/little >>).
|
||||||
|
|
||||||
|
%% @todo Boss related command.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_1113(Data, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#11130300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, Data/binary >>).
|
||||||
|
|
||||||
|
%% @todo Figure out what this packet does. Sane values for counter and missions for now.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_1202(Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#12020300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, 0:32, 16#10000000:32, 0:64, 16#14000000:32, 0:32 >>).
|
||||||
|
|
||||||
|
%% @todo Always the same value, no idea what it's for.
|
||||||
|
send_1204(Client=#egs_net{gid=DestGID, lid=DestLID}) ->
|
||||||
|
packet_send(Client, << 16#12040300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:96, 16#20000000:32, 0:256 >>).
|
||||||
|
|
||||||
|
%% @doc Object events response?
|
||||||
|
%% @todo Not sure what Value does exactly. It's either 0 or 1.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_1205(EventID, BlockID, Value, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#12050300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, EventID, BlockID, 0:16, Value, 0:24 >>).
|
||||||
|
|
||||||
|
%% @todo Figure out what this packet does. Sane values for counter and missions for now.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_1206(Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#12060300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, 0:32, 16#80020000:32, 0:5120 >>).
|
||||||
|
|
||||||
|
%% @todo Figure out what this packet does. Sane values for counter and missions for now.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_1207(Client=#egs_net{gid=DestGID}) ->
|
||||||
|
Chunk = << 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 16#ffffffff:32, 0:224, 16#0000ffff:32, 16#ff000000:32, 16#64000a00:32 >>,
|
||||||
|
packet_send(Client, << 16#12070300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64,
|
||||||
|
Chunk/binary, Chunk/binary, Chunk/binary, Chunk/binary, Chunk/binary, Chunk/binary >>).
|
||||||
|
|
||||||
|
%% @todo Object interaction? Figure out. C probably the interaction type.
|
||||||
|
%% @todo Apparently A would be TargetID/ffffffff, B would be the player LID, C would be the object type? D still completely unknown.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_1211(A, B, C, D, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#12110300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, A:32/little, B:32/little, C:32/little, D:32/little >>).
|
||||||
|
|
||||||
|
%% @doc Make the client load the quest previously sent.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_1212(Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#12120300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, 0:19200 >>).
|
||||||
|
|
||||||
|
%% @todo Not sure. Related to keys.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_1213(A, B, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#12130300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, A:32/little, B:32/little >>).
|
||||||
|
|
||||||
|
%% @todo Related to boss gates.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_1215(A, B, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#12150300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, A:32/little, 0:16, B:16/little >>).
|
||||||
|
|
||||||
|
%% @todo Not sure yet. Value is probably a TargetID. Used in Airboard Rally. Replying with the same value starts the race.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_1216(Value, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#12160300:32, 0:32, 16#00011300:32, DestGID:32/little, 0:64, 16#00011300:32, DestGID:32/little, 0:64, Value:32/little >>).
|
||||||
|
|
||||||
|
%% @todo Send an empty partner card list.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_1501(Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#15010300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:96 >>).
|
||||||
|
|
||||||
|
%% @todo Send an empty blacklist.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_1512(Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#15120300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:46144 >>).
|
||||||
|
|
||||||
|
%% @todo NPC related packet, sent when there's an NPC in the area.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_1601(PartyPos, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
{ok, << _:32, Bin/bits >>} = file:read_file("p/packet1601.bin"),
|
||||||
|
packet_send(Client, << 16#16010300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, PartyPos:32/little, Bin/binary >>).
|
||||||
|
|
||||||
|
%% @doc Send the player's NPC and PM information.
|
||||||
|
%% @todo The value 4 is the card priority. Find what 3 is. When sending, the first 0 is an unknown value.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_1602(Client=#egs_net{gid=DestGID}) ->
|
||||||
|
NPCList = egs_npc_db:all(),
|
||||||
|
NbNPC = length(NPCList),
|
||||||
|
Bin = iolist_to_binary([<< NPCid:8, 0, 4, 0, 3, 0:24 >> || {NPCid, _Data} <- NPCList]),
|
||||||
|
MiddlePaddingSize = 8 * (344 - byte_size(Bin)),
|
||||||
|
PMName = "My PM",
|
||||||
|
UCS2PMName = << << X:8, 0:8 >> || X <- PMName >>,
|
||||||
|
EndPaddingSize = 8 * (64 - byte_size(UCS2PMName)),
|
||||||
|
packet_send(Client, << 16#16020300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:96,
|
||||||
|
Bin/binary, 0:MiddlePaddingSize, NbNPC, 0:24, UCS2PMName/binary, 0:EndPaddingSize, 0:32 >>).
|
||||||
|
|
||||||
|
%% @doc Send the list of parties to join.
|
||||||
|
%% @todo Handle lists of parties.
|
||||||
|
%% @todo Probably has to handle a LID here, although it should always be 0.
|
||||||
|
send_1701(Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#17010300:32, 0:160, 16#00011300:32, DestGID:32/little, 0:96 >>).
|
||||||
|
|
||||||
|
%% @doc Party information.
|
||||||
|
%% @todo Handle existing parties.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_1706(CharName, Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#17060300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64,
|
||||||
|
16#00000300:32, 16#d5c0faff:32, 0:64, CharName/binary,
|
||||||
|
16#78000000:32, 16#01010000:32, 0:1536, 16#0100c800:32, 16#0601010a:32, 16#ffffffff:32, 0:32 >>).
|
||||||
|
|
||||||
|
%% @doc Party settings. Item distribution is random for now.
|
||||||
|
%% @todo Handle correctly.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_170a(Client=#egs_net{gid=DestGID}) ->
|
||||||
|
packet_send(Client, << 16#170a0300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, 16#01010c08:32 >>).
|
||||||
|
|
||||||
|
%% @todo Find what the heck this packet is.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_170c(Client=#egs_net{gid=DestGID}) ->
|
||||||
|
{ok, File} = file:read_file("p/packet170c.bin"),
|
||||||
|
packet_send(Client, << 16#170c0300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, File/binary >>).
|
||||||
|
|
||||||
|
%% @doc Send the background to use for the counter.
|
||||||
|
send_1711(Bg, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
|
||||||
|
packet_send(Client, << 16#17110300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:64, Bg:8, 0:24 >>).
|
||||||
|
|
||||||
|
%% @doc NPC shop request reply.
|
||||||
|
send_1a02(A, B, C, D, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
|
||||||
|
packet_send(Client, << 16#1a020300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:96,
|
||||||
|
A:16/little, B:16/little, C:16/little, D:16/little >>).
|
||||||
|
|
||||||
|
%% @doc Lumilass available hairstyles/headtypes handler.
|
||||||
|
send_1a03(User, Client=#egs_net{gid=DestGID, lid=DestLID}) ->
|
||||||
|
{ok, Conf} = file:consult("priv/lumilass.conf"),
|
||||||
|
NbHeadtypes = proplists:get_value({headtypes, User#users.gender, User#users.race}, Conf, 0),
|
||||||
|
HairstylesList = proplists:get_value({hairstyles, User#users.gender}, Conf),
|
||||||
|
NbHairstyles = length(HairstylesList),
|
||||||
|
HairstylesBin = iolist_to_binary([ << N:32 >> || N <- HairstylesList]),
|
||||||
|
packet_send(Client, << 16#1a030300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:96,
|
||||||
|
NbHairstyles:32/little, NbHeadtypes:32/little, 0:416, HairstylesBin/binary, 0:32 >>).
|
||||||
|
|
||||||
|
%% @doc PP cube handler.
|
||||||
|
%% @todo The 4 bytes before the file may vary. Everything past that is the same. Figure things out.
|
||||||
|
%% @todo This packet hasn't been reviewed at all yet.
|
||||||
|
send_1a04(Client=#egs_net{gid=DestGID}) ->
|
||||||
|
{ok, File} = file:read_file("p/ppcube.bin"),
|
||||||
|
packet_send(Client, << 16#1a040300:32, 16#ffff:16, 0:144, 16#00011300:32, DestGID:32/little, 0:64, 0:32, File/binary >>).
|
||||||
|
|
||||||
|
%% @doc Available types handler. Enable all 16 types.
|
||||||
|
send_1a07(Client=#egs_net{gid=DestGID, lid=DestLID}) ->
|
||||||
|
packet_send(Client, << 16#1a070300:32, DestLID:16/little, 0:144, 16#00011300:32, DestGID:32/little, 0:160,
|
||||||
|
16#01010101:32, 16#01010101:32, 16#01010101:32, 16#01010101:32 >>).
|
||||||
|
|
||||||
|
%% Common binary building functions.
|
||||||
|
|
||||||
|
%% @todo Handle class levels.
|
||||||
|
build_char_level(#users{type=Type, level=Level, exp=EXP, blastbar=BlastBar, luck=Luck, money=Money}) ->
|
||||||
|
ClassesBin = case Type of
|
||||||
|
npc ->
|
||||||
|
<< 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32,
|
||||||
|
16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32,
|
||||||
|
16#4e4f4630:32, 16#08000000:32, 0:32, 0:32, 16#4e454e44:32 >>;
|
||||||
|
_ ->
|
||||||
|
<< 0:160,
|
||||||
|
16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32,
|
||||||
|
16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32, 16#01000000:32 >>
|
||||||
|
end,
|
||||||
|
PlayTime = 0, %% @todo
|
||||||
|
<< Level:32/little, BlastBar:16/little, Luck:8, 0:40, EXP:32/little, 0:32, Money:32/little, PlayTime:32/little, ClassesBin/binary >>.
|
||||||
|
|
||||||
|
build_item_constants(#psu_clothing_item{appearance=Appearance, manufacturer=Manufacturer, type=Type, overlap=Overlap, gender=Gender, colors=Colors}) ->
|
||||||
|
GenderInt = case Gender of male -> 16#1b; female -> 16#2b end,
|
||||||
|
<< Appearance:16, Type:4, Manufacturer:4, Overlap:8, GenderInt:8, Colors/binary, 0:40 >>;
|
||||||
|
build_item_constants(#psu_consumable_item{max_quantity=MaxQuantity, pt_diff=PointsDiff,
|
||||||
|
status_effect=StatusEffect, target=Target, use_condition=UseCondition, item_effect=ItemEffect}) ->
|
||||||
|
<< 0:8, MaxQuantity:8, Target:8, UseCondition:8, PointsDiff:16/little, StatusEffect:8, ItemEffect:8, 0:96 >>;
|
||||||
|
build_item_constants(#psu_parts_item{appearance=Appearance, manufacturer=Manufacturer, type=Type, overlap=Overlap, gender=Gender}) ->
|
||||||
|
GenderInt = case Gender of male -> 16#14; female -> 16#24 end,
|
||||||
|
<< Appearance:16, Type:4, Manufacturer:4, Overlap:8, GenderInt:8, 0:120 >>;
|
||||||
|
%% @todo Handle rank properly.
|
||||||
|
build_item_constants(#psu_striking_weapon_item{pp=PP, atp=ATP, ata=ATA, atp_req=Req, shop_element=#psu_element{type=EleType, percent=ElePercent},
|
||||||
|
hand=Hand, max_upgrades=MaxUpgrades, attack_label=AttackLabel}) ->
|
||||||
|
Rank = 4,
|
||||||
|
HandInt = case Hand of
|
||||||
|
both -> 0;
|
||||||
|
_ -> error
|
||||||
|
end,
|
||||||
|
<< PP:16/little, ATP:16/little, ATA:16/little, Req:16/little, 16#ffffff:24,
|
||||||
|
EleType:8, ElePercent:8, HandInt:8, 0:8, Rank:8, 0:8, MaxUpgrades:8, AttackLabel:8, 0:8 >>;
|
||||||
|
build_item_constants(#psu_trap_item{max_quantity=MaxQuantity}) ->
|
||||||
|
<< 2:32/little, 16#ffffff:24, MaxQuantity:8, 0:96 >>;
|
||||||
|
build_item_constants(#psu_special_item{}) ->
|
||||||
|
<< 0:160 >>.
|
||||||
|
|
||||||
|
build_item_variables(ItemID, ItemUUID, #psu_clothing_item_variables{color=ColorNb}) ->
|
||||||
|
#psu_item{rarity=Rarity, data=#psu_clothing_item{colors=ColorsBin}} = egs_items_db:read(ItemID),
|
||||||
|
RarityInt = Rarity - 1,
|
||||||
|
ColorInt = if ColorNb < 5 -> ColorNb; true -> 16#10 + ColorNb - 5 end,
|
||||||
|
Bits = ColorNb * 8,
|
||||||
|
<< _Before:Bits, ColorA:4, ColorB:4, _After/bits >> = ColorsBin,
|
||||||
|
<< 0:32, ItemUUID:32/little, ItemID:32, 0:88, RarityInt:8, ColorA:8, ColorB:8, ColorInt:8, 0:72 >>;
|
||||||
|
build_item_variables(ItemID, ItemUUID, #psu_consumable_item_variables{quantity=Quantity}) ->
|
||||||
|
#psu_item{rarity=Rarity, data=#psu_consumable_item{max_quantity=MaxQuantity, action=Action}} = egs_items_db:read(ItemID),
|
||||||
|
RarityInt = Rarity - 1,
|
||||||
|
<< 0:32, ItemUUID:32/little, ItemID:32, Quantity:32/little, MaxQuantity:32/little, 0:24, RarityInt:8, Action:8, 0:88 >>;
|
||||||
|
build_item_variables(ItemID, ItemUUID, #psu_parts_item_variables{}) ->
|
||||||
|
#psu_item{rarity=Rarity} = egs_items_db:read(ItemID),
|
||||||
|
RarityInt = Rarity - 1,
|
||||||
|
<< 0:32, ItemUUID:32/little, ItemID:32, 0:88, RarityInt:8, 0:96 >>;
|
||||||
|
%% @todo Handle rank, rarity and hands properly.
|
||||||
|
build_item_variables(ItemID, ItemUUID, Variables) when element(1, Variables) =:= psu_striking_weapon_item_variables ->
|
||||||
|
#psu_striking_weapon_item_variables{is_active=IsActive, slot=Slot, current_pp=CurrentPP, max_pp=MaxPP,
|
||||||
|
element=#psu_element{type=EleType, percent=ElePercent}, pa=#psu_pa{type=PAType, level=PALevel}} = Variables,
|
||||||
|
Rank = 4,
|
||||||
|
Grind = 0,
|
||||||
|
Rarity = 14, %% Rarity - 1
|
||||||
|
Hand = both,
|
||||||
|
<< _:8, WeaponType:8, _:16 >> = << ItemID:32 >>,
|
||||||
|
HandBin = case Hand of
|
||||||
|
both -> << 16#0000:16 >>;
|
||||||
|
_ -> error
|
||||||
|
end,
|
||||||
|
<< IsActive:8, Slot:8, 0:16, ItemUUID:32/little, ItemID:32, 0:32, CurrentPP:16/little, MaxPP:16/little, 0:16, %% @todo What's this 0:16?
|
||||||
|
Grind:4, Rank:4, Rarity:8, EleType:8, ElePercent:8, HandBin/binary, WeaponType:8, PAType:8, PALevel:8, 0:40 >>;
|
||||||
|
build_item_variables(ItemID, ItemUUID, #psu_special_item_variables{}) ->
|
||||||
|
Action = case ItemID of
|
||||||
|
16#11010000 -> << 16#12020100:32 >>;
|
||||||
|
16#11020000 -> << 16#15000000:32 >>;
|
||||||
|
16#11020100 -> << 0:32 >>;
|
||||||
|
16#11020200 -> << 0:32 >>
|
||||||
|
end,
|
||||||
|
<< 0:32, ItemUUID:32/little, ItemID:32, 0:24, 16#80:8, 0:56, 16#80:8, 0:32, Action/binary, 0:32 >>;
|
||||||
|
build_item_variables(ItemID, ItemUUID, #psu_trap_item_variables{quantity=Quantity}) ->
|
||||||
|
#psu_item{rarity=Rarity, data=#psu_trap_item{max_quantity=MaxQuantity}} = egs_items_db:read(ItemID),
|
||||||
|
RarityInt = Rarity - 1,
|
||||||
|
<< 0:32, ItemUUID:32/little, ItemID:32, Quantity:32/little, MaxQuantity:32/little, 0:24, RarityInt:8, 0:96 >>.
|
||||||
|
|
||||||
|
%% Utility functions.
|
||||||
|
|
||||||
|
%% @doc Prepare a packet. Return the real size and padding at the end.
|
||||||
|
packet_prepare(Packet) ->
|
||||||
|
Size = 4 + byte_size(Packet),
|
||||||
|
case Size rem 4 of
|
||||||
|
0 -> {ok, Size, <<>>};
|
||||||
|
2 -> {ok, Size + 2, << 0:16 >>};
|
||||||
|
_ -> {error, badarg}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc Send a packet. The packet argument must not contain the size field.
|
||||||
|
packet_send(Client, Packet) ->
|
||||||
|
{ok, Size, Padding} = packet_prepare(Packet),
|
||||||
|
packet_send(Client, << Size:32/little, Packet/binary, Padding/binary >>, Size).
|
||||||
|
|
||||||
|
%% Send a normal command.
|
||||||
|
packet_send(#egs_net{socket=Socket, transport=Transport}, Packet, Size)
|
||||||
|
when Size =< 16#4000 ->
|
||||||
|
Transport:send(Socket, Packet);
|
||||||
|
%% Send a fragmented command when size is too big.
|
||||||
|
packet_send(Client, Packet, Size) ->
|
||||||
|
packet_fragment_send(Client, Packet, Size, 0).
|
||||||
|
|
||||||
|
%% Send the last chunk of a fragmented command.
|
||||||
|
packet_fragment_send(#egs_net{socket=Socket, transport=Transport}, Packet,
|
||||||
|
Size, Current) when Size - Current =< 16#4000 ->
|
||||||
|
FragmentSize = 16#10 + byte_size(Packet),
|
||||||
|
Fragment = << FragmentSize:32/little, 16#0b030000:32, Size:32/little, Current:32/little, Packet/binary >>,
|
||||||
|
Transport:send(Socket, Fragment);
|
||||||
|
%% Send another chunk of a fragmented command.
|
||||||
|
packet_fragment_send(Client=#egs_net{socket=Socket, transport=Transport}, Packet,
|
||||||
|
Size, Current) ->
|
||||||
|
<< Chunk:131072/bits, Rest/bits >> = Packet,
|
||||||
|
Fragment = << 16#10400000:32, 16#0b030000:32, Size:32/little, Current:32/little, Chunk/binary >>,
|
||||||
|
Transport:send(Socket, Fragment),
|
||||||
|
packet_fragment_send(Client, Rest, Size, Current + 16#4000).
|
71
apps/egs/src/egs_quests.erl
Normal file
71
apps/egs/src/egs_quests.erl
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2011 Loïc Hoguin.
|
||||||
|
%% @doc Quest handler.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_quests).
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
-export([start_link/2, stop/1, zone_pid/2]). %% API.
|
||||||
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
||||||
|
|
||||||
|
-record(state, {zones}).
|
||||||
|
|
||||||
|
%% API.
|
||||||
|
|
||||||
|
%% @spec start_link(UniID, QuestID) -> {ok,Pid::pid()}
|
||||||
|
start_link(UniID, QuestID) ->
|
||||||
|
gen_server:start_link(?MODULE, [UniID, QuestID], []).
|
||||||
|
|
||||||
|
%% @spec stop(Pid) -> stopped
|
||||||
|
stop(Pid) ->
|
||||||
|
gen_server:call(Pid, stop).
|
||||||
|
|
||||||
|
zone_pid(Pid, ZoneID) ->
|
||||||
|
gen_server:call(Pid, {zone_pid, ZoneID}).
|
||||||
|
|
||||||
|
%% gen_server.
|
||||||
|
|
||||||
|
init([UniID, QuestID]) ->
|
||||||
|
Zones = egs_quests_db:quest_zones(QuestID),
|
||||||
|
ZonesPids = lists:map(fun({ZoneID, ZoneData}) ->
|
||||||
|
{ok, Pid} = egs_zones_sup:start_zone(UniID, QuestID, ZoneID, ZoneData),
|
||||||
|
{ZoneID, Pid}
|
||||||
|
end, Zones),
|
||||||
|
{ok, #state{zones=ZonesPids}}.
|
||||||
|
|
||||||
|
handle_call({zone_pid, ZoneID}, _From, State) ->
|
||||||
|
{_, Pid} = lists:keyfind(ZoneID, 1, State#state.zones),
|
||||||
|
{reply, Pid, State};
|
||||||
|
|
||||||
|
handle_call(stop, _From, State) ->
|
||||||
|
{stop, normal, stopped, State};
|
||||||
|
|
||||||
|
handle_call(_Request, _From, State) ->
|
||||||
|
{reply, ignored, State}.
|
||||||
|
|
||||||
|
handle_cast(_Msg, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
handle_info(_Info, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
terminate(_Reason, _State) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
code_change(_OldVsn, State, _Extra) ->
|
||||||
|
{ok, State}.
|
190
apps/egs/src/egs_quests_db.erl
Normal file
190
apps/egs/src/egs_quests_db.erl
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
|
%% @doc EGS quests database and cache manager.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_quests_db).
|
||||||
|
-behavior(gen_server).
|
||||||
|
|
||||||
|
-export([start_link/0, stop/0, quest_nbl/1, zone_nbl/2, area_type/2, quest_zones/1, set/3, reload/0]). %% API.
|
||||||
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
||||||
|
|
||||||
|
%% Use the module name for the server's name.
|
||||||
|
-define(SERVER, ?MODULE).
|
||||||
|
|
||||||
|
-record(state, {quests=[], quests_bin=[], zones_bin=[], sets=[]}).
|
||||||
|
|
||||||
|
%% API.
|
||||||
|
|
||||||
|
%% @spec start_link() -> {ok,Pid::pid()}
|
||||||
|
start_link() ->
|
||||||
|
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||||||
|
|
||||||
|
%% @spec stop() -> stopped
|
||||||
|
stop() ->
|
||||||
|
gen_server:call(?SERVER, stop).
|
||||||
|
|
||||||
|
%% @spec quest_nbl(QuestID) -> binary()
|
||||||
|
quest_nbl(QuestID) ->
|
||||||
|
gen_server:call(?SERVER, {quest_nbl, QuestID}).
|
||||||
|
|
||||||
|
%% @spec zone_nbl(QuestID, ZoneID) -> binary()
|
||||||
|
zone_nbl(QuestID, ZoneID) ->
|
||||||
|
gen_server:call(?SERVER, {zone_nbl, QuestID, ZoneID}).
|
||||||
|
|
||||||
|
area_type(QuestID, ZoneID) ->
|
||||||
|
gen_server:call(?SERVER, {area_type, QuestID, ZoneID}).
|
||||||
|
|
||||||
|
quest_zones(QuestID) ->
|
||||||
|
gen_server:call(?SERVER, {quest_zones, QuestID}).
|
||||||
|
|
||||||
|
set(QuestID, ZoneID, SetID) ->
|
||||||
|
gen_server:call(?SERVER, {set, QuestID, ZoneID, SetID}).
|
||||||
|
|
||||||
|
%% @spec reload() -> ok
|
||||||
|
reload() ->
|
||||||
|
gen_server:cast(?SERVER, reload).
|
||||||
|
|
||||||
|
%% gen_server.
|
||||||
|
|
||||||
|
init([]) ->
|
||||||
|
{ok, #state{}}.
|
||||||
|
|
||||||
|
%% @doc Return a quest information either from the cache or from the configuration file,
|
||||||
|
%% in which case it gets added to the cache for subsequent attempts.
|
||||||
|
handle_call({quest_nbl, QuestID}, _From, State=#state{quests=Cache, quests_bin=BinCache}) ->
|
||||||
|
case proplists:get_value(QuestID, BinCache) of
|
||||||
|
undefined ->
|
||||||
|
Dir = io_lib:format("priv/quests/~b/", [QuestID]),
|
||||||
|
ConfFilename = Dir ++ "quest.conf",
|
||||||
|
{ok, Settings} = file:consult(ConfFilename),
|
||||||
|
{QuestXnrData, QuestXnrPtrs} = egs_files:load_quest_xnr(Settings),
|
||||||
|
UnitTitleBinFiles = load_unit_title_bin_files(Dir, Settings),
|
||||||
|
Files = [{data, "quest.xnr", QuestXnrData, QuestXnrPtrs}],
|
||||||
|
Files2 = Files ++ case UnitTitleBinFiles of
|
||||||
|
ignore -> [];
|
||||||
|
_Any ->
|
||||||
|
TablePos = egs_files:nbl_padded_size(byte_size(QuestXnrData)),
|
||||||
|
TextSize = lists:sum([egs_files:nbl_padded_size(byte_size(D)) || {data, _F, D, _P} <- UnitTitleBinFiles]),
|
||||||
|
TablePos2 = TablePos + TextSize,
|
||||||
|
{UnitTitleTableRelData, UnitTitleTableRelPtrs} = egs_files:load_unit_title_table_rel(ConfFilename, TablePos2),
|
||||||
|
UnitTitleBinFiles ++ [{data, "unit_title_table.rel", UnitTitleTableRelData, UnitTitleTableRelPtrs}]
|
||||||
|
end,
|
||||||
|
QuestNbl = egs_files:nbl_pack([{files, Files2}]),
|
||||||
|
{reply, QuestNbl, State#state{quests=[{QuestID, Settings}|Cache], quests_bin=[{QuestID, QuestNbl}|BinCache]}};
|
||||||
|
QuestNbl ->
|
||||||
|
{reply, QuestNbl, State}
|
||||||
|
end;
|
||||||
|
|
||||||
|
%% @doc Return a zone information either from the cache or from the configuration files.
|
||||||
|
%% @todo FilePos, text.bin, other sets, enemies.
|
||||||
|
handle_call({zone_nbl, QuestID, ZoneID}, _From, State=#state{quests=QuestsCache, zones_bin=BinCache}) ->
|
||||||
|
case proplists:get_value({QuestID, ZoneID}, BinCache) of
|
||||||
|
undefined ->
|
||||||
|
Dir = io_lib:format("priv/quests/~b/", [QuestID]),
|
||||||
|
ZoneDir = Dir ++ io_lib:format("zone-~b/", [ZoneID]),
|
||||||
|
QuestSettings = proplists:get_value(QuestID, QuestsCache),
|
||||||
|
Zones = proplists:get_value(zones, QuestSettings),
|
||||||
|
Zone = proplists:get_value(ZoneID, Zones),
|
||||||
|
AreaID = proplists:get_value(areaid, Zone),
|
||||||
|
Maps = proplists:get_value(maps, Zone),
|
||||||
|
FilePos = 0, %% @todo
|
||||||
|
{Set0, SetPtrs} = egs_files:load_set_rel(ZoneDir ++ io_lib:format("set_r~b.conf", [0]), AreaID, Maps, FilePos),
|
||||||
|
ScriptBin = egs_files:load_script_bin(ZoneDir ++ "script.es"),
|
||||||
|
ScriptBinSize = byte_size(ScriptBin),
|
||||||
|
ScriptBin2 = prs:compress(ScriptBin),
|
||||||
|
ScriptBinSize2 = byte_size(ScriptBin2),
|
||||||
|
ScriptBin3 = << ScriptBinSize:32/little, ScriptBinSize2:32/little, 0:32, 1:32/little, 0:96, ScriptBin2/binary >>,
|
||||||
|
TextBin = egs_files:load_text_bin(ZoneDir ++ "text.bin.en_US.txt"),
|
||||||
|
ZoneNbl = egs_files:nbl_pack([{files, [
|
||||||
|
{data, "set_r0.rel", Set0, SetPtrs},
|
||||||
|
{data, "script.bin", ScriptBin3, []},
|
||||||
|
{data, "text.bin", TextBin, []}
|
||||||
|
]}]),
|
||||||
|
{reply, ZoneNbl, State#state{zones_bin=[{{QuestID, ZoneID}, ZoneNbl}|BinCache]}};
|
||||||
|
ZoneNbl ->
|
||||||
|
{reply, ZoneNbl, State}
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_call({area_type, QuestID, ZoneID}, _From, State=#state{quests=QuestsCache}) ->
|
||||||
|
{_, Quest} = lists:keyfind(QuestID, 1, QuestsCache),
|
||||||
|
{_, Zones} = lists:keyfind(zones, 1, Quest),
|
||||||
|
{_, Zone} = lists:keyfind(ZoneID, 1, Zones),
|
||||||
|
{_, AreaID} = lists:keyfind(areaid, 1, Zone),
|
||||||
|
AreaType = case AreaID of
|
||||||
|
0 -> lobby;
|
||||||
|
2 -> lobby;
|
||||||
|
3 -> lobby;
|
||||||
|
4 -> lobby;
|
||||||
|
5 -> lobby;
|
||||||
|
22 -> myroom;
|
||||||
|
_Any -> mission
|
||||||
|
end,
|
||||||
|
{reply, AreaType, State};
|
||||||
|
|
||||||
|
handle_call({quest_zones, QuestID}, _From, State=#state{quests=QuestsCache}) ->
|
||||||
|
{_, Quest} = lists:keyfind(QuestID, 1, QuestsCache),
|
||||||
|
{_, Zones} = lists:keyfind(zones, 1, Quest),
|
||||||
|
{reply, Zones, State};
|
||||||
|
|
||||||
|
%% @todo The set file is loaded both here and in zone_nbl. Thinking about it zone_nbl should call this function.
|
||||||
|
%% @todo Same for quest_nbl loading quest files and binaries, there should be a function for the file itself called only when needed.
|
||||||
|
handle_call({set, QuestID, ZoneID, SetID}, _From, State=#state{sets=SetsCache}) ->
|
||||||
|
case proplists:get_value({QuestID, ZoneID, SetID}, SetsCache) of
|
||||||
|
undefined ->
|
||||||
|
SetFilename = io_lib:format("priv/quests/~b/zone-~b/set_r~b.conf", [QuestID, ZoneID, SetID]),
|
||||||
|
{ok, Set} = file:consult(SetFilename),
|
||||||
|
{reply, Set, State#state{sets=[{{QuestID, ZoneID, SetID}, Set}|SetsCache]}};
|
||||||
|
CachedSet ->
|
||||||
|
{reply, CachedSet, State}
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_call(stop, _From, State) ->
|
||||||
|
{stop, normal, stopped, State};
|
||||||
|
|
||||||
|
handle_call(_Request, _From, State) ->
|
||||||
|
{reply, ignored, State}.
|
||||||
|
|
||||||
|
handle_cast(reload, _State) ->
|
||||||
|
{noreply, #state{}};
|
||||||
|
|
||||||
|
handle_cast(_Msg, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
handle_info(_Info, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
terminate(_Reason, _State) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
code_change(_OldVsn, State, _Extra) ->
|
||||||
|
{ok, State}.
|
||||||
|
|
||||||
|
%% Internal.
|
||||||
|
|
||||||
|
load_unit_title_bin_files(Dir, Settings) ->
|
||||||
|
case proplists:get_value(notitles, Settings) of
|
||||||
|
true -> ignore;
|
||||||
|
_Any ->
|
||||||
|
Zones = proplists:get_value(zones, Settings),
|
||||||
|
[load_unit_title_bin(Dir, Zone) || Zone <- Zones]
|
||||||
|
end.
|
||||||
|
|
||||||
|
load_unit_title_bin(Dir, {ZoneID, _ZoneParams}) ->
|
||||||
|
Filename = io_lib:format("unit_title_~2.10.0b.bin", [ZoneID]),
|
||||||
|
TxtFilename = io_lib:format("~s~s.en_US.txt", [Dir, Filename]),
|
||||||
|
{data, Filename, egs_files:load_text_bin(TxtFilename), []}.
|
46
apps/egs/src/egs_quests_sup.erl
Normal file
46
apps/egs/src/egs_quests_sup.erl
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2011 Loïc Hoguin.
|
||||||
|
%% @doc Supervisor for the egs_quests gen_server.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_quests_sup).
|
||||||
|
-behaviour(supervisor).
|
||||||
|
|
||||||
|
-export([start_link/0, start_quest/2]). %% API.
|
||||||
|
-export([init/1]). %% supervisor.
|
||||||
|
|
||||||
|
-define(SUPERVISOR, ?MODULE).
|
||||||
|
|
||||||
|
%% API.
|
||||||
|
|
||||||
|
-spec start_link() -> {ok, pid()}.
|
||||||
|
start_link() ->
|
||||||
|
supervisor:start_link({local, ?SUPERVISOR}, ?MODULE, []).
|
||||||
|
|
||||||
|
-spec start_quest(egs:uniid(), egs:questid()) -> {ok, pid()}.
|
||||||
|
start_quest(UniID, QuestID) ->
|
||||||
|
supervisor:start_child(?SUPERVISOR, [UniID, QuestID]).
|
||||||
|
|
||||||
|
%% supervisor.
|
||||||
|
|
||||||
|
-spec init([]) -> {ok, {{simple_one_for_one, 0, 1}, [{egs_quests,
|
||||||
|
{egs_quests, start_link, []}, temporary, brutal_kill,
|
||||||
|
worker, [egs_quests]}]}}.
|
||||||
|
init([]) ->
|
||||||
|
{ok, {{simple_one_for_one, 0, 1}, [{egs_quests,
|
||||||
|
{egs_quests, start_link, []}, temporary, brutal_kill,
|
||||||
|
worker, [egs_quests]}]}}.
|
221
apps/egs/src/egs_script_compiler.erl
Normal file
221
apps/egs/src/egs_script_compiler.erl
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
|
%% @doc EGS script compiler.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_script_compiler).
|
||||||
|
-export([compile/1]).
|
||||||
|
|
||||||
|
%% @doc Compile a script parsed using egs_script_lexer and egs_script_parser.
|
||||||
|
compile(ParseTree) ->
|
||||||
|
{RootBin, Funcs} = root(ParseTree),
|
||||||
|
FuncsPos = byte_size(RootBin),
|
||||||
|
FuncsBin = funcs(Funcs),
|
||||||
|
FuncsSize = byte_size(FuncsBin),
|
||||||
|
<< $T, $S, $B, $2, FuncsPos:32/little, FuncsSize:32/little, RootBin/binary, FuncsBin/binary >>.
|
||||||
|
|
||||||
|
root(Routines) ->
|
||||||
|
root(Routines, 0, [], []).
|
||||||
|
root(nil, _Pos, Funcs, Acc) ->
|
||||||
|
{iolist_to_binary(lists:reverse(Acc)), Funcs};
|
||||||
|
root({Routine, Next}, Pos, Funcs, Acc) ->
|
||||||
|
{Bin, Funcs2} = routine(Routine, Funcs),
|
||||||
|
Pos2 = case Next of nil -> 0; _ -> Pos + byte_size(Bin) + 4 end,
|
||||||
|
root(Next, Pos2, Funcs2, [<< Pos2:32/little, Bin/binary >>|Acc]).
|
||||||
|
|
||||||
|
routine({event_def, {Name, _Type}, Instrs}, Funcs) ->
|
||||||
|
NameBin = routine_name_to_binary([$E, $V, $E, $N, $T, $.|Name]),
|
||||||
|
{BodyBin, Funcs2} = routine_body_to_binary(Instrs, Funcs),
|
||||||
|
{<< NameBin/binary, BodyBin/binary >>, Funcs2};
|
||||||
|
routine({function_def, {Name, _Type}, Instrs}, Funcs) ->
|
||||||
|
NameBin = routine_name_to_binary(Name),
|
||||||
|
{BodyBin, Funcs2} = routine_body_to_binary(Instrs, Funcs),
|
||||||
|
{<< NameBin/binary, BodyBin/binary >>, [Name|Funcs2]};
|
||||||
|
routine({num_var, {Name, _Type}}, Funcs) ->
|
||||||
|
NameBin = routine_name_to_binary(Name),
|
||||||
|
{<< NameBin/binary, 0:32/little, 16#3c:32/little, 0:32 >>, [Name|Funcs]};
|
||||||
|
routine({str_var, {Name, _Type}, Length}, Funcs) ->
|
||||||
|
NameBin = routine_name_to_binary(Name),
|
||||||
|
{<< NameBin/binary, 0:32/little, 16#49:32/little, Length:32/little >>, [Name|Funcs]}.
|
||||||
|
|
||||||
|
routine_body_to_binary(Instrs, Funcs) ->
|
||||||
|
{_Inc, InstrsBin, VarsList, Funcs2} = instructions(Instrs, Funcs),
|
||||||
|
InstrsSize = byte_size(InstrsBin) + 4,
|
||||||
|
NbVars = length(VarsList),
|
||||||
|
VarsBin = iolist_to_binary([<< VarN:32/little, VarPos:32/little >> || {VarN, VarPos} <- VarsList]),
|
||||||
|
{<< InstrsSize:32/little, 16#4c:32/little, NbVars:32/little, InstrsBin/binary, 0:32, VarsBin/binary >>, Funcs2}.
|
||||||
|
|
||||||
|
routine_name_to_binary(Name) ->
|
||||||
|
NameBin = list_to_binary(Name),
|
||||||
|
Padding = 8 * (32 - byte_size(NameBin)),
|
||||||
|
<< NameBin/binary, 0:Padding >>.
|
||||||
|
|
||||||
|
instructions(Instrs, Funcs) ->
|
||||||
|
instructions(Instrs, Funcs, 0, [], []).
|
||||||
|
instructions(Instrs, Funcs, Pos) ->
|
||||||
|
instructions(Instrs, Funcs, Pos, [], []).
|
||||||
|
instructions(nil, Funcs, Pos, Acc, VarsAcc) ->
|
||||||
|
{Pos, iolist_to_binary(lists:reverse(Acc)), lists:reverse(lists:flatten(VarsAcc)), Funcs};
|
||||||
|
instructions({Instr, Next}, Funcs, Pos, Acc, VarsAcc) ->
|
||||||
|
{Inc, Bin, Vars, Funcs2} = instruction(Instr, Funcs, Pos),
|
||||||
|
instructions(Next, Funcs2, Pos + Inc, [Bin|Acc], [Vars|VarsAcc]).
|
||||||
|
|
||||||
|
%% High level constructs.
|
||||||
|
instruction({'case', Tests}, Funcs, Pos) ->
|
||||||
|
{Pos2, Bin, Vars, Funcs2} = case_tests(Tests, Funcs, Pos),
|
||||||
|
{Pos2 - Pos, Bin, Vars, Funcs2};
|
||||||
|
%% Functions and syscalls.
|
||||||
|
instruction({function, {Name, Type}}, Funcs, Pos) ->
|
||||||
|
{N2, Funcs2} = case find_func(Name, Funcs) of
|
||||||
|
{{error, undefined}, N} -> {N, [Name|Funcs]};
|
||||||
|
{ok, N} -> {N, Funcs}
|
||||||
|
end,
|
||||||
|
SyncBin = case Type of sync -> << 16#56:32/little >>; _ -> << >> end,
|
||||||
|
{2 + byte_size(SyncBin) div 4, << 16#60:32/little, 16#ffffffff:32, SyncBin/binary >>, [{N2, Pos + 1}], Funcs2};
|
||||||
|
instruction({syscall, {N, async}}, Funcs, _Pos) ->
|
||||||
|
{2, << 16#61:32/little, N:32/little >>, [], Funcs};
|
||||||
|
instruction({syscall, {N, sync}}, Funcs, _Pos) ->
|
||||||
|
{3, << 16#61:32/little, N:32/little, 16#56:32/little >>, [], Funcs};
|
||||||
|
%% Low level instructions.
|
||||||
|
instruction('abs', Funcs, _Pos) ->
|
||||||
|
{1, << 16#11:32/little >>, [], Funcs};
|
||||||
|
instruction(add, Funcs, _Pos) ->
|
||||||
|
{1, << 16#04:32/little >>, [], Funcs};
|
||||||
|
instruction('band', Funcs, _Pos) ->
|
||||||
|
{1, << 16#0b:32/little >>, [], Funcs};
|
||||||
|
instruction('bor', Funcs, _Pos) ->
|
||||||
|
{1, << 16#0c:32/little >>, [], Funcs};
|
||||||
|
instruction('bxor', Funcs, _Pos) ->
|
||||||
|
{1, << 16#0d:32/little >>, [], Funcs};
|
||||||
|
instruction(dec, Funcs, _Pos) ->
|
||||||
|
{1, << 16#0f:32/little >>, [], Funcs};
|
||||||
|
instruction('div', Funcs, _Pos) ->
|
||||||
|
{1, << 16#07:32/little >>, [], Funcs};
|
||||||
|
instruction(inc, Funcs, _Pos) ->
|
||||||
|
{1, << 16#0e:32/little >>, [], Funcs};
|
||||||
|
instruction(is_eq, Funcs, _Pos) ->
|
||||||
|
{1, << 16#12:32/little >>, [], Funcs};
|
||||||
|
instruction(is_gt, Funcs, _Pos) ->
|
||||||
|
{1, << 16#15:32/little >>, [], Funcs};
|
||||||
|
instruction(is_gteq, Funcs, _Pos) ->
|
||||||
|
{1, << 16#14:32/little >>, [], Funcs};
|
||||||
|
instruction(is_lt, Funcs, _Pos) ->
|
||||||
|
{1, << 16#17:32/little >>, [], Funcs};
|
||||||
|
instruction(is_lteq, Funcs, _Pos) ->
|
||||||
|
{1, << 16#16:32/little >>, [], Funcs};
|
||||||
|
instruction(is_neq, Funcs, _Pos) ->
|
||||||
|
{1, << 16#13:32/little >>, [], Funcs};
|
||||||
|
instruction({jmp, N}, Funcs, _Pos) ->
|
||||||
|
{2, << 16#2c:32/little, N:32/little-signed >>, [], Funcs};
|
||||||
|
instruction({jnz, N}, Funcs, _Pos) ->
|
||||||
|
{2, << 16#2e:32/little, N:32/little-signed >>, [], Funcs};
|
||||||
|
instruction({jz, N}, Funcs, _Pos) ->
|
||||||
|
{2, << 16#2d:32/little, N:32/little-signed >>, [], Funcs};
|
||||||
|
instruction(land, Funcs, _Pos) ->
|
||||||
|
{1, << 16#18:32/little >>, [], Funcs};
|
||||||
|
instruction(lor, Funcs, _Pos) ->
|
||||||
|
{1, << 16#19:32/little >>, [], Funcs};
|
||||||
|
instruction(lshift, Funcs, _Pos) ->
|
||||||
|
{1, << 16#09:32/little >>, [], Funcs};
|
||||||
|
instruction(mod, Funcs, _Pos) ->
|
||||||
|
{1, << 16#08:32/little >>, [], Funcs};
|
||||||
|
instruction(mul, Funcs, _Pos) ->
|
||||||
|
{1, << 16#06:32/little >>, [], Funcs};
|
||||||
|
instruction(neg, Funcs, _Pos) ->
|
||||||
|
{1, << 16#10:32/little >>, [], Funcs};
|
||||||
|
instruction(nop, Funcs, _Pos) ->
|
||||||
|
{1, << 16#01:32/little >>, [], Funcs};
|
||||||
|
instruction({num_get, {Name, _Type}}, Funcs, Pos) ->
|
||||||
|
{ok, N} = find_func(Name, Funcs),
|
||||||
|
{2, << 16#3c:32/little, 16#ffffffff:32 >>, [{N, Pos + 1}], Funcs};
|
||||||
|
instruction({num_set, {Name, _Type}}, Funcs, Pos) ->
|
||||||
|
{ok, N} = find_func(Name, Funcs),
|
||||||
|
{2, << 16#3d:32/little, 16#ffffffff:32 >>, [{N, Pos + 1}], Funcs};
|
||||||
|
instruction({push, I}, Funcs, _Pos) when is_integer(I) ->
|
||||||
|
{2, << 16#02:32/little, I:32/little-signed >>, [], Funcs};
|
||||||
|
instruction({push, F}, Funcs, _Pos) when is_float(F) ->
|
||||||
|
{2, << 16#03:32/little, F:32/little-signed-float >>, [], Funcs};
|
||||||
|
instruction({push, Str}, Funcs, _Pos) when is_list(Str) ->
|
||||||
|
StrBin = list_to_binary(Str),
|
||||||
|
L = length(Str),
|
||||||
|
Padding = 8 * (4 - L rem 4),
|
||||||
|
StrBin2 = << StrBin/binary, 0:Padding >>,
|
||||||
|
Size = byte_size(StrBin2) div 4,
|
||||||
|
{2 + Size, << 16#46:32/little, Size:32/little, StrBin2/binary >>, [], Funcs};
|
||||||
|
instruction(restore, Funcs, _Pos) ->
|
||||||
|
{1, << 16#1b:32/little >>, [], Funcs};
|
||||||
|
instruction(return, Funcs, _Pos) ->
|
||||||
|
{1, << 16#00:32/little >>, [], Funcs};
|
||||||
|
instruction(rshift, Funcs, _Pos) ->
|
||||||
|
{1, << 16#0a:32/little >>, [], Funcs};
|
||||||
|
instruction(save, Funcs, _Pos) ->
|
||||||
|
{1, << 16#1a:32/little >>, [], Funcs};
|
||||||
|
instruction(savep, Funcs, _Pos) ->
|
||||||
|
{1, << 16#1d:32/little >>, [], Funcs};
|
||||||
|
instruction({str_get, {Name, _Type}}, Funcs, Pos) ->
|
||||||
|
{ok, N} = find_func(Name, Funcs),
|
||||||
|
{2, << 16#49:32/little, 16#ffffffff:32 >>, [{N, Pos + 1}], Funcs};
|
||||||
|
instruction({str_set, {Name, _Type}}, Funcs, Pos) ->
|
||||||
|
{ok, N} = find_func(Name, Funcs),
|
||||||
|
{2, << 16#4a:32/little, 16#ffffffff:32 >>, [{N, Pos + 1}], Funcs};
|
||||||
|
instruction(sub, Funcs, _Pos) ->
|
||||||
|
{1, << 16#05:32/little >>, [], Funcs};
|
||||||
|
%% Debug instruction. Do nothing by default. Change it to whatever needs testing.
|
||||||
|
instruction(debug, Funcs, _Pos) ->
|
||||||
|
Bin = << 16#01:32/little >>,
|
||||||
|
{byte_size(Bin) div 4, Bin, [], Funcs}.
|
||||||
|
|
||||||
|
case_tests(Tests, Funcs, Pos) ->
|
||||||
|
case_tests(Tests, Funcs, Pos, [], []).
|
||||||
|
case_tests(nil, Funcs, Pos, Acc, VarsAcc) ->
|
||||||
|
{Pos, case_tests_end(lists:reverse(Acc)), VarsAcc, Funcs};
|
||||||
|
case_tests({{case_default, Instrs}, Next}, Funcs, Pos, Acc, VarsAcc) ->
|
||||||
|
{Pos2, InstrsBin, VarsList, Funcs2} = instructions(Instrs, Funcs, Pos),
|
||||||
|
case_tests(Next, Funcs2, Pos2, [InstrsBin|Acc], [VarsList|VarsAcc]);
|
||||||
|
case_tests({{case_test, N, Instrs}, Next}, Funcs, Pos, Acc, VarsAcc) ->
|
||||||
|
{Pos2, InstrsBin, VarsList, Funcs2} = instructions(Instrs, Funcs, Pos + 7),
|
||||||
|
Jump = Pos2 - Pos - 4,
|
||||||
|
TestBin = << 16#02:32/little, N:32/little-signed, 16#1d:32/little, 16#12:32/little,
|
||||||
|
16#2d:32/little, Jump:32/little, 16#1b:32/little, InstrsBin/binary >>,
|
||||||
|
case_tests(Next, Funcs2, Pos2 + 2, [TestBin|Acc], [VarsList|VarsAcc]).
|
||||||
|
|
||||||
|
case_tests_end(Tests) ->
|
||||||
|
case_tests_end(Tests, []).
|
||||||
|
case_tests_end([], Acc) ->
|
||||||
|
iolist_to_binary(lists:reverse(Acc));
|
||||||
|
case_tests_end([TestBin|Tail], Acc) ->
|
||||||
|
TailBin = iolist_to_binary(Tail),
|
||||||
|
Jump = byte_size(TailBin) div 4 + 2 * length(Tail),
|
||||||
|
case_tests_end(Tail, [<< TestBin/binary, 16#2c:32/little, Jump:32/little >>|Acc]).
|
||||||
|
|
||||||
|
funcs(Funcs) ->
|
||||||
|
funcs(Funcs, []).
|
||||||
|
funcs([], Acc) ->
|
||||||
|
iolist_to_binary(Acc);
|
||||||
|
funcs([Name|Tail], Acc) ->
|
||||||
|
NameBin = list_to_binary(Name),
|
||||||
|
Padding = 8 * (32 - byte_size(NameBin)),
|
||||||
|
funcs(Tail, [<< NameBin/binary, 0:Padding >>|Acc]).
|
||||||
|
|
||||||
|
find_func(Name, Funcs) ->
|
||||||
|
find_func(Name, lists:reverse(Funcs), 0).
|
||||||
|
find_func(_Name, [], N) ->
|
||||||
|
{{error, undefined}, N};
|
||||||
|
find_func(Name, [Func|_Tail], N) when Name =:= Func ->
|
||||||
|
{ok, N};
|
||||||
|
find_func(Name, [_Func|Tail], N) ->
|
||||||
|
find_func(Name, Tail, N + 1).
|
1076
apps/egs/src/egs_script_lexer.erl
Normal file
1076
apps/egs/src/egs_script_lexer.erl
Normal file
File diff suppressed because it is too large
Load Diff
509
apps/egs/src/egs_script_lexer.xrl
Normal file
509
apps/egs/src/egs_script_lexer.xrl
Normal file
@ -0,0 +1,509 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
|
%% @doc EGS script lexer.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Definitions.
|
||||||
|
|
||||||
|
D = [0-9]
|
||||||
|
L = [a-zA-Z]
|
||||||
|
N = ({L}|{D}|_)
|
||||||
|
WS = ([\000-\s]|%.*)
|
||||||
|
|
||||||
|
Rules.
|
||||||
|
|
||||||
|
-*{D}+ : {token, {integer, TokenLine, list_to_integer(TokenChars)}}.
|
||||||
|
-*{D}+\.{D}+ : {token, {float, TokenLine, list_to_float(TokenChars)}}.
|
||||||
|
{N}+\.*{N}+ : {token, case reserved_word(TokenChars) of
|
||||||
|
false -> case syscall(TokenChars) of
|
||||||
|
false -> {function, TokenLine, function(TokenChars)};
|
||||||
|
Syscall -> {syscall, TokenLine, Syscall}
|
||||||
|
end;
|
||||||
|
KeyWord -> {KeyWord, TokenLine}
|
||||||
|
end}.
|
||||||
|
"(\\\^.|\\.|[^"])*" : %% Strip quotes.
|
||||||
|
S = lists:sublist(TokenChars, 2, TokenLen - 2),
|
||||||
|
{token, {string, TokenLine, string_gen(S)}}.
|
||||||
|
-> : {token, {'->', TokenLine}}.
|
||||||
|
[}{,;] : {token, {list_to_atom(TokenChars), TokenLine}}.
|
||||||
|
\.{WS} : {end_token, {dot, TokenLine}}.
|
||||||
|
{WS}+ : skip_token.
|
||||||
|
|
||||||
|
Erlang code.
|
||||||
|
|
||||||
|
%% Reserved words.
|
||||||
|
reserved_word("debug") -> debug;
|
||||||
|
%% Definitions.
|
||||||
|
reserved_word("event") -> event_def;
|
||||||
|
reserved_word("function") -> function_def;
|
||||||
|
reserved_word("num_var") -> num_var;
|
||||||
|
reserved_word("str_var") -> str_var;
|
||||||
|
%% Low level opcodes.
|
||||||
|
reserved_word("abs") -> abs;
|
||||||
|
reserved_word("add") -> add;
|
||||||
|
reserved_word("band") -> 'band';
|
||||||
|
reserved_word("bor") -> 'bor';
|
||||||
|
reserved_word("bxor") -> 'bxor';
|
||||||
|
reserved_word("dec") -> dec;
|
||||||
|
reserved_word("div") -> 'div';
|
||||||
|
reserved_word("inc") -> inc;
|
||||||
|
reserved_word("is_eq") -> is_eq;
|
||||||
|
reserved_word("is_gt") -> is_gt;
|
||||||
|
reserved_word("is_gteq") -> is_gteq;
|
||||||
|
reserved_word("is_lt") -> is_lt;
|
||||||
|
reserved_word("is_lteq") -> is_lteq;
|
||||||
|
reserved_word("is_neq") -> is_neq;
|
||||||
|
reserved_word("jmp") -> jmp;
|
||||||
|
reserved_word("jnz") -> jnz;
|
||||||
|
reserved_word("jz") -> jz;
|
||||||
|
reserved_word("land") -> land;
|
||||||
|
reserved_word("lor") -> lor;
|
||||||
|
reserved_word("lshift") -> lshift;
|
||||||
|
reserved_word("mod") -> mod;
|
||||||
|
reserved_word("mul") -> mul;
|
||||||
|
reserved_word("neg") -> neg;
|
||||||
|
reserved_word("nop") -> nop;
|
||||||
|
reserved_word("num_get") -> num_get;
|
||||||
|
reserved_word("num_set") -> num_set;
|
||||||
|
reserved_word("push") -> push;
|
||||||
|
reserved_word("restore") -> restore;
|
||||||
|
reserved_word("return") -> return;
|
||||||
|
reserved_word("rshift") -> rshift;
|
||||||
|
reserved_word("save") -> save;
|
||||||
|
reserved_word("savep") -> savep;
|
||||||
|
reserved_word("str_get") -> str_get;
|
||||||
|
reserved_word("str_set") -> str_set;
|
||||||
|
reserved_word("sub") -> sub;
|
||||||
|
%% Case statement.
|
||||||
|
reserved_word("case") -> 'case';
|
||||||
|
reserved_word("default") -> default;
|
||||||
|
reserved_word("end") -> 'end';
|
||||||
|
%% Otherwise isn't a reserved word.
|
||||||
|
reserved_word(_) -> false.
|
||||||
|
|
||||||
|
%% Syscalls.
|
||||||
|
syscall("mes.fukidasi_pc") -> {100, sync};
|
||||||
|
syscall("mes.fukidasi_apc") -> {101, sync};
|
||||||
|
syscall("mes.fukidasi_npc") -> {102, sync};
|
||||||
|
syscall("mes.fukidasi_pc_type") -> {103, sync};
|
||||||
|
syscall("mes.fukidasi_apc_type") -> {104, sync};
|
||||||
|
syscall("mes.fukidasi_npc_type") -> {105, sync};
|
||||||
|
syscall("mes.fukidasi_custom") -> {106, sync};
|
||||||
|
syscall("mes.erase_fukidasi") -> {107, async};
|
||||||
|
syscall("mes.chat_pc") -> {108, async};
|
||||||
|
syscall("mes.cut_in_chat") -> {109, async};
|
||||||
|
syscall("mes.cut_in_chat_custom") -> {110, async};
|
||||||
|
syscall("mes.system_win") -> {111, sync};
|
||||||
|
syscall("mes.custom_win") -> {112, sync};
|
||||||
|
syscall("mes.scroll_win") -> {113, sync};
|
||||||
|
syscall("mes.win_move") -> {114, async};
|
||||||
|
syscall("mes.erase_system_win") -> {115, async};
|
||||||
|
syscall("mes.select_win") -> {116, sync};
|
||||||
|
syscall("mes.select_win_set") -> {117, sync};
|
||||||
|
syscall("mes.select_win_single") -> {118, sync};
|
||||||
|
syscall("mes.select_win_custom") -> {119, sync};
|
||||||
|
syscall("mes.select_win_fukidasi") -> {120, sync};
|
||||||
|
syscall("mes.select_win_b") -> {121, sync};
|
||||||
|
syscall("mes.select_win_set_b") -> {122, sync};
|
||||||
|
syscall("mes.select_win_single_b") -> {123, sync};
|
||||||
|
syscall("mes.select_win_custom_b") -> {124, sync};
|
||||||
|
syscall("mes.select_win_fukidasi_b") -> {125, sync};
|
||||||
|
syscall("mes.erase_select_win") -> {126, sync};
|
||||||
|
syscall("mes.message_end") -> {127, async};
|
||||||
|
syscall("mes.set_name_npc") -> {128, async};
|
||||||
|
syscall("mes.set_color_npc") -> {129, async};
|
||||||
|
syscall("mes.line_window") -> {130, async};
|
||||||
|
syscall("adv.cut_in") -> {131, async};
|
||||||
|
syscall("adv.cut_in_custom") -> {132, async};
|
||||||
|
syscall("adv.window_disable") -> {133, async};
|
||||||
|
syscall("adv.window_enable") -> {134, async};
|
||||||
|
syscall("adv.fukidasi_disable") -> {135, async};
|
||||||
|
syscall("adv.fukidasi_enable") -> {136, async};
|
||||||
|
syscall("adv.player_disable") -> {137, async};
|
||||||
|
syscall("adv.player_enable") -> {138, async};
|
||||||
|
syscall("adv.chat_on") -> {139, async};
|
||||||
|
syscall("adv.chat_off") -> {140, async};
|
||||||
|
syscall("adv.mainmenu_disable") -> {141, async};
|
||||||
|
syscall("adv.mainmenu_enable") -> {142, async};
|
||||||
|
syscall("unit_flag.on") -> {143, async};
|
||||||
|
syscall("unit_flag.off") -> {144, async};
|
||||||
|
syscall("unit_flag.reverse") -> {145, async};
|
||||||
|
syscall("unit_flag.delay_on") -> {146, async};
|
||||||
|
syscall("unit_flag.delay_off") -> {147, async};
|
||||||
|
syscall("unit_flag.delay_reverse") -> {148, async};
|
||||||
|
syscall("unit_flag.chk") -> {149, async};
|
||||||
|
syscall("player.get_my_id") -> {150, async};
|
||||||
|
syscall("player.set_pos") -> {151, async};
|
||||||
|
syscall("player.change_unit") -> {152, async};
|
||||||
|
syscall("player.turn") -> {153, async};
|
||||||
|
syscall("player.walk") -> {154, async};
|
||||||
|
syscall("player.run") -> {155, async};
|
||||||
|
syscall("player.pad_off") -> {158, async};
|
||||||
|
syscall("player.pad_on") -> {159, async};
|
||||||
|
syscall("player.get_pos") -> {160, async};
|
||||||
|
syscall("player.get_ang") -> {161, async};
|
||||||
|
syscall("player.turn_coord") -> {162, async};
|
||||||
|
syscall("player.turn_member") -> {163, async};
|
||||||
|
syscall("plymotion.item_take_off") -> {173, async};
|
||||||
|
syscall("plymotion.set_pack") -> {174, sync};
|
||||||
|
syscall("plymotion.release_pack") -> {176, async};
|
||||||
|
syscall("plymotion.set_loop") -> {177, async};
|
||||||
|
syscall("plymotion.play_one_shot") -> {178, sync};
|
||||||
|
syscall("plymotion.cancel_one_shot") -> {179, async};
|
||||||
|
syscall("plymotion.restart_one_shot") -> {180, async};
|
||||||
|
syscall("apc.create") -> {183, sync};
|
||||||
|
syscall("apc.delete") -> {184, async};
|
||||||
|
syscall("apc.delete_inx") -> {185, async};
|
||||||
|
syscall("apc.team_into") -> {186, async};
|
||||||
|
syscall("apc.team_remove") -> {187, async};
|
||||||
|
syscall("apc.set_pos") -> {188, async};
|
||||||
|
syscall("apc.turn") -> {189, async};
|
||||||
|
syscall("apc.walk") -> {190, async};
|
||||||
|
syscall("apc.run") -> {191, async};
|
||||||
|
syscall("apc.think_off") -> {194, async};
|
||||||
|
syscall("apc.think_on") -> {195, async};
|
||||||
|
syscall("apc.get_pos") -> {196, async};
|
||||||
|
syscall("apc.get_ang") -> {197, async};
|
||||||
|
syscall("apc.reserve") -> {205, async};
|
||||||
|
syscall("apc.reserve_cancel") -> {206, async};
|
||||||
|
syscall("apc.is_reserve") -> {207, async};
|
||||||
|
syscall("apc.create_lv") -> {208, sync};
|
||||||
|
syscall("apc.reserve_lv") -> {209, async};
|
||||||
|
syscall("apc.target_guard") -> {210, async};
|
||||||
|
syscall("apc.target_guard_pid") -> {211, async};
|
||||||
|
syscall("apc.target_guard_aid") -> {212, async};
|
||||||
|
syscall("apc.cancel_target_guard") -> {213, async};
|
||||||
|
syscall("apc.set_move_out_party") -> {214, async};
|
||||||
|
syscall("apc.apc_set_ban") -> {215, async};
|
||||||
|
syscall("apc.get_aid_from_pid") -> {216, async};
|
||||||
|
syscall("apc.is_apc2") -> {217, async};
|
||||||
|
syscall("apc.is_alive") -> {218, async};
|
||||||
|
syscall("camera.set") -> {219, async};
|
||||||
|
syscall("camera.move") -> {220, async};
|
||||||
|
syscall("camera.release") -> {221, async};
|
||||||
|
syscall("camera.get_pos") -> {222, async};
|
||||||
|
syscall("camera.cam_rot") -> {223, async};
|
||||||
|
syscall("camera.target_rot") -> {224, async};
|
||||||
|
syscall("camera.dist_move") -> {225, async};
|
||||||
|
syscall("camera.get_cam_angle") -> {226, async};
|
||||||
|
syscall("camera.get_target_angle") -> {227, async};
|
||||||
|
syscall("camera.get_dist") -> {228, async};
|
||||||
|
syscall("camera.set_quake") -> {229, async};
|
||||||
|
syscall("camera.stop_quake") -> {230, async};
|
||||||
|
syscall("direction.fade_in") -> {232, async};
|
||||||
|
syscall("direction.fade_out") -> {233, async};
|
||||||
|
syscall("direction.fade_out_rgba") -> {234, async};
|
||||||
|
syscall("direction.cinema_on") -> {235, async};
|
||||||
|
syscall("direction.cinema_off") -> {236, async};
|
||||||
|
syscall("direction.vib") -> {237, async};
|
||||||
|
syscall("wait.abort_cancel") -> {238, async};
|
||||||
|
syscall("temp.exit_game") -> {239, async};
|
||||||
|
syscall("movie.play_prm") -> {240, sync};
|
||||||
|
syscall("movie.play_rtm") -> {242, sync};
|
||||||
|
syscall("movie.play_event") -> {245, sync};
|
||||||
|
syscall("movie.play_sub_title") -> {246, sync};
|
||||||
|
syscall("movie.telop") -> {247, sync};
|
||||||
|
syscall("sound.play_se") -> {251, async};
|
||||||
|
syscall("sound.play_bgm") -> {253, async}; %% Unofficial name.
|
||||||
|
syscall("sound.stop_bgm") -> {256, async};
|
||||||
|
syscall("render.scene_off") -> {257, async};
|
||||||
|
syscall("render.scene_on") -> {258, async};
|
||||||
|
syscall("render.npc_all_off") -> {259, async};
|
||||||
|
syscall("render.npc_all_on") -> {260, async};
|
||||||
|
syscall("render.npc_off") -> {261, async};
|
||||||
|
syscall("render.npc_on") -> {262, async};
|
||||||
|
syscall("render.player_off") -> {263, async};
|
||||||
|
syscall("render.player_on") -> {264, async};
|
||||||
|
syscall("render.enemy_off") -> {265, async};
|
||||||
|
syscall("render.enemy_on") -> {266, async};
|
||||||
|
syscall("render.obj_off") -> {267, async};
|
||||||
|
syscall("render.obj_on") -> {268, async};
|
||||||
|
syscall("seq.get_quest") -> {271, async};
|
||||||
|
syscall("seq.is_online") -> {272, async};
|
||||||
|
syscall("seq.save_qst") -> {273, sync};
|
||||||
|
syscall("seq.get_area") -> {274, async};
|
||||||
|
syscall("seq.get_zone") -> {275, async};
|
||||||
|
syscall("seq.get_unit") -> {276, async};
|
||||||
|
syscall("seq.get_item") -> {280, sync};
|
||||||
|
syscall("work.accountwork_get") -> {282, sync};
|
||||||
|
syscall("work.accountwork_set") -> {283, sync};
|
||||||
|
syscall("work.chrwork_get") -> {284, sync};
|
||||||
|
syscall("work.chrwork_set") -> {285, sync};
|
||||||
|
syscall("work.chrflag_get") -> {286, sync};
|
||||||
|
syscall("work.chrflag_set") -> {287, sync};
|
||||||
|
syscall("work.partyflag_on") -> {288, sync};
|
||||||
|
syscall("work.partyflag_off") -> {289, sync};
|
||||||
|
syscall("work.qstwork_get") -> {290, async};
|
||||||
|
syscall("work.qstwork_get_member") -> {291, async};
|
||||||
|
syscall("work.qstwork_set") -> {292, async};
|
||||||
|
syscall("work.partyflag_get") -> {297, async};
|
||||||
|
syscall("work.partywork_get") -> {298, async};
|
||||||
|
syscall("work.partywork_set") -> {299, async};
|
||||||
|
syscall("work.zone_cndflag_get") -> {300, async};
|
||||||
|
syscall("work.zone_cndflag_on") -> {301, async};
|
||||||
|
syscall("work.zone_cndflag_off") -> {302, async};
|
||||||
|
syscall("work.zone_sendwork_get") -> {303, async};
|
||||||
|
syscall("work.zone_sendwork_set") -> {304, async};
|
||||||
|
syscall("work.party_rand_get") -> {305, sync};
|
||||||
|
syscall("party.get_member") -> {306, async};
|
||||||
|
syscall("party.get_leader") -> {307, async};
|
||||||
|
syscall("party.chk_exist") -> {308, async};
|
||||||
|
syscall("party.sync_wait") -> {309, sync};
|
||||||
|
syscall("party.join_on") -> {310, async};
|
||||||
|
syscall("party.join_off") -> {311, async};
|
||||||
|
syscall("party.get_enemy_kill") -> {312, async};
|
||||||
|
syscall("ptcl.create") -> {315, async};
|
||||||
|
syscall("ptcl.move") -> {316, async};
|
||||||
|
syscall("ptcl.delete") -> {317, async};
|
||||||
|
syscall("col.create") -> {318, async};
|
||||||
|
syscall("col.create_lock_on") -> {319, async};
|
||||||
|
syscall("col.create_attack") -> {320, async};
|
||||||
|
syscall("col.move") -> {321, async};
|
||||||
|
syscall("col.scale") -> {322, async};
|
||||||
|
syscall("col.delete") -> {323, async};
|
||||||
|
syscall("npc.talk_on") -> {324, async};
|
||||||
|
syscall("npc.talk_off") -> {325, async};
|
||||||
|
syscall("npc.think_on") -> {326, async};
|
||||||
|
syscall("npc.think_off") -> {327, async};
|
||||||
|
syscall("npc.set_pos") -> {328, async};
|
||||||
|
syscall("npc.turn") -> {329, async};
|
||||||
|
syscall("npc.gaze") -> {330, async};
|
||||||
|
syscall("npc.walk") -> {331, async};
|
||||||
|
syscall("npc.run") -> {332, async};
|
||||||
|
syscall("npc.walk_path") -> {335, async};
|
||||||
|
syscall("npc.run_path") -> {336, async};
|
||||||
|
syscall("npc.get_pos") -> {338, async};
|
||||||
|
syscall("npc.get_ang") -> {339, async};
|
||||||
|
syscall("npc.cng_motion") -> {340, async};
|
||||||
|
syscall("npc.cancel_motion") -> {341, async};
|
||||||
|
syscall("npc.get_motion") -> {342, async};
|
||||||
|
syscall("npc.coli_end") -> {343, async};
|
||||||
|
syscall("obj.coli_end") -> {347, async}; %% Unofficial name.
|
||||||
|
syscall("status.get_posflag") -> {354, async};
|
||||||
|
syscall("lobby.start") -> {356, sync};
|
||||||
|
syscall("lobby.end") -> {357, async};
|
||||||
|
syscall("lobby.select_planet") -> {358, sync};
|
||||||
|
syscall("camera.def_cng") -> {359, async};
|
||||||
|
syscall("camera.def_remove") -> {360, async};
|
||||||
|
syscall("camera.def_move_back") -> {361, async};
|
||||||
|
syscall("camera.def_set_back") -> {362, async};
|
||||||
|
syscall("camera.def_set_quake") -> {364, async};
|
||||||
|
syscall("camera.def_stop_qake") -> {365, async};
|
||||||
|
syscall("calc.get_rot") -> {373, async};
|
||||||
|
syscall("sjis.fukidasi_pc") -> {389, sync};
|
||||||
|
syscall("sjis.fukidasi_apc") -> {390, sync};
|
||||||
|
syscall("sjis.fukidasi_npc") -> {391, sync};
|
||||||
|
syscall("sjis.fukidasi_pc_type") -> {392, sync};
|
||||||
|
syscall("sjis.fukidasi_apc_type") -> {393, sync};
|
||||||
|
syscall("sjis.fukidasi_npc_type") -> {394, sync};
|
||||||
|
syscall("sjis.fukidasi_custom") -> {395, sync};
|
||||||
|
syscall("sjis.chat_pc") -> {396, async};
|
||||||
|
syscall("sjis.cut_in_chat") -> {397, async};
|
||||||
|
syscall("sjis.cut_in_chat_custom") -> {398, async};
|
||||||
|
syscall("sjis.system_win") -> {399, sync};
|
||||||
|
syscall("sjis.custom_win") -> {400, sync};
|
||||||
|
syscall("sjis.scroll_win") -> {401, sync};
|
||||||
|
syscall("sjis.select_win") -> {402, sync};
|
||||||
|
syscall("sjis.select_win_set") -> {403, sync};
|
||||||
|
syscall("sjis.select_win_single") -> {404, sync};
|
||||||
|
syscall("sjis.select_win_custom") -> {405, sync};
|
||||||
|
syscall("sjis.select_win_fukidasi") -> {406, sync};
|
||||||
|
syscall("sjis.select_win_b") -> {407, sync};
|
||||||
|
syscall("sjis.select_win_set_b") -> {408, sync};
|
||||||
|
syscall("sjis.select_win_single_b") -> {409, sync};
|
||||||
|
syscall("sjis.select_win_custom_b") -> {410, sync};
|
||||||
|
syscall("sjis.select_win_fukidasi_b") -> {411, sync};
|
||||||
|
syscall("sjis.line_window") -> {412, async};
|
||||||
|
syscall("temp.line") -> {413, async};
|
||||||
|
syscall("temp.sline") -> {414, async};
|
||||||
|
syscall(_) -> false.
|
||||||
|
|
||||||
|
%% Functions.
|
||||||
|
function("adv.cut_in_load") -> {"q.adv.cut_in_load", async};
|
||||||
|
function("adv.cut_in_noise") -> {"q.adv.cut_in_noise", async};
|
||||||
|
function("adv.cut_in_release") -> {"q.adv.cut_in_release", async};
|
||||||
|
function("adv.cut_in_stop") -> {"q.adv.cut_in_stop", async};
|
||||||
|
%% @todo apc.apc_sp_exec
|
||||||
|
%% @todo q.apc.create_ex
|
||||||
|
function("apc.get_reserve_aid_all") -> {"apc.get_reserve_aid_all", async};
|
||||||
|
function("apc.get_reserve_count") -> {"apc.get_reserve_count", async};
|
||||||
|
function("apc.guildcard_disable") -> {"apc.guildcard_disable", async};
|
||||||
|
function("apc.guildcard_enable") -> {"apc.guildcard_enable", async};
|
||||||
|
function("apc.impossible_join_pt") -> {"apc.impossible_join_pt", async};
|
||||||
|
function("apc.is_set_ban") -> {"apc.is_set_ban", async};
|
||||||
|
%% @todo apc.move
|
||||||
|
%% @todo apc.move_xz
|
||||||
|
function("apc.possible_join_pt") -> {"apc.possible_join_pt", async};
|
||||||
|
%% @todo apc.set_goal_out_party
|
||||||
|
function("apc.set_sp_ban") -> {"apc.set_sp_ban", async};
|
||||||
|
function("apc.set_talk_ban") -> {"apc.set_talk_ban", async};
|
||||||
|
function("counter.create_dc_map") -> {"counter.create_dc_map", async};
|
||||||
|
function("counter.delete_dc_map") -> {"counter.delete_dc_map", async};
|
||||||
|
function("counter.set_arrow_pos") -> {"counter.set_arrow_pos", async};
|
||||||
|
function("counter.set_lookon_pos") -> {"counter.set_lookon_pos", async};
|
||||||
|
function("direction.fade_out_nowloading") -> {"direction.fade_out_nowloading", async};
|
||||||
|
function("font.set_party_member_id") -> {"font.set_party_member_id", async};
|
||||||
|
function("item.exchange") -> {"q.seq.exchange_item", sync};
|
||||||
|
function("item.get_num") -> {"q.seq.get_item_num", sync};
|
||||||
|
function("item.get_slot_num_req") -> {"q.item.get_slot_num_req", sync};
|
||||||
|
function("item.get_slot_num_ret") -> {"q.item.get_slot_num_ret", async};
|
||||||
|
%% @todo item.set_ability
|
||||||
|
function("mes.mes_broadcast_clear") -> {"mes.mes_broadcast_clear", async};
|
||||||
|
%% @todo mes.mes_broadcast_clear_all
|
||||||
|
function("mes.mes_broadcast_set") -> {"mes.mes_broadcast_set", async};
|
||||||
|
function("mes.line_window_time") -> {"q.mes.line_window_line_time", async};
|
||||||
|
function("mes.select_win_number") -> {"q.mes.select_win_number", sync};
|
||||||
|
function("movie.text_prm_delay") -> {"movie.text_prm_delay", async};
|
||||||
|
function("myroom.check_data_load") -> {"myroom.check_data_load", async};
|
||||||
|
function("myroom.disable_obj_ex") -> {"myroom.disable_obj_ex", async};
|
||||||
|
function("myroom.get_move_planet") -> {"myroom.get_move_planet", async};
|
||||||
|
function("myroom.get_probo_ang") -> {"myroom.get_probo_ang", async};
|
||||||
|
function("myroom.get_probo_pos") -> {"myroom.get_probo_pos", async};
|
||||||
|
function("myroom.get_probo_rank") -> {"myroom.get_probo_rank", async};
|
||||||
|
function("myroom.news_enable") -> {"myroom.news_enable", async};
|
||||||
|
%% @todo myroom.get_probo_type
|
||||||
|
function("npc.gaze_cancel") -> {"npc.gaze_cancel", async};
|
||||||
|
%% @todo npc.get_head_y
|
||||||
|
function("npc.is_load_comp") -> {"npc.is_load_comp", async};
|
||||||
|
%% @todo q.npc.run_fast
|
||||||
|
%% @todo npc.run_xz_fast
|
||||||
|
%% @todo q.npc.walk_fast
|
||||||
|
%% @todo npc.walk_xz_fast
|
||||||
|
%% @todo obj.chair_disable
|
||||||
|
%% @todo obj.chair_enable
|
||||||
|
function("obj.get_eventflag") -> {"obj.get_eventflag", async};
|
||||||
|
function("obj.set_caption") -> {"obj.set_caption", async};
|
||||||
|
function("obj.vehicle_resetpos") -> {"obj.vehicle_resetpos", async};
|
||||||
|
function("player.display") -> {"player.display", async};
|
||||||
|
function("player.disp_off_except_party") -> {"player.disp_off_except_party", async};
|
||||||
|
function("player.disp_on_except_party") -> {"player.disp_on_except_party", async};
|
||||||
|
function("player.face_set") -> {"player.face_set", async};
|
||||||
|
%% @todo player.gaze_body
|
||||||
|
%% @todo player.get_action
|
||||||
|
function("player.get_level") -> {"player.get_level", async};
|
||||||
|
function("player.get_my_pmid") -> {"q.player.get_my_pmid", async};
|
||||||
|
function("player.get_occupation") -> {"player.get_occupation", async};
|
||||||
|
function("player.get_off_vehicle") -> {"player.get_off_vehicle", async};
|
||||||
|
%% @todo player.get_on_vehicle
|
||||||
|
function("player.get_race") -> {"player.get_race", async};
|
||||||
|
function("player.get_sex") -> {"player.get_sex", async};
|
||||||
|
function("player.hide") -> {"player.hide", async};
|
||||||
|
function("player.is_dead") -> {"player.is_dead", async};
|
||||||
|
%% @todo player.is_equipping_item
|
||||||
|
function("player.is_in_sp") -> {"player.is_in_sp", async};
|
||||||
|
function("player.is_on_vehicle") -> {"player.is_on_vehicle", async};
|
||||||
|
%% @todo player.move
|
||||||
|
%% @todo player.move_xz
|
||||||
|
%% @todo player.set_vehicle_breakpos
|
||||||
|
function("player.sp_cancel") -> {"player.sp_cancel", async};
|
||||||
|
%% @todo player.turn_round
|
||||||
|
%% @todo player.turn_round_coord_xz
|
||||||
|
function("plymotion.can_i_set_event_pack") -> {"plymotion.can_i_set_event_pack", async};
|
||||||
|
%% @todo plymotion.get_pack
|
||||||
|
function("plymotion.is_ready") -> {"plymotion.is_ready", async};
|
||||||
|
function("plymotion.item_re_equip") -> {"plymotion.item_re_equip", async};
|
||||||
|
%% @todo radar.display
|
||||||
|
%% @todo radar.fullclose
|
||||||
|
function("radar.fullopen") -> {"radar.fullopen", async};
|
||||||
|
%% @todo radar.hide
|
||||||
|
function("seq.endroll_play") -> {"q.seq.endroll_play", sync};
|
||||||
|
%% @todo seq.get_death_count
|
||||||
|
%% @todo seq.get_death_count_apc
|
||||||
|
function("seq.get_destroy_enemy_count") -> {"seq.get_destroy_enemy_count", async};
|
||||||
|
function("seq.get_elapsed_time") -> {"seq.get_elapsed_time", async};
|
||||||
|
function("seq.get_judgement") -> {"seq.get_judgement", async};
|
||||||
|
function("seq.get_mission_step") -> {"seq.get_mission_step", async};
|
||||||
|
function("seq.get_rank") -> {"seq.get_rank", async};
|
||||||
|
%% @todo seq.get_remaining_time
|
||||||
|
%% @todo seq.get_setdata_inx
|
||||||
|
%% @todo seq.goto_title
|
||||||
|
function("seq.has_finished_result") -> {"seq.has_finished_result", async};
|
||||||
|
function("seq.open_ex_mode") -> {"seq.open_ex_mode", async};
|
||||||
|
function("seq.preview_network") -> {"q.seq.preview_network", sync};
|
||||||
|
function("seq.preview_play") -> {"q.seq.preview_play", sync};
|
||||||
|
function("seq.result") -> {"q.seq.result", async};
|
||||||
|
function("seq.save_menu") -> {"q.seq.save_menu", sync};
|
||||||
|
%% @todo seq.set_chapter_no
|
||||||
|
function("seq.set_ex_chapter_no") -> {"seq.set_ex_chapter_no", async};
|
||||||
|
%% @todo seq.set_story_end_flag
|
||||||
|
function("seq.staffroll_play") -> {"q.seq.staffroll_play", sync};
|
||||||
|
%% @todo q.seq.unify_counter
|
||||||
|
function("shop.open") -> {"shop.open", async};
|
||||||
|
%% @todo shop.req_load_data
|
||||||
|
function("sjis.line_window_time") -> {"q.sjis.line_window_time", async};
|
||||||
|
function("sound.check_jingle") -> {"sound.check_jingle", async};
|
||||||
|
%% @todo sound.get_player_pack
|
||||||
|
function("sound.load_enemy_pack") -> {"sound.load_enemy_pack", async};
|
||||||
|
%% @todo sound.load_enemy_port_pack
|
||||||
|
function("sound.load_event_pack") -> {"sound.load_event_pack", async};
|
||||||
|
function("sound.load_player_pack") -> {"sound.load_player_pack", async};
|
||||||
|
%% @todo sound.pause_adx_voice
|
||||||
|
function("sound.pause_bgm") -> {"sound.pause_bgm", async};
|
||||||
|
function("sound.play_jingle") -> {"sound.play_jingle", async};
|
||||||
|
function("sound.play_se3d_loop_stop") -> {"sound.play_se3d_loop_stop", async};
|
||||||
|
function("sound.play_se3d_loop_vol") -> {"sound.play_se3d_loop_vol", async};
|
||||||
|
function("sound.play_player") -> {"q.sound.play_player", sync};
|
||||||
|
function("sound.play_se3d") -> {"q.sound.play_se3d", sync};
|
||||||
|
%% @todo sound.play_se3d_loop
|
||||||
|
function("sound.play_se3d_vol") -> {"q.sound.play_se3d_vol", sync};
|
||||||
|
function("sound.play_technique") -> {"sound.play_technique", async};
|
||||||
|
function("sound.play_vehicle") -> {"sound.play_vehicle", async};
|
||||||
|
function("sound.stop_adx_voice") -> {"sound.stop_adx_voice", async};
|
||||||
|
function("system.frame_adjust") -> {"system.frame_adjust", async};
|
||||||
|
%% @todo system.frame_default
|
||||||
|
function("system.frame_get") -> {"system.frame_get", async};
|
||||||
|
%% @todo system.frame_set
|
||||||
|
function("system.get_episode") -> {"system.get_episode", async};
|
||||||
|
%% @todo system.get_language
|
||||||
|
function("system.get_platform") -> {"system.get_platform", async};
|
||||||
|
function("system.get_resolution") -> {"system.get_resolution", async};
|
||||||
|
function("work.get_stamp_bonus") -> {"q.work.get_stamp_bonus", sync};
|
||||||
|
function("work.get_stamp_num") -> {"q.work.get_stamp_num", sync};
|
||||||
|
function("work.get_stamp_quest") -> {"q.work.get_stamp_quest", sync};
|
||||||
|
%% @todo q.work.on_accountwork_get
|
||||||
|
function("work.partyworknum_get") -> {"q.work.partyworknum_get", async};
|
||||||
|
function("work.partyworknum_set") -> {"q.work.partyworknum_set", async};
|
||||||
|
%% @todo q.work.stamp_command
|
||||||
|
function(Name) -> {Name, local}.
|
||||||
|
|
||||||
|
%% Strings.
|
||||||
|
string_gen([$\\|Cs]) ->
|
||||||
|
string_escape(Cs);
|
||||||
|
string_gen([C|Cs]) ->
|
||||||
|
[C|string_gen(Cs)];
|
||||||
|
string_gen([]) ->
|
||||||
|
[].
|
||||||
|
|
||||||
|
string_escape([O1,O2,O3|S]) when O1 >= $0, O1 =< $7, O2 >= $0, O2 =< $7, O3 >= $0, O3 =< $7 ->
|
||||||
|
[(O1*8 + O2)*8 + O3 - 73*$0|string_gen(S)];
|
||||||
|
string_escape([$^,C|Cs]) ->
|
||||||
|
[C band 31|string_gen(Cs)];
|
||||||
|
string_escape([C|Cs]) when C >= $\000, C =< $\s ->
|
||||||
|
string_gen(Cs);
|
||||||
|
string_escape([C|Cs]) ->
|
||||||
|
[escape_char(C)|string_gen(Cs)].
|
||||||
|
|
||||||
|
escape_char($n) -> $\n; %% \n = LF
|
||||||
|
escape_char($r) -> $\r; %% \r = CR
|
||||||
|
escape_char($t) -> $\t; %% \t = TAB
|
||||||
|
escape_char($v) -> $\v; %% \v = VT
|
||||||
|
escape_char($b) -> $\b; %% \b = BS
|
||||||
|
escape_char($f) -> $\f; %% \f = FF
|
||||||
|
escape_char($e) -> $\e; %% \e = ESC
|
||||||
|
escape_char($s) -> $\s; %% \s = SPC
|
||||||
|
escape_char($d) -> $\d; %% \d = DEL
|
||||||
|
escape_char(C) -> C.
|
1331
apps/egs/src/egs_script_parser.erl
Normal file
1331
apps/egs/src/egs_script_parser.erl
Normal file
File diff suppressed because it is too large
Load Diff
90
apps/egs/src/egs_script_parser.yrl
Normal file
90
apps/egs/src/egs_script_parser.yrl
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
|
%% @doc EGS script parser.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Nonterminals declarations declaration instructions instruction case_tests case_test case_default.
|
||||||
|
Terminals
|
||||||
|
integer float string function syscall debug event_def function_def num_var str_var
|
||||||
|
abs add band bor bxor dec div inc is_eq is_gt is_gteq is_lt is_lteq is_neq
|
||||||
|
jmp jnz jz land lor lshift mod mul neg nop num_get num_set push restore return
|
||||||
|
rshift save savep str_get str_set sub case default end '->' ',' ';' dot.
|
||||||
|
Rootsymbol declarations.
|
||||||
|
|
||||||
|
declarations -> declaration declarations : {'$1', '$2'}.
|
||||||
|
declarations -> '$empty' : nil.
|
||||||
|
|
||||||
|
declaration -> event_def function '->' instructions dot : {event_def, unwrap('$2'), '$4'}.
|
||||||
|
declaration -> function_def function '->' instructions dot : {function_def, unwrap('$2'), '$4'}.
|
||||||
|
declaration -> num_var function dot : {num_var, unwrap('$2')}.
|
||||||
|
declaration -> str_var function integer dot : {str_var, unwrap('$2'), unwrap('$3')}.
|
||||||
|
|
||||||
|
instructions -> instruction ',' instructions : {'$1', '$3'}.
|
||||||
|
instructions -> instruction : {'$1', nil}.
|
||||||
|
|
||||||
|
instruction -> abs : abs.
|
||||||
|
instruction -> add : add.
|
||||||
|
instruction -> band : 'band'.
|
||||||
|
instruction -> bor : 'bor'.
|
||||||
|
instruction -> bxor : 'bxor'.
|
||||||
|
instruction -> dec : dec.
|
||||||
|
instruction -> div : 'div'.
|
||||||
|
instruction -> inc : inc.
|
||||||
|
instruction -> is_eq : is_eq.
|
||||||
|
instruction -> is_gt : is_gt.
|
||||||
|
instruction -> is_gteq : is_gteq.
|
||||||
|
instruction -> is_lt : is_lt.
|
||||||
|
instruction -> is_lteq : is_lteq.
|
||||||
|
instruction -> is_neq : is_neq.
|
||||||
|
instruction -> jmp integer : {jmp, unwrap('$2')}.
|
||||||
|
instruction -> jnz integer : {jnz, unwrap('$2')}.
|
||||||
|
instruction -> jz integer : {jz, unwrap('$2')}.
|
||||||
|
instruction -> land : land.
|
||||||
|
instruction -> lor : lor.
|
||||||
|
instruction -> lshift : lshift.
|
||||||
|
instruction -> mod : mod.
|
||||||
|
instruction -> mul : mul.
|
||||||
|
instruction -> neg : neg.
|
||||||
|
instruction -> nop : nop.
|
||||||
|
instruction -> num_get function : {num_get, unwrap('$2')}.
|
||||||
|
instruction -> num_set function : {num_set, unwrap('$2')}.
|
||||||
|
instruction -> push integer : {push, unwrap('$2')}.
|
||||||
|
instruction -> push float : {push, unwrap('$2')}.
|
||||||
|
instruction -> push string : {push, unwrap('$2')}.
|
||||||
|
instruction -> restore : restore.
|
||||||
|
instruction -> return : return.
|
||||||
|
instruction -> rshift : rshift.
|
||||||
|
instruction -> save : save.
|
||||||
|
instruction -> savep : savep.
|
||||||
|
instruction -> str_get function : {str_get, unwrap('$2')}.
|
||||||
|
instruction -> str_set function : {str_set, unwrap('$2')}.
|
||||||
|
instruction -> sub : sub.
|
||||||
|
|
||||||
|
instruction -> debug : debug.
|
||||||
|
instruction -> function : {function, unwrap('$1')}.
|
||||||
|
instruction -> syscall : {syscall, unwrap('$1')}.
|
||||||
|
|
||||||
|
instruction -> 'case' case_tests 'end' : {'case', '$2'}.
|
||||||
|
case_tests -> case_test ';' case_tests : {'$1', '$3'}.
|
||||||
|
case_tests -> case_test ';' case_default : {'$1', {'$3', nil}}.
|
||||||
|
case_tests -> case_test : {'$1', nil}.
|
||||||
|
case_test -> integer '->' instructions : {case_test, unwrap('$1'), '$3'}.
|
||||||
|
case_default -> default '->' instructions : {case_default, '$3'}.
|
||||||
|
|
||||||
|
Erlang code.
|
||||||
|
|
||||||
|
unwrap({_,_,V}) -> V.
|
@ -1,5 +1,5 @@
|
|||||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
%% @copyright 2010 Loïc Hoguin.
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
%% @doc EGS seasons management.
|
%% @doc EGS seasons management.
|
||||||
%% @todo When we know how to do it we should change the lobby automatically to the next season.
|
%% @todo When we know how to do it we should change the lobby automatically to the next season.
|
||||||
%%
|
%%
|
||||||
@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
-module(egs_seasons).
|
-module(egs_seasons).
|
||||||
-behavior(gen_server).
|
-behavior(gen_server).
|
||||||
|
|
||||||
-export([start_link/0, stop/0, read/1]). %% API.
|
-export([start_link/0, stop/0, read/1]). %% API.
|
||||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
%% @copyright 2010 Loïc Hoguin.
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
%% @doc EGS shops database.
|
%% @doc EGS shops database.
|
||||||
%%
|
%%
|
||||||
%% This file is part of EGS.
|
%% This file is part of EGS.
|
||||||
@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
-module(egs_shops_db).
|
-module(egs_shops_db).
|
||||||
-behavior(gen_server).
|
-behavior(gen_server).
|
||||||
|
|
||||||
-export([start_link/0, stop/0, nth/2, read/1, reload/0]). %% API.
|
-export([start_link/0, stop/0, nth/2, read/1, reload/0]). %% API.
|
||||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
||||||
|
|
54
apps/egs/src/egs_sup.erl
Normal file
54
apps/egs/src/egs_sup.erl
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
|
%% @doc Top-level supervisor for the egs application.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_sup).
|
||||||
|
-behaviour(supervisor).
|
||||||
|
|
||||||
|
-export([start_link/0]). %% API.
|
||||||
|
-export([init/1]). %% Supervisor.
|
||||||
|
|
||||||
|
-spec start_link() -> {ok, pid()}.
|
||||||
|
start_link() ->
|
||||||
|
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||||
|
|
||||||
|
-spec init([]) -> {ok, {{one_for_one, 10, 10}, [supervisor:child_spec(), ...]}}.
|
||||||
|
init([]) ->
|
||||||
|
Procs = procs([egs_conf, {sup, egs_quests_sup}, {sup, egs_zones_sup},
|
||||||
|
egs_accounts, egs_users, egs_seasons, egs_counters_db, egs_items_db,
|
||||||
|
egs_npc_db, egs_quests_db, egs_shops_db, egs_universes], []),
|
||||||
|
{ok, {{one_for_one, 10, 10}, Procs}}.
|
||||||
|
|
||||||
|
%% Internal.
|
||||||
|
|
||||||
|
-spec procs([module()|{sup, module()}], [supervisor:child_spec()])
|
||||||
|
-> [supervisor:child_spec()].
|
||||||
|
procs([], Acc) ->
|
||||||
|
lists:reverse(Acc);
|
||||||
|
procs([{sup, Module}|Tail], Acc) ->
|
||||||
|
procs(Tail, [sup(Module)|Acc]);
|
||||||
|
procs([Module|Tail], Acc) ->
|
||||||
|
procs(Tail, [worker(Module)|Acc]).
|
||||||
|
|
||||||
|
-spec worker(M) -> {M, {M, start_link, []}, permanent, 5000, worker, dynamic}.
|
||||||
|
worker(Module) ->
|
||||||
|
{Module, {Module, start_link, []}, permanent, 5000, worker, dynamic}.
|
||||||
|
|
||||||
|
-spec sup(M) -> {M, {M, start_link, []}, permanent, 5000, supervisor, [M]}.
|
||||||
|
sup(Module) ->
|
||||||
|
{Module, {Module, start_link, []}, permanent, 5000, supervisor, [Module]}.
|
157
apps/egs/src/egs_universes.erl
Normal file
157
apps/egs/src/egs_universes.erl
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
|
%% @doc EGS universes handler.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_universes).
|
||||||
|
-behavior(gen_server).
|
||||||
|
|
||||||
|
-export([start_link/0, stop/0, all/0, defaultid/0, enter/1, leave/1, myroomid/0, read/1, lobby_pid/2, reload/0]). %% API.
|
||||||
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
||||||
|
|
||||||
|
%% Use the module name for the server's name.
|
||||||
|
-define(SERVER, ?MODULE).
|
||||||
|
|
||||||
|
-record(state, {unis=[], lobbies=[]}).
|
||||||
|
|
||||||
|
%% Default universe IDs.
|
||||||
|
-define(MYROOM_ID, 21).
|
||||||
|
-define(DEFAULT_ID, 26).
|
||||||
|
|
||||||
|
%% Lobbies: permanent quests to start with the universe.
|
||||||
|
-define(LOBBIES, [1100000]).
|
||||||
|
|
||||||
|
%% API.
|
||||||
|
|
||||||
|
%% @spec start_link() -> {ok,Pid::pid()}
|
||||||
|
start_link() ->
|
||||||
|
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||||||
|
|
||||||
|
%% @spec stop() -> stopped
|
||||||
|
stop() ->
|
||||||
|
gen_server:call(?SERVER, stop).
|
||||||
|
|
||||||
|
%% @spec all() -> term()
|
||||||
|
all() ->
|
||||||
|
gen_server:call(?SERVER, all).
|
||||||
|
|
||||||
|
%% @spec defaultid() -> 26
|
||||||
|
%% @doc Return the default universe, Uni 01, with ID 26.
|
||||||
|
defaultid() ->
|
||||||
|
?DEFAULT_ID.
|
||||||
|
|
||||||
|
%% @spec enter(UniID) -> term()
|
||||||
|
enter(UniID) ->
|
||||||
|
gen_server:cast(?SERVER, {enter, UniID}).
|
||||||
|
|
||||||
|
%% @spec leave(UniID) -> term()
|
||||||
|
leave(UniID) ->
|
||||||
|
gen_server:cast(?SERVER, {leave, UniID}).
|
||||||
|
|
||||||
|
%% @spec myroomid() -> 21
|
||||||
|
%% @doc Return the ID for the myroom universe.
|
||||||
|
myroomid() ->
|
||||||
|
?MYROOM_ID.
|
||||||
|
|
||||||
|
%% @spec read(UniID) -> term()
|
||||||
|
read(UniID) ->
|
||||||
|
gen_server:call(?SERVER, {read, UniID}).
|
||||||
|
|
||||||
|
lobby_pid(UniID, QuestID) ->
|
||||||
|
gen_server:call(?SERVER, {lobby_pid, UniID, QuestID}).
|
||||||
|
|
||||||
|
%% @spec reload() -> ok
|
||||||
|
reload() ->
|
||||||
|
gen_server:cast(?SERVER, reload).
|
||||||
|
|
||||||
|
%% gen_server.
|
||||||
|
|
||||||
|
%% @doc Create the unis, then load all the permanent quests nbl files, then create processes for all the needed quests.
|
||||||
|
init([]) ->
|
||||||
|
State = #state{unis=[create_myroom()|create_unis()]},
|
||||||
|
[egs_quests_db:quest_nbl(QuestID) || QuestID <- ?LOBBIES],
|
||||||
|
Lobbies = lists:flatten([init_lobbies(UniID) || {UniID, {Type, _Name, _NbPlayers, _MaxPlayers}} <- State#state.unis, Type =:= universe]),
|
||||||
|
{ok, State#state{lobbies=Lobbies}}.
|
||||||
|
|
||||||
|
handle_call(all, _From, State) ->
|
||||||
|
{reply, State#state.unis, State};
|
||||||
|
|
||||||
|
handle_call({read, UniID}, _From, State) ->
|
||||||
|
{reply, proplists:get_value(UniID, State#state.unis), State};
|
||||||
|
|
||||||
|
handle_call({lobby_pid, UniID, QuestID}, _From, State) ->
|
||||||
|
{_, Pid} = lists:keyfind({UniID, QuestID}, 1, State#state.lobbies),
|
||||||
|
{reply, Pid, State};
|
||||||
|
|
||||||
|
handle_call(stop, _From, State) ->
|
||||||
|
{stop, normal, stopped, State};
|
||||||
|
|
||||||
|
handle_call(_Request, _From, State) ->
|
||||||
|
{reply, ignored, State}.
|
||||||
|
|
||||||
|
handle_cast({enter, UniID}, State) ->
|
||||||
|
{Type, Name, NbPlayers, MaxPlayers} = proplists:get_value(UniID, State#state.unis),
|
||||||
|
Unis = proplists:delete(UniID, State#state.unis),
|
||||||
|
Unis2 = [{UniID, {Type, Name, NbPlayers + 1, MaxPlayers}}|Unis],
|
||||||
|
Unis3 = lists:keysort(1, Unis2),
|
||||||
|
{noreply, State#state{unis=Unis3}};
|
||||||
|
|
||||||
|
handle_cast({leave, UniID}, State) ->
|
||||||
|
{Type, Name, NbPlayers, MaxPlayers} = proplists:get_value(UniID, State#state.unis),
|
||||||
|
Unis = proplists:delete(UniID, State#state.unis),
|
||||||
|
Unis2 = [{UniID, {Type, Name, NbPlayers - 1, MaxPlayers}}|Unis],
|
||||||
|
Unis3 = lists:keysort(1, Unis2),
|
||||||
|
{noreply, State#state{unis=Unis3}};
|
||||||
|
|
||||||
|
handle_cast(reload, _State) ->
|
||||||
|
{noreply, #state{unis=[create_myroom()|create_unis()]}};
|
||||||
|
|
||||||
|
handle_cast(_Msg, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
handle_info(_Info, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
terminate(_Reason, _State) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
code_change(_OldVsn, State, _Extra) ->
|
||||||
|
{ok, State}.
|
||||||
|
|
||||||
|
%% Internal.
|
||||||
|
|
||||||
|
%% @doc Max players defaults to 5000 for now.
|
||||||
|
create_myroom() ->
|
||||||
|
{ok, << 16#fffe:16, MyRoomName/binary >>} = file:read_file("priv/universes/myroom.en_US.txt"),
|
||||||
|
{?MYROOM_ID, {myroom, MyRoomName, 0, 5000}}.
|
||||||
|
|
||||||
|
%% @doc Max players defaults to 1000 for now.
|
||||||
|
create_unis() ->
|
||||||
|
{ok, << 16#fffe:16, Universes/binary >>} = file:read_file("priv/universes/universes.en_US.txt"),
|
||||||
|
Universes2 = re:split(Universes, "\n."),
|
||||||
|
create_unis(Universes2, ?DEFAULT_ID, []).
|
||||||
|
create_unis([], _UniID, Acc) ->
|
||||||
|
lists:reverse(Acc);
|
||||||
|
create_unis([Name|Tail], UniID, Acc) ->
|
||||||
|
create_unis(Tail, UniID + 2, [{UniID, {universe, Name, 0, 1000}}|Acc]).
|
||||||
|
|
||||||
|
%% @doc Start lobbies for the given universe.
|
||||||
|
init_lobbies(UniID) ->
|
||||||
|
lists:map(fun(QuestID) ->
|
||||||
|
{ok, Pid} = egs_quests_sup:start_quest(UniID, QuestID),
|
||||||
|
{{UniID, QuestID}, Pid}
|
||||||
|
end, ?LOBBIES).
|
239
apps/egs/src/egs_users.erl
Normal file
239
apps/egs/src/egs_users.erl
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
|
%% @doc Users handling.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_users).
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
-export([start_link/0, stop/0, broadcast/2, broadcast_all/1, find_by_pid/1, set_zone/3]). %% API.
|
||||||
|
-export([read/1, select/1, write/1, delete/1, item_nth/2, item_add/3, item_qty_add/3,
|
||||||
|
shop_enter/2, shop_leave/1, shop_get/1, money_add/2]). %% Deprecated API.
|
||||||
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
||||||
|
|
||||||
|
-define(SERVER, ?MODULE).
|
||||||
|
|
||||||
|
-include("include/records.hrl").
|
||||||
|
|
||||||
|
-record(state, {
|
||||||
|
users = [] :: list({egs:gid(), #users{}})
|
||||||
|
}).
|
||||||
|
|
||||||
|
%% API.
|
||||||
|
|
||||||
|
-spec start_link() -> {ok, Pid::pid()}.
|
||||||
|
start_link() ->
|
||||||
|
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||||||
|
|
||||||
|
-spec stop() -> stopped.
|
||||||
|
stop() ->
|
||||||
|
gen_server:call(?SERVER, stop).
|
||||||
|
|
||||||
|
broadcast(Message, PlayersGID) ->
|
||||||
|
gen_server:cast(?SERVER, {broadcast, Message, PlayersGID}).
|
||||||
|
|
||||||
|
broadcast_all(Message) ->
|
||||||
|
gen_server:cast(?SERVER, {broadcast_all, Message}).
|
||||||
|
|
||||||
|
find_by_pid(Pid) ->
|
||||||
|
gen_server:call(?SERVER, {find_by_pid, Pid}).
|
||||||
|
|
||||||
|
set_zone(GID, ZonePid, LID) ->
|
||||||
|
gen_server:call(?SERVER, {set_zone, GID, ZonePid, LID}).
|
||||||
|
|
||||||
|
%% Deprecated API.
|
||||||
|
|
||||||
|
%% @spec read(ID) -> {ok, User} | {error, badarg}
|
||||||
|
read(ID) ->
|
||||||
|
gen_server:call(?SERVER, {read, ID}).
|
||||||
|
|
||||||
|
select(GIDsList) ->
|
||||||
|
gen_server:call(?SERVER, {select, GIDsList}).
|
||||||
|
|
||||||
|
%% @spec write(User) -> ok
|
||||||
|
write(User) ->
|
||||||
|
gen_server:call(?SERVER, {write, User}).
|
||||||
|
|
||||||
|
%% @spec delete(GID) -> ok
|
||||||
|
delete(GID) ->
|
||||||
|
gen_server:call(?SERVER, {delete, GID}).
|
||||||
|
|
||||||
|
item_nth(GID, ItemIndex) ->
|
||||||
|
gen_server:call(?SERVER, {item_nth, GID, ItemIndex}).
|
||||||
|
|
||||||
|
item_add(GID, ItemID, Variables) ->
|
||||||
|
gen_server:call(?SERVER, {item_add, GID, ItemID, Variables}).
|
||||||
|
|
||||||
|
%% @todo Consumable items.
|
||||||
|
item_qty_add(GID, ItemIndex, QuantityDiff) ->
|
||||||
|
gen_server:call(?SERVER, {item_qty_add, GID, ItemIndex, QuantityDiff}).
|
||||||
|
|
||||||
|
shop_enter(GID, ShopID) ->
|
||||||
|
gen_server:call(?SERVER, {shop_enter, GID, ShopID}).
|
||||||
|
|
||||||
|
shop_leave(GID) ->
|
||||||
|
gen_server:call(?SERVER, {shop_leave, GID}).
|
||||||
|
|
||||||
|
shop_get(GID) ->
|
||||||
|
gen_server:call(?SERVER, {shop_get, GID}).
|
||||||
|
|
||||||
|
money_add(GID, MoneyDiff) ->
|
||||||
|
gen_server:call(?SERVER, {money_add, GID, MoneyDiff}).
|
||||||
|
|
||||||
|
%% gen_server.
|
||||||
|
|
||||||
|
init([]) ->
|
||||||
|
{ok, #state{}}.
|
||||||
|
|
||||||
|
handle_call({find_by_pid, Pid}, _From, State) ->
|
||||||
|
L = [User || {_GID, User} <- State#state.users, User#users.pid =:= Pid],
|
||||||
|
case L of
|
||||||
|
[] -> {reply, undefined, State};
|
||||||
|
[User] -> {reply, User, State}
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_call({set_zone, GID, ZonePid, LID}, _From, State) ->
|
||||||
|
{GID, User} = lists:keyfind(GID, 1, State#state.users),
|
||||||
|
Users2 = lists:delete({GID, User}, State#state.users),
|
||||||
|
{reply, ok, State#state{users=[{GID, User#users{zonepid=ZonePid, lid=LID}}|Users2]}};
|
||||||
|
|
||||||
|
handle_call({read, GID}, _From, State) ->
|
||||||
|
{GID, User} = lists:keyfind(GID, 1, State#state.users),
|
||||||
|
{reply, {ok, User}, State};
|
||||||
|
|
||||||
|
handle_call({select, UsersGID}, _From, State) ->
|
||||||
|
Users = [begin
|
||||||
|
{GID, User} = lists:keyfind(GID, 1, State#state.users),
|
||||||
|
User
|
||||||
|
end || GID <- UsersGID],
|
||||||
|
{reply, Users, State};
|
||||||
|
|
||||||
|
handle_call({write, User}, _From, State) ->
|
||||||
|
Users2 = lists:keydelete(User#users.gid, 1, State#state.users),
|
||||||
|
{reply, ok, State#state{users=[{User#users.gid, User}|Users2]}};
|
||||||
|
|
||||||
|
handle_call({delete, GID}, _From, State) ->
|
||||||
|
Users2 = lists:keydelete(GID, 1, State#state.users),
|
||||||
|
{reply, ok, State#state{users=Users2}};
|
||||||
|
|
||||||
|
handle_call({item_nth, GID, ItemIndex}, _From, State) ->
|
||||||
|
{GID, User} = lists:keyfind(GID, 1, State#state.users),
|
||||||
|
Item = lists:nth(ItemIndex + 1, User#users.inventory),
|
||||||
|
{reply, Item, State};
|
||||||
|
|
||||||
|
handle_call({item_add, GID, ItemID, Variables}, _From, State) ->
|
||||||
|
{GID, User} = lists:keyfind(GID, 1, State#state.users),
|
||||||
|
Inventory = case Variables of
|
||||||
|
#psu_consumable_item_variables{quantity=Quantity} ->
|
||||||
|
#psu_item{data=#psu_consumable_item{max_quantity=MaxQuantity}} = egs_items_db:read(ItemID),
|
||||||
|
{ItemID, #psu_consumable_item_variables{quantity=Quantity2}} = case lists:keyfind(ItemID, 1, User#users.inventory) of
|
||||||
|
false -> New = true, {ItemID, #psu_consumable_item_variables{quantity=0}};
|
||||||
|
Tuple -> New = false, Tuple
|
||||||
|
end,
|
||||||
|
Quantity3 = Quantity + Quantity2,
|
||||||
|
if Quantity3 =< MaxQuantity ->
|
||||||
|
lists:keystore(ItemID, 1, User#users.inventory, {ItemID, #psu_consumable_item_variables{quantity=Quantity3}})
|
||||||
|
end;
|
||||||
|
#psu_trap_item_variables{quantity=Quantity} ->
|
||||||
|
#psu_item{data=#psu_trap_item{max_quantity=MaxQuantity}} = egs_items_db:read(ItemID),
|
||||||
|
{ItemID, #psu_trap_item_variables{quantity=Quantity2}} = case lists:keyfind(ItemID, 1, User#users.inventory) of
|
||||||
|
false -> New = true, {ItemID, #psu_trap_item_variables{quantity=0}};
|
||||||
|
Tuple -> New = false, Tuple
|
||||||
|
end,
|
||||||
|
Quantity3 = Quantity + Quantity2,
|
||||||
|
if Quantity3 =< MaxQuantity ->
|
||||||
|
lists:keystore(ItemID, 1, User#users.inventory, {ItemID, #psu_trap_item_variables{quantity=Quantity3}})
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
New = true,
|
||||||
|
if length(User#users.inventory) < 60 ->
|
||||||
|
User#users.inventory ++ [{ItemID, Variables}]
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
Users2 = lists:keydelete(User#users.gid, 1, State#state.users),
|
||||||
|
State2 = State#state{users=[{GID, User#users{inventory=Inventory}}|Users2]},
|
||||||
|
case New of
|
||||||
|
false -> {reply, 16#ffffffff, State2};
|
||||||
|
true -> {reply, length(Inventory), State2}
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_call({item_qty_add, GID, ItemIndex, QuantityDiff}, _From, State) ->
|
||||||
|
{GID, User} = lists:keyfind(GID, 1, State#state.users),
|
||||||
|
{ItemID, Variables} = lists:nth(ItemIndex + 1, User#users.inventory),
|
||||||
|
Inventory = case Variables of
|
||||||
|
#psu_trap_item_variables{quantity=Quantity} ->
|
||||||
|
#psu_item{data=#psu_trap_item{max_quantity=MaxQuantity}} = egs_items_db:read(ItemID),
|
||||||
|
Quantity2 = Quantity + QuantityDiff,
|
||||||
|
if Quantity2 =:= 0 ->
|
||||||
|
string:substr(User#users.inventory, 1, ItemIndex) ++ string:substr(User#users.inventory, ItemIndex + 2);
|
||||||
|
Quantity2 > 0, Quantity2 =< MaxQuantity ->
|
||||||
|
Variables2 = Variables#psu_trap_item_variables{quantity=Quantity2},
|
||||||
|
string:substr(User#users.inventory, 1, ItemIndex) ++ [{ItemID, Variables2}] ++ string:substr(User#users.inventory, ItemIndex + 2)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
Users2 = lists:keydelete(User#users.gid, 1, State#state.users),
|
||||||
|
{reply, ok, State#state{users=[{GID, User#users{inventory=Inventory}}|Users2]}};
|
||||||
|
|
||||||
|
handle_call({shop_enter, GID, ShopID}, _From, State) ->
|
||||||
|
{GID, User} = lists:keyfind(GID, 1, State#state.users),
|
||||||
|
Users2 = lists:delete({GID, User}, State#state.users),
|
||||||
|
{reply, ok, State#state{users=[{GID, User#users{shopid=ShopID}}|Users2]}};
|
||||||
|
|
||||||
|
handle_call({shop_leave, GID}, _From, State) ->
|
||||||
|
{GID, User} = lists:keyfind(GID, 1, State#state.users),
|
||||||
|
Users2 = lists:delete({GID, User}, State#state.users),
|
||||||
|
{reply, ok, State#state{users=[{GID, User#users{shopid=undefined}}|Users2]}};
|
||||||
|
|
||||||
|
handle_call({shop_get, GID}, _From, State) ->
|
||||||
|
{GID, User} = lists:keyfind(GID, 1, State#state.users),
|
||||||
|
{reply, User#users.shopid, State};
|
||||||
|
|
||||||
|
handle_call({money_add, GID, MoneyDiff}, _From, State) ->
|
||||||
|
{GID, User} = lists:keyfind(GID, 1, State#state.users),
|
||||||
|
Money = User#users.money + MoneyDiff,
|
||||||
|
if Money >= 0 ->
|
||||||
|
Users2 = lists:delete({GID, User}, State#state.users),
|
||||||
|
{reply, ok, [{GID, User#users{money=Money}}|Users2]}
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_call(stop, _From, State) ->
|
||||||
|
{stop, normal, stopped, State};
|
||||||
|
|
||||||
|
handle_call(_Request, _From, State) ->
|
||||||
|
{reply, ignored, State}.
|
||||||
|
|
||||||
|
handle_cast({broadcast, Message, PlayersGID}, State) ->
|
||||||
|
[begin {GID, #users{pid=Pid}} = lists:keyfind(GID, 1, State#state.users),
|
||||||
|
Pid ! Message
|
||||||
|
end || GID <- PlayersGID],
|
||||||
|
{noreply, State};
|
||||||
|
|
||||||
|
handle_cast({broadcast_all, Message}, State) ->
|
||||||
|
[Pid ! Message || {_GID, #users{pid=Pid}} <- State#state.users],
|
||||||
|
{noreply, State};
|
||||||
|
|
||||||
|
handle_cast(_Msg, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
handle_info(_Info, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
terminate(_Reason, _State) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
code_change(_OldVsn, State, _Extra) ->
|
||||||
|
{ok, State}.
|
182
apps/egs/src/egs_zones.erl
Normal file
182
apps/egs/src/egs_zones.erl
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2011 Loïc Hoguin.
|
||||||
|
%% @doc Zone handler.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_zones).
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
-export([start_link/4, stop/1, setid/1, enter/2, leave/2, get_all_players/2, broadcast/3]). %% API.
|
||||||
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
||||||
|
|
||||||
|
-record(state, {
|
||||||
|
setid = 0 :: integer(),
|
||||||
|
objects = [] :: list(),
|
||||||
|
indexes = [] :: list(),
|
||||||
|
targets = [] :: list(),
|
||||||
|
players = [] :: list(),
|
||||||
|
freelids = [] :: list()
|
||||||
|
}).
|
||||||
|
|
||||||
|
%% API.
|
||||||
|
|
||||||
|
%% @spec start_link(UniID, QuestID, ZoneID, ZoneData) -> {ok,Pid::pid()}
|
||||||
|
start_link(UniID, QuestID, ZoneID, ZoneData) ->
|
||||||
|
gen_server:start_link(?MODULE, [UniID, QuestID, ZoneID, ZoneData], []).
|
||||||
|
|
||||||
|
%% @spec stop(Pid) -> stopped
|
||||||
|
stop(Pid) ->
|
||||||
|
gen_server:call(Pid, stop).
|
||||||
|
|
||||||
|
setid(Pid) ->
|
||||||
|
gen_server:call(Pid, setid).
|
||||||
|
|
||||||
|
enter(Pid, GID) ->
|
||||||
|
gen_server:call(Pid, {enter, GID}).
|
||||||
|
|
||||||
|
leave(undefined, _GID) ->
|
||||||
|
ok;
|
||||||
|
leave(Pid, GID) ->
|
||||||
|
gen_server:call(Pid, {leave, GID}).
|
||||||
|
|
||||||
|
get_all_players(Pid, ExcludeGID) ->
|
||||||
|
gen_server:call(Pid, {get_all_players, ExcludeGID}).
|
||||||
|
|
||||||
|
broadcast(Pid, FromGID, Packet) ->
|
||||||
|
gen_server:cast(Pid, {broadcast, FromGID, Packet}).
|
||||||
|
|
||||||
|
%% gen_server.
|
||||||
|
|
||||||
|
init([UniID, QuestID, ZoneID, ZoneData]) ->
|
||||||
|
SetID = rand_setid(proplists:get_value(sets, ZoneData, [100])),
|
||||||
|
Set = egs_quests_db:set(QuestID, ZoneID, SetID),
|
||||||
|
Objects = create_units(Set),
|
||||||
|
{Indexes, Targets} = index_objects(Objects),
|
||||||
|
FreeLIDs = lists:seq(0, 1023),
|
||||||
|
{ok, #state{setid=SetID, objects=Objects, indexes=Indexes, targets=Targets, freelids=FreeLIDs}}.
|
||||||
|
|
||||||
|
handle_call(setid, _From, State) ->
|
||||||
|
{reply, State#state.setid, State};
|
||||||
|
|
||||||
|
handle_call({enter, GID}, _From, State) ->
|
||||||
|
[LID|FreeLIDs] = State#state.freelids,
|
||||||
|
egs_users:set_zone(GID, self(), LID),
|
||||||
|
{ok, Spawn} = egs_users:read(GID),
|
||||||
|
egs_users:broadcast({egs, player_spawn, Spawn}, players_gid(State#state.players)),
|
||||||
|
{reply, LID, State#state{players=[{GID, LID}|State#state.players], freelids=FreeLIDs}};
|
||||||
|
|
||||||
|
handle_call({leave, GID}, _From, State) ->
|
||||||
|
{_, LID} = lists:keyfind(GID, 1, State#state.players),
|
||||||
|
Players = lists:delete({GID, LID}, State#state.players),
|
||||||
|
{ok, Spawn} = egs_users:read(GID),
|
||||||
|
egs_users:broadcast({egs, player_unspawn, Spawn}, players_gid(Players)),
|
||||||
|
{reply, ok, State#state{players=Players, freelids=[LID|State#state.freelids]}};
|
||||||
|
|
||||||
|
handle_call({get_all_players, ExcludeGID}, _From, State) ->
|
||||||
|
{reply, lists:delete(ExcludeGID, players_gid(State#state.players)), State};
|
||||||
|
|
||||||
|
handle_call(stop, _From, State) ->
|
||||||
|
{stop, normal, stopped, State};
|
||||||
|
|
||||||
|
handle_call(_Request, _From, State) ->
|
||||||
|
{reply, ignored, State}.
|
||||||
|
|
||||||
|
handle_cast({broadcast, FromGID, Packet}, State) ->
|
||||||
|
PlayersGID = lists:delete(FromGID, players_gid(State#state.players)),
|
||||||
|
egs_users:broadcast({egs, cast, Packet}, PlayersGID),
|
||||||
|
{noreply, State};
|
||||||
|
|
||||||
|
handle_cast(_Msg, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
handle_info(_Info, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
terminate(_Reason, _State) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
code_change(_OldVsn, State, _Extra) ->
|
||||||
|
{ok, State}.
|
||||||
|
|
||||||
|
%% Internal.
|
||||||
|
|
||||||
|
%% @doc Return a random setid from a list of chances per set.
|
||||||
|
rand_setid(Sets) ->
|
||||||
|
N = crypto:rand_uniform(1, lists:sum(Sets)),
|
||||||
|
rand_setid(N, Sets, 0).
|
||||||
|
rand_setid(N, [Set|_Tail], I) when N < Set ->
|
||||||
|
I;
|
||||||
|
rand_setid(N, [Set|Tail], I) ->
|
||||||
|
rand_setid(N - Set, Tail, I + 1).
|
||||||
|
|
||||||
|
%% @doc Create the objects for all units in a set.
|
||||||
|
create_units(Set) ->
|
||||||
|
create_units(Set, 0, []).
|
||||||
|
create_units([], _MapNb, Acc) ->
|
||||||
|
lists:flatten(lists:reverse(Acc));
|
||||||
|
create_units([{{map, _MapID}, Groups}|Tail], MapNb, Acc) ->
|
||||||
|
MapObjects = create_groups(Groups, MapNb),
|
||||||
|
create_units(Tail, MapNb + 1, [MapObjects|Acc]).
|
||||||
|
|
||||||
|
%% @doc Create the objects for all groups in a unit.
|
||||||
|
create_groups(Groups, MapNb) ->
|
||||||
|
create_groups(Groups, MapNb, 0, []).
|
||||||
|
create_groups([], _MapNb, _GroupNb, Acc) ->
|
||||||
|
lists:flatten(lists:reverse(Acc));
|
||||||
|
create_groups([Objects|Tail], MapNb, GroupNb, Acc) ->
|
||||||
|
GroupObjects = create_objects(Objects, MapNb, GroupNb),
|
||||||
|
create_groups(Tail, MapNb, GroupNb + 1, [GroupObjects|Acc]).
|
||||||
|
|
||||||
|
%% @doc Create the given objects.
|
||||||
|
create_objects(Objects, MapNb, GroupNb) ->
|
||||||
|
create_objects(Objects, MapNb, GroupNb, 0, []).
|
||||||
|
create_objects([], _MapNb, _GroupNb, _ObjectNb, Acc) ->
|
||||||
|
lists:reverse(Acc);
|
||||||
|
create_objects([{ObjType, ObjPos, ObjRot, ObjParams}|Tail], MapNb, GroupNb, ObjectNb, Acc) ->
|
||||||
|
Object = create_object(ObjType, ObjPos, ObjRot, ObjParams),
|
||||||
|
create_objects(Tail, MapNb, GroupNb, ObjectNb + 1, [{{MapNb, GroupNb, ObjectNb}, Object}|Acc]).
|
||||||
|
|
||||||
|
%% @doc Create the given object.
|
||||||
|
create_object(Type, Pos, Rot, Params) ->
|
||||||
|
M = list_to_existing_atom(lists:flatten(["egs_obj_", atom_to_list(Type)])),
|
||||||
|
M:init(Pos, Rot, Params).
|
||||||
|
|
||||||
|
%% @doc Build a list of object indexes and targets based on the list of objects.
|
||||||
|
index_objects(Objects) ->
|
||||||
|
index_objects(Objects, 0, [], 1024, []).
|
||||||
|
index_objects([], _Index, IndexesAcc, _Target, TargetsAcc) ->
|
||||||
|
{lists:reverse(IndexesAcc), lists:reverse(TargetsAcc)};
|
||||||
|
index_objects([{Key, Object}|Tail], Index, IndexesAcc, Target, TargetsAcc) ->
|
||||||
|
M = element(1, Object),
|
||||||
|
Attrs = M:module_info(attributes),
|
||||||
|
{Index2, IndexesAcc2} = case lists:keyfind(is_indexed, 1, Attrs) of
|
||||||
|
{_, [true]} -> {Index + 1, [{Index, Key}|IndexesAcc]};
|
||||||
|
{_, [false]} -> {Index, IndexesAcc}
|
||||||
|
end,
|
||||||
|
{Target2, TargetsAcc2} = case lists:keyfind(is_target, 1, Attrs) of
|
||||||
|
{_, [true]} -> {Target + 1, [{Target, Key}|TargetsAcc]};
|
||||||
|
{_, [false]} -> {Target, TargetsAcc}
|
||||||
|
end,
|
||||||
|
index_objects(Tail, Index2, IndexesAcc2, Target2, TargetsAcc2).
|
||||||
|
|
||||||
|
%% @doc Return a list of GID from a list of {GID, LID}.
|
||||||
|
players_gid(Players) ->
|
||||||
|
players_gid(Players, []).
|
||||||
|
players_gid([], Acc) ->
|
||||||
|
Acc;
|
||||||
|
players_gid([{GID, _LID}|Tail], Acc) ->
|
||||||
|
players_gid(Tail, [GID|Acc]).
|
47
apps/egs/src/egs_zones_sup.erl
Normal file
47
apps/egs/src/egs_zones_sup.erl
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2011 Loïc Hoguin.
|
||||||
|
%% @doc Supervisor for the egs_zones gen_server.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_zones_sup).
|
||||||
|
-behaviour(supervisor).
|
||||||
|
|
||||||
|
-export([start_link/0, start_zone/4]). %% API.
|
||||||
|
-export([init/1]). %% supervisor.
|
||||||
|
|
||||||
|
-define(SUPERVISOR, ?MODULE).
|
||||||
|
|
||||||
|
%% API.
|
||||||
|
|
||||||
|
-spec start_link() -> {ok, pid()}.
|
||||||
|
start_link() ->
|
||||||
|
supervisor:start_link({local, ?SUPERVISOR}, ?MODULE, []).
|
||||||
|
|
||||||
|
-spec start_zone(egs:uniid(), egs:questid(), egs:zoneid(), tuple())
|
||||||
|
-> {ok, pid()}.
|
||||||
|
start_zone(UniID, QuestID, ZoneID, ZoneData) ->
|
||||||
|
supervisor:start_child(?SUPERVISOR, [UniID, QuestID, ZoneID, ZoneData]).
|
||||||
|
|
||||||
|
%% supervisor.
|
||||||
|
|
||||||
|
-spec init([]) -> {ok, {{simple_one_for_one, 0, 1}, [{egs_zones,
|
||||||
|
{egs_zones, start_link, []}, temporary, brutal_kill,
|
||||||
|
worker, [egs_zones]}]}}.
|
||||||
|
init([]) ->
|
||||||
|
{ok, {{simple_one_for_one, 0, 1}, [{egs_zones,
|
||||||
|
{egs_zones, start_link, []}, temporary, brutal_kill,
|
||||||
|
worker, [egs_zones]}]}}.
|
33
apps/egs/src/objects/egs_obj_chair.erl
Normal file
33
apps/egs/src/objects/egs_obj_chair.erl
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2011 Loïc Hoguin.
|
||||||
|
%% @doc Chair object.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_obj_chair).
|
||||||
|
-export([init/3]).
|
||||||
|
|
||||||
|
%% @todo Real values.
|
||||||
|
-is_indexed(true).
|
||||||
|
-is_target(true).
|
||||||
|
|
||||||
|
-record(egs_obj_chair, {
|
||||||
|
pos :: {X :: float(), Y :: float(), Z :: float()},
|
||||||
|
rot :: {Rx :: float(), Ry :: float(), Zy :: float()}
|
||||||
|
}).
|
||||||
|
|
||||||
|
init(Pos, Rot, Params) ->
|
||||||
|
#egs_obj_chair{pos=Pos, rot=Rot}.
|
33
apps/egs/src/objects/egs_obj_door.erl
Normal file
33
apps/egs/src/objects/egs_obj_door.erl
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2011 Loïc Hoguin.
|
||||||
|
%% @doc Door object.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_obj_door).
|
||||||
|
-export([init/3]).
|
||||||
|
|
||||||
|
%% @todo Real values.
|
||||||
|
-is_indexed(true).
|
||||||
|
-is_target(false).
|
||||||
|
|
||||||
|
-record(egs_obj_door, {
|
||||||
|
pos :: {X :: float(), Y :: float(), Z :: float()},
|
||||||
|
rot :: {Rx :: float(), Ry :: float(), Zy :: float()}
|
||||||
|
}).
|
||||||
|
|
||||||
|
init(Pos, Rot, Params) ->
|
||||||
|
#egs_obj_door{pos=Pos, rot=Rot}.
|
33
apps/egs/src/objects/egs_obj_entrance.erl
Normal file
33
apps/egs/src/objects/egs_obj_entrance.erl
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2011 Loïc Hoguin.
|
||||||
|
%% @doc Entrance object.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_obj_entrance).
|
||||||
|
-export([init/3]).
|
||||||
|
|
||||||
|
%% @todo Real values.
|
||||||
|
-is_indexed(false).
|
||||||
|
-is_target(false).
|
||||||
|
|
||||||
|
-record(egs_obj_entrance, {
|
||||||
|
pos :: {X :: float(), Y :: float(), Z :: float()},
|
||||||
|
rot :: {Rx :: float(), Ry :: float(), Zy :: float()}
|
||||||
|
}).
|
||||||
|
|
||||||
|
init(Pos, Rot, Params) ->
|
||||||
|
#egs_obj_entrance{pos=Pos, rot=Rot}.
|
33
apps/egs/src/objects/egs_obj_exit.erl
Normal file
33
apps/egs/src/objects/egs_obj_exit.erl
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2011 Loïc Hoguin.
|
||||||
|
%% @doc Exit object.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_obj_exit).
|
||||||
|
-export([init/3]).
|
||||||
|
|
||||||
|
%% @todo Real values.
|
||||||
|
-is_indexed(false).
|
||||||
|
-is_target(true).
|
||||||
|
|
||||||
|
-record(egs_obj_exit, {
|
||||||
|
pos :: {X :: float(), Y :: float(), Z :: float()},
|
||||||
|
rot :: {Rx :: float(), Ry :: float(), Zy :: float()}
|
||||||
|
}).
|
||||||
|
|
||||||
|
init(Pos, Rot, Params) ->
|
||||||
|
#egs_obj_exit{pos=Pos, rot=Rot}.
|
33
apps/egs/src/objects/egs_obj_invisible_block.erl
Normal file
33
apps/egs/src/objects/egs_obj_invisible_block.erl
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2011 Loïc Hoguin.
|
||||||
|
%% @doc Invisible block object.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_obj_invisible_block).
|
||||||
|
-export([init/3]).
|
||||||
|
|
||||||
|
%% @todo Real values.
|
||||||
|
-is_indexed(false).
|
||||||
|
-is_target(false).
|
||||||
|
|
||||||
|
-record(egs_obj_invisible_block, {
|
||||||
|
pos :: {X :: float(), Y :: float(), Z :: float()},
|
||||||
|
rot :: {Rx :: float(), Ry :: float(), Zy :: float()}
|
||||||
|
}).
|
||||||
|
|
||||||
|
init(Pos, Rot, Params) ->
|
||||||
|
#egs_obj_invisible_block{pos=Pos, rot=Rot}.
|
33
apps/egs/src/objects/egs_obj_label.erl
Normal file
33
apps/egs/src/objects/egs_obj_label.erl
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2011 Loïc Hoguin.
|
||||||
|
%% @doc Label object.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_obj_label).
|
||||||
|
-export([init/3]).
|
||||||
|
|
||||||
|
%% @todo Real values.
|
||||||
|
-is_indexed(false).
|
||||||
|
-is_target(false).
|
||||||
|
|
||||||
|
-record(egs_obj_label, {
|
||||||
|
pos :: {X :: float(), Y :: float(), Z :: float()},
|
||||||
|
rot :: {Rx :: float(), Ry :: float(), Zy :: float()}
|
||||||
|
}).
|
||||||
|
|
||||||
|
init(Pos, Rot, Params) ->
|
||||||
|
#egs_obj_label{pos=Pos, rot=Rot}.
|
33
apps/egs/src/objects/egs_obj_npc.erl
Normal file
33
apps/egs/src/objects/egs_obj_npc.erl
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2011 Loïc Hoguin.
|
||||||
|
%% @doc NPC object.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_obj_npc).
|
||||||
|
-export([init/3]).
|
||||||
|
|
||||||
|
%% @todo Real values.
|
||||||
|
-is_indexed(true).
|
||||||
|
-is_target(true).
|
||||||
|
|
||||||
|
-record(egs_obj_npc, {
|
||||||
|
pos :: {X :: float(), Y :: float(), Z :: float()},
|
||||||
|
rot :: {Rx :: float(), Ry :: float(), Zy :: float()}
|
||||||
|
}).
|
||||||
|
|
||||||
|
init(Pos, Rot, Params) ->
|
||||||
|
#egs_obj_npc{pos=Pos, rot=Rot}.
|
33
apps/egs/src/objects/egs_obj_pp_cube.erl
Normal file
33
apps/egs/src/objects/egs_obj_pp_cube.erl
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2011 Loïc Hoguin.
|
||||||
|
%% @doc PP cube object.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_obj_pp_cube).
|
||||||
|
-export([init/3]).
|
||||||
|
|
||||||
|
%% @todo Real values.
|
||||||
|
-is_indexed(false).
|
||||||
|
-is_target(false).
|
||||||
|
|
||||||
|
-record(egs_obj_pp_cube, {
|
||||||
|
pos :: {X :: float(), Y :: float(), Z :: float()},
|
||||||
|
rot :: {Rx :: float(), Ry :: float(), Zy :: float()}
|
||||||
|
}).
|
||||||
|
|
||||||
|
init(Pos, Rot, Params) ->
|
||||||
|
#egs_obj_pp_cube{pos=Pos, rot=Rot}.
|
33
apps/egs/src/objects/egs_obj_sensor.erl
Normal file
33
apps/egs/src/objects/egs_obj_sensor.erl
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2011 Loïc Hoguin.
|
||||||
|
%% @doc Sensor object.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_obj_sensor).
|
||||||
|
-export([init/3]).
|
||||||
|
|
||||||
|
%% @todo Real values.
|
||||||
|
-is_indexed(true).
|
||||||
|
-is_target(true).
|
||||||
|
|
||||||
|
-record(egs_obj_sensor, {
|
||||||
|
pos :: {X :: float(), Y :: float(), Z :: float()},
|
||||||
|
rot :: {Rx :: float(), Ry :: float(), Zy :: float()}
|
||||||
|
}).
|
||||||
|
|
||||||
|
init(Pos, Rot, Params) ->
|
||||||
|
#egs_obj_sensor{pos=Pos, rot=Rot}.
|
33
apps/egs/src/objects/egs_obj_static_model.erl
Normal file
33
apps/egs/src/objects/egs_obj_static_model.erl
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2011 Loïc Hoguin.
|
||||||
|
%% @doc Static model object.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_obj_static_model).
|
||||||
|
-export([init/3]).
|
||||||
|
|
||||||
|
%% @todo Real values.
|
||||||
|
-is_indexed(false).
|
||||||
|
-is_target(false).
|
||||||
|
|
||||||
|
-record(egs_obj_static_model, {
|
||||||
|
pos :: {X :: float(), Y :: float(), Z :: float()},
|
||||||
|
rot :: {Rx :: float(), Ry :: float(), Zy :: float()}
|
||||||
|
}).
|
||||||
|
|
||||||
|
init(Pos, Rot, Params) ->
|
||||||
|
#egs_obj_static_model{pos=Pos, rot=Rot}.
|
33
apps/egs/src/objects/egs_obj_type_counter.erl
Normal file
33
apps/egs/src/objects/egs_obj_type_counter.erl
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2011 Loïc Hoguin.
|
||||||
|
%% @doc Type counter object.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_obj_type_counter).
|
||||||
|
-export([init/3]).
|
||||||
|
|
||||||
|
%% @todo Real values.
|
||||||
|
-is_indexed(true).
|
||||||
|
-is_target(true).
|
||||||
|
|
||||||
|
-record(egs_obj_type_counter, {
|
||||||
|
pos :: {X :: float(), Y :: float(), Z :: float()},
|
||||||
|
rot :: {Rx :: float(), Ry :: float(), Zy :: float()}
|
||||||
|
}).
|
||||||
|
|
||||||
|
init(Pos, Rot, Params) ->
|
||||||
|
#egs_obj_type_counter{pos=Pos, rot=Rot}.
|
33
apps/egs/src/objects/egs_obj_uni_cube.erl
Normal file
33
apps/egs/src/objects/egs_obj_uni_cube.erl
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2011 Loïc Hoguin.
|
||||||
|
%% @doc Uni cube object.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_obj_uni_cube).
|
||||||
|
-export([init/3]).
|
||||||
|
|
||||||
|
%% @todo Real values.
|
||||||
|
-is_indexed(true).
|
||||||
|
-is_target(true).
|
||||||
|
|
||||||
|
-record(egs_obj_uni_cube, {
|
||||||
|
pos :: {X :: float(), Y :: float(), Z :: float()},
|
||||||
|
rot :: {Rx :: float(), Ry :: float(), Zy :: float()}
|
||||||
|
}).
|
||||||
|
|
||||||
|
init(Pos, Rot, Params) ->
|
||||||
|
#egs_obj_uni_cube{pos=Pos, rot=Rot}.
|
96
apps/egs/src/psu/psu_characters.erl
Normal file
96
apps/egs/src/psu/psu_characters.erl
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
|
%% @doc General character functions.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(psu_characters).
|
||||||
|
-export([
|
||||||
|
character_user_to_binary/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
-include("include/records.hrl").
|
||||||
|
|
||||||
|
%% @doc Convert a character tuple into a binary to be sent to clients.
|
||||||
|
%% Only contains the actually saved data, not the stats and related information.
|
||||||
|
%% @todo The name isn't very good anymore now that I switched #characters to #users.
|
||||||
|
character_tuple_to_binary(Tuple) ->
|
||||||
|
#users{name=Name, race=Race, gender=Gender, class=Class, appearance=Appearance} = Tuple,
|
||||||
|
RaceBin = race_atom_to_binary(Race),
|
||||||
|
GenderBin = gender_atom_to_binary(Gender),
|
||||||
|
ClassBin = class_atom_to_binary(Class),
|
||||||
|
AppearanceBin = egs_net:character_appearance_to_binary(Race, Appearance),
|
||||||
|
LevelsBin = egs_proto:build_char_level(Tuple),
|
||||||
|
<< Name/binary, RaceBin:8, GenderBin:8, ClassBin:8, AppearanceBin/binary, LevelsBin/binary >>.
|
||||||
|
|
||||||
|
%% @doc Convert a character tuple into a binary to be sent to clients.
|
||||||
|
%% Contains everything from character_tuple_to_binary/1 along with location, stats, SE and more.
|
||||||
|
%% @todo The second StatsBin seems unused. Not sure what it's for.
|
||||||
|
%% @todo Find out what the big block of 0 is at the end.
|
||||||
|
%% @todo The value before IntDir seems to be the player's current animation. 01 stand up, 08 ?, 17 normal sit
|
||||||
|
|
||||||
|
character_user_to_binary(User) ->
|
||||||
|
#users{gid=CharGID, lid=CharLID, npcid=NPCid, type=Type, level=Level, stats=Stats, currenthp=CurrentHP, maxhp=MaxHP,
|
||||||
|
pos={X, Y, Z, Dir}, area={QuestID, ZoneID, MapID}, entryid=EntryID, prev_area={PrevQuestID, PrevZoneID, PrevMapID}, prev_entryid=PrevEntryID} = User,
|
||||||
|
CharBin = character_tuple_to_binary(User),
|
||||||
|
StatsBin = stats_tuple_to_binary(Stats),
|
||||||
|
EXPNextLevel = 100,
|
||||||
|
EXPCurrentLevel = 0,
|
||||||
|
IntDir = trunc(Dir * 182.0416),
|
||||||
|
TypeID = case Type of npc -> 16#00001d00; _ -> 16#00001200 end,
|
||||||
|
NPCStuff = case Type of npc -> 16#01ff; _ -> 16#0000 end,
|
||||||
|
<< TypeID:32, CharGID:32/little, 0:64, CharLID:16/little, 0:16, NPCStuff:16, NPCid:16/little, QuestID:32/little,
|
||||||
|
ZoneID:32/little, MapID:32/little, EntryID:16/little, 0:16,
|
||||||
|
16#0100:16, IntDir:16/little, X:32/little-float, Y:32/little-float, Z:32/little-float, 0:64,
|
||||||
|
PrevQuestID:32/little, PrevZoneID:32/little, PrevMapID:32/little, PrevEntryID:32/little,
|
||||||
|
CharBin/binary, EXPNextLevel:32/little, EXPCurrentLevel:32/little, MaxHP:32/little,
|
||||||
|
StatsBin/binary, 0:96, Level:32/little, StatsBin/binary, CurrentHP:32/little, MaxHP:32/little,
|
||||||
|
0:1344, 16#0000803f:32, 0:64, 16#0000803f:32, 0:64, 16#0000803f:32, 0:64, 16#0000803f:32, 0:64, 16#0000803f:32, 0:160, 16#0000803f:32, 0:352 >>.
|
||||||
|
|
||||||
|
%% @doc Convert a class atom into a binary to be sent to clients.
|
||||||
|
|
||||||
|
class_atom_to_binary(Class) ->
|
||||||
|
case Class of
|
||||||
|
hunter -> 12;
|
||||||
|
ranger -> 13;
|
||||||
|
force -> 14;
|
||||||
|
acro -> 15
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc Convert a gender atom into a binary to be sent to clients.
|
||||||
|
|
||||||
|
gender_atom_to_binary(Gender) ->
|
||||||
|
case Gender of
|
||||||
|
male -> 0;
|
||||||
|
female -> 1
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc Convert a race atom into a binary to be sent to clients.
|
||||||
|
|
||||||
|
race_atom_to_binary(Race) ->
|
||||||
|
case Race of
|
||||||
|
human -> 0;
|
||||||
|
newman -> 1;
|
||||||
|
cast -> 2;
|
||||||
|
beast -> 3
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc Convert the tuple of stats data into a binary to be sent to clients.
|
||||||
|
|
||||||
|
stats_tuple_to_binary(Tuple) ->
|
||||||
|
{stats, ATP, ATA, TP, DFP, EVP, MST, STA} = Tuple,
|
||||||
|
<< ATP:16/little, DFP:16/little, ATA:16/little, EVP:16/little,
|
||||||
|
STA:16/little, 0:16, TP:16/little, MST:16/little >>.
|
@ -1,5 +1,5 @@
|
|||||||
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
%% @copyright 2010 Loïc Hoguin.
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
%% @doc Party gen_server.
|
%% @doc Party gen_server.
|
||||||
%%
|
%%
|
||||||
%% This file is part of EGS.
|
%% This file is part of EGS.
|
4
apps/egs_net/rebar.config
Normal file
4
apps/egs_net/rebar.config
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{deps, [
|
||||||
|
{erlson, ".*", {git, "https://github.com/alavrik/erlson.git", "HEAD"}}
|
||||||
|
]}.
|
||||||
|
{plugins, [erlson_rebar_plugin]}.
|
12
apps/egs_net/src/egs_net.app.src
Normal file
12
apps/egs_net/src/egs_net.app.src
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
%%-*- mode: erlang -*-
|
||||||
|
{application, egs_net, [
|
||||||
|
{description, "EGS network layer."},
|
||||||
|
{vsn, "0.1.0"},
|
||||||
|
{modules, []},
|
||||||
|
{registered, []},
|
||||||
|
{applications, [
|
||||||
|
kernel,
|
||||||
|
stdlib,
|
||||||
|
cowboy
|
||||||
|
]}
|
||||||
|
]}.
|
1576
apps/egs_net/src/egs_net.erl
Normal file
1576
apps/egs_net/src/egs_net.erl
Normal file
File diff suppressed because it is too large
Load Diff
BIN
apps/egs_patch/priv/files/DATA/1ffd0db3e0b54048caff394e9c09eda8
Normal file
BIN
apps/egs_patch/priv/files/DATA/1ffd0db3e0b54048caff394e9c09eda8
Normal file
Binary file not shown.
BIN
apps/egs_patch/priv/files/DATA/bb04cc8e1727288bd2a336d60040eff1
Normal file
BIN
apps/egs_patch/priv/files/DATA/bb04cc8e1727288bd2a336d60040eff1
Normal file
Binary file not shown.
27
apps/egs_patch/priv/patch.conf
Normal file
27
apps/egs_patch/priv/patch.conf
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
%% @doc List of folders where patched files are located.
|
||||||
|
{folders, [root, "DATA"]}.
|
||||||
|
|
||||||
|
%% @doc Files in the root folder.
|
||||||
|
{{folder, root}, [
|
||||||
|
]}.
|
||||||
|
|
||||||
|
%% @doc Files in the DATA folder.
|
||||||
|
{{folder, "DATA"}, [
|
||||||
|
"1ffd0db3e0b54048caff394e9c09eda8", %% crc.bin
|
||||||
|
"bb04cc8e1727288bd2a336d60040eff1" %% SmutFilter_J.bin
|
||||||
|
]}.
|
3
apps/egs_patch/rebar.config
Normal file
3
apps/egs_patch/rebar.config
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{deps, [
|
||||||
|
{cowboy, ".*", {git, "git://github.com/extend/cowboy.git", "HEAD"}}
|
||||||
|
]}.
|
14
apps/egs_patch/src/egs_patch.app.src
Normal file
14
apps/egs_patch/src/egs_patch.app.src
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
%%-*- mode: erlang -*-
|
||||||
|
{application, egs_patch, [
|
||||||
|
{description, "EGS patch server"},
|
||||||
|
{vsn, "0.1.0"},
|
||||||
|
{modules, []},
|
||||||
|
{registered, []},
|
||||||
|
{applications, [
|
||||||
|
kernel,
|
||||||
|
stdlib,
|
||||||
|
cowboy
|
||||||
|
]},
|
||||||
|
{mod, {egs_patch_app, []}},
|
||||||
|
{env, []}
|
||||||
|
]}.
|
47
apps/egs_patch/src/egs_patch_app.erl
Normal file
47
apps/egs_patch/src/egs_patch_app.erl
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
%% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_patch_app).
|
||||||
|
|
||||||
|
-behaviour(application).
|
||||||
|
-export([start/2, stop/1]). %% API.
|
||||||
|
|
||||||
|
-type application_start_type()
|
||||||
|
:: normal | {takeover, node()} | {failover, node()}.
|
||||||
|
|
||||||
|
%% API.
|
||||||
|
|
||||||
|
-spec start(application_start_type(), term()) -> {ok, pid()}.
|
||||||
|
start(_Type, _StartArgs) ->
|
||||||
|
{ok, PatchPorts} = application:get_env(patch_ports),
|
||||||
|
start_listeners(PatchPorts),
|
||||||
|
egs_patch_sup:start_link().
|
||||||
|
|
||||||
|
-spec stop(term()) -> ok.
|
||||||
|
stop(_State) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% Internal.
|
||||||
|
|
||||||
|
-spec start_listeners([inet:ip_port()]) -> ok.
|
||||||
|
start_listeners([]) ->
|
||||||
|
ok;
|
||||||
|
start_listeners([Port|Tail]) ->
|
||||||
|
{ok, _Pid} = cowboy:start_listener({patch, Port}, 10,
|
||||||
|
cowboy_tcp_transport, [{port, Port}],
|
||||||
|
egs_patch_protocol, []),
|
||||||
|
start_listeners(Tail).
|
175
apps/egs_patch/src/egs_patch_files_db.erl
Normal file
175
apps/egs_patch/src/egs_patch_files_db.erl
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
|
%% @doc EGS patch files database and cache manager.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_patch_files_db).
|
||||||
|
-behavior(gen_server).
|
||||||
|
|
||||||
|
-export([start_link/0, stop/0, list/0, check/3, get_size/1, get_info/1, reload/0]). %% API.
|
||||||
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
||||||
|
|
||||||
|
%% Use the module name for the server's name.
|
||||||
|
-define(SERVER, ?MODULE).
|
||||||
|
|
||||||
|
-include_lib("kernel/include/file.hrl").
|
||||||
|
|
||||||
|
-record(state, {list_bin=[], files=[]}).
|
||||||
|
-record(file, {crc, size, folder, filename_bin, full_filename}).
|
||||||
|
|
||||||
|
%% API.
|
||||||
|
|
||||||
|
%% @spec start_link() -> {ok,Pid::pid()}
|
||||||
|
start_link() ->
|
||||||
|
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||||||
|
|
||||||
|
%% @spec stop() -> stopped
|
||||||
|
stop() ->
|
||||||
|
gen_server:call(?SERVER, stop).
|
||||||
|
|
||||||
|
%% @spec list() -> binary()
|
||||||
|
list() ->
|
||||||
|
gen_server:call(?SERVER, list).
|
||||||
|
|
||||||
|
%% @spec check(FileNumber, CRC, Size) -> ok | invalid
|
||||||
|
check(FileNumber, CRC, Size) ->
|
||||||
|
gen_server:call(?SERVER, {check, FileNumber, CRC, Size}).
|
||||||
|
|
||||||
|
%% @spec get_size(FileNumber) -> Size
|
||||||
|
get_size(FileNumber) ->
|
||||||
|
gen_server:call(?SERVER, {get_size, FileNumber}).
|
||||||
|
|
||||||
|
%% @spec get_info(FileNumber) -> {CRC, Size, FilenameBin, FullFilename}
|
||||||
|
get_info(FileNumber) ->
|
||||||
|
gen_server:call(?SERVER, {get_info, FileNumber}).
|
||||||
|
|
||||||
|
%% @spec reload() -> ok
|
||||||
|
reload() ->
|
||||||
|
gen_server:cast(?SERVER, reload).
|
||||||
|
|
||||||
|
%% gen_server.
|
||||||
|
|
||||||
|
init([]) ->
|
||||||
|
{ok, build_state()}.
|
||||||
|
|
||||||
|
handle_call(list, _From, State=#state{list_bin=Bin}) ->
|
||||||
|
{reply, Bin, State};
|
||||||
|
|
||||||
|
handle_call({check, FileNumber, CRC, Size}, _From, State=#state{files=Files}) ->
|
||||||
|
File = proplists:get_value(FileNumber, Files),
|
||||||
|
case File of
|
||||||
|
#file{crc=CRC, size=Size} -> {reply, ok, State};
|
||||||
|
_Any -> {reply, invalid, State}
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_call({get_size, FileNumber}, _From, State=#state{files=Files}) ->
|
||||||
|
File = proplists:get_value(FileNumber, Files),
|
||||||
|
{reply, File#file.size, State};
|
||||||
|
|
||||||
|
handle_call({get_info, FileNumber}, _From, State=#state{files=Files}) ->
|
||||||
|
{reply, proplists:get_value(FileNumber, Files), State};
|
||||||
|
|
||||||
|
handle_call(stop, _From, State) ->
|
||||||
|
{stop, normal, stopped, State};
|
||||||
|
|
||||||
|
handle_call(_Request, _From, State) ->
|
||||||
|
{reply, ignored, State}.
|
||||||
|
|
||||||
|
handle_cast(reload, _State) ->
|
||||||
|
{noreply, build_state()};
|
||||||
|
|
||||||
|
handle_cast(_Msg, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
handle_info(_Info, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
terminate(_Reason, _State) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
code_change(_OldVsn, State, _Extra) ->
|
||||||
|
{ok, State}.
|
||||||
|
|
||||||
|
%% Internal.
|
||||||
|
|
||||||
|
build_state() ->
|
||||||
|
{ok, App} = application:get_application(),
|
||||||
|
PrivDir = code:priv_dir(App),
|
||||||
|
{ok, Terms} = file:consult([PrivDir, "/patch.conf"]),
|
||||||
|
Folders = proplists:get_value(folders, Terms),
|
||||||
|
{ListBin, Files} = build_list_bin(Folders, Terms, [PrivDir, "/files/"]),
|
||||||
|
#state{list_bin=ListBin, files=Files}.
|
||||||
|
|
||||||
|
%% The file number must start at 0.
|
||||||
|
build_list_bin(Folders, Terms, PatchDir) ->
|
||||||
|
build_list_bin(Folders, Terms, PatchDir, 0, [], []).
|
||||||
|
build_list_bin([], _Terms, _PatchDir, _N, Acc, FilesAcc) ->
|
||||||
|
Bin = list_to_binary(lists:reverse(Acc)),
|
||||||
|
Bin2 = << 16#08:32/little, 16#06:32/little, Bin/binary, 16#08:32/little, 16#08:32/little >>,
|
||||||
|
{Bin2, lists:flatten(FilesAcc)};
|
||||||
|
build_list_bin([Folder|Tail], Terms, PatchDir, N, Acc, FilesAcc) ->
|
||||||
|
Filenames = proplists:get_value({folder, Folder}, Terms),
|
||||||
|
{BinFiles, Files, N2} = build_files_bin(Folder, Filenames, PatchDir, N),
|
||||||
|
BinFiles2 = case Folder of
|
||||||
|
root -> BinFiles;
|
||||||
|
_Any ->
|
||||||
|
FolderBin = list_to_binary(Folder),
|
||||||
|
Padding = 8 * (64 - length(Folder)),
|
||||||
|
<< 16#48:32/little, 16#09:32/little, FolderBin/binary, 0:Padding,
|
||||||
|
BinFiles/binary, 16#08:32/little, 16#0a:32/little >>
|
||||||
|
end,
|
||||||
|
build_list_bin(Tail, Terms, PatchDir, N2, [BinFiles2|Acc], [Files|FilesAcc]).
|
||||||
|
|
||||||
|
build_files_bin(Folder, Filenames, PatchDir, N) ->
|
||||||
|
build_files_bin(Folder, Filenames, PatchDir, N, [], []).
|
||||||
|
build_files_bin(_Folder, [], _PatchDir, N, Acc, FilesAcc) ->
|
||||||
|
Bin = list_to_binary(lists:reverse(Acc)),
|
||||||
|
{Bin, FilesAcc, N};
|
||||||
|
build_files_bin(Folder, [Filename|Tail], PatchDir, N, Acc, FilesAcc) ->
|
||||||
|
FullFilename = case Folder of
|
||||||
|
root -> [PatchDir|Filename];
|
||||||
|
_Any -> [PatchDir,Folder,"/"|Filename]
|
||||||
|
end,
|
||||||
|
Size = file_get_size(FullFilename),
|
||||||
|
CRC = file_get_crc(FullFilename),
|
||||||
|
FilenameBin = list_to_binary(Filename),
|
||||||
|
Padding = 8 * (64 - length(Filename)),
|
||||||
|
FilenameBin2 = << FilenameBin/binary, 0:Padding >>,
|
||||||
|
Bin = << 16#4c:32/little, 16#07:32/little, N:32/little, FilenameBin2/binary >>,
|
||||||
|
build_files_bin(Folder, Tail, PatchDir, N + 1, [Bin|Acc], [{N, #file{crc=CRC, size=Size, folder=Folder, filename_bin=FilenameBin2, full_filename=FullFilename}}|FilesAcc]).
|
||||||
|
|
||||||
|
file_get_size(Filename) ->
|
||||||
|
{ok, FileInfo} = file:read_file_info(Filename),
|
||||||
|
FileInfo#file_info.size.
|
||||||
|
|
||||||
|
file_get_crc(Filename) ->
|
||||||
|
{ok, IoDevice} = file:open(Filename, [read, raw, binary]),
|
||||||
|
case file:read(IoDevice, 524288) of
|
||||||
|
eof -> 0;
|
||||||
|
{ok, Data} ->
|
||||||
|
CRC = erlang:crc32(Data),
|
||||||
|
file_get_crc(IoDevice, CRC)
|
||||||
|
end.
|
||||||
|
file_get_crc(IoDevice, CRC) ->
|
||||||
|
case file:read(IoDevice, 524288) of
|
||||||
|
{ok, Data} ->
|
||||||
|
CRC2 = erlang:crc32(CRC, Data),
|
||||||
|
file_get_crc(IoDevice, CRC2);
|
||||||
|
eof ->
|
||||||
|
file:close(IoDevice),
|
||||||
|
CRC
|
||||||
|
end.
|
218
apps/egs_patch/src/egs_patch_protocol.erl
Normal file
218
apps/egs_patch/src/egs_patch_protocol.erl
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
%% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
%% @doc Cowboy protocol module for the patch server.
|
||||||
|
-module(egs_patch_protocol).
|
||||||
|
|
||||||
|
-export([start_link/4, init/2]).
|
||||||
|
|
||||||
|
%% @todo Move that in a configuration file.
|
||||||
|
-define(TIMEOUT, 5000).
|
||||||
|
|
||||||
|
-record(state, {
|
||||||
|
socket :: inet:socket(),
|
||||||
|
transport :: module(),
|
||||||
|
buffer = <<>> :: binary(),
|
||||||
|
files = [] :: list(integer())
|
||||||
|
}).
|
||||||
|
|
||||||
|
-type state() :: #state{}.
|
||||||
|
-type cmd() :: 0..16#14.
|
||||||
|
-type cmd_size() :: 0..16#ffffffff.
|
||||||
|
|
||||||
|
-spec start_link(pid(), inet:socket(), module(), []) -> {ok, pid()}.
|
||||||
|
start_link(_ListenerPid, Socket, Transport, []) ->
|
||||||
|
Pid = spawn_link(?MODULE, init, [Socket, Transport]),
|
||||||
|
{ok, Pid}.
|
||||||
|
|
||||||
|
-spec init(inet:socket(), module()) -> ok | closed.
|
||||||
|
init(Socket, Transport) ->
|
||||||
|
State = #state{socket=Socket, transport=Transport},
|
||||||
|
send_01(State),
|
||||||
|
wait_hello(State).
|
||||||
|
|
||||||
|
-spec next(state()) -> {ok, cmd(), cmd_size(), binary(), state()} | closed.
|
||||||
|
next(State=#state{buffer= << Size:32/little, Cmd:16/little, _:16, Rest/bits >>})
|
||||||
|
when byte_size(Rest) + 8 >= Size ->
|
||||||
|
Size2 = Size - 8,
|
||||||
|
<< Data:Size2/binary, Buffer/bits >> = Rest,
|
||||||
|
{ok, Cmd, Size, Data, State#state{buffer=Buffer}};
|
||||||
|
next(State=#state{socket=Socket, transport=Transport, buffer=Buffer}) ->
|
||||||
|
Transport:setopts(Socket, [{active, once}]),
|
||||||
|
{OK, Closed, Error} = Transport:messages(),
|
||||||
|
receive
|
||||||
|
{OK, Socket, Data} -> next(State#state{
|
||||||
|
buffer= << Buffer/binary, Data/binary >>});
|
||||||
|
{Closed, Socket} -> closed;
|
||||||
|
{Error, Socket, _Reason} -> closed
|
||||||
|
after ?TIMEOUT ->
|
||||||
|
closed
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec wait_hello(state()) -> ok | closed.
|
||||||
|
wait_hello(State) ->
|
||||||
|
case next(State) of
|
||||||
|
{ok, 16#14, 52, Data, State2} -> handle_hello(State2, Data);
|
||||||
|
closed -> closed
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec handle_hello(state(), binary()) -> ok | closed.
|
||||||
|
handle_hello(State=#state{socket=Socket, transport=Transport}, Data) ->
|
||||||
|
<< 16#e44c0915:32, UnknownA:32/little,
|
||||||
|
UnknownB:32/little, UnknownC:32/little, UnknownD:32/little,
|
||||||
|
_GameVersion:32/little, UnknownE:32/little, 0:128 >> = Data,
|
||||||
|
io:format("patch hello: ~p ~p ~p ~p ~p~n",
|
||||||
|
[UnknownA, UnknownB, UnknownC, UnknownD, UnknownE]),
|
||||||
|
ListBin = egs_patch_files_db:list(),
|
||||||
|
Transport:send(Socket, ListBin),
|
||||||
|
wait_fileinfo_begin(State).
|
||||||
|
|
||||||
|
-spec wait_fileinfo_begin(state()) -> ok | closed.
|
||||||
|
wait_fileinfo_begin(State) ->
|
||||||
|
case next(State) of
|
||||||
|
{ok, 16#0c, 8, <<>>, State2} -> wait_fileinfo(State2);
|
||||||
|
closed -> closed
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec wait_fileinfo(state()) -> ok | closed.
|
||||||
|
wait_fileinfo(State) ->
|
||||||
|
case next(State) of
|
||||||
|
{ok, 16#0d, 20, Data, State2} -> handle_fileinfo(State2, Data);
|
||||||
|
{ok, 16#0e, 8, <<>>, State2} -> handle_fileinfo_end(State2);
|
||||||
|
closed -> closed
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec handle_fileinfo(state(), binary()) -> ok | closed.
|
||||||
|
handle_fileinfo(State=#state{files=Files}, Data) ->
|
||||||
|
<< FileNumber:32/little, CRC:32/little, Size:32/little >> = Data,
|
||||||
|
case egs_patch_files_db:check(FileNumber, CRC, Size) of
|
||||||
|
ok -> wait_fileinfo(State);
|
||||||
|
invalid -> wait_fileinfo(State#state{files=[FileNumber|Files]})
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec handle_fileinfo_end(state()) -> ok.
|
||||||
|
handle_fileinfo_end(State=#state{files=[]}) ->
|
||||||
|
handle_update_complete(State);
|
||||||
|
handle_fileinfo_end(State=#state{files=Files}) ->
|
||||||
|
Files2 = lists:reverse(Files),
|
||||||
|
State2 = State#state{files=Files2},
|
||||||
|
send_0f(State2),
|
||||||
|
handle_update(State2, root, Files2).
|
||||||
|
|
||||||
|
-spec handle_update(state(), root | string(), list(integer())) -> ok.
|
||||||
|
handle_update(State, _CurrentDir, []) ->
|
||||||
|
handle_update_complete(State);
|
||||||
|
handle_update(State=#state{}, CurrentDir, [FileNumber|Tail]) ->
|
||||||
|
{file, _CRC, Size, Dir, FilenameBin, FullFilename}
|
||||||
|
= egs_patch_files_db:get_info(FileNumber),
|
||||||
|
change_directory(State, CurrentDir, Dir),
|
||||||
|
send_10(State, Size, FilenameBin),
|
||||||
|
sendfile(State, FullFilename),
|
||||||
|
send_12(State),
|
||||||
|
handle_update(State, Dir, Tail).
|
||||||
|
|
||||||
|
-spec change_directory(state(), root | string(), root | string()) -> ok.
|
||||||
|
change_directory(_State, CurrentDir, CurrentDir) ->
|
||||||
|
ok;
|
||||||
|
change_directory(State, _CurrentDir, root) ->
|
||||||
|
send_0a(State);
|
||||||
|
change_directory(State, root, Dir) ->
|
||||||
|
send_09(State, Dir).
|
||||||
|
|
||||||
|
-spec sendfile(state(), string()) -> ok.
|
||||||
|
sendfile(State, Filename) ->
|
||||||
|
{ok, IoDevice} = file:open(Filename, [read, raw, binary]),
|
||||||
|
sendfile(State, IoDevice, 0).
|
||||||
|
sendfile(State, IoDevice, N) ->
|
||||||
|
case file:read(IoDevice, 24576) of
|
||||||
|
{ok, Data} ->
|
||||||
|
send_11(State, Data, N),
|
||||||
|
sendfile(State, IoDevice, N + 1);
|
||||||
|
eof ->
|
||||||
|
ok = file:close(IoDevice)
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec handle_update_complete(state()) -> ok.
|
||||||
|
handle_update_complete(State=#state{socket=Socket, transport=Transport}) ->
|
||||||
|
send_13(State),
|
||||||
|
ok = Transport:close(Socket).
|
||||||
|
|
||||||
|
-spec send_01(state()) -> ok.
|
||||||
|
%% @doc Hello command sent on connect. Encryption is disabled.
|
||||||
|
send_01(#state{socket=Socket, transport=Transport}) ->
|
||||||
|
Bin = << 16#28:32/little, 16#01:32/little,
|
||||||
|
16#8b9f2dfa:32, 0:96, 1:32/little, 0:96 >>,
|
||||||
|
ok = Transport:send(Socket, Bin).
|
||||||
|
|
||||||
|
-spec send_09(state(), string()) -> ok.
|
||||||
|
%% @doc Change folder command.
|
||||||
|
send_09(#state{socket=Socket, transport=Transport}, Folder) ->
|
||||||
|
FolderBin = list_to_binary(Folder),
|
||||||
|
Padding = 8 * (64 - length(Folder)),
|
||||||
|
Bin = << 16#48:32/little, 16#09:32/little, FolderBin/binary, 0:Padding >>,
|
||||||
|
ok = Transport:send(Socket, Bin).
|
||||||
|
|
||||||
|
-spec send_0a(state()) -> ok.
|
||||||
|
%% @doc Back to root folder command.
|
||||||
|
send_0a(#state{socket=Socket, transport=Transport}) ->
|
||||||
|
Bin = << 16#8:32/little, 16#0a:32/little >>,
|
||||||
|
ok = Transport:send(Socket, Bin).
|
||||||
|
|
||||||
|
-spec send_0f(state()) -> ok.
|
||||||
|
%% @doc General update information command. Prepare the update screen.
|
||||||
|
send_0f(#state{socket=Socket, transport=Transport, files=Files}) ->
|
||||||
|
Size = lists:foldl(
|
||||||
|
fun(N, Acc) -> Acc + egs_patch_files_db:get_size(N) end, 0, Files),
|
||||||
|
NbFiles = length(Files),
|
||||||
|
Bin = << 16#10:32/little, 16#0f:32/little,
|
||||||
|
Size:32/little, NbFiles:32/little >>,
|
||||||
|
ok = Transport:send(Socket, Bin).
|
||||||
|
|
||||||
|
-spec send_10(state(), non_neg_integer(), binary()) -> ok.
|
||||||
|
%% @doc File update begin command. Prepare sending an individual file.
|
||||||
|
send_10(#state{socket=Socket, transport=Transport}, Size, FilenameBin) ->
|
||||||
|
Bin = << 16#50:32/little, 16#10:32/little, 0:32,
|
||||||
|
Size:32/little, FilenameBin/binary >>,
|
||||||
|
ok = Transport:send(Socket, Bin).
|
||||||
|
|
||||||
|
-spec send_11(state(), binary(), non_neg_integer()) -> ok.
|
||||||
|
%% @doc Command to send a file fragment.
|
||||||
|
send_11(#state{socket=Socket, transport=Transport}, Data, N) ->
|
||||||
|
DataSize = byte_size(Data),
|
||||||
|
Padding = case DataSize rem 4 of
|
||||||
|
0 -> 0;
|
||||||
|
Rem -> 8 * (4 - Rem)
|
||||||
|
end,
|
||||||
|
Data2 = << Data/binary, 0:Padding >>,
|
||||||
|
DataSize2 = DataSize + Padding div 8,
|
||||||
|
Size = DataSize2 + 16#14,
|
||||||
|
CRC = erlang:crc32(Data2),
|
||||||
|
Bin = << Size:32/little, 16#11:32/little, N:32/little,
|
||||||
|
CRC:32/little, DataSize:32/little, Data2/binary >>,
|
||||||
|
ok = Transport:send(Socket, Bin).
|
||||||
|
|
||||||
|
-spec send_12(state()) -> ok.
|
||||||
|
%% @doc File update end command.
|
||||||
|
send_12(#state{socket=Socket, transport=Transport}) ->
|
||||||
|
Bin = << 16#8:32/little, 16#12:32/little >>,
|
||||||
|
ok = Transport:send(Socket, Bin).
|
||||||
|
|
||||||
|
-spec send_13(state()) -> ok.
|
||||||
|
%% @doc Update complete command. Followed by the server closing the connection.
|
||||||
|
send_13(#state{socket=Socket, transport=Transport}) ->
|
||||||
|
Bin = << 16#8:32/little, 16#13:32/little >>,
|
||||||
|
ok = Transport:send(Socket, Bin).
|
32
apps/egs_patch/src/egs_patch_sup.erl
Normal file
32
apps/egs_patch/src/egs_patch_sup.erl
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
%% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_patch_sup).
|
||||||
|
-behaviour(supervisor).
|
||||||
|
|
||||||
|
-export([start_link/0]). %% API.
|
||||||
|
-export([init/1]). %% Supervisor.
|
||||||
|
|
||||||
|
-spec start_link() -> {ok, pid()}.
|
||||||
|
start_link() ->
|
||||||
|
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||||
|
|
||||||
|
-spec init([]) -> {ok, {{one_for_one, 10, 10}, [supervisor:child_spec(), ...]}}.
|
||||||
|
init([]) ->
|
||||||
|
Procs = [{egs_patch_files_db, {egs_patch_files_db, start_link, []},
|
||||||
|
permanent, 5000, worker, [egs_patch_files_db]}],
|
||||||
|
{ok, {{one_for_one, 10, 10}, Procs}}.
|
0
apps/egs_store/priv/.gitignore
vendored
Normal file
0
apps/egs_store/priv/.gitignore
vendored
Normal file
13
apps/egs_store/src/egs_store.app.src
Normal file
13
apps/egs_store/src/egs_store.app.src
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
%%-*- mode: erlang -*-
|
||||||
|
{application, egs_store, [
|
||||||
|
{description, "EGS storage layer."},
|
||||||
|
{vsn, "0.1.0"},
|
||||||
|
{modules, []},
|
||||||
|
{registered, []},
|
||||||
|
{applications, [
|
||||||
|
kernel,
|
||||||
|
stdlib
|
||||||
|
]},
|
||||||
|
{mod, {egs_store_app, []}},
|
||||||
|
{env, []}
|
||||||
|
]}.
|
92
apps/egs_store/src/egs_store.erl
Normal file
92
apps/egs_store/src/egs_store.erl
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_store).
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
-export([start_link/0,
|
||||||
|
load_character/2, load_characters/2, save_character/3]). %% API.
|
||||||
|
-export([init/1, handle_call/3, handle_cast/2,
|
||||||
|
handle_info/2, terminate/2, code_change/3]). %% gen_server.
|
||||||
|
|
||||||
|
-define(SERVER, ?MODULE).
|
||||||
|
-define(ACCOUNTS_TBL, accounts_tbl).
|
||||||
|
-define(ACCOUNTS_VSN, 1).
|
||||||
|
-define(CHARACTERS_TBL, characters_tbl).
|
||||||
|
-define(CHARACTERS_VSN, 1).
|
||||||
|
|
||||||
|
%% API.
|
||||||
|
|
||||||
|
start_link() ->
|
||||||
|
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||||||
|
|
||||||
|
load_character(GID, Slot) ->
|
||||||
|
gen_server:call(?SERVER, {load_character, GID, Slot}).
|
||||||
|
|
||||||
|
load_characters(GID, Slots) ->
|
||||||
|
gen_server:call(?SERVER, {load_characters, GID, Slots}).
|
||||||
|
|
||||||
|
save_character(GID, Slot, Data) ->
|
||||||
|
gen_server:call(?SERVER, {save_character, GID, Slot, Data}).
|
||||||
|
|
||||||
|
%% gen_server.
|
||||||
|
|
||||||
|
init([]) ->
|
||||||
|
{ok, App} = application:get_application(),
|
||||||
|
PrivDir = code:priv_dir(App),
|
||||||
|
AccountsFile = PrivDir ++ "/accounts.tbl",
|
||||||
|
CharactersFile = PrivDir ++ "/characters.tbl",
|
||||||
|
{ok, ?ACCOUNTS_TBL} = dets:open_file(?ACCOUNTS_TBL,
|
||||||
|
[{file, AccountsFile}]),
|
||||||
|
io:format("accounts tbl:~n~p~n~n", [dets:info(?ACCOUNTS_TBL)]),
|
||||||
|
{ok, ?CHARACTERS_TBL} = dets:open_file(?CHARACTERS_TBL,
|
||||||
|
[{file, CharactersFile}]),
|
||||||
|
io:format("characters tbl:~n~p~n~n", [dets:info(?CHARACTERS_TBL)]),
|
||||||
|
{ok, undefined}.
|
||||||
|
|
||||||
|
handle_call({load_character, GID, Slot}, _From, State) ->
|
||||||
|
case dets:lookup(?CHARACTERS_TBL, {GID, Slot}) of
|
||||||
|
[{{GID, Slot}, Version, Data}] ->
|
||||||
|
{reply, {ok, Version, Data}, State};
|
||||||
|
[] ->
|
||||||
|
{reply, {error, notfound}, State}
|
||||||
|
end;
|
||||||
|
handle_call({load_characters, GID, Slots}, _From, State) ->
|
||||||
|
Characters = lists:map(fun(Slot) ->
|
||||||
|
case dets:lookup(?CHARACTERS_TBL, {GID, Slot}) of
|
||||||
|
[{{GID, Slot}, Version, Data}] ->
|
||||||
|
{Version, Data};
|
||||||
|
[] ->
|
||||||
|
notfound
|
||||||
|
end
|
||||||
|
end, Slots),
|
||||||
|
{reply, {ok, Characters}, State};
|
||||||
|
handle_call({save_character, GID, Slot, Data}, _From, State) ->
|
||||||
|
ok = dets:insert(?CHARACTERS_TBL, {{GID, Slot}, ?CHARACTERS_VSN, Data}),
|
||||||
|
{reply, ok, State};
|
||||||
|
handle_call(_Request, _From, State) ->
|
||||||
|
{reply, ignored, State}.
|
||||||
|
|
||||||
|
handle_cast(_Msg, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
handle_info(_Info, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
terminate(_Reason, _State) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
code_change(_OldVsn, State, _Extra) ->
|
||||||
|
{ok, State}.
|
31
apps/egs_store/src/egs_store_app.erl
Normal file
31
apps/egs_store/src/egs_store_app.erl
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_store_app).
|
||||||
|
-behaviour(application).
|
||||||
|
-export([start/2, stop/1]). %% API.
|
||||||
|
|
||||||
|
-type application_start_type()
|
||||||
|
:: normal | {takeover, node()} | {failover, node()}.
|
||||||
|
|
||||||
|
%% API.
|
||||||
|
|
||||||
|
-spec start(application_start_type(), any()) -> {ok, pid()}.
|
||||||
|
start(_Type, _StartArgs) ->
|
||||||
|
egs_store_sup:start_link().
|
||||||
|
|
||||||
|
-spec stop(any()) -> ok.
|
||||||
|
stop(_State) ->
|
||||||
|
ok.
|
31
apps/egs_store/src/egs_store_sup.erl
Normal file
31
apps/egs_store/src/egs_store_sup.erl
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(egs_store_sup).
|
||||||
|
-behaviour(supervisor).
|
||||||
|
|
||||||
|
-export([start_link/0]). %% API.
|
||||||
|
-export([init/1]). %% Supervisor.
|
||||||
|
|
||||||
|
-spec start_link() -> {ok, pid()}.
|
||||||
|
start_link() ->
|
||||||
|
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||||
|
|
||||||
|
-spec init([]) -> {ok, {{one_for_one, 10, 10}, [supervisor:child_spec(), ...]}}.
|
||||||
|
init([]) ->
|
||||||
|
{ok, {{one_for_one, 10, 10}, [
|
||||||
|
{egs_store, {egs_store, start_link, []},
|
||||||
|
permanent, 5000, worker, [egs_store]}
|
||||||
|
]}}.
|
173
apps/prs/c_src/prs.c
Normal file
173
apps/prs/c_src/prs.c
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
/*
|
||||||
|
Original code from the PRSutil project.
|
||||||
|
http://www.fuzziqersoftware.com/projects.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
unsigned char bitpos;
|
||||||
|
unsigned char* controlbyteptr;
|
||||||
|
unsigned char* srcptr_orig;
|
||||||
|
unsigned char* dstptr_orig;
|
||||||
|
unsigned char* srcptr;
|
||||||
|
unsigned char* dstptr; } PRS_COMPRESSOR;
|
||||||
|
|
||||||
|
void prs_put_control_bit(PRS_COMPRESSOR* pc,unsigned char bit)
|
||||||
|
{
|
||||||
|
*pc->controlbyteptr = *pc->controlbyteptr >> 1;
|
||||||
|
*pc->controlbyteptr |= ((!!bit) << 7);
|
||||||
|
pc->bitpos++;
|
||||||
|
if (pc->bitpos >= 8)
|
||||||
|
{
|
||||||
|
pc->bitpos = 0;
|
||||||
|
pc->controlbyteptr = pc->dstptr;
|
||||||
|
pc->dstptr++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void prs_put_control_bit_nosave(PRS_COMPRESSOR* pc,unsigned char bit)
|
||||||
|
{
|
||||||
|
*pc->controlbyteptr = *pc->controlbyteptr >> 1;
|
||||||
|
*pc->controlbyteptr |= ((!!bit) << 7);
|
||||||
|
pc->bitpos++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void prs_put_control_save(PRS_COMPRESSOR* pc)
|
||||||
|
{
|
||||||
|
if (pc->bitpos >= 8)
|
||||||
|
{
|
||||||
|
pc->bitpos = 0;
|
||||||
|
pc->controlbyteptr = pc->dstptr;
|
||||||
|
pc->dstptr++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void prs_put_static_data(PRS_COMPRESSOR* pc,unsigned char data)
|
||||||
|
{
|
||||||
|
*pc->dstptr = data;
|
||||||
|
pc->dstptr++;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned char prs_get_static_data(PRS_COMPRESSOR* pc)
|
||||||
|
{
|
||||||
|
unsigned char data = *pc->srcptr;
|
||||||
|
pc->srcptr++;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* **************************************************** */
|
||||||
|
|
||||||
|
void prs_init(PRS_COMPRESSOR* pc,void* src,void* dst)
|
||||||
|
{
|
||||||
|
pc->bitpos = 0;
|
||||||
|
pc->srcptr = (unsigned char*)src;
|
||||||
|
pc->srcptr_orig = (unsigned char*)src;
|
||||||
|
pc->dstptr = (unsigned char*)dst;
|
||||||
|
pc->dstptr_orig = (unsigned char*)dst;
|
||||||
|
pc->controlbyteptr = pc->dstptr;
|
||||||
|
pc->dstptr++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void prs_finish(PRS_COMPRESSOR* pc)
|
||||||
|
{
|
||||||
|
prs_put_control_bit(pc,0);
|
||||||
|
prs_put_control_bit(pc,1);
|
||||||
|
if (pc->bitpos != 0)
|
||||||
|
{
|
||||||
|
*pc->controlbyteptr = ((*pc->controlbyteptr << pc->bitpos) >> 8);
|
||||||
|
}
|
||||||
|
prs_put_static_data(pc,0);
|
||||||
|
prs_put_static_data(pc,0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void prs_rawbyte(PRS_COMPRESSOR* pc)
|
||||||
|
{
|
||||||
|
prs_put_control_bit_nosave(pc,1);
|
||||||
|
prs_put_static_data(pc,prs_get_static_data(pc));
|
||||||
|
prs_put_control_save(pc);
|
||||||
|
}
|
||||||
|
|
||||||
|
void prs_shortcopy(PRS_COMPRESSOR* pc,int offset,unsigned char size)
|
||||||
|
{
|
||||||
|
size -= 2;
|
||||||
|
prs_put_control_bit(pc,0);
|
||||||
|
prs_put_control_bit(pc,0);
|
||||||
|
prs_put_control_bit(pc,(size >> 1) & 1);
|
||||||
|
prs_put_control_bit_nosave(pc,size & 1);
|
||||||
|
prs_put_static_data(pc,offset & 0xFF);
|
||||||
|
prs_put_control_save(pc);
|
||||||
|
}
|
||||||
|
|
||||||
|
void prs_longcopy(PRS_COMPRESSOR* pc,int offset,unsigned char size)
|
||||||
|
{
|
||||||
|
if (size <= 9)
|
||||||
|
{
|
||||||
|
prs_put_control_bit(pc,0);
|
||||||
|
prs_put_control_bit_nosave(pc,1);
|
||||||
|
prs_put_static_data(pc,((offset << 3) & 0xF8) | ((size - 2) & 0x07));
|
||||||
|
prs_put_static_data(pc,(offset >> 5) & 0xFF);
|
||||||
|
prs_put_control_save(pc);
|
||||||
|
} else {
|
||||||
|
prs_put_control_bit(pc,0);
|
||||||
|
prs_put_control_bit_nosave(pc,1);
|
||||||
|
prs_put_static_data(pc,(offset << 3) & 0xF8);
|
||||||
|
prs_put_static_data(pc,(offset >> 5) & 0xFF);
|
||||||
|
prs_put_static_data(pc,size - 1);
|
||||||
|
prs_put_control_save(pc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void prs_copy(PRS_COMPRESSOR* pc,int offset,unsigned char size)
|
||||||
|
{
|
||||||
|
if ((offset > -0x100) && (size <= 5))
|
||||||
|
prs_shortcopy(pc,offset,size);
|
||||||
|
else
|
||||||
|
prs_longcopy(pc,offset,size);
|
||||||
|
pc->srcptr += size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* **************************************************** */
|
||||||
|
|
||||||
|
unsigned long prs_compress(void* source,void* dest,unsigned long size)
|
||||||
|
{
|
||||||
|
PRS_COMPRESSOR pc;
|
||||||
|
int x,y;
|
||||||
|
unsigned long xsize;
|
||||||
|
int lsoffset,lssize;
|
||||||
|
prs_init(&pc,source,dest);
|
||||||
|
for (x = 0; x < size; x++)
|
||||||
|
{
|
||||||
|
lsoffset = lssize = xsize = 0;
|
||||||
|
for (y = x - 3; (y > 0) && (y > (x - 0x1FF0)) && (xsize < 255); y--)
|
||||||
|
{
|
||||||
|
xsize = 3;
|
||||||
|
if (!memcmp((void*)((unsigned long)source + y),(void*)((unsigned long)source + x),xsize))
|
||||||
|
{
|
||||||
|
do xsize++;
|
||||||
|
while (!memcmp((void*)((unsigned long)source + y),
|
||||||
|
(void*)((unsigned long)source + x),
|
||||||
|
xsize) &&
|
||||||
|
(xsize < 256) &&
|
||||||
|
((y + xsize) < x) &&
|
||||||
|
((x + xsize) <= size)
|
||||||
|
);
|
||||||
|
xsize--;
|
||||||
|
if (xsize > lssize)
|
||||||
|
{
|
||||||
|
lsoffset = -(x - y);
|
||||||
|
lssize = xsize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lssize == 0)
|
||||||
|
{
|
||||||
|
prs_rawbyte(&pc);
|
||||||
|
} else {
|
||||||
|
prs_copy(&pc,lsoffset,lssize);
|
||||||
|
x += (lssize - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prs_finish(&pc);
|
||||||
|
return pc.dstptr - pc.dstptr_orig;
|
||||||
|
}
|
74
apps/prs/c_src/prs_drv.c
Normal file
74
apps/prs/c_src/prs_drv.c
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
@author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
@copyright 2010-2011 Loïc Hoguin.
|
||||||
|
@doc PRS Erlang driver for EGS.
|
||||||
|
|
||||||
|
This file is part of EGS.
|
||||||
|
|
||||||
|
EGS is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
EGS is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <erl_nif.h>
|
||||||
|
|
||||||
|
extern unsigned long prs_compress(unsigned char* source, unsigned char* dest, unsigned long size);
|
||||||
|
|
||||||
|
static ERL_NIF_TERM compress_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
|
||||||
|
{
|
||||||
|
ErlNifBinary srcbin;
|
||||||
|
ErlNifBinary destbin;
|
||||||
|
unsigned int size;
|
||||||
|
|
||||||
|
if (argc != 1)
|
||||||
|
return enif_make_badarg(env);
|
||||||
|
|
||||||
|
if (!enif_is_binary(env, argv[0]))
|
||||||
|
return enif_make_badarg(env);
|
||||||
|
|
||||||
|
if (!enif_inspect_binary(env, argv[0], &srcbin))
|
||||||
|
return enif_make_badarg(env);
|
||||||
|
|
||||||
|
if (!enif_alloc_binary((9 * srcbin.size) / 8 + 2, &destbin))
|
||||||
|
return enif_make_badarg(env);
|
||||||
|
|
||||||
|
size = prs_compress(srcbin.data, destbin.data, srcbin.size);
|
||||||
|
enif_realloc_binary(&destbin, size);
|
||||||
|
|
||||||
|
return enif_make_binary(env, &destbin);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int load(ErlNifEnv* env, void** priv, ERL_NIF_TERM load_info)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int reload(ErlNifEnv* env, void** priv, ERL_NIF_TERM load_info)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int upgrade(ErlNifEnv* env, void** priv, void** old_priv, ERL_NIF_TERM load_info)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void unload(ErlNifEnv* env, void* priv)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ErlNifFunc nif_funcs[] = {
|
||||||
|
{"compress", 1, compress_nif}
|
||||||
|
};
|
||||||
|
|
||||||
|
ERL_NIF_INIT(prs, nif_funcs, load, reload, upgrade, unload)
|
11
apps/prs/src/prs.app.src
Normal file
11
apps/prs/src/prs.app.src
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
%%-*- mode: erlang -*-
|
||||||
|
{application, prs, [
|
||||||
|
{description, "PRS compression library."},
|
||||||
|
{vsn, "0.1.0"},
|
||||||
|
{modules, []},
|
||||||
|
{registered, []},
|
||||||
|
{applications, [
|
||||||
|
kernel,
|
||||||
|
stdlib
|
||||||
|
]}
|
||||||
|
]}.
|
29
apps/prs/src/prs.erl
Normal file
29
apps/prs/src/prs.erl
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
%% @author Loïc Hoguin <essen@dev-extend.eu>
|
||||||
|
%% @copyright 2010-2011 Loïc Hoguin.
|
||||||
|
%% @doc EGS file creation functions.
|
||||||
|
%%
|
||||||
|
%% This file is part of EGS.
|
||||||
|
%%
|
||||||
|
%% EGS is free software: you can redistribute it and/or modify
|
||||||
|
%% it under the terms of the GNU Affero General Public License as
|
||||||
|
%% published by the Free Software Foundation, either version 3 of the
|
||||||
|
%% License, or (at your option) any later version.
|
||||||
|
%%
|
||||||
|
%% EGS is distributed in the hope that it will be useful,
|
||||||
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
%% GNU Affero General Public License for more details.
|
||||||
|
%%
|
||||||
|
%% You should have received a copy of the GNU Affero General Public License
|
||||||
|
%% along with EGS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
-module(prs).
|
||||||
|
-export([init/0, compress/1]).
|
||||||
|
-on_load(init/0).
|
||||||
|
|
||||||
|
init() ->
|
||||||
|
PrivDir = code:priv_dir(prs),
|
||||||
|
erlang:load_nif(PrivDir ++ "/prs_drv", 0).
|
||||||
|
|
||||||
|
compress(_SrcBin) ->
|
||||||
|
erlang:nif_error(not_loaded).
|
1
data/counters/.gitignore
vendored
1
data/counters/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
*
|
|
1
data/lobby/.gitignore
vendored
1
data/lobby/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
*
|
|
1
data/missions/.gitignore
vendored
1
data/missions/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
*
|
|
1
data/rooms/.gitignore
vendored
1
data/rooms/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
*
|
|
1
data/tutorial/.gitignore
vendored
1
data/tutorial/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
*
|
|
1
docs/protocol/README
Normal file
1
docs/protocol/README
Normal file
@ -0,0 +1 @@
|
|||||||
|
All the files in this folder are research released in the public domain.
|
419
docs/protocol/commands.txt
Normal file
419
docs/protocol/commands.txt
Normal file
@ -0,0 +1,419 @@
|
|||||||
|
[ Research Status / Importance / Occurences / Command / Channels / Description ]
|
||||||
|
|
||||||
|
Characters
|
||||||
|
|
||||||
|
!* C 6 0101 B--
|
||||||
|
WIP C 7 0102 BCS animation state change (movement, sit, stand up, trigger unicube, talk to npc...)
|
||||||
|
!* C 6 0104 B-- sit position change (floor and chairs); goggle position change
|
||||||
|
WIP A 7 0105 -CS weapon/armor equip, item use/drop and more; request/reply
|
||||||
|
? 6 0106 -CS
|
||||||
|
!* C 4 0107 B-- sit on chair, get on vehicle
|
||||||
|
? 6 0108 --S
|
||||||
|
! ? 4 0109 -C- weapon/item palette change
|
||||||
|
WIP B 5 010a -CS shop listing request/reply
|
||||||
|
OK ? 5 010b -C- player enter area (probably broadcast spawn here)
|
||||||
|
WIP A 6 010d --S character details (excluding location); similar to 0201
|
||||||
|
!* C 7 010f B-- talk to npc; set trap, boom trap
|
||||||
|
WIP A 5 0110 -C- triggered event (death, type change, online status change...)
|
||||||
|
OK A 6 0111 --S trigger event
|
||||||
|
C 3 0112 --S
|
||||||
|
!* B 4 0113 --S types capabilities list
|
||||||
|
OK A 7 0115 --S update EXP/money...
|
||||||
|
OK A 5 0117 --S revive
|
||||||
|
? 5 0119 -CS (probably make the RCSM shoot)
|
||||||
|
|
||||||
|
System
|
||||||
|
|
||||||
|
OK A 5 0200 --S zone init
|
||||||
|
WIP A 6 0201 --S character details (including location); similar to 010d
|
||||||
|
OK A 5 0202 --S hello
|
||||||
|
OK A 5 0203 --S spawn character
|
||||||
|
OK B 5 0204 --S unspawn character
|
||||||
|
OK A 6 0205 --S load map (related to 0806)
|
||||||
|
OK A 5 0208 --S end of loading (related to 0808)
|
||||||
|
OK A 4 020b -C- character selection: select slot
|
||||||
|
OK ? 5 020c -C- (despite the channel, sent by the server; channel 2)
|
||||||
|
OK A 4 020d -C- game auth: token authentication request
|
||||||
|
OK A 5 020e --S quest file transfer
|
||||||
|
OK A 5 020f --S zone file transfer
|
||||||
|
OK C 5 0210 --S current unix time
|
||||||
|
OK C 5 0215 --S
|
||||||
|
OK A 5 0216 --S login: forward to game server
|
||||||
|
OK A 5 0217 -C- login: game server address request
|
||||||
|
OK ? 5 0219 -C- login: password authentication request
|
||||||
|
OK A 5 021b --S (character selection: end)
|
||||||
|
OK ? 5 021c -C- (character selection: end) reply
|
||||||
|
OK A 4 021d -C- uni cube request
|
||||||
|
OK A 4 021e --S uni cube reply
|
||||||
|
OK A 4 021f -C- uni cube selection
|
||||||
|
! ? 4 0220 -C- (uni cube related, sent by the client? thought it was sent by the server)
|
||||||
|
OK A 5 0222 --S current uni details
|
||||||
|
OK ? 5 0223 --S login: password authentication success
|
||||||
|
! ? 3 0224 --S disconnect user, reason given
|
||||||
|
OK ? 5 0225 --S login: motd reply
|
||||||
|
OK A 5 0226 -C- login: motd request (jp only): same as 023f (apparently uses channel 3)
|
||||||
|
OK ? 5 0227 -C- login: whether the motd (TOS on official) was accepted
|
||||||
|
OK ? 3 0228 --S display a notice: global message; rare mission get message
|
||||||
|
!* ? 5 022c --S (probably related to parties or NPCs)
|
||||||
|
-- - - 022d --S gameguard ping
|
||||||
|
-- - - 022e -C- gameguard pong
|
||||||
|
OK ? 4 0230 --S (universe change related, including room doors)
|
||||||
|
OK A 2 0231 --S send user to website (used on wrong password)
|
||||||
|
WIP A 5 0233 --S list of other characters in zone/lobby/area, whichever
|
||||||
|
! ? 3 0234 -C- (probably related to 0235; probably partner card related)
|
||||||
|
? 3 0235 --S (probably related to 0234; probably partner card related; sends 1 GID and 4 card-like character infos)
|
||||||
|
OK A 5 0236 --S start zone handler (load objects and enable them)
|
||||||
|
! ? 4 0237 -C-
|
||||||
|
! ? 4 0238 --S (probably related to 0237)
|
||||||
|
OK A 5 023f -C- login: motd request (us only): same as 0226
|
||||||
|
! ? 3 0245 --S
|
||||||
|
! ? 5 0246 --S add or replace text strings
|
||||||
|
? 5 024b -C-
|
||||||
|
? ? 024f -C- (gam/gas dialog request; jp only)
|
||||||
|
? ? 0250 --S (gam/gas dialog reply; jp only)
|
||||||
|
? ? 025e --S (lumilass-related; jp only)
|
||||||
|
|
||||||
|
Chat
|
||||||
|
|
||||||
|
! B 2 0302 -C- shortcuts change
|
||||||
|
! B 5 0303 --S shortcuts list
|
||||||
|
OK A 6 0304 -CS message
|
||||||
|
! C 4 0305 --S sound
|
||||||
|
|
||||||
|
Missions
|
||||||
|
|
||||||
|
!* C 7 0402 -C- enemy event (safely ignored?)
|
||||||
|
!* C 6 0404 -C- object event
|
||||||
|
|
||||||
|
Movements
|
||||||
|
|
||||||
|
!** A 8 0503 B-S position change
|
||||||
|
!* C 7 050f B-- camera change
|
||||||
|
!* C 7 0514 B-- stay still
|
||||||
|
|
||||||
|
Area/State change
|
||||||
|
|
||||||
|
OK ? 5 0806 -C- (related to 0808)
|
||||||
|
OK A 5 0807 -C- area change
|
||||||
|
OK ? 5 0808 -C- (related to 0806)
|
||||||
|
WIP ? 4 080c -C- npc force invite-- NO, more like invite npc specific for this quest (in the file), probably kills NPC on zone change too (but not map change apparently)
|
||||||
|
OK ? 5 080d -C- (area change successful notice?)
|
||||||
|
OK C 5 080e -C- client/platform/chosen entrance info
|
||||||
|
OK ? 4 080f -C- (npc related or story related)
|
||||||
|
OK A 4 0811 -C- enter counter
|
||||||
|
OK A 4 0812 -C- leave counter
|
||||||
|
OK A 3 0813 -C- npc invite
|
||||||
|
OK ? 4 0814 -C- (mission area change successful notice?)
|
||||||
|
OK ? 5 0815 -C- (non-mission area change successful notice?)
|
||||||
|
OK C 4 0818 -C- cpu/gpu info
|
||||||
|
|
||||||
|
Items
|
||||||
|
|
||||||
|
? 5 0a01 --S
|
||||||
|
? 5 0a02 --S
|
||||||
|
? 3 0a03 --S
|
||||||
|
* A 4 0a04 --S npc inventory
|
||||||
|
OK A 5 0a05 --S (player inventory related; the LID is changed right before this command, or right before the previous 0215 command in the case of missions)
|
||||||
|
OK A 5 0a06 --S player inventory: list of unique items id
|
||||||
|
? 4 0a07 -CS (view stats/equip)
|
||||||
|
? 3 0a08 --S
|
||||||
|
** C 5 0a09 -CS (probably: receive item)
|
||||||
|
!* A 5 0a0a --S player inventory
|
||||||
|
? 2 0a0b --S
|
||||||
|
? 4 0a0e --S
|
||||||
|
? 4 0a0f --S
|
||||||
|
OK C 5 0a10 -C- item description request
|
||||||
|
OK C 5 0a11 --S item description reply
|
||||||
|
|
||||||
|
Packet fragments
|
||||||
|
|
||||||
|
OK A 6 0b03 --- fragment (no channel)
|
||||||
|
OK A 6 0b05 --- reception confirmed (no channel)
|
||||||
|
|
||||||
|
Quests
|
||||||
|
|
||||||
|
OK A 5 0c00 --S quest init
|
||||||
|
OK A 4 0c01 -C- mission start request
|
||||||
|
!* A 4 0c02 --S mission start reply (no 1020 or 1015 when rejected)
|
||||||
|
OK A 4 0c05 -C- counter quest files pack request
|
||||||
|
OK A 4 0c06 --S counter quest files pack transfer
|
||||||
|
OK A 3 0c07 -C- lobby transport request
|
||||||
|
OK A 3 0c08 --S lobby transport reply
|
||||||
|
* A 4 0c09 --S trial start; end (contains reward)
|
||||||
|
? 5 0c0a --S
|
||||||
|
? 4 0c0b --S
|
||||||
|
? 4 0c0c --S
|
||||||
|
OK ? 4 0c0d -C- (mission start and mission area change related)
|
||||||
|
OK A 3 0c0e -C- mission abort
|
||||||
|
OK A 4 0c0f -C- counter options request
|
||||||
|
OK A 4 0c10 --S counter options reply
|
||||||
|
!* ? 3 0c11 -C- (story mission related; probably related to 0c12)
|
||||||
|
!* ? 3 0c12 --S (probably related to 0c11)
|
||||||
|
? 3 0c13 -C- (probably related to 0c14)
|
||||||
|
? 3 0c14 -C- (probably related to 0c13)
|
||||||
|
|
||||||
|
Account/Flags
|
||||||
|
|
||||||
|
WIP A 5 0d01 --S character selection: selected character details
|
||||||
|
OK A 2 0d02 -C- character selection: create character
|
||||||
|
!* A 4 0d03 --S character selection: character list reply
|
||||||
|
!** C 5 0d04 -CS set flag
|
||||||
|
OK C 4 0d05 --S system flags list
|
||||||
|
OK A 4 0d06 -C- character selection: character list request
|
||||||
|
OK A 3 0d07 -C- options change
|
||||||
|
! ? 1 0d08 -C- (gbr info request, 1 per dialog)
|
||||||
|
? 1 0d09 --S (gbr info reply 2, 4, 5; during GBR only)
|
||||||
|
! ? 1 0d0a --S (gbr info reply 1, 3; many GBR 0d04 sent by the server before that; also sent when not in GBR)
|
||||||
|
|
||||||
|
Hits
|
||||||
|
|
||||||
|
WIP A 6 0e00 -C- hit
|
||||||
|
! ? 3 0e01 -C-
|
||||||
|
* A 7 0e07 --S hit reply
|
||||||
|
? 4 0e08 --S
|
||||||
|
|
||||||
|
Zones
|
||||||
|
|
||||||
|
!* C 2 0f00 -C- vehicle init
|
||||||
|
!* C 2 0f02 -C- enter vehicle
|
||||||
|
? 4 0f03 -C- (vehicle move?)
|
||||||
|
! ? 1 0f04 -C-
|
||||||
|
! ? 1 0f05 -C- (vehicle related)
|
||||||
|
(0f06 vehicle related?)
|
||||||
|
!* C 2 0f07 -C- (enter vehicle related)
|
||||||
|
! ? 2 0f09 -C- (vehicle related)
|
||||||
|
WIP A 6 0f0a -C- object event
|
||||||
|
|
||||||
|
Parties
|
||||||
|
|
||||||
|
! ? 3 1002 -C- (leave party request)
|
||||||
|
? 3 1003 --S
|
||||||
|
* A 4 1004 --S join event (add character info to the left of the screen)
|
||||||
|
!* ? 5 1005 --S
|
||||||
|
OK A 5 1006 --S trigger party event
|
||||||
|
OK ? 4 1007 -C- remove member request
|
||||||
|
? 3 100a -CS (invite other player to party; received invite)
|
||||||
|
? 3 100c -CS
|
||||||
|
OK A 5 100e --S character location
|
||||||
|
* ? 4 100f --S
|
||||||
|
? 4 1011 --S
|
||||||
|
? 2 1013 -C-
|
||||||
|
!* A 4 1015 --S mission start: quest file transfer (for mission details window)
|
||||||
|
* ? 4 1016 -CS (probably related to NPCs)
|
||||||
|
? 6 1017 -CS (related to NPC removal from party in story mission)
|
||||||
|
* ? 6 1019 -CS
|
||||||
|
* ? 3 101a --S
|
||||||
|
? 3 101b -C-
|
||||||
|
? 3 101c --- (channel 5; accept party invite)
|
||||||
|
C 1 101d -C- (set invite block)
|
||||||
|
? 3 101e --S
|
||||||
|
? 6 101f --S character level and hp update (on the left of the screen); sometimes sent in other places
|
||||||
|
OK A 4 1020 --S (mission start related)
|
||||||
|
OK ? 6 1022 --S character hp update (on the left of the screen)
|
||||||
|
|
||||||
|
Bosses
|
||||||
|
|
||||||
|
! ? 4 1101 -C-
|
||||||
|
? 3 1102 -C-
|
||||||
|
? 4 1103 -C-
|
||||||
|
? 4 1104 --S
|
||||||
|
? 3 1105 -C-
|
||||||
|
* ? 3 1106 -C-
|
||||||
|
? 3 1107 -C-
|
||||||
|
? 3 1108 --S
|
||||||
|
? 3 1109 --S
|
||||||
|
? 2 110a --S
|
||||||
|
? 2 110b --S
|
||||||
|
? 4 110c --S
|
||||||
|
? 3 110d --S
|
||||||
|
* ? 4 110e --S
|
||||||
|
? 4 110f --S
|
||||||
|
? 4 1110 --S
|
||||||
|
? 4 1111 --S
|
||||||
|
* ? 3 1112 -C- (probably related to 1113)
|
||||||
|
* ? 3 1113 --S (probably related to 1112)
|
||||||
|
? 3 1114 --S
|
||||||
|
? 2 1116 --S
|
||||||
|
? 1 1117 --S
|
||||||
|
? 2 1118 --S
|
||||||
|
? 4 1119 --S
|
||||||
|
? 3 111b --S
|
||||||
|
? 3 111c --S
|
||||||
|
? 2 111d --S
|
||||||
|
? 2 111e --S
|
||||||
|
? 3 111f --S
|
||||||
|
? 4 1120 --S
|
||||||
|
? 4 1121 --S
|
||||||
|
? 4 1122 --S
|
||||||
|
? 4 1123 -C-
|
||||||
|
? 2 1124 --S
|
||||||
|
|
||||||
|
Instances state
|
||||||
|
|
||||||
|
? 7 1201 --S (related to 0402; probably this enemy synchronization thing)
|
||||||
|
* A 6 1202 --S
|
||||||
|
OK A 5 1204 --S
|
||||||
|
!* A 6 1205 --S block/map event
|
||||||
|
* A 5 1206 --S
|
||||||
|
* A 4 1207 --S
|
||||||
|
* C 3 1208 --S (vehicle init related)
|
||||||
|
* C 3 120a --S (enter vehicle related)
|
||||||
|
? 5 120b --S
|
||||||
|
? 1 120c --S
|
||||||
|
* C 3 120f --S (enter vehicle related; reply for 0f07)
|
||||||
|
? 1 1210 --S
|
||||||
|
!* A 6 1211 --S (object event related)
|
||||||
|
* A 5 1212 --S
|
||||||
|
!* A 6 1213 --S object state on/off
|
||||||
|
C 1 1214 --S season on/off
|
||||||
|
* A 3 1215 --S (boss gate related)
|
||||||
|
** C 3 1216 --- (airboard rally race start; channel 0)
|
||||||
|
|
||||||
|
Player rooms
|
||||||
|
|
||||||
|
C 4 1304 -C-
|
||||||
|
C 2 1305 -C-
|
||||||
|
* C 4 1309 --S
|
||||||
|
C 4 130a --S
|
||||||
|
C 2 130b -C-
|
||||||
|
C 2 130c --S
|
||||||
|
C 6 130d -C-
|
||||||
|
C 6 130e --S
|
||||||
|
C 5 130f -C-
|
||||||
|
C 3 1310 --S
|
||||||
|
C 3 1312 --S
|
||||||
|
C 3 1313 -C-
|
||||||
|
C 3 1314 --S
|
||||||
|
C 2 1316 --S
|
||||||
|
C 5 1318 --S
|
||||||
|
C 2 1319 -C-
|
||||||
|
C 2 131a -C-
|
||||||
|
C 6 131b --S
|
||||||
|
C 3 131c --S
|
||||||
|
C 5 131e --S
|
||||||
|
C 3 131f --S
|
||||||
|
C 3 1320 --S
|
||||||
|
C 2 1321 --S
|
||||||
|
C 4 1323 --S
|
||||||
|
C 5 1324 --S
|
||||||
|
C 3 1325 --S
|
||||||
|
C 3 1326 --S
|
||||||
|
C 5 1327 --S
|
||||||
|
C 4 1328 -C-
|
||||||
|
C 3 1329 --S
|
||||||
|
C 2 132a -C-
|
||||||
|
C 4 132b --S
|
||||||
|
C 3 132c --S
|
||||||
|
C 4 132d -C-
|
||||||
|
C 3 132e --S (probably related to 132f)
|
||||||
|
C 3 132f -C- (probably related to 132e)
|
||||||
|
C 4 1330 -C-
|
||||||
|
C 2 1331 --S
|
||||||
|
* C 4 1332 --S
|
||||||
|
C 3 1333 --S
|
||||||
|
C 4 1334 -C-
|
||||||
|
C 4 1335 --S
|
||||||
|
C 4 1336 --S
|
||||||
|
C 4 1337 -C- (probably related to 1338)
|
||||||
|
C 4 1338 --S (probably related to 1337)
|
||||||
|
C 3 1339 --S
|
||||||
|
|
||||||
|
???
|
||||||
|
|
||||||
|
? 3 1402 -C- (occurs in 1025012, Sakura Blast A 2nd route)
|
||||||
|
? 2 1403 -C-
|
||||||
|
? 3 1410 --S
|
||||||
|
|
||||||
|
Partner cards/Simple mail
|
||||||
|
|
||||||
|
OK A 5 1500 --S player's own partner card
|
||||||
|
!* B 5 1501 --S partner cards list
|
||||||
|
! ? 3 1503 --S (card received)
|
||||||
|
! ? 3 1504 -C- (card accepted)
|
||||||
|
! C 4 1506 -C- (partner cards online status request)
|
||||||
|
! C 4 1507 --S (partner cards online status reply; whether cards are online or offline)
|
||||||
|
! ? 3 1508 -C- (edit partner card; self only?)
|
||||||
|
! ? 3 1509 --S (add card to the list)
|
||||||
|
! ? 4 150b -C- send simple mail
|
||||||
|
! ? 4 150d -C- (mark simple mail as read?)
|
||||||
|
! ? 4 150f --S receive simple mail
|
||||||
|
!* B 5 1512 --S blacklist cards list
|
||||||
|
! ? 2 1513 -C- (add to blacklist)
|
||||||
|
! ? 3 1514 -C- (send partner card)
|
||||||
|
! ? 4 1515 -C- (probably related to 1516)
|
||||||
|
? 4 1516 --S (probably related to 1515)
|
||||||
|
|
||||||
|
NPC characters
|
||||||
|
|
||||||
|
* A 4 1601 --S (probably npc AI parameters)
|
||||||
|
!* A 5 1602 --S player's available NPC and PM info
|
||||||
|
! ? 3 1603 -C- (receive npc card related; see tutorial)
|
||||||
|
! ? 2 1604 -C- (sort priority related)
|
||||||
|
! ? 3 1605 -C- (receive npc card related; see tutorial; sort priority related)
|
||||||
|
|
||||||
|
Counters
|
||||||
|
|
||||||
|
WIP ? 4 1701 -CS parties list request/reply
|
||||||
|
? 3 1702 -CS (probably join party request/reply)
|
||||||
|
OK B 4 1705 -C- party info recap request
|
||||||
|
!* B 4 1706 -CS party info recap change(C) reply(S)
|
||||||
|
OK C 4 1707 -C- menu event; select mission
|
||||||
|
OK B 4 1709 -C- party settings request
|
||||||
|
!* B 4 170a -CS party settings change(C) reply(S)
|
||||||
|
OK C 4 170b -C- counter background location points request
|
||||||
|
WIP C 4 170c --S counter background location points reply (9 of them total)
|
||||||
|
! ? 3 170d -C- (probably related to 170e)
|
||||||
|
? 3 170e --S (probably related to 170d)
|
||||||
|
? 4 170f --S (probably reply to 1707, but not all the time?)
|
||||||
|
OK C 4 1710 -C- counter background request
|
||||||
|
OK C 4 1711 --S counter background reply
|
||||||
|
|
||||||
|
???
|
||||||
|
|
||||||
|
! ? 2 1803 -C- (occurs in the AOTI story mission 1231022)
|
||||||
|
|
||||||
|
Trade
|
||||||
|
|
||||||
|
? 3 1901 -CS (trade request)
|
||||||
|
? 3 1902 --S
|
||||||
|
? 3 1903 -C-
|
||||||
|
? 3 1904 --S
|
||||||
|
|
||||||
|
Dialogs
|
||||||
|
|
||||||
|
OK A 5 1a01 -C- dialog request
|
||||||
|
OK B 4 1a02 --S dialog reply: standard
|
||||||
|
OK B 3 1a03 --S dialog reply: lumilass options
|
||||||
|
!* A 4 1a04 --S dialog reply: pp cube
|
||||||
|
! ? 3 1a05 -C- (lumilass: bought something)
|
||||||
|
OK B 4 1a07 --S dialog reply: types
|
||||||
|
? 3 1a08 -C- (shop delivery request)
|
||||||
|
? 3 1a09 --S (shop delivery reply)
|
||||||
|
|
||||||
|
Casino/Temple
|
||||||
|
|
||||||
|
! C 4 1d01 -C- (casino shop/coins request; also on casino shop buy)
|
||||||
|
! C 3 1d04 --S (casino shop related)
|
||||||
|
! C 3 1d05 --S (casino shop buy related)
|
||||||
|
! C 4 1d06 -C- (casino roulette bet request)
|
||||||
|
! C 5 1d07 --S (casino coin request and shop related)
|
||||||
|
! C 2 1d08 -C- (luck lady request)
|
||||||
|
! C 2 1d09 --S (luck lady reply)
|
||||||
|
! C 3 1d0a -C- (probably related to 1d0b; offering box)
|
||||||
|
! C 3 1d0b --S (probably related to 1d0a)
|
||||||
|
! C 3 1d0c -C- (casino enter; slot machine request)
|
||||||
|
! C 3 1d0d -C-
|
||||||
|
! C 4 1d0e -C-
|
||||||
|
! C 3 1d0f --S (reply to 1d0c?)
|
||||||
|
C 4 1d10 --S
|
||||||
|
C 3 1d11 --S
|
||||||
|
C 4 1d12 --S
|
||||||
|
C 3 1d13 --S
|
||||||
|
C 3 1d16 --S
|
||||||
|
C 3 1d17 --S (casino coin request related)
|
||||||
|
C 3 1d18 --S (casino coin request related)
|
||||||
|
|
||||||
|
13 WIP 85 OK 337 Total
|
||||||
|
|
||||||
|
! means the command has already been analyzed, at least partially
|
||||||
|
* means the command is already found somewhere in the server but needs to be reviewed
|
30
docs/protocol/commands/010b.txt
Normal file
30
docs/protocol/commands/010b.txt
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
010b
|
||||||
|
|
||||||
|
Size:32
|
||||||
|
Command:16 Channel:8 Garbage:8
|
||||||
|
FromLID:16 0:16
|
||||||
|
0:32
|
||||||
|
FromGID:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
FromGID:32
|
||||||
|
PartyPosOrLID:32
|
||||||
|
0:16 IntDir:16
|
||||||
|
PosX:32
|
||||||
|
PosY:32
|
||||||
|
PosZ:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
QuestID:32
|
||||||
|
ZoneID:32
|
||||||
|
MapID:32
|
||||||
|
EntryID:32
|
||||||
|
|
||||||
|
Size: 0x5c
|
||||||
|
Channel: 2
|
||||||
|
FromGID: should be the same in both the header and the body (even for NPC characters)
|
||||||
|
IntDir: character direction as a float converted to int:16
|
32
docs/protocol/commands/0111.txt
Normal file
32
docs/protocol/commands/0111.txt
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
0111
|
||||||
|
|
||||||
|
Size:32
|
||||||
|
Command:16 Channel:8 Garbage:8
|
||||||
|
DestLID:16 Garbage:16
|
||||||
|
0:32
|
||||||
|
TargetGID:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
DestTypeID:32
|
||||||
|
DestGID:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
TargetGID:32
|
||||||
|
TargetLID:32
|
||||||
|
EventID:32
|
||||||
|
Param:32
|
||||||
|
|
||||||
|
Size: 60
|
||||||
|
Channel: 3
|
||||||
|
TargetGID: player or npc
|
||||||
|
DestTypeID: 00011300
|
||||||
|
EventID: 2 3 4 5 6 7
|
||||||
|
Param: only for EventID 3 and 7
|
||||||
|
|
||||||
|
Events:
|
||||||
|
2: ? ; Param=0
|
||||||
|
3: red screen of death; Param: whether to ask if the player wants to go back to lobby, 0 or 1
|
||||||
|
4: healing pad aura + sound; Param=0
|
||||||
|
5: ? ; Param=0
|
||||||
|
6: ? ; Param=0 ; sent for lobby area change just before 010d
|
||||||
|
7: EXP FULL message; Param is the enemy TargetID that triggered this
|
40
docs/protocol/commands/0115.txt
Normal file
40
docs/protocol/commands/0115.txt
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
0115
|
||||||
|
|
||||||
|
Size:32
|
||||||
|
Command:16 Channel:8 Garbage:8
|
||||||
|
DestLID:16 Garbage:16
|
||||||
|
0:32
|
||||||
|
TargetGID:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
DestTypeID:32
|
||||||
|
DestGID:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
TargetGID:32
|
||||||
|
TargetLID:32
|
||||||
|
EnemyTargetID:32
|
||||||
|
Level:32
|
||||||
|
BlastBar:16 Luck:8 _UnknownJ:8
|
||||||
|
0:32
|
||||||
|
EXP:32
|
||||||
|
0:32
|
||||||
|
Money:32
|
||||||
|
PlayTime:32
|
||||||
|
for npc characters:
|
||||||
|
01000000:32 01000000:32 01000000:32 01000000:32 01000000:32 01000000:32 01000000:32 01000000:32
|
||||||
|
01000000:32 01000000:32 01000000:32 01000000:32 01000000:32 01000000:32 01000000:32 01000000:32
|
||||||
|
4e4f4630:32 08000000:32 00000000:32 00000000:32 4e454e44:32
|
||||||
|
for player characters:
|
||||||
|
_UnknownK:32 _UnknownL:32 0:32 0:32 0:32
|
||||||
|
ClassLevelAndEXP:32 ClassLevelAndEXP:32 ClassLevelAndEXP:32 ClassLevelAndEXP:32
|
||||||
|
ClassLevelAndEXP:32 ClassLevelAndEXP:32 ClassLevelAndEXP:32 ClassLevelAndEXP:32
|
||||||
|
ClassLevelAndEXP:32 ClassLevelAndEXP:32 ClassLevelAndEXP:32 ClassLevelAndEXP:32
|
||||||
|
ClassLevelAndEXP:32 ClassLevelAndEXP:32 ClassLevelAndEXP:32 ClassLevelAndEXP:32
|
||||||
|
|
||||||
|
Size: 168
|
||||||
|
Channel: 3
|
||||||
|
DestTypeID: 00011300
|
||||||
|
TargetGID: either player or npc
|
||||||
|
EnemyTargetID: presumably the TargetID of the enemy that got you leveled
|
||||||
|
Level and following: seems to take the same format as 0201, refer there for details
|
26
docs/protocol/commands/0117.txt
Normal file
26
docs/protocol/commands/0117.txt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
0117
|
||||||
|
|
||||||
|
Size:32
|
||||||
|
Command:16 Channel:8 Garbage:8
|
||||||
|
DestLID:16 Garbage:16
|
||||||
|
0:32
|
||||||
|
TargetGID:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
DestTypeID:32
|
||||||
|
DestGID:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
TargetGID:32
|
||||||
|
TargetLID:32
|
||||||
|
SE1:32 SE2:32
|
||||||
|
HP:32
|
||||||
|
0:32
|
||||||
|
|
||||||
|
Size: 68
|
||||||
|
Channel: 3
|
||||||
|
TargetGID: either player or npc
|
||||||
|
DestTypeID: 00011300
|
||||||
|
SE1: 00000000 or 00000200(stay down) or 00001000(no idea); SE part 1
|
||||||
|
SE2: various; SE part 2
|
||||||
|
HP: how many HP to revive the player with
|
26
docs/protocol/commands/0200.txt
Normal file
26
docs/protocol/commands/0200.txt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
0200
|
||||||
|
|
||||||
|
Size:32
|
||||||
|
Command:16 Channel:8 Garbage:8
|
||||||
|
LID:16 Garbage:16
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
TypeID:32
|
||||||
|
GID:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
LID:16 ZoneID:16
|
||||||
|
NbPlayers:32
|
||||||
|
Var:320
|
||||||
|
|
||||||
|
Size: always 5c
|
||||||
|
TypeID: always 00011300
|
||||||
|
LID: same as in both places; probably the player's new LID after entering this zone
|
||||||
|
ZoneID: 0 1 2 3 4 5 6 7 8 9 10 11 32; I now have doubt over whether this is a ZoneID
|
||||||
|
NbPlayers: many values from 0 to more than 255; number of players in this zone, starts at 1 (self) - LID is supposed to stay below that
|
||||||
|
Var: either of
|
||||||
|
ffffffff 00040000 00000000 00000000 00000000 00000000 00000000 00140000 ffffffff ffffffff
|
||||||
|
ffffffff 06000000 02000000 00000000 00000000 40000000 00010000 00010000 ffffffff ffffffff
|
||||||
|
ffffffff 06000500 01000000 00000000 00000000 00040000 00010000 00140000 ffffffff ffffffff
|
25
docs/protocol/commands/0202.txt
Normal file
25
docs/protocol/commands/0202.txt
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
0202
|
||||||
|
|
||||||
|
Size:32
|
||||||
|
Command:16 Channel:8 Garbage:8
|
||||||
|
ffff:16 0:16
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
SessionID:32
|
||||||
|
0:1024
|
||||||
|
|
||||||
|
Size: 0xb0
|
||||||
|
Channel: 3
|
||||||
|
SessionID: like GID except typically only temporary; apparently official use the same counter for NPCs and for these SessionID
|
||||||
|
0:1024: can also contain an error message, though details are limited on that; it's similar to 0223 though
|
||||||
|
|
||||||
|
The following format will display an error:
|
||||||
|
0:32, 3:32/little, 0:48, Len:16/little, Error/binary, 0:Padding
|
||||||
|
|
||||||
|
The server should disconnect the client right after sending that command if an error has been sent.
|
19
docs/protocol/commands/0203.txt
Normal file
19
docs/protocol/commands/0203.txt
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
0203
|
||||||
|
|
||||||
|
Size:32
|
||||||
|
Command:16 Channel:8 0:8
|
||||||
|
DestLID:16 0:16
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
DestTypeID:32
|
||||||
|
DestGID:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
TargetGID:32
|
||||||
|
TargetLID:32
|
||||||
|
|
||||||
|
Size: 52
|
||||||
|
Channel: 3
|
||||||
|
DestTypeID: 00011300
|
23
docs/protocol/commands/0204.txt
Normal file
23
docs/protocol/commands/0204.txt
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
0204
|
||||||
|
|
||||||
|
Size:32
|
||||||
|
Command:16 Channel:8 Garbage:8
|
||||||
|
DestLID:16 Garbage:16
|
||||||
|
CharTypeID:32
|
||||||
|
CharGID:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
DestTypeID:32
|
||||||
|
DestGID:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
CharGID:32
|
||||||
|
CharLID:32
|
||||||
|
_UnknownC:32
|
||||||
|
|
||||||
|
Size: 0x38
|
||||||
|
Channel: 3
|
||||||
|
CharTypeID: 00001200 for players, 00001d00 for NPCs
|
||||||
|
CharGID: both NPC and players
|
||||||
|
DestTypeID: 00011300
|
||||||
|
_UnknownC: seems to be an LID that isn't either of DestLID or CharLID; should be the total number of players in the zone after the removal
|
30
docs/protocol/commands/0205.txt
Normal file
30
docs/protocol/commands/0205.txt
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
0205
|
||||||
|
|
||||||
|
Size:32
|
||||||
|
Command:16 Channel:8 Garbage:8
|
||||||
|
LID:16 Garbage:16
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
TypeID:32
|
||||||
|
GID:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
ffffffff:32
|
||||||
|
ZoneID:32
|
||||||
|
MapID:32
|
||||||
|
EntryID:32
|
||||||
|
AreaNb:32
|
||||||
|
PlayerLID:16 _UnknownF:8 IsSeasonal:8
|
||||||
|
|
||||||
|
TypeID: always 00011300
|
||||||
|
ZoneID: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 20
|
||||||
|
MapID: 313 of them
|
||||||
|
EntryID: 0 1 2 3 4 5 6 7 8 10 11 12 13 14 20 21 22 23 32767
|
||||||
|
AreaNb: odd value, starts at 1, incremented after sending this command; replied by 0806
|
||||||
|
PlayerLID: LID of the related player
|
||||||
|
_UnknownF: sometimes 1
|
||||||
|
IsSeasonal: 0 otherwise
|
||||||
|
|
||||||
|
not sure how AreaNb works when 0205 is sent for spawning other players
|
19
docs/protocol/commands/0208.txt
Normal file
19
docs/protocol/commands/0208.txt
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
0208
|
||||||
|
|
||||||
|
Size:32
|
||||||
|
Command:16 Channel:8 Garbage:8
|
||||||
|
DestLID:16 Garbage:16
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
DestTypeID:32
|
||||||
|
DestGID:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
AreaNb:32
|
||||||
|
|
||||||
|
Size: 48
|
||||||
|
Channel: 3
|
||||||
|
DestTypeID: 00011300
|
||||||
|
AreaNb: even value, starts at 2 following the first 0205 sent, incremented after sending this command; replied by 0808
|
20
docs/protocol/commands/020b.txt
Normal file
20
docs/protocol/commands/020b.txt
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
020b
|
||||||
|
|
||||||
|
Size:32
|
||||||
|
Command:16 Channel:8 Garbage:8
|
||||||
|
ffff:16 0:16
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
Slot:32
|
||||||
|
0:8 BackToPreviousField:8 0:16
|
||||||
|
|
||||||
|
Size: 52
|
||||||
|
Channel: 2
|
||||||
|
Slot: character save slot
|
||||||
|
BackToPreviousField: if any, whether to go back to the previous field the character was in
|
16
docs/protocol/commands/020c.txt
Normal file
16
docs/protocol/commands/020c.txt
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
020c
|
||||||
|
|
||||||
|
Size:32
|
||||||
|
Command:16 Channel:8 Garbage:8
|
||||||
|
ffff:16 Garbage:16
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
|
||||||
|
Size: 0x2c
|
||||||
|
Channel: 2, despite being sent by the server
|
20
docs/protocol/commands/020d.txt
Normal file
20
docs/protocol/commands/020d.txt
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
020d
|
||||||
|
|
||||||
|
Size:32
|
||||||
|
Command:16 Channel:8 Garbage:8
|
||||||
|
ffff:16 0:16
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
AuthGID:32
|
||||||
|
AuthKey:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
|
||||||
|
Size: 60
|
||||||
|
Channel: 2
|
21
docs/protocol/commands/020e.txt
Normal file
21
docs/protocol/commands/020e.txt
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
020e
|
||||||
|
|
||||||
|
Size:32
|
||||||
|
Command:16 Channel:8 0:8
|
||||||
|
ffff:16 0:16
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
FileSize:32
|
||||||
|
_UnknownA:32
|
||||||
|
File/binary
|
||||||
|
0:32
|
||||||
|
|
||||||
|
Size: variable, depends on File
|
||||||
|
Channel: 3
|
||||||
|
_UnknownA: odd value but always in the same region; maybe the addition of all values in the file or something?
|
20
docs/protocol/commands/020f.txt
Normal file
20
docs/protocol/commands/020f.txt
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
020f
|
||||||
|
|
||||||
|
Size:32
|
||||||
|
Command:16 Channel:8 0:8
|
||||||
|
ffff:16 0:16
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
SetID:8 SeasonID:8 0:16
|
||||||
|
FileSize:32
|
||||||
|
File:N
|
||||||
|
|
||||||
|
Size: depends on FileSize
|
||||||
|
SetID: which set file to open (0 to N - 1)
|
||||||
|
SeasonID: what season to load, if any; 255 otherwise
|
19
docs/protocol/commands/0210.txt
Normal file
19
docs/protocol/commands/0210.txt
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
0210
|
||||||
|
|
||||||
|
Size:32
|
||||||
|
Command:16 Channel:8 Garbage:8
|
||||||
|
ffff:16 Garbage:16
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
TypeID:32
|
||||||
|
GID:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
UnixTime:32
|
||||||
|
|
||||||
|
Size: 52
|
||||||
|
TypeID: 00011300
|
||||||
|
UnixTime: current unix time
|
19
docs/protocol/commands/0215.txt
Normal file
19
docs/protocol/commands/0215.txt
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
0215
|
||||||
|
|
||||||
|
Size:32
|
||||||
|
Command:16 Channel:8 Garbage:8
|
||||||
|
LID:16 Garbage:16
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
TypeID:32
|
||||||
|
GID:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
_UnknownC:32
|
||||||
|
|
||||||
|
Size: 0x30
|
||||||
|
Channel: 3
|
||||||
|
TypeID: always 00011300
|
||||||
|
_UnknownC: 0 1 2 3 4 5 ffffffff (PartyPos? Leader's PartyPos?)
|
22
docs/protocol/commands/0216.txt
Normal file
22
docs/protocol/commands/0216.txt
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
0216
|
||||||
|
|
||||||
|
Size:32
|
||||||
|
Command:16 Channel:8 0:8
|
||||||
|
ffff:16 0:16
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
TypeID:32
|
||||||
|
GID:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
IP:32
|
||||||
|
Port:16 0:16
|
||||||
|
|
||||||
|
Size: 52
|
||||||
|
Channel: 3
|
||||||
|
TypeID: 00000f00
|
||||||
|
GID: the temporary SessionID
|
||||||
|
IP: standard binary format, as four 8 bit integers
|
||||||
|
Port: standard binary format
|
16
docs/protocol/commands/0217.txt
Normal file
16
docs/protocol/commands/0217.txt
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
0217
|
||||||
|
|
||||||
|
Size:32
|
||||||
|
Command:16 Channel:8 0:8
|
||||||
|
ffff:16 0:16
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
|
||||||
|
Size: 44
|
||||||
|
Channel: 2
|
21
docs/protocol/commands/0219.txt
Normal file
21
docs/protocol/commands/0219.txt
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
0219
|
||||||
|
|
||||||
|
Size:32
|
||||||
|
Command:16 Channel:8 Garbage:8
|
||||||
|
ffff:16 0:16
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
Username:192
|
||||||
|
Password:192
|
||||||
|
_UnknownC:64
|
||||||
|
|
||||||
|
Size: 100
|
||||||
|
Channel: 2
|
||||||
|
Username, Password: ASCII
|
||||||
|
_UnknownC: always 29525a52 7510439e?
|
17
docs/protocol/commands/021b.txt
Normal file
17
docs/protocol/commands/021b.txt
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
021b
|
||||||
|
|
||||||
|
Size:32
|
||||||
|
Command:16 Channel:8 Garbage:8
|
||||||
|
ffff:16 Garbage:16
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
TypeID:32
|
||||||
|
GID:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
|
||||||
|
Size: 44
|
||||||
|
Channel: 3
|
||||||
|
TypeID: 00011300
|
17
docs/protocol/commands/021c.txt
Normal file
17
docs/protocol/commands/021c.txt
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
021c
|
||||||
|
|
||||||
|
Size:32
|
||||||
|
Command:16 Channel:8 Garbage:8
|
||||||
|
FromLID:16 0:16
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
0:32
|
||||||
|
|
||||||
|
Size: 0x2c
|
||||||
|
Channel: 2
|
||||||
|
FromLID: ffff or LID
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user