diff --git a/punyverse/world.py b/punyverse/world.py
index 7df3bb7..93af638 100644
--- a/punyverse/world.py
+++ b/punyverse/world.py
@@ -57,173 +57,12 @@ def get_best_texture(info, optional=False, loader=load_texture):
     return cheap, skip, texture
 
 
-def load_world(file):
-    with open(os.path.join(os.path.dirname(__file__), file)) as f:
-        root = json.load(f, object_pairs_hook=OrderedDict)
-
-        world = World()
-        au = root.get('au', 2000)
-        e = lambda x: eval(str(x), {'__builtins__': None}, {'AU': au, 'TEXTURE': texture.max_texture})
-        tick = root.get('tick', 4320)  # How many second is a tick?
-        length = root.get('length', 4320)  # Satellite distance is in km, divide by this gets in world units
-        world.tick_length = tick
-
-        if 'start' in root:
-            info = root['start']
-            x = e(info.get('x', 0))
-            y = e(info.get('y', 0))
-            z = e(info.get('z', 0))
-            pitch = e(info.get('pitch', 0))
-            yaw = e(info.get('yaw', 0))
-            roll = e(info.get('roll', 0))
-            world.start = (x, y, z)
-            world.direction = (pitch, yaw, roll)
-
-        def body(name, info, parent=None):
-            lighting = info.get('lighting', True)
-            x = e(info.get('x', 0))
-            y = e(info.get('y', 0))
-            z = e(info.get('z', 0))
-            pitch = e(info.get('pitch', 0))
-            yaw = e(info.get('yaw', 0))
-            roll = e(info.get('roll', 0))
-            rotation = e(info.get('rotation', 86400))
-            radius = e(info.get('radius', length)) / length
-            background = info.get('background', False)
-            orbit_distance = e(info.get('orbit_distance', au))
-            division = info.get('division', max(min(int(radius / 8), 60), 10))
-
-            if 'texture' in info:
-                cheap, skip, texture = get_best_texture(info['texture'], optional=info.get('optional', False))
-                if skip:
-                    return
-                if cheap:
-                    object_id = compile(colourball, radius, division, division, texture)
-                else:
-                    if 'normal' in info:
-                        object_id = compile(normal_sphere, radius, division, texture, info['normal'], lighting=lighting)
-                    else:
-                        object_id = compile(sphere, radius, division, division, texture, lighting=lighting)
-            elif 'model' in info:
-                scale = info.get('scale', 1)
-                object_id = model_list(load_model(info['model']), info.get('sx', scale), info.get('sy', scale),
-                                       info.get('sz', scale), (0, 0, 0))
-            else:
-                print 'Nothing to load for %s.' % name
-                return
-
-            params = {'world': world, 'orbit_distance': orbit_distance}
-            if parent is None:
-                type = Body
-            else:
-                x, y, z = parent.location
-                distance = e(info.get('distance', 100))  # Semi-major axis when actually displayed in virtual space
-                sma = e(info.get('sma', distance))       # Semi-major axis used to calculate orbital speed
-                if hasattr(parent, 'mass') and parent.mass is not None:
-                    period = 2 * pi * sqrt((sma * 1000) ** 3 / (G * parent.mass))
-                    speed = 360 / (period + .0)
-                    if not rotation:  # Rotation = 0 assumes tidal lock
-                        rotation = period
-                else:
-                    speed = info.get('orbit_speed', 1)
-                type = Satellite
-                params.update(parent=parent, orbit_speed=speed,
-                              distance=distance / length, eccentricity=info.get('eccentricity', 0),
-                              inclination=info.get('inclination', 0), longitude=info.get('longitude', 0),
-                              argument=info.get('argument', 0))
-
-            if 'mass' in info:
-                params['mass'] = info['mass']
-
-            atmosphere_id = 0
-            cloudmap_id = 0
-            corona_id = 0
-            if 'atmosphere' in info:
-                atmosphere_data = info['atmosphere']
-                atm_size = e(atmosphere_data.get('diffuse_size', None))
-                atm_texture = atmosphere_data.get('diffuse_texture', None)
-                cloud_texture = atmosphere_data.get('cloud_texture', None)
-                corona_texture = atmosphere_data.get('corona_texture', None)
-                if cloud_texture is not None:
-                    cheap, _, cloud_texture = get_best_texture(cloud_texture, loader=load_clouds)
-                    if not cheap:
-                        cloudmap_id = compile(sphere, radius + 2, division, division, cloud_texture,
-                                              lighting=False)
-                if corona_texture is not None:
-                    cheap, _, corona = get_best_texture(corona_texture)
-                    if not cheap:
-                        corona_size = atmosphere_data.get('corona_size', radius / 2)
-                        corona_division = atmosphere_data.get('corona_division', 100)
-                        corona_ratio = atmosphere_data.get('corona_ratio', 0.5)
-                        corona_id = compile(flare, radius, radius + corona_size, corona_division,
-                                            corona_ratio, corona)
-
-                if atm_texture is not None:
-                    cheap, _, atm_texture = get_best_texture(atm_texture)
-                    if not cheap:
-                        atmosphere_id = compile(disk, radius, radius + atm_size, 30, atm_texture)
-
-            theta = 360 / (rotation + .0) if rotation else 0
-            object = type(object_id, (x, y, z), (pitch, yaw, roll), rotation_angle=theta,
-                          atmosphere=atmosphere_id, cloudmap=cloudmap_id, background=background,
-                          corona=corona_id, **params)
-            world.tracker.append(object)
-
-            if 'ring' in info:
-                ring_data = info['ring']
-                texture = ring_data.get('texture', None)
-                distance = e(ring_data.get('distance', radius * 1.2))
-                size = e(ring_data.get('size', radius / 2))
-                pitch = e(ring_data.get('pitch', pitch))
-                yaw = e(ring_data.get('yaw', yaw))
-                roll = e(ring_data.get('roll', roll))
-
-                cheap, _, texture = get_best_texture(texture)
-                if not cheap:
-                    world.tracker.append(
-                        type(compile(disk, distance, distance + size, 30, texture), (x, y, z),
-                             (pitch, yaw, roll), **params))
-
-            for satellite, info in info.get('satellites', {}).iteritems():
-                print 'Loading %s, satellite of %s.' % (satellite, name)
-                body(satellite, info, object)
-
-        for planet, info in root['bodies'].iteritems():
-            print 'Loading %s.' % planet
-            body(planet, info)
-
-        for name, info in root['belts'].iteritems():
-            print 'Loading %s.' % name
-            x = e(info.get('x', 0))
-            y = e(info.get('y', 0))
-            z = e(info.get('z', 0))
-            radius = e(info.get('radius', 0))
-            cross = e(info.get('cross', 0))
-            count = int(e(info.get('count', 0)))
-            scale = info.get('scale', 1)
-            longitude = info.get('longitude', 0)
-            inclination = info.get('inclination', 0)
-            argument = info.get('argument', 0)
-            rotation = info.get('period', 31536000)
-            theta = 360 / (rotation + .0) if rotation else 0
-
-            models = info['model']
-            if not isinstance(models, list):
-                models = [models]
-            objects = []
-            for model in models:
-                objects.append(model_list(load_model(model), info.get('sx', scale), info.get('sy', scale),
-                                          info.get('sz', scale), (0, 0, 0)))
-
-            world.tracker.append(Belt(compile(belt, radius, cross, objects, count),
-                                      (x, y, z), (inclination, longitude, argument),
-                                      rotation_angle=theta, world=world))
-
-        return world
+def load_world(file, callback=lambda message, completion: None):
+    return World(file, callback)
 
 
 class World(object):
-    def __init__(self):
+    def __init__(self, file, callback):
         self.tracker = []
         self.start = (0, 0, 0)
         self.direction = (0, 0, 0)
@@ -232,3 +71,174 @@ class World(object):
         self.z = None
         self.tick_length = 1
         self.tick = 0
+
+        self.callback = callback
+        self._parse(file)
+        del self.callback # So it can't be used after loading finishes
+
+    def _eval(self, value):
+        return eval(str(value), {'__builtins__': None}, self.context)
+
+    def _parse(self, file):
+        self.callback('Loading configuration file...', 0)
+        with open(os.path.join(os.path.dirname(__file__), file)) as f:
+            root = json.load(f, object_pairs_hook=OrderedDict)
+        self.au = root.get('au', 2000)
+        self.length = root.get('length', 4320)
+        self.context = {'AU': self.au, 'TEXTURE': texture.max_texture, 'KM': 1.0 / self.length}
+
+        tick = root.get('tick', 4320)  # How many second is a tick?
+        self.tick_length = tick
+
+        if 'start' in root:
+            info = root['start']
+            x = self._eval(info.get('x', 0))
+            y = self._eval(info.get('y', 0))
+            z = self._eval(info.get('z', 0))
+            pitch = self._eval(info.get('pitch', 0))
+            yaw = self._eval(info.get('yaw', 0))
+            roll = self._eval(info.get('roll', 0))
+            self.start = (x, y, z)
+            self.direction = (pitch, yaw, roll)
+
+        for planet, info in root['bodies'].iteritems():
+            print 'Loading %s.' % planet
+            self._body(planet, info)
+
+        for name, info in root['belts'].iteritems():
+            print 'Loading %s.' % name
+
+    def _belt(self, name, info):
+        x = self._eval(info.get('x', 0))
+        y = self._eval(info.get('y', 0))
+        z = self._eval(info.get('z', 0))
+        radius = self._eval(info.get('radius', 0))
+        cross = self._eval(info.get('cross', 0))
+        count = int(self._eval(info.get('count', 0)))
+        scale = info.get('scale', 1)
+        longitude = info.get('longitude', 0)
+        inclination = info.get('inclination', 0)
+        argument = info.get('argument', 0)
+        rotation = info.get('period', 31536000)
+        theta = 360 / (rotation + .0) if rotation else 0
+
+        models = info['model']
+        if not isinstance(models, list):
+            models = [models]
+        objects = []
+        for model in models:
+            objects.append(model_list(load_model(model), info.get('sx', scale), info.get('sy', scale),
+                                      info.get('sz', scale), (0, 0, 0)))
+
+        self.tracker.append(Belt(compile(belt, radius, cross, objects, count),
+                                 (x, y, z), (inclination, longitude, argument),
+                                 rotation_angle=theta, world=self))
+
+    def _body(self, name, info, parent=None):
+        lighting = info.get('lighting', True)
+        x = self._eval(info.get('x', 0))
+        y = self._eval(info.get('y', 0))
+        z = self._eval(info.get('z', 0))
+        pitch = self._eval(info.get('pitch', 0))
+        yaw = self._eval(info.get('yaw', 0))
+        roll = self._eval(info.get('roll', 0))
+        rotation = self._eval(info.get('rotation', 86400))
+        radius = self._eval(info.get('radius', self.length)) / self.length
+        background = info.get('background', False)
+        orbit_distance = self._eval(info.get('orbit_distance', self.au))
+        division = info.get('division', max(min(int(radius / 8), 60), 10))
+
+        if 'texture' in info:
+            cheap, skip, texture = get_best_texture(info['texture'], optional=info.get('optional', False))
+            if skip:
+                return
+            if cheap:
+                object_id = compile(colourball, radius, division, division, texture)
+            else:
+                if 'normal' in info:
+                    object_id = compile(normal_sphere, radius, division, texture, info['normal'], lighting=lighting)
+                else:
+                    object_id = compile(sphere, radius, division, division, texture, lighting=lighting)
+        elif 'model' in info:
+            scale = info.get('scale', 1)
+            object_id = model_list(load_model(info['model']), info.get('sx', scale), info.get('sy', scale),
+                                   info.get('sz', scale), (0, 0, 0))
+        else:
+            print 'Nothing to load for %s.' % name
+            return
+
+        params = {'world': self, 'orbit_distance': orbit_distance}
+        if parent is None:
+            type = Body
+        else:
+            x, y, z = parent.location
+            distance = self._eval(info.get('distance', 100))  # Semi-major axis when actually displayed in virtual space
+            sma = self._eval(info.get('sma', distance))       # Semi-major axis used to calculate orbital speed
+            if hasattr(parent, 'mass') and parent.mass is not None:
+                period = 2 * pi * sqrt((sma * 1000) ** 3 / (G * parent.mass))
+                speed = 360 / (period + .0)
+                if not rotation:  # Rotation = 0 assumes tidal lock
+                    rotation = period
+            else:
+                speed = info.get('orbit_speed', 1)
+            type = Satellite
+            params.update(parent=parent, orbit_speed=speed,
+                          distance=distance / self.length, eccentricity=info.get('eccentricity', 0),
+                          inclination=info.get('inclination', 0), longitude=info.get('longitude', 0),
+                          argument=info.get('argument', 0))
+
+        if 'mass' in info:
+            params['mass'] = info['mass']
+
+        atmosphere_id = 0
+        cloudmap_id = 0
+        corona_id = 0
+        if 'atmosphere' in info:
+            atmosphere_data = info['atmosphere']
+            atm_size = self._eval(atmosphere_data.get('diffuse_size', None))
+            atm_texture = atmosphere_data.get('diffuse_texture', None)
+            cloud_texture = atmosphere_data.get('cloud_texture', None)
+            corona_texture = atmosphere_data.get('corona_texture', None)
+            if cloud_texture is not None:
+                cheap, _, cloud_texture = get_best_texture(cloud_texture, loader=load_clouds)
+                if not cheap:
+                    cloudmap_id = compile(sphere, radius + 2, division, division, cloud_texture,
+                                          lighting=False)
+            if corona_texture is not None:
+                cheap, _, corona = get_best_texture(corona_texture)
+                if not cheap:
+                    corona_size = atmosphere_data.get('corona_size', radius / 2)
+                    corona_division = atmosphere_data.get('corona_division', 100)
+                    corona_ratio = atmosphere_data.get('corona_ratio', 0.5)
+                    corona_id = compile(flare, radius, radius + corona_size, corona_division,
+                                        corona_ratio, corona)
+
+            if atm_texture is not None:
+                cheap, _, atm_texture = get_best_texture(atm_texture)
+                if not cheap:
+                    atmosphere_id = compile(disk, radius, radius + atm_size, 30, atm_texture)
+
+        theta = 360 / (rotation + .0) if rotation else 0
+        object = type(object_id, (x, y, z), (pitch, yaw, roll), rotation_angle=theta,
+                      atmosphere=atmosphere_id, cloudmap=cloudmap_id, background=background,
+                      corona=corona_id, **params)
+        self.tracker.append(object)
+
+        if 'ring' in info:
+            ring_data = info['ring']
+            texture = ring_data.get('texture', None)
+            distance = self._eval(ring_data.get('distance', radius * 1.2))
+            size = self._eval(ring_data.get('size', radius / 2))
+            pitch = self._eval(ring_data.get('pitch', pitch))
+            yaw = self._eval(ring_data.get('yaw', yaw))
+            roll = self._eval(ring_data.get('roll', roll))
+
+            cheap, _, texture = get_best_texture(texture)
+            if not cheap:
+                self.tracker.append(
+                    type(compile(disk, distance, distance + size, 30, texture), (x, y, z),
+                         (pitch, yaw, roll), **params))
+
+        for satellite, info in info.get('satellites', {}).iteritems():
+            print 'Loading %s, satellite of %s.' % (satellite, name)
+            self._body(satellite, info, object)
\ No newline at end of file