Код аддона для блендер 3.3.0:

bl_info = {
    "name": "F1 Track CTF Renamer",
    "blender": (3, 3, 0),
    "category": "3D View",
    "description": "Quickly rename .ctf files.",
    "author": "AlonsoBeast",
    "version": (1, 1),
    "support": "COMMUNITY",
}

import bpy
import os
import shutil
import subprocess

class F1TrackCTFRenamerPanel(bpy.types.Panel):
    """Creates a panel for the addon"""
    bl_label = "F1 Track CTF Renamer"
    bl_idname = "PT_F1TrackCTFRenamer"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'F1 Track DataEditor'

    def draw(self, context):
        layout = self.layout
        scene = context.scene

        # Оборачиваем в box для туториала
        layout.prop(scene, "tutorial_expanded", text="Tutorial", icon='INFO', emboss=True)
        if scene.tutorial_expanded:
            box = layout.box()
            box.label(text="1. Select F1 directory cars/ folder")
            box.label(text="2. Select output folder to export")
            box.label(text="3. Rename source track name .ctf file to new one")

        layout.prop(context.scene, "source_directory", text="Source Directory")

        row = layout.row()
        row.operator("f1.track_ctf_select_source_directory", text="Select Source Directory", icon='OUTLINER_COLLECTION')

        layout.prop(context.scene, "output_directory", text="Output Directory")

        row = layout.row()
        row.operator("f1.track_ctf_select_directory", text="Select Output Directory", icon='FOLDER_REDIRECT')

        layout.prop(context.scene, "ctf_track_name", text="New Track Name")

        layout.prop(context.scene, "search_name", text="Source Track Name")

        row = layout.row()
        row.operator("f1.track_ctf_rename", text="Rename CTF Files", icon='GREASEPENCIL')

class F1TrackCTFSelectDirectory(bpy.types.Operator):
    """Operator to select the output directory"""
    bl_idname = "f1.track_ctf_select_directory"
    bl_label = "Select Output Directory"

    directory: bpy.props.StringProperty(subtype="DIR_PATH")

    def execute(self, context):
        context.scene.output_directory = self.directory
        print(f"Selected Output Directory: {context.scene.output_directory}")
        return {'FINISHED'}

    def invoke(self, context, event):
        self.directory = context.scene.output_directory
        context.window_manager.fileselect_add(self)
        return {'RUNNING_MODAL'}

class F1TrackCTFSelectSourceDirectory(bpy.types.Operator):
    """Operator to select the source directory"""
    bl_idname = "f1.track_ctf_select_source_directory"
    bl_label = "Select Source Directory"

    directory: bpy.props.StringProperty(subtype="DIR_PATH")

    def execute(self, context):
        context.scene.source_directory = self.directory
        print(f"Selected Source Directory: {context.scene.source_directory}")
        return {'FINISHED'}

    def invoke(self, context, event):
        self.directory = context.scene.source_directory
        context.window_manager.fileselect_add(self)
        return {'RUNNING_MODAL'}

class F1TrackCTFRenamerOperator(bpy.types.Operator):
    """Operator to rename .ctf files"""
    bl_idname = "f1.track_ctf_rename"
    bl_label = "Rename CTF Files"

    def execute(self, context):
        output_dir = context.scene.output_directory
        source_dir = context.scene.source_directory
        ctf_track_name = context.scene.ctf_track_name
        search_name = context.scene.search_name 

        if not output_dir:
            self.report({'ERROR'}, "Output Directory must be set.")
            return {'CANCELLED'}

        if not ctf_track_name:
            self.report({'ERROR'}, "Track Name must be set.")
            return {'CANCELLED'}

        if not search_name:
            self.report({'ERROR'}, "Original Track Name must be set.")  
            return {'CANCELLED'}

        if not source_dir:
            self.report({'ERROR'}, "Source Directory must be set.")
            return {'CANCELLED'}

        if not os.path.exists(source_dir):
            self.report({'ERROR'}, f"Source directory does not exist: {source_dir}")
            return {'CANCELLED'}

        if not os.path.exists(output_dir):
            os.makedirs(output_dir)

        cars = ["ca2", "fc2", "fe2", "lr2", "ma2", "mc2", "me2", "rb2", "sa2", "sc2", "tr2", "wi2"]

        for car in cars:
            car_source_dir = os.path.join(source_dir, car)

            if os.path.exists(car_source_dir):
                for root, dirs, files in os.walk(car_source_dir):
                    for file_name in files:
                        if file_name.endswith(".ctf") and search_name in file_name:  # Используем search_name
                            new_file_name = file_name.replace(search_name, ctf_track_name)
                            
                            source_file_path = os.path.join(root, file_name)
                            
                            relative_path = os.path.relpath(root, start=car_source_dir)
                            output_car_directory = os.path.join(output_dir, car, relative_path)
                            os.makedirs(output_car_directory, exist_ok=True)
                            
                            destination_file_path = os.path.join(output_car_directory, new_file_name)
                            
                            shutil.copy2(source_file_path, destination_file_path)
                            print(f"Copied {source_file_path} to {destination_file_path}")

            else:
                self.report({'ERROR'}, f"Car directory does not exist: {car_source_dir}")
                return {'CANCELLED'}

        print("Renaming .ctf and duplication to a new directory was successful.")

        if os.name == 'nt':
            subprocess.Popen(f'explorer {os.path.realpath(output_dir)}')

        return {'FINISHED'}

def register():
    bpy.utils.register_class(F1TrackCTFRenamerPanel)
    bpy.utils.register_class(F1TrackCTFSelectDirectory)
    bpy.utils.register_class(F1TrackCTFSelectSourceDirectory)
    bpy.utils.register_class(F1TrackCTFRenamerOperator)

    bpy.types.Scene.ctf_track_name = bpy.props.StringProperty(name="Set Your New Track Name", default="")
    bpy.types.Scene.output_directory = bpy.props.StringProperty(name="Output Directory", default="")
    bpy.types.Scene.source_directory = bpy.props.StringProperty(name="Select inside cars folder", default="")
    bpy.types.Scene.search_name = bpy.props.StringProperty(name="Original track .ctf file to rename, example: melbourne", default="melbourne")  
    bpy.types.Scene.tutorial_expanded = bpy.props.BoolProperty(name="Tutorial Expanded", default=False)  # Новое свойство

def unregister():
    bpy.utils.unregister_class(F1TrackCTFRenamerPanel)
    bpy.utils.unregister_class(F1TrackCTFSelectDirectory)
    bpy.utils.unregister_class(F1TrackCTFSelectSourceDirectory)
    bpy.utils.unregister_class(F1TrackCTFRenamerOperator)

    del bpy.types.Scene.ctf_track_name
    del bpy.types.Scene.output_directory
    del bpy.types.Scene.source_directory
    del bpy.types.Scene.search_name 
    del bpy.types.Scene.tutorial_expanded  # Удаление свойства

if __name__ == "__main__":
    register()