Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMitchell Stokes <mogurijin@gmail.com>2014-06-26 03:49:50 +0400
committerMitchell Stokes <mogurijin@gmail.com>2014-06-26 03:53:19 +0400
commitc5fddd6f67ecde0aaca60337f0a3a5e635d3b671 (patch)
treeb194403eb98bd8740de9815ca9c0ac297edde938 /game_engine_publishing.py
parentd5426e412430181d91ebd103d685fce601ec59fd (diff)
BGE Publishing: Adding a new addon to help make publishing games easier.
Some highlights: * New panel in the render options to control publishing * Easier cross-platform publishing * Ability to archive published binaries More information can be found on the addons wiki page: http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Game_Engine/Publishing Note: This addon is intended to replace the Save As Runtime addon.
Diffstat (limited to 'game_engine_publishing.py')
-rw-r--r--game_engine_publishing.py541
1 files changed, 541 insertions, 0 deletions
diff --git a/game_engine_publishing.py b/game_engine_publishing.py
new file mode 100644
index 00000000..991a9acf
--- /dev/null
+++ b/game_engine_publishing.py
@@ -0,0 +1,541 @@
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+import bpy
+import os
+import tempfile
+import shutil
+import tarfile
+import time
+import stat
+
+
+bl_info = {
+ "name": "Game Engine Publishing",
+ "author": "Mitchell Stokes (Moguri)",
+ "version": (0, 1, 0),
+ "blender": (2, 72, 0),
+ "location": "Render Properties > Publishing Info",
+ "description": "",
+ "warning": "beta",
+ "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Game_Engine/Publishing",
+ "category": "Game Engine",
+}
+
+
+def WriteRuntime(player_path, output_path, asset_paths, copy_python, overwrite_lib, copy_dlls, make_archive, report=print):
+ import struct
+
+ player_path = bpy.path.abspath(player_path)
+ ext = os.path.splitext(player_path)[-1].lower()
+ output_path = bpy.path.abspath(output_path)
+ output_dir = os.path.dirname(output_path)
+ if not os.path.exists(output_dir):
+ os.makedirs(output_dir)
+
+ python_dir = os.path.join(os.path.dirname(player_path),
+ bpy.app.version_string.split()[0],
+ "python",
+ "lib")
+
+ # Check the paths
+ if not os.path.isfile(player_path) and not(os.path.exists(player_path) and player_path.endswith('.app')):
+ report({'ERROR'}, "The player could not be found! Runtime not saved")
+ return
+
+ # Check if we're bundling a .app
+ if player_path.lower().endswith('.app'):
+ # Python doesn't need to be copied for OS X since it's already inside blenderplayer.app
+ copy_python = False
+
+ output_path = bpy.path.ensure_ext(output_path, '.app')
+
+ if os.path.exists(output_path):
+ shutil.rmtree(output_path)
+
+ shutil.copytree(player_path, output_path)
+ bpy.ops.wm.save_as_mainfile(filepath=os.path.join(output_path, 'Contents', 'Resources', 'game.blend'),
+ relative_remap=False,
+ compress=False,
+ copy=True,
+ )
+ else:
+ # Enforce "exe" extension on Windows
+ if player_path.lower().endswith('.exe'):
+ output_path = bpy.path.ensure_ext(output_path, '.exe')
+
+ # Get the player's binary and the offset for the blend
+ file = open(player_path, 'rb')
+ player_d = file.read()
+ offset = file.tell()
+ file.close()
+
+ # Create a tmp blend file (Blenderplayer doesn't like compressed blends)
+ tempdir = tempfile.mkdtemp()
+ blend_path = os.path.join(tempdir, bpy.path.clean_name(output_path))
+ bpy.ops.wm.save_as_mainfile(filepath=blend_path,
+ relative_remap=False,
+ compress=False,
+ copy=True,
+ )
+
+ # Get the blend data
+ blend_file = open(blend_path, 'rb')
+ blend_d = blend_file.read()
+ blend_file.close()
+
+ # Get rid of the tmp blend, we're done with it
+ os.remove(blend_path)
+ os.rmdir(tempdir)
+
+ # Create a new file for the bundled runtime
+ output = open(output_path, 'wb')
+
+ # Write the player and blend data to the new runtime
+ print("Writing runtime...", end=" ", flush=True)
+ output.write(player_d)
+ output.write(blend_d)
+
+ # Store the offset (an int is 4 bytes, so we split it up into 4 bytes and save it)
+ output.write(struct.pack('BBBB', (offset >> 24) & 0xFF,
+ (offset >> 16) & 0xFF,
+ (offset >> 8) & 0xFF,
+ (offset >> 0) & 0xFF))
+
+ # Stuff for the runtime
+ output.write(b'BRUNTIME')
+ output.close()
+
+ print("done", flush=True)
+
+ # Make sure the runtime is executable
+ os.chmod(output_path, 0o755)
+
+ # Copy bundled Python
+ blender_dir = os.path.dirname(player_path)
+
+ if copy_python:
+ print("Copying Python files...", end=" ", flush=True)
+ py_folder = os.path.join(bpy.app.version_string.split()[0], "python", "lib")
+ dst = os.path.join(output_dir, py_folder)
+ src = python_dir
+
+ if os.path.exists(dst) and overwrite_lib:
+ shutil.rmtree(dst)
+
+ if not os.path.exists(dst):
+ shutil.copytree(src, dst, ignore=lambda dir, contents: [i for i in contents if i == '__pycache__'])
+ print("done", flush=True)
+ else:
+ print("used existing Python folder", flush=True)
+
+ # And DLLs if we're doing a Windows runtime)
+ if copy_dlls and ext == ".exe":
+ print("Copying DLLs...", end=" ", flush=True)
+ for file in [i for i in os.listdir(blender_dir) if i.lower().endswith('.dll')]:
+ src = os.path.join(blender_dir, file)
+ dst = os.path.join(output_dir, file)
+ shutil.copy2(src, dst)
+
+ print("done", flush=True)
+
+ # Copy assets
+ for ap in asset_paths:
+ src = bpy.path.abspath(ap.name)
+ dst = os.path.join(output_dir, ap.name[2:] if ap.name.startswith('//') else ap.name)
+
+ if os.path.exists(src):
+ if os.path.isdir(src):
+ if ap.overwrite and os.path.exists(dst):
+ shutil.rmtree(dst)
+ elif not os.path.exists(dst):
+ shutil.copytree(src, dst)
+ else:
+ if ap.overwrite or not os.path.exists(dst):
+ shutil.copy2(src, dst)
+ else:
+ report({'ERROR'}, "Could not find asset path: '%s'" % src)
+
+ # Make archive
+ if make_archive:
+ print("Making archive...", end=" ", flush=True)
+
+ arctype = ''
+ if player_path.lower().endswith('.exe'):
+ arctype = 'zip'
+ elif player_path.lower().endswith('.app'):
+ arctype = 'zip'
+ else: # Linux
+ arctype = 'gztar'
+
+ basedir = os.path.normpath(os.path.join(os.path.dirname(output_path), '..'))
+ afilename = os.path.join(basedir, os.path.basename(output_dir))
+
+ if arctype == 'gztar':
+ # Create the tarball ourselves instead of using shutil.make_archive
+ # so we can handle permission bits.
+
+ # The runtimename needs to use forward slashes as a path separator
+ # since this is what tarinfo.name is using.
+ runtimename = os.path.relpath(output_path, basedir).replace('\\', '/')
+
+ def _set_ex_perm(tarinfo):
+ if tarinfo.name == runtimename:
+ tarinfo.mode = 0o755
+ return tarinfo
+
+ with tarfile.open(afilename + '.tar.gz', 'w:gz') as tf:
+ tf.add(output_dir, os.path.relpath(output_dir, basedir), filter=_set_ex_perm)
+ elif arctype == 'zip':
+ shutil.make_archive(afilename, 'zip', output_dir)
+ else:
+ report({'ERROR'}, "Unknown archive type %s for runtime %s\n" % (arctype, player_path))
+
+ print("done", flush=True)
+
+
+class PublishAllPlatforms(bpy.types.Operator):
+ bl_idname = "wm.publish_platforms"
+ bl_label = "Exports a runtime for each listed platform"
+
+ def execute(self, context):
+ ps = context.scene.ge_publish_settings
+
+ if ps.publish_default_platform:
+ print("Publishing default platform")
+ blender_bin_path = bpy.app.binary_path
+ blender_bin_dir = os.path.dirname(blender_bin_path)
+ ext = os.path.splitext(blender_bin_path)[-1].lower()
+ WriteRuntime(os.path.join(blender_bin_dir, 'blenderplayer' + ext),
+ os.path.join(ps.output_path, 'default', ps.runtime_name),
+ ps.asset_paths,
+ True,
+ True,
+ True,
+ ps.make_archive,
+ self.report
+ )
+ else:
+ print("Skipping default platform")
+
+ for platform in ps.platforms:
+ if platform.publish:
+ print("Publishing", platform.name)
+ WriteRuntime(platform.player_path,
+ os.path.join(ps.output_path, platform.name, ps.runtime_name),
+ ps.asset_paths,
+ True,
+ True,
+ True,
+ ps.make_archive,
+ self.report
+ )
+ else:
+ print("Skipping", platform.name)
+
+ return {'FINISHED'}
+
+
+class RENDER_PT_publish(bpy.types.Panel):
+ bl_label = "Publishing Info"
+ bl_space_type = "PROPERTIES"
+ bl_region_type = "WINDOW"
+ bl_context = "render"
+
+ @classmethod
+ def poll(cls, context):
+ scene = context.scene
+ return scene and (scene.render.engine == "BLENDER_GAME")
+
+ def draw(self, context):
+ ps = context.scene.ge_publish_settings
+ layout = self.layout
+
+ layout.prop(ps, 'output_path')
+ layout.prop(ps, 'runtime_name')
+ layout.prop(ps, 'lib_path')
+ layout.prop(ps, 'make_archive')
+
+ layout.label("Asset Paths")
+ row = layout.row()
+ row.template_list("UI_UL_list", "assets_list", ps, 'asset_paths', ps, 'asset_paths_active')
+ col = row.column(align=True)
+ col.operator(PublishAddAssetPath.bl_idname, icon='ZOOMIN', text="")
+ col.operator(PublishRemoveAssetPath.bl_idname, icon='ZOOMOUT', text="")
+
+ if len(ps.asset_paths) > ps.asset_paths_active >= 0:
+ ap = ps.asset_paths[ps.asset_paths_active]
+ row = layout.row()
+ row.prop(ap, 'name')
+ row.prop(ap, 'overwrite')
+
+ layout.label("Platforms")
+ row = layout.row()
+ row.template_list("UI_UL_list", "platforms_list", ps, 'platforms', ps, 'platforms_active')
+
+ col = row.column(align=True)
+ col.operator(PublishAddPlatform.bl_idname, icon='ZOOMIN', text="")
+ col.operator(PublishRemovePlatform.bl_idname, icon='ZOOMOUT', text="")
+ col.menu("PUBLISH_MT_platform_specials", icon='DOWNARROW_HLT', text="")
+
+ if len(ps.platforms) > ps.platforms_active >= 0:
+ platform = ps.platforms[ps.platforms_active]
+ layout.prop(platform, 'name')
+ layout.prop(platform, 'player_path')
+ layout.prop(platform, 'publish')
+
+ layout.prop(ps, 'publish_default_platform')
+ layout.operator(PublishAllPlatforms.bl_idname, 'Publish')
+
+
+class PublishAutoPlatforms(bpy.types.Operator):
+ bl_idname = "scene.publish_auto_platforms"
+ bl_label = "Auto Add Platforms"
+
+ def execute(self, context):
+ ps = context.scene.ge_publish_settings
+
+ lib_path = bpy.path.abspath(ps.lib_path)
+
+ for lib in [i for i in os.listdir(lib_path) if os.path.isdir(os.path.join(lib_path, i))]:
+ print("Found folder:", lib)
+ player_found = False
+ for root, dirs, files in os.walk(os.path.join(lib_path, lib)):
+ if "__MACOSX" in root:
+ continue
+
+ for f in dirs + files:
+ if f.startswith("blenderplayer.app") or f.startswith("blenderplayer"):
+ a = ps.platforms.add()
+ if lib.startswith('blender-'):
+ # Clean up names for packages from blender.org
+ # example: blender-2.71-RC2-OSX_10.6-x86_64.zip => OSX_10.6-x86_64.zip
+ # We're pretty consistent on naming, so this should hold up.
+ a.name = '-'.join(lib.split('-')[3 if 'rc' in lib.lower() else 2:])
+ else:
+ a.name = lib
+ a.player_path = bpy.path.relpath(os.path.join(root, f))
+ player_found = True
+ break
+
+ if player_found:
+ break
+
+ return {'FINISHED'}
+
+# TODO This operator takes a long time to run, which is bad for UX. Could this instead be done as some sort of
+# modal dialog? This could also allow users to select which platforms to download and give a better progress
+# indicator.
+class PublishDownloadPlatforms(bpy.types.Operator):
+ bl_idname = "scene.publish_download_platforms"
+ bl_label = "Download Platforms"
+
+ def execute(self, context):
+ import html.parser
+ import urllib.request
+
+ remote_platforms = []
+
+ ps = context.scene.ge_publish_settings
+ lib_path = bpy.path.abspath(ps.lib_path)
+
+ print("Retrieving list of platforms from blender.org...", end=" ", flush=True)
+
+ class AnchorParser(html.parser.HTMLParser):
+ def handle_starttag(self, tag, attrs):
+ if tag == 'a':
+ for key, value in attrs:
+ if key == 'href' and value.startswith('blender'):
+ remote_platforms.append(value)
+
+ url = 'http://download.blender.org/release/Blender' + bpy.app.version_string.split()[0]
+ parser = AnchorParser()
+ data = urllib.request.urlopen(url).read()
+ parser.feed(str(data))
+
+ print("done", flush=True)
+
+ print("Downloading files (this will take a while depending on your internet connection speed).", flush=True)
+ for i in remote_platforms:
+ src = '/'.join((url, i))
+ dst = os.path.join(lib_path, i)
+
+ dst_dir = '.'.join([i for i in dst.split('.') if i not in {'zip', 'tar', 'bz2'}])
+ if not os.path.exists(dst) and not os.path.exists(dst.split('.')[0]):
+ print("Downloading " + src + "...", end=" ", flush=True)
+ urllib.request.urlretrieve(src, dst)
+ print("done", flush=True)
+ else:
+ print("Reusing existing file: " + dst, flush=True)
+
+ print("Unpacking " + dst + "...", end=" ", flush=True)
+ if os.path.exists(dst_dir):
+ shutil.rmtree(dst_dir)
+ shutil.unpack_archive(dst, dst_dir)
+ print("done", flush=True)
+
+ print("Creating platform from libs...", flush=True)
+ bpy.ops.scene.publish_auto_platforms()
+ return {'FINISHED'}
+
+
+class PublishAddPlatform(bpy.types.Operator):
+ bl_idname = "scene.publish_add_platform"
+ bl_label = "Add Publish Platform"
+
+ def execute(self, context):
+ a = context.scene.ge_publish_settings.platforms.add()
+ a.name = a.name
+ return {'FINISHED'}
+
+
+class PublishRemovePlatform(bpy.types.Operator):
+ bl_idname = "scene.publish_remove_platform"
+ bl_label = "Remove Publish Platform"
+
+ def execute(self, context):
+ ps = context.scene.ge_publish_settings
+ if ps.platforms_active < len(ps.platforms):
+ ps.platforms.remove(ps.platforms_active)
+ return {'FINISHED'}
+ return {'CANCELLED'}
+
+
+# TODO maybe this should display a file browser?
+class PublishAddAssetPath(bpy.types.Operator):
+ bl_idname = "scene.publish_add_assetpath"
+ bl_label = "Add Asset Path"
+
+ def execute(self, context):
+ a = context.scene.ge_publish_settings.asset_paths.add()
+ a.name = a.name
+ return {'FINISHED'}
+
+
+class PublishRemoveAssetPath(bpy.types.Operator):
+ bl_idname = "scene.publish_remove_assetpath"
+ bl_label = "Remove Asset Path"
+
+ def execute(self, context):
+ ps = context.scene.ge_publish_settings
+ if ps.asset_paths_active < len(ps.asset_paths):
+ ps.asset_paths.remove(ps.asset_paths_active)
+ return {'FINISHED'}
+ return {'CANCELLED'}
+
+
+class PUBLISH_MT_platform_specials(bpy.types.Menu):
+ bl_label = "Platform Specials"
+
+ def draw(self, context):
+ layout = self.layout
+ layout.operator(PublishAutoPlatforms.bl_idname)
+ layout.operator(PublishDownloadPlatforms.bl_idname)
+
+
+class PlatformSettings(bpy.types.PropertyGroup):
+ name = bpy.props.StringProperty(
+ name = "Platform Name",
+ description = "The name of the platform",
+ default = "Platform",
+ )
+
+ player_path = bpy.props.StringProperty(
+ name = "Player Path",
+ description = "The path to the Blenderplayer to use for this platform",
+ default = "//lib/platform/blenderplayer",
+ subtype = 'FILE_PATH',
+ )
+
+ publish = bpy.props.BoolProperty(
+ name = "Publish",
+ description = "Whether or not to publish to this platform",
+ default = True,
+ )
+
+
+class AssetPath(bpy.types.PropertyGroup):
+ # TODO This needs a way to be a FILE_PATH or a DIR_PATH
+ name = bpy.props.StringProperty(
+ name = "Asset Path",
+ description = "Path to the asset to be copied",
+ default = "//src",
+ subtype = 'FILE_PATH',
+ )
+
+ overwrite = bpy.props.BoolProperty(
+ name = "Overwrite Asset",
+ description = "Overwrite the asset if it already exists in the destination folder",
+ default = True,
+ )
+
+
+class PublishSettings(bpy.types.PropertyGroup):
+ output_path = bpy.props.StringProperty(
+ name = "Publish Output",
+ description = "Where to publish the game",
+ default = "//bin/",
+ subtype = 'DIR_PATH',
+ )
+
+ runtime_name = bpy.props.StringProperty(
+ name = "Runtime name",
+ description = "The filename for the created runtime",
+ default = "game",
+ )
+
+ lib_path = bpy.props.StringProperty(
+ name = "Library Path",
+ description = "Directory to search for platforms",
+ default = "//lib/",
+ subtype = 'DIR_PATH',
+ )
+
+ publish_default_platform = bpy.props.BoolProperty(
+ name = "Publish Default Platform",
+ description = "Whether or not to publish the default platform (the Blender install running this addon) when publishing platforms",
+ default = True,
+ )
+
+
+ platforms = bpy.props.CollectionProperty(type=PlatformSettings, name="Platforms")
+ platforms_active = bpy.props.IntProperty()
+
+ asset_paths = bpy.props.CollectionProperty(type=AssetPath, name="Asset Paths")
+ asset_paths_active = bpy.props.IntProperty()
+
+ make_archive = bpy.props.BoolProperty(
+ name = "Make Archive",
+ description = "Create a zip archive of the published game",
+ default = True,
+ )
+
+
+def register():
+ bpy.utils.register_module(__name__)
+
+ bpy.types.Scene.ge_publish_settings = bpy.props.PointerProperty(type=PublishSettings)
+
+
+def unregister():
+ bpy.utils.unregister_module(__name__)
+ del bpy.types.Scene.ge_publish_settings
+
+
+if __name__ == "__main__":
+ register()