First Person Space Shooter:Networking,Collision,Movement

UPDATE(12th June, 2007):
Screenshots of new models/graphics:
http://legion.panda3dprojects.com/
click download and look into the “space” folder

UPDATE(21st May, 2007):
Ok, the old download Link is dead…
The only download link left is:

http://legion.panda3dprojects.com/

Anyway, you won’t find the first (and only fully working) Version there… yet

I’m going to rewrite this first Version so it’ll be easier to download and run it.

I can’t tell when I’ll release any final version (or at least some kind of alpha/beta). I’ve got another project that I’ll have to finish first, before I can even start rewriting the whole concept of my game. (Yes, I’m rewriting it AGAIN, but this time not only rewriting the same code in a different style, but changeing the code fundamentaly)

UPDATE(5th April, 2007):

Thanks to bigfoot :wink: I’ve got another web space, that accepts files that are larger than 1MB (The old webspace couldn’t handle files larger than 1MB, so I couldn’t upload a rar or zip file with everything the game needs).
There’re now two versions of the game. The first ist still uploaded as seperate files on the old webspace, the second is uploaded as BadLands.rar on the new web space.
With the second Version I was going to rewrite the whole code, so it’ll become more structured and easier to modify. However, this new code isn’t nearly as functional as the old one. It implements one new feature(Model camera, a frequent request from some friends is implemented on the key ‘c’), but it hasn’t got working network code or a HUD. It is anyway much easier to use, just extract the folder somewhere and start badlands.py, this schould start the ‘game’. Then start the server…

UPDATE:

Download Link:
http://23legion23.funpic.de/downloads/
Some Screenshots of the latest(December 4th 2006) Version:



I’ve finished some working code on my way to develop a First Person Space Shooter (like Wing Commander) for Multiplayer!

The following Code has basic movement, basic collision detection and of course basic networking. So it might be a good start for anyone who wants to try the same.

The code was tested with two players as well as two clients on the same machine as with two clients over the internet. It works. Although I didn’t have the opportunity to test it with more players, but I will soon and update this post, if I had to fix some bugs.

The Key mapping:

After starting the program press either

1 for becoming a Server
2 for becoming a Client and connect to the Server defined in the cfg file

Controls:
w : UP
s : DOWN
a : LEFT
d : RIGHT
q : RollLEFT
e : RollRIGHT
y : move forward
mouse1 : forward
ESC : disconnect (after connect), else quit

So here it is, enjoy and learn!
(I suggest copy&paste before reading, as some comments are long and might not fit into the forums screen width. And DON’T forget to COPY THE CFG FILE)

MSG_NONE = 0
MSG_AUTH = 1
MSG_POS = 2
MSG_QUIT = 3
MSG_CQUIT = 4
MSG_CPOS = 5








timeout=1000        #set timeout for connection attempt

import direct.directbase.DirectStart
from direct.showbase import DirectObject
from pandac.PandaModules import *
from direct.task import Task
import math, sys
from direct.distributed.PyDatagram import PyDatagram
from direct.distributed.PyDatagramIterator import PyDatagramIterator

from direct.gui.DirectGui import *



class main(DirectObject.DirectObject):
    def __init__(self):
        self.SERVER = 0          #Are we Server? 1 = Server, 0 = Client

        self.Clients = {}        #PlayerID by Connection
        self.PList = {}          #Player Stats by PlayerID
        self.Models = {}         #Models by PlayerID
        self.CSP = {}            #CollisionSpheres by PlayerID
        self.Cnodes = {}         #Collision Nodes by PlayerID

        file = open("./3dnetwork.cfg", 'r')         #read config file
        while 1:
            str = file.readline()
            if (str == ""):
                break
            part = str.rsplit('=')
            if (part[0].strip() == 'myID'):
                self.myID = part[1].strip()         #set the players id on this machine
            if (part[0].strip() == 'server'):
                self.serverAddress = part[1].strip()    #set the server Address we connect to
            if (part[0].strip() == 'port'):
                self.port = int(part[1].strip())        #set the port to connect to
        file.close()
    
                

        base.setBackgroundColor(0,0,0)
        
        #set key bindings
        self.accept('mouse1', self.forw, [1])
        self.accept('mouse1-up', self.forw, [0])
        self.accept('escape', self.quit)
        self.acceptOnce('w', self.up,[1])
        self.acceptOnce('w-up', self.up,[0])
        self.acceptOnce('a', self.left,[1])
        self.acceptOnce('a-up', self.left,[0])
        self.acceptOnce('s', self.down,[1])
        self.acceptOnce('s-up', self.down,[0])
        self.acceptOnce('d', self.right,[1])
        self.acceptOnce('d-up', self.right,[0])
        self.acceptOnce('q', self.rleft,[1])
        self.acceptOnce('q-up', self.rleft,[0])
        self.acceptOnce('e', self.rright,[1])
        self.acceptOnce('e-up', self.rright,[0])
        self.acceptOnce('y', self.forw,[1])
        self.acceptOnce('y-up', self.forw,[0])
        
        #Loading Model as 0 Point for Vector calculation (invisible at world base 0,0,0)
        self.basis = loader.loadModel("models/box")
        self.basis.reparentTo(render)
        self.basis.setPos(0,0,0)
        self.basis.hide()
        
        #Load a Model as a reference point in Front of the camera
        #needed for vector calculation (also visible as crosshair)
        self.null = loader.loadModel('models/box')
        self.null.setScale(0.05)
        self.null.setColor(255,255,255,1)
        self.null.reparentTo(base.camera)
        self.null.setPos(0,20,0)
        
        #Load a model to construct the environement from(instancing)
        self.beta = loader.loadModel('models/fighter')
        self.beta.setScale(0.25)
        
        self.cs = range(2)      #setting up the nodePath for the two collision solids 
                                #(for yourself and the static models of the environment
                                
        #preparing collision detection
        #assign Collsion Solids to NodePath
        self.cs[0] = CollisionSphere(0,0,0,15)
        self.cs[1] = CollisionSphere(0,0,0,15)
        
        self.traverser=CollisionTraverser()         #setting traverser (needed to define fromObjects)
        self.pusher=CollisionHandlerPusher()        #setting a collision Handler (in this case the pusher, 
                                                    #which does all the work for us to keep objects from passing through each other
                                                    
        self.betacnode = self.beta.attachNewNode(CollisionNode('cnode1'))       #adding a collision node to our Model/Cam
        self.camcnode = base.camera.attachNewNode(CollisionNode('cnode2'))
        
        self.betacnode.node().addSolid(self.cs[0])                              #adding the collidable geometry to our collision node
        self.camcnode.node().addSolid(self.cs[1])
        
        #initialise Collision Detection Handler and Traverser
        base.cTrav = self.traverser
        self.traverser.traverse(render)         #any collsion geometry under render that's not defined as fromObject will be an intoObject
        self.pusher.addCollider(self.camcnode, base.camera)     #passing collisionNodePath and ModelNodePath to our handler
        self.traverser.addCollider(self.camcnode, self.pusher)  #making our collisionNode a fromObject with the handler self.pusher
        
        ##uncomment to see the collisionSpheres
        #self.betacnode.show()
        
        
        #creating the environment through instancing and positioning the same model 
        #in circles aroung the world axles
        x = 0
        for i in range(100):
            anglerad=x
            self.placeholder = render.attachNewNode("Placeholder")
            self.placeholder.setPos(100*math.sin(anglerad), 100.0*math.cos(anglerad),0)
            self.placeholder.setColor(255,0,0,1)
            self.beta.instanceTo(self.placeholder)
            x = x +200
        x = 0
        for i in range(100):
            anglerad=x
            self.placeholder2 = render.attachNewNode("Placeholder2")
            self.placeholder2.setPos(100*math.sin(anglerad),0, 100.0*math.cos(anglerad))
            self.placeholder2.setColor(0,255,0,1)
            self.beta.instanceTo(self.placeholder2)
            x = x +200
        x = 0
        for i in range(100):
            anglerad=x
            self.placeholder3 = render.attachNewNode("Placeholder2")
            self.placeholder3.setPos(0,100*math.sin(anglerad), 100.0*math.cos(anglerad))
            self.placeholder3.setColor(0,0,255,1)
            self.beta.instanceTo(self.placeholder3)
            x = x +200
            
        #Disable Mouse control over camera
        base.disableMouse()
        #INIT NETWORK
        self.initNetwork()
    
    ##Movement
    #Setting up the new HPR relative to the current HPR
    #the commented print statements are for debugging
        
    #to achieve a smooth movement and the ability to press multiple keys at once, we have to use a work around:
    #if a key is pressed, a task will be started that calculates the new POS/HPR, when the key is released the task is removed again
    
    def rleft(self, keyDown):
        if keyDown:
            taskMgr.add(self.moveRleft, 'moveRleft')
        else:
            taskMgr.remove('moveRleft')
            self.acceptOnce('q', self.rleft,[1])
            self.acceptOnce('q-up', self.rleft,[0])
    def moveRleft(self, task):
        base.camera.setR(base.camera, -1)   #do the actual movement
        
        #updating the PList entry for yourself and then send the data via network
        X = base.camera.getX()
        Y = base.camera.getY()
        Z = base.camera.getZ()
        H = base.camera.getH()
        P = base.camera.getP()
        R = base.camera.getR()
        self.PList[self.myID] = [X,Y,Z,H,P,R]
        self.updateServer() 
        
        return Task.cont
        #print '(R)roll r/L: ', base.camera.getR()
        
    def rright(self, keyDown):
        if keyDown:
            taskMgr.add(self.moveRright, 'moveRright')
        else:
            taskMgr.remove('moveRright')
            self.acceptOnce('e', self.rright,[1])
            self.acceptOnce('e-up', self.rright,[0])
    def moveRright(self, task):
        base.camera.setR(base.camera, +1)
        X = base.camera.getX()
        Y = base.camera.getY()
        Z = base.camera.getZ()
        H = base.camera.getH()
        P = base.camera.getP()
        R = base.camera.getR()
        self.PList[self.myID] = [X,Y,Z,H,P,R]
        self.updateServer() 
        return Task.cont
        #print '(R)roll R/l: ', base.camera.getR()
        
    def left(self, keyDown):
        if keyDown:
            taskMgr.add(self.moveLeft, 'moveLeft')
        else:
            taskMgr.remove('moveLeft')
            self.acceptOnce('a', self.left, [1])
            self.acceptOnce('a-up', self.left, [0])
    def moveLeft(self, task):
        base.camera.setH(base.camera, +1)
        X = base.camera.getX()
        Y = base.camera.getY()
        Z = base.camera.getZ()
        H = base.camera.getH()
        P = base.camera.getP()
        R = base.camera.getR()
        self.PList[self.myID] = [X,Y,Z,H,P,R]
        self.updateServer() 
        return Task.cont
        #print '(H)rot L/r: ', base.camera.getH()
        
    def down(self, keyDown):
        if keyDown:
            taskMgr.add(self.moveDown, 'moveDown')
        else:
            taskMgr.remove('moveDown')
            self.acceptOnce('s', self.down,[1])
            self.acceptOnce('s-up', self.down,[0])
    def moveDown(self, task):
        base.camera.setP(base.camera, -1)
        X = base.camera.getX()
        Y = base.camera.getY()
        Z = base.camera.getZ()
        H = base.camera.getH()
        P = base.camera.getP()
        R = base.camera.getR()
        self.PList[self.myID] = [X,Y,Z,H,P,R]
        self.updateServer() 
        return Task.cont
        #print '(P)rot u/D: ', base.camera.getP()
        
    def up(self, keyDown):
        if keyDown:
            taskMgr.add( self.moveUp, 'moveUp')
        else:
            taskMgr.remove('moveUp')
            self.acceptOnce('w', self.up, [1])
            self.acceptOnce('w-up', self.up, [0])
    def moveUp(self, task):
        base.camera.setP(base.camera, +1)
        X = base.camera.getX()
        Y = base.camera.getY()
        Z = base.camera.getZ()
        H = base.camera.getH()
        P = base.camera.getP()
        R = base.camera.getR()
        self.PList[self.myID] = [X,Y,Z,H,P,R]
        self.updateServer() 
        return Task.cont
        #print '(P)rot U/d: ', base.camera.getP()
        
    def right(self, keyDown):
        if keyDown:
            taskMgr.add(self.moveRight, 'moveRight')
        else:
            taskMgr.remove('moveRight')
            self.acceptOnce('d', self.right,[1])
            self.acceptOnce('d-up', self.right,[0])
    def moveRight(self, task):
        base.camera.setH(base.camera, -1 )
        X = base.camera.getX()
        Y = base.camera.getY()
        Z = base.camera.getZ()
        H = base.camera.getH()
        P = base.camera.getP()
        R = base.camera.getR()
        self.PList[self.myID] = [X,Y,Z,H,P,R]
        self.updateServer() 
        return Task.cont
        #print '(H)rot l/R: ', base.camera.getH()
    
    #calculating forward movement
    def forw(self, keyDown):
        if keyDown:
            taskMgr.add(self.moveForw, 'moveForw')
        else:
            taskMgr.remove('moveForw')
            self.acceptOnce('y', self.forw,[1])
            self.acceptOnce('y-up', self.forw,[0])
    def moveForw(self, task):
        #getting two position vectors from basis to camera and from basis to 
        #the point in front of the camera
        x = base.camera.getPos(self.basis)
        y = self.null.getPos(self.basis)
        #calculating the pointing vector
        g = x-y
        #normalize
        g.normalize()
        #aplly translation
        base.camera.setFluidPos(base.camera.getPos()-g)
        X = base.camera.getX()
        Y = base.camera.getY()
        Z = base.camera.getZ()
        H = base.camera.getH()
        P = base.camera.getP()
        R = base.camera.getR()
        self.PList[self.myID] = [X,Y,Z,H,P,R]
        self.updateServer() 
        return Task.cont
    
## Networking    
    
    def initNetwork(self):      #common definitions for Client and Server
        print "Network INIT"
        
        self.cManager = QueuedConnectionManager()   #the names (like self.cManager) are arbitrary
        self.cListener = QueuedConnectionListener(self.cManager, 0)
        self.cReader = QueuedConnectionReader(self.cManager, 0)
        self.cWriter = ConnectionWriter(self.cManager,0)
        
        self.acceptOnce('1', self.startServer)  #Press 1 to become Server
        self.acceptOnce('2', self.startClient)  #Press 2 to connect as Client to a Server
        
    def startServer(self):
        #set up socket on port (defined in the 3dnetwork.cfg)
        self.tcpSocket = self.cManager.openTCPServerRendezvous(self.port, timeout)

        # Tell the listener to listen for new connections on this socket
        self.cListener.addConnection(self.tcpSocket)

        # Start Listener task 
        taskMgr.add(self.listenTask, "serverListenTask",-40)

        # Start Read task
        taskMgr.add(self.readTask, "serverReadTask", -39)
        
        self.SERVER = 1  #Now we are Server
        
        self.myID="123"  #setting Server ID to 123 by default
        
        self.PList[self.myID] = [0,0,0,0,0,0]   #adding yourself to the Player List
        #starting a task to update the world, as network transfers only update the lists this task will actually update the "visible" stuff
        taskMgr.add(self.updateWorld, "updateWorldTask")  
        
        print "Server STARTED"
    
    def startClient(self):
        #connect to server on port and try until timeout is reached
        self.Connection = self.cManager.openTCPClientConnection(self.serverAddress, self.port, timeout)
        self.cReader.addConnection(self.Connection)
        taskMgr.add(self.readTask, "serverReaderPollTask", -39)
        taskMgr.add(self.updateWorld, "updateWorldTask")
        
        self.SERVER = 0 #now we are Client
        
                
        self.PList[self.myID] = [40,40,40,0,0,0] #Add yourself to the Player List and set a position away from the servers start Pos
        base.camera.setX(40)
        base.camera.setY(40)
        base.camera.setZ(40)
        print "Client STARTED"
        self.csendMsgAuth()
    
    def csendMsgAuth(self):     #send the first datagram to the server
        
        #construct the AUTH package
        dta = PyDatagram()
        dta.addUint16(MSG_AUTH)  #Set Package Type
        dta.addString(self.myID)
        stats = self.PList[self.myID]
        
        num = len(stats)
        for i in range(num):
            dta.addFloat64(stats[i])
        self.cWriter.send(dta, self.Connection) #Send Data Package to Server

    
    def listenTask(self, task): #the task that listens for new connections
        """
        Accept new incoming connections from the client
        """
        # Run this task after the dataLoop
        
        # If there's a new connection Handle it
        if self.cListener.newConnectionAvailable():
            rendezvous = PointerToConnection()
            netAddress = NetAddress()
            newConnection = PointerToConnection()
            
            if self.cListener.getNewConnection(rendezvous,netAddress,newConnection):
                newConnection = newConnection.p()
                # tell the Reader that there's a new connection to read from
                self.cReader.addConnection(newConnection)
                #Clients[newConnection] = netAddress.getIpString()
                self.lastConnection = newConnection
                print "Got a connection!"
            else:
                print "getNewConnection returned false"
        return Task.cont

    def readTask(self, task): #this tasks waits for datagrams, if one arrives with valig MSG ID, datagram handler will be called
        while 1:
            (datagram, pkg, msgID) = self.nonBlockingRead(self.cReader)
            if msgID is MSG_NONE:
                break
            else:
                self.datagramHandler(pkg, msgID)
               
        return Task.cont
    
    def nonBlockingRead(self,qcr):  #the actual reading task
        """
        Return a datagram iterator and type if data is available on the
        queued connection reader
        """
        if self.cReader.dataAvailable():
            datagram = NetDatagram()
            if self.cReader.getData(datagram):
                pkg = PyDatagramIterator(datagram)
                
                msgID = pkg.getUint16()
                
            else:
                pkg = None
                msgID = MSG_NONE
        else:
            datagram = None
            pkg = None
            msgID = MSG_NONE
        # Note, return datagram to keep a handle on the data
        return (datagram, pkg, msgID)

    def datagramHandler(self, pkg, msgID):           #Common Handler
        #Every MSG ID has its own datagram format, so each one needs its own handler
        if (msgID == MSG_AUTH):
            #Data from a Datagram is read in the order and the format it was put there from the sender
            PlayerID=pkg.getString()
            
            self.Clients[self.lastConnection] = PlayerID
            X=pkg.getFloat64()
            Y=pkg.getFloat64()
            Z=pkg.getFloat64()
            H=pkg.getFloat64()
            P=pkg.getFloat64()
            R=pkg.getFloat64()
            StatList = [X,Y,Z,H,P,R]
            
            self.PList[PlayerID] = StatList
            
            self.updateClients()
        elif (msgID == MSG_POS):
            NUM = pkg.getUint16()
            for i in range(NUM):
                PlayerID=pkg.getString()
                if (PlayerID != self.myID):
            
                    X=pkg.getFloat64()
                    Y=pkg.getFloat64()
                    Z=pkg.getFloat64()
                    H=pkg.getFloat64()
                    P=pkg.getFloat64()
                    R=pkg.getFloat64()
                    StatList = [X,Y,Z,H,P,R]
                    
                    self.PList[PlayerID] = StatList
            if (self.SERVER == 1):
                self.updateClients()
        elif (msgID == MSG_QUIT):
            #This handler handels if someone quits the game
            if (self.SERVER == 0):
                print "server quit"
                self.cManager.closeConnection(self.Connection)
                self.accept("escape", sys.exit())
            else:
                print "client quit"
                PlayerID = pkg.getString()
                self.Models[PlayerID].detachNode()
                del self.Models[PlayerID]
                del self.PList[PlayerID]
                dta = PyDatagram()
                dta.addUint16(MSG_CQUIT)
                dta.addString(PlayerID)
                for Client in self.Clients:
                    self.cWriter.send(dta, Client)
        elif (msgID == MSG_CQUIT): #this message is send from server to client only, when another client has quit
            print "client quit"
            PlayerID = pkg.getString()
            self.Models[PlayerID].detachNode()
            del self.Models[PlayerID]
            del self.PList[PlayerID]
        
        elif (msgID == MSG_CPOS):
            PlayerID=pkg.getString()
            if (PlayerID != self.myID):
            
                X=pkg.getFloat64()
                Y=pkg.getFloat64()
                Z=pkg.getFloat64()
                H=pkg.getFloat64()
                P=pkg.getFloat64()
                R=pkg.getFloat64()
                StatList = [X,Y,Z,H,P,R]
                    
                self.PList[PlayerID] = StatList
                
            
    def updateClients(self):            #Server Side Function
        dta = PyDatagram()
        NUM = len(self.PList)           #calculate number of records in the data package
        dta.addUint16(MSG_POS)  #Set Package Type
        dta.addUint16(NUM)      #Define Package Length
        for x in self.PList:         #Construct data package
            dta.addString(x)
            stats = self.PList[x]
            num = len(stats)
            for y in range(num):        #construct a single record
                dta.addFloat64(y)
        
        for Client in self.Clients:    #send Data package to Clients
            self.cWriter.send(dta, Client)
    
    def updateWorld(self, task):            #Common Task
    
        for i in self.PList:
            
            print i, self.PList[i]
            if self.Models.has_key(i):
                if (i != self.myID):
                    
                    data = self.PList[i]
                    self.Models[i].setX(data[0])
                    self.Models[i].setY(data[1])
                    self.Models[i].setZ(data[2])
                    self.Models[i].setH(data[3])
                    self.Models[i].setP(data[4])
                    self.Models[i].setR(data[5])
                    
            elif(i.strip() != ""):              
                if (i != self.myID):
                    self.Models[i]= loader.loadModel("models/fighter")
                    self.CSP[i] = CollisionSphere(0,0,0,15)
                    self.Cnodes[i] = self.Models[i].attachNewNode(CollisionNode(i))
                    self.Cnodes[i].node().addSolid(self.CSP[i])
                    self.Models[i].reparentTo(render)
                    data = self.PList[i]
                    self.Models[i].setX(data[0])
                    self.Models[i].setY(data[1])
                    self.Models[i].setZ(data[2])
                    self.Models[i].setH(data[3])
                    self.Models[i].setP(data[4])
                    self.Models[i].setR(data[5])
        
        return Task.cont
            
    def updateServer(self):         #Client sends his Stats to Server... or Player@Server updates Clients
        dta = PyDatagram()
        
        dta.addUint16(MSG_CPOS)  #Set Package Type
        
        dta.addString(self.myID)
        stats = self.PList[self.myID]
        num = len(stats)
        for i in range(num):
            dta.addFloat64(stats[i])
        if (self.SERVER == 1):
            print "server sends its position"
            for Client in self.Clients:    #send Data package to Clients
                self.cWriter.send(dta, Client)
        else:
            print "client sends its position"
            self.cWriter.send(dta, self.Connection) #Send Data Package to Server
    
    def quit(self):     #Prepare message if you want to quit
        if (self.SERVER == 0):  #Message if you are client
            dta = PyDatagram()
            dta.addUint16(MSG_QUIT)
            dta.addString(self.myID)
            self.cWriter.send(dta, self.Connection)
            self.cManager.closeConnection(self.Connection)
            sys.exit()
        else:   #message if you are server
            dta = PyDatagram()
            dta.addUint16(MSG_QUIT)
            for Client in self.Clients:
                self.cWriter.send(dta, Client)
            self.cManager.closeConnection(self.tcpSocket)
            sys.exit()




s=main()


run()

So the program has a cfg file, where it reads the server address, the clients ID and the port.
This file is for convenience, so you won’t have to alter the source code, to test it with some other guys.
The format however is:

myID=345
server=127.0.0.1
port=8500

PS: The secret of the network code is to construct the proper datagram handlers, that’s the only creative part. The rest might be as easy as copy&paste.

Excellent stuff! Thanks very much Legion :smiley:.

Thank you so much! :slight_smile: I am planning to make something similar and I am sure that looking at your example will help me a lot!

Thanks for your encouragements :wink:

I’ll soon update this code, because I’ve improved it.

I’ve:

  1. fixed some networking bugs
  2. saved some lines of code
  3. improved network overhead

I’ll update the sample code as soon as I’ve built in the ability to shoot other players. :wink: This could probably take the weekend, as I’ll have to go deeper into collision detection and intervals.

So here it is, the promised update…

It includes:

-improved Network Code
-added weapon Fire and Frag counter

This code isn’t as well commented as the last code. This is the reason why I’m not updating my first post (yet).

To fire you can either press ‘space’ or ‘mouse3’

A destroyed player will be relocated in a random Position.

I will update the code in this section until it has reached a state of development, where I’ll use custom content (models, world maps, sounds, etc.).
When this is the case, I’ll have to upload a package including this custom content somewhere. I will however alwys try to share my code and artwork on this project with you.

So now here’s the code:

MSG_NONE = 0
MSG_AUTH = 1
MSG_POS = 2
MSG_QUIT = 3
MSG_CQUIT = 4
MSG_CPOS = 5
MSG_EVENT = 6

EV_NONE = 0
EV_GUNS = 1
EV_MISSILES = 2
EV_DESTROYED = 3
EV_HIT = 4

timeout=1000        #set timeout for connection attempt

import direct.directbase.DirectStart
from direct.showbase import DirectObject
from pandac.PandaModules import *
from direct.task import Task
import math, sys, random
from direct.distributed.PyDatagram import PyDatagram
from direct.distributed.PyDatagramIterator import PyDatagramIterator
from direct.interval.IntervalGlobal import *

from direct.gui.DirectGui import *





class main(DirectObject.DirectObject):
    def __init__(self):
        self.Scores = DirectLabel(text = '',
                    relief = None,
                    pos = (base.a2dLeft + 0.05, 0, 0.9),
                    text_align = TextNode.ALeft,
                    scale = 0.07)
        self.Scores.setColor(255,255,255)
        self.SERVER = 0          #Are we Server? 1 = Server, 0 = Client
        
        self.Health = {}
        self.HighScore = {}
        self.firecount=0
        self.Intervals = {}

        self.FireModels = {}     #Models of Gunfire by Number+PlayerID

        self.Clients = {}        #PlayerID by Connection
        self.PList = {}          #Player Stats by PlayerID
        self.Models = {}         #Models by PlayerID
        self.CSP = {}            #CollisionSpheres by PlayerID
        self.Cnodes = {}         #Collision Nodes by PlayerID

        file = open("./3dnetwork.cfg", 'r')         #read config file
        while 1:
            str = file.readline()
            if (str == ""):
                break
            part = str.rsplit('=')
            if (part[0].strip() == 'myID'):
                self.myID = part[1].strip()         #set the players id on this machine
            if (part[0].strip() == 'server'):
                self.serverAddress = part[1].strip()    #set the server Address we connect to
            if (part[0].strip() == 'port'):
                self.port = int(part[1].strip())        #set the port to connect to
        file.close()

        
                

        base.setBackgroundColor(0,0,0)
        
        #set key bindings
        self.accept('mouse1', self.forw, [1])
        self.accept('mouse1-up', self.forw, [0])
        self.accept('escape', self.quit)
        self.acceptOnce('w', self.up,[1])
        self.acceptOnce('w-up', self.up,[0])
        self.acceptOnce('a', self.left,[1])
        self.acceptOnce('a-up', self.left,[0])
        self.acceptOnce('s', self.down,[1])
        self.acceptOnce('s-up', self.down,[0])
        self.acceptOnce('d', self.right,[1])
        self.acceptOnce('d-up', self.right,[0])
        self.acceptOnce('q', self.rleft,[1])
        self.acceptOnce('q-up', self.rleft,[0])
        self.acceptOnce('e', self.rright,[1])
        self.acceptOnce('e-up', self.rright,[0])
        self.acceptOnce('y', self.forw,[1])
        self.acceptOnce('y-up', self.forw,[0])
        self.accept('space', self.fgun)
        self.accept('mouse3', self.fgun)
        #self.ignore('space')
        
        #Loading Model as 0 Point for Vector calculation (invisible at world base 0,0,0)
        self.basis = loader.loadModel("models/box")
        self.basis.reparentTo(render)
        self.basis.setPos(0,0,0)
        self.basis.hide()
        
        #Load a Model as a reference point in Front of the camera
        #needed for vector calculation (also visible as crosshair)
        self.null = loader.loadModel('models/box')
        self.null.setScale(0.05)
        self.null.setColor(255,255,255,1)
        self.null.reparentTo(base.camera)
        self.null.setPos(0,20,0)
        
        #Load a model to construct the environement from(instancing)
        self.beta = loader.loadModel('models/fighter')
        self.beta.setScale(0.25)
        
        self.cs = range(2)      #setting up the nodePath for the two collision solids 
                                #(for yourself and the static models of the environment
                                
        #preparing collision detection
        #assign Collsion Solids to NodePath
        self.cs[0] = CollisionSphere(0,0,0,15)
        self.cs[1] = CollisionSphere(0,0,0,15)
                
        self.handler = CollisionHandlerEvent()
        self.handler.addInPattern('into')
        self.handler.addAgainPattern('again')
        self.handler.addOutPattern('out')
        self.accept('into', self.handleit)
        self.accept('again', self.handleagain)
        #self.accept('%fn-out-%in', self.handleit)
        self.traverser=CollisionTraverser()         #setting traverser (needed to define fromObjects)
        self.pusher=CollisionHandlerPusher()        #setting a collision Handler (in this case the pusher, 
                                                    #which does all the work for us to keep objects from passing through each other
        self.traverser.setRespectPrevTransform(True)                                            
        self.betacnode = self.beta.attachNewNode(CollisionNode('cnode1'))       #adding a collision node to our Model/Cam
        self.camcnode = base.camera.attachNewNode(CollisionNode('cnode2'))
        
        self.betacnode.node().addSolid(self.cs[0])                              #adding the collidable geometry to our collision node
        self.camcnode.node().addSolid(self.cs[1])
        
        #initialise Collision Detection Handler and Traverser
        base.cTrav = self.traverser
        self.traverser.traverse(render)         #any collsion geometry under render that's not defined as fromObject will be an intoObject
        self.pusher.addCollider(self.camcnode, base.camera)     #passing collisionNodePath and ModelNodePath to our handler
        self.traverser.addCollider(self.camcnode, self.handler)  #making our collisionNode a fromObject with the handler self.pusher
        
        ##uncomment to see the collisionSpheres
        #self.betacnode.show()
        
        
        #creating the environment through instancing and positioning the same model 
        #in circles aroung the world axles
        x = 0
        for i in range(100):
            anglerad=x
            self.placeholder = render.attachNewNode("Placeholder")
            self.placeholder.setPos(100*math.sin(anglerad), 100.0*math.cos(anglerad),0)
            self.placeholder.setColor(255,0,0,1)
            self.beta.instanceTo(self.placeholder)
            x = x +200
        x = 0
        for i in range(100):
            anglerad=x
            self.placeholder2 = render.attachNewNode("Placeholder2")
            self.placeholder2.setPos(100*math.sin(anglerad),0, 100.0*math.cos(anglerad))
            self.placeholder2.setColor(0,255,0,1)
            self.beta.instanceTo(self.placeholder2)
            x = x +200
        x = 0
        for i in range(100):
            anglerad=x
            self.placeholder3 = render.attachNewNode("Placeholder2")
            self.placeholder3.setPos(0,100*math.sin(anglerad), 100.0*math.cos(anglerad))
            self.placeholder3.setColor(0,0,255,1)
            self.beta.instanceTo(self.placeholder3)
            x = x +200
            
        #Disable Mouse control over camera
        base.disableMouse()
        #INIT NETWORK
        self.initNetwork()
    
    ##Movement
    #Setting up the new HPR relative to the current HPR
    #the commented print statements are for debugging
        
    #to achieve a smooth movement and the ability to press multiple keys at once, we have to use a work around:
    #if a key is pressed, a task will be started that calculates the new POS/HPR, when the key is released the task is removed again
    
    def rleft(self, keyDown):
        if keyDown:
            taskMgr.add(self.moveRleft, 'moveRleft', -50)
        else:
            taskMgr.remove('moveRleft')
            self.acceptOnce('q', self.rleft,[1])
            self.acceptOnce('q-up', self.rleft,[0])
    def moveRleft(self, task):
        base.camera.setR(base.camera, -1)   #do the actual movement
        
        #updating the PList entry for yourself and then send the data via network
        X = base.camera.getX()
        Y = base.camera.getY()
        Z = base.camera.getZ()
        H = base.camera.getH()
        P = base.camera.getP()
        R = base.camera.getR()
        self.PList[self.myID] = [X,Y,Z,H,P,R]
        self.updateServer() 
        
        return Task.cont
        #print '(R)roll r/L: ', base.camera.getR()
        
    def rright(self, keyDown):
        if keyDown:
            taskMgr.add(self.moveRright, 'moveRright',-50)
        else:
            taskMgr.remove('moveRright')
            self.acceptOnce('e', self.rright,[1])
            self.acceptOnce('e-up', self.rright,[0])
    def moveRright(self, task):
        base.camera.setR(base.camera, +1)
        X = base.camera.getX()
        Y = base.camera.getY()
        Z = base.camera.getZ()
        H = base.camera.getH()
        P = base.camera.getP()
        R = base.camera.getR()
        self.PList[self.myID] = [X,Y,Z,H,P,R]
        self.updateServer() 
        return Task.cont
        #print '(R)roll R/l: ', base.camera.getR()
        
    def left(self, keyDown):
        if keyDown:
            taskMgr.add(self.moveLeft, 'moveLeft',-50)
        else:
            taskMgr.remove('moveLeft')
            self.acceptOnce('a', self.left, [1])
            self.acceptOnce('a-up', self.left, [0])
    def moveLeft(self, task):
        base.camera.setH(base.camera, +1)
        X = base.camera.getX()
        Y = base.camera.getY()
        Z = base.camera.getZ()
        H = base.camera.getH()
        P = base.camera.getP()
        R = base.camera.getR()
        self.PList[self.myID] = [X,Y,Z,H,P,R]
        self.updateServer() 
        return Task.cont
        #print '(H)rot L/r: ', base.camera.getH()
        
    def down(self, keyDown):
        if keyDown:
            taskMgr.add(self.moveDown, 'moveDown',-50)
        else:
            taskMgr.remove('moveDown')
            self.acceptOnce('s', self.down,[1])
            self.acceptOnce('s-up', self.down,[0])
    def moveDown(self, task):
        base.camera.setP(base.camera, -1)
        X = base.camera.getX()
        Y = base.camera.getY()
        Z = base.camera.getZ()
        H = base.camera.getH()
        P = base.camera.getP()
        R = base.camera.getR()
        self.PList[self.myID] = [X,Y,Z,H,P,R]
        self.updateServer() 
        return Task.cont
        #print '(P)rot u/D: ', base.camera.getP()
        
    def up(self, keyDown):
        if keyDown:
            taskMgr.add( self.moveUp, 'moveUp',-50)
        else:
            taskMgr.remove('moveUp')
            self.acceptOnce('w', self.up, [1])
            self.acceptOnce('w-up', self.up, [0])
    def moveUp(self, task):
        base.camera.setP(base.camera, +1)
        X = base.camera.getX()
        Y = base.camera.getY()
        Z = base.camera.getZ()
        H = base.camera.getH()
        P = base.camera.getP()
        R = base.camera.getR()
        self.PList[self.myID] = [X,Y,Z,H,P,R]
        self.updateServer() 
        return Task.cont
        #print '(P)rot U/d: ', base.camera.getP()
        
    def right(self, keyDown):
        if keyDown:
            taskMgr.add(self.moveRight, 'moveRight',-50)
        else:
            taskMgr.remove('moveRight')
            self.acceptOnce('d', self.right,[1])
            self.acceptOnce('d-up', self.right,[0])
    def moveRight(self, task):
        base.camera.setH(base.camera, -1 )
        X = base.camera.getX()
        Y = base.camera.getY()
        Z = base.camera.getZ()
        H = base.camera.getH()
        P = base.camera.getP()
        R = base.camera.getR()
        self.PList[self.myID] = [X,Y,Z,H,P,R]
        self.updateServer() 
        return Task.cont
        #print '(H)rot l/R: ', base.camera.getH()
    
    #calculating forward movement
    def forw(self, keyDown):
        if keyDown:
            taskMgr.add(self.moveForw, 'moveForw',-50)
        else:
            taskMgr.remove('moveForw')
            self.acceptOnce('y', self.forw,[1])
            self.acceptOnce('y-up', self.forw,[0])
    def moveForw(self, task):
        #getting two position vectors from basis to camera and from basis to 
        #the point in front of the camera
        x = base.camera.getPos(self.basis)
        y = self.null.getPos(self.basis)
        #calculating the pointing vector
        g = x-y
        #normalize
        g.normalize()
        #aplly translation
        base.camera.setFluidPos(base.camera.getPos()-g)
        X = base.camera.getX()
        Y = base.camera.getY()
        Z = base.camera.getZ()
        H = base.camera.getH()
        P = base.camera.getP()
        R = base.camera.getR()
        self.PList[self.myID] = [X,Y,Z,H,P,R]
        self.updateServer() 
        return Task.cont
    
    def back(self):
        #getting two position vectors from basis to camera and from basis to 
        #the point in front of the camera
        x = base.camera.getPos(self.basis)
        y = self.null.getPos(self.basis)
        #calculating the pointing vector
        g = x-y
        #normalize
        g.normalize()
        #aplly translation
        base.camera.setFluidPos(base.camera.getPos()+g)
        X = base.camera.getX()
        Y = base.camera.getY()
        Z = base.camera.getZ()
        H = base.camera.getH()
        P = base.camera.getP()
        R = base.camera.getR()
        self.PList[self.myID] = [X,Y,Z,H,P,R]
        self.updateServer() 
    
    def fgun(self):
        sX = self.null.getX(self.basis)
        sY = self.null.getY(self.basis)
        sZ = self.null.getZ(self.basis)
        
        x = base.camera.getPos(self.basis)
        y = self.null.getPos(self.basis)
        #calculating the pointing vector
        g = x-y
        #normalize
        lookVec = (-g*50)+self.null.getPos(self.basis)
        eX = lookVec.getX()
        eY = lookVec.getY()
        eZ = lookVec.getZ()
        duration = 0.5
        PlayerID = self.myID
        self.firecount = self.firecount + 1
        length = '-'+str(self.firecount)
        self.FireModels[PlayerID+length] = loader.loadModel("models/misc/sphere")
        self.FireModels[PlayerID+length].setScale(0.25)
        self.FireModels[PlayerID+length].setPos(sX, sY, sZ)
        self.FireModels[PlayerID+length].reparentTo(render)
        self.FireModels[PlayerID+length].setColor(0,255,0)
        self.CSP[PlayerID+length] = CollisionSphere(0,0,0,0.25)
        
        self.Cnodes[PlayerID+length] = self.FireModels[PlayerID+length].attachNewNode(CollisionNode(PlayerID+length))
        self.Cnodes[PlayerID+length].node().addSolid(self.CSP[PlayerID+length])
        self.traverser.addCollider(self.Cnodes[PlayerID+length], self.handler)
        self.Intervals[PlayerID+length] = LerpPosInterval(self.FireModels[PlayerID+length], duration, Point3(eX, eY, eZ), Point3(sX, sY, sZ), fluid=1)
        self.Intervals[PlayerID+length].start()
        dta = PyDatagram()
        dta.addUint16(MSG_EVENT)
        dta.addUint16(EV_GUNS)
        dta.addString(self.myID)
        dta.addFloat64(sX)
        dta.addFloat64(sY)
        dta.addFloat64(sZ)
        dta.addFloat64(eX)
        dta.addFloat64(eY)
        dta.addFloat64(eZ)
        dta.addFloat64(duration)
        if (self.SERVER == 1):
            for Client in self.Clients:    #send Data package to Clients
                self.cWriter.send(dta, Client)
        else:
            self.cWriter.send(dta, self.Connection)
    
    def checkIntervals(self, task):
        for i in self.Intervals:
            print i
            if (not self.Intervals[i].isPlaying()):
                self.Intervals[i] = None
                del self.Intervals[i]
                
                self.traverser.removeCollider(self.Cnodes[i])
                self.Cnodes[i].detachNode()
                del self.Cnodes[i]
                del self.CSP[i]
                self.FireModels[i].detachNode()
                del self.FireModels[i]
                print "done"
                
                break
        return Task.cont
    
## Networking    
    
    def initNetwork(self):      #common definitions for Client and Server
        print "Network INIT"
        
        self.cManager = QueuedConnectionManager()   #the names (like self.cManager) are arbitrary
        self.cListener = QueuedConnectionListener(self.cManager, 0)
        self.cReader = QueuedConnectionReader(self.cManager, 0)
        self.cWriter = ConnectionWriter(self.cManager,0)
        
        self.acceptOnce('1', self.startServer)  #Press 1 to become Server
        self.acceptOnce('2', self.startClient)  #Press 2 to connect as Client to a Server
        
    def startServer(self):
        #set up socket on port (defined in the 3dnetwork.cfg)
        self.tcpSocket = self.cManager.openTCPServerRendezvous(self.port, timeout)

        # Tell the listener to listen for new connections on this socket
        self.cListener.addConnection(self.tcpSocket)

        # Start Listener task 
        taskMgr.add(self.listenTask, "serverListenTask",-40)

        # Start Read task
        taskMgr.add(self.readTask, "serverReadTask", -39)
        
        taskMgr.add(self.checkIntervals, "checkIntervals", -38)
        
        self.SERVER = 1  #Now we are Server
        
        self.myID="123"  #setting Server ID to 123 by default
        
        self.PList[self.myID] = [0,0,0,0,0,0]   #adding yourself to the Player List
        #starting a task to update the world, as network transfers only update the lists this task will actually update the "visible" stuff
        taskMgr.add(self.updateWorld, "updateWorldTask")  
        
        print "Server STARTED"
        self.Health[self.myID] = 100
        self.HighScore[self.myID] = 0

    
    def startClient(self):
        #connect to server on port and try until timeout is reached
        self.Connection = self.cManager.openTCPClientConnection(self.serverAddress, self.port, timeout)
        self.cReader.addConnection(self.Connection)
        taskMgr.add(self.readTask, "serverReaderPollTask", -39)
        taskMgr.add(self.updateWorld, "updateWorldTask")
        taskMgr.add(self.checkIntervals, "checkIntervals", -38)
        
        self.SERVER = 0 #now we are Client
        
                
        self.PList[self.myID] = [40,40,40,0,0,0] #Add yourself to the Player List and set a position away from the servers start Pos
        base.camera.setX(40)
        base.camera.setY(40)
        base.camera.setZ(40)
        print "Client STARTED"
        self.csendMsgAuth()
        self.Health[self.myID] = 100
        self.HighScore[self.myID] = 0

    def csendMsgAuth(self):     #send the first datagram to the server
        
        #construct the AUTH package
        dta = PyDatagram()
        dta.addUint16(MSG_AUTH)  #Set Package Type
        dta.addString(self.myID)
        stats = self.PList[self.myID]
        
        num = len(stats)
        for i in range(num):
            dta.addFloat64(stats[i])
        self.cWriter.send(dta, self.Connection) #Send Data Package to Server

    
    def listenTask(self, task): #the task that listens for new connections
        """
        Accept new incoming connections from the client
        """
        # Run this task after the dataLoop
        
        # If there's a new connection Handle it
        if self.cListener.newConnectionAvailable():
            rendezvous = PointerToConnection()
            netAddress = NetAddress()
            newConnection = PointerToConnection()
            
            if self.cListener.getNewConnection(rendezvous,netAddress,newConnection):
                newConnection = newConnection.p()
                # tell the Reader that there's a new connection to read from
                self.cReader.addConnection(newConnection)
                #Clients[newConnection] = netAddress.getIpString()
                self.lastConnection = newConnection
                print "Got a connection!"
            else:
                print "getNewConnection returned false"
        return Task.cont

    def readTask(self, task): #this tasks waits for datagrams, if one arrives with valig MSG ID, datagram handler will be called
        while 1:
            (datagram, pkg, msgID) = self.nonBlockingRead(self.cReader)
            if msgID is MSG_NONE:
                break
            else:
                self.datagramHandler(pkg, msgID)
               
        return Task.cont
    
    def nonBlockingRead(self,qcr):  #the actual reading task
        """
        Return a datagram iterator and type if data is available on the
        queued connection reader
        """
        if self.cReader.dataAvailable():
            datagram = NetDatagram()
            if self.cReader.getData(datagram):
                pkg = PyDatagramIterator(datagram)
                
                msgID = pkg.getUint16()
                
            else:
                pkg = None
                msgID = MSG_NONE
        else:
            datagram = None
            pkg = None
            msgID = MSG_NONE
        # Note, return datagram to keep a handle on the data
        return (datagram, pkg, msgID)

    def datagramHandler(self, pkg, msgID):           #Common Handler
        #Every MSG ID has its own datagram format, so each one needs its own handler
        if (msgID == MSG_AUTH):
            
            #Data from a Datagram is read in the order and the format it was put there from the sender
            PlayerID=pkg.getString()
            
            self.Health[PlayerID] = 100
            self.HighScore[PlayerID] = 0
            
            if (self.SERVER == 1):
                self.Clients[self.lastConnection] = PlayerID
                X=pkg.getFloat64()
                Y=pkg.getFloat64()
                Z=pkg.getFloat64()
                H=pkg.getFloat64()
                P=pkg.getFloat64()
                R=pkg.getFloat64()
                StatList = [X,Y,Z,H,P,R]
                self.PList[PlayerID] = StatList
                
                dta = PyDatagram()
                dta.addUint16(MSG_AUTH)
                dta.addString(PlayerID)
                dta.addFloat64(X)
                dta.addFloat64(Y)
                dta.addFloat64(Z)
                dta.addFloat64(H)
                dta.addFloat64(P)
                dta.addFloat64(R)
                for Client in self.Clients:    #send Data package to Clients
                    self.cWriter.send(dta, Client)
            
            
            if (self.SERVER == 0):
                self.Clients[self.Connection] = PlayerID
                X=pkg.getFloat64()
                Y=pkg.getFloat64()
                Z=pkg.getFloat64()
                H=pkg.getFloat64()
                P=pkg.getFloat64()
                R=pkg.getFloat64()
                StatList = [X,Y,Z,H,P,R]
                self.PList[PlayerID] = StatList
                
                dta = PyDatagram()
                dta.addUint16(MSG_CPOS)
                dta.addString(self.myID)
                dta.addFloat64(base.camera.getX())
                dta.addFloat64(base.camera.getY())
                dta.addFloat64(base.camera.getZ())
                dta.addFloat64(base.camera.getH())
                dta.addFloat64(base.camera.getP())
                dta.addFloat64(base.camera.getR())
                self.cWriter.send(dta, self.Connection)
                
        
        elif (msgID == MSG_QUIT):
            #This handler handels if someone quits the game
            if (self.SERVER == 0):
                print "server quit"
                self.cManager.closeConnection(self.Connection)
                self.accept("escape", sys.exit())
            else:
                print "client quit"
                PlayerID = pkg.getString()
                self.Models[PlayerID].detachNode()
                del self.Models[PlayerID]
                del self.PList[PlayerID]
                dta = PyDatagram()
                dta.addUint16(MSG_CQUIT)
                dta.addString(PlayerID)
                for Client in self.Clients:
                    self.cWriter.send(dta, Client)
        elif (msgID == MSG_CQUIT): #this message is send from server to client only, when another client has quit
            print "client quit"
            PlayerID = pkg.getString()
            self.Models[PlayerID].detachNode()
            del self.Models[PlayerID]
            del self.PList[PlayerID]
        
        elif (msgID == MSG_CPOS):
            PlayerID=pkg.getString()
            if (PlayerID != self.myID):
            
                X=pkg.getFloat64()
                Y=pkg.getFloat64()
                Z=pkg.getFloat64()
                H=pkg.getFloat64()
                P=pkg.getFloat64()
                R=pkg.getFloat64()
                StatList = [X,Y,Z,H,P,R]
                    
                self.PList[PlayerID] = StatList
                #print "client :", PlayerID
                #print " to Server with: ", StatList
            elif (PlayerID == self.myID):  #data must still be read...
                X=pkg.getFloat64()
                Y=pkg.getFloat64()
                Z=pkg.getFloat64()
                H=pkg.getFloat64()
                P=pkg.getFloat64()
                R=pkg.getFloat64()
                
            
            if (self.SERVER == 1):
                dta = PyDatagram()
                dta.addUint16(MSG_CPOS)
                dta.addString(PlayerID)
                dta.addFloat64(X)
                dta.addFloat64(Y)
                dta.addFloat64(Z)
                dta.addFloat64(H)
                dta.addFloat64(P)
                dta.addFloat64(R)
                for Client in self.Clients:    #send Data package to Clients
                    
                    self.cWriter.send(dta, Client)
        elif (msgID == MSG_EVENT):
            eventID = pkg.getUint16()
            if (eventID == EV_GUNS):
                PlayerID = pkg.getString()
                sX = pkg.getFloat64()
                sY = pkg.getFloat64()
                sZ = pkg.getFloat64()
                eX = pkg.getFloat64()
                eY = pkg.getFloat64()
                eZ = pkg.getFloat64()
                duration = pkg.getFloat64()
                #self.GunsStats = [PlayerID, sX, sY, sZ, eX,eY,eZ,duration]
                #self.GunsList.append(self.GunsStats)
                self.firecount=self.firecount +1 
                length ='-'+str(self.firecount)
                self.FireModels[PlayerID+length] = loader.loadModel("models/misc/sphere")
                self.FireModels[PlayerID+length].setScale(0.25)
                self.FireModels[PlayerID+length].setPos(sX, sY, sZ)
                self.FireModels[PlayerID+length].reparentTo(render)
                self.FireModels[PlayerID+length].setColor(0,255,0)
                self.CSP[PlayerID+length] = CollisionSphere(0,0,0,0.25)
                #self.CSP[PlayerID+length].setScale(0.25)
                self.Cnodes[PlayerID+length] = self.FireModels[PlayerID+length].attachNewNode(CollisionNode(PlayerID+length))
                self.Cnodes[PlayerID+length].node().addSolid(self.CSP[PlayerID+length])
                self.traverser.addCollider(self.Cnodes[PlayerID+length], self.handler)
                self.Intervals[PlayerID+length] = LerpPosInterval(self.FireModels[PlayerID+length], duration, Point3(eX, eY, eZ), Point3(sX, sY, sZ), fluid=1)
                self.Intervals[PlayerID+length].start()
                print "incoming Fire"
                                
                if (self.SERVER == 1):
                    dta = PyDatagram()
                    dta.addUint16(MSG_EVENT)
                    dta.addUint16(EV_GUNS)
                    dta.addString(PlayerID)
                    dta.addFloat64(sX)
                    dta.addFloat64(sY)
                    dta.addFloat64(sZ)
                    dta.addFloat64(eX)
                    dta.addFloat64(eY)
                    dta.addFloat64(eZ)
                    dta.addFloat64(duration)
                    for Client in self.Clients:    #send Data package to Clients
                        if (self.Clients[Client] != PlayerID):
                            self.cWriter.send(dta, Client)
            if (eventID == EV_HIT):
                intName = pkg.getString()
                if (self.Intervals.has_key(intName)):
                    self.Intervals[intName].finish()
                print "hit registered"
                PlayerID = pkg.getString()
                newHealth = pkg.getUint32()
                self.Health[PlayerID] = newHealth
                print self.Health
            
            if (eventID == EV_DESTROYED):
                hitID = pkg.getString()
                fireID = pkg.getString()
                PlayerID = pkg.getString()
                newScore = pkg.getUint32()
                newHealth = pkg.getUint32()
                self.HighScore[PlayerID] = newScore
                self.Health[hitID] = newHealth
                newX = random.randrange(-600, 600)
                newY = random.randrange(-600, 600)
                newZ = random.randrange(-600, 600)
                if (hitID == self.myID):
                    base.camera.setFluidPos(newX, newY, newZ)
                    stats = [newX, newY, newZ, base.camera.getH(), base.camera.getP(), base.camera.getR()]
                    self.PList[self.myID] = stats
                    self.updateServer()
                print self.PList    
                print "someone destroyed"
                print self.Health
    
    def updateWorld(self, task):            #Common Task
        Score = self.myID + " Frags: " + str(self.HighScore[self.myID])
        self.Scores['text'] = Score
        for i in self.PList:
            
            #print i, self.PList[i]
            if self.Models.has_key(i):
                if (i != self.myID):
                    
                    data = self.PList[i]
                    self.Models[i].setFluidX(data[0])
                    self.Models[i].setFluidY(data[1])
                    self.Models[i].setFluidZ(data[2])
                    self.Models[i].setH(data[3])
                    self.Models[i].setP(data[4])
                    self.Models[i].setR(data[5])
                    
            elif(i.strip() != ""):              
                if (i != self.myID):
                    self.Models[i]= loader.loadModel("models/fighter")
                    self.CSP[i] = CollisionSphere(0,0,0,15)
                    self.Cnodes[i] = self.Models[i].attachNewNode(CollisionNode(i))
                    self.Cnodes[i].node().addSolid(self.CSP[i])
                    self.Models[i].reparentTo(render)
                    data = self.PList[i]
                    self.Models[i].setX(data[0])
                    self.Models[i].setY(data[1])
                    self.Models[i].setZ(data[2])
                    self.Models[i].setH(data[3])
                    self.Models[i].setP(data[4])
                    self.Models[i].setR(data[5])
        
        return Task.cont
            
    def updateServer(self):         #Client sends his Stats to Server... or Player@Server updates Clients
        print "updating Server"
        dta = PyDatagram()
        
        dta.addUint16(MSG_CPOS)  #Set Package Type
        
        dta.addString(self.myID)
        stats = self.PList[self.myID]
        print stats
        num = len(stats)
        for i in range(num):
            dta.addFloat64(stats[i])
        if (self.SERVER == 1):
            #print "server sends its position"
            for Client in self.Clients:    #send Data package to Clients
                self.cWriter.send(dta, Client)
        else:
            #print "client sends its position"
            self.cWriter.send(dta, self.Connection) #Send Data Package to Server
    
    def quit(self):     #Prepare message if you want to quit
        if (self.SERVER == 0):  #Message if you are client
            dta = PyDatagram()
            dta.addUint16(MSG_QUIT)
            dta.addString(self.myID)
            self.cWriter.send(dta, self.Connection)
            self.cManager.closeConnection(self.Connection)
            sys.exit()
        else:   #message if you are server
            dta = PyDatagram()
            dta.addUint16(MSG_QUIT)
            for Client in self.Clients:
                self.cWriter.send(dta, Client)
            self.cManager.closeConnection(self.tcpSocket)
            sys.exit()
    
    def handleit(self, entry):
        fromNode = entry.getFromNode().getName()
        intoNode = entry.getIntoNode().getName()
                    
        print fromNode
        print intoNode
        if (fromNode == 'cnode2' or intoNode == 'cnode2'):
            print 'player collides with geometry'
            self.back()
        if (self.SERVER == 1):
            print "from: ", fromNode
            print "into: ", intoNode

            if (self.Intervals.has_key(fromNode)):
                fireID = fromNode
                hitID = intoNode
                if (hitID == 'cnode2'):
                    hitID = self.myID
                print "Gun Fire collides with geometry"
                str = fireID.rsplit('-')
                print 'str: ',str[0], hitID
                if (str[0] != hitID):
                    self.Intervals[fireID].finish()
                if (self.PList.has_key(hitID)):
                    print "Gun Fire collides with other Player"
                    self.Health[hitID] = self.Health[hitID] - 10
                    self.EventHit(fireID, hitID)
                    if (self.Health[hitID] <= 0):
                        self.EventDestroy(fireID, hitID)
    
    def handleagain(self, entry):
        fromNode = entry.getFromNode().getName()
        intoNode = entry.getIntoNode().getName()
                    
        print fromNode
        print intoNode
        if (fromNode == 'cnode2' or intoNode == 'cnode2'):
            print 'player collides with geometry'
            self.back()
    
    def EventHit(self, fireID, hitID):
        print "eventHit"
        print self.HighScore
        
        dta = PyDatagram()
        dta.addUint16(MSG_EVENT)
        dta.addUint16(EV_HIT)
        dta.addString(fireID)
        dta.addString(hitID)
        newHealth = self.Health[hitID]
        dta.addUint32(newHealth) 
        for Client in self.Clients:    #send Data package to Clients
                    
            self.cWriter.send(dta, Client)
    
    def EventDestroy(self, fireID, hitID):

        str = fireID.rsplit('-')
        PlayerID = str[0].strip()
        self.HighScore[PlayerID] = self.HighScore[PlayerID] + 1
        self.Health[hitID] = 100
        print "eventDestroy"
        if (hitID == self.myID):
            newX = random.randrange(-600, 600)
            newY = random.randrange(-600, 600)
            newZ = random.randrange(-600, 600)
            base.camera.setFluidPos(newX, newY, newZ)
            stats = [newX, newY, newZ, base.camera.getH(), base.camera.getP(), base.camera.getR()]
            self.PList[self.myID] = stats
            self.updateServer()
        dta = PyDatagram()
        dta.addUint16(MSG_EVENT)
        dta.addUint16(EV_DESTROYED)
        dta.addString(hitID)
        dta.addString(fireID)
        dta.addString(PlayerID)
        newScore = self.HighScore[PlayerID]
        dta.addUint32(newScore)
        newHealth = self.Health[hitID]
        dta.addUint32(newHealth) 
        for Client in self.Clients:    #send Data package to Clients
                    
            self.cWriter.send(dta, Client)
        #part = fromNodePath.rsplit(' ')
        #fromNode = part[1].rsplit(' ')
        #print fromNode.strip()
        




s=main()


run()

UPDATE

Well, actually I’m adding new features to my project every day (sometimes several features each day). I’m already at a point, where just posting my code won’t do any good, because I can’t post the required files here.

So I’ve started uploading my work for some reasons.
First, my http Server(dyndns…) had a HDD crash, so I had to bring the code online in order for my pre-alpha testers to get the code(they are of great help to me!).
And second this way my code is always online and available for everybody who wants it.

Anyway I thought now is a good time to post the URL…

23legion23.funpic.de/downloads/

You’ll find all required files there… usually all the models files go into the model folder and the other files have to be in a common directory.

Basically this code is a proof of concept only, it looks crappy but works. So the real game will be compleatly new code.
You can see this code as a teaser for the real game (I will add code to this version until it is a working deathmatch type game with some models/weapons/sounds… in other words until it has everything to be a a real teaser :wink: )

Current changes:

-I’ve changed flight controls. You can control your up/down, left/right with the mouse.
-added strafing (up/down, left/right) via w,a,s,d
-added some sounds
-added some particle effects (starfield&explosions)
-added a working radar
-added a simple skybox
-added join/leave/kill messages

I hope you enjoy the code and stay tuned for updates…

I downloaded all the files in that directory Legion but when I run 3dnetwork.py I get this error

C:\Documents and Settings\Desktop\MyProjects\Panda3d\Legion>ppython 3dnetwork.py
DirectStart: Starting the game.
Warning: DirectNotify: category 'Interval' already exists
Known pipe types:
  wdxGraphicsPipe8
(3 aux display modules not yet loaded.)
:gobj(error): Texture::read() - couldn't read: fire.png
:gobj(error): Unable to find texture "fire.png" on texture_path .;/c/Panda3D-1.2.3/etc/..;/c/Panda3D
-1.2.3/etc/../models or model_path .;/c/Panda3D-1.2.3/etc/..;/c/Panda3D-1.2.3/etc/../models
:pgraph(warning): Using deprecated LightAttrib interface.
:loader(error): Couldn't load file models/skybox/space_sky_box.x: not found on model path (which is
currently: ".;/c/Panda3D-1.2.3/etc/..;/c/Panda3D-1.2.3/etc/../models")
Traceback (most recent call last):
  File "3dnetwork.py", line 1641, in ?
    s=main()
  File "3dnetwork.py", line 85, in __init__
    self.spaceSkyBox.setScale(3)
AttributeError: 'NoneType' object has no attribute 'setScale'

Am I doing somthing wrong or is this a coding error?

That’s because the program didn’t find the skybox model…

Did you download the rar file and unpack it into the models folder (including the folder in the rar, so the path should be something like Panda/models/skybox/)?

Anyway, I’ve already made some more updates on the code and the artwork…
I currently find it confusing myself, so I’ll add some structure to my files, then it’ll be easier to find the proper locations for all the files.

Currently the files go into the follwing folders:

Panda/python/

3dnetwork.py
3dnetwork.cfg
bumpMapper.sha
crosshair1.png
engine1.ogg
expl1.ogg
fire1.ogg
glowShader.sha
hit1.ogg
metal.jpg
nova.png
radar.png
stars.png

Panda/models/

fighter.egg
organicStation.egg
skybox/sphereSky.egg
station.egg
texturemap.png

As you can see through this list, there’s no real order in the files placement. That’ll change in the next few days and then the folder structure on the server will reflect the folder structure on the computer
As always, there have been some changes

-The keymapping can now be found in the config file(and changed)
-I’ve added a crosshair (that can be modified)
-changed the look of the radar
-added two Models for Space Stations
-changed the skybox
-experimented with shaders (for the explosions)
-changed movement (now it’s like a real space shooter, you can set speed, use afterburner…)
-tweaked the starfield effect while moving(it’ll now change according to your speed and flight direction)

I’m still trying to “unlag” the network code, but that’s a topic of it’s own.
Next on the list is to add MAPs (loading different space environements/skybox textures from a file).

I’m still in pre-alpha phase of development, so any feedback is appreciated.

Ok I see you changed everything as far as the directory structure. Why not just put everything into a zip or rar file with the files in the proper directory in the zip or rar. Then instruct the user to download the zip or rar into the panda3d directory. Unzip the file and check keep directory structure. This would be a lot easier then trying to determine where each file has to be put based on the structure on your server.

There’s a godd reason for that…
As I’ve already said, I’ve got some friends who help me testing the game out. And they don’t want to download the rar file, when I’m just updating the main code most of the time.

In case I update some artwork, I just tell them what other files to download.

A rar file isn’t very practical. Each time I update the code or some artwork I would have to change the files in the rar and then upload them. In the past few weeks I’ve update the files many times every day.

By the way, I didn’t only change the file structure today, but added a basic map structure and a new particle effect.

If someone wants to learn from the code, I suggest he first looks at the code posted here in order to understand the somewhat messy code on the server :wink:
I’ll start cleaning up the code and writing comments, when I’ve finished the work on this teaser… there’s still much to learn for me, especially using shaders and creating nice effects and graphics.
For the moment I’m still working on the basic game code (for example I just added the map structure today and I still have only one gun… not much of a game so far).
Considering that New Years Eve is just one month ahead, my work on the code is slowing down (this is the time of the year, where I’m practicing my pyromaniac skills… last year I prepared 2000 effects, this year it’ll be 2000 rockets alone, the plan is to reach at least 30 000 effects. And that takes serious work :wink: )

OK I redownloaded every file and made the proper directories and copied those files and directories under the panda3d-1.2.3 directory. I then ran
C:\Panda3D-1.2.3\python\3dnetwork.py and here is what I get.

 C:\Panda3D-1.2.3\python>ppython 3dnetwork.py
DirectStart: Starting the game.
Warning: DirectNotify: category 'Interval' already exists
Known pipe types:
  wdxGraphicsPipe8
(3 aux display modules not yet loaded.)
:display:wdxdisplay8(error): GetAvailableVidMem failed for device #0 at (c:\temp\mkpr\panda3d-1.2.3\
panda\src\dxgsg8\wdxGraphicsPipe8.cxx:300), hr=DDERR_NODIRECTDRAWHW: A hardware only DirectDraw obje
ct creation was attempted but the driver did not support any hardware.
:display:wdxdisplay8(error): GetAvailableVidMem failed for device #0 at (c:\temp\mkpr\panda3d-1.2.3\
panda\src\dxgsg8\wdxGraphicsPipe8.cxx:318), hr=DDERR_NODIRECTDRAWHW: A hardware only DirectDraw obje
ct creation was attempted but the driver did not support any hardware.
:display:wdxdisplay8(error): No DirectX 8 D3D-capable 3D hardware detected for device # 0 (VIA/S3G U
niChrome Pro IGP)!
:display:wdxdisplay8(error): no usable display devices.
Assertion failed: _d3d_device != NULL, file panda/src/dxgsg8/dxGraphicsStateGuardian8.cxx, line 1618


This application has requested the Runtime to terminate it in an unusual way.
Please contact the application's support team for more information.

This is frustrating. From reading this thread your program looks very interesting,but if a person cannot even get it to run whats the use. I know you are doing this on your own and that everyone here just programs becasue they like to program and love panda3d. But some documentation and testing would be nice. I mean I like what you said when you first posted but after three days I have yet to see anything on screen other then errors.

I’m sorry…
But see, without people like you (guys who permanently run into errors) programmers like us would be useless… :wink:
I can only develop programs on my own hardware and I have to rely on others to test my programs on their systems…

I’ve got to tell you, the error you’ve got never occured neither on my system, nor on the systems of my few test players so far.

BUT from what I’ve read in your posting it is an issue with your hardware.
What graphics card do you have?
The only thing I can think of, that can cause this trouble might be the used shaders. As the manual says, they can be problematic on hardware that doesn’t support them.
Theres also a “shaderTest” program on the server, that’ll print out if your hardware supports shaders or not. (This is printed on the prompt, so after the panda window opened check your prompt for a statement that includes “Shader…”)

In the meantime I’ll go over the code, remove all shaders and save it as “shaderless.py”. It’ll be the same code but without shaders, so you won’t have to download anything else, nor worry about the different file name.
I really hope that shaders cause this error on your system. Otherwise it might be a Panda Version issue… I’m coding on Panda 1.3.0 and currently it’s working for everybody who has at least version 1.3.0 and a graphics card like GeForce 5900…

Ok I do have a strange vidchip in this laptop. I turned ON hardware accleration, I had it off the last time. I normally keep it off in order to run blender. Here is what I got when I ran shaderTest,py.py(notice the , in the file name)

[code]
C:\Panda3D-1.2.3\python>ppython schaderTest,py.py
sys:1: DeprecationWarning: Non-ASCII character ‘\xfc’ in file schaderTest,py.py on line 20, but no e
ncoding declared; see http://www.python.org/peps/pep-0263.html for details
DirectStart: Starting the game.
Warning: DirectNotify: category ‘Interval’ already exists
Known pipe types:
wdxGraphicsPipe8
(3 aux display modules not yet loaded.)
keine shader verfâ

Ok. Here is the error message after running 3dnetwork.py with HW acceleration ON

C:\Panda3D-1.2.3\python>ppython 3dnetwork.py
DirectStart: Starting the game.
Warning: DirectNotify: category 'Interval' already exists
Known pipe types:
  wdxGraphicsPipe8
(3 aux display modules not yet loaded.)
:pgraph(warning): Using deprecated LightAttrib interface.
:loader(error): Couldn't load file models/skybox/sphereSky: not found on model path (which is curren
tly: ".;/c/Panda3D-1.2.3/etc/..;/c/Panda3D-1.2.3/etc/../models")
Traceback (most recent call last):
  File "3dnetwork.py", line 1843, in ?
    s=main()
  File "3dnetwork.py", line 603, in __init__
    self.world.loadMap('maps/dm1.map')
  File "3dnetwork.py", line 165, in loadMap
    self.spaceSkyBox.setScale(70.5)
AttributeError: 'NoneType' object has no attribute 'setScale'

No problem…

I’m sorry agin, I forgot that I’ve written this shader test in my own language, so you couldn’t understand that one line it printef:

[code]

keine shader verfâ

Okay… That means he didn’t find the model for the skybox. Make sure it is in PANDAROOT/models/skybox/sphereSky.egg

Ok I moved downloaded skysphere.egg and put it in the proper directory here is the result of running shaderless.py

C:\Panda3D-1.2.3\python>ppython shaderless.py
DirectStart: Starting the game.
Warning: DirectNotify: category 'Interval' already exists
Known pipe types:
  wdxGraphicsPipe8
(3 aux display modules not yet loaded.)
:display:windisplay(warning): SetForegroundWindow() failed!
:display:wdxdisplay8(warning): SetForegroundWindow() failed!
:pgraph(warning): Using deprecated LightAttrib interface.
:pgraph(warning): Using deprecated LightAttrib interface.
['station', '-3000', '0', '0', '100', '0', '0', '0\n']
['organicStation', '5000', '0', '0', '100', '0', '0', '0\n']
Network INIT
Traceback (most recent call last):
  File "shaderless.py", line 1843, in ?
    s=main()
  File "shaderless.py", line 610, in __init__
    self.explosion()
  File "shaderless.py", line 764, in explosion
    self.p1.renderer.getColorInterpolationManager().addSinusoid(0.0,1.0,Vec4(0.0,0.0,1.0,1.0),Vec4(1
.0,0.0,0.0,1.0),1.0,1)
TypeError: ColorInterpolationManager::add_sinusoid() takes 1, 2, 3, 4, 5, or 6 arguments (7 given)

Here is the result of running 3dnetwork.py:

C:\Panda3D-1.2.3\python>ppython 3dnetwork.py
DirectStart: Starting the game.
Warning: DirectNotify: category 'Interval' already exists
Known pipe types:
  wdxGraphicsPipe8
(3 aux display modules not yet loaded.)
:pgraph(warning): Using deprecated LightAttrib interface.
:pgraph(warning): Using deprecated LightAttrib interface.
['station', '-3000', '0', '0', '100', '0', '0', '0\n']
['organicStation', '5000', '0', '0', '100', '0', '0', '0\n']
Network INIT
Traceback (most recent call last):
  File "3dnetwork.py", line 1843, in ?
    s=main()
  File "3dnetwork.py", line 610, in __init__
    self.explosion()
  File "3dnetwork.py", line 764, in explosion
    self.p1.renderer.getColorInterpolationManager().addSinusoid(0.0,1.0,Vec4(0.0,0.0,1.0,1.0),Vec4(1
.0,0.0,0.0,1.0),1.0,1)
TypeError: ColorInterpolationManager::add_sinusoid() takes 1, 2, 3, 4, 5, or 6 arguments (7 given)

Both were run with HW accleration ON.

I am happy to help you test. I just hope I can help you get it working and show you some of the things you might run into on a release version

And if it helps here is the result again of running schadertTest,py.py with HW accleration on and with the skysphere.egg file in the proper directory.

[code]
C:\Panda3D-1.2.3\python>ppython schaderTest,py.py
sys:1: DeprecationWarning: Non-ASCII character ‘\xfc’ in file schaderTest,py.py on line 20, but no e
ncoding declared; see http://www.python.org/peps/pep-0263.html for details
DirectStart: Starting the game.
Warning: DirectNotify: category ‘Interval’ already exists
Known pipe types:
wdxGraphicsPipe8
(3 aux display modules not yet loaded.)
keine shader verfâ

Okay, you still got no shaders…

but that doesn’t mind if you run shaderless.py

Problem is, he’s complaining about the particle system, although this was directly created with the particle panel (that writes working code into a file that you can copy to have the created particle effect anywhere).

This might actually be a Version issue… I don’t know if the particle engine was used differently in Versions prior to 1.3.0…

Have you ever considered updating your Panda Version?

I currently can’t think of anything else that might cause the problem.
I could offer you to remove particle effects as well… but I’ll do that tomorrow, it’s late already.

Ok I downloaded panda3d 1.3.1. I ran the executable and installed it right over my present Panda3d-1.2.3 directory. Then I just renamed the folder and adjusted the system path.

I then ran 3dnetwork.py and viola I got a sloting in space scene with what looks like asteroids and a transparent bullseye.

Here is the output from the console window

C:\Panda3D-1.3.1\python>ppython 3dnetwork.py
DirectStart: Starting the game.
Warning: DirectNotify: category 'Interval' already exists
Known pipe types:
  wdxGraphicsPipe8
(3 aux display modules not yet loaded.)
:util(warning): Adjusting global clock's real time by 2.7332 seconds.
:pgraph(warning): Using deprecated LightAttrib interface.
:pgraph(warning): Using deprecated LightAttrib interface.
['station', '-3000', '0', '0', '100', '0', '0', '0\n']
['organicStation', '5000', '0', '0', '100', '0', '0', '0\n']
Network INIT
:util(warning): Adjusting global clock's real time by -3.17892 seconds.
Traceback (most recent call last):
  File "3dnetwork.py", line 1846, in ?
    run()
  File "C:\Panda3D-1.2.3\direct\src\showbase\ShowBase.py", line 2028, in run
  File "C:\Panda3D-1.2.3\direct\src\task\Task.py", line 839, in run
  File "C:\Panda3D-1.2.3\direct\src\task\Task.py", line 787, in step
  File "C:\Panda3D-1.2.3\direct\src\task\Task.py", line 721, in __stepThroughList
  File "C:\Panda3D-1.2.3\direct\src\task\Task.py", line 644, in __executeTask
  File "C:\Panda3D-1.2.3\direct\src\showbase\EventManager.py", line 38, in eventLoopTask
  File "C:\Panda3D-1.2.3\direct\src\showbase\EventManager.py", line 32, in doEvents
  File "C:\Panda3D-1.2.3\direct\src\showbase\EventManager.py", line 88, in processEvent
  File "C:\Panda3D-1.2.3\direct\src\showbase\Messenger.py", line 223, in send
  File "3dnetwork.py", line 1074, in fgun
    self.sndNodes[self.myID+'-fire'].setVolume(0.25)
KeyError: '345-fire'

The only problem is that it did not seem to respond to any of the keys you mentioned in your first post. I tried pressing 1 then 2 as you said in your first post but nothing happend. But Hey this is progress :slight_smile:

Well, finally… :wink:

From what you’ve written, I can tell what the mistake was this time…

If you press 1 and 2 in the same application the program tries to be server and client at the same time. And that won’t work. :wink:

If you press 1 you’re server, and as such you can explore the space scene.
Pressing 2 won’t do you any good unless theres another one who already is server.
So if you want to have server and client on the same system at the same time you’ll have to start the 3dnetwork.py two times!

But anyway, the key setting changed since my very first post.
You can now control the ship with your mouse mouse(that of course only when you are server, or a client connected to a server. before you chose to be either server or client you won’t have any mouse control.)

Despite movement with the mouse, the rest of the keysettings are defined in the 3dnetwork.cfg, and can be changed to whatever you like (except for the mouse movement though).

You could also change the crosshair in the cfg if you’ve got a transparent png (view the crosshair1.png in gimp, there you might edit it for your needs and save it as crosshair2.png or something like that).
You can change everything in the cfg, just make sure to follow the syntax. (variable name=value - without any spaces between the name, the = and the value…).
From time to time the config file will be updated, too… as I might add some more variables.

Now I hope you can run this thing without any further problem :slight_smile: