Skip to content

Instantly share code, notes, and snippets.

@unvestigate
Created April 17, 2025 08:12
Show Gist options
  • Select an option

  • Save unvestigate/92d99a62e3f53e751385b107e3c63455 to your computer and use it in GitHub Desktop.

Select an option

Save unvestigate/92d99a62e3f53e751385b107e3c63455 to your computer and use it in GitHub Desktop.
The main source code files for the BlenderSync addon
import bpy
import ctypes
import tempfile
import pathlib
import subprocess
import zipfile
import os
import mathutils
blender_sync_lib = None
server_running = False
server_shutdown_requested = False
editing_mesh = False
surroundings_obj_file_path = ""
# Update the server four times per second.
server_update_rate = 0.25
# These need to match the values on the C side:
BLENDERSYNC_EVENT_NONE = 0
BLENDERSYNC_EVENT_CLIENT_CONNECTED = 1
BLENDERSYNC_EVENT_CLIENT_DISCONNECTED = 2
BLENDERSYNC_EVENT_TARGET_MESH_PATH_RECEIVED = 3
BLENDERSYNC_EVENT_TARGET_AND_SURROUNDINGS_MESH_PATH_RECEIVED = 4
BLENDERSYNC_TARGET_OBJECT_NAME = "BlenderSyncTarget"
BLENDERSYNC_TARGET_OBJECT_DATA_NAME = "BlenderSyncTargetData"
BLENDERSYNC_SURROUNDINGS_OBJECT_NAME = "BlenderSyncSurroundingsObj"
BLENDERSYNC_HELPERS_COLLECTION_NAME = "BlenderSyncHelpers"
BLENDERSYNC_ZIP_BLEND_ARCNAME = "blendersync_target.blend"
BLENDERSYNC_ZIP_FBX_ARCNAME = "blendersync_target.fbx"
#server_iteration = 0
def get_addon_name():
return __package__.split('.')[0]
def update_server():
global server_running
global server_shutdown_requested
global editing_mesh
global surroundings_obj_file_path
#global server_iteration
#print(f"server iteration: {server_iteration}")
#server_iteration += 1
if server_shutdown_requested:
print("BasisBlenderSync: Shutting down server...")
server_running = False
server_shutdown_requested = False
editing_mesh = False
blender_sync_lib.stopServer()
return None
evt = blender_sync_lib.service()
if evt == BLENDERSYNC_EVENT_NONE:
pass
elif evt == BLENDERSYNC_EVENT_CLIENT_CONNECTED:
print("BasisBlenderSync: client connected")
elif evt == BLENDERSYNC_EVENT_CLIENT_DISCONNECTED:
print("BasisBlenderSync: client disconnected")
elif (evt == BLENDERSYNC_EVENT_TARGET_MESH_PATH_RECEIVED or
evt == BLENDERSYNC_EVENT_TARGET_AND_SURROUNDINGS_MESH_PATH_RECEIVED):
target_mesh_path = get_target_mesh_file_path()
editing_mesh = True
#print(f"Received target path: {target_mesh_path}")
if target_mesh_path == "":
print("BasisBlenderSync: Creating new scene.")
create_new_mesh()
else:
print(f"BasisBlenderSync: Loading previous scene: {target_mesh_path}")
open_mesh(target_mesh_path)
if evt == BLENDERSYNC_EVENT_TARGET_AND_SURROUNDINGS_MESH_PATH_RECEIVED:
surroundings_obj_file_path = get_surroundings_mesh_file_path()
print(f"BasisBlenderSync: Importing surroundings from: {surroundings_obj_file_path}")
import_surroundings_mesh(surroundings_obj_file_path, False)
#print(f"Received target path: {target_mesh_path}, and surroundings path: {surroundings_obj_file_path}")
else:
surroundings_obj_file_path = ""
return server_update_rate
def get_target_mesh_file_path():
b = blender_sync_lib.getTargetMeshFilePath()
return b.decode('UTF-8')
def get_surroundings_mesh_file_path():
b = blender_sync_lib.getSurroundingsMeshFilePath()
return b.decode('UTF-8')
#############################
# Commented out, this is used for adding a panel to the "N-menu" on the right of the view.
# class VIEW3D_PT_blendersync(bpy.types.Panel):
# bl_space_type = "VIEW_3D"
# bl_region_type = "UI"
# bl_category = "BlenderSync"
# bl_label = "Basis BlenderSync"
# def draw(self, context):
# global server_running
# if server_running:
# self.layout.label(text="Server running", icon="FUND") # FUND is a heart.
# self.layout.operator("object.stop_blendersync_server")
# else:
# self.layout.label(text="Server stopped")
# self.layout.operator("object.start_blendersync_server")
# self.layout.separator()
# col = self.layout.column(align=True)
# col.enabled = server_running
# col.operator("object.finish_editing_blendersync_object")
class VIEW3D_PT_blendersync_button(bpy.types.Panel):
bl_space_type = "VIEW_3D"
bl_region_type = "HEADER"
#bl_category = "BlenderSync"
bl_label = "Basis BlenderSync"
@staticmethod
def draw_popover(self, context):
global server_running
global editing_mesh
self.layout.separator()
if editing_mesh:
# We use the ERROR icon here since it is a warning sign. There is no error, it's just to notify the
# user that we are editing the mesh at the moment.
self.layout.popover('VIEW3D_PT_blendersync_button', text="Basis BlenderSync", icon="FILE_3D")
else:
if server_running:
self.layout.popover('VIEW3D_PT_blendersync_button', text="Basis BlenderSync", icon="LINKED")
else:
self.layout.popover('VIEW3D_PT_blendersync_button', text="Basis BlenderSync", icon="UNLINKED")
def draw(self, context):
global server_running
global editing_mesh
layout = self.layout
if server_running:
layout.label(text="Server running", icon="LINKED")
layout.operator("object.stop_blendersync_server", icon="SNAP_FACE") # SNAP_FACE looks like a stop sign.
else:
layout.label(text="Server stopped", icon="UNLINKED")
layout.operator("object.start_blendersync_server", icon="PLAY")
if editing_mesh:
layout.separator()
col = layout.column(align=True)
col.label(text="Currently editing an object")
col.operator(FinishEditingBlenderSyncObject.bl_idname, text="Finish editing", icon="CHECKMARK")
col.operator(SendBlenderSyncDataAndKeepEditing.bl_idname, text="Update and keep editing", icon="UV_SYNC_SELECT")
col.separator()
col.operator(CancelBlenderSyncEdit.bl_idname, text="Cancel edit", icon="CANCEL")
col.separator()
col.operator(CreateHelpersCollectionOperator.bl_idname, text="Create helpers collection", icon="OUTLINER_COLLECTION")
##################################################################
class AddonPreferences(bpy.types.AddonPreferences):
bl_idname = get_addon_name()
server_port_number: bpy.props.IntProperty(name="Port number",
description="The UDP port number to start the server on.",
min=0,
max=65535,
default=1950)
basis_mesh_converter_path: bpy.props.StringProperty(name="Mesh converter exe path",
description="The path to the Basis mesh converter exe.",
default="")
def draw(self, context):
self.layout.label(text="These are the preferences for Basis BlenderSync.")
self.layout.prop(self, "server_port_number")
self.layout.prop(self, "basis_mesh_converter_path")
def get_addon_prefs(context):
preferences = context.preferences
addon_prefs = preferences.addons[get_addon_name()].preferences
return addon_prefs
##################################################################
# Operators for starting/stopping the server.
class StartServerOperator(bpy.types.Operator):
bl_idname = "object.start_blendersync_server"
bl_label = "Start Server"
def execute(self, context):
global server_running
global server_shutdown_requested
server_shutdown_requested = False
addon_prefs = get_addon_prefs(context)
if addon_prefs.basis_mesh_converter_path == "":
self.report({'ERROR'}, "The Basis mesh converter exe path is not set in the BlenderSync addon preferences.")
return {'FINISHED'}
print(f"Starting server on port {addon_prefs.server_port_number}")
s = blender_sync_lib.startServer(addon_prefs.server_port_number)
if s == 0:
# Kick off the server update loop. persistent=True means that
# the timer keeps running even after a new blend file is loaded.
bpy.app.timers.register(update_server, persistent=True)
server_running = True
else:
self.report({'ERROR'}, "The server failed to start.")
return {'FINISHED'}
class StopServerOperator(bpy.types.Operator):
bl_idname = "object.stop_blendersync_server"
bl_label = "Stop Server"
def execute(self, context):
global server_running
global server_shutdown_requested
server_shutdown_requested = True
server_running = False
return {'FINISHED'}
##################################################################
# Operator for sending the selected object back to Basis.
class FinishEditingBlenderSyncObject(bpy.types.Operator):
bl_idname = "object.finish_editing_blendersync_object"
bl_label = "Finish editing object"
def execute(self, context):
global server_running
global editing_mesh
if not server_running:
self.report({'ERROR'}, "Cannot send object to Basis, the server is not running.")
return {'FINISHED'}
keep_editing = False
if send_mesh_data_to_basis(self, context, keep_editing):
editing_mesh = False
return {'FINISHED'}
class SendBlenderSyncDataAndKeepEditing(bpy.types.Operator):
bl_idname = "object.send_blendersync_data_and_keep_editing"
bl_label = "Send data to Basis and keep editing"
def execute(self, context):
global server_running
if not server_running:
self.report({'ERROR'}, "Cannot send object to Basis, the server is not running.")
return {'FINISHED'}
keep_editing = True
send_mesh_data_to_basis(self, context, keep_editing)
return {'FINISHED'}
import bpy
class CancelBlenderSyncEdit(bpy.types.Operator):
bl_idname = "object.cancel_blendersync_edit"
bl_label = "Really cancel the edit?"
bl_options = {'REGISTER', 'INTERNAL'}
@classmethod
def poll(cls, context):
return True
def execute(self, context):
cancel_edit(self)
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_confirm(self, event)
class CreateHelpersCollectionOperator(bpy.types.Operator):
bl_idname = "object.create_blendersync_helper_collection"
bl_label = "Create Helpers Collection"
def execute(self, context):
create_helpers_collection()
return {'FINISHED'}
##################################################################
# # Operator for clearing the scene.
# class DeleteObjectsOperator(bpy.types.Operator):
# bl_idname = "object.blendersync_clear_scene"
# bl_label = "Delete Objects (BlenderSync)"
# def execute(self, context):
# if bpy.context.object.mode == 'EDIT':
# bpy.ops.object.mode_set(mode='OBJECT')
# # deselect all objects
# bpy.ops.object.select_all(action='SELECT')
# # delete all selected objects
# bpy.ops.object.delete()
# return {'FINISHED'}
# class TestCreateNewMesh(bpy.types.Operator):
# bl_idname = "object.test_blendersync_create_new_mesh"
# bl_label = "Test Create New BlenderSync Mesh"
# def execute(self, context):
# #print(context.selected_objects)
# create_new_mesh()
# return {'FINISHED'}
# class TestSaveBlenderSyncToTempFile(bpy.types.Operator):
# bl_idname = "object.test_blendersync_save_to_temp_file"
# bl_label = "Test Save Blender Sync To Temp File"
# def execute(self, context):
# finish_editing_mesh(self, context)
# return {'FINISHED'}
##################################################################
# Function for reading/writing BlenderSync meshes.
def create_helpers_collection():
# If the helpers collection isn't already there, create and link it.
if bpy.data.collections.get(BLENDERSYNC_HELPERS_COLLECTION_NAME) == None:
coll = bpy.data.collections.new(BLENDERSYNC_HELPERS_COLLECTION_NAME)
bpy.context.scene.collection.children.link(coll)
def create_new_mesh():
bpy.ops.wm.read_homefile(use_empty=True)
bpy.ops.mesh.primitive_cube_add()
target_obj = bpy.context.scene.objects[0]
target_obj.name = BLENDERSYNC_TARGET_OBJECT_NAME
target_obj.data.name = BLENDERSYNC_TARGET_OBJECT_DATA_NAME
set_target_object_rotation(target_obj)
def open_mesh(zipped_work_file_path):
containing_folder = str(pathlib.Path(zipped_work_file_path).parent)
archive = zipfile.ZipFile(zipped_work_file_path, 'r')
blend_file_path = archive.extract(BLENDERSYNC_ZIP_BLEND_ARCNAME, path=containing_folder)
#print(f"Extracted blend file: {blend_file_path}")
bpy.ops.wm.open_mainfile(filepath=blend_file_path)
target_obj = bpy.context.scene.objects.get(BLENDERSYNC_TARGET_OBJECT_NAME)
target_obj.select_set(True)
bpy.context.view_layer.objects.active = target_obj
set_target_object_rotation(target_obj)
print(f"BasisBlenderSync: Opened blend file: {blend_file_path}")
def import_surroundings_mesh(surroundings_obj_file_path, hide_surroundings):
# Activate the master collection.
bpy.context.view_layer.active_layer_collection = bpy.context.view_layer.layer_collection
# Use the new obj importer, starting with Blender 4.0.
if (bpy.app.version[0] >= 4):
# We need the context.temp_override stuff, otherwise we get
# 'RuntimeError: Operator bpy.ops.wm.obj_import.poll() failed, context is incorrect'
with bpy.context.temp_override(window=bpy.data.window_managers[0].windows[0]):
bpy.ops.wm.obj_import(filepath=surroundings_obj_file_path)
else:
bpy.ops.import_scene.obj(filepath=surroundings_obj_file_path)
helpers_coll = bpy.data.collections.get(BLENDERSYNC_HELPERS_COLLECTION_NAME)
# Any and all objects in the scene that are not the target object nor a
# helper object are considered to be part of the surroundings
for obj in bpy.context.scene.objects:
if (helpers_coll != None) and (obj in list(helpers_coll.objects)):
continue
if obj.name != BLENDERSYNC_TARGET_OBJECT_NAME:
obj.name = BLENDERSYNC_SURROUNDINGS_OBJECT_NAME
obj.hide_set(hide_surroundings)
#obj.hide_viewport = hide_surroundings
# Re-select the target object.
bpy.ops.object.select_all(action='DESELECT')
target_obj = bpy.context.scene.objects.get(BLENDERSYNC_TARGET_OBJECT_NAME)
target_obj.select_set(True)
bpy.context.view_layer.objects.active = target_obj
# Sends the mesh data to Basis and returns whether or not the operation succeeded.
def send_mesh_data_to_basis(op, context, keep_editing):
if bpy.context.object.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
surroundings_object_hidden = False
target_obj = bpy.context.scene.objects.get(BLENDERSYNC_TARGET_OBJECT_NAME)
surroundings_obj = bpy.context.scene.objects.get(BLENDERSYNC_SURROUNDINGS_OBJECT_NAME)
if not target_obj:
op.report({'ERROR'}, f"Could not find BlenderSync target object: '{BLENDERSYNC_TARGET_OBJECT_NAME}'")
return False
if surroundings_obj:
surroundings_object_hidden = not surroundings_obj.visible_get()
print(f"BasisBlenderSync: Surroundings object found, hidden: {surroundings_object_hidden}")
# Unhide all objects.
for obj in bpy.context.scene.objects:
obj.hide_set(False)
#obj.hide_viewport = False
world_origin = mathutils.Vector([0, 0, 0])
distance_to_origin = (target_obj.location - world_origin).length
if distance_to_origin > 0.0001:
op.report({'ERROR'}, "The target object pivot is not at the origin.")
return False
# Should be able to use this to "apply all transforms" to the target object if we want. (untested)
#bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
# Reset the rotation of the target object.
target_obj.rotation_mode = 'XYZ'
target_obj.rotation_euler = [0, 0, 0]
# If we are in local-view (numpad '/') we need to toggle out of it before proceeding.
# Otherwise the attempt to delete the surroundings object(s) fails.
if context.space_data.local_view is not None:
bpy.ops.view3d.localview()
#print("Toggled out of local-view.");
# Deselect all objects.
bpy.ops.object.select_all(action='DESELECT')
# If we have a helpers collection, select every object in it.
helpers_coll = bpy.data.collections.get(BLENDERSYNC_HELPERS_COLLECTION_NAME)
if not helpers_coll is None:
for helper_obj in helpers_coll.objects:
helper_obj.select_set(True)
# Make the target the active object.
#bpy.context.view_layer.objects.active = target_obj
# Select the target object and invert the selection.
target_obj.select_set(True)
bpy.ops.object.select_all(action='INVERT')
# Delete the selected object(s), ie. all objects except the target (and any helper objects).
bpy.ops.object.delete()
# Remove all unused data.
for block in bpy.data.meshes:
if block.users == 0:
bpy.data.meshes.remove(block)
for block in bpy.data.materials:
if block.users == 0:
bpy.data.materials.remove(block)
for block in bpy.data.images:
if block.users == 0:
bpy.data.images.remove(block)
for block in bpy.data.textures:
if block.users == 0:
bpy.data.textures.remove(block)
# Select the target object again.
target_obj.select_set(True)
# Generate a writable temp file name by creating a temp file and appending the extensions as appropriate.
fo = tempfile.NamedTemporaryFile()
temp_file_path = fo.name
fo.close()
work_file_path = temp_file_path + ".blend"
zipped_work_file_path = temp_file_path + ".zip"
exported_file_path = temp_file_path + ".fbx"
exported_file_meta_path = temp_file_path + ".fbx.meta"
converted_file_path = temp_file_path + ".binmesh"
converter_metadata_file_path = temp_file_path + ".meta"
# Save the blender scene to a temp file. copy=True saves the file but doesn't make it the active document.
bpy.ops.wm.save_as_mainfile(filepath=work_file_path, copy=True)
print(f"BasisBlenderSync: Work file saved to '{work_file_path}'")
# Export the target object to a temp fbx file.
# bpy.ops.export_scene.fbx(filepath='', check_existing=True, filter_glob='*.fbx', use_selection=False,
# use_active_collection=False, global_scale=1.0, apply_unit_scale=True, apply_scale_options='FBX_SCALE_NONE',
# use_space_transform=True, bake_space_transform=False,
# object_types={'ARMATURE', 'CAMERA', 'EMPTY', 'LIGHT', 'MESH', 'OTHER'}, use_mesh_modifiers=True,
# use_mesh_modifiers_render=True, mesh_smooth_type='OFF', use_subsurf=False, use_mesh_edges=False,
# use_tspace=False, use_custom_props=False, add_leaf_bones=True, primary_bone_axis='Y', secondary_bone_axis='X',
# use_armature_deform_only=False, armature_nodetype='NULL', bake_anim=True, bake_anim_use_all_bones=True,
# bake_anim_use_nla_strips=True, bake_anim_use_all_actions=True, bake_anim_force_startend_keying=True,
# bake_anim_step=1.0, bake_anim_simplify_factor=1.0, path_mode='AUTO', embed_textures=False, batch_mode='OFF',
# use_batch_own_dir=True, use_metadata=True, axis_forward='-Z', axis_up='Y')
bpy.ops.export_scene.fbx(filepath=exported_file_path,
use_selection=True, object_types={'MESH'}, axis_forward='Z')
print(f"BasisBlenderSync: Object exported to '{exported_file_path}'")
# Write a meta file to instruct the Basis mesh converter what to do:
# Example:
# {
# "vertexFormat":"PositionTangentBinormalNormalTexcoord",
# "generatePhysicsTriMesh":false,
# "linearVertexColors":false,
# "optimizeGeometry":true,
# "resourceTags":[]
# }
exported_mesh_metadata_json = '{"generatePhysicsTriMesh":true}'
with open(exported_file_meta_path, 'w') as outfile:
outfile.write(exported_mesh_metadata_json)
# Make sure we have the Basis mesh converter path set up.
addon_prefs = get_addon_prefs(context)
print(f"BasisBlenderSync: Mesh converter path: {addon_prefs.basis_mesh_converter_path}")
#s = blender_sync_lib.startServer(addon_prefs.server_port_number)
if addon_prefs.basis_mesh_converter_path == "":
op.report({'ERROR'}, "The Basis mesh converter exe path is not set in the BlenderSync addon preferences.")
return False
mesh_converter_path = pathlib.Path(addon_prefs.basis_mesh_converter_path)
if not mesh_converter_path.exists():
op.report({'ERROR'}, f"The Basis mesh converter exe path {addon_prefs.basis_mesh_converter_path} is not valid.")
return False
# Convert the fbx file to a binmesh.
conversion_command = f'{addon_prefs.basis_mesh_converter_path} -i:"{exported_file_path}" -o:"{converted_file_path}" -m:"{converter_metadata_file_path}" -p:physx'
conversion_result = subprocess.run(conversion_command, shell=True)
print(f"BasisBlenderSync: Object converted to '{converted_file_path}'")
if conversion_result.returncode != 0:
op.report({'ERROR'}, f"Error converting the object to a Basis mesh. Check the Blender console for more info.")
return False
# Write the blend file to a zip archive.
with zipfile.ZipFile(zipped_work_file_path, 'w') as zipObj:
zipObj.write(work_file_path, arcname=BLENDERSYNC_ZIP_BLEND_ARCNAME, compress_type=zipfile.ZIP_DEFLATED, compresslevel=9)
zipObj.write(exported_file_path, arcname=BLENDERSYNC_ZIP_FBX_ARCNAME, compress_type=zipfile.ZIP_DEFLATED, compresslevel=9)
# Remove the files we don't need anymore.
os.remove(work_file_path)
os.remove(exported_file_path)
workFileZipPathBytes = zipped_work_file_path.encode('UTF-8')
binmeshFilePathBytes = converted_file_path.encode('UTF-8')
if not server_running:
op.report({'ERROR'}, f"The BlenderSync server is not running. Cannot send the mesh data to Basis.")
return False
if blender_sync_lib.sendUpdatedMeshFilePaths(workFileZipPathBytes, len(workFileZipPathBytes),
binmeshFilePathBytes, len(binmeshFilePathBytes), keep_editing) == 0:
op.report({'INFO'}, "Successfully sent the mesh data to the Basis editor.")
else:
op.report({'ERROR'}, f"Error sending the mesh data to Basis. Is the client connected?")
if keep_editing:
# The user wants to keep editing.
open_mesh(zipped_work_file_path)
if surroundings_obj_file_path != "":
import_surroundings_mesh(surroundings_obj_file_path, surroundings_object_hidden)
else:
# The user does not want to keep editing. Clear the scene.
bpy.ops.wm.read_homefile(use_empty=True)
return True
def set_target_object_rotation(target_obj):
w = blender_sync_lib.getTargetObjectRotationW()
x = blender_sync_lib.getTargetObjectRotationX()
y = blender_sync_lib.getTargetObjectRotationY()
z = blender_sync_lib.getTargetObjectRotationZ()
print(f"BasisBlenderSync: Target rotation W: {w}")
print(f"BasisBlenderSync: Target rotation X: {x}")
print(f"BasisBlenderSync: Target rotation Y: {y}")
print(f"BasisBlenderSync: Target rotation Z: {z}")
target_obj.rotation_mode = 'QUATERNION'
# This is how we convert from a Basis quaternion to a Blender quaternion:
target_obj.rotation_quaternion = mathutils.Quaternion((-w, x, z, -y))
# Reset the rotation mode.
target_obj.rotation_mode = 'XYZ'
def cancel_edit(op):
global editing_mesh
editing_mesh = False
if blender_sync_lib.cancelEdit() == 0:
op.report({'INFO'}, "Successfully canceled the mesh editing.")
else:
op.report({'ERROR'}, f"Error sending the cancel message to Basis. Is the client connected?")
bpy.ops.wm.read_homefile(use_empty=True)
#include "BlenderSyncLibExport.h"
#include <enet/enet.h>
#include <stdio.h>
#include <stdint.h>
#include <string>
#include <fstream>
#include "../Basis/include/editor/BasisBlenderSync.h"
#define BLENDERSYNC_MAX_PATH_LENGTH 2048
#define BLENDERSYNC_RESPONSE_BUFFER_SIZE 2 * 1024
// These are the return values of the service() function:
#define BLENDERSYNC_EVENT_NONE 0
#define BLENDERSYNC_EVENT_CLIENT_CONNECTED 1
#define BLENDERSYNC_EVENT_CLIENT_DISCONNECTED 2
#define BLENDERSYNC_EVENT_TARGET_MESH_PATH_RECEIVED 3
#define BLENDERSYNC_EVENT_TARGET_AND_SURROUNDINGS_MESH_PATH_RECEIVED 4
// Longer timeouts:
#define BLENDERSYNC_PEER_TIMEOUT_LIMIT 32
#define BLENDERSYNC_PEER_TIMEOUT_MINIMUM 30000
#define BLENDERSYNC_PEER_TIMEOUT_MAXIMUM 120000
// The filesystem-based communication (which admittedly is a bit of a hack)
// can be completely left out by commenting out this define.
//#define BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
// Global state:
ENetHost* gEnetHost = nullptr;
ENetPeer* gEnetClientPeer = nullptr;
#ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
bool gRunningWithFilesystemComms = false;
#endif
uint32_t gEditID = 0xFFFFFFFF;
float gTargetObjectRotationW = 1.0f;
float gTargetObjectRotationX = 0.0f;
float gTargetObjectRotationY = 0.0f;
float gTargetObjectRotationZ = 0.0f;
char gTargetMeshFilePath[BLENDERSYNC_MAX_PATH_LENGTH + 1];
char gSurroundingsMeshFilePath[BLENDERSYNC_MAX_PATH_LENGTH + 1];
uint8_t gTempBuffer[BLENDERSYNC_RESPONSE_BUFFER_SIZE];
#ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
// Returns the path to the filesystem-based communication data file.
std::string getFSCommDataFilePath(bool clientToServer)
{
char tempFolderPath[MAX_PATH];
GetTempPathA(MAX_PATH, tempFolderPath);
std::string path;
path.append(tempFolderPath);
path.append(clientToServer ? BLENDERSYNC_FS_CLIENT_TO_SERVER_DATA_FILE_NAME : BLENDERSYNC_FS_SERVER_TO_CLIENT_DATA_FILE_NAME);
return path;
}
bool fileExists(const std::string& filePath)
{
bool found = false;
DWORD dwAttrib = GetFileAttributes(filePath.c_str());
found = (dwAttrib != INVALID_FILE_ATTRIBUTES) && !(dwAttrib & FILE_ATTRIBUTE_DIRECTORY);
return found;
}
// When using filesystem communication, this returns true if the client has sent data to the server.
// After returning true, the data can be found in gTempBuffer.
bool readDataFromClientFS(size_t& dataLength)
{
std::string filePath = getFSCommDataFilePath(true);
if (fileExists(filePath))
{
std::ifstream file(filePath.c_str(), std::ios::in | std::ios::binary | std::ios::ate);
if (!file.is_open())
{
return false;
}
dataLength = file.tellg();
file.seekg(0, std::ios::beg);
file.read((char*)gTempBuffer, dataLength);
file.close();
DeleteFile(filePath.c_str());
return true;
}
return false;
}
void writeDataToClientFS(uint8_t* data, size_t dataLength)
{
std::string filePath = getFSCommDataFilePath(false);
std::ofstream outfile(filePath.c_str(), std::ofstream::binary);
if (outfile.fail())
{
printf("ERROR writing data to file: %s\n", filePath.c_str());
return;
}
outfile.write((const char*)data, dataLength);
if (outfile.bad())
{
printf("ERROR writing data to file: %s\n", filePath.c_str());
return;
}
outfile.close();
}
#endif // BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
uint32_t readUint(enet_uint8* data, uint32_t index)
{
// This is little-endian:
return data[index] + (data[index + 1] << 8) + (data[index + 2] << 16) + (data[index + 3] << 24);
// This is big-endian:
//return (data[index] << 24) + (data[index + 1] << 16) + (data[index + 2] << 8) + data[index + 3];
}
void writeUint(enet_uint8* data, uint32_t index, uint32_t value)
{
// This is little-endian:
data[index + 0] = (value) & 0xFF;
data[index + 1] = (value >> 8) & 0xFF;
data[index + 2] = (value >> 16) & 0xFF;
data[index + 3] = (value >> 24) & 0xFF;
}
float readFloat(enet_uint8* data, uint32_t index)
{
union FloatUnion
{
float floatValue;
enet_uint8 bytes[4];
};
FloatUnion u;
for (int i = 0; i < 4; ++i)
u.bytes[i] = data[index + i];
return u.floatValue;
}
int parseData(enet_uint8* data, size_t dataLength)
{
/*
Data protocol:
Byte 0: Data packet type (uint8)
Bytes 1 - 4: Edit ID (uint32)
Bytes 5 - 8: Target object rotation W (float)
Bytes 9 - 12: Target object rotation X (float)
Bytes 13 - 16: Target object rotation Y (float)
Bytes 17 - 20: Target object rotation Z (float)
Bytes 21 - 24: Target mesh path length (uint32)
Bytes: 25 - N: Target mesh path (string data, UTF-8)
Bytes: N+1 - N+4: Surroundings mesh path length(uint32)
Bytes: N+5 - EOD: Surroundings mesh path (string data, UTF-8)
*/
uint8_t dataPacketType = data[0];
uint32_t cursor = 1; // Move to the second byte.
gEditID = readUint(data, cursor);
cursor += 4;
gTargetObjectRotationW = readFloat(data, cursor);
cursor += 4;
gTargetObjectRotationX = readFloat(data, cursor);
cursor += 4;
gTargetObjectRotationY = readFloat(data, cursor);
cursor += 4;
gTargetObjectRotationZ = readFloat(data, cursor);
cursor += 4;
if (dataPacketType == BLENDERSYNC_DATA_PACKET_TARGET_MESH_PATH ||
dataPacketType == BLENDERSYNC_DATA_PACKET_TARGET_AND_SURROUNDINGS_MESH_PATH)
{
// Target mesh path received:
uint32_t pathLength = readUint(data, cursor);
cursor += 4;
if (pathLength > 0)
{
strncpy_s(gTargetMeshFilePath, (const char*)(data + cursor), pathLength);
cursor += pathLength;
}
else
{
strcpy_s(gTargetMeshFilePath, sizeof(gTargetMeshFilePath), "");
}
if (dataPacketType == BLENDERSYNC_DATA_PACKET_TARGET_MESH_PATH)
{
return BLENDERSYNC_EVENT_TARGET_MESH_PATH_RECEIVED;
}
}
if (dataPacketType == BLENDERSYNC_DATA_PACKET_TARGET_AND_SURROUNDINGS_MESH_PATH)
{
// Target + surroundings mesh path received:
uint32_t pathLength = readUint(data, cursor);
cursor += 4;
strncpy_s(gSurroundingsMeshFilePath, (const char*)(data + cursor), pathLength);
cursor += pathLength;
return BLENDERSYNC_EVENT_TARGET_AND_SURROUNDINGS_MESH_PATH_RECEIVED;
}
return BLENDERSYNC_EVENT_NONE;
}
extern "C" BLENDERSYNCLIBAPI int initBlenderSyncLib()
{
if (enet_initialize() != 0)
{
return 1; // Error initializing ENet.
}
strcpy_s(gTargetMeshFilePath, sizeof(gTargetMeshFilePath), "");
strcpy_s(gSurroundingsMeshFilePath, sizeof(gSurroundingsMeshFilePath), "");
return 0;
}
extern "C" BLENDERSYNCLIBAPI void deinitBlenderSyncLib()
{
enet_deinitialize();
}
extern "C" BLENDERSYNCLIBAPI int startServer(int port)
{
#ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
gRunningWithFilesystemComms = false;
#endif
ENetAddress addr;
addr.host = ENET_HOST_ANY;
addr.port = port;
size_t maxPeerCount = 1;
size_t channelLimit = 0; // 0 means maximum number of allowed channels
gEnetHost = enet_host_create(&addr, maxPeerCount, channelLimit, 0, 0);
if (!gEnetHost)
{
return 1; // Error.
}
return 0; // Success.
}
#ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
extern "C" BLENDERSYNCLIBAPI int startServerFS()
{
gRunningWithFilesystemComms = true;
// Delete any communications data files from a previous session, left in the temp folder.
{
std::string filePath = getFSCommDataFilePath(true);
if (fileExists(filePath)) DeleteFile(filePath.c_str());
}
{
std::string filePath = getFSCommDataFilePath(false);
if (fileExists(filePath)) DeleteFile(filePath.c_str());
}
return 0; // Success.
}
#endif
extern "C" BLENDERSYNCLIBAPI void stopServer()
{
#ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
if (!gRunningWithFilesystemComms && !gEnetHost)
#else
if (!gEnetHost)
#endif
{
return;
}
if (gEnetClientPeer)
{
ENetEvent event;
enet_peer_disconnect(gEnetClientPeer, 0);
// Allow up to 3 seconds for the disconnect to succeed and drop any packets received packets.
while (enet_host_service(gEnetHost, &event, 3000) > 0)
{
switch (event.type)
{
case ENET_EVENT_TYPE_RECEIVE:
enet_packet_destroy(event.packet);
break;
case ENET_EVENT_TYPE_DISCONNECT:
gEnetClientPeer = nullptr;
break;
}
}
if (gEnetClientPeer)
{
// We've arrived here, so the disconnect attempt didn't succeed yet. Force the connection down.
enet_peer_reset(gEnetClientPeer);
gEnetClientPeer = nullptr;
}
}
if (gEnetHost)
{
enet_host_destroy(gEnetHost);
gEnetHost = nullptr;
}
#ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
gRunningWithFilesystemComms = false;
#endif
}
extern "C" BLENDERSYNCLIBAPI int isServerRunning()
{
#ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
return (gRunningWithFilesystemComms || gEnetHost != nullptr) ? 1 : 0;
#else
return (gEnetHost != nullptr) ? 1 : 0;
#endif
}
extern "C" BLENDERSYNCLIBAPI float getTargetObjectRotationW()
{
return gTargetObjectRotationW;
}
extern "C" BLENDERSYNCLIBAPI float getTargetObjectRotationX()
{
return gTargetObjectRotationX;
}
extern "C" BLENDERSYNCLIBAPI float getTargetObjectRotationY()
{
return gTargetObjectRotationY;
}
extern "C" BLENDERSYNCLIBAPI float getTargetObjectRotationZ()
{
return gTargetObjectRotationZ;
}
extern "C" BLENDERSYNCLIBAPI const char* getTargetMeshFilePath()
{
return gTargetMeshFilePath;
}
extern "C" BLENDERSYNCLIBAPI const char* getSurroundingsMeshFilePath()
{
return gSurroundingsMeshFilePath;
}
extern "C" BLENDERSYNCLIBAPI int sendUpdatedMeshFilePaths(const char* workFileZipPath, int workFileZipPathLength, const char* binmeshFilePath, int binmeshFilePathLength, bool keepEditing)
{
printf("BlenderSyncLib: workFileZipPath: %s, length: %d\n", workFileZipPath, workFileZipPathLength);
printf("BlenderSyncLib: binmeshFilePath: %s, length: %d\n", binmeshFilePath, binmeshFilePathLength);
#ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
if (gRunningWithFilesystemComms || gEnetClientPeer)
#else
if (gEnetClientPeer)
#endif
{
gTempBuffer[0] = BLENDERSYNC_DATA_PACKET_TARGET_MESH_UPDATED;
uint32_t cursor = 1; // Move to the second byte.
writeUint(gTempBuffer, cursor, gEditID);
cursor += 4;
// One byte specifying whether or not to keep editing.
gTempBuffer[cursor] = keepEditing ? 1 : 0;
cursor += 1;
// Writing the paths prepended with the length as uint32_ts means that we can "get" them out of the stream as Basis strings on the C++ side.
writeUint(gTempBuffer, cursor, (uint32_t)workFileZipPathLength);
cursor += 4;
memcpy_s(gTempBuffer + cursor, BLENDERSYNC_RESPONSE_BUFFER_SIZE - cursor, workFileZipPath, workFileZipPathLength);
cursor += workFileZipPathLength;
writeUint(gTempBuffer, cursor, (uint32_t)binmeshFilePathLength);
cursor += 4;
memcpy_s(gTempBuffer + cursor, BLENDERSYNC_RESPONSE_BUFFER_SIZE - cursor, binmeshFilePath, binmeshFilePathLength);
cursor += binmeshFilePathLength;
#ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
if (gRunningWithFilesystemComms)
{
writeDataToClientFS(gTempBuffer, cursor);
}
else
#endif
{
ENetPacket* packet = enet_packet_create(gTempBuffer, cursor, ENET_PACKET_FLAG_RELIABLE);
enet_peer_send(gEnetClientPeer, 0, packet);
enet_host_flush(gEnetHost);
}
if (!keepEditing)
{
// The edit is officially "over" now, so reset the ID.
gEditID = 0xFFFFFFFF;
}
return 0;
}
return 1; // No peer connected, cannot send.
}
extern "C" BLENDERSYNCLIBAPI int cancelEdit()
{
#ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
if (gRunningWithFilesystemComms || gEnetClientPeer)
#else
if (gEnetClientPeer)
#endif
{
printf("BlenderSyncLib: cancelEdit()\n");
gTempBuffer[0] = BLENDERSYNC_DATA_PACKET_CANCEL_EDIT;
uint32_t cursor = 1; // Move to the second byte.
writeUint(gTempBuffer, cursor, gEditID);
cursor += 4;
#ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
if (gRunningWithFilesystemComms)
{
writeDataToClientFS(gTempBuffer, cursor);
}
else
#endif
{
ENetPacket* packet = enet_packet_create(gTempBuffer, cursor, ENET_PACKET_FLAG_RELIABLE);
enet_peer_send(gEnetClientPeer, 0, packet);
enet_host_flush(gEnetHost);
}
gEditID = 0xFFFFFFFF;
return 0;
}
return 1; // No peer connected, cannot send.
}
extern "C" BLENDERSYNCLIBAPI int service()
{
#ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
if (gRunningWithFilesystemComms)
{
size_t dataLength = 0;
if (readDataFromClientFS(dataLength))
{
printf("BlenderSyncLib: Data received (via filesystem), %u bytes\n", (int)dataLength);
int returnCode = parseData(gTempBuffer, dataLength);
return returnCode;
}
}
else
#endif
if (gEnetHost)
{
ENetEvent enetEvent;
uint32_t timeoutMs = 0;
int res = enet_host_service(gEnetHost, &enetEvent, timeoutMs);
if (res > 0)
{
switch (enetEvent.type)
{
case ENET_EVENT_TYPE_CONNECT:
gEnetClientPeer = enetEvent.peer;
// Set longer timeouts.
enet_peer_timeout(gEnetClientPeer, BLENDERSYNC_PEER_TIMEOUT_LIMIT,
BLENDERSYNC_PEER_TIMEOUT_MINIMUM, BLENDERSYNC_PEER_TIMEOUT_MAXIMUM);
printf("BlenderSyncLib: Client connected.\n");
return BLENDERSYNC_EVENT_CLIENT_CONNECTED;
case ENET_EVENT_TYPE_DISCONNECT:
gEnetClientPeer = nullptr;
printf("BlenderSyncLib: Client disconnected.\n");
return BLENDERSYNC_EVENT_CLIENT_DISCONNECTED;
case ENET_EVENT_TYPE_RECEIVE:
{
printf("BlenderSyncLib: Data received, %u bytes\n", (int)enetEvent.packet->dataLength);
int returnCode = parseData(enetEvent.packet->data, enetEvent.packet->dataLength);
enet_packet_destroy(enetEvent.packet);
return returnCode;
}
default:
return BLENDERSYNC_EVENT_NONE;
}
}
else //if (res == 0)
{
return BLENDERSYNC_EVENT_NONE;
}
/*else
{
// A negative value indicates an error.
}*/
}
return BLENDERSYNC_EVENT_NONE;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment