diff options
Diffstat (limited to 'game_engine_publishing.py')
-rw-r--r-- | game_engine_publishing.py | 576 |
1 files changed, 0 insertions, 576 deletions
diff --git a/game_engine_publishing.py b/game_engine_publishing.py deleted file mode 100644 index 495b0123..00000000 --- a/game_engine_publishing.py +++ /dev/null @@ -1,576 +0,0 @@ -# ##### 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), Oren Titane (Genome36)", - "version": (0, 1, 0), - "blender": (2, 75, 0), - "location": "Render Properties > Publishing Info", - "description": "Publish .blend file as game engine runtime, manage versions and platforms", - "warning": "", - "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 - with open(player_path, "rb") as file: - player_d = file.read() - offset = file.tell() - - # 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 - with open(blend_path, "rb") as blend_file: - blend_d = blend_file.read() - - # 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 - with open(output_path, "wb") as output: - # 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') - - 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_UL_assets(bpy.types.UIList): - bl_label = "Asset Paths Listing" - - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): - layout.prop(item, "name", text="", emboss=False) - - -class RENDER_UL_platforms(bpy.types.UIList): - bl_label = "Platforms Listing" - - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): - row = layout.row() - row.label(item.name) - row.prop(item, "publish", text="") - - -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 - - # config - layout.prop(ps, 'output_path') - layout.prop(ps, 'runtime_name') - layout.prop(ps, 'lib_path') - layout.prop(ps, 'make_archive') - - layout.separator() - - # assets list - layout.label("Asset Paths") - - # UI_UL_list - row = layout.row() - row.template_list("RENDER_UL_assets", "assets_list", ps, 'asset_paths', ps, 'asset_paths_active') - - # operators - col = row.column(align=True) - col.operator(PublishAddAssetPath.bl_idname, icon='ZOOMIN', text="") - col.operator(PublishRemoveAssetPath.bl_idname, icon='ZOOMOUT', text="") - - # indexing - if len(ps.asset_paths) > ps.asset_paths_active >= 0: - ap = ps.asset_paths[ps.asset_paths_active] - row = layout.row() - row.prop(ap, 'overwrite') - - layout.separator() - - # publishing list - row = layout.row(align=True) - row.label("Platforms") - row.prop(ps, 'publish_default_platform') - - # UI_UL_list - row = layout.row() - row.template_list("RENDER_UL_platforms", "platforms_list", ps, 'platforms', ps, 'platforms_active') - - # operators - 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="") - - # indexing - if len(ps.platforms) > ps.platforms_active >= 0: - platform = ps.platforms[ps.platforms_active] - layout.prop(platform, 'name') - layout.prop(platform, 'player_path') - - layout.operator(PublishAllPlatforms.bl_idname, 'Publish Platforms') - - -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 - - # verify lib folder - lib_path = bpy.path.abspath(ps.lib_path) - if not os.path.exists(lib_path): - self.report({'ERROR'}, "Could not add platforms, lib folder (%s) does not exist" % lib_path) - return {'CANCELLED'} - - 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 - - # create lib folder if not already available - lib_path = bpy.path.abspath(ps.lib_path) - if not os.path.exists(lib_path): - os.makedirs(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() |