diff --git a/.gitignore b/.gitignore index d176978..55bdd6f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -*.swp +*.sw* *.dump *.beam Mnesia.* +*.swo diff --git a/HOWTO b/HOWTO deleted file mode 100644 index 2d28ea8..0000000 --- a/HOWTO +++ /dev/null @@ -1,21 +0,0 @@ -PREREQUISITES: -python version 2.x set to default. - -INSTALL -1. Cd into directory where you to have the project -2. git clone git@github.com:jeena/GGS.git (remember to have a local key) -3. cd GGS/ -4. git submodule init -5. git submodule update -6. cd erlang_js -7. make -8. make test (Optional. It has to work though.) -10. cd ../ -11. ./build -12. - -USAGE -1. start a second terminal -2. in new terminal do command: ./python_client 9000 -3. back to first terminal -4. ./start diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d40790a --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +ERLC=erlc +ERLCFLAGS=-o +SRCDIR=src +TESTDIR=tests +BEAMDIR=ebin + +all: compile erlang_js + +compile: + mkdir -p $(BEAMDIR) ; + $(ERLC) $(ERLCFLAGS) $(BEAMDIR) $(SRCDIR)/*.erl ; + +erlang_js: force_look + cd erlang_js ; $(MAKE) $(MFLAGS); + +test: + echo "==> test $(MOD)" ; + mkdir -p $(BEAMDIR) ; +ifeq ($(strip $(MOD)),) + $(ERLC) $(ERLCFLAGS) $(BEAMDIR) $(TESTDIR)/*.erl ; + cd $(BEAMDIR) ; erl -noinput -eval 'eunit:test({dir, "."}, [verbose]), init:stop()' ; +else + $(ERLC) $(ERLCFLAGS) $(BEAMDIR) $(TESTDIR)/$(MOD)_test.erl ; + cd $(BEAMDIR) ; erl -noinput -eval 'eunit:test($(MOD)_test, [verbose]), init:stop()' ; +endif + +clean: + rm -rf $(BEAMDIR)/*.beam ; + rm -rf erl_crush.dump ; + echo "==> clean ggs" ; + $(MAKE) -C erlang_js/ clean + +run: + erl \ + -sname ggs \ + -mnesia dir '"/tmp/ggs"' \ + -boot start_sasl \ + -pa erlang_js/ebin/ \ + -pa ebin \ + -pa src \ + -s start_ggs + +force_look: + true \ No newline at end of file diff --git a/README b/README index 853cc89..9aa5357 100644 --- a/README +++ b/README @@ -1,3 +1,35 @@ GGS is a Generic Game Server -Check out http://ggs-kandidat.blogspot.com/ \ No newline at end of file +Check out http://ggs-kandidat.blogspot.com/ + +PREREQUISITES: +python version 2.x set to default. + +INSTALL +1. cd into directory where you to have the project +2. git clone git@github.com:jeena/GGS.git (remember to have a local key) +3. cd GGS/ +4. git submodule init +5. git submodule update + +USAGE +1. start a second terminal +2. in new terminal do command: ./python_client 9000 +3. back to first terminal +4. make run + +MAKE +To compile modules (even erlang_js): + make + +To run server: + make run + +To clean (even erlang_js): + make clean + +To compile and run all tests: + make test + +To compile and run one test: + make test MOD=ggs_modulename # (must have /tests/ggs_modulename_test.erl) diff --git a/build b/build deleted file mode 100755 index a9c1c79..0000000 --- a/build +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -for i in `find src -name "*.erl"` -do - erlc -o ebin $i -done \ No newline at end of file diff --git a/build_test b/build_test deleted file mode 100755 index 2f9d630..0000000 --- a/build_test +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -./build - -for i in `find tests -name "*.erl"` -do - erlc -o ebin_test $i -done diff --git a/client b/client deleted file mode 100644 index dff11aa..0000000 --- a/client +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env ruby -wKU - -require 'socket' # Sockets are in standard library - -hostname = 'localhost' -port = 7000 - -s = TCPSocket.open(hostname, port) - - - -s.print(q.chop) - -while line = s.gets # Read lines from the socket - puts "Got Echo: " + line.chop # And print with platform line terminator -end -s.close # Close the socket when done diff --git a/ebin/ggs.app b/ebin/ggs.app index 3315864..b3ea744 100644 --- a/ebin/ggs.app +++ b/ebin/ggs.app @@ -3,7 +3,8 @@ {vsn, "0.1.0"}, {modules, [ ggs_app, - ggs_sup + ggs_sup, + ggs_dispatcher ]}, {registered, [ggs_sup]}, {applications, [kernel, stdlib]}, diff --git a/echo_test b/echo_test deleted file mode 100755 index 1bbff1f..0000000 --- a/echo_test +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env ruby -wKU - -require 'socket' # Sockets are in standard library - -hostname = 'localhost' -port = 7000 - -print "Which port @ loclhost?" -port = gets - -s = TCPSocket.open(hostname, port.chop) - -s.print("__hello 0") - -while true - line = s.gets # Read lines from the socket - puts ">> " + line.chop # And print with platform line terminator - s.print(gets.chop) -end -s.close # Close the socket when done diff --git a/erlang_js b/erlang_js index 5350ed2..2f2785f 160000 --- a/erlang_js +++ b/erlang_js @@ -1 +1 @@ -Subproject commit 5350ed21606606dbee5ecb07e974f2abb9106270 +Subproject commit 2f2785fafb0da6db75810eb6fa97d09c58257588 diff --git a/games/GGSCalc/calc.glade b/games/GGSCalc/calc.glade new file mode 100644 index 0000000..720b7b4 --- /dev/null +++ b/games/GGSCalc/calc.glade @@ -0,0 +1,317 @@ + + + + + + + + True + + + True + True + + + + False + 0 + + + + + True + 5 + 4 + + + True + True + True + + + + + / + True + True + True + + + + 1 + 2 + + + + + * + True + True + True + + + + 2 + 3 + + + + + - + True + True + True + + + + 3 + 4 + + + + + 7 + True + True + True + + + + 1 + 2 + + + + + 8 + True + True + True + + + + 1 + 2 + 1 + 2 + + + + + 9 + True + True + True + + + + 2 + 3 + 1 + 2 + + + + + + + True + True + True + + + + 3 + 4 + 1 + 2 + + + + + 4 + True + True + True + + + + 2 + 3 + + + + + 5 + True + True + True + + + + 1 + 2 + 2 + 3 + + + + + 6 + True + True + True + + + + 2 + 3 + 2 + 3 + + + + + + True + True + True + + + + 3 + 4 + 2 + 3 + + + + + 1 + True + True + True + + + + 3 + 4 + + + + + 2 + True + True + True + + + + 1 + 2 + 3 + 4 + + + + + 3 + True + True + True + + + + 2 + 3 + 3 + 4 + + + + + = + True + True + True + + + + 3 + 4 + 3 + 4 + + + + + 0 + True + True + True + + + + 4 + 5 + + + + + True + True + True + + + 1 + 2 + 4 + 5 + + + + + True + True + True + + + 2 + 3 + 4 + 5 + + + + + + True + True + True + + + + 3 + 4 + 4 + 5 + + + + + 1 + + + + + True + 2 + + + False + 2 + + + + + + diff --git a/games/GGSCalc/calc.py b/games/GGSCalc/calc.py new file mode 100644 index 0000000..8468ceb --- /dev/null +++ b/games/GGSCalc/calc.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python + +import sys, socket +try: + import pygtk + pygtk.require("2.16") +except: + pass +try: + import gtk + import gtk.glade +except: + sys.exit(1) + +class GGSCalc: + + def __init__(self): + #Set the Glade file + self.gladefile = "calc.glade" + self.wTree = gtk.glade.XML(self.gladefile, "window1") + + #Create our dictionay and connect it + dic = {"on_mainWindow_destroy" : gtk.main_quit + , "on_btn0_clicked" : lambda x: self.OnBtnClick(0) + , "on_btn1_clicked" : lambda x: self.OnBtnClick(1) + , "on_btn2_clicked" : lambda x: self.OnBtnClick(2) + , "on_btn3_clicked" : lambda x: self.OnBtnClick(3) + , "on_btn4_clicked" : lambda x: self.OnBtnClick(4) + , "on_btn5_clicked" : lambda x: self.OnBtnClick(5) + , "on_btn6_clicked" : lambda x: self.OnBtnClick(6) + , "on_btn7_clicked" : lambda x: self.OnBtnClick(7) + , "on_btn8_clicked" : lambda x: self.OnBtnClick(8) + , "on_btn9_clicked" : lambda x: self.OnBtnClick(9) + , "on_btnDiv_clicked" : lambda x: self.OnBtnClick("/") + , "on_btnMul_clicked" : lambda x: self.OnBtnClick("*") + , "on_btnMin_clicked" : lambda x: self.OnBtnClick("-") + , "on_btnPlus_clicked" : lambda x: self.OnBtnClick("+") + , "on_btnEq_clicked" : lambda x: self.calc() + , "on_btnDel_clicked" : lambda x: self.OnBtnClick("Del") + , "on_btnConnect_clicked" : lambda x: self.connect() + } + + for i in range(0,9): + dic + self.wTree.signal_autoconnect(dic) + + self.wTree.get_widget("window1").show() + self.setStatus("Not connected") + + def setStatus(self, msg): + self.wTree.get_widget("statusbar").push(0, msg) + + def calc(self): + exp = self.wTree.get_widget("txtCalc").get_text() + self.s.send("Server-Command: call\n"+ + "Token: %s\n" % self.token + + "Content-Type: text\n"+ + "Content-Length: %s\n" % len(exp)+ + "\n"+ + exp) + fs = self.s.makefile() + self.wTree.get_widget("txtCalc").set_text(fs.readline().split(" ")[1]) + + + def connect(self): + print "Connecting" + self.setStatus("Connecting") + HOST = 'localhost' # The remote host + PORT = 9000 # The same port as used by the server + self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.s.connect((HOST, PORT)) + self.s.send("Server-Command: hello\n"+ + "Content-Type: text\n"+ + "Content-Length: 0\n"+ + "\n") + fs = self.s.makefile() + self.token = fs.readline().split(" ")[0] + self.setStatus("Connected!") + + def OnBtnClick(self, btn): + calcTxt = self.wTree.get_widget("txtCalc") + t = calcTxt.get_text() + if btn == "+": + t += "+" + elif btn == "-": + t += "-" + elif btn == "/": + t += "/" + elif btn == "=": + t += "=" + elif btn == "*": + t += "*" + elif btn == "Del": + t = t[:-1] + else: + t += str("\""+str(btn)+"\"") + calcTxt.set_text(t) + +if __name__ == "__main__": + calc = GGSCalc() + gtk.main() diff --git a/games/GGSChat/calc.glade b/games/GGSChat/calc.glade new file mode 100644 index 0000000..720b7b4 --- /dev/null +++ b/games/GGSChat/calc.glade @@ -0,0 +1,317 @@ + + + + + + + + True + + + True + True + + + + False + 0 + + + + + True + 5 + 4 + + + True + True + True + + + + + / + True + True + True + + + + 1 + 2 + + + + + * + True + True + True + + + + 2 + 3 + + + + + - + True + True + True + + + + 3 + 4 + + + + + 7 + True + True + True + + + + 1 + 2 + + + + + 8 + True + True + True + + + + 1 + 2 + 1 + 2 + + + + + 9 + True + True + True + + + + 2 + 3 + 1 + 2 + + + + + + + True + True + True + + + + 3 + 4 + 1 + 2 + + + + + 4 + True + True + True + + + + 2 + 3 + + + + + 5 + True + True + True + + + + 1 + 2 + 2 + 3 + + + + + 6 + True + True + True + + + + 2 + 3 + 2 + 3 + + + + + + True + True + True + + + + 3 + 4 + 2 + 3 + + + + + 1 + True + True + True + + + + 3 + 4 + + + + + 2 + True + True + True + + + + 1 + 2 + 3 + 4 + + + + + 3 + True + True + True + + + + 2 + 3 + 3 + 4 + + + + + = + True + True + True + + + + 3 + 4 + 3 + 4 + + + + + 0 + True + True + True + + + + 4 + 5 + + + + + True + True + True + + + 1 + 2 + 4 + 5 + + + + + True + True + True + + + 2 + 3 + 4 + 5 + + + + + + True + True + True + + + + 3 + 4 + 4 + 5 + + + + + 1 + + + + + True + 2 + + + False + 2 + + + + + + diff --git a/games/GGSChat/chat.py b/games/GGSChat/chat.py new file mode 100644 index 0000000..17c30ea --- /dev/null +++ b/games/GGSChat/chat.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python + +import sys, socket, thread, gobject, getpass +try: + import pygtk + pygtk.require("2.16") +except: + pass +try: + import gtk + import gtk.glade +except: + sys.exit(1) + +class GGSChat: + + def __init__(self,host, port): + #Set the Glade file + self.gladefile = "ggschat.glade" + self.wTree = gtk.glade.XML(self.gladefile, "window1") + + self.setStatus("Not connected") + self.connect(host, port) + thread.start_new_thread(self.listenChat, ()) + #Create our dictionay and connect it + dic = {"on_window1_destroy_event" : gtk.main_quit + , "on_sendButton_clicked" : lambda x: self.chat() + , "on_entry_activate" : lambda x : self.chat() + , "on_chatBox_focus" : lambda x, y: self.wTree.get_widget("entry").grab_focus() + } + + self.wTree.signal_autoconnect(dic) + + self.wTree.get_widget("nickBox").set_text(getpass.getuser()) + self.wTree.get_widget("window1").show() + self.wTree.get_widget("entry").grab_focus() + + def setStatus(self, msg): + self.wTree.get_widget("statusbar").push(0, msg) + + def chat(self): + exp = self.wTree.get_widget("entry").get_text() + nick = self.wTree.get_widget("nickBox").get_text() + if exp[0] == "/": + cmdStr = exp[1:].split(" ") + cmd = cmdStr[0] + params = ' '.join(cmdStr[1:]) + self.s.send("Game-Command: %s\n" % exp[1:] + + "Token: %s\n" % self.token + + "Content-Type: text\n" + + "Content-Length: %s\n" % len(params)+ + "\n"+ + params) + else: + exp = "<%s> %s" % (nick, exp) + self.s.send("Game-Command: chat\n"+ + "Token: %s\n" % self.token + + "Content-Type: text\n"+ + "Content-Length: %s\n" % (len(exp))+ + "\n"+ + exp+"\n") + self.wTree.get_widget("entry").set_text("") + #self.listenChat() + + + def connect(self, host,port): + print "Connecting" + self.setStatus("Connecting") + HOST = host # The remote host + PORT = port # The same port as used by the server + self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.s.connect((HOST, PORT)) + self.token = self.s.recv(1024) + self.setStatus("Connected!") + + def listenChat(self): + print "listening" + fs = self.s.makefile() + while True: + line = fs.readline() + print "Received: ", line + gobject.idle_add(self.updateChatText, line) + + def updateChatText(self, text): + self.wTree.get_widget("chatBox").get_buffer().insert_at_cursor(text) +if __name__ == "__main__": + host = "localhost" + port = 9000 + if len(sys.argv) >= 2: + host = sys.argv[1] + port = int(sys.argv[2]) + chat = GGSChat(host, port) + gobject.threads_init() + gtk.main() diff --git a/games/GGSChat/ggschat.glade b/games/GGSChat/ggschat.glade new file mode 100644 index 0000000..ac4e9cb --- /dev/null +++ b/games/GGSChat/ggschat.glade @@ -0,0 +1,92 @@ + + + + + + 500 + 500 + + + + True + + + True + + + True + True + False + + + + 0 + + + + + 0 + + + + + True + + + True + True + + 10 + Anonymous + + + False + 0 + + + + + True + True + + + + + 1 + + + + + Chat! + True + True + True + + + + False + False + 2 + + + + + False + False + 1 + + + + + True + 2 + + + False + 2 + + + + + + diff --git a/mnesia/.gamedb.erl.swp b/mnesia/.gamedb.erl.swp deleted file mode 100644 index 469b1f8..0000000 Binary files a/mnesia/.gamedb.erl.swp and /dev/null differ diff --git a/mnesia/gamedb.erl b/mnesia/gamedb.erl deleted file mode 100644 index 751eb92..0000000 --- a/mnesia/gamedb.erl +++ /dev/null @@ -1,50 +0,0 @@ -%%%%---------------------------------------------------- -%%% @author Mattias Pettersson -%%% @copyright 2011 Mattias Pettersson -%%% @doc Database for runtime game variable storage. -%%% @end - - Test Mnesia --module(gamedb). --import(mnesia). --export([init/0,insert_player/1,example_player/0,read_player/1,test_player/0]). --include("gamedb.hrl"). - -%%----------------------------------------------------- -%% Creation -%%----------------------------------------------------- -init() -> - mnesia:create_table(player, [{attributes, record_info(fields, player)}]). - -%%----------------------------------------------------- -%% Test -%%----------------------------------------------------- -test_player() -> - insert_player(example_player()), - read_player(0001). - -example_player() -> - #player{id = 0001, - name = "Tux"}. - -%%----------------------------------------------------- -%% Insertions -%%----------------------------------------------------- -insert_player(Player) -> - Fun = fun() -> - mnesia:write(Player) - end, - mnesia:transaction(Fun). - - -%%----------------------------------------------------- -%% Querries -%%----------------------------------------------------- -read_player(Player_Key) -> - Fun = fun() -> - [P] = mnesia:read(player, Player_Key), - Name = P#player.name, - io:format("Player name: ~s~n",[Name]) - end, - mnesia:transaction(Fun). - diff --git a/mnesia/gamedb.hrl b/mnesia/gamedb.hrl deleted file mode 100644 index 1ae9c8f..0000000 --- a/mnesia/gamedb.hrl +++ /dev/null @@ -1,6 +0,0 @@ -%% gamedb.hrl - --record(player, {id, name}). - - - diff --git a/mnesia/gamedb_usage.txt b/mnesia/gamedb_usage.txt deleted file mode 100644 index b3a07a0..0000000 --- a/mnesia/gamedb_usage.txt +++ /dev/null @@ -1,12 +0,0 @@ -1. From terminal: erl -mnesia dir '"/home/user/dir/to/GGS/GameDB.Player"' -2. mnesia:create_schema([node()]). -3. mnesia:start(). -4. c(gamedb). -5. gamedb:init(). -6. mnesia:info(). -7. gamedb:test_player(). - -Last output should be: - Player name: Tux - {atomic,ok} - diff --git a/python_client b/python_client index 3db0751..50e8dbd 100755 --- a/python_client +++ b/python_client @@ -7,50 +7,56 @@ PORT = int(sys.argv[1]) # The same port as used by the server s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) -# Say hello +# Define ourselves a function! +token = s.recv(1024) -print "Saying hello to server" +#print "Defining a function called myFun" +#s.send( +#"Token: %s\n\ +#Server-Command: define\n\ +#Content-Type: text\n\ +#Content-Length: 49\n\ +#\n\ +#function myFun() {return 'Hello World!' ;}" % token) +#fs = s.makefile() +#data = fs.readline() +#print "Token:", token +#print "Data: ", ' '.join(data.split(" ")[1:]) + +# Call that function! +fs = s.makefile() +print "Token: ", token s.send( -"Server-Command: hello\n\ +"Token: %s\n\ +Game-Command: greet\n\ Content-Type: text\n\ Content-Length: 0\n\ \n\ -") -fs = s.makefile() -data = fs.readline() -token = data.split(" ")[0] -print "Token:", token -print "Data: ", ' '.join(data.split(" ")[1:]) +" % token) +time.sleep(1) -# Define ourselves a function! - -print "Defining a function called myFun" s.send( "Token: %s\n\ -Server-Command: define\n\ +Game-Command: uname\n\ Content-Type: text\n\ -Content-Length: 49\n\ +Content-Length: 0\n\ \n\ -function myFun() {return 'Hello World!' ;}" % token) -fs = s.makefile() -data = fs.readline() -print "Token:", token -print "Data: ", ' '.join(data.split(" ")[1:]) +" % token) +time.sleep(1) -# Call that function! - -print "Calling myFun" s.send( "Token: %s\n\ -Server-Command: call\n\ +Game-Command: chat\n\ Content-Type: text\n\ -Content-Length: 6\n\ +Content-Length: 23\n\ \n\ -myFun" % token) -fs = s.makefile() -data = fs.readline() -print "Token:", token -print "Data: ", ' '.join(data.split(" ")[1:]) +Hello guys, what's up?\n" % token) +time.sleep(1) + + +while True: + data = fs.readline() + print "Data: ", data s.close() diff --git a/src/.ggs_connection.erl.swp b/src/.ggs_connection.erl.swp deleted file mode 100644 index 0c009f8..0000000 Binary files a/src/.ggs_connection.erl.swp and /dev/null differ diff --git a/src/.ggs_server.erl.swo b/src/.ggs_server.erl.swo deleted file mode 100644 index 3048659..0000000 Binary files a/src/.ggs_server.erl.swo and /dev/null differ diff --git a/src/ggs_backup.erl b/src/ggs_backup.erl deleted file mode 100644 index 30c80a2..0000000 --- a/src/ggs_backup.erl +++ /dev/null @@ -1,41 +0,0 @@ --module(ggs_backup). --behaviour(gen_server). - -%% API --export([start_link/0 ]). - -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). - - --define(SERVER, ?MODULE). - --record(state, {port, lsock, client_vm_map = []}). - -start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). - -init([]) -> - {ok, #state{port = -1, lsock = -1, client_vm_map = -1}, 0}. - -handle_call(get_backup, _From, State) -> - BackedUpState = case State of - #state{port = -1, lsock = -1, client_vm_map = -1} -> - not_initialized; - Other -> - Other - end, - {reply, {backup_state, BackedUpState}, State}. - -handle_cast({set_backup, NewState}, _State) -> - {noreply, NewState}. - -handle_info(_Msg, State) -> - {noreply, State}. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -terminate(_Reason, _State) -> - ok. diff --git a/src/ggs_coordinator.erl b/src/ggs_coordinator.erl new file mode 100644 index 0000000..48b4639 --- /dev/null +++ b/src/ggs_coordinator.erl @@ -0,0 +1,92 @@ +-module(ggs_coordinator). + +%% API Exports +-export([start_link/0, stop/1, join_table/1, create_table/1, join_lobby/0, + respawn_player/2, respawn_table/1, remove_player/2]). + +%% gen_server callback exports +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). +-define(SERVER, ?MODULE). + +-record(co_state, + {players = [], % List of all player processes + player_table_map = [], % Players <-> Table map + table_state_map = [], + tables = []}). % Table <-> Table state map + +%% @doc This module act as "the man in the middle". +%% Creates the starting connection between table and players. + +%% @doc Starts the coordinator process. +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +%% @doc Terminates the coordinator process. +stop(Reason) -> + gen_server:cast(ggs_coordinator, {stop, Reason}). + +%% @doc Joins table with specified token, returns {error, no_such_table} +%% if the specified table token does not exist +join_lobby() -> + gen_server:call(ggs_coordinator, join_lobby). + +%% @doc Act as a supervisor to player and respawns player when it gets bad data. +respawn_player(_Player, _Socket) -> + ggs_logger:not_implemented(). + +%% @doc Act as a supervisor to table and respawns table when it gets bad data. +respawn_table(_Token) -> + ggs_logger:not_implemented(). + +%% @doc Removes a player from coordinator. +remove_player(_From, _Player) -> + %gen_server:cast(ggs_coordinator, {remove_player, Player}). + ggs_logger:not_implemented(). + +%% gen_server callbacks + +init([]) -> + {ok, #co_state{}}. + +handle_call(join_lobby, _From, State) -> + Token = helpers:get_new_token(), + {reply, {ok, Token}, State}; + +handle_call({join_table, Table}, From, State) -> + {FromPlayer, _Ref} = From, + Tables = State#co_state.tables, + case lists:keyfind(Table, 1, Tables) of + {_TableID, TablePID} -> + ggs_table:add_player(TablePID, FromPlayer), + {reply, {ok, TablePID}, State}; + false -> + {reply, {error, no_such_table}, State} + end; + +handle_call({create_table, {force, TableID}}, From, State) -> + TableIDMap = State#co_state.player_table_map, + Tables = State#co_state.tables, + NewTableProc = ggs_table:start_link(), + {reply, {ok, TableID}, State#co_state{ + player_table_map = [{From, TableID} | TableIDMap], + tables = [{TableID, NewTableProc} | Tables] + }}; + +handle_call(_Message, _From, State) -> + {noreply, State}. + +handle_cast({stop, _Reason}, _State) -> + {stop, normal, state}; + +handle_cast(_Message, State) -> + {noreply, State}. + +handle_info(_Message, State) -> + {noreply, State}. + +terminate(normal, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. diff --git a/src/ggs_db.erl b/src/ggs_db.erl new file mode 100644 index 0000000..29665a3 --- /dev/null +++ b/src/ggs_db.erl @@ -0,0 +1,91 @@ +%%%%---------------------------------------------------- +%%% @author Mattias Pettersson +%%% @copyright 2011 Mattias Pettersson +%%% @doc Database for runtime game variable storage. +%%% @end + +-module(ggs_db). +-export([init/0,stop/0,setItem/4,getItem/3,removeItem/3,key/3,clear/2,clear/1,length/2]). +%-include("ggs_db.hrl"). +-record(data, {key, value}). + +%%----------------------------------------------------- +%% Creation +%%----------------------------------------------------- +init() -> +% mnesia:create_schema([node()]), + mnesia:start(), + mnesia:create_table(data, [{attributes, record_info(fields, data)}]). + +stop() -> + mnesia:stop(). + + +%%----------------------------------------------------- +%% Insertions +%%----------------------------------------------------- +setItem(GameToken,Ns,Key,Value) -> + Fun = fun() -> + Data = #data{key = {GameToken,Ns,Key}, value = Value}, + mnesia:write(Data) + end, + mnesia:transaction(Fun). + + +%%----------------------------------------------------- +%% Deletions +%%----------------------------------------------------- +removeItem(GameToken,Ns,Key) -> + Fun = fun() -> + mnesia:delete({data,{GameToken,Ns,Key}}) + end, + mnesia:transaction(Fun). + +clear(GameToken,Ns) -> + Fun = fun() -> + Keys = mnesia:all_keys(data), + Rest = lists:filter(fun({A,B,_}) -> ((A==GameToken) and (B==Ns)) end, Keys), + lists:map(fun({A,B,C}) -> removeItem(A,B,C) end, Rest) + end, + {atomic, Ret} = mnesia:transaction(Fun), + Ret. + +clear(GameToken) -> + Fun = fun() -> + Keys = mnesia:all_keys(data), + Rest = lists:filter(fun({A,_,_}) -> (A==GameToken) end, Keys), + lists:map(fun({A,B,C}) -> removeItem(A,B,C) end, Rest) + end, + {atomic, Ret} = mnesia:transaction(Fun), + Ret. + +%%----------------------------------------------------- +%% Queries +%%----------------------------------------------------- +getItem(GameToken,Ns,Key) -> + Fun = fun() -> + mnesia:read(data, {GameToken,Ns,Key}) + end, + case mnesia:transaction(Fun) of + {atomic, []} -> + {error}; + {atomic, [Ret]} -> + Ret#data.value +end. + +length(GameToken,Ns) -> + Fun = fun() -> + Keys = mnesia:all_keys(data), + length(lists:filter(fun({A,B,_}) -> ((A==GameToken) and (B==Ns)) end, Keys)) + end, + {atomic, Ret} = mnesia:transaction(Fun), + Ret. + +key(GameToken,Ns,Position) -> + Fun = fun() -> + Keys = mnesia:all_keys(data), + Rest = lists:filter(fun({A,B,_}) -> ((A==GameToken) and (B==Ns)) end, Keys), + lists:nth(Position, Rest) + end, + {atomic, Ret} = mnesia:transaction(Fun), + Ret. diff --git a/src/ggs_db.hrl b/src/ggs_db.hrl new file mode 100644 index 0000000..b7f1f4c --- /dev/null +++ b/src/ggs_db.hrl @@ -0,0 +1,5 @@ +%% gamedb.hrl + +-record(data, {key, value}). + + diff --git a/src/ggs_dispatcher.erl b/src/ggs_dispatcher.erl new file mode 100644 index 0000000..11dd729 --- /dev/null +++ b/src/ggs_dispatcher.erl @@ -0,0 +1,68 @@ +-module(ggs_dispatcher). + +-behaviour(gen_server). + +%% API Exports +-export([start_link/1, stop/1]). + +%% gen_server callback exports +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). + +-define(SERVER, ?MODULE). + + +%% @doc This module is the entry-point for clients connecting to GGS. This is +%% the module responsible for: +%% * Greeting a connecting client, and associating a socket for it +%% * Spawning a ggs_player for the connecting client, passing the socket + +%% @doc Starts a new dispatcher with the specified port. Registers this +%% dispatcher under the name "ggs_dispatcher". The pid of the dispatcher +%% is returned. +%% @spec start_link(Port) -> Pid +%% Port = Integer +%% Pid = # +start_link(Port) -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [Port], []). + +%% @doc Stops the dispatcher with the specified reason. +%% @spec stop(Reason) -> ok. +%% Reason = String +stop(_Reason) -> ggs_logger:not_implemented(). + +%% gen_server callbacks + +%% @doc Initiate the dispatcher. This is called from gen_server +init([Port]) -> + {ok, LSock} = gen_tcp:listen(Port, [{active, true}, + {reuseaddr, true}]), + {ok, LSock, 0}. + +handle_call(_Message, _From, State) -> + {noreply, State}. + +handle_cast(_Message, State) -> + {noreply, State}. + +handle_info({tcp, _Socket, _RawData}, State) -> + io:format("Got connect request!~n"), + {noreply, State}; + +handle_info({tcp_closed, Socket}, State) -> + gen_tcp:close(Socket), + {stop, "Client closed socket", State}; + +%% @doc This is our function for accepting connections. When a client connects, +%% it will immediately time out due to timing settings set in init and here, +%% and when it does, we accept the connection. +handle_info(timeout, LSock) -> + {ok, Sock} = gen_tcp:accept(LSock), + spawn(ggs_player, start_link, [Sock]), + {noreply, LSock, 0}. + +terminate(normal, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. diff --git a/src/ggs_gamevm.erl b/src/ggs_gamevm.erl new file mode 100644 index 0000000..babee27 --- /dev/null +++ b/src/ggs_gamevm.erl @@ -0,0 +1,99 @@ +%% @doc This module is responsible for running the game VM:s. You can issue +%% commands to a vm using this module. + +-module(ggs_gamevm). +-behaviour(gen_server). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-record(state, { port, table } ). + +%% API +-export([start_link/1, define/2, user_command/4, stop/1, call_js/2]). + +-include_lib("eunit/include/eunit.hrl"). + + +%% ---------------------------------------------------------------------- +% API implementation + +%% @doc Create a new VM process. The process ID is returned and can be used +%% with for example the define method of this module. +start_link(Table) -> + erlang_js:start(), %% @TODO: should only be done once + {ok, Pid} = gen_server:start_link(?MODULE, [Table], []), + Pid. + +%% @doc Define some new code on the specified VM, returns the atom ok. +define(GameVM, SourceCode) -> + gen_server:cast(GameVM, {define, SourceCode}). + +%% @doc Execute a user command on the specified VM. This function is +%% asynchronous, and returns ok. +%% @spec user_command(GameVM, User, Command, Args) -> ok +%% GameVM = process IS of VM +%% Player = the player running the command +%% Command = a game command to run +%% Args = arguments for the Command parameter +user_command(GameVM, Player, Command, Args) -> + gen_server:cast(GameVM, {user_command, Player, Command, Args}). + +%% @private +% only for tests +call_js(GameVM, SourceCode) -> + gen_server:call(GameVM, {eval, SourceCode}). + +% @doc stops the gamevm process +stop(GameVM) -> + gen_server:cast(GameVM, stop). + + +%% ---------------------------------------------------------------------- + +%% @private +init([Table]) -> + process_flag(trap_exit, true), + {ok, Port} = js_driver:new(), + %% @TODO: add here default JS API instead + {ok, #state { port = Port, table = Table }}. + +%% private +% only needed for the tests +handle_call({eval, SourceCode}, _From, #state { port = Port } = State) -> + {ok, Ret} = js:eval(Port, list_to_binary(SourceCode)), + {reply, Ret, State}. + +%% @private +handle_cast({define, SourceCode}, #state { port = Port } = State) -> + ok = js:define(Port, list_to_binary(SourceCode)), + {noreply, State}; +handle_cast({user_command, Player, Command, Args}, #state { port = Port } = State) -> + Arguments = string:concat("'", string:concat( + string:join([js_escape(Player), js_escape(Command), js_escape(Args)], "','"), "'")), + Js = list_to_binary(string:concat(string:concat("userCommand(", Arguments), ");")), + js_driver:define_js(Port, Js), + {noreply, State}; +handle_cast(stop, State) -> + {stop, normal, State}; +handle_cast(Msg, S) -> + error_logger:error_report([unknown_msg, Msg]), + {noreply, S}. + +%% @private +handle_info(Msg, S) -> + error_logger:error_report([unknown_msg, Msg]), + {noreply, S}. + +%% @private +terminate(_Reason, _State) -> + ok. + +%% @private +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +js_escape(S) -> + lists:flatmap(fun($\') -> [$\\, $\']; (X) -> [X] end, S). + diff --git a/src/ggs_gamevm_e.erl b/src/ggs_gamevm_e.erl new file mode 100644 index 0000000..3cc6b17 --- /dev/null +++ b/src/ggs_gamevm_e.erl @@ -0,0 +1,60 @@ +-module(ggs_gamevm_e). +-export([start_link/1, define/2, user_command/4]). +%% @doc This module is responsible for running the game VM:s. You can issue +%% commands to a vm using this module. + +%% @doc Create a new VM process. The process ID is returned and can be used +%% with for example the define method of this module. +start_link(Table) -> + PortPid = spawn( fun() -> + loop(Table) + end ), + PortPid. + +%% @doc Define some new code on the specified VM, returns the atom ok. +define(GameVM, SourceCode) -> + GameVM ! {define,SourceCode}, + ok. + +%% @doc Execute a user command on the specified VM. This function is +%% asynchronous, and returns ok. +%% @spec user_command(GameVM, User, Command, Args) -> ok +%% GameVM = process IS of VM +%% Player = the player running the command +%% Command = a game command to run +%% Args = arguments for the Command parameter +user_command(GameVM, Player, Command, Args) -> + Ref = make_ref(), + GameVM ! {user_command, Player, Command, Args, self(), Ref}, + ok. + +%% Helper functions + +loop(Table) -> + receive + {define, _SourceCode} -> + io:format("GameVM_e can't define functions, sorry!~n"), + loop(Table); + {user_command, Player, Command, Args, _From, _Ref} -> + erlang:display(Command), + do_stuff(Command, Args, Player, Table), + loop(Table) + end. + +do_stuff(Command, Args, Player, Table) -> + case Command of + "greet" -> + ggs_player:notify(Player, server, "Hello there!\n"); + "chat" -> + ggs_table:notify_all_players(Table, Args ++ "\n"); + "uname" -> + Uname = os:cmd("uname -a"), + ggs_player:notify(Player, server, Uname); + "lusers" -> + {ok, Players} = ggs_table:get_player_list(Table), + ggs_player:notify(Player, server,io_lib:format("~p\n",[Players])); + "nick" -> + io:format("Changing nickname of ~p to ~p.", [Player, Args]); + _Other -> + ggs_player:notify(Player, server, "I don't know that command..\n") + end. diff --git a/src/ggs_logger.erl b/src/ggs_logger.erl new file mode 100644 index 0000000..49f5abc --- /dev/null +++ b/src/ggs_logger.erl @@ -0,0 +1,8 @@ +-module(ggs_logger). +-export([not_implemented/0, log/2]). + +not_implemented() -> + exit(not_implemented). + +log(Format, Args) -> + error_logger:info_msg(Format, Args). diff --git a/src/ggs_mnesia_controller_server.erl b/src/ggs_mnesia_controller_server.erl deleted file mode 100644 index c1f8a10..0000000 --- a/src/ggs_mnesia_controller_server.erl +++ /dev/null @@ -1,68 +0,0 @@ --module(ggs_mnesia_controller_server). --behaviour(gen_server). - -%% API --export([start_link/0, - stop/0 - ]). - -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). - - --define(SERVER, ?MODULE). - --record(state, {}). - -%%%==================================================== -%%% API -%%%==================================================== - -%%----------------------------------------------------- -%% @doc Starts the server -%% @end -%%----------------------------------------------------- -start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). - -%%----------------------------------------------------- -%% @doc Stops the server. -%% @spec stop() -> ok -%% @end -%%----------------------------------------------------- -stop() -> - gen_server:cast(?SERVER, stop). - -%%----------------------------------------------------- -%% gen_server callbacks -%%----------------------------------------------------- - -init([]) -> - mnesia:create_schema([node()]), - mnesia:start(), - {ok, {}, 0}. - -handle_cast(a, State) -> - {noreply, State}. - -% Request a value from the Mnesia database -handle_call({getValue, _Key},_From,State) -> - {reply,value_of_key_requested_goes_here, State}; - -% Set a value in the Mnesia database -handle_call({setValue, _Key, Value},_From,State) -> - {reply,value_set_or_updated, State}. - -handle_info(timeout, State) -> - {noreply, State}. - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%----------------------------------------------------- -%% Internal functions -%%----------------------------------------------------- diff --git a/src/ggs_network.erl b/src/ggs_network.erl deleted file mode 100644 index be1b9d1..0000000 --- a/src/ggs_network.erl +++ /dev/null @@ -1,170 +0,0 @@ -%%%---------------------------------------------------- -%%% @author Jonatan Plsson -%%% @copyright 2010 Jonatan Plsson -%%% @doc RPC over TCP server -%%% @end -%%%---------------------------------------------------- -%%% @author Mattias Pettersson -%%% @doc Socket module for GGS -%%% @end -%%%---------------------------------------------------- - - --module(ggs_network). - --behaviour(gen_server). - -%define --define(SERVER, ?MODULE). --define(DEFAULT_PORT, 1055). - - -% export --export([start_link/0,start_link/1]). --export([init/1, handle_call/3, handle_cast/2, handle_info/2]). -%-export([get_count/1,crash/0,vms/0,hello/0,echo/0]). --export([get_count/1]). --export([send/3, send/4]). --export([stop/0]). - -%% gen_server callbacks --export([terminate/2, code_change/3]). - -%state --record(state, {port, lsock, client_vm_map = []}). - - -%%----------------------------------------------------- -%% @doc Starts gen_server -%% @end -%%----------------------------------------------------- -start_link() -> - start_link(?DEFAULT_PORT). - -start_link(Port) -> - process_flag(trap_exit, true), - gen_server:start_link({local, ?SERVER}, ?MODULE, [Port], []). - -%%----------------------------------------------------- -%% Creation -%%----------------------------------------------------- -init([Port]) -> - {ok, LSock} = gen_tcp:listen(Port, [{active, true}, - {reuseaddr, true}]), - {ok, #state{port = Port, lsock = LSock}, 0}. - -%%----------------------------------------------------- -%% @doc Fetches the number of requests made to this server -%% @spec get_count() -> {ok, Count} -%% where -%% Count = integer() -%% @end -%%----------------------------------------------------- -get_count(get_count) -> - gen_server:call(?SERVER, get_count). - -%crash() -> gen_server:call(?SERVER, crash). -%vms() -> gen_server:call(?SERVER, vms). -%hello() -> gen_server:call(?SERVER, hello). -%echo() -> gen_server:call(?SERVER, {echo, RefID, _, MSG}). - - - - -%%----------------------------------------------------- -%% @doc Stops the server. -%% @spec stop() -> ok -%% @end -%%----------------------------------------------------- -stop() -> - gen_server:cast(?SERVER, stop). - -%%----------------------------------------------------- -%% Handlers -%%----------------------------------------------------- -handle_call(get_count, _From, State) -> - {reply, {ok, State#state.client_vm_map}, State}. - -handle_cast(stop, State) -> - {stop, normal, State}. - -handle_info({tcp, Socket, RawData}, State) -> %parameters coming from gen_server - NewState = do_JSCall(Socket, RawData, State), %TODO - OldMap = State#state.client_vm_map, - io:format("Old map: ~p NewState: ~p~n", [OldMap, NewState]), - {noreply, State#state{client_vm_map = OldMap ++ [NewState]}}; - -handle_info(timeout, #state{lsock = LSock} = State) -> - {ok, _Sock} = gen_tcp:accept(LSock), - {noreply, State}. - - -%%----------------------------------------------------- -%% TCP Calls -%%----------------------------------------------------- -send(Socket, RefID, String) -> - gen_tcp:send(Socket, io_lib:fwrite("~p ~p~n", [RefID,String])). - -send(Socket, RefID, String1, String2) -> - gen_tcp:send(Socket, io_lib:fwrite("~p ~p ~p~n", [RefID, String1, String2])). - - -%%----------------------------------------------------- -%% gen_server callbacks -%%----------------------------------------------------- -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%----------------------------------------------------- -%% Internal functions -%%----------------------------------------------------- -do_JSCall(Socket, Data, State) -> - JSVM = js_runner:boot(), - js_runner:define(JSVM, "function userCommand(cmd, par) {return cmd+' '+ par}"), - Parsed = ggs_protocol:parse(Data), - NewState = case Parsed of - {cmd, Command, Parameter} -> - % Set the new state to [] - Ret = js_runner:call(JSVM, "userCommand", - [list_to_binary(Command), - list_to_binary(Parameter)]), - send(Socket, "RefID", "JS says: ", Ret), - []; - % Set the new state to the reference generated, and JSVM associated - {hello} -> - Client = getRef(), - send(Socket, Client, "__ok_hello"), - {Client, JSVM}; - {echo, RefID, _, MSG} -> - send(Socket, RefID, "Your VM is ", getJSVM(RefID, State)), - []; - {crash, Zero} -> - 10/Zero; - {vms} -> - send(Socket, "RefID", State); - % Set the new state to [] - Other -> - send(Socket, "RefID", "__error"), - [] - end, - % Return the new state - NewState. - -%%----------------------------------------------------- -%% Helpers -%%----------------------------------------------------- -getRef() -> - {A1,A2,A3} = now(), - random:seed(A1, A2, A3), - random:uniform(1000). - -%%----------------------------------------------------- -%% Helpers -%%----------------------------------------------------- -getJSVM(RefID, State) -> - VMs = State#state.client_vm_map, - {value, {_,VM}} = lists:keysearch(RefID, 1, VMs), - VM. diff --git a/src/ggs_player.erl b/src/ggs_player.erl new file mode 100644 index 0000000..0211f3f --- /dev/null +++ b/src/ggs_player.erl @@ -0,0 +1,83 @@ +-module(ggs_player). +-export([start_link/1, notify/3, get_token/1, stop/2]). +-record(pl_state, + {token, % Player's token + socket, % Player's socket + table}). % Player's table + +%% @doc This module handles communication between a player and GGS. This module is +%%responsible for: +%% * The storage of the player socket, player token and a table token. +%% * Ability to fetch a player token. +%% * Forwarding messages from players to the game +%% * Remove a player from GGS + +%% @doc Spawns a process representing the player in GGS. Takes the player socket as +%% an argument for storage and later usage. Creates a unique player token +%% identifying the player. +%% @spec start_link(Socket::socket()) -> {ok, Pid} | {error, Reason} +start_link(Socket) -> + % The socket is in 'active' mode, and that means we are pushed any data + % that arrives on it, we do not need to recv() manually. Since the socket + % was opened in our parent process, we need to change the owner of it to + % us, otherwise these messages end up in our parent. + erlang:port_connect(Socket, self()), + {ok, Token} = ggs_coordinator:join_lobby(), + TableStatus = ggs_coordinator:join_table(1337), + case TableStatus of + {ok, Table} -> + notify(self(), self(), Token), + loop(#pl_state{socket = Socket, token = Token, table = Table}); + {error, no_such_table} -> + ggs_coordinator:create_table({force, 1337}), + {ok, Table} = ggs_coordinator:join_table(1337), + notify(self(), self(), Token), + loop(#pl_state{socket = Socket, token = Token, table = Table}) + end. + +%% @doc Handles incoming messages from the GGS and forwards them through the player +%% socket to the player. +%% @spec notify(Player::Pid(), From::Pid(), +%% {Command::String(), Message::string()}) -> ok +notify(Player, From, Message) -> + Player ! {notify, From, Message}. + +%% @doc Get the player token uniquely representing the player. +%% @spec get_token() -> string() +get_token(_Player) -> + ggs_logger:not_implemented(). + +%% @doc Properly terminates the player process. The player token will be lost +%% together with the table token. It should also close the player socket and the +%% process should return in the end. +%% @spec stop(Table::pid()) -> Reason::string() +stop(_Player,_Table) -> + ggs_logger:not_implemented(). + +%% Internals + +loop(#pl_state{token = _Token, socket = Socket, table = Table} = State) -> + receive + {tcp, Socket, Data} -> % Just echo for now.. + io:format("Parsing via protocol module..~n"), + Parsed = ggs_protocol:parse(Data), + self() ! Parsed, + loop(State); + {notify, _From, Message} -> + gen_tcp:send(Socket, Message), + loop(State); + % Below are messages generated by the parser + {game_cmd,Cmd, _Headers, Data} -> + ggs_table:notify(Table, self(), {game, Cmd, Data}), + loop(State); + {srv_cmd,"define", _Headers, Data} -> + ggs_table:notify(Table, self(), {server, define, Data}), + loop(State); + {tcp_closed, _Socket} -> + io:format("Client disconnected, but THIS IS NOT SUPPORTED YET!~n"), + loop(State); + Other -> + io:format("Got UNKNOWN message: "), + erlang:display(Other), + io:format("~n") + end. diff --git a/src/ggs_protocol.erl b/src/ggs_protocol.erl index 35da585..3dc88f9 100644 --- a/src/ggs_protocol.erl +++ b/src/ggs_protocol.erl @@ -33,6 +33,8 @@ handle([[]]) -> {separator, data_next}; handle(["Server-Command", Param]) -> {{srv_cmd, Param}, more}; +handle(["Game-Command", Param]) -> + {{game_cmd, Param}, more}; handle(["Content-Length", Param]) -> {{content_len, Param}, more}; handle(["Token", Param]) -> @@ -47,11 +49,11 @@ handle_data(Data, Length) -> prettify({Args, Data}) -> case lists:keyfind(srv_cmd, 1, Args) of {_, Value} -> - gen_server:cast(ggs_server, {srv_cmd, Value, Args, Data}); + {srv_cmd, Value, Args, Data}; _Other -> case lists:keyfind(game_cmd, 1, Args) of {_, Value} -> - gen_server:cast(ggs_server, {game_cmd, Value, Args, Data}); + {game_cmd, Value, Args, Data}; _ -> ok end diff --git a/src/ggs_server.erl b/src/ggs_server.erl deleted file mode 100644 index ab9b8f8..0000000 --- a/src/ggs_server.erl +++ /dev/null @@ -1,134 +0,0 @@ --module(ggs_server). --behaviour(gen_server). - -%% API --export([start_link/1, - start_link/0, - stop/0 - ]). - -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). - - --define(SERVER, ?MODULE). --define(DEFAULT_PORT, 1055). - --record(state, {port, lsock, client_vm_map = []}). - -%%%==================================================== -%%% API -%%%==================================================== - -%%----------------------------------------------------- -%% @doc Starts the server -%% @end -%%----------------------------------------------------- -start_link(Port) -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [Port], []). - -start_link() -> - start_link(?DEFAULT_PORT). - -%%----------------------------------------------------- -%% @doc Stops the server. -%% @spec stop() -> ok -%% @end -%%----------------------------------------------------- -stop() -> - gen_server:cast(?SERVER, stop). - -%%----------------------------------------------------- -%% gen_server callbacks -%%----------------------------------------------------- - -init([Port]) -> - case gen_server:call(ggs_backup, get_backup) of - {backup_state, not_initialized} -> - {ok, LSock} = gen_tcp:listen(Port, [{active, true}, - {reuseaddr, true}]), - {ok, #state{port = Port, lsock = LSock}, 0}; - {backup_state, State} -> - {ok, LSock} = gen_tcp:listen(Port, [{active, true}, - {reuseaddr, true}]), - {ok, State#state{lsock = LSock}, 0} - end. - -handle_call({backup_state, OldState}, _From, State) -> - io:format("Received old state from backup~n"), - {noreply, OldState}. - - -handle_info({tcp, Socket, RawData}, State) -> - ggs_protocol:parse(RawData), - {noreply, State#state{lsock = Socket}}; - -handle_info({tcp_closed, Socket}, State) -> - gen_tcp:close(Socket), - {stop, "Client closed socket", State}; - -handle_info(timeout, #state{lsock = LSock} = State) -> - {ok, _Sock} = gen_tcp:accept(LSock), - {noreply, State}; - -handle_info(Other, State) -> - erlang:display(Other). - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%----------------------------------------------------- -%% Internal functions -%%----------------------------------------------------- -handle_cast(stop, State) -> - {stop, normal, State}; - -% Handle javascript defines -handle_cast({srv_cmd, "define", Headers, Data}, State) -> - Token = ggs_protocol:getToken(Headers), - GameVM = getJSVM(Token, State), - ggs_vm_runner:define(GameVM, Data), - send(State#state.lsock, "Token", "Okay, defined that for you!"), - {noreply, State}; - -% Handle javascript calls -handle_cast({srv_cmd, "call", Headers, Data}, State) -> - Token = ggs_protocol:getToken(Headers), - io:format("Got call request: ~p~n", [Data]), - GameVM = getJSVM(Token, State), - Ret = ggs_vm_runner:user_command(GameVM, "User", Data, []), - send(State#state.lsock, Token, "JS says:", binary_to_list(Ret)), - {noreply, State}; - -% Set the new state to the reference generated, and JSVM associated -handle_cast({srv_cmd, "hello", Headers, Data}, State) -> - GameVM = ggs_vm_runner:start_link(), - Client = getRef(), - send(State#state.lsock, Client, "This is your refID"), - OldMap = State#state.client_vm_map, - NewState = State#state{client_vm_map = OldMap ++ [{Client, GameVM}]}, - gen_server:cast(ggs_backup, {set_backup, NewState}), - {noreply, NewState}. - -%%----------------------------------------------------- -%% Helpers -%%----------------------------------------------------- -getRef() -> - string:strip(os:cmd("uuidgen"), right, $\n ). - -getJSVM(RefID, State) -> - VMs = State#state.client_vm_map, - erlang:display(RefID), - erlang:display(VMs), - {value, {_,VM}} = lists:keysearch(RefID, 1, VMs), - VM. - -send(Socket, RefID, String) -> - gen_tcp:send(Socket, string:join([RefID,String,"\n"], " ")). - -send(Socket, RefID, String1, String2) -> - gen_tcp:send(Socket, string:join([RefID,String1, String2,"\n"], " ")). diff --git a/src/ggs_server_sup.erl b/src/ggs_server_sup.erl deleted file mode 100644 index 23d32f7..0000000 --- a/src/ggs_server_sup.erl +++ /dev/null @@ -1,48 +0,0 @@ --module(ggs_server_sup). --behaviour(supervisor). - -%% API --export([start/1, start_link/1]). - -%% Supervisor callbacks --export([init/1]). --define(SERVER, ?MODULE). - -start(Port) -> - [FirstArg] = Port, - {IntPort, _} = string:to_integer(FirstArg), - start_link(IntPort). - -start_link(Port) -> - supervisor:start_link({local, ?SERVER}, ?MODULE, [Port]). - -init([Port]) -> - GGSServer = {ggs_server, - {ggs_server, start_link, [Port]}, - permanent, - 2000, - worker, - [ggs_server] - }, - Backup = {ggs_backup, - {ggs_backup, start_link, []}, - permanent, - 2000, - worker, - [ggs_backup] - }, - MnesiaServer = {ggs_mnesia_controller_server, - {ggs_mnesia_controller_server, start_link, []}, - permanent, - 2000, - worker, - [ggs_mnesia_controller_server] - }, - Children = [MnesiaServer, Backup, GGSServer], - - RestartStrategy = { one_for_one, % Restart only crashing child - 10, % Allow ten crashes per.. - 1 % 1 second, then crash supervisor. - }, - {ok, {RestartStrategy, Children}}. - diff --git a/src/ggs_sup.erl b/src/ggs_sup.erl index ee6f8cd..05fab65 100644 --- a/src/ggs_sup.erl +++ b/src/ggs_sup.erl @@ -2,29 +2,31 @@ -behaviour(supervisor). %% API --export([start/1, start_link/1]). +-export([start_link/1]). %% Supervisor callbacks -export([init/1]). -define(SERVER, ?MODULE). -start(Port) -> - [FirstArg] = Port, - {IntPort, _} = string:to_integer(FirstArg), - start_link(IntPort). - start_link(Port) -> supervisor:start_link({local, ?SERVER}, ?MODULE, [Port]). init([Port]) -> - Server = {ggs_server_sup, - {ggs_server_sup, start_link, [Port]}, + Dispatcher = {ggs_dispatcher, + {ggs_dispatcher, start_link, [Port]}, permanent, 2000, worker, - [ggs_server_sup] + [ggs_dispatcher] }, - Children = [Server], + Coordinator = {ggs_coordinator, + {ggs_coordinator, start_link, []}, + permanent, + 2000, + worker, + [ggs_coordinator] + }, + Children = [Dispatcher, Coordinator], RestartStrategy = { one_for_one, % Restart only crashing child 10, % Allow ten crashes per.. diff --git a/src/ggs_table.erl b/src/ggs_table.erl new file mode 100644 index 0000000..d6d2988 --- /dev/null +++ b/src/ggs_table.erl @@ -0,0 +1,125 @@ +%% @doc This module represents a table with players + +-module(ggs_table). +-behaviour(gen_server). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3, notify_all_players/2, notify_game/3, + get_player_list/1]). + +-record(state, { players, game_vm } ). + +%% API +-export([start_link/0, + add_player/2, + remove_player/2, + stop/1, + notify/3, + notify_all_players/2, + notify_game/3]). + + +%% ---------------------------------------------------------------------- +% API implementation + +% @doc returns a new table +start_link() -> + {ok, Pid} = gen_server:start_link(?MODULE, [], []), + Pid. + +%% @private +call(Pid, Msg) -> + gen_server:call(Pid, Msg, infinity). + +% @doc adds a player to a table +add_player(Table, Player) -> + call(Table, {add_player, Player}). + +% @doc removes player form a table +remove_player(Table, Player) -> + call(Table, {remove_player, Player}). + +%% @doc Get a list of all player processes attached to this table +get_player_list(Table) -> + gen_server:call(Table, get_player_list). + +% @doc stops the table process +stop(Table) -> + gen_server:cast(Table, stop). + +% @doc notifies the table with a message from a player +notify(Table, Player, Message) -> + gen_server:cast(Table, {notify, Player, Message}). + +notify_all_players(Table, Message) -> + gen_server:cast(Table, {notify_all_players, Message}). + +notify_game(Table, From, Message) -> + erlang:display(Table), + io:format("~n"), + gen_server:cast(Table, {notify_game, Message, From}). + +%% ---------------------------------------------------------------------- + +%% @private +init([]) -> + GameVM = ggs_gamevm_e:start_link(self()), %% @TODO: Temporary erlang gamevm + {ok, #state { + game_vm = GameVM, + players = [] }}. + +%% @private +handle_call({add_player, Player}, _From, #state { players = Players } = State) -> + {reply, ok, State#state { players = [Player | Players] }}; + +handle_call({remove_player, Player}, _From, #state { players = Players } = State) -> + {reply, ok, State#state { players = Players -- [Player] }}; + +handle_call(get_player_list, _From, #state { players = Players } = State) -> + {reply, {ok, Players}, State}; + +handle_call(Msg, _From, State) -> + error_logger:error_report([unknown_msg, Msg]), + {reply, ok, State}. + +%% @private +handle_cast({notify, Player, Message}, #state { game_vm = GameVM } = State) -> + case Message of + {server, define, Args} -> + ggs_gamevm_e:define(GameVM, Args); + {game, Command, Args} -> + ggs_gamevm_e:user_command(GameVM, Player, Command, Args) + end, + {noreply, State}; + +handle_cast({notify_game, Message, From}, #state { game_vm = GameVM } = State) -> + ggs_gamevm_e:user_command(GameVM, From, Message, ""), + {noreply, State}; + +handle_cast({notify_all_players, Message}, #state{players = Players} = State) -> + lists:foreach( + fun(P) -> ggs_player:notify(P, "Server", Message) end, + Players + ), + {noreply, State}; + +handle_cast(stop, State) -> + {stop, normal, State}; +handle_cast(Msg, S) -> + error_logger:error_report([unknown_msg, Msg]), + {noreply, S}. + +%% @private +handle_info(Msg, S) -> + error_logger:error_report([unknown_msg, Msg]), + {noreply, S}. + +%% @private +terminate(_Reason, _State) -> + ok. + +%% @private +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + diff --git a/src/ggs_vm_runner.erl b/src/ggs_vm_runner.erl deleted file mode 100644 index e149c51..0000000 --- a/src/ggs_vm_runner.erl +++ /dev/null @@ -1,42 +0,0 @@ --module(ggs_vm_runner). --export([start_link/0, define/2, user_command/4]). - -%Mattias -start_link() -> - erlang_js:start(), - PortPid = spawn_link( fun() -> - process_flag(trap_exit, true), - {ok, Port} = js_driver:new(), - js:define(Port, <<"function userCommand(user, command, args){return 'Hello world';}">>), - loop(Port) - end ), - PortPid. - - -loop(Port) -> - receive - {define, SourceCode} -> - ok = js:define(Port, list_to_binary(SourceCode)), - loop(Port); - {user_command, User, Command, Args, From, Ref} -> - {ok, Ret} = js:call(Port, <<"userCommand">>, - [ list_to_binary(User), - list_to_binary(Command), - list_to_binary(Args) - ]), - From ! {Ref, Ret}, - loop(Port) - end. - - -define(GameVM, SourceCode) -> - GameVM ! {define,SourceCode}. - -user_command(GameVM, User, Command, Args) -> - Ref = make_ref(), - GameVM ! {user_command, User, Command, Args, self(), Ref}, - receive - {Ref, RetVal} -> - RetVal; - Other -> Other - end. diff --git a/src/helpers.erl b/src/helpers.erl new file mode 100644 index 0000000..3a42fbf --- /dev/null +++ b/src/helpers.erl @@ -0,0 +1,8 @@ +-module(helpers). +-export([not_implemented/0, get_new_token/0]). + +not_implemented() -> + exit("Not implemented"). + +get_new_token() -> + string:strip(os:cmd("uuidgen"), right, $\n ). diff --git a/start b/start deleted file mode 100755 index 6de5737..0000000 --- a/start +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -erl -sname ggs -mnesia -boot start_sasl -pa erlang_js/ebin/ -pa ebin -pa src -s start_ggs diff --git a/start_test b/start_test deleted file mode 100755 index 5920151..0000000 --- a/start_test +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -erl -boot start_sasl -pa ebin_test -pa erlang_js/ebin/ -pa erlv8/ebin -pa ebin -pa src -eval 'ggs_protocol_test:test_parse().' diff --git a/start_test_shell b/start_test_shell deleted file mode 100755 index f2135c4..0000000 --- a/start_test_shell +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -erl -boot start_sasl -pa ebin_test -pa erlang_js/ebin/ -pa erlv8/ebin -pa ebin -pa src diff --git a/tests/ggs_coordinator_test.erl b/tests/ggs_coordinator_test.erl new file mode 100644 index 0000000..6ec41c6 --- /dev/null +++ b/tests/ggs_coordinator_test.erl @@ -0,0 +1,53 @@ +-module(ggs_coordinator_test). +-include_lib("eunit/include/eunit.hrl"). + +coordinator_test_() -> + {foreach, + fun() -> + {ok, _Coord} = ggs_coordinator:start_link(), + timer:sleep(100) + end, + fun(_X) -> + ggs_coordinator:stop("End of test"), + timer:sleep(100) + end, + [ + fun test_start_link/0, + fun test_stop/0, + fun test_join_bad_table/0, + fun test_join_lobby/0 + ] + }. + +test_start_link() -> + % Check process info + PInfo = whereis(ggs_coordinator), + ?assert((PInfo /= undefined)). % Did the server start? + +test_stop() -> + ok = ggs_coordinator:stop(""), % Extra cleaning + timer:sleep(100), + % Did it stop? + ?assert((whereis(ggs_coordinator)) == undefined). + +test_join_bad_table() -> + Response = ggs_coordinator:join_table("Nonexistant table"), + ?assert(Response == {error, no_such_table}). + +test_join_lobby() -> + {Response, _} = ggs_coordinator:join_lobby(), + ?assert(Response /= error). + +%% 'Manual' tests + +create_table_test() -> + {ok, _Coord} = ggs_coordinator:start_link(), + timer:sleep(100), + % Forcibly create a table. This functionality should be disabled + % in the production system, but is pretty nice for testing. + Response = ggs_coordinator:create_table({force, 1337}), + ?assert(Response == {ok, 1337}). + +join_good_table_test() -> + Response = ggs_coordinator:join_table(1337), + ?assert(Response == {ok, 1337}). diff --git a/tests/ggs_db_test.erl b/tests/ggs_db_test.erl new file mode 100644 index 0000000..f2608f1 --- /dev/null +++ b/tests/ggs_db_test.erl @@ -0,0 +1,47 @@ +-module(ggs_db_test). +%-compile({no_auto_import,[get/1,set/2]}). +-include_lib("eunit/include/eunit.hrl"). + +%ggs_db_test_() -> +% {spawn, +% {setup, fun setup/0, fun cleanup/1,[ fun ggs_db_test/0 ]} +% }. + + +%Key should be a tuple of two elements +getItem_setItem_test() -> + ggs_db:init(), + ggs_db:setItem("dbname","nsname","keyname1","Hello"), + ggs_db:setItem("dbname","nsname","keyname2","Hello2"), + ggs_db:setItem("dbname2","nsname","keyname1","Hello3"), + ggs_db:setItem("dbname2","nsname","keyname1","Hello4"), + ggs_db:setItem("dbname3","nsname","keyname1","Hello5"), + "Hello" = ggs_db:getItem("dbname","nsname","keyname1"). + +%Test the length function of our database +length_test() -> + ggs_db:setItem(1,1,2,"112"), + ggs_db:setItem(1,2,2,"122"), + ggs_db:setItem(1,1,3,"113"), + ggs_db:setItem(1,1,4,"114"), + ?assertEqual(ggs_db:length(1,1), 3). + +%Test if we can remove correctly from the database +removeItem_test() -> + ggs_db:removeItem(1,1,4), + ?assertNot(ggs_db:getItem(1,1,4) =:= "114"). + +%Test the key function +key_test() -> + ?assert(ggs_db:key(1,1,2) =:= {1,1,2}). + +%Test the clear function(for gametoken and ns) +clear_test() -> + ggs_db:clear(1,1), + ?assert(ggs_db:length(1,1) =:= 0). + +%Test the clear function(gametoken) +clear_GameToken_test() -> + ggs_db:clear(1), + ?assert((ggs_db:length(1,1) + ggs_db:length(1,2)) =:= 0), + ggs_db:stop(). diff --git a/tests/ggs_gamevm_test.erl b/tests/ggs_gamevm_test.erl new file mode 100644 index 0000000..1689dd1 --- /dev/null +++ b/tests/ggs_gamevm_test.erl @@ -0,0 +1,31 @@ +-module(ggs_gamevm_test). +-include_lib("eunit/include/eunit.hrl"). + +start_link_test() -> + erlang_js:start(), %% @TODO: should only be done once + GameVM = start_link(test_table), + ?assertNot(GameVM =:= undefined). + +define_test() -> + GameVM = start_link(test_table), + define(GameVM, "function hello(test) { return test; }"), + ?assertMatch(<<"jeena">>, gen_server:call(GameVM, {eval, "hello('jeena')"})). + +stop_test() -> + GameVM = start_link(test_table), + ok = stop(GameVM). + +user_command_test() -> + GameVM = start_link(test_table), + define(GameVM, "var t = '';\nfunction userCommand(user, command, args) { t = user + command + args; }\n"), + user_command(GameVM, "'jeena", "thecommand", "theargs'"), + ?assertMatch(<<"'jeenathecommandtheargs'">>, gen_server:call(GameVM, {eval, "t;"})). + +js_erlang_test() -> + GameVM = start_link(test_table), + define(GameVM, "var t = '';\nfunction userCommand(user, command, args) { t = callErlang('erlang time') + ''; }\n"), + user_command(GameVM, "", "", ""), + {A, B, C} = erlang:time(), + T = "{" ++ integer_to_list(A) ++ ", " ++ integer_to_list(B) ++ ", " ++ integer_to_list(C) ++ "}", + ?assertMatch(T, binary_to_list(gen_server:call(GameVM, {eval, "t;"}))). + diff --git a/tests/ggs_player_test.erl b/tests/ggs_player_test.erl new file mode 100644 index 0000000..efe7530 --- /dev/null +++ b/tests/ggs_player_test.erl @@ -0,0 +1,27 @@ +-module(ggs_player_test). +-include_lib("eunit/include/eunit.hrl"). + +%% @doc start_link should always return ok for any valid socket. A valid socket +%% should always return {ok, Pid} and {error, Reason} otherwise. +start_link_test() -> + ggs_logger:not_implemented(). + +%% @doc Given that start_link returned {ok, Player}. Notify shall always return ok and +%% deliver a specified message through the socket. +notify_test() -> + Player = ggs_player:start_link("bad arg"), + Message = {"something", ""}, + Ret = ggs_player:notify(Player, self(), Message), + ?assertNot(ok =:= Ret). + +%% @doc Given that start_link returned {ok, Player}. get_token shall always return a valid +%% player token. a valid token should be unique. +get_token_test() -> + ggs_logger:not_implemented(). + +%% @doc Given that start_link returned {ok, Pid}. There shouldn't be possible to +%% execute this function with the same Player and Table arguments twice. +stop_test() -> + Player = ggs_player:start_link(something), + Table = test, + ok = ggs_player:stop(Player, Table). diff --git a/tests/ggs_protocol_test.erl b/tests/ggs_protocol_test.erl deleted file mode 100644 index 2230cbf..0000000 --- a/tests/ggs_protocol_test.erl +++ /dev/null @@ -1,6 +0,0 @@ --module(ggs_protocol_test). --export([test_parse/0]). - -test_parse() -> - Ret = ggs_protocol:parse("<> __define JavaScript"), - io:format("~p~n", [Ret]). diff --git a/tests/ggs_table_test.erl b/tests/ggs_table_test.erl new file mode 100644 index 0000000..8dbe11e --- /dev/null +++ b/tests/ggs_table_test.erl @@ -0,0 +1,38 @@ +-module(ggs_table_test). +-include_lib("eunit/include/eunit.hrl"). + +start_link_test() -> + Table = ggs_table:start_link(), + ?assertNot(Table =:= undefined). + +add_player_test() -> + Table = ggs_table:start_link(), + Player = test_player, + ggs_table:add_player(Table, Player), + {ok, [Player]} = gen_server:call(Table, get_player_list). + +remove_player_test() -> + Table = ggs_table:start_link(), + Player = test_player, + Player2 = test_player2, + ggs_table:add_player(Table, Player), + {ok, [Player]} = gen_server:call(Table, get_player_list), + ggs_table:add_player(Table, Player2), + {ok, [Player2, Player]} = gen_server:call(Table, get_player_list), + ggs_table:remove_player(Table, Player), + {ok, [Player2]} = gen_server:call(Table, get_player_list), + ggs_table:remove_player(Table, Player2), + {ok, []} = gen_server:call(Table, get_player_list). + +stop_test() -> + Table = ggs_table:start_link(), + ok = ggs_table:stop(Table). + +notify_test() -> + Table = ggs_table:start_link(), + Player = test_player, + Message = {server, define, "function helloWorld(x) { }"}, + ok = ggs_table:notify(Table, Player, Message), + Message2 = {game, "helloWorld", "test"}, + ok = ggs_table:notify(Table, Player, Message2). +