Skip to content

Instantly share code, notes, and snippets.

@ArnCarveris
Forked from unvestigate/BlenderSyncLib.cpp
Last active August 6, 2025 16:28
Show Gist options
  • Select an option

  • Save ArnCarveris/469c100cff06b17678eeccbb402272ba to your computer and use it in GitHub Desktop.

Select an option

Save ArnCarveris/469c100cff06b17678eeccbb402272ba to your computer and use it in GitHub Desktop.
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