diff --git a/B2G3/blender2godot/b2g_misc/server.py b/B2G3/blender2godot/b2g_misc/server.py new file mode 100644 index 0000000..5536341 --- /dev/null +++ b/B2G3/blender2godot/b2g_misc/server.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 + +import argparse +import contextlib +import os +import socket +import subprocess +import sys +from http.server import HTTPServer, SimpleHTTPRequestHandler, test # type: ignore +from pathlib import Path + + +# See cpython GH-17851 and GH-17864. +class DualStackServer(HTTPServer): + def server_bind(self): + # Suppress exception when protocol is IPv4. + with contextlib.suppress(Exception): + self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) + return super().server_bind() + + +class CORSRequestHandler(SimpleHTTPRequestHandler): + def end_headers(self): + self.send_header("Cross-Origin-Opener-Policy", "same-origin") + self.send_header("Cross-Origin-Embedder-Policy", "require-corp") + self.send_header("Access-Control-Allow-Origin", "*") + super().end_headers() + + +def shell_open(url): + if sys.platform == "win32": + os.startfile(url) + else: + opener = "open" if sys.platform == "darwin" else "xdg-open" + subprocess.call([opener, url]) + + +def serve(root, port, run_browser): + os.chdir(root) + + if run_browser: + # Open the served page in the user's default browser. + print("Opening the served URL in the default browser (use `--no-browser` or `-n` to disable this).") + shell_open(f"http://127.0.0.1:{port}") + + test(CORSRequestHandler, DualStackServer, port=port) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-p", "--port", help="port to listen on", default=8060, type=int) + parser.add_argument( + "-r", "--root", help="path to serve as root (relative to `platform/web/`)", default="../../bin", type=Path + ) + browser_parser = parser.add_mutually_exclusive_group(required=False) + browser_parser.add_argument( + "-n", "--no-browser", help="don't open default web browser automatically", dest="browser", action="store_false" + ) + parser.set_defaults(browser=True) + args = parser.parse_args() + + # Change to the directory where the script is located, + # so that the script can be run from any location. + os.chdir(Path(__file__).resolve().parent) + + serve(args.root, args.port, args.browser) + diff --git a/B2G3/blender2godot/game_export/game_export.py b/B2G3/blender2godot/game_export/game_export.py index 247251f..37c2930 100644 --- a/B2G3/blender2godot/game_export/game_export.py +++ b/B2G3/blender2godot/game_export/game_export.py @@ -175,7 +175,6 @@ def build_game(self, context): self.export_presets_filepath = os.path.join(context.scene.project_folder, "export_presets.cfg") self.add_selected_export_presets(context) print("Compiling...") - #bpy.ops.scene.compile_selected_versions_operator() bpy.ops.scene.compile_selected_versions_operator('INVOKE_DEFAULT') def config_presets(self, context): @@ -401,6 +400,7 @@ def compile_exe(self, context): os.mkdir(context.scene.web_exports_path) print("Web version...OK") context.scene.web_exe_filepath = os.path.join(context.scene.web_exports_path, context.scene.game_name) + context.scene.web_exe_filepath += ".html" self.process = subprocess.Popen([bpy.path.abspath(context.scene.godot_executable), "--no-window", "--path", context.scene.project_folder, "--export", "HTML5", context.scene.web_exe_filepath], stdout=subprocess.PIPE, stderr=subprocess.PIPE) print("Web version exported at :", context.scene.web_exports_path) #return {'FINISHED'} @@ -423,27 +423,6 @@ def set_pending_platforms(self, context): self.pending_platforms.append(self.available_platforms[4]) print("Pending versions: ", self.pending_platforms) - ''' - def execute(self, context): - self.all_compiled = False - self.process = None - self.create_builds_folder(context) - self.set_pending_platforms(context) - while (len(self.pending_platforms) > 0): - if self.process is None: - print("Process None") - context.scene.current_version_compiling = self.pending_platforms.pop() - self.compile_exe(context) - else: - if self.process.poll() == 0: - print("Process Next") - context.scene.current_version_compiling = self.pending_platforms.pop() - self.compile_exe(context) - self.all_compiled = True - print("Processes completed") - return {'FINISHED'} - ''' - def modal(self, context, event): if self.all_compiled == True: print("Processes completed") diff --git a/B2G3/blender2godot/test_project/test_project.py b/B2G3/blender2godot/test_project/test_project.py index 1993853..02a45b5 100644 --- a/B2G3/blender2godot/test_project/test_project.py +++ b/B2G3/blender2godot/test_project/test_project.py @@ -17,31 +17,94 @@ """ -Testing game actions +Testing game """ import subprocess import os +import contextlib +import socket +import sys +from http.server import HTTPServer, SimpleHTTPRequestHandler, test # type: ignore +from pathlib import Path + import bpy +from blender2godot.addon_config import addon_config # type: ignore + + +### BROWSER SERVER ### +# See cpython GH-17851 and GH-17864. +class DualStackServer(HTTPServer): + def server_bind(self): + # Suppress exception when protocol is IPv4. + with contextlib.suppress(Exception): + self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) + return super().server_bind() + +class CORSRequestHandler(SimpleHTTPRequestHandler): + def end_headers(self): + self.send_header("Cross-Origin-Opener-Policy", "same-origin") + self.send_header("Cross-Origin-Embedder-Policy", "require-corp") + self.send_header("Access-Control-Allow-Origin", "*") + super().end_headers() -class TestGameOperator(bpy.types.Operator): # It blocks blender execution until game exits - """Test Game Operator""" - bl_idname = "scene.test_game_operator" - bl_label = "Test Last Exported Game" + +### END BROWSER SERVER ### + + +class TestProjectGameOperator(bpy.types.Operator): # It blocks blender execution until game exits + """Test Project Game Operator""" + bl_idname = "scene.test_project_game_operator" + bl_label = "Test Last Exported Project" - def start_game(self, context): - print("Starting game", context.scene.project_folder) + def start_project_game(self, context): + print("Starting project game", context.scene.project_folder) self.cmd = subprocess.Popen([bpy.path.abspath(context.scene.godot_executable), "--path", context.scene.project_folder], stdout=subprocess.PIPE, stderr=subprocess.PIPE) def main(self, context): - self.start_game(context) + self.start_project_game(context) def execute(self, context): self.main(context) return {'FINISHED'} +class TestBrowserGameOperator(bpy.types.Operator): # It blocks blender execution until game exits + """Test Browser Game Operator""" + bl_idname = "scene.test_browser_game_operator" + bl_label = "Test Browser Build" + + _output = None + _testing = False + _port = 8060 + + def shell_open(self, context, url): + if sys.platform == "win32": + os.startfile(url) + else: + opener = "open" if sys.platform == "darwin" else "xdg-open" + self._output = subprocess.run([opener, url]) + print("Output:", self._output) + + def execute(self, context): + return {'FINISHED'} + + def modal(self, context, event): + if not self._testing: + self._testing = True + test(CORSRequestHandler, DualStackServer, port=self._port) + return {'PASS_THROUGH'} + + def invoke(self, context, event): + print("Starting browser game", context.scene.project_folder) + self._path = context.scene.web_exe_filepath.rpartition(os.sep)[0] + print(self._path) + os.chdir(self._path) + self._testing = False + self.shell_open(context, f"http://127.0.0.1:{self._port}") + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} class TestGamePanel(bpy.types.Panel): """Test Game Panel""" @@ -63,7 +126,6 @@ def draw_header(self, context): def draw(self, context): layout = self.layout - scene = context.scene if not bpy.data.is_saved: return @@ -79,15 +141,19 @@ def draw(self, context): box = row.box() if (os.path.isdir(context.scene.project_folder) and (context.scene.godot_export_ok)): row.alignment="CENTER" - box.operator("scene.test_game_operator", icon="PLAY") + box.operator("scene.test_project_game_operator", icon="PLAY") + if os.path.isfile(context.scene.web_exe_filepath + ".html"): + box.operator_context = 'INVOKE_DEFAULT' + box.operator("scene.test_browser_game_operator", icon_value=addon_config.preview_collections[0]["godot_icon"].icon_id) else: box.label(text="Export to godot before testing", icon="ERROR") - def register(): - bpy.utils.register_class(TestGameOperator) + bpy.utils.register_class(TestProjectGameOperator) + bpy.utils.register_class(TestBrowserGameOperator) bpy.utils.register_class(TestGamePanel) def unregister(): bpy.utils.unregister_class(TestGamePanel) - bpy.utils.unregister_class(TestGameOperator) + bpy.utils.unregister_class(TestBrowserGameOperator) + bpy.utils.unregister_class(TestProjectGameOperator)