Index: framspy/gui.py
===================================================================
--- framspy/gui.py	(revision 1198)
+++ framspy/gui.py	(revision 1198)
@@ -0,0 +1,19 @@
+from gui.widgets.mainPage import MainPage
+import sys, argparse
+
+def main():
+    parser = argparse.ArgumentParser(description="Framsticks GUI for library/server")
+    parser.add_argument("-l", "--library", help="<path>: load Framsticks library (dll/so/dylib) from the provided path")
+    parser.add_argument("-s", "--server", help="<address:port>: connect to the running Framsticks server at the provided address, e.g. localhost:9009")
+
+    args = parser.parse_args()
+
+    if args.server and args.library:
+        print("Do not provide both the library path and the network server address.")
+        sys.exit(2)
+
+    app = MainPage(None, args.server, args.library)
+    app.mainloop()
+
+if __name__ == '__main__':
+    main()
Index: framspy/gui/README.md
===================================================================
--- framspy/gui/README.md	(revision 1198)
+++ framspy/gui/README.md	(revision 1198)
@@ -0,0 +1,101 @@
+# Python GUI for Framsticks library and server
+
+## Table of contents
+
+- [About](#about)
+- [Running](#running)
+- [Sources and directories](#sources-and-directories)
+- [Threading](#threading)
+- [Development](#development)
+
+
+
+## About
+
+This GUI offers basic features when connecting to a native Framsticks library (```dll/so/dylib```) or the [Framsticks server](https://www.framsticks.com/common/server.html). While there exist native GUIs for Framsticks which are much faster and offer more features, the advantage of this GUI is that it is written entirely in Python (thus being portable), it is entirely open-source, and can be freely modified or serve as an implementation example. This GUI is also completely separate from the Framsticks simulator (either the library or the server).
+
+
+
+
+## Running
+
+Run the ```gui.py``` script.
+
+
+
+
+## Sources and directories
+
+```text
+framspy/
+├── frams.py
+├── gui.py
+└── gui/
+    ├── framsutils/
+    ├── res/
+    │   ├── icons/
+    │   ├── img/
+    │   └── obj/
+    ├── tests/
+    ├── visual/
+    │   └── shaders/
+    └── widgets/
+```
+
+- ```framsutils``` contains all classes used for low-level data manipulation.
+- ```res``` is a resource directory.
+- ```tests``` contains basic test classes.
+- ```visual``` contains all OpenGL-related classes and files (shaders).
+- ```widgets``` contains all tkinter widgets.
+
+
+
+
+## Threading
+
+The GUI runs the ```Tkinter``` main loop in the main thread.
+
+```glFrame``` runs an additional thread to periodically and asynchronously obtain creatures data to render without freezing the render window. This is especially required for the socket connection, because requesting data is time-consuming relatively to rendering.
+
+```FramsSocket``` runs the communication module with ```asyncio``` in another thread to provide fast and concurrent network communication. 
+
+The communication module uses another thread to detect asynchronous events from server and to call an adequate callback.
+
+
+
+
+
+## Development
+
+### Communication
+
+If you want to add another feature to socket communication, send feature negotiation request in ```CommWrapper```'s *start* method in ```comm.py```.
+
+There is no generic system for registering for new events in the socket communication. If you want to register for a new event, add callback in ```EventConsumer```'s *run* method in ```comm.py``` and register for an event in ```FramsSocket```'s *initConnection* method in ```FramsSocket.py```.
+
+### UI
+
+All property features for input widgets are handled in ```framsProperty.py``` in the *propertyToTkinter* function. 
+
+All widget's definitions for the main window are added in the *\_\_init\_\_* method of the ```MainPage``` class in ```mainPage.py```. This method is divided into sections:
+- OPENGL FRAME
+- SIDE FRAME
+    - CONTROL PANEL
+    - TREEVIEW
+- STATUS BAR
+- MENU BAR
+- WINDOW
+- ORGANIZE WINDOWS POSITIONS for all displayed windows
+
+If you need to ask for more fields in ```ListGenePoolWindow``` or ```ListPopulationsWindow```, add their ids to *self.fields* list. If you want to show them as columns, make sure to add them in the right order at the begining of the list, because only *len(self.headers)* first fields with *self.headers* headers are shown. Also fill *self.headers_width* with the correct percentage value of the initial window width, for example:
+```python
+fields = ["name", "genotype", "numjoints", "index", "uid"]
+headers = ["Name", "Genotype"]
+headers_width = [0.5, 0.5]
+```
+The above example asks for *name*, *genotype*, *numjoints*, *index*, *uid*, but only shows *name* in the *Name* column and *genotype* in the *Genotype* column with 50% and 50% widths for each column. The remaining fields can be used during the preparation of the row.
+
+### Contributing
+
+There are a number of "TODO" items (features, bugs) for this Python GUI described [here](https://docs.google.com/document/d/1KeY6RceeeiPDkt_Z6S2GihpcA4YsfujNfk3AErvXP3Q/edit?usp=sharing). Contact ```support@framsticks.com``` to learn more about the development.
+
Index: framspy/gui/framsutils/FramsInterface.py
===================================================================
--- framspy/gui/framsutils/FramsInterface.py	(revision 1198)
+++ framspy/gui/framsutils/FramsInterface.py	(revision 1198)
@@ -0,0 +1,174 @@
+from typing import Dict, List, Any, Tuple
+from abc import ABC, abstractmethod
+import enum
+from xmlrpc.client import Boolean
+from gui.framsutils.creature import Creature
+from gui.framsutils.framsProperty import Property
+import glm
+
+class TreeNode:
+	def __init__(self, node: Property, children: List[Property] = None, parent: "TreeNode" = None) -> None:
+		self.node = node
+		self.parent = parent
+		self.children = []
+		if children is not None:
+			for child in children:
+				self.addChild(child)
+
+	def addChild(self, child):
+		assert isinstance(child, TreeNode)
+		self.children.append(child)
+
+	def setParent(self, parent):
+		self.parent = parent
+
+class InterfaceType(enum.Enum):
+	SOCKET = 1
+	LIB = 2
+
+class FramsInterface(ABC):
+	interfaceType: InterfaceType
+
+	@abstractmethod
+	def connect(self, address: str, port: int):
+		raise NotImplementedError
+
+	@abstractmethod
+	def disconnect(self):
+		raise NotImplementedError
+
+	@abstractmethod
+	def start(self):
+		raise NotImplementedError
+		
+	@abstractmethod
+	def getError(self):
+		raise NotImplementedError
+	
+	@abstractmethod
+	def stop(self):
+		raise NotImplementedError
+		
+	@abstractmethod
+	def step(self):
+		raise NotImplementedError
+		
+	@abstractmethod
+	def listTree(self, path="/") -> List[Property]:
+		raise NotImplementedError
+		
+	@abstractmethod
+	def listTreeList(self, path) -> List[Property]:
+		raise NotImplementedError
+
+	@abstractmethod
+	def makeInfoTree(self) -> TreeNode:
+		raise NotImplementedError
+	
+	@abstractmethod
+	def readCreatures(self, colors: Boolean) -> List[Creature]:
+		"""read all creatures to render"""
+		raise NotImplementedError
+
+	@abstractmethod
+	def readGenePoolsGroups(self) -> Dict[str, int]:
+		"""returns dict of {name, index} of gene pools groups."""
+		raise NotImplementedError
+
+	@abstractmethod
+	def readGenePools(self, props: List[str]) -> Dict[str, List[Dict[str, Any]]]:
+		"""returns list of props and values for every gene pool group in format {gp group, [{prop, value}]}."""
+		raise NotImplementedError
+
+	@abstractmethod
+	def readPopulationsGroups(self) -> Dict[str, int]:
+		"""returns dict of {name, index} of populations groups."""
+		raise NotImplementedError
+
+	@abstractmethod
+	def readPopulations(self, props: List[str]) -> Dict[str, List[Dict[str, Any]]]:
+		"""returns list of props and values for every population group in format {pop group, [{prop, value}]}."""
+		raise NotImplementedError
+
+	@abstractmethod
+	def readCreatureDetails(self, group: int, index: str) -> List[Property]:
+		"""read all details of creature specified by group and index."""
+		raise NotImplementedError
+
+	@abstractmethod
+	def readGenotypeDetails(self, group: int, index: str) -> List[Property]:
+		"""read all details of genotype specified by group and index."""
+		raise NotImplementedError
+
+	@abstractmethod
+	def readParameterDetails(self, prop: str) -> List[Property]:
+		"""read all property details, prop is the standard server path."""
+		raise NotImplementedError
+
+	@abstractmethod
+	def writeCreatureDetail(self, creatureNo: str, prop: str, value: str):
+		"""write value to property of creature."""
+		raise NotImplementedError
+
+	@abstractmethod
+	def writeGenotypeDetail(self, genotypeNo: str, prop: str, value: str):
+		"""write value to property of genotype."""
+		raise NotImplementedError
+
+	@abstractmethod
+	def writeParameterDetail(self, path: str, prop: str, value: str):
+		"""write value to property of path."""
+		raise NotImplementedError
+
+	@abstractmethod
+	def getMotd(self) -> str:
+		"""get message of the day."""
+		raise NotImplementedError
+
+	@abstractmethod
+	def loadFile(self, path: str) -> None:
+		"""load file from path into frams."""
+		raise NotImplementedError
+
+	@abstractmethod
+	def importFile(self, path: str, options: int) -> None:
+		"""import file from path into frams with options."""
+		raise NotImplementedError
+
+	@abstractmethod
+	def saveFile(self, path: str, options: int) -> None:
+		"""save file into path from frams with options."""
+		raise NotImplementedError
+
+	@abstractmethod
+	def getWorldType(self) -> int:
+		"""get type of the world."""
+		raise NotImplementedError
+
+	@abstractmethod
+	def getWorldSize(self) -> float:
+		raise NotImplementedError
+
+	@abstractmethod
+	def getWorldWaterLevel(self) -> float:
+		raise NotImplementedError
+
+	@abstractmethod
+	def getWorldBoundaries(self) -> int:
+		raise NotImplementedError
+
+	@abstractmethod
+	def getWorldMap(self) -> str:
+		"""get geometry of the world."""
+		raise NotImplementedError
+
+	@abstractmethod
+	def getSimtype(self) -> int:
+		raise NotImplementedError
+
+	@abstractmethod
+	def getFPSDefinitions(self) -> List[Tuple[int, int]]:
+		"""get list of the fps definitions for fps combobox
+		first value determines fps limit, second number of steps per frame.
+		"""
+		raise NotImplementedError
Index: framspy/gui/framsutils/FramsLib.py
===================================================================
--- framspy/gui/framsutils/FramsLib.py	(revision 1198)
+++ framspy/gui/framsutils/FramsLib.py	(revision 1198)
@@ -0,0 +1,343 @@
+import sys
+#sys.path.append("..") # why was needed?
+from typing import List, Dict, Any
+import frams
+from .creature import Creature
+from gui.framsutils.framsProperty import Property
+import os
+import glm
+from .utils import parseNeuronDToOrient
+
+class FramsLib:
+	def __init__(self):
+		self.frams = frams
+		self.connected = False
+
+	def initConnection(self, frams_path: str) -> None:
+		self.frams.init(frams_path, "-t") #enable ExtValue thread synchronization with "-t"
+		#self.messageCatcher = self.frams.MessageCatcher.new() # experiments with catching messages so that they could be later displayed in GUI... but let's rather print them on console
+		#self.messageCatcher.store = 1
+		self.connected = True
+		
+	def closeConnection(self) -> None:
+		self.connected = False
+
+	def getError(self) -> None or str:
+		#if self.messageCatcher.error_count._value() > 0:
+		#	return self.messageCatcher.messages._value()
+		return None
+
+	def writeCreatureFromString(self, groupNo: int, creature: str) -> None:
+		if self.connected:
+			self.frams.Populations[groupNo].add(creature)
+
+	def step(self):
+		if self.connected:
+			self.frams.Simulator.step()
+
+	def start(self):
+		if self.connected:
+			self.frams.Simulator.start()
+
+	def stop(self):
+		if self.connected:
+			self.frams.Simulator.stop()
+
+	def info(self, path:str = "/") -> List[Property]:
+		properties: List[Property] = []
+
+		if self.connected:
+			if path.startswith('/'):
+				path = path[1:]
+
+			c = 0 if path == "" else path.count("/") + 1
+
+			if path.endswith('/'):
+				path = path[:-1]
+
+			if c > 0:
+				path += '/'
+
+			current = self.frams.sim_params
+			G = current._groupCount()
+			for g in range(G):
+				Id = current._groupName(g)
+				name = Id.rsplit(': ', 1)[-1]
+				Id = Id.replace(": ", "/")
+				tmpId = Id + "/"
+				prop = Property(Id=Id, Name=name, Type="o")
+				if(Id.count('/') == c and tmpId.startswith(path)):
+					prop.p["id"] = Id.split('/')[-1]
+					properties.append(prop)
+
+		return properties
+
+	def infoList(self, path: str) -> List[Property]:
+		properties: List[Property] = []
+		raise Exception("unimplemented/unwanted")
+		return properties
+
+	def _VstyleParserColor(self, style: str):
+		f = "color=0x"
+		idx = style.find(f)
+		if idx > -1:
+			hx = style[idx+len(f):idx+len(f)+6]
+			r = int(hx[0:2], 16) / 255.0
+			g = int(hx[2:4], 16) / 255.0
+			b = int(hx[4:6], 16) / 255.0
+			return r, g, b
+		return None, None, None
+
+	def readCreatures(self, groupNo: int, creatureNo: int, colors):
+		ret_creatures: List[Creature] = []
+		if self.connected:
+			groups = [self.frams.Populations[groupNo]] if groupNo != '+' else self.frams.Populations
+			for group in groups:
+				creatures = [group[creatureNo]] if groupNo != '+' else group
+				for c in creatures:
+					creature = Creature(c.population.index._int(), c.index._int())
+					numparts = c.numparts._int()
+					numjoints = c.numjoints._int()
+					numneurons = c.numneurons._int()
+
+					vstyle = c.model.Vstyle._value()
+					modelR, modelG, modelB = self._VstyleParserColor(vstyle)
+
+					mechPart = [0,0,0]
+					for i in range(numparts):
+						if i >= c.numparts._int():
+							break
+						mp = c.getMechPart(i)
+						mechPart = [mp.x._double(), mp.y._double(), mp.z._double()]
+						creature.mechParts.append(mechPart)
+						if colors:
+							vstyle = mp.part.Vstyle._value()
+							r, g, b = self._VstyleParserColor(vstyle)
+
+							tr = mp.part.vr._value()
+							tg = mp.part.vg._value() 
+							tb = mp.part.vb._value()
+
+							if modelR is not None:
+								tr = modelR
+								tg = modelG
+								tb = modelB
+
+							if r is not None:
+								tr = r
+								tg = g
+								tb = b
+							
+							creature.colorsPart.append(glm.vec3(tr, tg, tb))
+							
+						creature.stylePart.append(mp.part.Vstyle._value())
+						orient = mp.orient.toVector.toString._value()
+						orient = orient[orient.find('[')+1:orient.find(']')]
+						R = glm.mat4(glm.mat3(*[float(x) for x in orient.split(',')]))
+						creature.partOrient.append(R)
+					for i in range(numjoints):
+						if i >= c.numjoints._int():
+							break
+						j = c.getJoint(i)
+						joint = [j.p1._int(), j.p2._int()]
+						creature.joints.append(joint)
+						if colors:
+							vstyle = mp.part.Vstyle._value()
+							r, g, b = self._VstyleParserColor(vstyle)
+
+							tr = j.vr._value()
+							tg = j.vg._value() 
+							tb = j.vb._value()
+
+							if r is not None:
+								tr = r
+								tg = g
+								tb = b
+							
+							creature.colorsJoint.append(glm.vec3(tr, tg, tb))
+
+						creature.styleJoint.append(j.Vstyle._value())
+
+					for i in range(numneurons):
+						if i >= c.numneurons._int():
+							break
+						n = c.getNeuroDef(i)
+						d = n.d._value()
+						creature.neurons.append((n.p._value(), n.j._value()))
+						creature.styleNeuron.append(d.split(':')[0])
+						
+						rorient = parseNeuronDToOrient(d)
+
+						creature.neuronRelativeOrient.append(rorient)
+					ret_creatures.append(creature)
+		return ret_creatures
+
+	def readGenePoolsGroups(self) -> Dict[str, int]:
+		genotypes = {}
+		if self.connected:
+			for gp in self.frams.GenePools:
+				genotypes[getattr(gp, "name")._value()] = getattr(gp, "index")._value()
+		return genotypes
+
+	def readGenePools(self, props: List[str]) -> Dict[str, List[Dict[str, Any]]]:
+		genotypes = {}
+		if self.connected:
+			for gp in self.frams.GenePools:
+				group = gp.name._value()
+				genotypes[group] = []
+				for g in gp:
+					genotype = {}
+					for p in props:
+						genotype[p] = getattr(g, p)._value()
+					genotype["group"] = group
+					genotypes[group].append(genotype)
+		return genotypes
+
+	def readPopulationsGroups(self) -> Dict[str, int]:
+		creatures = {}
+		if self.connected:
+			for p in self.frams.Populations:
+				creatures[getattr(p, "name")._value()] = getattr(p, "index")._value()
+
+		return creatures
+
+	def readPopulations(self, props: List[str]) -> Dict[str, List[Dict[str, Any]]]:
+		creatures = {}
+		if self.connected:
+			for p in self.frams.Populations:
+				group = p.name._value()
+				creatures[group] = []
+				for c in p:
+					creature = {}
+					for p in props:
+						creature[p] = getattr(c, p)._value()
+					creature["group"] = group
+					creatures[group].append(creature)
+		return creatures
+
+	def readCreatureInfo(self, groupNo: int, creatureNo: str) -> List[Property]:
+		properties: List[Property] = []
+		if self.connected:
+			creature = next(pop for group in self.frams.Populations for pop in group if pop.uid._string() == creatureNo)
+			G = creature._groupCount()
+			for g in range(G):
+				groupName = creature._groupName(g)
+				for m in range(creature._memberCount(g)):
+					p = creature._groupMember(g, m)
+					t = creature._propType(p)
+					if t[0] and t[0] not in "xople":
+						prop = Property(creature._propId(p), creature._propName(p), t)
+						prop.p["flags"] = creature._propFlags(p)
+						prop.p["value"] = getattr(creature, creature._propId(p))._string()
+						prop.p["help"] = creature._propHelp(p)
+						prop.p["group"] = groupName
+						properties.append(prop)
+
+		return properties
+
+	def readGenotypeInfo(self, groupNo: int, genotypeNo: str) -> List[Property]:
+		properties: List[Property] = []
+		if self.connected:
+			genotype = next(gen for group in self.frams.GenePools for gen in group if gen.uid._string() == genotypeNo)
+			G = genotype._groupCount()
+			for g in range(G):
+				groupName = genotype._groupName(g)
+				for m in range(genotype._memberCount(g)):
+					p = genotype._groupMember(g, m)
+					t = genotype._propType(p)
+					if t[0] and t[0] not in "xople":
+						prop = Property(genotype._propId(p), genotype._propName(p), t)
+						prop.p["flags"] = genotype._propFlags(p)
+						prop.p["value"] = getattr(genotype, genotype._propId(p))._string()
+						prop.p["help"] = genotype._propHelp(p)
+						prop.p["group"] = groupName
+						properties.append(prop)
+
+		return properties
+
+	def readPropertyInfo(self, path: str) -> List[Property]:
+		properties: List[Property] = []
+		if self.connected:
+			if path.startswith('/'):
+				path = path[1:]
+
+			c = path.count("/")
+
+			if path.endswith('/'):
+				path = path[:-1]
+
+			current = self.frams.sim_params
+			G = current._groupCount()
+			for g in range(G):
+				Id = current._groupName(g).replace(": ", "/")
+				groupName = current._groupName(g)
+				if(Id.count('/') == c and Id == path):
+					for m in range(current._memberCount(g)):
+						p = current._groupMember(g, m)
+						t = current._propType(p)
+						if t[0] and t[0] not in "xole":
+							prop = Property(current._propId(p), current._propName(p), t)
+							if not prop.p["id"].startswith("_"):
+								prop.p["flags"] = current._propFlags(p)
+								prop.p["value"] = getattr(current, current._propId(p)) if t[0] == 'p' else getattr(current, current._propId(p))._string()
+								prop.p["help"] = current._propHelp(p)
+								prop.p["v"] = current
+								prop.p["group"] = groupName
+								properties.append(prop)
+					break
+
+		return properties
+
+	def writeCreatureInfo(self, uid: str, prop: str, value: str) -> bool:
+		if self.connected:
+			creature = next(pop for group in self.frams.Populations for pop in group if pop.uid._string() == uid)
+			creature.__setattr__(prop, value)
+			return True
+		return False
+
+	def writeGenotypeInfo(self, uid: str, prop: str, value: str) -> bool:
+		if self.connected:
+			genotype = next(gen for group in self.frams.GenePools for gen in group if gen.uid._string() == uid)
+			genotype.__setattr__(prop, value)
+			return True
+		return False
+
+	def writePropertyInfo(self, path: str, prop: str, value: str):
+		if self.connected:
+			current = self.frams.sim_params
+			current.__setattr__(prop, value)
+			return True
+		return False
+
+	def getSimulationStatus(self) -> bool:
+		if self.connected:
+			return True if self.frams.Simulator.running._int() == 1 else False
+		return False
+
+	def loadFile(self, path: str) -> None:
+		self.frams.Simulator.load(path)
+
+	def importFile(self, path: str, options: int) -> None:
+		self.frams.Simulator.ximport(path, options)
+
+	def saveFile(self, path: str, options: int) -> None:
+		path = os.path.relpath(path, self.frams.res_dir + "\\scripts_output")
+		self.frams.Simulator.export(path, options, -1, -1)
+
+	def getWorldType(self) -> int:
+		return self.frams.World.wrldtyp._value()
+
+	def getWorldSize(self) -> float:
+		return self.frams.World.wrldsiz._value()
+
+	def getWorldWaterLevel(self) -> float:
+		return self.frams.World.wrldwat._value()
+
+	def getWorldBoundaries(self) -> int:
+		return self.frams.World.wrldbnd._value()
+
+	def getWorldMap(self) -> str:
+		return self.frams.WorldMap.getAsString("", "")._value()
+
+	def getSimtype(self) -> int:
+		return self.frams.World.simtype._value()
Index: framspy/gui/framsutils/FramsSocket.py
===================================================================
--- framspy/gui/framsutils/FramsSocket.py	(revision 1198)
+++ framspy/gui/framsutils/FramsSocket.py	(revision 1198)
@@ -0,0 +1,549 @@
+from typing import List, Dict, Any, Tuple, Callable
+from .comm import CommWrapper
+from .creature import Creature
+from gui.framsutils.framsProperty import Property
+from framsfiles import reader
+import threading
+import asyncio
+from functools import partial
+import time
+import glm
+from .utils import parseNeuronDToOrient
+
+class FramsSocket:
+	def __init__(self):
+		self.comm = CommWrapper()
+		self.refreshControlButtonsCallback = None
+		self.refreshTreeviewCallback = None
+		self.loop = asyncio.get_event_loop()
+		self.loop_thread = threading.Thread(target=self.loop.run_forever)
+		self.loop_thread.start()
+		
+	def initConnection(self, address: str = '127.0.0.1', port: int = 9009) -> None:
+		self.comm = CommWrapper()
+		i = 0
+		time_delta = 0.1
+		total_time = 10
+		self.comm.start(address, port)
+		while self.comm.client.connecting == True and i < total_time / time_delta:
+			time.sleep(time_delta)
+			i += 1
+		response = self.comm.client.connected
+		if response:
+			self.comm.client.consumer.runningChangeEventCallback = self._runningChangeEventCallback
+			self.comm.client.consumer.populationsGroupChangeEventCallback = self._populationsGroupChangeEventCallback
+			self.comm.client.consumer.genepoolsGroupChangeEventCallback = self._genepoolsGroupChangeEventCallback
+			self.sendRequest("reg /simulator/populations/groups_changed")
+			self.sendRequest("reg /simulator/genepools/groups_changed")
+			self.sendRequest("reg /simulator/running_changed")
+			self.sendRequest("reg /cli/messages")
+		else:
+			self.comm.close()
+			raise ConnectionError()
+
+	def closeConnection(self) -> None:
+		if self.comm:
+			self.comm.stop()
+			self.loop.call_soon_threadsafe(self.loop.stop)
+			self.loop_thread.join()
+
+	def sendRequest(self, request: str, timeout = 1.0, return_index = False, index=-1) -> List[str]:
+		response = []
+
+		if self.comm.connected:
+			if self.comm:
+				idx = self.comm.write(request, index=index)
+				future = asyncio.run_coroutine_threadsafe(self.comm.read(idx, timeout), self.loop)
+				try:
+					response = future.result(timeout)
+					response = [e+'\n' for e in response.split('\n')]
+				except asyncio.TimeoutError:
+					future.cancel()
+				except Exception as ex:
+					print("dropped request id {}, exception: {}".format(idx, ex))
+
+		append = False
+		change = False
+		for i in range(len(response) - 2, -1, -1):
+			if response[i].find('~\n') >= 0 and response[i].find('\~\n') < 0:
+				response[i].replace('~\n', '')
+				change = True
+
+			if append and not change:
+				response[i] = response[i] + response.pop(i + 1) #merge middle line
+			elif append and change:
+				response[i] = response[i] + response.pop(i + 1) #merge last line
+
+			if change:
+				append = not append
+				change = False
+		if return_index:
+			return response, idx
+		return response
+
+	def writeCreatureFromString(self, groupNo: int, creature: str) -> None:
+		creatureString = "call /simulator/populations/groups/{} createFromString {}".format(groupNo, creature)
+		self.sendRequest(creatureString)
+
+	def step(self):
+		self.sendRequest("call /simulator step")
+
+	def start(self):
+		self.sendRequest("call /simulator start")
+
+	def stop(self):
+		self.sendRequest("call /simulator stop")
+
+	def info(self, path: str = "/") -> List[Property]:
+		response = self.sendRequest("info {}".format(path))
+		properties = self._infoParser(response)
+		return properties
+
+	def infoList(self, path: str) -> List[Property]:
+		response = self.sendRequest("get {}".format(path))
+		properties = self._infoParser(response)
+		return properties
+
+	def _infoParser(self, response: List[str], packToProperty = True) -> List[Property]:
+		properties: List[Property] = []
+
+		tmp = reader.loads(''.join(response))
+		if packToProperty:
+			properties = [Property(p=x) for x in tmp]
+			return properties
+		return tmp
+
+	def _VstyleParserColor(self, style: str):
+		f = "color=0x"
+		idx = style.find(f)
+		if idx > -1:
+			hx = style[idx+len(f):idx+len(f)+6]
+			r = int(hx[0:2], 16) / 255.0
+			g = int(hx[2:4], 16) / 255.0
+			b = int(hx[4:6], 16) / 255.0
+			return r, g, b
+		return None, None, None
+
+	def readCreatures(self, groupNo: int, creatureNo: int, color):
+		creatures: List[Creature] = []
+		if color:
+			creatureString = "get /simulator/populations/groups/{}/creatures/{}(/mechparts{{x,y,z,orient}},/joints{{p1,p2,vr,vg,vb,Vstyle}},/parts{{vr,vg,vb,Vstyle}},/neurodefs{{p,j,d}},{{model_Vstyle}})".format(groupNo, creatureNo)
+		else:
+			creatureString = "get /simulator/populations/groups/{}/creatures/{}(/mechparts{{x,y,z,orient}},/joints{{p1,p2}},/neurodefs{{p,j,d}})".format(groupNo, creatureNo)
+		response = self.sendRequest(creatureString)
+
+		mechParts = []
+		joints = []
+		partColors = []
+		neurons = []
+		neuronStyles = []
+		creature: Creature = None
+
+		mechPart: List[float] = [0,0,0]
+		joint: List[int] = [0,0]
+		partColor: glm.vec3 = glm.vec3(0,0,0)
+		neuron: Tuple(int, int) = (-1,-1)
+
+		files = [i for i, x in enumerate(response) if x.startswith("file")]
+		files.append(-1)
+
+		for i in range(len(files)-1):
+			file = response[files[i]]
+
+			groupsStr = "groups/"
+			groupBegin = file.find(groupsStr) + len(groupsStr)
+			groupEnd = file.find("/", groupBegin)
+			group = int(file[groupBegin:groupEnd])
+
+			creaturesStr = "creatures/"
+			creatureIndexBegin = file.find(creaturesStr) + len(creaturesStr)
+			creatureIndexEnd = file.find("/", creatureIndexBegin)
+			creatureIndex = int(file[creatureIndexBegin:creatureIndexEnd])
+
+			creature = next((x for x in creatures if x.group == group and x.index == creatureIndex), None)
+			if not creature:
+				creature = Creature(group, creatureIndex)
+				creatures.append(creature)
+
+			mechParts = []
+			joints = []
+			partColors = []
+			jointColors = []
+			neurons = []
+			neuronStyles = []
+			partOrient = []
+			neuroRelativeOrient = []
+
+			resp = self._infoParser(response[files[i]:files[i+1]], False)
+
+			for p in resp:
+				if p["_classname"] == "MechPart":
+					mechPart = [p['x'], p['y'], p['z']]
+					orient = p["orient"]
+					orient = orient[orient.find('[')+1:orient.find(']')]
+					orient = glm.mat4(glm.mat3(*[float(x) for x in orient.split(',')]))
+					mechParts.append(mechPart)
+					partOrient.append(orient)
+				elif p["_classname"] == "Joint":
+					joint = [p["p1"], p["p2"]]
+					joints.append(joint)
+					
+					if color:
+						jointColor = glm.vec3(p["vr"], p["vg"], p["vb"])
+						colors = self._VstyleParserColor(p["Vstyle"])
+						if colors and colors[0] != None:
+							jointColor = glm.vec3(colors)
+
+						jointColors.append(jointColor)
+				elif p["_classname"] == "Part":
+					partColor = glm.vec3(p["vr"], p["vg"], p["vb"])
+
+					colors = self._VstyleParserColor(p["Vstyle"])
+					if colors and colors[0] != None:
+						partColor = glm.vec3(colors)
+
+					partColors.append(partColor)
+				elif p["_classname"] == "NeuroDef":
+					neuron = (p['p'], p['j'])
+					neurons.append(neuron)
+					neuronStyles.append(p['d'].split(':')[0])
+
+					'''ix = p['d'].find(':')
+					rorient = glm.mat4(1)
+					if ix != -1:
+						ds = p['d'][ix+1:].split(',')
+						ds = [x.split('=') for x in ds]
+						rots = [x for x in ds if x[0].startswith('r')]
+
+						if not any("ry" in x[0] for x in rots):
+							rots.append(["ry", '0'])
+						if not any("rz" in x[0] for x in rots):
+							rots.append(["rz", '0'])
+
+						angle = next(float(x[1]) for x in rots if x[0] == "rz")
+						rorient = glm.rotate(rorient, angle, glm.vec3(0,0,1))
+						angle = -next(float(x[1]) for x in rots if x[0] == "ry")
+						rorient = glm.rotate(rorient, angle, glm.vec3(0,1,0))'''
+						
+					rorient = parseNeuronDToOrient(p['d'])
+					neuroRelativeOrient.append(rorient)
+				elif p["_classname"] == "Creature":
+					colors = self._VstyleParserColor(p["model_Vstyle"])
+					if colors and colors[0] != None:
+						creature.colorsPart = [glm.vec3(colors) for i in creature.colorsPart]
+
+			creature.mechParts.extend(mechParts.copy())
+			creature.joints.extend(joints.copy())
+			creature.colorsPart.extend(partColors.copy())
+			creature.colorsJoint.extend(jointColors.copy())
+			creature.neurons.extend(neurons.copy())
+			creature.styleNeuron.extend(neuronStyles.copy())
+			creature.partOrient.extend(partOrient.copy())
+			creature.neuronRelativeOrient.extend(neuroRelativeOrient.copy())
+
+		invalidCreatures = []
+		for creature in creatures:
+			if len(creature.mechParts) == 0:
+				invalidCreatures.append(creature)
+		for creature in invalidCreatures:
+			creatures.remove(creature)
+
+		return creatures
+
+	def readGenePoolsGroups(self) -> Dict[str, int]:
+		genotypes: Dict[str, int] = {}
+		tmp = []
+		for i in range(3):
+			response = self.sendRequest("get /simulator/genepools/groups/+ index,name")
+			tmp = self._infoParser(response)
+			if len(tmp) > 0:
+				break
+
+		for i in tmp:
+			genotypes[i.p["name"]] = i.p["index"]
+
+		return genotypes
+
+	def readGenePools(self, props: List[str]) -> Dict[str, List[Dict[str, Any]]]:
+		genotypes: Dict[str, List[Dict[str, Any]]] = {}
+
+		for i in range(3):
+			response = self.sendRequest("get /simulator/genepools/groups/+ index,name")
+			tmp = self._infoParser(response)
+			if len(tmp) > 0:
+				break
+		else:
+			return genotypes
+
+		tu = {}
+		for i in tmp:
+			tu[i.p["index"]] = i.p["name"]
+			genotypes[i.p["index"]] = []
+
+		p = ",".join(props)
+		response = self.sendRequest("get /simulator/genepools/groups/+/genotypes/+ {}".format(p))
+
+		files = [i for i, x in enumerate(response) if x.startswith("file")]
+		files.append(-1)
+
+		for i in range(len(files)-1):
+			file = response[files[i]]
+
+			groupsStr = "groups/"
+			groupBegin = file.find(groupsStr) + len(groupsStr)
+			groupEnd = file.find("/", groupBegin)
+			group = int(file[groupBegin:groupEnd])
+
+			genotypesStr = "genotypes/"
+			genotypeIndexBegin = file.find(genotypesStr) + len(genotypesStr)
+			genotypeIndexEnd = file.find("/", genotypeIndexBegin-1) +2
+			genotypeIndex = int(file[genotypeIndexBegin:genotypeIndexEnd])
+
+			resp = self._infoParser(response[files[i]:files[i+1]], False)
+
+			genotypes[group].append(resp[0])
+			genotypes[group][-1].update({"group": group, "index": genotypeIndex})
+
+		g = {}
+		for (i, gen) in genotypes.items():
+			g[tu[i]] = gen
+
+		return g
+
+	def readPopulationsGroups(self) -> Dict[str, int]:
+		creatures: Dict[str, int] = {}
+		tmp = []
+		for i in range(3):
+			response = self.sendRequest("get /simulator/populations/groups/+ index,name")
+			tmp = self._infoParser(response)
+			if len(tmp) > 0:
+				break
+
+		for i in tmp:
+			creatures[i.p["name"]] = i.p["index"]
+
+		return creatures
+
+	def readPopulations(self, props: List[str]) -> Dict[str, List[Dict[str, Any]]]:
+		creatures: Dict[str, List[Dict[str, Any]]] = {}
+		for i in range(3):
+			response = self.sendRequest("get /simulator/populations/groups/+ index,name")
+			tmp = self._infoParser(response)
+			if len(tmp) > 0:
+				break
+		else:
+			return creatures
+
+		tu = {}
+		for i in tmp:
+			tu[i.p["index"]] = i.p["name"]
+			creatures[i.p["index"]] = []
+
+		p = ",".join(props)
+		response = self.sendRequest("get /simulator/populations/groups/+/creatures/+ {}".format(p))
+
+		files = [i for i, x in enumerate(response) if x.startswith("file")]
+		files.append(-1)
+
+		for i in range(len(files)-1):
+			file = response[files[i]]
+
+			groupsStr = "groups/"
+			groupBegin = file.find(groupsStr) + len(groupsStr)
+			groupEnd = file.find("/", groupBegin)
+			group = int(file[groupBegin:groupEnd])
+
+			creaturesStr = "creatures/"
+			creatureIndexBegin = file.find(creaturesStr) + len(creaturesStr)
+			creatureIndexEnd = file.find("/", creatureIndexBegin-1) +2
+			creatureIndex = int(file[creatureIndexBegin:creatureIndexEnd])
+
+			resp = self._infoParser(response[files[i]:files[i+1]], False)
+
+			creatures[group].append(resp[0])
+			creatures[group][-1].update({"group": group, "index": creatureIndex})
+
+		c = {}
+		for (i, cre) in creatures.items():
+			c[tu[i]] = cre
+
+		return c
+
+	def readCreatureInfo(self, groupNo: int, creatureNo: int) -> List[Property]:
+		response = self.sendRequest("info /simulator/populations/groups/{}/creatures/{}".format(groupNo, creatureNo))
+		response2 = self.sendRequest("get /simulator/populations/groups/{}/creatures/{}".format(groupNo, creatureNo))
+
+		tmpProperties = self._infoParser(response)
+		tmpValues = self._infoParser(response2)
+		properties: List[Property] = []
+		for prop in tmpProperties:
+			if "type" in prop.p and "id" in prop.p and len(prop.p["type"]) > 0:
+				if prop.p["type"][0] not in "xole": #xol is a list of unwanted types
+					if prop.p["type"][0] == 'p':
+						prop.p["value"] = partial(self.call, "/simulator/populations/groups/{}/creatures/{} {}".format(groupNo, creatureNo, prop.p["id"]))
+					else:
+						prop.p["value"] = next(v for k, v in tmpValues[0].p.items() if k == prop.p["id"])
+					properties.append(prop)
+
+		return properties
+
+	def readGenotypeInfo(self, groupNo: int, genotypeNo: int) -> List[Property]:
+		response = self.sendRequest("info /simulator/genepools/groups/{}/genotypes/{}".format(groupNo, genotypeNo))
+		response2 = self.sendRequest("get /simulator/genepools/groups/{}/genotypes/{}".format(groupNo, genotypeNo))
+
+		tmpProperties = self._infoParser(response)
+		tmpValues = self._infoParser(response2)
+		properties: List[Property] = []
+		for prop in tmpProperties:
+			if "type" in prop.p and "id" in prop.p and len(prop.p["type"]) > 0:
+				if prop.p["type"][0] not in "xole": #xol is a list of unwanted types
+					if prop.p["type"][0] == 'p':
+						prop.p["value"] = partial(self.call, "/simulator/genepools/groups/{}/genotypes/{} {}".format(groupNo, genotypeNo, prop.p["id"]))
+					else:
+						prop.p["value"] = next(v for k, v in tmpValues[0].p.items() if k == prop.p["id"])
+					properties.append(prop)
+
+		return properties
+
+	def readPropertyInfo(self, path) -> List[Property]:
+		response = self.sendRequest("info {}".format(path))
+		response2 = self.sendRequest("get {}".format(path))
+
+		tmpProperties = self._infoParser(response)
+		tmpValues = self._infoParser(response2)
+		properties: List[Property] = []
+		for prop in tmpProperties:
+			if "type" in prop.p and "id" in prop.p and len(prop.p["type"]) > 0:
+				if prop.p["type"][0] and prop.p["type"][0] not in "xole": #xole is a list of unwanted types
+					if prop.p["type"][0] == 'p':
+						prop.p["value"] = partial(self.call, "{} {}".format(path, prop.p["id"]))
+					else:
+						prop.p["value"] = next((v for k, v in tmpValues[0].p.items() if k == prop.p["id"]), "")
+					properties.append(prop)
+
+		return properties
+
+	def writeCreatureInfo(self, uid: str, prop: str, value: str) -> bool:
+		response = self.sendRequest("get /simulator/populations size")
+		size = next(int(x[x.find(":")+1:-1]) for x in response if x.startswith("size"))
+		if response:
+			for i in range(size):
+				res = self.sendRequest("set /simulator/populations/groups/{}/creatures/{} {} \"{}\"".format(i, uid, prop, value))
+			return True
+		return False
+
+	def writeGenotypeInfo(self, uid: str, prop: str, value: str) -> bool:
+		response = self.sendRequest("get /simulator/genepools size")
+		size = next(int(x[x.find(":")+1:-1]) for x in response if x.startswith("size"))
+		if response:
+			for i in range(size):
+				res = self.sendRequest("set /simulator/genepools/groups/{}/genotypes/{} {} \"{}\"".format(i, uid, prop, value))
+			return True
+		return False
+
+	def writePropertyInfo(self, path: str, prop: str, value: str):
+		response = self.sendRequest("set {} {} \"{}\"".format(path, prop, value))
+		if response:
+			return True
+		return False
+
+	def call(self, path: str):
+		self.sendRequest("call {}".format(path))
+
+	def _runningChangeEventCallback(self, block: str, header: str):
+		prop = self._infoParser([block])[0]
+		if self.refreshControlButtonsCallback:
+			self.refreshControlButtonsCallback(prop.p["value"])
+		
+	def _populationsGroupChangeEventCallback(self, block: str, header: str):
+		if self.refreshTreeviewCallback:
+			self.refreshTreeviewCallback()
+
+	def _genepoolsGroupChangeEventCallback(self, block: str, header: str):
+		if self.refreshTreeviewCallback:
+			self.refreshTreeviewCallback()
+
+	def registerMessageEventCallback(self, callback: Callable[[str, str], None]):
+		self.comm.client.consumer.messagesEventCallback = callback
+
+	def loadFile(self, path: str) -> None:
+		response, idx = self.sendRequest("call /simulator netload", return_index=True)
+		if response:
+			if response[0].startswith("needfile"):
+				with open(path) as file:
+					data = "file \n" + file.read() + "eof"
+					self.sendRequest(data, index=idx)
+
+	def importFile(self, path: str, options: int) -> None:
+		response, idx = self.sendRequest("call /simulator netimport {}".format(options), return_index=True)
+		if response:
+			if response[0].startswith("needfile"):
+				with open(path) as file:
+					data = "file \n" + file.read() + "eof"
+					self.sendRequest(data, index=idx)
+
+	def saveFile(self, path: str, options: int) -> None:
+		response = self.sendRequest("call /simulator netexport {} -1 -1".format(options))
+		if response:
+			start = 0
+			end = len(response) - 1
+			if response[0].startswith("file"):
+				start = 1
+
+			while response[end].strip() == '':
+				end -= 1
+
+			if response[end].startswith("ok"):
+				end -= 1
+			if response[end].startswith("eof"):
+				end -= 1
+
+			while response[end].strip() == '':
+				end -= 1
+			end += 1
+
+		with open(path, 'w') as file:
+			file.write(''.join(response[start:end]))
+			file.write("\n")
+
+	def getWorldType(self) -> int:
+		info = []
+		while not info:
+			response = self.sendRequest("get /simulator/world wrldtyp")
+			info = self._infoParser(response)
+		return info[0].p["wrldtyp"]
+
+	def getWorldSize(self) -> float:
+		info = []
+		while not info:
+			response = self.sendRequest("get /simulator/world wrldsiz")
+			info = self._infoParser(response)
+		return info[0].p["wrldsiz"]
+
+	def getWorldWaterLevel(self) -> float:
+		info = []
+		while not info:
+			response = self.sendRequest("get /simulator/world wrldwat")
+			info = self._infoParser(response)
+		return info[0].p["wrldwat"]
+
+	def getWorldBoundaries(self) -> int:
+		info = []
+		while not info:
+			response = self.sendRequest("get /simulator/world wrldbnd")
+			info = self._infoParser(response)
+		return info[0].p["wrldbnd"]
+
+	def getWorldMap(self) -> str:
+		info = []
+		while not info:
+			response = self.sendRequest("get /simulator/world geometry")
+			info = self._infoParser(response)
+		return info[0].p["geometry"]
+
+	def getSimtype(self) -> int:
+		info = []
+		while not info:
+			response = self.sendRequest("get /simulator/world simtype")
+			info = self._infoParser(response)
+		return info[0].p["simtype"]
Index: framspy/gui/framsutils/comm.py
===================================================================
--- framspy/gui/framsutils/comm.py	(revision 1198)
+++ framspy/gui/framsutils/comm.py	(revision 1198)
@@ -0,0 +1,185 @@
+import asyncore
+import socket
+from io import StringIO
+from typing import Callable, Dict
+import threading
+import asyncio
+import queue
+import time
+
+class EventConsumer(threading.Thread):
+	def __init__(self) -> None:
+		threading.Thread.__init__(self)
+		self.condition = threading.Condition()
+		self.queue = queue.Queue()
+		self.is_running = True
+		self.runningChangeEventCallback = None
+		self.populationsGroupChangeEventCallback = None
+		self.genepoolsGroupChangeEventCallback = None
+		self.messagesEventCallback = None
+
+	def run(self):
+		while self.is_running:
+			if self.queue.empty():
+				time.sleep(0.0001)
+			else:
+				block = self.queue.get()
+				#print("------- EVENT -------")
+				#print(block)
+				#print("------- END EVENT -------")
+				header: str = block[:block.find('\n')]
+				if header.find("running_changed") != -1 and self.runningChangeEventCallback:
+					self.runningChangeEventCallback(block, header)
+				elif header.find("populations/groups_changed") != -1 and self.populationsGroupChangeEventCallback:
+					self.populationsGroupChangeEventCallback(block, header)
+				elif header.find("genepools/groups_changed") != -1 and self.genepoolsGroupChangeEventCallback:
+					self.genepoolsGroupChangeEventCallback(block, header)
+				elif header.find("messages") != -1 and self.messagesEventCallback:
+					self.messagesEventCallback(block, header)
+
+	def stop(self):
+		self.is_running = False
+
+class CommWrapper(object):
+	def start(self, host: str, port: int):
+		self.client = Comm(host, port, self.onClose)
+		self.thread = threading.Thread(target=asyncore.loop, kwargs={'timeout': 0.0005})
+		self.thread.start()
+		self.client.write_data("version 5")
+		self.client.write_data("use request_id")
+		self.client.write_data("use needfile_id")
+		self.client.write_data("use groupnames")
+		self.connected = True
+
+	def stop(self):
+		self.client.close()
+		self.thread.join()
+
+	def onClose(self):
+		self.connected = False
+
+	def close(self):
+		self.thread.join()
+
+	async def read(self, i: int, timeout: float) -> str:
+		data = ""
+		try:
+			data = await asyncio.wait_for(self._read(i), timeout=timeout)
+		except:
+			pass
+		#print("response")
+		#print(repr(data))
+		return data
+
+	async def _read(self, i: int) -> str:
+		while True:
+			if i in self.client.blocks:
+				return self.client.blocks.pop(i)
+			try:
+				await asyncio.sleep(0.005)
+			except asyncio.CancelledError:
+				break
+
+	def write(self, data: str, index = -1) -> int:
+		command = data.split(' ')[0]
+		if command in ["info", "set", "get", "call", "reg", "file"]:
+			if index == -1:
+				index = self.client.getIndex()
+			data = command + ' r' + str(index) + data[len(command):]
+		self.client.write_data(data)
+		return index
+
+class Comm(asyncore.dispatcher):
+	def __init__(self, host: str, port: int, onClose: Callable[[None], None] = None) -> None:
+		self.onClose = onClose
+
+		asyncore.dispatcher.__init__(self)
+		self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
+		self.host = host
+		self.port = port
+		
+		self.connect((self.host, self.port))
+		self.out_buffer = "".encode()
+		self.in_buffer = StringIO()
+		self.blocks: Dict[str] = {}
+		self._tmpbuff = StringIO()
+		self.index = 1
+		self.is_embedded = False
+
+		self.consumer = EventConsumer()
+		self.consumer.start()
+	
+	def writable(self):
+		is_writable = (len(self.out_buffer) > 0)
+		return is_writable
+
+	def readable(self):
+		return True
+
+	def handle_close(self):
+		self.close()
+
+	def close(self) -> None:
+		self.in_buffer.close()
+		self._tmpbuff.close()
+		self.consumer.stop()
+		if self.onClose:
+			self.onClose()
+		super().close()
+
+	def handle_connect(self):
+		pass
+
+	def handle_read(self):
+		rec_msg = self.recv(1024)
+		self.in_buffer.write(rec_msg.decode())
+		self.parse()
+
+	def handle_write(self) -> None:
+		sent = self.send(self.out_buffer)
+		self.out_buffer = self.out_buffer[sent:]
+
+	def write_data(self, data: str):
+		#print("request")
+		#print(repr(data))
+		self.out_buffer += (data + '\n' if data[-1] != '\n' else '').encode()
+
+	def read_data(self):
+		return self.in_buffer.getvalue()
+
+	def parse(self):
+		self.in_buffer.seek(0)
+		for line in self.in_buffer:
+			if line.endswith('\n'):
+				self._tmpbuff.write(line)
+				if line.endswith(":~\n"):
+					self.is_embedded = True
+				if self.is_embedded:
+					if line.endswith("~\n") and not line.endswith("\~\n"):
+						self.is_embedded = False
+				else:
+					if line.startswith("eof"):
+						block = self._tmpbuff.getvalue().strip()
+						if block.startswith("event"):
+							self.consumer.queue.put(block)
+							self._tmpbuff.truncate(0)
+							self._tmpbuff.seek(0)
+					elif line.startswith("ok") or line.startswith("error") or line.startswith("needfile"):
+						block = self._tmpbuff.getvalue()
+						if line.find(' ') > -1:
+							self.blocks[int(line.split(' ')[1][1:])] = block
+						self._tmpbuff.truncate(0)
+						self._tmpbuff.seek(0)
+		self.in_buffer.truncate(0)
+		self.in_buffer.seek(0)
+		if not line.endswith('\n'):
+			self.in_buffer.write(line)
+
+	def getIndex(self) -> int:
+		# Not every response could be fetched, because of timeout of request.
+		# Stale responses needs to be cleaned to prevent stack growing.
+		r = 100	# Remove all messagess older than r
+		if self.index % r != (self.index + 1) % r:
+			self.blocks = {k: v for k, v in self.blocks.items() if k > self.index - r}
+		self.index += 1
+		return self.index - 1
Index: framspy/gui/framsutils/creature.py
===================================================================
--- framspy/gui/framsutils/creature.py	(revision 1198)
+++ framspy/gui/framsutils/creature.py	(revision 1198)
@@ -0,0 +1,82 @@
+import glm
+from typing import List, Tuple
+
+class Creature:
+	def __init__(self, group: int, index: int) -> None:
+		self.mechParts = []
+		self.joints = []
+		self.neurons: List[Tuple(int, int)] = []
+		self.colorsJoint: List[glm.vec3] = []
+		self.colorsPart: List[glm.vec3] = []
+		self.stylePart: List[str] = []
+		self.styleJoint: List[str] = []
+		self.styleNeuron: List[str] = []
+		self.partOrient: List[glm.mat4] = []
+		self.neuronRelativeOrient: List[glm.mat3] = []
+		self.group: int = group
+		self.index: int = index
+
+	def jointTranslation(self, index: int):
+		p1 = glm.vec3(self.mechParts[self.joints[index][0]])
+		return p1
+
+	def jointLength(self, index: int):
+		p1 = glm.vec3(self.mechParts[self.joints[index][0]])
+		p2 = glm.vec3(self.mechParts[self.joints[index][1]])
+		return glm.length(p2 - p1)
+
+	def jointRotation(self, index: int) -> glm.mat4:
+		p1 = glm.vec3(self.mechParts[self.joints[index][0]])
+		p2 = glm.vec3(self.mechParts[self.joints[index][1]])
+
+		z_axis = None
+		along_d = p2 - p1
+		along_o = self._lookAt(along_d)
+		along_z = self._revTransform(*along_o, self.partOrient[self.joints[index][1]][2])
+
+		if along_z.y * along_z.y + along_z.z * along_z.z > 0.5*0.5:
+			z_axis = glm.vec3(self.partOrient[self.joints[index][1]][2])
+		else:
+			z_axis = glm.vec3(self.partOrient[self.joints[index][1]][1])
+		orient = self._lookAt2(along_d, z_axis)
+		return glm.mat4(glm.mat3(*orient))
+
+	def partTranslation(self, index: int):
+		return glm.vec3(self.mechParts[index])
+
+	def partRotationMatrix(self, index: int):
+		return self.partOrient[index]
+
+	def _lookAt(self, X: glm.vec3) -> Tuple[glm.vec3, glm.vec3, glm.vec3]:
+		x = glm.normalize(X)
+
+		ax = abs(x.x)
+		ay = abs(x.y)
+		az = abs(x.z)
+		if (ax <= ay) and (ax <= az):
+			y = glm.vec3(0, -x.z, x.y)
+		elif (ay <= ax) and (ay <= az):
+			y = glm.vec3(-x.z, 0, x.x)
+		else:
+			y = glm.vec3(-x.y, x.x, 0)
+		y = glm.normalize(y)
+		z = glm.cross(x, y)
+		return x, y, z
+
+	def _lookAt2(self, X: glm.vec3, d: glm.vec3) -> Tuple[glm.vec3, glm.vec3, glm.vec3]:
+		x = glm.normalize(X)
+		y = glm.cross(d, x)
+		z = glm.cross(x, y)
+		if glm.length(y) < 1e-50 or glm.length(z) < 1e-50:
+			return self._lookAt(X)
+		
+		y = glm.normalize(y)
+		z = glm.normalize(z)
+		return x, y, z
+
+	def _revTransform(self, x: glm.vec3, y: glm.vec3, z: glm.vec3, s: glm.vec3) -> glm.vec3:
+		return glm.vec3(
+			s.x * x.x + s.y * x.y + s.z * x.z,
+			s.x * y.x + s.y * y.y + s.z * y.z,
+			s.x * z.x + s.y * z.y + s.z * z.z
+		)
Index: framspy/gui/framsutils/framsProperty.py
===================================================================
--- framspy/gui/framsutils/framsProperty.py	(revision 1198)
+++ framspy/gui/framsutils/framsProperty.py	(revision 1198)
@@ -0,0 +1,274 @@
+import tkinter as tk
+import tkinter.ttk as ttk
+from typing import Dict, List, Optional, Tuple
+from datetime import datetime
+from time import mktime
+from gui.widgets.ScrolledText import ScrolledText
+import re
+
+trans = {
+	"~": r"\~",
+	"\\": r"\\",
+	"\"": r"\"",
+	"\n": r"\n",
+	"\t": r"\t"}
+
+def encodeString(data: str) -> str:
+	return data.translate(data.maketrans(trans))
+
+datetimeformat = '%Y-%m-%d %H:%M:%S'
+
+class Property:
+	def __init__(self, Id: str = "", Name: str = "", Type: str = "", p: Dict = None) -> None:
+		self.p = {}
+		self.p["id"] = Id
+		self.p["name"] = Name
+		self.p["type"] = Type
+		self.p["flags"] = 0
+		self.p["help"] = ""
+		self.p["value"] = None
+		if p:
+			self.p = {**self.p, **p}
+
+class PropertyCallback:
+	def __init__(self, widget: tk.Widget, Id: str, xtype: str) -> None:
+		self.widget: tk.Widget = widget
+		self.id = Id
+		self._changed = False
+		self.type = xtype[0]
+		self.subtype = None
+		if len(xtype) > 1:
+			self.subtype = xtype[1]
+
+		if isinstance(self.widget, tk.Checkbutton):
+			self.var = self.widget.variable
+		elif isinstance(self.widget, tk.Text):
+			self.widget.edit_modified(False)
+		elif isinstance(self.widget, tk.Button):
+			pass
+
+		self.initValue = self.value()
+
+	def changed(self) -> bool:
+		if isinstance(self.widget, tk.Text):
+			self._changed = self.widget.edit_modified()
+		else:
+			if self.initValue != self.value():
+				self._changed = True
+		return self._changed
+
+	def restart(self) -> None:
+		self.initValue = self.value()
+		self._changed = False
+
+	def value(self) -> str:
+		if isinstance(self.widget, tk.Checkbutton):
+			return self._checkButton()
+		elif isinstance(self.widget, ttk.Combobox):
+			return self._combobox()
+		elif isinstance(self.widget, ttk.Spinbox):
+			return self._spinbox()
+		elif isinstance(self.widget, tk.Text):
+			return encodeString(self._text())
+		elif isinstance(self.widget, tk.Entry):
+			return self._entry()
+		return None
+
+	def rawValue(self, translate: bool = True) -> str:
+		if self.type == 'd':
+			if self.subtype == 'b':
+				return int(self.value(), 2)
+			elif self.subtype == 'c':
+				return int(self.value(), 16)
+			else:
+				return self.value()
+		elif self.type == 'f':
+			if self.subtype == 't':
+				el = datetime.strptime(self.value(), datetimeformat)
+				t = el.timetuple()
+				return mktime(t)
+			elif self.subtype == 'i':
+				return float(self.value())
+			else:
+				return self.value()
+		elif self.type == 's':
+			if not translate and isinstance(self.widget, tk.Text):
+				return self._text() 
+			else:
+				return self.value()
+		elif self.type == 'p':
+			return self.value()
+
+	def updateValue(self, value: Property) -> None:
+		_, cb = propertyToTkinter(value, self.widget.master)
+
+		if isinstance(self.widget, tk.Checkbutton):
+			if value.p["value"] == '1' or value.p["value"] == 1:
+				self.widget.select()
+			else:
+				self.widget.deselect()
+		elif isinstance(self.widget, ttk.Combobox):
+			if hasattr(self.widget, "mapvalues"):
+				self.widget.set(list(self.widget.mapvalues.keys())[list(self.widget.mapvalues.values()).index(cb.value())])
+			else:
+				self.widget.set(cb.value())
+		elif isinstance(self.widget, ttk.Spinbox):
+			self.widget.set(cb.value())
+		elif isinstance(self.widget, tk.Text):
+			self.widget.config(state=tk.NORMAL)
+			if len(self.value()) > 0:
+				self.widget.delete("1.0", tk.END)
+			self.widget.insert("1.0", cb._text()) #translates and escapes \\ as expected 
+			self.widget.edit_modified(False)
+			self.widget.config(state=tk.DISABLED)
+		elif isinstance(self.widget, tk.Entry):
+			self.widget.delete(0, tk.END)
+			self.widget.insert(0, cb.value())
+
+	def _checkButton(self) -> str:
+		return "1" if self.var.get() else "0"
+
+	def _combobox(self) -> str:
+		if hasattr(self.widget, "mapvalues"):
+			return self.widget.mapvalues.get(self.widget.get())
+		else:
+			return self.widget.get()
+
+	def _spinbox(self) -> str:
+		return str(self.widget.get())
+
+	def _text(self) -> str:
+		return self.widget.get("1.0", "end").strip()
+
+	def _entry(self) -> str:
+		return self.widget.get().strip()
+
+def propertyToTkinter(prop: Property, master: tk.Widget) -> Optional[Tuple[tk.Widget, PropertyCallback]]:
+	global trans
+	widget = None
+	callback: PropertyCallback = None
+	t = prop.p["type"].split()
+	propType = t[0]
+
+	readonly = True if int(prop.p["flags"]) & 1 or int(prop.p["flags"]) & 16 else False
+	visible = False if int(prop.p["flags"]) & 32 else True
+
+	if not visible:
+		return None, None
+
+	class MinMaxDefault:
+		def __init__(self, minValue, maxValue, default = None) -> None:
+			self.minValue = minValue
+			self.maxValue = maxValue
+			self.default = default
+
+	def minMaxDefault(prop_type: List) -> MinMaxDefault:
+		if len(prop_type) >= 4:
+			return MinMaxDefault(prop_type[1], prop_type[2], prop_type[3])
+		elif len(prop_type) >= 3:
+			return MinMaxDefault(prop_type[1], prop_type[2])
+		else:
+			return MinMaxDefault(None, None)
+
+	minmax = minMaxDefault(t)
+
+	if propType[0] == 'd':
+		value = int(prop.p["value"])
+		if len(propType) > 1:
+			if propType[1] == 'b':
+				value = bin(value)
+			elif propType[1] == 'c':
+				value = hex(value)
+
+		if minmax.minValue == '0' and minmax.maxValue == '1' and prop.p["type"].find('~') == -1:
+			var = tk.BooleanVar()
+			widget = tk.Checkbutton(master=master, text='', onvalue=1, offvalue=0, var=var)
+			if value == 1:
+				widget.select()
+			if readonly:
+				widget['state'] = 'disabled'
+			widget.variable = var
+		elif prop.p["type"].find('~') > 0:
+			options = prop.p["type"].split('~')[1:]
+			widget = ttk.Combobox(master=master, values=options, state="readonly")
+			widget.mapvalues = {k:v for (v, k) in enumerate(options, int(t[1]))}
+			widget.set(options[value])
+		else:
+			minVal = 0 if minmax.minValue is None else minmax.minValue
+			maxVal = 100 if minmax.maxValue is None else minmax.maxValue
+			widget = ttk.Spinbox(master=master, from_=minVal, to=maxVal)
+			widget.set(value)
+			if readonly:
+				widget["state"] = "disabled"
+	elif propType[0] == 'f':
+		value = None
+		var = None
+		if len(propType) > 1:
+			if propType[1] == 't':
+				timestamp = float(prop.p["value"])
+				value = datetime.fromtimestamp(timestamp).strftime(datetimeformat)
+			elif propType[1] == 'i':
+				var = tk.StringVar()
+				value = float(prop.p["value"])
+		else:
+			var = tk.StringVar()
+			value = float(prop.p["value"])
+
+		if var:
+			def callback(*_):
+				val = var.get()
+				try:
+					v = float(val)
+					if minmax.minValue and v < float(minmax.minValue):
+						v = float(minmax.minValue)
+					elif minmax.maxValue and v > float(minmax.maxValue):
+						v = float(minmax.maxValue)
+					var.set(str(v))
+				except ValueError:
+					pass
+
+			def on_validate(string):
+				regex = re.compile(r"(\+|\-)?[0-9.]*$")
+				result = regex.match(string)
+				return (string == ""
+						or (string.count('+') <= 1
+							and string.count('-') <= 1
+							and string.count(',') <= 1
+							and result is not None
+							and result.group(0) != ""))
+
+			var.trace_add("write", callback)
+			widget = tk.Entry(master=master, validate="key", textvariable=var)
+			vcmd = (widget.register(on_validate), '%P')
+			widget.config(validatecommand=vcmd)
+		else:
+			widget = tk.Entry(master=master)
+		widget.insert(0, value)
+		if readonly:
+			widget["state"] = "disabled"
+	elif propType[0] == 's':
+		if t[-1].find('~') >= 0:
+			options = t[-1].split('~')[1:]
+			widget = ttk.Combobox(master=master)
+			widget["values"] = options
+			widget.set(prop.p["value"])
+		else:
+			multiline = True if len(t) > 1 and t[1] != '0' else False
+			if multiline:
+				widget = ScrolledText(master=master, height=1 + multiline * 3)
+				widget.insert(tk.INSERT, prop.p["value"])
+			else:
+				widget = tk.Entry(master=master)
+				widget.insert(0, prop.p["value"])
+		if readonly:
+			widget["state"] = "disabled"
+	elif propType[0] == 'p':
+		widget = tk.Button(master=master, text=prop.p["name"], command=prop.p["value"])
+		callback = PropertyCallback(widget, prop.p["id"], propType)
+		widget.anchor('w')
+
+	if widget and propType[0] != 'p':
+		callback = PropertyCallback(widget, prop.p["id"], propType)
+		widget.anchor('w')
+
+	return widget, callback
Index: framspy/gui/framsutils/utils.py
===================================================================
--- framspy/gui/framsutils/utils.py	(revision 1198)
+++ framspy/gui/framsutils/utils.py	(revision 1198)
@@ -0,0 +1,20 @@
+import glm
+
+def parseNeuronDToOrient(d: str) -> glm.mat4:
+    ix = d.find(':')
+    rorient = glm.mat4(1)
+    if ix != -1:
+        ds = d[ix+1:].split(',')
+        ds = [x.split('=') for x in ds]
+        rots = [x for x in ds if x[0].startswith('r')]
+
+        if not any("ry" in x[0] for x in rots):
+            rots.append(["ry", '0'])
+        if not any("rz" in x[0] for x in rots):
+            rots.append(["rz", '0'])
+
+        angle = next(float(x[1]) for x in rots if x[0] == "rz")
+        rorient = glm.rotate(rorient, angle, glm.vec3(0,0,1))
+        angle = -next(float(x[1]) for x in rots if x[0] == "ry")
+        rorient = glm.rotate(rorient, angle, glm.vec3(0,1,0))
+    return rorient
Index: framspy/gui/libInterface.py
===================================================================
--- framspy/gui/libInterface.py	(revision 1198)
+++ framspy/gui/libInterface.py	(revision 1198)
@@ -0,0 +1,152 @@
+from typing import Dict, List, Any, Tuple
+from gui.framsutils.FramsLib import FramsLib
+from gui.framsutils.creature import Creature
+from gui.framsutils.framsProperty import Property
+from gui.framsutils.FramsInterface import FramsInterface, TreeNode, InterfaceType
+
+class LibInterface(FramsInterface):
+	DEBUG_ADD_FIRST_CREATURE = False
+	DEBUG_FIRST_CREATURE = "fffX(fffIX[S:-1.556],RRlFFFMMMX[|-1:-10,3:10]llllFFMMMX[|1:-4.450]RlllFFFMMMX[|G:-1.404],fffIX[S:1])"
+
+	def __init__(self) -> None:
+		self.interfaceType = InterfaceType.LIB
+		self.frams: FramsLib = FramsLib()
+
+	def connect(self, address: str, port: int):
+		self.frams.initConnection(address)
+		if self.DEBUG_ADD_FIRST_CREATURE:
+			self.frams.writeCreatureFromString(0, self.DEBUG_FIRST_CREATURE)
+			self.frams.frams.GenePools[0].add(self.DEBUG_FIRST_CREATURE)
+
+	def disconnect(self):
+		self.frams.closeConnection()
+
+	def getError(self) -> None or str:
+		return self.frams.getError()
+
+	def start(self):
+		self.frams.start()
+
+	def stop(self):
+		self.frams.stop()
+
+	def step(self):
+		self.frams.step()
+
+	def listTree(self, path="/") -> List[Property]:
+		return self.frams.info(path)
+
+	def listTreeList(self, path) -> List[Property]:
+		return self.frams.infoList(path)
+
+	def makeInfoTree(self) -> TreeNode:
+		root = Property("", "/", "o")
+		childs = self.listTree()
+		tree = self._makeTree(None, root, childs)
+		tree.node.p["id"] = "/"
+		return tree
+	
+	def _makeTree(self, parent: TreeNode, node: Property, childs: List[Property]) -> TreeNode:
+		treeNode = TreeNode(node, None, parent)
+		p = parent
+		path = treeNode.node.p["id"]
+		while p:
+			if p.node.p["id"] != '/':
+				path = p.node.p["id"] + "/" + path
+			p = p.parent
+
+		for child in childs:
+			if child.p["id"] and child.p["name"] and child.p["type"]:
+				if child.p["type"][0] == "o":
+					nodeChilds = self.listTree(path + "/" + child.p["id"])
+					c = self._makeTree(treeNode, child, nodeChilds)
+					treeNode.addChild(c)
+				elif child.p["type"][0] == "l" and child.p["id"] == "groups":
+					nodeChilds = self.listTree(path + "/" + child.p["id"])
+					c = self._makeTreeFromList(treeNode, child, nodeChilds)
+					treeNode.addChild(c)
+		return treeNode
+
+	def _makeTreeFromList(self, parent: TreeNode, node: Property, childs: List[Property]) -> TreeNode:
+		treeNode = TreeNode(node, None, parent)
+		return treeNode
+
+	def readCreatures(self, colors = True) -> List[Creature]:
+		return self.frams.readCreatures("+", "+", colors)
+
+	def readGenePoolsGroups(self) -> Dict[str, int]:
+		return self.frams.readGenePoolsGroups()
+
+	def readGenePools(self, props: List[str]) -> Dict[str, List[Dict[str, Any]]]:
+		return self.frams.readGenePools(props)
+
+	def readPopulationsGroups(self) -> Dict[str, int]:
+		return self.frams.readPopulationsGroups()
+
+	def readPopulations(self, props: List[str]) -> Dict[str, List[Dict[str, Any]]]:
+		return self.frams.readPopulations(props)
+
+	def readCreatureDetails(self, group: int, index: int):
+		return self.frams.readCreatureInfo(group, index)
+
+	def readGenotypeDetails(self, group: int, index: int):
+		return self.frams.readGenotypeInfo(group, index)
+
+	def readParameterDetails(self, prop: str):
+		return self.frams.readPropertyInfo(prop)
+
+	def writeCreatureDetail(self, creatureNo: str, prop: str, value: str):
+		return self.frams.writeCreatureInfo(creatureNo, prop, value)
+
+	def writeGenotypeDetail(self, genotypeNo: str, prop: str, value: str):
+		return self.frams.writeGenotypeInfo(genotypeNo, prop, value)
+
+	def writeParameterDetail(self, path: str, prop: str, value: str):
+		return self.frams.writePropertyInfo(path, prop, value)
+
+	def getMotd(self) -> str:
+		return ""
+
+	def getSimulationStatus(self) -> bool:
+		return self.frams.getSimulationStatus()
+
+	def loadFile(self, path: str) -> None:
+		self.frams.loadFile(path)
+
+	def importFile(self, path: str, options: int) -> None:
+		self.frams.importFile(path, options)
+
+	def saveFile(self, path: str, options: int) -> None:
+		self.frams.saveFile(path, options)
+
+	def getWorldType(self) -> int:
+		return self.frams.getWorldType()
+
+	def getWorldSize(self) -> float:
+		return self.frams.getWorldSize()
+
+	def getWorldWaterLevel(self) -> float:
+		return self.frams.getWorldWaterLevel()
+
+	def getWorldBoundaries(self) -> int:
+		return self.frams.getWorldBoundaries()
+
+	def getWorldMap(self) -> str:
+		return self.frams.getWorldMap()
+
+	def getSimtype(self) -> int:
+		return self.frams.getSimtype()
+
+	def getFPSDefinitions(self) -> List[Tuple[int, int]]:
+		return [(2, 1), 
+				(5, 1),
+				(10, 1),
+				(25, 1),
+				(50, 1),
+				(-1, 1),
+				(-1, 2),
+				(-1, 3),
+				(-1, 4),
+				(-1, 10),
+				(-1, 100),
+				(-1, 1000)]
Index: framspy/gui/requirements.txt
===================================================================
--- framspy/gui/requirements.txt	(revision 1198)
+++ framspy/gui/requirements.txt	(revision 1198)
@@ -0,0 +1,6 @@
+numpy==1.21.0
+Pillow==8.3.1
+PyGLM==1.99.3
+PyOpenGL==3.1.5
+PyOpenGL-accelerate==3.1.5
+pyopengltk==0.0.3
Index: framspy/gui/socketInterface.py
===================================================================
--- framspy/gui/socketInterface.py	(revision 1198)
+++ framspy/gui/socketInterface.py	(revision 1198)
@@ -0,0 +1,172 @@
+from typing import Dict, List, Any, Tuple
+from gui.framsutils.FramsSocket import FramsSocket
+from gui.framsutils.creature import Creature
+from gui.framsutils.framsProperty import Property
+from gui.framsutils.FramsInterface import FramsInterface, TreeNode, InterfaceType
+
+class SocketInterface(FramsInterface):
+	DEBUG_ADD_FIRST_CREATURE = False
+	DEBUG_FIRST_CREATURE = "X"
+
+	def __init__(self) -> None:
+		self.interfaceType = InterfaceType.SOCKET
+		self.frams: FramsSocket = FramsSocket()
+
+	def connect(self, address: str, port: int):
+		self.frams.initConnection(address, port)
+		if self.DEBUG_ADD_FIRST_CREATURE:
+			self.frams.writeCreatureFromString(0, self.DEBUG_FIRST_CREATURE)
+			self.frams.sendRequest("call /simulator/genepools/groups/0 add {}".format(self.DEBUG_FIRST_CREATURE))
+
+	def disconnect(self):
+		self.frams.closeConnection()
+
+	def start(self):
+		self.frams.start()
+
+	def getError(self):
+		return None
+
+	def stop(self):
+		self.frams.stop()
+
+	def step(self):
+		self.frams.step()
+
+	def listTree(self, path="/") -> List[Property]:
+		return self.frams.info(path)
+
+	def listTreeList(self, path) -> List[Property]:
+		return self.frams.infoList(path)
+
+	def makeInfoTree(self) -> TreeNode:
+		root = Property("", "/", "o")
+		childs = self.listTree()
+		tree = self._makeTree(None, root, childs)
+		tree.node.p["id"] = "/"
+		return tree
+	
+	def _makeTree(self, parent: TreeNode, node: Property, childs: List[Property]) -> TreeNode:
+		treeNode = TreeNode(node, None, parent)
+		p = parent
+		path = treeNode.node.p["id"]
+		while p:
+			if p.node.p["id"] != '/':
+				path = p.node.p["id"] + "/" + path
+			p = p.parent
+
+		for child in childs:
+			if child.p["id"] and child.p["name"] and child.p["type"]:
+				if child.p["type"][0] == "o":
+					nodeChilds = self.listTree(path + "/" + child.p["id"])
+					c = self._makeTree(treeNode, child, nodeChilds)
+					treeNode.addChild(c)
+				elif child.p["type"][0] == "l" and child.p["id"] == "groups":
+					nodeChilds = self.listTree(path + "/" + child.p["id"])
+					c = self._makeTreeFromList(treeNode, child, nodeChilds)
+					treeNode.addChild(c)
+		return treeNode
+
+	def _makeTreeFromList(self, parent: TreeNode, node: Property, childs: List[Property]) -> TreeNode:
+		treeNode = TreeNode(node, None, parent)
+		p = parent
+		path: str = " " + treeNode.node.p["id"]
+		while p:
+			if p.node.p["id"] != '/' and p.parent and p.parent.node.p["id"] != "/":
+				path = "/" + p.node.p["id"] + path
+			p = p.parent
+
+		groupsNo = int(self.listTreeList(path)[0].p["groups"])
+		path = path.replace(' ', '/') + '/'
+		childs = [Property("", str(x), "o") for x in range(groupsNo)]
+
+		for child in childs:
+			child.p["id"] = child.p["name"]
+			if child.p["type"][0] == "o":
+				nodeChilds = self.listTree(path + child.p["id"])
+				c = self._makeTree(treeNode, child, nodeChilds)
+				treeNode.addChild(c)
+
+		return treeNode
+
+	def readCreatures(self, colors = True) -> List[Creature]:
+		return self.frams.readCreatures("+", "+", colors)
+
+	def readGenePoolsGroups(self) -> Dict[str, int]:
+		return self.frams.readGenePoolsGroups()
+
+	def readGenePools(self, props: List[str]) -> Dict[str, List[Dict[str, Any]]]:
+		return self.frams.readGenePools(props)
+
+	def readPopulationsGroups(self) -> Dict[str, int]:
+		return self.frams.readPopulationsGroups()
+
+	def readPopulations(self, props: List[str]) -> Dict[str, List[Dict[str, Any]]]:
+		return self.frams.readPopulations(props)
+
+	def readCreatureDetails(self, group: int, index: str):
+		return self.frams.readCreatureInfo(group, index)
+
+	def readGenotypeDetails(self, group: int, index: str):
+		return self.frams.readGenotypeInfo(group, index)
+
+	def readParameterDetails(self, prop: str):
+		return self.frams.readPropertyInfo(prop)
+
+	def writeCreatureDetail(self, creatureNo: str, prop: str, value: str):
+		return self.frams.writeCreatureInfo(creatureNo, prop, value)
+
+	def writeGenotypeDetail(self, genotypeNo: str, prop: str, value: str):
+		return self.frams.writeGenotypeInfo(genotypeNo, prop, value)
+
+	def writeParameterDetail(self, path: str, prop: str, value: str):
+		return self.frams.writePropertyInfo(path, prop, value)
+
+	def registerRunningChangeEventCallback(self, callback):
+		self.frams.refreshControlButtonsCallback = callback
+
+	def registerTreeviewRefreshEventCallback(self, callback):
+		self.frams.refreshTreeviewCallback = callback
+
+	def getMotd(self) -> str:
+		try:
+			tmp = self.frams.infoList("/ motd")[0].p["motd"]
+		except:
+			tmp = "Not connected"
+		return tmp
+
+	def loadFile(self, path: str) -> None:
+		self.frams.loadFile(path)
+
+	def importFile(self, path: str, options: int) -> None:
+		self.frams.importFile(path, options)
+
+	def saveFile(self, path: str, options: int) -> None:
+		self.frams.saveFile(path, options)
+
+	def getWorldType(self) -> int:
+		return self.frams.getWorldType()
+
+	def getWorldSize(self) -> float:
+		return self.frams.getWorldSize()
+
+	def getWorldWaterLevel(self) -> float:
+		return self.frams.getWorldWaterLevel()
+
+	def getWorldBoundaries(self) -> int:
+		return self.frams.getWorldBoundaries()
+
+	def getWorldMap(self) -> str:
+		return self.frams.getWorldMap()
+
+	def getSimtype(self) -> int:
+		return self.frams.getSimtype()
+
+	def getFPSDefinitions(self) -> List[Tuple[int, int]]:
+		return [(0.5, 1),
+				(2, 1), 
+				(5, 1),
+				(10, 1),
+				(25, 1),
+				(50, 1),
+				(-1, 1)]
Index: framspy/gui/tests/socketTest.py
===================================================================
--- framspy/gui/tests/socketTest.py	(revision 1198)
+++ framspy/gui/tests/socketTest.py	(revision 1198)
@@ -0,0 +1,133 @@
+import unittest
+
+import os
+import sys
+import inspect
+
+currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
+parentdir = os.path.dirname(currentdir)
+parentdir = os.path.dirname(parentdir)
+sys.path.insert(0, parentdir) 
+
+from gui.framsutils.FramsSocket import FramsSocket
+
+class SocketTest(unittest.TestCase):
+    # Make sure to launch an empty server, without any running jobs etc.
+    ADDRESS = "127.0.0.1"
+    PORT = 9009
+
+    def test_connection(self):
+        frams = FramsSocket()
+        
+        try:
+            frams.initConnection(self.ADDRESS, self.PORT)
+            self.assertTrue(frams.comm.client.connected, "Connection inner state")
+        except ConnectionError:
+            self.assertTrue(False, "ConnectionError exception")
+
+        frams.closeConnection()
+
+    def test_communication_parsing1(self):
+        frams = FramsSocket()
+        frams.initConnection(self.ADDRESS, self.PORT)
+
+        response = frams.sendRequest("get / motd")
+        data = frams._infoParser(response)
+        frams.closeConnection()
+
+        self.assertEqual(len(data), 1, "Response is to short")
+        self.assertEqual(data[0].p["_classname"], "Server")
+        self.assertIn("motd", data[0].p) #check if contains motd field
+        self.assertIsInstance(data[0].p["motd"], str) #check if response have a corect type
+
+    def test_communication_parsing2(self):
+        frams = FramsSocket()
+        frams.initConnection(self.ADDRESS, self.PORT)
+
+        response = frams.sendRequest("get /simulator{version_string,version_int}")
+        data = frams._infoParser(response)
+        frams.closeConnection()
+
+        self.assertEqual(len(data), 1, "Response is to short")
+        self.assertEqual(data[0].p["_classname"], "Simulator")
+        self.assertIn("version_string", data[0].p) #check if contains motd field
+        self.assertIsInstance(data[0].p["version_string"], str) #check if response have a corect type
+        self.assertIn("version_int", data[0].p) #check if contains motd field
+        self.assertIsInstance(data[0].p["version_int"], int) #check if response have a corect type
+
+    def test_genotype_mod(self):
+        frams = FramsSocket()
+        frams.initConnection(self.ADDRESS, self.PORT)
+
+        genotype = "X"
+        info = "test info"
+
+        frams.sendRequest("call /simulator/genepools/groups/0 clear")
+        frams.sendRequest("call /simulator/genepools/groups/0 add {}".format(genotype))
+        genotypeInfo = frams.readGenotypeInfo(0, 0)
+        prop = next((p for p in genotypeInfo if p.p["id"] == "genotype"), None)
+        self.assertTrue(prop, "genotype property not found")
+        self.assertEqual(prop.p["value"], genotype, "genotype not match")
+        
+        frams.sendRequest("set /simulator/genepools/groups/0/genotypes/0 info \"{}\"".format(info))
+        genotypeInfo = frams.readGenotypeInfo(0, 0)
+        prop = next((p for p in genotypeInfo if p.p["id"] == "info"), None)
+        self.assertTrue(prop, "info property not found")
+        self.assertEqual(prop.p["value"], info, "info not match")
+
+        frams.sendRequest("call /simulator/genepools/groups/0/genotypes/0 delete")
+
+        frams.closeConnection()
+
+    def test_multiline_strings(self):
+        frams = FramsSocket()
+        frams.initConnection(self.ADDRESS, self.PORT)
+
+        info = r"test\ntest2"
+        infoResponse = "test\ntest2"
+
+        #prepare known multiline field
+        frams.sendRequest("call /simulator/genepools/groups/0 clear")
+        frams.sendRequest("call /simulator/genepools/groups/0 add {}".format("X"))
+
+        #set and get multiline field
+        frams.sendRequest("set /simulator/genepools/groups/0/genotypes/0 info \"{}\"".format(info))
+        genotypeInfo = frams.readGenotypeInfo(0, 0)
+        prop = next((p for p in genotypeInfo if p.p["id"] == "info"), None)
+        self.assertTrue(prop, "info property not found")
+        self.assertMultiLineEqual(prop.p["value"], infoResponse, "multiline string not match")
+
+        frams.sendRequest("call /simulator/genepools/groups/0/genotypes/0 delete")
+
+        frams.closeConnection()
+
+    def test_multiline_string_encoding(self):
+        import gui.framsutils.framsProperty as framsProperty
+
+        frams = FramsSocket()
+        frams.initConnection(self.ADDRESS, self.PORT)
+
+        #prepare known multiline field
+        frams.sendRequest("call /simulator/genepools/groups/0 clear")
+        frams.sendRequest("call /simulator/genepools/groups/0 add {}".format("X"))
+
+        info = \
+r"Long multiline text\
+1\\~\
+22\~\n\
+333\ \\\
+4444"
+        encodedInfo = framsProperty.encodeString(info)
+
+        frams.sendRequest("set /simulator/genepools/groups/0/genotypes/0 info \"{}\"".format(encodedInfo))
+        genotypeInfo = frams.readGenotypeInfo(0, 0)
+        prop = next((p for p in genotypeInfo if p.p["id"] == "info"), None)
+        self.assertTrue(prop, "info property not found")
+        self.assertMultiLineEqual(prop.p["value"], info, "encoded string not match")
+
+        frams.sendRequest("call /simulator/genepools/groups/0/genotypes/0 delete")
+
+        frams.closeConnection()
+
+if __name__ == "__main__":
+    unittest.main()
Index: framspy/gui/utils.py
===================================================================
--- framspy/gui/utils.py	(revision 1198)
+++ framspy/gui/utils.py	(revision 1198)
@@ -0,0 +1,50 @@
+from threading import Timer
+
+class Swap:
+    def __init__(self, init = None) -> None:
+        self.init = init
+        self.clear()
+        self.select = False
+        self.empty = False
+
+    def update(self, new):
+        if not new:
+            if self.empty == False:
+                self.empty = True
+                return
+        else:
+            self.empty = False
+
+        if self.select:
+            self.one = new
+        else:
+            self.two = new
+        self.select = not self.select
+
+    def get(self):
+        if self.select:
+            return self.two
+        else:
+            return self.one
+
+    def clear(self):
+        self.one = self.init
+        self.two = self.init
+
+#source: https://gist.github.com/walkermatt/2871026
+def debounce(wait):
+    """ Decorator that will postpone a function's
+        execution until after wait seconds
+        have elapsed since the last time it was invoked. """
+    def decorator(fn):
+        def debounced(*args, **kwargs):
+            def call_it():
+                fn(*args, **kwargs)
+            try:
+                debounced.t.cancel()
+            except(AttributeError):
+                pass
+            debounced.t = Timer(wait, call_it)
+            debounced.t.start()
+        return debounced
+    return decorator
Index: framspy/gui/visual/camera.py
===================================================================
--- framspy/gui/visual/camera.py	(revision 1198)
+++ framspy/gui/visual/camera.py	(revision 1198)
@@ -0,0 +1,76 @@
+import glm
+from gui.visual.mouse import Mouse
+from gui.visual.player import Player
+
+class Camera:
+    def __init__(self, player: Player) -> None:
+        self.position = glm.vec3(0, 0, 1)
+        self.pith = 10
+        self.yaw = 0
+        self.roll = 0
+        self.angleAroundPlayer = 0
+        self.player = player
+        self.distanceFromPlayer = 10
+        self.mx = 0
+        self.my = 0
+        self.scale = 1
+
+    def setPosition(self, x, y):
+        self.player.position = glm.vec3(x, y, 0)
+
+    def setZoomScale(self, scale):
+        self.scale = scale
+
+    def setZoom(self, zoom):
+        self.distanceFromPlayer = 0.70140280561122 * zoom - 0.028056112224449 #values from linear interpolation
+
+    def move(self):
+        self.calculateZoom()
+        self.calculatePitchAndAngleAroundPLayer()
+        horizontalDistance = self.calculateHorizotalDistance()
+        verticalDistance = self.calculateVerticalDistance()
+        self.calculateCameraPosition(horizontalDistance, verticalDistance)
+
+    def calculateZoom(self):
+        zoomLevel = Mouse.getDWheel() * self.scale
+        self.distanceFromPlayer -= zoomLevel
+        if self.distanceFromPlayer < 0.1:
+            self.distanceFromPlayer = 0.1
+
+    def calculateCameraPosition(self, horizontalDistance, verticalDistance):
+        theta = self.player.rotY + self.angleAroundPlayer
+        offsetX = horizontalDistance * glm.sin(glm.radians(theta))
+        offsetY = horizontalDistance * glm.cos(glm.radians(theta))
+        self.position.x = self.player.position.x - offsetX
+        self.position.y = self.player.position.y + offsetY
+        self.position.z = self.player.position.z + verticalDistance
+        yaw = 180 - theta
+
+    def calculateHorizotalDistance(self):
+        return self.distanceFromPlayer * glm.cos(glm.radians(self.pith))
+
+    def calculateVerticalDistance(self):
+        return self.distanceFromPlayer * glm.sin(glm.radians(self.pith))
+
+    def calculatePitchAndAngleAroundPLayer(self):
+        if Mouse.isButtonDown(Mouse.MOUSE_LEFT):
+            dx = Mouse.getX() - self.mx
+            dy = Mouse.getY() - self.my
+            pitchChange = dy * 0.1
+            self.pith += pitchChange
+            if self.pith < -90:
+                self.pith = -89.99
+            if self.pith > 90:
+                self.pith = 89.99
+
+            angleChange = dx * 0.3
+            self.angleAroundPlayer -= angleChange
+        elif Mouse.isButtonDown(Mouse.MOUSE_RIGHT):
+            dx = Mouse.getX() - self.mx
+            dy = Mouse.getY() - self.my
+            theta = glm.radians(self.player.rotY + self.angleAroundPlayer)
+            scale = self.distanceFromPlayer * 10 / 250
+            self.player.position.x += (dx * glm.cos(theta) + dy * glm.sin(theta)) * 0.1 * scale
+            self.player.position.y += (dx * glm.sin(theta) - dy * glm.cos(theta)) * 0.1 * scale
+        self.mx = Mouse.getX()
+        self.my = Mouse.getY()
Index: framspy/gui/visual/entity.py
===================================================================
--- framspy/gui/visual/entity.py	(revision 1198)
+++ framspy/gui/visual/entity.py	(revision 1198)
@@ -0,0 +1,36 @@
+import glm
+from gui.visual.modelData import ModelData
+from OpenGL.GL import *
+
+class Entity():
+    def __init__(self, x: float, y: float, z: float, model: ModelData, scale: float = None, rotX: float = None, rotY: float = None, rotZ: float = None, angle: float = None, vector: glm.vec3 = None, color: glm.vec3 = None, rotMatrix: glm.mat4 = None):
+        self.position = glm.vec3(x, y, z)
+        self.rotX = rotX
+        self.rotY = rotY
+        self.rotZ = rotZ
+        self.rotMatrix = rotMatrix
+        self.angle = angle
+        self.vector = vector
+        self.scale = scale
+        self.modelData = model
+        self.textureIndex = 0
+        self.scaleX = None
+        self.scaleY = None
+        self.scaleZ = None
+        self.color = color
+
+    def getTextureXOffset(self):
+        column = self.textureIndex % self.modelData.texture.numberOfRows
+        return column / self.modelData.texture.numberOfRows
+
+    def getTextureYOffset(self):
+        row = self.textureIndex % self.modelData.texture.numberOfRows
+        return row / self.modelData.texture.numberOfRows
+
+    def increasePosition(self, dx: float, dy: float, dz: float):
+        self.position += glm.vec3(dx, dy, dz)
+
+    def increaseRotation(self, dx: float, dy: float, dz: float):
+        self.rotX += dx
+        self.rotY += dy
+        self.rotZ += dz
Index: framspy/gui/visual/entityRenderer.py
===================================================================
--- framspy/gui/visual/entityRenderer.py	(revision 1198)
+++ framspy/gui/visual/entityRenderer.py	(revision 1198)
@@ -0,0 +1,71 @@
+from gui.visual.staticShader import StaticShader
+from gui.visual.entity import Entity
+import OpenGL.GL as gl
+import glm
+
+class EntityRenderer:
+	def __init__(self, staticShader: StaticShader, projectionMatrix) -> None:
+		self.shader = staticShader
+		self.shader.start()
+		self.loadProjectionMatrix(projectionMatrix)
+
+	def render(self, entities: dict, textures: bool):
+		for item in entities.items():
+			self.prepareModelData(item[0], textures)
+			batch = item[1]
+			for entity in batch:
+				self.prepareInstance(entity)
+				gl.glDrawElements(gl.GL_TRIANGLES, item[0].rawModel.vertexCount, gl.GL_UNSIGNED_INT, None)
+			self.unbindModelData()
+
+	def prepareModelData(self, model, textures: bool):
+		rawModel = model.rawModel
+		gl.glBindVertexArray(rawModel.vaoID)
+		gl.glEnableVertexAttribArray(0)
+		gl.glEnableVertexAttribArray(1)
+		gl.glEnableVertexAttribArray(2)
+
+		self.shader.loadTextureOn(textures)
+
+		if textures:
+			gl.glActiveTexture(gl.GL_TEXTURE0)
+			gl.glBindTexture(gl.GL_TEXTURE_2D, model.texture.id)
+		else:
+			gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
+			gl.glDisable(gl.GL_TEXTURE_2D)
+
+	def unbindModelData(self):
+		gl.glDisableVertexAttribArray(0)
+		gl.glDisableVertexAttribArray(1)
+		gl.glDisableVertexAttribArray(2)
+		gl.glBindVertexArray(0)
+
+	def createTransformationMatrix(self, entity: Entity):
+		matrix = glm.mat4(1)
+		matrix = glm.translate(matrix, entity.position)
+
+		if entity.angle is not None:
+			matrix = glm.rotate(matrix, entity.angle, entity.vector)
+		elif entity.rotMatrix is not None:
+			matrix = matrix * entity.rotMatrix
+		else:
+			matrix = glm.rotate(matrix, entity.rotX, glm.vec3(1, 0, 0))
+			matrix = glm.rotate(matrix, entity.rotY, glm.vec3(0, 1, 0))
+			matrix = glm.rotate(matrix, entity.rotZ, glm.vec3(0, 0, 1))
+
+		if entity.scaleX is not None and entity.scaleY is not None and entity.scaleZ is not None:
+			matrix = glm.scale(matrix, glm.vec3(entity.scaleX, entity.scaleY, entity.scaleZ))
+		else:
+			matrix = glm.scale(matrix, glm.vec3(entity.scale))
+
+		return matrix
+
+	def prepareInstance(self, entity: Entity):
+		transformationMatrix = self.createTransformationMatrix(entity)
+		self.shader.loadTransformationMatrix(transformationMatrix)
+
+		if entity.color:
+			self.shader.loadColor(entity.color)
+
+	def loadProjectionMatrix(self, projectionMatrix):
+		self.shader.loadProjectionMatrix(projectionMatrix)
Index: framspy/gui/visual/loader.py
===================================================================
--- framspy/gui/visual/loader.py	(revision 1198)
+++ framspy/gui/visual/loader.py	(revision 1198)
@@ -0,0 +1,118 @@
+import OpenGL.GL as gl
+from OpenGL.raw.GL.VERSION.GL_1_5 import GL_STATIC_DRAW
+from gui.visual.rawModel import RawModel
+from PIL import Image
+import numpy as np
+from typing import List
+
+class Loader:
+    def __init__(self) -> None:
+        self.vaos = []
+        self.vbos = []
+        self.textures = []
+
+    def loadToVao5(self, positions, textureCoords, normals, indieces, vertexCount: int):
+        vaoid = self.createVAO()
+        self.bindIndicesBuffer(indieces)
+        self.storeDataInAttributeList(0, 3, positions)
+        self.storeDataInAttributeList(1, 2, textureCoords)
+        self.storeDataInAttributeList(2, 3, normals)
+        self.unbindVAO()
+        return RawModel(vaoid, vertexCount)
+
+    def loadToVao3(self, positions, textureCoords, vertexCount: int):
+        vaoid = self.createVAO()
+        self.storeDataInAttributeList(0, 3, positions)
+        self.storeDataInAttributeList(1, 2, textureCoords)
+        self.unbindVAO()
+        return RawModel(vaoid, vertexCount)
+
+    def loadToVao2(self, positions, colors, vertexCount: int):
+        vaoid = self.createVAO()
+        self.storeDataInAttributeList(0, 3, positions)
+        self.storeDataInAttributeList(1, 3, colors)
+        self.unbindVAO()
+        return RawModel(vaoid, vertexCount)
+
+    def loadToVao1(self, positions, vertexCount: int):
+        vaoid = self.createVAO()
+        self.storeDataInAttributeList(0, 3, positions)
+        self.unbindVAO()
+        return RawModel(vaoid, vertexCount)
+
+    def loadTexture(self, filename: str) -> int:
+        image = Image.open(filename)
+        img_data = np.array(list(image.getdata()), np.uint8)
+
+        textureid = gl.glGenTextures(1)
+        gl.glBindTexture(gl.GL_TEXTURE_2D, textureid)
+        colors = gl.GL_RGB
+        if image.mode == 'RGBA':
+            colors = gl.GL_RGBA
+        gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGB, image.width, image.height, 0, colors, gl.GL_UNSIGNED_BYTE, img_data)
+        
+        gl.glGenerateMipmap(gl.GL_TEXTURE_2D)
+        gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR_MIPMAP_LINEAR),
+        gl.glTexParameterf(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_LOD_BIAS, -2.4)
+
+        self.textures.append(textureid)
+        return textureid
+
+    def loadCubemap(self, filenames: List[str]) -> int:
+        if len(filenames) != 6:
+            raise Exception("filenames must contain exactly 6 textures")
+        textureid = gl.glGenTextures(1)
+        gl.glBindTexture(gl.GL_TEXTURE_CUBE_MAP, textureid)
+
+        for i, filename in enumerate(filenames):
+            image = Image.open(filename)
+            img_data = np.array(list(image.getdata()), np.uint8)
+            colors = gl.GL_RGB
+            if image.mode == "RGBA":
+                colors = gl.GL_RGBA
+            gl.glTexImage2D(gl.GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, gl.GL_RGB, image.width, image.height, 0, colors, gl.GL_UNSIGNED_BYTE, img_data)
+
+        gl.glTexParameteri(gl.GL_TEXTURE_CUBE_MAP, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
+        gl.glTexParameteri(gl.GL_TEXTURE_CUBE_MAP, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
+        gl.glTexParameteri(gl.GL_TEXTURE_CUBE_MAP, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
+        gl.glTexParameteri(gl.GL_TEXTURE_CUBE_MAP, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
+        gl.glTexParameteri(gl.GL_TEXTURE_CUBE_MAP, gl.GL_TEXTURE_WRAP_R, gl.GL_CLAMP_TO_EDGE)
+
+        return textureid
+
+    def cleanUp(self):
+        for vao in self.vaos:
+            gl.glDeleteVertexArrays(1, vao)
+
+        for vbo in self.vbos:
+            gl.glDeleteBuffers(1, vbo)
+
+        for texture in self.textures:
+            gl.glDeleteTextures(1, texture)
+
+    def createVAO(self):
+        vao_id = gl.glGenVertexArrays(1)
+        self.vaos.append(vao_id)
+        gl.glBindVertexArray(vao_id)
+        return vao_id
+
+    def storeDataInAttributeList(self, attributeNumber: int, coordinateSize: int, data: list):
+        vboid = gl.glGenBuffers(1)
+        self.vbos.append(vboid)
+        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, vboid)
+        npdata = np.array(data, dtype=np.float32)
+        gl.glBufferData(gl.GL_ARRAY_BUFFER, len(data) * 4, npdata, gl.GL_STATIC_DRAW)
+        gl.glVertexAttribPointer(attributeNumber, coordinateSize, gl.GL_FLOAT, gl.GL_FALSE, 0, None)
+
+        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0)
+
+    def unbindVAO(self):
+        gl.glBindVertexArray(0)
+
+    def bindIndicesBuffer(self, indices):
+        vboid = gl.glGenBuffers(1)
+        self.vbos.append(vboid)
+        gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, vboid)
+        npindices = np.array(indices, dtype=np.int32)
+        gl.glBufferData(gl.GL_ELEMENT_ARRAY_BUFFER, len(indices) * 4, npindices, GL_STATIC_DRAW)
+        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0)
Index: framspy/gui/visual/masterRenderer.py
===================================================================
--- framspy/gui/visual/masterRenderer.py	(revision 1198)
+++ framspy/gui/visual/masterRenderer.py	(revision 1198)
@@ -0,0 +1,83 @@
+from gui.visual.player import Player
+from gui.visual.entity import Entity
+import glm
+import OpenGL.GL as gl
+from gui.visual.camera import Camera
+from gui.visual.staticShader import StaticShader
+from gui.visual.entityRenderer import EntityRenderer
+from gui.visual.skyboxRenderer import SkyboxRenderer
+from gui.visual.worldRenderer import WorldRenderer
+
+class MasterRenderer:
+    FOV = 70
+    NEAR_PLANE = 0.1
+    FAR_PLANE = 17500 # max world size + some offset
+    INITIAL_WIDTH = 800
+    INITIAL_HEIGHT = 600
+
+    def __init__(self, loader) -> None:
+        self._width = self.INITIAL_WIDTH
+        self._height = self.INITIAL_HEIGHT
+        self._createProjectionMatrix()
+        self._shader = StaticShader()
+        self._renderer = EntityRenderer(self._shader, self.projectionMatrix)
+        self._skyboxRenderer = SkyboxRenderer(loader, self.projectionMatrix)
+        self.worldRenderer = WorldRenderer(loader, self.projectionMatrix, self._shader)
+        self._entities = {}
+        self._isTexturesOn = True
+
+    def renderScene(self, player: Player, entities: list, camera: Camera):
+        for entity in entities:
+            self.processEntity(entity)
+
+        self.render(camera)
+
+    def render(self, camera: Camera):
+        self.__prepare()
+        self._shader.start()
+        self._shader.loadViewMatrix(camera)
+        self._renderer.render(self._entities, self._isTexturesOn)
+        self.worldRenderer.render(camera)
+        self._skyboxRenderer.render(camera)
+        self._shader.stop()
+        self._entities.clear()
+
+    def processEntity(self, entity: Entity):
+        entityModel = entity.modelData
+        batch = self._entities.get(entityModel)
+        if batch:
+            batch.append(entity)
+        else:
+            newBatch = [entity]
+            self._entities[entityModel] = newBatch
+
+    def toggleTextures(self):
+        self._isTexturesOn = not self.isTexturesOn
+
+    def cleanUp(self):
+        self._shader.cleanUp()
+
+    def resize(self, width, height):
+        self._width = width
+        self._height = height
+        self._createProjectionMatrix()
+        self._shader.start()
+        self._renderer.loadProjectionMatrix(self.projectionMatrix)
+        self._shader.stop()
+        self._skyboxRenderer.shader.start()
+        self._skyboxRenderer.loadProjectionMatrix(self.projectionMatrix)
+        self._skyboxRenderer.shader.stop()
+        self.worldRenderer.shader.start()
+        self.worldRenderer.loadProjectionMatrix(self.projectionMatrix)
+        self.worldRenderer.shader.stop()
+
+    def __prepare(self):
+        gl.glEnable(gl.GL_DEPTH_TEST)
+        gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
+        gl.glClearColor(0.0, 0.0, 0.0, 1)
+
+    def _createProjectionMatrix(self):
+        self.projectionMatrix = glm.perspective(self.FOV, self._width / self._height, self.NEAR_PLANE, self.FAR_PLANE)
+
+    def reloadWorld(self, loader, worldType: int, simType: int, worldSize: float, worldMap: str, worldBoundaries: int, worldWaterLevel: float) -> None:
+        self.worldRenderer.reloadWorld(loader, worldType, simType, worldSize, worldMap, worldBoundaries, worldWaterLevel)
Index: framspy/gui/visual/modelData.py
===================================================================
--- framspy/gui/visual/modelData.py	(revision 1198)
+++ framspy/gui/visual/modelData.py	(revision 1198)
@@ -0,0 +1,7 @@
+from gui.visual.rawModel import RawModel
+from gui.visual.modelTexture import ModelTexture
+
+class ModelData:
+    def __init__(self, rawModel: RawModel, modelTexture: ModelTexture) -> None:
+        self.rawModel = rawModel
+        self.texture = modelTexture
Index: framspy/gui/visual/modelTexture.py
===================================================================
--- framspy/gui/visual/modelTexture.py	(revision 1198)
+++ framspy/gui/visual/modelTexture.py	(revision 1198)
@@ -0,0 +1,4 @@
+class ModelTexture:
+    def __init__(self, texture: int) -> None:
+        self.id = texture
+        self.numberOfRows = 1
Index: framspy/gui/visual/mouse.py
===================================================================
--- framspy/gui/visual/mouse.py	(revision 1198)
+++ framspy/gui/visual/mouse.py	(revision 1198)
@@ -0,0 +1,46 @@
+class Mouse:
+    MOUSE_NONE = 0
+    MOUSE_LEFT = 1
+    MOUSE_RIGHT = 2
+
+    _dwheel = 0
+    _dbutton = MOUSE_NONE
+    _xactual = -1
+    _yactual = -1
+    _dx = -1
+    _dy = -1
+
+    @staticmethod
+    def setDWheel(value: float):
+        Mouse._dwheel = value
+
+    @staticmethod
+    def incrementDWheel(value: float):
+        Mouse._dwheel += value
+
+    @staticmethod
+    def setButton(button: int):
+        Mouse._dbutton = button
+
+    @staticmethod
+    def setXY(x: int, y: int):
+        Mouse._xactual = x
+        Mouse._yactual = y
+
+    @staticmethod
+    def getDWheel() -> float:
+        t = Mouse._dwheel
+        Mouse._dwheel = 0
+        return t
+
+    @staticmethod
+    def getX() -> float:
+        return Mouse._xactual
+
+    @staticmethod
+    def getY() -> float:
+        return Mouse._yactual
+
+    @staticmethod
+    def isButtonDown(button: int) -> bool:
+        return Mouse._dbutton == button
Index: framspy/gui/visual/objLoader.py
===================================================================
--- framspy/gui/visual/objLoader.py	(revision 1198)
+++ framspy/gui/visual/objLoader.py	(revision 1198)
@@ -0,0 +1,128 @@
+from gui.visual.loader import Loader
+from gui.visual.modelData import ModelData
+from gui.visual.modelTexture import ModelTexture
+import glm
+
+class OBJLoader:
+    class PackedVertex:
+        def __init__(self, position, uv, normal) -> None:
+            self.position = position
+            self.uv = uv
+            self.normal = normal
+
+    @staticmethod
+    def loadOBJ(objFileName: str, textureFileName: str, loader: Loader):
+        vertices, uvs, normals = OBJLoader._loadOBJ(objFileName)
+        indices, indexed_vertices, indexed_uvs, indexed_normals = OBJLoader._indexVBO(vertices, uvs, normals)
+        verticesf = OBJLoader._vec3ToFloat(indexed_vertices)
+        uvsf = OBJLoader._vec2ToFloat(indexed_uvs)
+        normalsf = OBJLoader._vec3ToFloat(indexed_normals)
+        rawModel = loader.loadToVao5(verticesf, uvsf, normalsf, indices, len(vertices))
+        model = ModelData(rawModel, ModelTexture(loader.loadTexture(textureFileName) if textureFileName else 0))
+        return model
+
+    @staticmethod
+    def _loadOBJ(path):
+        vertexIndices = []
+        uvIndices = []
+        normalIndices = []
+        temp_vertices = []
+        temp_uvs = []
+        temp_normals = []
+        out_vertices = []
+        out_uvs = []
+        out_normals = []
+
+        with open(path, "r") as file:
+            while (line := file.readline()):
+                if line[0:2] == 'v ':
+                    currentLine = line.split()
+                    vertex = glm.vec3(float(currentLine[1]), float(currentLine[2]), float(currentLine[3]))
+                    temp_vertices.append(vertex)
+                elif line[0:2] == 'vt':
+                    currentLine = line.split()
+                    uv = glm.vec2(float(currentLine[1]), float(currentLine[2]))
+                    temp_uvs.append(uv)
+                elif line[0:2] == 'vn':
+                    currentLine = line.split()
+                    normal = glm.vec3(float(currentLine[1]), float(currentLine[2]), float(currentLine[3]))
+                    temp_normals.append(normal)
+                elif line[0] == 'f':
+                    currentLine = line.split()
+                    vertexIndex = [0,0,0]
+                    uvIndex = [0,0,0]
+                    normalIndex = [0,0,0]
+                    for i in range(1, 4):
+                        trip = currentLine[i].split('/')
+                        vertexIndex[i-1] = int(trip[0])
+                        if len(trip) > 1:
+                            if trip[1]:
+                                uvIndex[i-1] = int(trip[1])
+                            else:
+                                uvIndex[i-1] = 0
+                        if len(trip) > 2:
+                            normalIndex[i-1] = int(trip[2])
+                    vertexIndices.extend(vertexIndex)
+                    uvIndices.extend(uvIndex)
+                    normalIndices.extend(normalIndex)
+            
+            for i in range(len(vertexIndices)):
+                vertex = temp_vertices[vertexIndices[i] - 1]
+                out_vertices.append(vertex)
+                if temp_uvs:
+                    uv = temp_uvs[uvIndices[i] - 1]
+                else:
+                    uv = glm.vec2(0)
+                out_uvs.append(uv)
+                if temp_normals:
+                    normal = temp_normals[normalIndices[i] - 1]
+                else:
+                    normal = glm.vec3(0)
+                out_normals.append(normal)
+
+        return out_vertices, out_uvs, out_normals
+
+    @staticmethod
+    def _indexVBO(vertices, uvs, normals):
+        out_indices = []
+        out_vertices = []
+        out_uvs = []
+        out_normals = []
+        vertexToOutIndex = {}
+
+        for vertex, uv, normal in zip(vertices, uvs, normals):
+            packed = OBJLoader.PackedVertex(vertex, uv, normal)
+            found, index = OBJLoader._getSimilarVertexIndex(packed, vertexToOutIndex)
+
+            if not found:
+                out_vertices.append(vertex)
+                out_uvs.append(uv)
+                out_normals.append(normal)
+                index = len(out_vertices) - 1
+                vertexToOutIndex[packed] = index
+            
+            out_indices.append(index)
+        
+        return out_indices, out_vertices, out_uvs, out_normals
+
+    @staticmethod
+    def _getSimilarVertexIndex(vertex, VertexToOutIndex: dict):
+        result = VertexToOutIndex.get(vertex)
+        return True if result else False, result
+
+    @staticmethod
+    def _vec3ToFloat(vector):
+        out = []
+        for v in vector:
+            out.append(v.x)
+            out.append(v.y)
+            out.append(v.z)
+        return out
+
+    @staticmethod
+    def _vec2ToFloat(vector):
+        out = []
+        for v in vector:
+            out.append(v.x)
+            out.append(v.y)
+        return out
Index: framspy/gui/visual/player.py
===================================================================
--- framspy/gui/visual/player.py	(revision 1198)
+++ framspy/gui/visual/player.py	(revision 1198)
@@ -0,0 +1,5 @@
+from gui.visual.entity import Entity
+
+class Player(Entity):
+    def __init__(self, x, y, z, rotX, rotY, rotZ):
+        super().__init__(x, y, z, 0, [], rotX, rotY, rotZ)
Index: framspy/gui/visual/rawModel.py
===================================================================
--- framspy/gui/visual/rawModel.py	(revision 1198)
+++ framspy/gui/visual/rawModel.py	(revision 1198)
@@ -0,0 +1,4 @@
+class RawModel:
+    def __init__(self, vaoID: int, vertexCount: int) -> None:
+        self.vaoID = vaoID
+        self.vertexCount = vertexCount
Index: framspy/gui/visual/shaderProgram.py
===================================================================
--- framspy/gui/visual/shaderProgram.py	(revision 1198)
+++ framspy/gui/visual/shaderProgram.py	(revision 1198)
@@ -0,0 +1,134 @@
+import OpenGL.GL as gl
+import os
+import glm
+
+class ShaderProgram:
+    def __init__(self, vertexFile: str, fragmentFile: str, geometryFile: str = None) -> None:
+        self.initShaderFromGLSL(vertexFile, fragmentFile, geometryFile)
+        self.programID = gl.glCreateProgram()
+        gl.glAttachShader(self.programID, self.vs)
+        gl.glAttachShader(self.programID, self.fs)
+        if geometryFile:
+            gl.glAttachShader(self.programID, self.gs)
+
+    def start(self):
+        gl.glUseProgram(self.programID)
+
+    def stop(self):
+        gl.glUseProgram(0)
+
+    def cleanUp(self):
+        self.stop()
+        gl.glDetachShader(self.programID, self.vs)
+        gl.glDetachShader(self.programID, self.fs)
+        gl.glDeleteShader(self.vs)
+        gl.glDeleteShader(self.fs)
+        gl.glDeleteProgram(self.programID)
+
+    def bindAttributes(self):
+        pass
+
+    def getAllUniformLocations(self):
+        pass
+
+    def getUniformLocation(self, uniformName):
+        return gl.glGetUniformLocation(self.programID, uniformName)
+
+    def bindAttribute(self, attribute: int, variableName: str):
+        gl.glBindAttribLocation(self.programID, attribute, variableName)
+
+    def loadFloat(self, location: int, value: float):
+        gl.glUniform1f(location, value)
+
+    def loadInt(self, location: int, value: int):
+        gl.glUniform1i(location, value)
+
+    def loadVector3(self, location: int, value):
+        gl.glUniform3fv(location, 1, glm.value_ptr(value))
+
+    def loadVector2(self, location: int, value):
+        gl.glUniform2fv(location, 1, glm.value_ptr(value))
+
+    def loadVector4(self, location: int, value):
+        gl.glUniform4fv(location, 1, glm.value_ptr(value))
+
+    def loadBool(self, location: int, value: bool):
+        gl.glUniform1f(location, value == 1)
+
+    def loadMatrix4(self, location: int, value):
+        gl.glUniformMatrix4fv(location, 1, False, glm.value_ptr(value))
+
+    def initShaderFromGLSL(self, vertex_shader_path, fragment_shader_path, geometry_shader_path = None):
+        vertex_shader_source_list = []
+        fragment_shader_source_list = []
+        geometry_shader_source_list = []
+        if(isinstance(vertex_shader_path, str)):
+            absDIR =  os.path.abspath(os.path.join(os.path.join(os.path.dirname(__file__), ".."), vertex_shader_path))
+            f = open(absDIR, 'rb')
+            vertex_shader_source_list.append(f.read())
+            f.close()
+
+            absDIR =  os.path.abspath(os.path.join(os.path.join(os.path.dirname(__file__), ".."), fragment_shader_path))
+            f = open(absDIR, 'rb')
+            fragment_shader_source_list.append(f.read())      
+            f.close()
+
+            if geometry_shader_path:
+                absDIR =  os.path.abspath(os.path.join(os.path.join(os.path.dirname(__file__), ".."), geometry_shader_path))
+                f = open(absDIR, 'rb')
+                geometry_shader_source_list.append(f.read())      
+                f.close()
+
+            self.initShader(vertex_shader_source_list, fragment_shader_source_list, geometry_shader_source_list)
+
+    def initShader(self, vertex_shader_source_list, fragment_shader_source_list, geometry_shader_source_list):
+        # create program
+        self.program = gl.glCreateProgram() # pylint: disable=E1111
+        #print('create program ',self.program)
+        ShaderProgram.printOpenGLError()
+
+        # vertex shader
+        #print('compile vertex shader...')
+        self.vs = gl.glCreateShader(gl.GL_VERTEX_SHADER) # pylint: disable=E1111
+        gl.glShaderSource(self.vs, vertex_shader_source_list)
+        gl.glCompileShader(self.vs)
+        if gl.GL_TRUE != gl.glGetShaderiv(self.vs, gl.GL_COMPILE_STATUS):
+            err = gl.glGetShaderInfoLog(self.vs) 
+            raise Exception(err)  
+        #gl.glAttachShader(self.program, self.vs)
+        ShaderProgram.printOpenGLError()
+
+        # fragment shader
+        #print('compile fragment shader...')
+        self.fs = gl.glCreateShader(gl.GL_FRAGMENT_SHADER) # pylint: disable=E1111
+        gl.glShaderSource(self.fs, fragment_shader_source_list)
+        gl.glCompileShader(self.fs)
+        if gl.GL_TRUE!=gl.glGetShaderiv(self.fs, gl.GL_COMPILE_STATUS):
+            err = gl.glGetShaderInfoLog(self.fs) 
+            raise Exception(err)       
+        #gl.glAttachShader(self.program, self.fs)
+        ShaderProgram.printOpenGLError()
+
+        if geometry_shader_source_list:
+            self.gs = gl.glCreateShader(gl.GL_GEOMETRY_SHADER) # pylint: disable=E1111
+            gl.glShaderSource(self.gs, geometry_shader_source_list)
+            gl.glCompileShader(self.gs)
+            if gl.GL_TRUE!=gl.glGetShaderiv(self.gs, gl.GL_COMPILE_STATUS):
+                err = gl.glGetShaderInfoLog(self.gs) 
+                print(err.decode("utf-8"))
+                raise Exception(err)       
+            #gl.glAttachShader(self.program, self.gs)
+            ShaderProgram.printOpenGLError()
+
+        #print('link...')
+        #gl.glLinkProgram(self.program)
+        #if(gl.GL_TRUE != gl.glGetProgramiv(self.program, gl.GL_LINK_STATUS)):
+        #    err =  gl.glGetShaderInfoLog(self.vs) 
+        #    raise Exception(err)          
+        #printOpenGLError()
+
+    @staticmethod
+    def printOpenGLError():
+        err = gl.glGetError() # pylint: disable=E1111
+        if (err != gl.GL_NO_ERROR):
+            print('GLERROR: ', gl.gluErrorString(err)) # pylint: disable=E1101
Index: framspy/gui/visual/shaders/SkyboxFragmentShader.glsl
===================================================================
--- framspy/gui/visual/shaders/SkyboxFragmentShader.glsl	(revision 1198)
+++ framspy/gui/visual/shaders/SkyboxFragmentShader.glsl	(revision 1198)
@@ -0,0 +1,13 @@
+#version 330
+
+in vec3 textureCoords;
+out vec4 out_Color;
+
+uniform samplerCube modelTexture;
+
+void main(void){
+	vec4 textureColor = texture(modelTexture, textureCoords);
+
+    out_Color = textureColor;
+	//out_Color = vec4(1, 0, 0, 1);
+}
Index: framspy/gui/visual/shaders/SkyboxVertexShader.glsl
===================================================================
--- framspy/gui/visual/shaders/SkyboxVertexShader.glsl	(revision 1198)
+++ framspy/gui/visual/shaders/SkyboxVertexShader.glsl	(revision 1198)
@@ -0,0 +1,13 @@
+#version 330
+
+in vec3 position;
+
+out vec3 textureCoords;
+
+uniform mat4 projectionMatrix;
+uniform mat4 viewMatrix;
+
+void main(void){
+	gl_Position = (projectionMatrix * viewMatrix * vec4(position, 1.0)).xyww; 
+	textureCoords = position;
+}
Index: framspy/gui/visual/shaders/WorldFragmentShader.glsl
===================================================================
--- framspy/gui/visual/shaders/WorldFragmentShader.glsl	(revision 1198)
+++ framspy/gui/visual/shaders/WorldFragmentShader.glsl	(revision 1198)
@@ -0,0 +1,37 @@
+#version 330
+
+in vec3 g_color;
+in float height;
+in vec3 normal;
+in vec3 position;
+
+out vec4 out_Color;
+
+uniform int mode;
+uniform float worldSize;
+
+const vec3 color_low = vec3(1,0,0);
+const vec3 color_high = vec3(0.5,0.5,0);
+const float height_min = -5.f;
+const float height_max = 5.f;
+vec3 light_pos = vec3(worldSize/2, worldSize/2, worldSize);
+
+void main(void){
+    if(mode == 0)
+    {
+        float h = clamp(height, height_min, height_max);
+        h -= height_min;
+        h /= (height_max - height_min);
+
+        vec3 lightDir = normalize(light_pos - position);
+        float diff = max(dot(normal, lightDir), 0.0);
+        vec3 diffuse = vec3(diff);
+
+        if(abs(normal.z) < 0.0001)
+            out_Color = vec4(diffuse * vec3(0.5, 0.5, 0.5), 1);
+        else
+            out_Color = vec4(diffuse * mix(color_low, color_high, h), 1);
+    }
+    else
+        out_Color = vec4(g_color, 0.5);
+}
Index: framspy/gui/visual/shaders/WorldGeometryShader.glsl
===================================================================
--- framspy/gui/visual/shaders/WorldGeometryShader.glsl	(revision 1198)
+++ framspy/gui/visual/shaders/WorldGeometryShader.glsl	(revision 1198)
@@ -0,0 +1,49 @@
+#version 330
+
+layout(triangles) in;
+in vec3 v_color[];
+
+layout (triangle_strip, max_vertices = 3) out;
+out vec3 g_color;
+out float height;
+out vec3 normal;
+out vec3 position;
+
+uniform mat4 projectionMatrix;
+uniform mat4 viewMatrix;
+uniform int mode;
+uniform float worldSize;
+uniform float waterLevel;
+
+vec3 GetNormal()
+{
+    vec3 a = vec3(gl_in[0].gl_Position) - vec3(gl_in[1].gl_Position);
+    vec3 b = vec3(gl_in[2].gl_Position) - vec3(gl_in[1].gl_Position);
+    return normalize(cross(a, b));
+}
+
+void main(void) {
+    for(int i = 0; i < 3; i++)
+    {
+        if(mode == 0)
+        {
+            gl_Position = projectionMatrix * viewMatrix * gl_in[i].gl_Position;
+            height = gl_in[i].gl_Position.z;
+            position = gl_in[i].gl_Position.xyz;
+        }
+        else
+        {
+            mat4 transformation = mat4(1);
+            transformation[0][0] = worldSize;
+            transformation[1][1] = worldSize;
+            transformation[3][2] = waterLevel;
+            gl_Position = projectionMatrix * viewMatrix  * transformation * gl_in[i].gl_Position;
+            position = vec3(transformation * gl_in[i].gl_Position);
+            height = 0;
+        }
+        g_color = v_color[0];
+        normal = -GetNormal().xyz;
+        EmitVertex();
+    }
+    EndPrimitive();
+}
Index: framspy/gui/visual/shaders/WorldVertexShader.glsl
===================================================================
--- framspy/gui/visual/shaders/WorldVertexShader.glsl	(revision 1198)
+++ framspy/gui/visual/shaders/WorldVertexShader.glsl	(revision 1198)
@@ -0,0 +1,33 @@
+#version 330
+
+layout(location = 0) in vec3 position;
+layout(location = 1) in vec3 color;
+
+out vec3 v_color;
+
+uniform mat4 projectionMatrix;
+uniform mat4 viewMatrix;
+uniform int mode;
+uniform float worldSize;
+uniform float waterLevel;
+
+void main(void) {
+	gl_Position = vec4(position, 1.0);
+	v_color = color;
+
+	/*if(mode == 0)
+	{
+		gl_Position = projectionMatrix * viewMatrix * vec4(position, 1.0);
+		height = position.y;
+	}
+	else
+	{
+		mat4 transformation = mat4(1);
+		transformation[0][0] = worldSize;
+		transformation[2][2] = worldSize;
+		transformation[3][1] = waterLevel;
+		gl_Position = projectionMatrix * viewMatrix  * transformation * vec4(position, 1.0);
+		height = 0;
+	}
+	v_color = color;*/
+}
Index: framspy/gui/visual/shaders/fragmentShader.glsl
===================================================================
--- framspy/gui/visual/shaders/fragmentShader.glsl	(revision 1198)
+++ framspy/gui/visual/shaders/fragmentShader.glsl	(revision 1198)
@@ -0,0 +1,19 @@
+#version 330
+
+in vec2 pass_textureCoordinates;
+
+out vec4 out_Color;
+
+uniform sampler2D modelTexture;
+uniform vec3 color;
+uniform float textureOn;
+
+void main(void){
+	vec4 textureColor = texture(modelTexture, pass_textureCoordinates);
+	if(textureColor.a < 0.5){
+		discard;
+	}
+	
+
+	out_Color = textureOn * textureColor + (1.0 - textureOn) * vec4(color, 1);
+}
Index: framspy/gui/visual/shaders/vertexShader.glsl
===================================================================
--- framspy/gui/visual/shaders/vertexShader.glsl	(revision 1198)
+++ framspy/gui/visual/shaders/vertexShader.glsl	(revision 1198)
@@ -0,0 +1,18 @@
+#version 330
+
+in vec3 position;
+in vec2 textureCoordinates;
+in vec3 normal;
+
+out vec2 pass_textureCoordinates;
+
+uniform mat4 transformationMatrix;
+uniform mat4 projectionMatrix;
+uniform mat4 viewMatrix;
+
+void main(void){
+	vec4 worldPosition = transformationMatrix * vec4(position,1.0);
+	vec4 positionRelativeToCam = viewMatrix * worldPosition;
+	gl_Position = projectionMatrix * positionRelativeToCam;
+	pass_textureCoordinates = textureCoordinates;
+}
Index: framspy/gui/visual/skyboxRenderer.py
===================================================================
--- framspy/gui/visual/skyboxRenderer.py	(revision 1198)
+++ framspy/gui/visual/skyboxRenderer.py	(revision 1198)
@@ -0,0 +1,90 @@
+from gui.visual.loader import Loader
+from gui.visual.camera import Camera
+from gui.visual.skyboxShader import SkyboxShader
+import glm
+import OpenGL.GL as gl
+
+class SkyboxRenderer:
+    def __init__(self, loader: Loader, projectionMatrix) -> None:
+        self.shader = SkyboxShader()
+        vertices, text = self.createCube()
+        self.skycube = loader.loadToVao3(vertices, text, int(len(vertices)/3))
+        self.texture = loader.loadCubemap([
+            "gui/res/img/field3.png",
+            "gui/res/img/field5.png",
+            "gui/res/img/field4.png",
+            "gui/res/img/field2.png",
+            "gui/res/img/field1.png",
+            "gui/res/img/field6.png",
+        ])
+        self.shader.start()
+        self.loadProjectionMatrix(projectionMatrix)
+        self.shader.stop()
+
+    def loadProjectionMatrix(self, projectionMatrix):
+        self.shader.loadProjectionMatrix(projectionMatrix)
+
+    def render(self, camera: Camera):
+        gl.glDepthFunc(gl.GL_LEQUAL)
+        self.shader.start()
+        self.shader.loadViewMatrix(camera)
+        gl.glBindVertexArray(self.skycube.vaoID)
+        gl.glEnableVertexAttribArray(0)
+        gl.glEnableVertexAttribArray(1)
+        self.bindTextures()
+        gl.glDrawArrays(gl.GL_TRIANGLES, 0, self.skycube.vertexCount)
+        gl.glDisableVertexAttribArray(0)
+        gl.glDisableVertexAttribArray(1)
+        gl.glBindVertexArray(0)
+        self.shader.stop()
+        gl.glDepthFunc(gl.GL_LESS)
+
+    def bindTextures(self):
+        gl.glActiveTexture(gl.GL_TEXTURE0)
+        gl.glBindTexture(gl.GL_TEXTURE_CUBE_MAP, self.texture)
+
+    def createCube(self):
+        skyboxVertices = [       
+        -1.0,  -1.0, 1.0,
+        -1.0, -1.0, -1.0,
+        1.0, -1.0, -1.0,
+        1.0, -1.0, -1.0,
+        1.0,  -1.0, 1.0,
+        -1.0,  -1.0, 1.0,
+
+        -1.0, 1.0,  -1.0,
+        -1.0, -1.0, -1.0,
+        -1.0,  -1.0, 1.0,
+        -1.0,  -1.0, 1.0,
+        -1.0,  1.0,  1.0,
+        -1.0, 1.0,  -1.0,
+
+        1.0, -1.0, -1.0,
+        1.0, 1.0,  -1.0,
+        1.0,  1.0,  1.0,
+        1.0,  1.0,  1.0,
+        1.0, -1.0, 1.0,
+        1.0, -1.0, -1.0,
+
+        -1.0, 1.0,  -1.0,
+        -1.0,  1.0,  1.0,
+        1.0,  1.0,  1.0,
+        1.0,  1.0,  1.0,
+        1.0, 1.0,  -1.0,
+        -1.0, 1.0,  -1.0,
+
+        -1.0,  -1.0, 1.0,
+        1.0,  -1.0, 1.0,
+        1.0,  1.0,  1.0,
+        1.0,  1.0,  1.0,
+        -1.0,  1.0,  1.0,
+        -1.0,  -1.0, 1.0,
+
+        -1.0, -1.0, -1.0,
+        -1.0, 1.0,  -1.0,
+        1.0, -1.0, -1.0,
+        1.0, -1.0, -1.0,
+        -1.0, 1.0,  -1.0,
+        1.0, 1.0,  -1.0]
+
+        return skyboxVertices, skyboxVertices
Index: framspy/gui/visual/skyboxShader.py
===================================================================
--- framspy/gui/visual/skyboxShader.py	(revision 1198)
+++ framspy/gui/visual/skyboxShader.py	(revision 1198)
@@ -0,0 +1,30 @@
+from gui.visual.shaderProgram import ShaderProgram
+import OpenGL.GL as gl
+import glm
+
+class SkyboxShader(ShaderProgram):
+    def __init__(self) -> None:
+        super().__init__("visual/shaders/SkyboxVertexShader.glsl", "visual/shaders/SkyboxFragmentShader.glsl")
+        self.bindAttributes()
+        gl.glLinkProgram(self.programID)
+        gl.glValidateProgram(self.programID)
+        self.getAllUniformLocations()
+
+    def loadProjectionMatrix(self, matrix):
+        self.loadMatrix4(self.location_projectionMatrix, matrix)
+
+    def loadViewMatrix(self, camera):
+        viewMatrix = glm.lookAt(camera.position, camera.player.position, glm.vec3(0, 0, 1))
+        viewMatrix[3][0] = 0
+        viewMatrix[3][1] = 0
+        viewMatrix[3][2] = 0
+        self.loadMatrix4(self.location_viewMatrix, viewMatrix)
+
+    def bindAttributes(self):
+        self.bindAttribute(0, "position")
+        self.bindAttribute(1, "textureCoordinates")
+
+    def getAllUniformLocations(self):
+        self.location_projectionMatrix = self.getUniformLocation("projectionMatrix")
+        self.location_viewMatrix = self.getUniformLocation("viewMatrix")
+        location_modelTexture = self.getUniformLocation("modelTexture")
Index: framspy/gui/visual/staticShader.py
===================================================================
--- framspy/gui/visual/staticShader.py	(revision 1198)
+++ framspy/gui/visual/staticShader.py	(revision 1198)
@@ -0,0 +1,47 @@
+from gui.visual.shaderProgram import ShaderProgram
+from gui.visual.camera import Camera
+import OpenGL.GL as gl
+import glm
+
+class StaticShader(ShaderProgram):
+    def __init__(self) -> None:
+        super().__init__("visual/shaders/vertexShader.glsl", "visual/shaders/fragmentShader.glsl")
+
+        self.location_transformationMatrix = 0
+        self.location_projectionMatrix = 0
+        self.location_viewMatrix = 0
+        self.location_modelTexture = 0
+
+        self.bindAttributes()
+        gl.glLinkProgram(self.programID)
+        gl.glValidateProgram(self.programID)
+        self.getAllUniformLocations()
+
+    def loadProjectionMatrix(self, projection):
+        self.loadMatrix4(self.location_projectionMatrix, projection)
+
+    def loadViewMatrix(self, camera: Camera):
+        viewMatrix = glm.lookAt(camera.position, camera.player.position, glm.vec3(0, 0, 1))
+        self.loadMatrix4(self.location_viewMatrix, viewMatrix)
+
+    def loadTransformationMatrix(self, matrix):
+        self.loadMatrix4(self.location_transformationMatrix, matrix)
+
+    def loadColor(self, color: glm.vec3):
+        self.loadVector3(self.loaction_color, color)
+
+    def loadTextureOn(self, textureOn: bool):
+        self.loadFloat(self.location_textureOn, 1.0 if textureOn else 0.0)
+
+    def bindAttributes(self):
+        self.bindAttribute(0, "position")
+        self.bindAttribute(1, "textureCoordinates")
+        self.bindAttribute(2, "normal")
+
+    def getAllUniformLocations(self):
+        self.location_transformationMatrix = self.getUniformLocation("transformationMatrix")
+        self.location_projectionMatrix = self.getUniformLocation("projectionMatrix")
+        self.location_viewMatrix = self.getUniformLocation("viewMatrix")
+        self.location_modelTexture = self.getUniformLocation("modelTexture")
+        self.loaction_color = self.getUniformLocation("color")
+        self.location_textureOn = self.getUniformLocation("textureOn")
Index: framspy/gui/visual/worldRenderer.py
===================================================================
--- framspy/gui/visual/worldRenderer.py	(revision 1198)
+++ framspy/gui/visual/worldRenderer.py	(revision 1198)
@@ -0,0 +1,220 @@
+from gui.visual.loader import Loader
+from gui.visual.worldShader import WorldShader
+from gui.visual.staticShader import StaticShader
+from gui.visual.camera import Camera
+from gui.visual.objLoader import OBJLoader
+import OpenGL.GL as gl
+from typing import Tuple, List
+import numpy as np
+import re
+import glm
+
+class WorldRenderer:
+    WORLD_Z_OFFSET = -0.2
+
+    def __init__(self, loader: Loader, projectionMatrix, staticShader: StaticShader) -> None:
+        self.shader = WorldShader()
+        self._createWater(loader)
+        self.staticShader: StaticShader = staticShader
+
+        self.fence = OBJLoader.loadOBJ("gui/res/obj/fence-element.obj", "gui/res/img/wood.png", loader)
+        self.teleport = OBJLoader.loadOBJ("gui/res/obj/teleport.obj", "gui/res/img/teleportglow.png", loader)
+
+        self.boundary: List[Tuple[glm.vec3, float]] = []
+        self.worldType = 0
+        self.worldSize = 20
+        self.worldBoundaries = 0
+        self.z_offset = 0
+
+        self.shader.start()
+        self.loadProjectionMatrix(projectionMatrix)
+        self.shader.stop()
+
+    def render(self, camera: Camera):
+        if hasattr(self, "plane"):
+            self.shader.start()
+            self.shader.loadViewMatrix(camera)
+            self.shader.loadMode(0)
+            gl.glEnable(gl.GL_BLEND)
+            gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
+
+            #render world plane
+            gl.glBindVertexArray(self.plane.vaoID)
+            gl.glEnableVertexAttribArray(0)
+            gl.glDrawArrays(gl.GL_TRIANGLES, 0, int(self.plane.vertexCount))
+            gl.glDisableVertexAttribArray(0)
+            gl.glBindVertexArray(0)
+
+            #render water plane
+            gl.glBindVertexArray(self.water.vaoID)
+            gl.glEnableVertexAttribArray(0)
+            gl.glEnableVertexAttribArray(1)
+            self.shader.loadMode(1)
+            gl.glDrawArrays(gl.GL_TRIANGLES, 0, int(self.water.vertexCount))
+            gl.glDisableVertexAttribArray(0)
+            gl.glBindVertexArray(0)
+            gl.glBindVertexArray(1)
+
+            gl.glDisable(gl.GL_BLEND)
+
+            #render bounds
+            self.renderBounds()
+            
+            self.shader.stop()
+
+    def loadProjectionMatrix(self, projectionMatrix):
+        self.shader.loadProjectionMatrix(projectionMatrix)
+
+    def reloadWorld(self, loader: Loader, worldType: int, simType: int, worldSize: float, worldMap: str, worldBoundaries: int, worldWaterLevel: float):
+        self.worldType = worldType
+        self.worldSize = worldSize
+        self.worldBoundaries = worldBoundaries
+
+        if simType == 0:
+            self.z_offset = self.WORLD_Z_OFFSET
+        else:
+            self.z_offset = 0
+
+        v = self._loadQuads(worldMap)
+        self.plane = loader.loadToVao1(v, len(v) / 3)
+        self.shader.start()
+        self.shader.loadWorldSize(worldSize)
+        self.shader.loadWaterLevel(worldWaterLevel + self.z_offset)
+        self.shader.stop()
+
+        if worldBoundaries == 1:
+            self._addFence()
+        elif worldBoundaries == 2:
+            self._addTeleport()
+
+    def _createBlock(self, p1: Tuple[float, float, float], p2: Tuple[float, float, float], 
+                            p3: Tuple[float, float, float], p4: Tuple[float, float, float]) -> List[float]:
+        vertices = []
+        vertices.append(p1[0])
+        vertices.append(p1[1])
+        vertices.append(p1[2])
+
+        vertices.append(p2[0])
+        vertices.append(p2[1])
+        vertices.append(p2[2])
+
+        vertices.append(p3[0])
+        vertices.append(p3[1])
+        vertices.append(p3[2])
+
+        vertices.append(p1[0])
+        vertices.append(p1[1])
+        vertices.append(p1[2])
+
+        vertices.append(p3[0])
+        vertices.append(p3[1])
+        vertices.append(p3[2])
+
+        vertices.append(p4[0])
+        vertices.append(p4[1])
+        vertices.append(p4[2])
+
+        return vertices
+
+    def _createColorsForBlock(self, color: Tuple[float, float, float]) -> List[float]:
+        return [color[0], color[1], color[2]] * 6
+
+    def _createWater(self, loader: Loader):
+        vertices = self._createBlock((0,0,-0.01), (1,0,-0.01), (1,1,-0.01), (0,1,-0.01))
+        colors = self._createColorsForBlock((0,0,1))
+
+        self.water = loader.loadToVao2(vertices, colors, len(vertices) / 3)
+
+    def _loadQuads(self, a: str):
+        points = []
+        indices = []
+        lines = a.splitlines()
+        self.primitive = None
+
+        for line in lines:
+            if line[0] == 'v':
+                v = line.split()
+                points.append((float(v[1]), float(v[2]), float(v[3]) + self.z_offset))
+            elif line[0] == 'f':
+                f = line.split()
+                if len(f) == 4:
+                    indices.append(int(f[1]))
+                    indices.append(int(f[2]))
+                    indices.append(int(f[3]))
+                elif len(f) == 5:
+                    indices.append(int(f[1]))
+                    indices.append(int(f[2]))
+                    indices.append(int(f[3]))
+
+                    indices.append(int(f[1]))
+                    indices.append(int(f[3]))
+                    indices.append(int(f[4]))
+
+        vertices = []
+        for i in indices:
+            vertices.append(points[i-1][0])
+            vertices.append(points[i-1][1])
+            vertices.append(points[i-1][2])
+
+        return vertices
+
+    def renderBounds(self):
+        if self.worldBoundaries == 1:
+            self.staticShader.start()
+            gl.glBindVertexArray(self.fence.rawModel.vaoID)
+            gl.glEnableVertexAttribArray(0)
+            gl.glEnableVertexAttribArray(1)
+            gl.glActiveTexture(gl.GL_TEXTURE0)
+            gl.glBindTexture(gl.GL_TEXTURE_2D, self.fence.texture.id)
+            self.staticShader.loadTextureOn(True)
+
+            for b in self.boundary:
+                matrix = glm.mat4(1)
+                matrix = glm.translate(matrix, b[0])
+                matrix = glm.rotate(matrix, 3.14/2, glm.vec3(1,0,0))
+                matrix = glm.rotate(matrix, b[1], glm.vec3(0,1,0))
+                self.staticShader.loadTransformationMatrix(matrix)
+                gl.glDrawArrays(gl.GL_TRIANGLES, 0, int(self.fence.rawModel.vertexCount))
+
+            self.staticShader.stop()
+        elif self.worldBoundaries == 2:
+            self.staticShader.start()
+            gl.glBindVertexArray(self.teleport.rawModel.vaoID)
+            gl.glEnableVertexAttribArray(0)
+            gl.glEnableVertexAttribArray(1)
+            gl.glActiveTexture(gl.GL_TEXTURE0)
+            gl.glBindTexture(gl.GL_TEXTURE_2D, self.teleport.texture.id)
+
+            for b in self.boundary:
+                matrix = glm.mat4(1)
+                matrix = glm.translate(matrix, b[0])
+                matrix = glm.rotate(matrix, 3.14/2, glm.vec3(1,0,0))
+                matrix = glm.rotate(matrix, b[1], glm.vec3(0,1,0))
+                self.staticShader.loadTransformationMatrix(matrix)
+                gl.glDrawArrays(gl.GL_TRIANGLES, 0, int(self.teleport.rawModel.vertexCount))
+
+            self.staticShader.stop()
+
+        gl.glDisableVertexAttribArray(0)
+        gl.glDisableVertexAttribArray(1)
+        gl.glBindVertexArray(0)
+
+    def _addFence(self):
+        i = 0
+        self.boundary.clear()
+        while i < self.worldSize / 2:
+            self.boundary.append((glm.vec3(i * 2, 0, self.z_offset), 0))
+            self.boundary.append((glm.vec3(i * 2, self.worldSize, self.z_offset), 0))
+            self.boundary.append((glm.vec3(0, i * 2, self.z_offset), glm.radians(90)))
+            self.boundary.append((glm.vec3(self.worldSize, i * 2, self.z_offset), glm.radians(90)))
+            i += 1
+
+    def _addTeleport(self):
+        i = 0
+        self.boundary.clear()
+        while i < self.worldSize / 3:
+            self.boundary.append((glm.vec3(i * 3, 0, self.z_offset), 0))
+            self.boundary.append((glm.vec3(i * 3, self.worldSize, self.z_offset), 0))
+            self.boundary.append((glm.vec3(0, i * 3, self.z_offset), glm.radians(-90)))
+            self.boundary.append((glm.vec3(self.worldSize, i * 3, self.z_offset), glm.radians(-90)))
+            i += 1
Index: framspy/gui/visual/worldShader.py
===================================================================
--- framspy/gui/visual/worldShader.py	(revision 1198)
+++ framspy/gui/visual/worldShader.py	(revision 1198)
@@ -0,0 +1,38 @@
+from gui.visual.shaderProgram import ShaderProgram
+import OpenGL.GL as gl
+import glm
+
+class WorldShader(ShaderProgram):
+    def __init__(self) -> None:
+        super().__init__("visual/shaders/WorldVertexShader.glsl", "visual/shaders/WorldFragmentShader.glsl", "visual/shaders/WorldGeometryShader.glsl")
+        self.bindAttributes()
+        gl.glLinkProgram(self.programID)
+        gl.glValidateProgram(self.programID)
+        self.getAllUniformLocations()
+
+    def loadProjectionMatrix(self, matrix):
+        self.loadMatrix4(self.location_projectionMatrix, matrix)
+
+    def loadViewMatrix(self, camera):
+        viewMatrix = glm.lookAt(camera.position, camera.player.position, glm.vec3(0, 0, 1))
+        self.loadMatrix4(self.location_viewMatrix, viewMatrix)
+
+    def loadMode(self, mode: int):
+        self.loadInt(self.location_mode, mode)
+
+    def loadWorldSize(self, size: float):
+        self.loadFloat(self.location_worldSize, size)
+
+    def loadWaterLevel(self, level: float):
+        self.loadFloat(self.location_waterLevel, level)
+
+    def bindAttributes(self):
+        self.bindAttribute(0, "position")
+        self.bindAttribute(1, "color")
+
+    def getAllUniformLocations(self):
+        self.location_projectionMatrix = self.getUniformLocation("projectionMatrix")
+        self.location_viewMatrix = self.getUniformLocation("viewMatrix")
+        self.location_mode = self.getUniformLocation("mode")
+        self.location_worldSize = self.getUniformLocation("worldSize")
+        self.location_waterLevel = self.getUniformLocation("waterLevel")
Index: framspy/gui/widgets/ConsoleWindow.py
===================================================================
--- framspy/gui/widgets/ConsoleWindow.py	(revision 1198)
+++ framspy/gui/widgets/ConsoleWindow.py	(revision 1198)
@@ -0,0 +1,99 @@
+import tkinter as tk
+from gui.widgets.ScrolledText import ScrolledText
+from gui.framsutils.FramsSocket import FramsSocket
+from typing import Tuple, Callable
+
+class ConsoleWindow(tk.Toplevel):
+    def __init__(self, master, frams: FramsSocket) -> None:
+        super().__init__(master)
+
+        self.frams: FramsSocket = frams
+        self.frams.registerMessageEventCallback(self.messageCallbackEvent)
+
+        self.protocol("WM_DELETE_WINDOW", self._dismiss)
+        self.title("Console")
+
+        self.frame_main = tk.Frame(master=self)
+
+        self.console = ScrolledText(self.frame_main, height=10, state="disabled")
+        self.console.grid(row=0, column=0, columnspan=2, sticky="NSEW")
+
+        self.input = tk.Entry(self.frame_main)
+        self.input.bind("<Return>", self.buttonSendCommand)
+        self.input.grid(row=1, column=0, sticky="NSEW")
+
+        self.sendButton = tk.Button(self.frame_main, text="Send", command=self.buttonSendCommand)
+        self.sendButton.grid(row=1, column=1, sticky="SE")
+
+        self.frame_main.rowconfigure(0, weight=1)
+        self.frame_main.rowconfigure(1, weight=0)
+
+        self.frame_main.columnconfigure(0, weight=1)
+        self.frame_main.columnconfigure(1, weight=0)
+
+        self.frame_main.grid(row=0, column=0, sticky="NSEW")
+        self.rowconfigure(0, weight=1)
+        self.columnconfigure(0, weight=1)
+
+        def parseGeometryString(geometry: str) -> Tuple[int, int, int, int]:
+            widthxrest = geometry.split('x')
+            heightxy = widthxrest[1].split('+')
+            maxwidth = int(widthxrest[0])
+            maxheight = int(heightxy[0])
+            x = int(heightxy[1])
+            y = int(heightxy[2])
+            return maxwidth, maxheight, x, y
+
+        self.attributes("-alpha", 0)
+        self.update_idletasks()
+
+        width, height, x, y = parseGeometryString(self.winfo_geometry())
+
+        self.state('zoomed')
+        self.update()
+        rootx = self.winfo_rootx()
+
+        maxWidth, maxHeight, x, y = parseGeometryString(self.winfo_geometry())
+
+        self.state("normal")
+        self.update()
+
+        x = (maxWidth - width) / 2
+        y = (maxHeight - height) / 2
+
+        self.geometry("+%d+%d" % (int(rootx + x), int(y)))
+        self.attributes("-alpha", 1)
+        self.update()
+        self.maxsize(maxWidth, maxHeight)
+
+    def buttonSendCommand(self, event=None):
+        request = self.input.get().strip()
+        if len(request) > 0:
+            self.sendButton.config(state=tk.DISABLED)
+            self.input.delete(0, tk.END)
+
+            response = self.frams.sendRequest(request)
+
+            self.console.config(state=tk.NORMAL)
+            self.console.insert(tk.END, ">>> {}\n".format(request))
+            for r in response:
+                self.console.insert(tk.END, r)
+            self.console.see(tk.END)
+            self.console.config(state=tk.DISABLED)
+
+            self.sendButton.config(state=tk.NORMAL)
+
+    def _dismiss(self):
+        self.frams.registerMessageEventCallback(None)
+        self.destroy()
+
+    def messageCallbackEvent(self, block, header):
+        response = self.frams._infoParser(block)
+
+        self.console.config(state=tk.NORMAL)
+        self.console.insert(tk.END, ">>> {}\n".format(response[0].p["func"]))
+        for r in str(response[0].p["message"]):
+            self.console.insert(tk.END, r)
+        self.console.insert(tk.END, "\n")
+        self.console.see(tk.END)
+        self.console.config(state=tk.DISABLED)
Index: framspy/gui/widgets/ScrolledText.py
===================================================================
--- framspy/gui/widgets/ScrolledText.py	(revision 1198)
+++ framspy/gui/widgets/ScrolledText.py	(revision 1198)
@@ -0,0 +1,27 @@
+from tkinter import Frame, Text, Scrollbar, Pack, Grid, Place
+from tkinter.constants import RIGHT, LEFT, Y, BOTH
+
+#source: https://github.com/python/cpython/blob/main/Lib/tkinter/scrolledtext.py
+
+class ScrolledText(Text):
+    def __init__(self, master=None, **kw):
+        self.frame = Frame(master)
+        self.vbar = Scrollbar(self.frame)
+        self.vbar.pack(side=RIGHT, fill=Y)
+
+        kw.update({'yscrollcommand': self.vbar.set})
+        Text.__init__(self, self.frame, **kw)
+        self.pack(side=LEFT, fill=BOTH, expand=True)
+        self.vbar['command'] = self.yview
+
+        # Copy geometry methods of self.frame without overriding Text methods -- hack!
+        text_meths = vars(Text).keys()
+        methods = vars(Pack).keys() | vars(Grid).keys() | vars(Place).keys()
+        methods = methods.difference(text_meths)
+
+        for m in methods:
+            if m[0] != '_' and m != 'config' and m != 'configure':
+                setattr(self, m, getattr(self.frame, m))
+
+    def __str__(self):
+        return str(self.frame)
Index: framspy/gui/widgets/ToolTip.py
===================================================================
--- framspy/gui/widgets/ToolTip.py	(revision 1198)
+++ framspy/gui/widgets/ToolTip.py	(revision 1198)
@@ -0,0 +1,38 @@
+import tkinter as tk
+
+class ToolTip(object):
+
+    def __init__(self, widget):
+        self.widget = widget
+        self.tipwindow = None
+        self.id = None
+        self.x = self.y = 0
+
+    def showtip(self, text, event):
+        "Display text in a tooltip window"
+        self.text = text
+        if self.tipwindow or not self.text:
+            return
+        
+        x = event.x_root + 5
+        y = event.y_root + 5
+        self.tipwindow = tw = tk.Toplevel(self.widget)
+        tw.wm_overrideredirect(1)
+        tw.wm_geometry("+%d+%d" % (x, y))
+        label = tk.Label(tw, text=self.text, justify=tk.LEFT, background="#ffffe0", relief=tk.SOLID, borderwidth=1, font=("tahoma", "8", "normal"))
+        label.pack(ipadx=1)
+
+    def hidetip(self):
+        tw = self.tipwindow
+        self.tipwindow = None
+        if tw:
+            tw.destroy()
+
+def CreateToolTip(widget, text):
+    toolTip = ToolTip(widget)
+    def enter(event):
+        toolTip.showtip(text, event)
+    def leave(event):
+        toolTip.hidetip()
+    widget.bind('<Enter>', enter)
+    widget.bind('<Leave>', leave)
Index: framspy/gui/widgets/dialogBox.py
===================================================================
--- framspy/gui/widgets/dialogBox.py	(revision 1198)
+++ framspy/gui/widgets/dialogBox.py	(revision 1198)
@@ -0,0 +1,220 @@
+import tkinter as tk
+import tkinter.ttk as ttk
+from tkinter import simpledialog, filedialog
+
+class SliderDialogBox(simpledialog._QueryDialog):
+    def __init__(self, *args, **kw):
+        if "show" in kw:
+            self.__show = kw["show"]
+            del kw["show"]
+        else:
+            self.__show = None
+        if "min" in kw:
+            self.__min = kw["min"]
+            del kw["min"]
+        else:
+            self.__min = 0
+        if "max" in kw:
+            self.__max = kw["max"]
+            del kw["max"]
+        else:
+            self.__max = 100
+        if "interval" in kw:
+            self.__interval = kw["interval"]
+            del kw["interval"]
+        else:
+            self.__interval = 1
+        if "init_val" in kw:
+            self.__init_val = kw["init_val"]
+            del kw["init_val"]
+        else:
+            self.__init_val = None
+        if "hint" in kw:
+            self.hint = kw["hint"]
+            del kw["hint"]
+        else:
+            self.hint = None
+
+        simpledialog._QueryDialog.__init__(self, *args, **kw)
+
+    def body(self, master):
+        self.entry = tk.Scale(master=master, from_=self.__min, to=self.__max, orient=tk.HORIZONTAL, resolution=self.__interval)
+        if self.__init_val:
+            self.entry.set(self.__init_val)
+        if self.hint:
+            hint = tk.Label(master=master, text=self.hint)
+            hint.grid(row=0, padx=5, sticky="WE")
+        self.entry.grid(row=1, padx=5, sticky="WE")
+        if self.__show is not None:
+            self.entry.configure(show=self.__show)
+        return self.entry
+
+    def getresult(self):
+        return self.entry.get()
+
+class ComboboxDialogBox(simpledialog._QueryDialog):
+    def __init__(self, *args, **kw):
+        if "show" in kw:
+            self.__show = kw["show"]
+            del kw["show"]
+        else:
+            self.__show = None
+        if "values" in kw:
+            self.__values = kw["values"]
+            del kw["values"]
+        else:
+            self.__ = None
+        if "init_val" in kw:
+            self.__init_val = kw["init_val"]
+            del kw["init_val"]
+        else:
+            self.__init_val = None
+        if "hint" in kw:
+            self.hint = kw["hint"]
+            del kw["hint"]
+        else:
+            self.hint = None
+
+        simpledialog._QueryDialog.__init__(self, *args, **kw)
+
+    def body(self, master):
+        self.entry = ttk.Combobox(master=master, values=self.__values, state="readonly")
+        if self.__init_val:
+            self.entry.set(self.__init_val)
+        if self.hint:
+            hint = tk.Label(master=master, text=self.hint)
+            hint.grid(row=0, padx=5, sticky="WE")
+        self.entry.grid(row=1, padx=5, sticky="WE")
+        if self.__show is not None:
+            self.entry.configure(show=self.__show)
+        return self.entry
+
+    def getresult(self):
+        return self.entry.get()
+
+class DirectoryDialgoBox(simpledialog._QueryDialog):
+    def __init__(self, *args, **kw):
+        if "show" in kw:
+            self.__show = kw["show"]
+            del kw["show"]
+        else:
+            self.__show = None
+        if "mustexist" in kw:
+            self.__mustexist = kw["mustexist"]
+            del kw["mustexist"]
+        else:
+            self.__mustexist = None
+        if "init_val" in kw:
+            self.__init_val = kw["init_val"]
+            del kw["init_val"]
+        else:
+            self.__init_val = None
+
+        self.path = ""
+
+        simpledialog._QueryDialog.__init__(self, *args, **kw)
+
+    def body(self, master):
+        self._pathField = tk.Entry(master, width=50)
+        self._pathButton = tk.Button(master, text="Browse", command=lambda:self.__askDirectory())
+        
+        self._pathField.grid(row=1, column=1, padx=5, sticky="WE")
+        self._pathButton.grid(row=1, column=2, sticky="WE")
+
+        return self._pathField
+
+    def __askDirectory(self):
+        self.path = filedialog.askdirectory(initialdir=self.__init_val, mustexist=self.__mustexist)
+        self._pathField.delete(0, tk.END)
+        self._pathField.insert(0, self.path)
+
+    def getresult(self):
+        return self._pathField.get().strip()
+
+class FileOpenDialogBox(simpledialog._QueryDialog):
+    def __init__(self, *args, **kw):
+        if "show" in kw:
+            self.__show = kw["show"]
+            del kw["show"]
+        else:
+            self.__show = None
+        if "mustexist" in kw:
+            self.__mustexist = kw["mustexist"]
+            del kw["mustexist"]
+        else:
+            self.__mustexist = None
+        if "init_val" in kw:
+            self.__init_val = kw["init_val"]
+            del kw["init_val"]
+        else:
+            self.__init_val = None
+        if "filetypes" in kw:
+            self.__filetypes = kw["filetypes"]
+            del kw["filetypes"]
+        else:
+            self.__filetypes = None
+
+        self.path = ""
+
+        simpledialog._QueryDialog.__init__(self, *args, **kw)
+
+    def body(self, master):
+        self._pathField = tk.Entry(master, width=50)
+        self._pathButton = tk.Button(master, text="Browse", command=lambda:self.__askFile())
+        
+        self._pathField.grid(row=1, column=1, padx=5, sticky="WE")
+        self._pathButton.grid(row=1, column=2, sticky="WE")
+
+        return self._pathField
+
+    def __askFile(self):
+        self.path = filedialog.askopenfilename(initialdir=self.__init_val, mustexist=self.__mustexist, filetypes=self.__filetypes)
+        self._pathField.delete(0, tk.END)
+        self._pathField.insert(0, self.path)
+
+    def getresult(self):
+        return self._pathField.get().strip()
+
+class FileSaveDialogBox(simpledialog._QueryDialog):
+    def __init__(self, *args, **kw):
+        if "show" in kw:
+            self.__show = kw["show"]
+            del kw["show"]
+        else:
+            self.__show = None
+        if "mustexist" in kw:
+            self.__mustexist = kw["mustexist"]
+            del kw["mustexist"]
+        else:
+            self.__mustexist = None
+        if "init_val" in kw:
+            self.__init_val = kw["init_val"]
+            del kw["init_val"]
+        else:
+            self.__init_val = None
+        if "filetypes" in kw:
+            self.__filetypes = kw["filetypes"]
+            del kw["filetypes"]
+        else:
+            self.__filetypes = None
+
+        self.path = ""
+
+        simpledialog._QueryDialog.__init__(self, *args, **kw)
+
+    def body(self, master):
+        self._pathField = tk.Entry(master, width=50)
+        self._pathButton = tk.Button(master, text="Browse", command=lambda:self.__askFile())
+        
+        self._pathField.grid(row=1, column=0, padx=5, sticky="WE")
+        self._pathButton.grid(row=1, column=1, sticky="WE")
+
+        return self._pathField
+
+    def __askFile(self):
+        self.path = filedialog.asksaveasfilename(initialdir=self.__init_val, mustexist=self.__mustexist, filetypes=self.__filetypes)
+        self._pathField.delete(0, tk.END)
+        self._pathField.insert(0, self.path)
+
+    def getresult(self):
+        return self._pathField.get().strip()
Index: framspy/gui/widgets/glFrame.py
===================================================================
--- framspy/gui/widgets/glFrame.py	(revision 1198)
+++ framspy/gui/widgets/glFrame.py	(revision 1198)
@@ -0,0 +1,190 @@
+from xmlrpc.client import Boolean
+from gui.visual.entity import Entity
+import OpenGL.GL as gl
+from pyopengltk import OpenGLFrame
+from gui.visual.camera import Camera
+from gui.visual.player import Player
+from gui.visual.masterRenderer import MasterRenderer
+from gui.visual.loader import Loader
+from gui.visual.objLoader import OBJLoader
+from gui.visual.player import Player
+from gui.visual.mouse import Mouse
+from gui.framsutils.creature import Creature
+from typing import List, Callable
+import threading
+import time as timetime
+from gui.utils import Swap
+from typing import Dict
+from gui.visual.modelData import ModelData
+
+class AppOgl(OpenGLFrame):
+    class GLContext(object):
+        pass
+
+    def __init__(self, *args, **kw):
+        super().__init__(*args, **kw)
+        self.REFRESH_RATE = 0.1
+        self.player = Player(10, 10, 0, 0, 0, 0)
+        self.camera = Camera(self.player)
+        self.entities = []
+        self.frams_readCreatures: Callable[[None], List[Creature]] = None
+        self.frams_readCreatures_color: Boolean = False
+        self.onDraw: Callable[[], None] = lambda: None
+        self.commThread = threading.Thread(target=self.commThread, daemon=True)
+        self.commThreadLock = threading.Lock()
+        self.read_creature_semaphore = threading.Semaphore()
+        self.creatures: List[Creature] = []
+        self.swap_buffer = Swap(init=[])
+        self.swap_buffer.update([])
+        self.swap_buffer.update([])
+        self.commThread.start()
+
+    #show tri-colored axis instead of balls
+    SHOW_AXIS = False
+
+    NEURON_SCALE = 1.5
+    OBJ_PATH = "gui/res/obj/"
+    TEX_PATH = "gui/res/img/"
+
+    NEURONS = ["|", "@", "G", "Gpart", "T", "S"]
+
+    def neuronname_in_filename(self, neuronname):
+        if neuronname == "|":
+            return "bend"
+        elif neuronname == "@":
+            return "rot"
+        return neuronname
+
+    def initgl(self):
+        gl.glClearColor(0, 0, 0.4, 0)
+        
+        self.loader = Loader()
+
+        if self.SHOW_AXIS:
+            self.bodyPartModel = OBJLoader.loadOBJ(self.OBJ_PATH+"axis.obj", self.TEX_PATH+"axis.png", self.loader)
+        else:
+            self.bodyPartModel = OBJLoader.loadOBJ(self.OBJ_PATH+"part.obj", self.TEX_PATH+"green2.png", self.loader)
+        self.jointModel = OBJLoader.loadOBJ(self.OBJ_PATH+"stick5red3.obj", self.TEX_PATH+"green2.png", self.loader)
+        self.neurons: Dict[str, ModelData] = dict()
+        for n in self.NEURONS:
+            self.neurons[n] = OBJLoader.loadOBJ(self.OBJ_PATH+"neuro-"+self.neuronname_in_filename(n)+".obj", self.TEX_PATH+"amber.jpg", self.loader)
+
+        self.renderer = MasterRenderer(self.loader)
+
+        gl.glEnable(gl.GL_DEPTH_TEST)
+        gl.glClearDepth(1.0)
+        gl.glEnable(gl.GL_TEXTURE_2D)
+
+        self.initalized = True
+        self._resize(self.width, self.height)
+
+    def init_context(self):
+        self.context = self.GLContext()
+
+    def redraw(self):
+        #call onDraw callback  only for frams library 
+        self.onDraw()
+        self.entities.clear()
+        self.creatures: List[Creature] = self.swap_buffer.get()
+        #for every creature, prepare entities for every parts, joints and neurons
+        for creature in self.creatures:
+            parts = creature.mechParts
+            joints = creature.joints
+            neurons = creature.neurons
+
+            '''
+            TODO: solid shapes should be handled here
+            obj models should be loaded in initgl method and used depending on the shape type (probably need to add new lists to Creature class).
+            Position should already be loaded in the part object, as well as rotation matrix, you need to add scale in FramsSocket and FramsLib.
+            You can add scale in each axis in the Entity constructor or as independent properties of the Entity class.
+            scaleX, scaleY, scaleZ have higher priority than scale only if all three (x, y, z) are set.
+            '''
+            for i, part in enumerate(parts):
+                rotMatrix = creature.partRotationMatrix(i)
+                partEntity = Entity(part[0], part[1], part[2], self.bodyPartModel, scale=0.7, rotMatrix=rotMatrix)
+                if i < len(creature.colorsPart):
+                    partEntity.color = creature.colorsPart[i]
+                self.entities.append(partEntity)
+
+            for i in range(len(joints)):
+                translation = creature.jointTranslation(i)
+                rotMatrix = creature.jointRotation(i)
+                jointEntity = Entity(translation[0], translation[1], translation[2], self.jointModel, rotMatrix=rotMatrix)
+                jointEntity.scaleX = creature.jointLength(i)
+                jointEntity.scaleY = 1
+                jointEntity.scaleZ = 1
+                if i < len(creature.colorsJoint):
+                    partEntity.color = creature.colorsJoint[i]
+                self.entities.append(jointEntity)
+
+            for i, neuron in enumerate(neurons):
+                if creature.styleNeuron[i] in self.neurons:
+                    style = creature.styleNeuron[i]
+                    if neuron[0] >= 0:
+                        translation = creature.partTranslation(neuron[0])
+                        rotMatrix = creature.partRotationMatrix(neuron[0])
+                        rotMatrix = rotMatrix * creature.neuronRelativeOrient[i]
+                        neuronEntity = Entity(translation[0], translation[1], translation[2], self.neurons[style], scale=self.NEURON_SCALE, rotMatrix=rotMatrix)
+                    else:
+                        translation = creature.jointTranslation(neuron[1])
+                        if style == 'G':
+                            rotMatrix = creature.jointRotation(neuron[1])
+                        else:
+                            rotMatrix = creature.partRotationMatrix(joints[neuron[1]][0])
+                        rotMatrix = rotMatrix * creature.neuronRelativeOrient[i]
+                        neuronEntity = Entity(translation[0], translation[1], translation[2], self.neurons[style], scale=self.NEURON_SCALE, rotMatrix=rotMatrix)
+                    self.entities.append(neuronEntity)
+
+        self.camera.move()
+        self.renderer._isTexturesOn = not self.frams_readCreatures_color
+        self.renderer.renderScene(self.player, self.entities, self.camera)
+
+    #reload all creatures from frams with self.REFRESH_RATE interval
+    def commThread(self):
+        while True:
+            if self.frams_readCreatures:
+                with self.read_creature_semaphore:
+                    c = self.frams_readCreatures(self.frams_readCreatures_color)
+                self.swap_buffer.update(c)
+            timetime.sleep(self.REFRESH_RATE)
+
+    def onResize(self, event):
+        width, height = event.width, event.height
+        self._resize(width, height)
+
+    def _resize(self, width, height):
+        self.width = width
+        self.height = height
+        try:
+            self.initalized
+            gl.glViewport(0, 0, width, height)
+            self.renderer.resize(width, height)
+        except AttributeError:
+            pass
+
+    def onMouseMotion(self, event):
+        Mouse.setXY(event.x, event.y)
+
+    def onScroll(self, event):
+        if event.delta > 0:
+            Mouse.incrementDWheel(1)
+        else:
+            Mouse.incrementDWheel(-1)
+
+    def onMouseClick(self, event):
+        if event.num == 1:
+            Mouse.setButton(Mouse.MOUSE_LEFT)
+        elif event.num == 3:
+            Mouse.setButton(Mouse.MOUSE_RIGHT)
+
+    def onMouseRelease(self, event):
+        Mouse.setButton(Mouse.MOUSE_NONE)
+
+    def onMouseEnter(self, event):
+        Mouse.setXY(event.x, event.y)
+
+    def reloadWorld(self, worldType: int, simType: int, worldSize: float, worldMap: str, worldBoundaries: int, worldWaterLevel: float) -> None:
+        self.renderer.reloadWorld(self.loader, worldType, simType, worldSize, worldMap, worldBoundaries, worldWaterLevel)
+        self.camera.setPosition(worldSize / 2.0, worldSize / 2.0)
+        self.camera.setZoomScale(worldSize / 20)
+        self.camera.setZoom(worldSize)
Index: framspy/gui/widgets/importWindow.py
===================================================================
--- framspy/gui/widgets/importWindow.py	(revision 1198)
+++ framspy/gui/widgets/importWindow.py	(revision 1198)
@@ -0,0 +1,62 @@
+import tkinter as tk
+import tkinter.ttk as ttk
+from tkinter import simpledialog
+from typing import Tuple
+from framsfiles import reader
+from collections import Counter
+
+class ImportWindow(simpledialog._QueryDialog):
+    def __init__(self, *args, **kw):
+        if "filename" in kw:
+            self.__filename = kw["filename"]
+            del kw["filename"]
+        else:
+            self.__filename = None
+
+        self.vec = [tk.IntVar(), tk.IntVar(), tk.IntVar(), tk.IntVar(), tk.IntVar(), tk.IntVar()]
+
+        simpledialog._QueryDialog.__init__(self, "Select import options for '{}'".format(self.__filename.split("/")[-1]), "options", *args, **kw)
+
+    def body(self, master):
+        objectCounters = self.__countOccurrences(self.__filename)
+
+        text = "Import genotypes - {} genotypes found".format(objectCounters[0])
+        cb = ttk.Checkbutton(master, text=text, variable=self.vec[0], state="enabled" if objectCounters[0] > 0 else "disabled")
+        cb.pack(anchor=tk.W)
+
+        text = "Import simulator parameters without changing expdef - {} parameter sets found".format(objectCounters[1])
+        cb = ttk.Checkbutton(master, text=text, variable=self.vec[1], state="enabled" if objectCounters[1] > 0 else "disabled")
+        cb.pack(anchor=tk.W)
+
+        text = "Import genepool settings - {} gene pools found".format(objectCounters[2])
+        cb = ttk.Checkbutton(master, text=text, variable=self.vec[2], state="enabled" if objectCounters[2] > 0 else "disabled")
+        cb.pack(anchor=tk.W)
+
+        text = "Import population settings - {} populations found".format(objectCounters[3])
+        cb = ttk.Checkbutton(master, text=text, variable=self.vec[3], state="enabled" if objectCounters[3] > 0 else "disabled")
+        cb.pack(anchor=tk.W)
+
+        cb = ttk.Checkbutton(master, text="Create new groups for imported genepools and populations", variable=self.vec[4])
+        tk.Label(master, text="").pack(anchor=tk.W)
+
+        cb.pack(anchor=tk.W)
+
+        cb = ttk.Checkbutton(master, text="Allow switching to a different expdef while importing simulator parameters", variable=self.vec[5])
+        cb.pack(anchor=tk.W)
+        
+        return cb
+
+    def getresult(self):
+        options = 0
+        for i, v in enumerate(self.vec):
+            options += v.get() * (2 ** (i + 1))
+        return options
+
+    def __countOccurrences(self, filename) -> Tuple[int, int, int, int]:
+        objects = ["org", "sim_params", "GenePool", "Population"]
+
+        file = reader.load(filename)
+        counts = Counter(p["_classname"] for p in file)
+        result = [counts[o] for o in objects]
+
+        return tuple(result)
Index: framspy/gui/widgets/listGenePoolWindow.py
===================================================================
--- framspy/gui/widgets/listGenePoolWindow.py	(revision 1198)
+++ framspy/gui/widgets/listGenePoolWindow.py	(revision 1198)
@@ -0,0 +1,176 @@
+import tkinter as tk
+import tkinter.ttk as ttk
+from typing import Dict
+from gui.widgets.propertyWindow import PropertyWindow
+from gui.framsutils.FramsInterface import FramsInterface
+from functools import partial
+import operator
+from threading import Semaphore
+
+class ListGenePoolWindow(tk.Toplevel):
+	TREEVIEW_WIDTH = 600
+
+	class FrameData:
+		def __init__(self, frame, treeview, prev, index, text) -> None:
+			self.frame = frame
+			self.treeview = treeview
+			self.prev = prev
+			self.index = index
+			self.text = text
+
+	fields = ["name", "genotype", "fit2", "numjoints", "index", "uid"]
+	headers = ["Name", "Genotype", "Final fitness"]
+	headers_width = [0.3, 0.5, 0.2]
+
+	def __init__(self, parent, posX, posY, height, refreshRate: int, framsSemaphore: Semaphore, frams: FramsInterface):
+		super().__init__(parent)
+		self.parent = parent
+		#self.transient(parent)
+		self.protocol("WM_DELETE_WINDOW", self._dismiss)
+		self.title("Gene pools")
+
+		self.frams: FramsInterface = None
+		self.semaphore: Semaphore = framsSemaphore
+		self.frams = frams
+
+		self.update()
+		self.height = height - self.winfo_rooty() + self.winfo_y()
+		self.posX = posX
+
+		self.refreshRate = refreshRate
+
+		#MAIN SECTION
+		frame_main = tk.Frame(master=self)
+		self.notebook_notebook = ttk.Notebook(frame_main)
+		self.frames: Dict[str, self.FrameData] = {} #group name as a key
+
+		#WINDOW
+		self.notebook_notebook.grid(row=0, column=0, sticky="NSEW")
+		frame_main.columnconfigure(0, weight=1, minsize=0)
+		frame_main.rowconfigure(0, weight=1, minsize=0)
+		frame_main.grid(row=0, column=0, sticky="NSEW")
+
+		self.columnconfigure(0, weight=1, minsize=self.TREEVIEW_WIDTH)
+		self.rowconfigure(0, weight=1, minsize=self.height)
+
+		self.update()
+		width = self.winfo_width()
+		self.geometry("%dx%d+%d+%d" % (width, self.height, posX, posY))
+		#self.update()
+		self.refresh = False
+		self.dismissed = False
+		self.opened = True
+
+	def clearList(self):
+		self.update()
+
+	def refreshGenePools(self):
+		if not self.dismissed and self.frams:
+			self.semaphore.acquire()
+			genePoolsDict = self.frams.readGenePools(self.fields)
+			groupIndexes = self.frams.readGenePoolsGroups()
+			self.semaphore.release()
+
+			groups = set(genePoolsDict.keys())
+			groups_prev = set(self.frames.keys())
+			groups_remove = groups_prev - groups
+			for g in groups_remove:
+				f = self.frames.pop(g)
+				self.notebook_notebook.forget(f.frame)
+			groups_add = groups - groups_prev
+			for k, v in (sorted(groupIndexes.items(), key=lambda item: item[1])):
+				if k in groups_add:
+					self._addGroup(k, v)
+
+			for (group, genePools) in genePoolsDict.items():
+				if genePools or (not genePools and not self.frames[group].prev):
+					prevGP = set(self.frames[group].treeview.get_children())
+					gpUids = {x["uid"] for x in genePools}
+
+					gpToRemove = prevGP - gpUids
+					gpToAdd = gpUids - prevGP
+					gpToUpdate = gpUids.intersection(prevGP)
+
+					#first remove unwanted items to reduce time of further operations
+					for gp in gpToRemove:
+						self.frames[group].treeview.delete(gp)
+					
+					#update remaining items
+					for gp in gpToUpdate:
+						rec = next(x for x in genePools if x["uid"] == gp)
+						self.frames[group].treeview.item(gp, text="{}-{}".format(rec["group"], rec["uid"]), values=self._makeValue(rec))
+					
+					#add new items 
+					for gp in gpToAdd:
+						rec = next(x for x in genePools if x["uid"] == gp)
+						self.frames[group].treeview.insert(parent="", index="end", iid=rec["uid"], text="{}-{}".format(rec["group"], rec["uid"]), values=self._makeValue(rec))
+				self.frames[group].prev = len(genePools)
+
+			if self.refresh:
+				self.after(self.refreshRate, self.refreshGenePools)
+
+	def _makeValue(self, rec):
+		ret = []
+		for i, label in enumerate(self.fields):
+			if isinstance(rec[label], str):
+				ret.append(rec[label].replace('\n', "\\n"))
+			else:
+				ret.append(rec[label])
+			if i == len(self.headers) - 1:
+				break
+
+		return tuple(ret)
+
+	def _treeviewSelect(self, event):
+		itemId = event.widget.focus()
+		if itemId and event.widget.identify_row(event.y) == itemId:
+			item = event.widget.item(itemId)
+			if item:
+				group, uid = item["text"].split('-')
+				info = partial(self.frams.readGenotypeDetails, group, uid)
+				update = partial(self.frams.writeGenotypeDetail, uid)
+				gw = PropertyWindow(self, "Genotype data", self.posX, info, update, self.frams.getError, self.frams, self.semaphore)
+
+	def _dismiss(self):
+		self.dismissed = True
+		self.grab_release()
+		self.destroy()
+		self.opened = False
+
+	def _addGroup(self, g, index):
+		masterFrame = tk.Frame(master=self)
+		masterFrame.columnconfigure(0, weight=1)
+		masterFrame.columnconfigure(1, weight=0)
+		masterFrame.rowconfigure(0, weight=1)
+		
+		treeview = ttk.Treeview(master=masterFrame, columns=tuple(self.headers), selectmode="browse")
+		scrollbar_treeview = ttk.Scrollbar(master=masterFrame, orient=tk.VERTICAL, command=treeview.yview)
+		treeview.configure(yscrollcommand=scrollbar_treeview.set)
+		treeview['show'] = 'headings'
+		for h, w in zip(self.headers, self.headers_width):
+			treeview.heading(h, text=h)
+			treeview.column(h, stretch=tk.YES, width=int(w*self.TREEVIEW_WIDTH))
+		treeview.bind("<Double-1>", self._treeviewSelect) #on double left click callback 
+
+		treeview.grid(row=0, column=0, sticky="NSEW")
+		scrollbar_treeview.grid(row=0, column=1, sticky="NSEW")
+		masterFrame.grid(row=0, column=0, sticky="NSEW")
+
+		self.frames[g] = self.FrameData(masterFrame, treeview, 0, index, g)
+
+		selected = self.notebook_notebook.select()
+
+		self.notebook_notebook.add(self.frames[g].frame, text=g)
+		
+		#hide all tabs
+		for f in self.frames.values():
+			self.notebook_notebook.forget(f.frame)
+
+		#show all tabs sorted
+		for frame in (sorted(self.frames.values(), key=operator.attrgetter("index"))):
+			self.notebook_notebook.add(frame.frame, text=frame.text)
+
+		if selected:
+			self.notebook_notebook.select(selected)
+		else:
+			self.notebook_notebook.select(self.frames[g].frame)
Index: framspy/gui/widgets/listPopulationsWindow.py
===================================================================
--- framspy/gui/widgets/listPopulationsWindow.py	(revision 1198)
+++ framspy/gui/widgets/listPopulationsWindow.py	(revision 1198)
@@ -0,0 +1,176 @@
+import tkinter as tk
+import tkinter.ttk as ttk
+from typing import Dict
+from gui.widgets.propertyWindow import PropertyWindow
+from gui.framsutils.FramsInterface import FramsInterface
+from functools import partial
+import operator
+from threading import Semaphore
+
+class ListPopulationsWindow(tk.Toplevel):
+	TREEVIEW_WIDTH = 600
+
+	class FrameData:
+		def __init__(self, frame, treeview, prev, index, text) -> None:
+			self.frame = frame
+			self.treeview = treeview
+			self.prev = prev
+			self.index = index
+			self.text = text
+
+	fields = ["name", "genotype", "energy", "lifespan", "uid"]
+	headers = ["Name", "Genotype", "Energy", "Lifespan"]
+	headers_width = [0.3, 0.5, 0.1, 0.1]
+
+	def __init__(self, parent, posX, posY, height, refreshRate: int, framsSemaphore: Semaphore, frams: FramsInterface):
+		super().__init__(parent)
+		self.parent = parent
+		#self.transient(parent)
+		self.protocol("WM_DELETE_WINDOW", self._dismiss)
+		self.title("Populations")
+
+		self.frams: FramsInterface = None
+		self.semaphore: Semaphore = framsSemaphore
+		self.frams = frams
+
+		self.update()
+		self.height = height - self.winfo_rooty() + self.winfo_y()
+		self.posX = posX
+
+		self.refreshRate = refreshRate
+
+		#MAIN SECTION
+		frame_main = tk.Frame(master=self)
+		self.notebook_notebook = ttk.Notebook(frame_main)
+		self.frames: Dict[str, self.FrameData] = {}
+
+		#WINDOW
+		self.notebook_notebook.grid(row=0, column=0, sticky="NSEW")
+		frame_main.columnconfigure(0, weight=1, minsize=0)
+		frame_main.rowconfigure(0, weight=1, minsize=0)
+		frame_main.grid(row=0, column=0, sticky="NSEW")
+
+		self.columnconfigure(0, weight=1, minsize=self.TREEVIEW_WIDTH)
+		self.rowconfigure(0, weight=1, minsize=self.height)
+
+		self.update()
+		width = self.winfo_width()
+		self.geometry("%dx%d+%d+%d" % (width, self.height, posX, posY))
+		#self.update()
+		self.refresh = False
+		self.dismissed = False
+		self.opened = True
+
+	def clearList(self):
+		self.update()
+
+	def refreshPopulations(self):
+		if not self.dismissed and self.frams:
+			self.semaphore.acquire()
+			populationsDict = self.frams.readPopulations(self.fields)
+			groupIndexes = self.frams.readPopulationsGroups()
+			self.semaphore.release()
+
+			groups = set(populationsDict.keys())
+			groups_prev = set(self.frames.keys())
+			groups_remove = groups_prev - groups
+			for g in groups_remove:
+				f = self.frames.pop(g)
+				self.notebook_notebook.forget(f.frame)
+			groups_add = groups - groups_prev
+			for k, v in (sorted(groupIndexes.items(), key=lambda item: item[1])):
+				if k in groups_add:
+					self._addGroup(k, v)
+
+			for (group, populations) in populationsDict.items():
+				if populations or (not populations and not self.frames[group].prev):
+					prevPop = set(self.frames[group].treeview.get_children())
+					popUids = {x["uid"] for x in populations}
+
+					popToRemove = prevPop - popUids
+					popToAdd = popUids - prevPop
+					popToUpdate = popUids.intersection(prevPop)
+
+					#first remove unwanted items to reduce time of further operations
+					for pop in popToRemove:
+						self.frames[group].treeview.delete(pop)
+					
+					#update remaining items
+					for pop in popToUpdate:
+						rec = next(x for x in populations if x["uid"] == pop)
+						self.frames[group].treeview.item(pop, text="{}-{}".format(rec["group"], rec["uid"]), values=self._makeValue(rec))
+					
+					#add new items 
+					for pop in popToAdd:
+						rec = next(x for x in populations if x["uid"] == pop)
+						self.frames[group].treeview.insert(parent="", index="end", iid=rec["uid"], text="{}-{}".format(rec["group"], rec["uid"]), values=self._makeValue(rec))
+				self.frames[group].prev = len(populations)
+
+			if self.refresh:
+				self.after(self.refreshRate + 1, self.refreshPopulations)
+
+	def _makeValue(self, rec):
+		ret = []
+		for i, label in enumerate(self.fields):
+			if isinstance(rec[label], str):
+				ret.append(rec[label].replace('\n', "\\n"))
+			else:
+				ret.append(rec[label])
+			if i == len(self.headers) - 1:
+				break
+
+		return tuple(ret)
+
+	def _treeviewSelect(self, event):
+		itemId = event.widget.focus()
+		if itemId and event.widget.identify_row(event.y) == itemId:
+			item = event.widget.item(itemId)
+			if item:
+				group, uid = item["text"].split('-')
+				info = partial(self.frams.readCreatureDetails, group, uid)
+				update = partial(self.frams.writeCreatureDetail, uid)
+				cw = PropertyWindow(self, "Creature data", self.posX, info, update, self.frams.getError, self.frams, self.semaphore)
+
+	def _dismiss(self):
+		self.dismissed = True
+		self.grab_release()
+		self.destroy()
+		self.opened = False
+
+	def _addGroup(self, g, index):
+		masterFrame = tk.Frame(master=self)
+		masterFrame.columnconfigure(0, weight=1)
+		masterFrame.columnconfigure(1, weight=0)
+		masterFrame.rowconfigure(0, weight=1)
+		
+		treeview = ttk.Treeview(master=masterFrame, columns=tuple(self.headers), selectmode="browse")
+		scrollbar_treeview = ttk.Scrollbar(master=masterFrame, orient=tk.VERTICAL, command=treeview.yview)
+		treeview.configure(yscrollcommand=scrollbar_treeview.set)
+		treeview['show'] = 'headings'
+		for h, w in zip(self.headers, self.headers_width):
+			treeview.heading(h, text=h)
+			treeview.column(h, stretch=tk.YES, width=int(w*self.TREEVIEW_WIDTH))
+		treeview.bind("<Double-1>", self._treeviewSelect) #on double left click callback 
+
+		treeview.grid(row=0, column=0, sticky="NSEW")
+		scrollbar_treeview.grid(row=0, column=1, sticky="NSEW")
+		masterFrame.grid(row=0, column=0, sticky="NSEW")
+
+		self.frames[g] = self.FrameData(masterFrame, treeview, 0, index, g)
+
+		selected = self.notebook_notebook.select()
+
+		self.notebook_notebook.add(self.frames[g].frame, text=g)
+
+		#hide all tabs
+		for f in self.frames.values():
+			self.notebook_notebook.forget(f.frame)
+
+		#show all tabs sorted
+		for frame in (sorted(self.frames.values(), key=operator.attrgetter("index"))):
+			self.notebook_notebook.add(frame.frame, text=frame.text)
+
+		if selected:
+			self.notebook_notebook.select(selected)
+		else:
+			self.notebook_notebook.select(self.frames[g].frame)
Index: framspy/gui/widgets/mainPage.py
===================================================================
--- framspy/gui/widgets/mainPage.py	(revision 1198)
+++ framspy/gui/widgets/mainPage.py	(revision 1198)
@@ -0,0 +1,586 @@
+import tkinter as tk
+import tkinter.ttk as ttk
+from tkinter import StringVar, simpledialog, messagebox
+from gui.widgets.glFrame import AppOgl
+from typing import List
+from functools import partial
+from gui.framsutils.FramsInterface import TreeNode, InterfaceType
+from gui.widgets.mainTreeView import TreeView
+from gui.widgets.listGenePoolWindow import ListGenePoolWindow
+from gui.widgets.listPopulationsWindow import ListPopulationsWindow
+from gui.widgets.dialogBox import DirectoryDialgoBox, FileOpenDialogBox, FileSaveDialogBox
+from gui.widgets.ConsoleWindow import ConsoleWindow
+from gui.widgets.importWindow import ImportWindow
+from gui.widgets.propertyWindow import PropertyWindow
+from gui.libInterface import LibInterface
+from gui.socketInterface import SocketInterface
+from gui.utils import debounce
+from gui.widgets.ToolTip import CreateToolTip
+from time import perf_counter
+
+class MainPage(tk.Tk):
+    OPENGL_WIDTH = 720
+    OPENGL_HEIGHT = 480
+
+    SIDEBAR_WIDTH = 400
+    CONTROL_HEIGHT = 50
+    OPTIONS_WIDTH = 100
+    STATUSBAR_HEIGHT = 20
+
+    OFFSET_HEIGHT = 60
+
+    OPENGL_ANIMATE_DELAY = 1
+
+    MENU_CONNECT_TO_SERVER = "Connect to server"
+    MENU_CONNECT_TO_LIB = "Connect to library"
+
+    WORKAROUND_TKINTER_FREEZE_BUG = True # There is a bug in tkinter that freezes whole app when dialogs are called too fast, hint: https://stackoverflow.com/questions/40666956/tkinter-hangs-on-rapidly-repeated-dialog
+
+    refresh_rate_dict = {"0.1s": 100, "0.2s": 200, "0.5s": 500, "1s": 1000, "2s": 2000, "5s": 5000, "10s": 10000}
+
+    #paths which can reload world and tree
+    reload_path = ["/Experiment", "/Advanced scripting", "/World", "/User scripts"]
+
+    def __init__(self, parent, networkAddress: str = None, libPath: str = None):
+        super().__init__(parent)
+        self.parent = parent
+        self.protocol("WM_DELETE_WINDOW", self._dismiss)
+        self.title("Framsticks GUI for library/server")
+        self.option_add('*tearOff', tk.FALSE)
+
+        self.listRefreshRate = 1000
+        self.frams = None
+        self.canStep = False    #disable step while drawing 
+
+        #OPENGL FRAME
+        self.frame_opengl = AppOgl(self, width=self.OPENGL_WIDTH, height=self.OPENGL_HEIGHT)
+        self.frame_opengl.animate = self.OPENGL_ANIMATE_DELAY
+        self.frame_opengl.bind("<Configure>", self.frame_opengl.onResize)
+        self.frame_opengl.bind("<Motion>", self.frame_opengl.onMouseMotion)
+        self.frame_opengl.bind("<MouseWheel>", self.frame_opengl.onScroll)
+        self.frame_opengl.bind("<Button>", self.frame_opengl.onMouseClick)
+        self.frame_opengl.bind("<ButtonRelease>", self.frame_opengl.onMouseRelease)
+        self.frame_opengl.bind("<Enter>", self.frame_opengl.onMouseEnter)
+
+        #SIDE FRAME
+        frame_sidebar = tk.Frame(master=self)
+        frame_sidebar.rowconfigure(0, weight=0)
+        frame_sidebar.rowconfigure(1, weight=1)
+        frame_sidebar.columnconfigure(0, weight=1)
+
+        ##CONTROL PANEL
+        frame_control_panel = tk.Frame(master=frame_sidebar, width=self.SIDEBAR_WIDTH, height=self.CONTROL_HEIGHT)
+        frame_control_panel.columnconfigure(0, weight=1, minsize=0)
+        frame_control_panel.columnconfigure(1, weight=1, minsize=0)
+        frame_control_panel.columnconfigure(2, weight=1, minsize=0)
+        frame_control_panel.columnconfigure(3, weight=1, minsize=0)
+        frame_control_panel.rowconfigure(0, weight=1, minsize=0)
+        frame_control_panel.grid_propagate(0)
+
+        frame_control_panel_combobox = tk.Frame(master=frame_control_panel, width=int(self.SIDEBAR_WIDTH/4))
+        frame_control_panel_combobox.rowconfigure(0, weight=1, minsize=0)
+        frame_control_panel_combobox.rowconfigure(1, weight=1, minsize=0)
+        frame_control_panel_combobox.columnconfigure(0, weight=1, minsize=0)
+        frame_control_panel_combobox.grid_propagate(0)
+        self.combobox_control_panel_fps = ttk.Combobox(master=frame_control_panel_combobox, state="readonly")
+        self.combobox_control_panel_fps.bind("<<ComboboxSelected>>", self.FPSCbCallback)
+        self.combobox_control_panel_refresh_rate = ttk.Combobox(master=frame_control_panel_combobox, values=list(self.refresh_rate_dict.keys()), state="readonly")
+        self.combobox_control_panel_refresh_rate.set(next(k for k, v in self.refresh_rate_dict.items() if v == self.listRefreshRate))
+        self.combobox_control_panel_refresh_rate.bind("<<ComboboxSelected>>", self.refreshRateCbCallback)
+        CreateToolTip(self.combobox_control_panel_fps, "Simulation steps to show")
+        CreateToolTip(self.combobox_control_panel_refresh_rate, "Refresh rate of gene pools and populations windows")
+
+        frame_control_panel_buttons = tk.Frame(master=frame_control_panel)
+        frame_control_panel_buttons.columnconfigure(0, weight=1, minsize=0)
+        frame_control_panel_buttons.columnconfigure(1, weight=1, minsize=0)
+        frame_control_panel_buttons.columnconfigure(2, weight=1, minsize=0)
+        frame_control_panel_buttons.rowconfigure(0, weight=1)
+        self.button_control_panel_start = tk.Button(master=frame_control_panel_buttons, text="start", command=self.controlPanelStartCommand)
+        self.button_control_panel_stop = tk.Button(master=frame_control_panel_buttons, text="stop", command=self.controlPanelStopCommand)
+        self.button_control_panel_step = tk.Button(master=frame_control_panel_buttons, text="step", command=self.controlPanelStepCommand)
+        self.button_control_panel_start["state"] = tk.DISABLED
+        self.button_control_panel_stop["state"] = tk.DISABLED
+        self.button_control_panel_step["state"] = tk.DISABLED
+        self.button_control_panel_start.grid(row=0, column=0, sticky="NSEW")
+        self.button_control_panel_stop.grid(row=0, column=1, sticky="NSEW")
+        self.button_control_panel_step.grid(row=0, column=2, sticky="NSEW")
+        self.combobox_control_panel_fps.grid(row=0, column=0, sticky="NSEW")
+        self.combobox_control_panel_refresh_rate.grid(row=1, column=0, sticky="NSEW")
+        frame_control_panel_combobox.grid(row=0, column=0, sticky="NSEW")
+        frame_control_panel_buttons.grid(row=0, column=1, columnspan=3, sticky="NSEW")
+        frame_control_panel.grid(row=0, column=0, sticky="NSEW")
+
+        ##TREEVIEW
+        frame_treeview = tk.Frame(master=frame_sidebar, width=self.SIDEBAR_WIDTH, height=self.OPENGL_HEIGHT - self.CONTROL_HEIGHT)
+        frame_treeview.columnconfigure(0, weight=1)
+        frame_treeview.columnconfigure(1, weight=0)
+        frame_treeview.rowconfigure(0, weight=1)
+        frame_treeview.rowconfigure(1, weight=0)
+
+        self.treeview_treeview = TreeView(master=frame_treeview, iconPath="gui/res/icons/", selectmode="browse")
+        scrollbar_treeview = ttk.Scrollbar(master=frame_treeview, orient=tk.VERTICAL, command=self.treeview_treeview.yview)
+        self.treeview_treeview.configure(yscrollcommand=scrollbar_treeview.set)
+        self.treeview_treeview.bind("<Double-1>", self.onTreeViewDoubleClick)
+        button_treeviewRefresh = tk.Button(master=frame_treeview, text="Refresh", command=self.refreshInfoTreeCommand)
+
+        self.treeview_treeview.grid(row=0, column=0, sticky="NSEW")
+        scrollbar_treeview.grid(row=0, column=1, sticky="NSEW")
+        button_treeviewRefresh.grid(row=1, column=0, sticky="NSEW")
+        frame_treeview.grid(row=1, column=0, sticky="NSEW")
+
+        #STATUS BAR
+        self.motd_text = StringVar("")
+        label_statusbar_motd = tk.Label(self, textvariable=self.motd_text, bd=1, height=1, relief=tk.SUNKEN, anchor=tk.W)
+
+        #MENU BAR
+        menu = tk.Menu(self)
+        self.menu_open = tk.Menu(menu, tearoff=0)
+        self.menu_open.add_command(label=self.MENU_CONNECT_TO_SERVER, command=self.menuConnectServerCommand)
+        self.menu_open.add_command(label=self.MENU_CONNECT_TO_LIB, command=self.menuConnectLibCommand)
+        self.menu_open.add_command(label="Disconnect", command=self.menuDisconnectCommand)
+        self.menu_open.entryconfig("Disconnect", state="disabled")
+        self.menu_open.add_separator()
+        self.menu_open.add_command(label="Exit", command=self.menuExitCommand)
+        menu.add_cascade(label="Main", menu=self.menu_open)
+        self.menu_file = tk.Menu(menu, tearoff=0)
+        self.menu_file.add_command(label="Load", command=self.menuFileLoadCommand, state="disabled")
+        self.menu_file.add_command(label="Import", command=self.menuFileImportCommand, state="disabled")
+        self.menu_file.add_command(label="Save experiment state as...", command=self.menuFileSaveESCommand, state="disabled")
+        self.menu_file.add_command(label="Save genotypes as...", command=self.menuFileSaveGCommand, state="disabled")
+        self.menu_file.add_command(label="Save simulator parameters as...", command=self.menuFileSaveSPCommand, state="disabled")
+        menu.add_cascade(label="File", menu=self.menu_file)
+        self.menu_options = tk.Menu(menu, tearoff=0)
+        self.menu_options.add_command(label="Console", command=self.menuConsoleCommand, state="disabled")
+        self.menu_options.add_command(label="Refresh world", command=self.refreshWorld, state="disabled")
+        self.enableColorsVar = tk.BooleanVar(value=False)
+        self.enableColorsVar.trace_add("write", self.menuEnableColorsCommand)
+        self.menu_options.add_checkbutton(label="Enable colors", onvalue=True, offvalue=False, variable=self.enableColorsVar)
+        self.menu_options.add_command(label="Restore windows", command=self.menuRestoreWindowsCommand)
+        menu.add_cascade(label="Options", menu=self.menu_options)
+        self.config(menu=menu)
+
+        #WINDOW
+        self.columnconfigure(0, weight=1)
+        self.columnconfigure(1, weight=0)
+        self.rowconfigure(0, weight=1)
+        self.rowconfigure(1, weight=0)
+        self.frame_opengl.grid(row=0, column=0, sticky="NSEW")
+        frame_sidebar.grid(row=0, column=1, sticky="NSEW")
+        label_statusbar_motd.grid(row=1, column=0, columnspan=2, sticky="NSEW")
+
+        #ORGANIZE WINDOWS POSITIONS
+        ## need to do some workaround to determine screen width and height
+        self.attributes("-alpha", 0)
+        self.state('zoomed')
+        self.update()
+        maxHeight = self.winfo_rooty() + self.winfo_height()
+        maxWidth = self.winfo_rootx() + self.winfo_width()
+        self.rootx = self.winfo_rootx()
+        self.state("normal")
+        self.update()
+        self.geometry("%dx%d+%d+%d" % (self.OPENGL_WIDTH + self.SIDEBAR_WIDTH, self.OPENGL_HEIGHT, self.rootx, 0))
+        self.attributes("-alpha", 1)
+        self.update()
+        height = self.winfo_rooty() + self.winfo_height() - self.winfo_y()
+
+        self.list_gene_pool_window_height = int((maxHeight - height) / 2)
+        self.list_gene_pool_window_pos_y = height
+        self.list_gene_pool_window = ListGenePoolWindow(self, self.rootx, self.list_gene_pool_window_pos_y, self.list_gene_pool_window_height, self.listRefreshRate, self.frame_opengl.read_creature_semaphore, self.frams)
+        height2 = self.list_gene_pool_window.winfo_rooty() + self.list_gene_pool_window.winfo_height() - self.list_gene_pool_window.winfo_y()
+        self.list_populations_window_height = int((maxHeight - height) / 2)
+        self.list_populations_window_pos_y = height + height2
+        self.list_populations_window = ListPopulationsWindow(self, self.rootx, self.list_populations_window_pos_y, self.list_populations_window_height, self.listRefreshRate, self.frame_opengl.read_creature_semaphore, self.frams)
+
+        self.bind("<FocusIn>", self.on_focus_in)
+        self.bind("<FocusOut>", self.on_focus_out)
+
+        self.fps = 50
+        self.c_steps = 1
+
+        if networkAddress and not libPath:
+            self.menuConnectServerCommand(networkAddress)
+        elif libPath and not networkAddress:
+            self.menuConnectLibCommand(libPath)
+
+        self.sock_adr = "127.0.0.1:9009"
+        self.lib_adr = "D:\\Framsticks50rc25\\"
+
+        self.FPSCbCallback(None)
+
+        #step counter
+        self.nb_frames = 0
+        self.c_time = perf_counter()
+        self.sps = 0
+
+    def _dismiss(self):
+        """dismiss main window, close all connections, release all resources."""
+        if self.frams:
+            self.frams.disconnect()
+            
+        self.destroy()
+
+    def on_focus_in(self, event):
+        """restart rendering on focusing on main window."""
+        self.frame_opengl.animate = self.OPENGL_ANIMATE_DELAY
+        self.frame_opengl.tkExpose(None)
+
+    def on_focus_out(self, event):
+        """stop the rendering when main window lose focus."""
+        self.frame_opengl.animate = 0
+
+    def menuConnectServerCommand(self, adr: str = None):
+        """on "connect to server" button command."""
+        #if connection started from command argument
+        if adr:
+            address = adr
+        #else ask for server address
+        else:
+            address = simpledialog.askstring("Server address", "Address", parent=self, initialvalue=self.sock_adr)
+        if address:
+            ip, port = address.split(":")
+            try:
+                self.frams = SocketInterface()
+                self.frams.registerRunningChangeEventCallback(self.refreshControlPanelButtons)
+                self.frams.registerTreeviewRefreshEventCallback(self.refreshInfoTree)
+                self.frams.connect(ip, int(port)) #try to connect to server
+                self.sock_adr = address
+            except ConnectionError: #if connection cannot be established
+                messagebox.showerror(message="Cannot connect to server")
+                if self.frams:
+                    self.frams.disconnect()
+                self.frams = None
+        
+        #if connected successfully 
+        if self.frams and self.frams.frams.comm.connected:
+            self._connect(address)
+
+    def menuConnectLibCommand(self, path: str = None):
+        """on "connect to library" button command."""
+        #if connection started from command argument
+        if path:
+            address = path
+        #else ask for library path
+        else:
+            address = DirectoryDialgoBox("Framsticks library path", "Path", parent=self, initialvalue=self.lib_adr)
+            address = address.result
+        if address:
+            try:
+                self.frams = LibInterface()
+                self.frame_opengl.onDraw = self.onDraw
+                self.prev_run = False
+                self.frams.connect(address, 0)
+                self.lib_adr = address
+            except ConnectionError:
+                messagebox.showerror(message="Cannot find Framsticks library")
+
+        self._connect(address)
+
+    def _connect(self, address):
+        """set all control's states if connected."""
+        if address and self.frams:
+            #set creatures read callback
+            self.frame_opengl.frams_readCreatures = self.frams.readCreatures
+            #prepare populations and gene pools windows
+            self.list_populations_window.frams = self.frams
+            self.list_gene_pool_window.frams = self.frams
+            self.list_populations_window.refresh = True
+            self.list_gene_pool_window.refresh = True
+            self.list_populations_window.refreshPopulations()
+            self.list_gene_pool_window.refreshGenePools()
+            #enable control buttons
+            self.button_control_panel_start["state"] = tk.NORMAL
+            self.button_control_panel_stop["state"] = tk.NORMAL
+            self.button_control_panel_step["state"] = tk.NORMAL
+            self.refreshInfoTree()
+            self.motd_text.set(self.frams.getMotd())
+            #setup all menus
+            self.menu_open.entryconfig("Disconnect", state="normal")
+            self.menu_open.entryconfig(self.MENU_CONNECT_TO_SERVER, state="disabled")
+            self.menu_open.entryconfig(self.MENU_CONNECT_TO_LIB, state="disabled")
+            self.menu_file.entryconfig("Load", state="normal")
+            self.menu_file.entryconfig("Import", state="normal")
+            self.menu_file.entryconfig("Save experiment state as...", state="normal")
+            self.menu_file.entryconfig("Save genotypes as...", state="normal")
+            self.menu_file.entryconfig("Save simulator parameters as...", state="normal")
+            self.menu_options.entryconfig("Refresh world", state="normal")
+
+            if self.frams.interfaceType == InterfaceType.SOCKET:
+                self.menu_options.entryconfig("Console", state="normal")
+            else:
+                self.menu_options.entryconfig("Console", state="disabled")
+
+            self.fps_values = self.frams.getFPSDefinitions()
+            def mapper(fps, step):
+                return "Every{}".format(", {} fps".format(fps) if fps > 0 else "") if step == 1 else "1:{}".format(step)
+            self.fps_mapvalues = [mapper(fps, step) for fps, step in self.fps_values]
+            init_val = mapper(self.fps, self.c_steps)
+            self.combobox_control_panel_fps['values'] = self.fps_mapvalues
+            self.combobox_control_panel_fps.set(init_val)
+
+            self.FPSCbCallback(None)
+
+            self.refreshWorld()
+            self.canStep = True     #enable step while drawing 
+
+    def menuDisconnectCommand(self):
+        """set all control's states if disconnected."""
+        if self.frams:
+            self.canStep = False
+            self.frams.disconnect()
+            self.button_control_panel_start["state"] = tk.DISABLED
+            self.button_control_panel_stop["state"] = tk.DISABLED
+            self.button_control_panel_step["state"] = tk.DISABLED
+            self.list_populations_window.refresh = False
+            self.list_gene_pool_window.refresh = False  
+            self.list_populations_window.clearList()
+            self.list_gene_pool_window.clearList()
+            self.frame_opengl.frams_readCreatures = None
+            self.frame_opengl.onDraw = lambda: None
+            self.refreshInfoTree()
+            self.frams = None
+            self.frame_opengl.swap_buffer.clear()
+            self.motd_text.set("")
+            self.menu_open.entryconfig("Disconnect", state="disabled")
+            self.menu_open.entryconfig(self.MENU_CONNECT_TO_SERVER, state="normal")
+            self.menu_open.entryconfig(self.MENU_CONNECT_TO_LIB, state="normal")
+            self.menu_options.entryconfig("Console", state="disabled")
+            self.menu_file.entryconfig("Load", state="disabled")
+            self.menu_file.entryconfig("Import", state="disabled")
+            self.menu_file.entryconfig("Save experiment state as...", state="disabled")
+            self.menu_file.entryconfig("Save genotypes as...", state="disabled")
+            self.menu_file.entryconfig("Save simulator parameters as...", state="disabled")
+            self.menu_options.entryconfig("Refresh world", state="disabled")
+
+    def menuExitCommand(self):
+        self._dismiss()
+
+    def FPSCbCallback(self, event):
+        """handle fps change."""
+        value = self.combobox_control_panel_fps.get()
+        if value:
+            i = self.fps_mapvalues.index(value)
+            fps, step = self.fps_values[i]
+            self.fps = fps
+            self.c_steps = step
+
+            if fps == -1:
+                self.frame_opengl.animate = self.OPENGL_ANIMATE_DELAY = 1
+                self.frame_opengl.REFRESH_RATE = 0.001 #set to "as fast as possible" because how often redrawing is called is decided by the timer
+            else:
+                self.frame_opengl.animate = self.OPENGL_ANIMATE_DELAY = int(1000 / fps)
+                self.frame_opengl.REFRESH_RATE = 1 / fps
+
+    def refreshRateCbCallback(self, event):
+        """handle change of refresh rate of gene pools and populations windows."""
+        value = self.combobox_control_panel_refresh_rate.get()
+        if value:
+            self.listRefreshRate = self.refresh_rate_dict[value]
+            self.list_gene_pool_window.refreshRate = self.listRefreshRate
+            self.list_populations_window.refreshRate = self.listRefreshRate
+
+    def controlPanelStartCommand(self):
+        if self.frams:
+            self.frams.start()
+
+    def controlPanelStopCommand(self):
+        if self.frams:
+            self.frams.stop()
+            self.motd_text.set("")
+
+    def refreshControlPanelButtons(self, is_running: bool):
+        if is_running:
+            self.button_control_panel_start["state"] = tk.DISABLED
+            self.button_control_panel_stop["state"] = tk.NORMAL
+            self.button_control_panel_step["state"] = tk.DISABLED
+        else:
+            self.button_control_panel_start["state"] = tk.NORMAL
+            self.button_control_panel_stop["state"] = tk.DISABLED
+            self.button_control_panel_step["state"] = tk.NORMAL
+
+    def controlPanelStepCommand(self):
+        """hangle step button."""
+        if self.frams:  #if connected to frams
+            if self.frams.interfaceType == InterfaceType.LIB:
+                self.canStep = False    #disable step while drawing 
+                run = self.frams.getSimulationStatus()
+                self.frams.start()
+                self.frams.step()
+                if not run:
+                    self.frams.stop()
+                self.canStep = True     #enable step while drawing 
+            else:
+                self.canStep = False    #disable step while drawing 
+                self.frams.step()
+                self.canStep = True     #enable step while drawing 
+
+    def refreshInfoTreeCommand(self):
+        self.refreshInfoTree()
+        self.refreshWorld()
+
+    @debounce(1)
+    def refreshInfoTree(self):
+        """refresh info tree
+            debounce decorator prevents refreshing too often."""
+        self.treeview_treeview.delete(*self.treeview_treeview.get_children())
+        if self.frams:
+            tree: List[TreeNode] = self.frams.makeInfoTree()
+            self._recRefreshInfoTree(tree, True)
+
+    def _recRefreshInfoTree(self, node: TreeNode, open: bool = False):
+        #self.treeview_treeview.insert("" if not node.parent else node.parent.node.Id, index="end", iid=node.node.Id, text=node.node.Name)
+        self.treeview_treeview.insert(self._generateInfoTreeParent(node), index="end", iid=self._generateInfoTreeId(node), text=node.node.p["name"], ico=node.node.p["id"], open=open)
+        for child in node.children:
+            self._recRefreshInfoTree(child)
+
+    def _generateInfoTreeParent(self, node: TreeNode):
+        if not node.parent:
+            return ""
+        return self._generateInfoTreeId(node.parent)
+
+    def _generateInfoTreeId(self, node: TreeNode):
+        """generate unique id for info tree using paths and ids."""
+        response = node.node.p["id"]
+        while node.parent:
+            if node.parent:
+                if node.parent.node.p["id"] != '/':
+                    response = node.parent.node.p["id"] + "/" + response
+                else:
+                    response = node.parent.node.p["id"] + response
+                node = node.parent
+        return response
+
+    def onTreeViewDoubleClick(self, event):
+        if self.frams:
+            item = self.treeview_treeview.identify_row(event.y)
+            if item:
+                info = partial(self.frams.readParameterDetails, item)
+                update = partial(self.frams.writeParameterDetail, item)
+
+                if any(item.lower().endswith(x.lower()) for x in self.reload_path):
+                    pw = PropertyWindow(self, item, self.rootx, info, update, self.frams.getError, self.frams, self.frame_opengl.read_creature_semaphore, self.refreshWorld)
+                else:
+                    pw = PropertyWindow(self, item, self.rootx, info, update, self.frams.getError, self.frams, self.frame_opengl.read_creature_semaphore)
+                return 'break'
+
+    def onDraw(self):
+        """on draw callback, only for frams library."""
+        if self.frams:
+            run = self.frams.getSimulationStatus()
+            if run != self.prev_run:    #check if simulation status changed since last onDraw
+                self.refreshControlPanelButtons(run)    #if so, refresh command buttons
+                self.prev_run = run
+            if run:
+                if self.canStep: #if running and can step, perform N steps 
+                    with self.frame_opengl.read_creature_semaphore:
+                        for i in range(self.c_steps):
+                            self.frams.step()
+
+                            #calculate steps per second
+                            c_time = perf_counter()
+                            self.nb_frames += 1
+                            e_time = c_time - self.c_time
+                            if e_time >= 1.0:
+                                self.sps = int(self.nb_frames / e_time)
+                                self.c_time = perf_counter()
+                                self.nb_frames = 0
+                                self.motd_text.set("{} steps/second".format(self.sps))
+            error = self.frams.getError()   #check for errors, not used anymore
+            if error:
+                messagebox.showerror("Framsticks error", error)
+
+    def menuConsoleCommand(self):
+        if self.frams.interfaceType == InterfaceType.SOCKET:
+            ConsoleWindow(self, self.frams.frams)
+        else:
+            self.menu_options.entryconfig("Console", state="disabled")
+
+    def menuFileLoadCommand(self):
+        if self.frams:
+            self.canStep = False
+            path = FileOpenDialogBox("Open File", "Path", parent=self, initialvalue=self.lib_adr, filetypes=[("Framsticks files", "*.expt;*.gen;*.sim"), ("Experiment state", "*.expt"), ("Genotypes", "*.gen"), ("Simulator parameters", "*.sim"), ("All files", "*.*")])
+            path = path.result
+            if path:
+                #acquire read creature semaphore, so render thread could not ask for them, while they are changing
+                with self.frame_opengl.read_creature_semaphore:
+                    self.frams.loadFile(path)
+                self.refreshWorld()
+            self.canStep = True
+
+    def menuFileImportCommand(self):
+        if self.frams:
+            self.canStep = False
+            path = FileOpenDialogBox("Open File", "Path", parent=self, init_val=self.lib_adr, filetypes=[("Framsticks files", "*.expt;*.gen;*.sim"), ("Experiment state", "*.expt"), ("Genotypes", "*.gen"), ("Simulator parameters", "*.sim"), ("All files", "*.*")])
+            path = path.result
+            if path:
+                #acquire read creature semaphore, so render thread could not ask for them, while they are changing
+                with self.frame_opengl.read_creature_semaphore:
+                    def handle_import_options():
+                            options = ImportWindow(parent=self, filename=path)
+                            options = options.result
+                            self.frams.importFile(path, options)
+                    if self.WORKAROUND_TKINTER_FREEZE_BUG:
+                        self.after(1, handle_import_options) 
+                    else:
+                        handle_import_options()
+                    
+            self.canStep = True
+
+    def menuFileSaveESCommand(self):
+        if self.frams:
+            path = FileSaveDialogBox("Save experiment state to:", "Path", parent=self, initialvalue=self.lib_adr, filetypes=[("Experiment state", "*.expt")])
+            path = path.result
+            if path:
+                if path.split(".")[-1] != "expt":
+                    path += ".expt"
+                self.frams.saveFile(path, 1)
+
+    def menuFileSaveGCommand(self):
+        if self.frams:
+            path = FileSaveDialogBox("Save genotypes to:", "Path", parent=self, initialvalue=self.lib_adr, filetypes=[("Genotypes", "*.gen")])
+            path = path.result
+            if path:
+                if path.split(".")[-1] != "gen":
+                    path += ".gen"
+                self.frams.saveFile(path, 2)
+
+    def menuFileSaveSPCommand(self):
+        if self.frams:
+            path = FileSaveDialogBox("Save simulator parameters to:", "Path", parent=self, initialvalue=self.lib_adr, filetypes=[("Simulator parameters", "*.sim")])
+            path = path.result
+            if path:
+                if path.split(".")[-1] != "sim":
+                    path += ".sim"
+                self.frams.saveFile(path, 4)
+
+    def refreshWorld(self):
+        """refresh world parameters and shape."""
+        worldType = self.frams.getWorldType()
+        worldSize = self.frams.getWorldSize()
+        worldWaterLevel = self.frams.getWorldWaterLevel()
+        worldBoundaries = self.frams.getWorldBoundaries()
+        worldMap = self.frams.getWorldMap()
+        simType = self.frams.getSimtype()
+        self.frame_opengl.reloadWorld(worldType, simType, worldSize, worldMap, worldBoundaries, worldWaterLevel)
+
+    def menuEnableColorsCommand(self, *_):
+        v = self.enableColorsVar.get()
+        self.frame_opengl.frams_readCreatures_color = v
+
+    def menuRestoreWindowsCommand(self):
+        self.geometry("%dx%d+%d+%d" % (self.OPENGL_WIDTH + self.SIDEBAR_WIDTH, self.OPENGL_HEIGHT, self.rootx, 0))
+
+        if self.list_gene_pool_window.opened:
+            self.list_gene_pool_window._dismiss()
+        self.list_gene_pool_window = ListGenePoolWindow(self, self.rootx, self.list_gene_pool_window_pos_y, self.list_gene_pool_window_height, self.listRefreshRate, self.frame_opengl.read_creature_semaphore, self.frams)
+        if self.frams:
+            self.list_gene_pool_window.frams = self.frams
+            self.list_gene_pool_window.refresh = True
+            self.list_gene_pool_window.refreshGenePools()
+
+        if self.list_populations_window.opened:
+            self.list_populations_window._dismiss()
+        self.list_populations_window = ListPopulationsWindow(self, self.rootx, self.list_populations_window_pos_y, self.list_populations_window_height, self.listRefreshRate, self.frame_opengl.read_creature_semaphore, self.frams)
+        if self.frams:
+            self.list_populations_window.frams = self.frams
+            self.list_populations_window.refresh = True
+            self.list_populations_window.refreshPopulations()
Index: framspy/gui/widgets/mainTreeView.py
===================================================================
--- framspy/gui/widgets/mainTreeView.py	(revision 1198)
+++ framspy/gui/widgets/mainTreeView.py	(revision 1198)
@@ -0,0 +1,36 @@
+import tkinter as tk
+import tkinter.ttk as ttk
+import glob
+from pathlib import Path
+from PIL import Image, ImageTk
+
+class TreeView(ttk.Treeview):
+    ICON_SIZE = 32
+    def __init__(self, master=None, iconPath="", **kw) -> None:
+        kw["show"] = "tree"
+        super().__init__(master=master, **kw)
+        self.icons = {}
+        self.s = ttk.Style()
+        self.s.configure('Treeview', rowheight=self.ICON_SIZE)
+        self.iconLoader(iconPath)
+
+    def iconLoader(self, path):
+        files = glob.glob(path + "\\*.png")
+        for file in files:
+            icon = Image.open(file)
+            icon = icon.resize((self.ICON_SIZE, self.ICON_SIZE))
+            name = Path(file).name
+            name = name[:name.index('.')]
+            self.icons[name.lower()] = ImageTk.PhotoImage(icon)
+
+    def insert(self, parent, index, iid=None, **kw) -> str:
+        if "ico" in kw:
+            ico = kw["ico"].lower()
+            del kw["ico"]
+        else:
+            ico = None
+
+        if ico in self.icons:
+            img = self.icons[ico]
+            return super().insert(parent, index, iid=iid, image=img, **kw)
+        return super().insert(parent, index, iid=iid, **kw)
Index: framspy/gui/widgets/propertyWindow.py
===================================================================
--- framspy/gui/widgets/propertyWindow.py	(revision 1198)
+++ framspy/gui/widgets/propertyWindow.py	(revision 1198)
@@ -0,0 +1,162 @@
+import tkinter as tk
+import tkinter.ttk as ttk
+import tkinter.messagebox as messagebox
+from typing import Dict, List, Callable, Tuple
+from gui.framsutils.framsProperty import Property, PropertyCallback, propertyToTkinter
+from gui.widgets.ToolTip import CreateToolTip
+from gui.framsutils.FramsInterface import FramsInterface, InterfaceType
+
+'''
+BUG: when you open this window during the creature loading for render (commThread in glFrame) and modify the list of creatures (with buttons) or parts (with editing genotype), it can get stuck on the semaphore or throw an exception when exiting the window and returning to creature loading.
+SOLUTION: open window only when the semaphore is released.
+'''
+class PropertyWindow(tk.Toplevel):
+    def __init__(self, parent, title: str, posX, dataCallback: Callable[[None], List[Property]], propUpdateCallback: Callable[[str, str], None], getError: Callable[[None], str or None], frams: FramsInterface, semaphore = None, onUpdate: Callable[[None], None] = None):
+        super().__init__(parent)
+        self.parent = parent
+        #self.transient(parent)
+        self.protocol("WM_DELETE_WINDOW", self._dismiss)
+        self.title(title)
+
+        self.propUpdateCallback: Callable[[str, str], None] = propUpdateCallback
+        self.dataCallback: Callable[[None], List[Property]] = dataCallback
+        self.getError: Callable([None], str or None) = getError
+        self.onUpdate: Callable([None], None) = onUpdate
+        self.frams: FramsInterface = frams
+
+        #MAIN SECTION
+        frame_main = tk.Frame(master=self)
+        notebook_notebook = ttk.Notebook(frame_main)
+        frames: Dict[str, tk.Frame] = {}
+        frames_idx: Dict[str, int] = {}
+        data = self.dataCallback()
+
+        self.callbacks: List[PropertyCallback] = []
+        self.semaphore = semaphore
+
+        for prop in data:
+            if "group" not in prop.p:
+                prop.p["group"] = "other"
+                
+            if prop.p["group"] not in frames:
+                frames[prop.p["group"]] = tk.Frame(master=frame_main)
+                frames_idx[prop.p["group"]] = 0
+
+                frames[prop.p["group"]].columnconfigure(0, weight=0, minsize=0)
+                frames[prop.p["group"]].columnconfigure(1, weight=1, minsize=0)
+                notebook_notebook.add(frames[prop.p["group"]], text=prop.p["group"])
+
+            widget, callback = propertyToTkinter(prop, frames[prop.p["group"]])
+            if widget:
+                label = tk.Label(master=frames[prop.p["group"]], text=prop.p["name"], anchor='w')
+                if prop.p["type"][0] == 'p':
+                    label.text = ""
+                label.grid(row=frames_idx[prop.p["group"]], column=0, sticky="NSEW")
+                if issubclass(type(widget), tk.Checkbutton):
+                    widget.grid(row=frames_idx[prop.p["group"]], column=1, sticky="NSW")
+                else:
+                    widget.grid(row=frames_idx[prop.p["group"]], column=1, sticky="NSEW")
+                if callback:
+                    self.callbacks.append(callback)
+                if prop.p["help"] != "":
+                    CreateToolTip(label, prop.p["help"])
+                    CreateToolTip(widget, prop.p["help"])
+                if issubclass(type(widget), tk.Text) and prop.p["type"][0] == 's':
+                    frames[prop.p["group"]].rowconfigure(frames_idx[prop.p["group"]], weight=1, minsize=0)
+                else:
+                    frames[prop.p["group"]].rowconfigure(frames_idx[prop.p["group"]], weight=0, minsize=0)
+                frames_idx[prop.p["group"]] += 1
+
+        notebook_notebook.grid(row=0, column=0, sticky="NSEW") #sometimes it freezes here
+        frame_main.columnconfigure(0, weight=1, minsize=0)
+        frame_main.rowconfigure(0, weight=1, minsize=0)
+
+        #CONTROL BUTTONS
+        frame_buttons = tk.Frame(master=frame_main)
+        self.button_refresh = tk.Button(master=frame_buttons, text="Refresh", command=self.refreshButtonCommand)
+        self.button_cancel = tk.Button(master=frame_buttons, text="Cancel", command=self.cancelButtonCommand)
+        self.button_apply = tk.Button(master=frame_buttons, text="Apply", command=self.applyButtonCommand)
+        self.button_ok = tk.Button(master=frame_buttons, text="OK", command=self.okButtonCommand)
+        self.button_refresh.grid(row=0, column=0, sticky="E")
+        self.button_cancel.grid(row=0, column=1, sticky="E")
+        self.button_apply.grid(row=0, column=2, sticky="E")
+        self.button_ok.grid(row=0, column=3, sticky="E")
+
+        frame_buttons.columnconfigure(0, weight=1, minsize=0)
+        frame_buttons.columnconfigure(1, weight=1, minsize=0)
+        frame_buttons.columnconfigure(2, weight=1, minsize=0)
+        frame_buttons.columnconfigure(3, weight=1, minsize=0)
+        frame_buttons.rowconfigure(0, weight=1, minsize=0)
+        frame_buttons.grid(row=1, column=0, sticky="SE")
+        frame_main.rowconfigure(1, weight=0, minsize=0)
+
+        frame_main.grid(row=0, column=0, sticky="NSEW")
+
+        self.columnconfigure(0, weight=1, minsize=0)
+        self.rowconfigure(0, weight=1, minsize=0)
+
+        def parseGeometryString(geometry: str) -> Tuple[int, int, int, int]:
+            widthxrest = geometry.split('x')
+            heightxy = widthxrest[1].split('+')
+            maxwidth = int(widthxrest[0])
+            maxheight = int(heightxy[0])
+            x = int(heightxy[1])
+            y = int(heightxy[2])
+            return maxwidth, maxheight, x, y
+
+        self.attributes("-alpha", 0)
+        self.update_idletasks()
+
+        width, height, x, y = parseGeometryString(self.winfo_geometry())
+
+        self.state('zoomed')
+        self.update()
+        rootx = self.winfo_rootx()
+
+        maxWidth, maxHeight, x, y = parseGeometryString(self.winfo_geometry())
+
+        self.state("normal")
+        self.update()
+
+        x = (maxWidth - width) / 2
+        y = (maxHeight - height) / 2
+
+        self.geometry("+%d+%d" % (int(rootx + x), int(y)))
+        self.attributes("-alpha", 1)
+        self.update()
+        self.maxsize(maxWidth, maxHeight)
+
+        self.grab_set()
+
+    def refreshButtonCommand(self):
+        data = self.dataCallback()
+        for d in data:
+            prop = next((x for x in self.callbacks if d.p["id"] == x.id), None)
+            if prop:
+                prop.updateValue(d)
+
+    def cancelButtonCommand(self):
+        self._dismiss()
+
+    def applyButtonCommand(self):
+        if self.propUpdateCallback:
+            updated = False
+            with self.semaphore:
+                for c in self.callbacks:
+                    if c.changed():
+                        self.propUpdateCallback(c.id, c.rawValue(self.frams.interfaceType == InterfaceType.SOCKET))
+                        updated = True
+                        error = self.getError()
+                        if error:
+                            messagebox.showerror("Framsticks error", error)
+                self.refreshButtonCommand()
+                if updated and self.onUpdate:
+                    self.onUpdate()
+    
+    def okButtonCommand(self):
+        self.applyButtonCommand()
+        self.cancelButtonCommand()
+
+    def _dismiss(self):
+        self.grab_release()
+        self.destroy()
