├── .gitignore ├── screenshot.png ├── TODO.md ├── .github └── FUNDING.yml ├── constants.py ├── README.md ├── utils.py ├── FORMAT.md ├── qnorm.py ├── quakepal.py ├── hexen2pal.py ├── qfplist.py ├── __init__.py ├── export_mdl.py ├── mdl.py ├── import_mdl.py └── version_cheatsheet.md /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | 3 | .DS_Store 4 | .vscode 5 | version_cheatsheet.md 6 | models -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victorfeitosa/quake-hexen2-mdl-export-import/HEAD/screenshot.png -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ### DONE ### 2 | - [x] Fix compatbility issues with Blender 6.8 3 | - [x] Make a palette picker menu when importing/exporting 4 | - [x] Fix eye position scaling 5 | - [x] Add support for Hexen II flags 6 | 7 | ### BUG FIXES ### 8 | - [ ] Fix import and export scaling to not rely on blender mesh resize 9 | 10 | 11 | ### IMPPROVEMENTS ### 12 | - [ ] Add option to define how many frames to export 13 | - [ ] Refactor code for speed 14 | 15 | ### NEW FEATURES ### 16 | - [ ] Add support for custom palettes 17 | - [ ] Add support for boilerplate QuakeC 18 | 19 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | #github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | #patreon: # Replace with a single Patreon username 5 | #open_collective: # Replace with a single Open Collective username 6 | ko_fi: victorfeitosa 7 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: victorf 10 | #issuehunt: # Replace with a single IssueHunt username 11 | #otechie: # Replace with a single Otechie username 12 | #custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /constants.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class MDLSyncType(IntEnum): 5 | ST_SYNC = 0 6 | ST_RAND = 1 7 | 8 | 9 | class MDLEffects(IntEnum): 10 | # Quake and general MDL Effects 11 | EF_ROCKET = 1 12 | EF_GRENADE = 2 13 | EF_GIB = 4 14 | EF_ROTATE = 8 15 | EF_TRACER = 16 16 | EF_ZOMGIB = 32 17 | EF_TRACER2 = 64 18 | EF_TRACER3 = 128 19 | 20 | # Hexen II Effects 21 | EF_FIREBALL = 256 22 | EF_ICE = 512 23 | EF_MIP_MAP = 1024 24 | EF_SPIT = 2048 25 | EF_TRANSPARENT = 4096 26 | EF_SPELL = 8192 27 | EF_HOLEY = 16384 28 | EF_SPECIAL_TRANS = 32768 29 | EF_FACE_VIEW = 65536 30 | EF_VORP_MISSILE = 131072 31 | EF_SET_STAFF = 262144 32 | EF_MAGICMISSILE = 524288 33 | EF_BONESHARD = 1048576 34 | EF_SCARAB = 2097152 35 | EF_ACIDBALL = 4194304 36 | EF_BLOODSHOT = 8388608 37 | EF_MIP_MAP_FAR = 16777216 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quake and Hexen II MDL Importer and Exporter for Blender 2.8+ 2 | Import and Export ID's Quake and Ravensoft's Hexen II MDL models to and from Blender 2.8+ 3 | 4 | # Download 5 | Check out the latest versions on the Releases page 6 | 7 | # About 8 | ![Importer in action](screenshot.png) 9 | 10 | This model exporter/importer is a fork of [Blender MDL import/export](https://en.blender.org/index.php/Extensions:2.6/Py/Scripts/Import-Export/Quake_mdl) by QuakeForge. 11 | 12 | This versions is updated to work with Blender 2.8 or greater and has some new features such as support for Hexen II palette. 13 | Some cues were taken from [Khreator](https://twitter.com/khreathor) adaptation to MDL Exporter for Blender 2.8+. It can be found [here](https://bitbucket.org/khreathor/mdl-for-blender/wiki/Home). 14 | 15 | # How to Contribute 16 | ## On Github 17 | - Create issues with a description and a minimum of steps to reproduce if it's a bug 18 | - Submit feature requests 19 | - Contact me to contribute with pull requests 20 | ## Buy me coffee ☕️ 21 | [ko-fi page](https://ko-fi.com/victorfeitosa) 22 | 23 | # Roadmap 24 | ## Latest new features added 25 | - Import/Export scale adjusts for the model and the `eyeposition` tag 26 | - Support for Hexen II palette on the Import/Export file menu 27 | - Better support for skins 28 | - Emissive material added automatically 29 | - Support for Hexen II Portals of Praevus model format 30 | - Added import and export support for Hexen II model flags 31 | 32 | ## TO DO 33 | - Add the option to exclude brightmap colours from the texture conversion process, approximating textures colours to only the non-bright palette colours 34 | - Add support for custom palettes 35 | - Add support for boilerplate QuakeC 36 | - Major refactor code for speed and organization 37 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | from struct import unpack, pack 3 | 4 | 5 | def getPaletteFromName(palette_name): 6 | palette_module_name = "..{0}pal".format(palette_name.lower()) 7 | palette = importlib.import_module(palette_module_name, __name__).palette 8 | return palette 9 | 10 | 11 | # File reading and writing utils 12 | # Reading 13 | def read_byte(file, count=1): 14 | size = 1 * count 15 | data = file.read(size) 16 | data = unpack("<%dB" % count, data) 17 | if count == 1: 18 | return data[0] 19 | return data 20 | 21 | 22 | def read_int(file, count=1): 23 | size = 4 * count 24 | data = file.read(size) 25 | data = unpack("<%di" % count, data) 26 | if count == 1: 27 | return data[0] 28 | return data 29 | 30 | 31 | def read_ushort(file, count=1): 32 | size = 2 * count 33 | data = file.read(size) 34 | data = unpack("<%dH" % count, data) 35 | if count == 1: 36 | return data[0] 37 | return data 38 | 39 | 40 | def read_float(file, count=1): 41 | size = 4 * count 42 | data = file.read(size) 43 | data = unpack("<%df" % count, data) 44 | if count == 1: 45 | return data[0] 46 | return data 47 | 48 | 49 | def read_bytestring(file, size): 50 | return file.read(size) 51 | 52 | 53 | def read_string(file, size): 54 | data = file.read(size) 55 | s = "" 56 | for c in data: 57 | s = s + chr(c) 58 | return s 59 | 60 | 61 | # Writing 62 | def write_byte(file, data): 63 | if not hasattr(data, "__len__"): 64 | data = (data,) 65 | file.write(pack(("<%dB" % len(data)), *data)) 66 | 67 | 68 | def write_int(file, data): 69 | if not hasattr(data, "__len__"): 70 | data = (data,) 71 | file.write(pack(("<%di" % len(data)), *data)) 72 | 73 | 74 | def write_float(file, data): 75 | if not hasattr(data, "__len__"): 76 | data = (data,) 77 | file.write(pack(("<%df" % len(data)), *data)) 78 | 79 | 80 | def write_bytestring(file, data, size=-1): 81 | if size == -1: 82 | size = len(data) 83 | file.write(data[:size]) 84 | if size > len(data): 85 | file.write(bytes(size - len(data))) 86 | 87 | 88 | def write_string(file, data, size=-1): 89 | data = data.encode() 90 | write_bytestring(file, data, size) 91 | -------------------------------------------------------------------------------- /FORMAT.md: -------------------------------------------------------------------------------- 1 | ### MDL header 2 | ```c 3 | struct mdl_header_t 4 | { 5 | int ident; /* magic number: "IDPO" */ 6 | int version; /* version: 6 */ 7 | 8 | vec3_t scale; /* scale factor */ 9 | vec3_t translate; /* translation vector */ 10 | float boundingradius; 11 | vec3_t eyeposition; /* eyes' position */ 12 | 13 | int num_skins; /* number of textures */ 14 | int skinwidth; /* texture width */ 15 | int skinheight; /* texture height */ 16 | 17 | int num_verts; /* number of vertices */ 18 | int num_tris; /* number of triangles */ 19 | int num_frames; /* number of frames */ 20 | 21 | int synctype; /* 0 = synchron, 1 = random */ 22 | int flags; /* state flag */ 23 | float size; 24 | int numstverts; /* only present in Hexen II expansion pack mdl files */ 25 | }; 26 | ``` 27 | 28 | ### Skin 29 | ```c 30 | struct mdl_skin_t 31 | { 32 | int group; /* 0 = single, 1 = group */ 33 | GLubyte *data; /* texture data */ 34 | }; 35 | ``` 36 | 37 | 38 | ### Group of skins 39 | ```c 40 | struct mdl_groupskin_t 41 | { 42 | int group; /* 1 = group */ 43 | int nb; /* number of pics */ 44 | float *time; /* time duration for each pic */ 45 | ubyte **data; /* texture data */ 46 | }; 47 | ``` 48 | 49 | ### Texture coords 50 | ```c 51 | struct mdl_texcoord_t 52 | { 53 | int onseam; 54 | int s; 55 | int t; 56 | }; 57 | ``` 58 | 59 | ### Triangle info 60 | ```c 61 | struct mdl_triangle_t 62 | { 63 | int facesfront; /* 0 = backface, 1 = frontface */ 64 | int vertex[3]; /* vertex indices */ 65 | }; 66 | ``` 67 | 68 | ### Hexen II Expansion Pack Triangle info 69 | ```c 70 | struct mdl_triangle_t 71 | { 72 | int facesfront; /* 0 = backface, 1 = frontface */ 73 | short vertex[3]; /* vertex indices */ 74 | short stindex[3]; /* uv indices */ 75 | }; 76 | ``` 77 | 78 | ### Compressed vertex 79 | ```c 80 | struct mdl_vertex_t 81 | { 82 | unsigned char v[3]; 83 | unsigned char normalIndex; 84 | }; 85 | ``` 86 | 87 | ### Simple frame 88 | ```c 89 | struct mdl_simpleframe_t 90 | { 91 | struct mdl_vertex_t bboxmin; /* bouding box min */ 92 | struct mdl_vertex_t bboxmax; /* bouding box max */ 93 | char name[16]; 94 | struct mdl_vertex_t *verts; /* vertex list of the frame */ 95 | }; 96 | ``` 97 | 98 | ### Group of simple frames 99 | ```c 100 | struct mdl_groupframe_t 101 | { 102 | int type; /* !0 = group */ 103 | struct mdl_vertex_t min; /* min pos in all simple frames */ 104 | struct mdl_vertex_t max; /* max pos in all simple frames */ 105 | float *time; /* time duration for each frame */ 106 | struct mdl_simpleframe_t *frames; /* simple frame list */ 107 | }; 108 | ``` 109 | 110 | -------------------------------------------------------------------------------- /qnorm.py: -------------------------------------------------------------------------------- 1 | # vim:ts=4:et 2 | # ##### BEGIN GPL LICENSE BLOCK ##### 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software Foundation, 16 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 17 | # 18 | # ##### END GPL LICENSE BLOCK ##### 19 | 20 | # 21 | 22 | from mathutils import Vector 23 | 24 | # Covert normals to quake's normal palette. Implementation taken from ajmdl 25 | # 26 | # AJA: I use the following shortcuts to speed up normal lookup: 27 | # 28 | # Firstly, a preliminary match only uses the first quadrant 29 | # (where all coords are >= 0). Then we use the appropriate 30 | # normal index for the actual quadrant. We can do this because 31 | # the 162 MDL/MD2 normals are not arbitrary but are mirrored in 32 | # every quadrant. The eight numbers in the lists above are the 33 | # indices for each quadrant (+++ ++- +-+ +-- -++ -+- --+ ---). 34 | # 35 | # Secondly we use the axis with the greatest magnitude (of the 36 | # incoming normal) to search an axis-specific group, which means 37 | # we only need to check about 1/3rd of the normals. 38 | # Actually, about 1/14th (taniwha) 39 | x_group = ( 40 | (Vector((1.0000, 0.0000, 0.0000)), (52, 52, 52, 52, 143, 143, 143, 143)), 41 | (Vector((0.9554, 0.2952, 0.0000)), (51, 51, 55, 55, 141, 141, 145, 145)), 42 | (Vector((0.9511, 0.1625, 0.2629)), (53, 63, 57, 70, 142, 148, 146, 151)), 43 | (Vector((0.8642, 0.4429, 0.2389)), (46, 61, 56, 69, 19, 147, 123, 150)), 44 | (Vector((0.8507, 0.5257, 0.0000)), (41, 41, 54, 54, 18, 18, 116, 116)), 45 | (Vector((0.8507, 0.0000, 0.5257)), (60, 67, 60, 67, 144, 155, 144, 155)), 46 | (Vector((0.8090, 0.3090, 0.5000)), (48, 62, 58, 68, 16, 149, 124, 152)), 47 | (Vector((0.7166, 0.6817, 0.1476)), (42, 43, 111, 100, 20, 25, 118, 117)), 48 | (Vector((0.6882, 0.5878, 0.4253)), (47, 76, 140, 101, 21, 156, 125, 161)), 49 | (Vector((0.6817, 0.1476, 0.7166)), (49, 65, 59, 66, 15, 153, 126, 154)), 50 | (Vector((0.5878, 0.4253, 0.6882)), (50, 75, 139, 102, 17, 157, 128, 160)) 51 | ) 52 | y_group = ( 53 | (Vector((0.0000, 1.0000, 0.0000)), (32, 32, 104, 104, 32, 32, 104, 104)), 54 | (Vector((0.0000, 0.9554, 0.2952)), (33, 30, 107, 103, 33, 30, 107, 103)), 55 | (Vector((0.2629, 0.9511, 0.1625)), (36, 39, 109, 105, 34, 31, 122, 115)), 56 | (Vector((0.2389, 0.8642, 0.4429)), (35, 38, 108, 97, 23, 29, 121, 113)), 57 | (Vector((0.5257, 0.8507, 0.0000)), (44, 44, 112, 112, 27, 27, 119, 119)), 58 | (Vector((0.0000, 0.8507, 0.5257)), (6, 28, 106, 90, 6, 28, 106, 90)), 59 | (Vector((0.5000, 0.8090, 0.3090)), (37, 40, 110, 98, 22, 26, 120, 114)), 60 | (Vector((0.1476, 0.7166, 0.6817)), (8, 71, 136, 92, 7, 77, 130, 91)), 61 | (Vector((0.4253, 0.6882, 0.5878)), (45, 73, 138, 99, 24, 158, 131, 159)), 62 | (Vector((0.7166, 0.6817, 0.1476)), (42, 43, 111, 100, 20, 25, 118, 117)), 63 | (Vector((0.6882, 0.5878, 0.4253)), (47, 76, 140, 101, 21, 156, 125, 161)) 64 | ) 65 | z_group = ( 66 | (Vector((0.0000, 0.0000, 1.0000)), (5, 84, 5, 84, 5, 84, 5, 84)), 67 | (Vector((0.2952, 0.0000, 0.9554)), (12, 85, 12, 85, 2, 82, 2, 82)), 68 | (Vector((0.1625, 0.2629, 0.9511)), (14, 86, 134, 96, 4, 83, 132, 89)), 69 | (Vector((0.4429, 0.2389, 0.8642)), (13, 74, 133, 95, 1, 81, 127, 87)), 70 | (Vector((0.5257, 0.0000, 0.8507)), (11, 64, 11, 64, 0, 80, 0, 80)), 71 | (Vector((0.0000, 0.5257, 0.8507)), (9, 79, 137, 93, 9, 79, 137, 93)), 72 | (Vector((0.3090, 0.5000, 0.8090)), (10, 72, 135, 94, 3, 78, 129, 88)), 73 | (Vector((0.6817, 0.1476, 0.7166)), (49, 65, 59, 66, 15, 153, 126, 154)), 74 | (Vector((0.5878, 0.4253, 0.6882)), (50, 75, 139, 102, 17, 157, 128, 160)), 75 | (Vector((0.1476, 0.7166, 0.6817)), (8, 71, 136, 92, 7, 77, 130, 91)), 76 | (Vector((0.4253, 0.6882, 0.5878)), (45, 73, 138, 99, 24, 158, 131, 159)) 77 | ) 78 | 79 | 80 | def map_normal(n): 81 | fn = Vector((abs(n.x), abs(n.y), abs(n.z))) 82 | group = x_group 83 | if fn.y > fn.x and fn.y > fn.z: 84 | group = y_group 85 | if fn.z > fn.x and fn.z > fn.y: 86 | group = z_group 87 | best = 0 88 | best_dot = -1 89 | for i in range(len(group)): 90 | dot = group[i][0].dot(fn) 91 | if dot > best_dot: 92 | best = i 93 | best_dot = dot 94 | quadrant = 0 95 | if n.x < 0: 96 | quadrant += 4 97 | if n.y < 0: 98 | quadrant += 2 99 | if n.z < 0: 100 | quadrant += 1 101 | return group[best][1][quadrant] 102 | -------------------------------------------------------------------------------- /quakepal.py: -------------------------------------------------------------------------------- 1 | palette = ( 2 | (0x00, 0x00, 0x00), 3 | (0x0f, 0x0f, 0x0f), 4 | (0x1f, 0x1f, 0x1f), 5 | (0x2f, 0x2f, 0x2f), 6 | (0x3f, 0x3f, 0x3f), 7 | (0x4b, 0x4b, 0x4b), 8 | (0x5b, 0x5b, 0x5b), 9 | (0x6b, 0x6b, 0x6b), 10 | (0x7b, 0x7b, 0x7b), 11 | (0x8b, 0x8b, 0x8b), 12 | (0x9b, 0x9b, 0x9b), 13 | (0xab, 0xab, 0xab), 14 | (0xbb, 0xbb, 0xbb), 15 | (0xcb, 0xcb, 0xcb), 16 | (0xdb, 0xdb, 0xdb), 17 | (0xeb, 0xeb, 0xeb), 18 | (0x0f, 0x0b, 0x07), 19 | (0x17, 0x0f, 0x0b), 20 | (0x1f, 0x17, 0x0b), 21 | (0x27, 0x1b, 0x0f), 22 | (0x2f, 0x23, 0x13), 23 | (0x37, 0x2b, 0x17), 24 | (0x3f, 0x2f, 0x17), 25 | (0x4b, 0x37, 0x1b), 26 | (0x53, 0x3b, 0x1b), 27 | (0x5b, 0x43, 0x1f), 28 | (0x63, 0x4b, 0x1f), 29 | (0x6b, 0x53, 0x1f), 30 | (0x73, 0x57, 0x1f), 31 | (0x7b, 0x5f, 0x23), 32 | (0x83, 0x67, 0x23), 33 | (0x8f, 0x6f, 0x23), 34 | (0x0b, 0x0b, 0x0f), 35 | (0x13, 0x13, 0x1b), 36 | (0x1b, 0x1b, 0x27), 37 | (0x27, 0x27, 0x33), 38 | (0x2f, 0x2f, 0x3f), 39 | (0x37, 0x37, 0x4b), 40 | (0x3f, 0x3f, 0x57), 41 | (0x47, 0x47, 0x67), 42 | (0x4f, 0x4f, 0x73), 43 | (0x5b, 0x5b, 0x7f), 44 | (0x63, 0x63, 0x8b), 45 | (0x6b, 0x6b, 0x97), 46 | (0x73, 0x73, 0xa3), 47 | (0x7b, 0x7b, 0xaf), 48 | (0x83, 0x83, 0xbb), 49 | (0x8b, 0x8b, 0xcb), 50 | (0x00, 0x00, 0x00), 51 | (0x07, 0x07, 0x00), 52 | (0x0b, 0x0b, 0x00), 53 | (0x13, 0x13, 0x00), 54 | (0x1b, 0x1b, 0x00), 55 | (0x23, 0x23, 0x00), 56 | (0x2b, 0x2b, 0x07), 57 | (0x2f, 0x2f, 0x07), 58 | (0x37, 0x37, 0x07), 59 | (0x3f, 0x3f, 0x07), 60 | (0x47, 0x47, 0x07), 61 | (0x4b, 0x4b, 0x0b), 62 | (0x53, 0x53, 0x0b), 63 | (0x5b, 0x5b, 0x0b), 64 | (0x63, 0x63, 0x0b), 65 | (0x6b, 0x6b, 0x0f), 66 | (0x07, 0x00, 0x00), 67 | (0x0f, 0x00, 0x00), 68 | (0x17, 0x00, 0x00), 69 | (0x1f, 0x00, 0x00), 70 | (0x27, 0x00, 0x00), 71 | (0x2f, 0x00, 0x00), 72 | (0x37, 0x00, 0x00), 73 | (0x3f, 0x00, 0x00), 74 | (0x47, 0x00, 0x00), 75 | (0x4f, 0x00, 0x00), 76 | (0x57, 0x00, 0x00), 77 | (0x5f, 0x00, 0x00), 78 | (0x67, 0x00, 0x00), 79 | (0x6f, 0x00, 0x00), 80 | (0x77, 0x00, 0x00), 81 | (0x7f, 0x00, 0x00), 82 | (0x13, 0x13, 0x00), 83 | (0x1b, 0x1b, 0x00), 84 | (0x23, 0x23, 0x00), 85 | (0x2f, 0x2b, 0x00), 86 | (0x37, 0x2f, 0x00), 87 | (0x43, 0x37, 0x00), 88 | (0x4b, 0x3b, 0x07), 89 | (0x57, 0x43, 0x07), 90 | (0x5f, 0x47, 0x07), 91 | (0x6b, 0x4b, 0x0b), 92 | (0x77, 0x53, 0x0f), 93 | (0x83, 0x57, 0x13), 94 | (0x8b, 0x5b, 0x13), 95 | (0x97, 0x5f, 0x1b), 96 | (0xa3, 0x63, 0x1f), 97 | (0xaf, 0x67, 0x23), 98 | (0x23, 0x13, 0x07), 99 | (0x2f, 0x17, 0x0b), 100 | (0x3b, 0x1f, 0x0f), 101 | (0x4b, 0x23, 0x13), 102 | (0x57, 0x2b, 0x17), 103 | (0x63, 0x2f, 0x1f), 104 | (0x73, 0x37, 0x23), 105 | (0x7f, 0x3b, 0x2b), 106 | (0x8f, 0x43, 0x33), 107 | (0x9f, 0x4f, 0x33), 108 | (0xaf, 0x63, 0x2f), 109 | (0xbf, 0x77, 0x2f), 110 | (0xcf, 0x8f, 0x2b), 111 | (0xdf, 0xab, 0x27), 112 | (0xef, 0xcb, 0x1f), 113 | (0xff, 0xf3, 0x1b), 114 | (0x0b, 0x07, 0x00), 115 | (0x1b, 0x13, 0x00), 116 | (0x2b, 0x23, 0x0f), 117 | (0x37, 0x2b, 0x13), 118 | (0x47, 0x33, 0x1b), 119 | (0x53, 0x37, 0x23), 120 | (0x63, 0x3f, 0x2b), 121 | (0x6f, 0x47, 0x33), 122 | (0x7f, 0x53, 0x3f), 123 | (0x8b, 0x5f, 0x47), 124 | (0x9b, 0x6b, 0x53), 125 | (0xa7, 0x7b, 0x5f), 126 | (0xb7, 0x87, 0x6b), 127 | (0xc3, 0x93, 0x7b), 128 | (0xd3, 0xa3, 0x8b), 129 | (0xe3, 0xb3, 0x97), 130 | (0xab, 0x8b, 0xa3), 131 | (0x9f, 0x7f, 0x97), 132 | (0x93, 0x73, 0x87), 133 | (0x8b, 0x67, 0x7b), 134 | (0x7f, 0x5b, 0x6f), 135 | (0x77, 0x53, 0x63), 136 | (0x6b, 0x4b, 0x57), 137 | (0x5f, 0x3f, 0x4b), 138 | (0x57, 0x37, 0x43), 139 | (0x4b, 0x2f, 0x37), 140 | (0x43, 0x27, 0x2f), 141 | (0x37, 0x1f, 0x23), 142 | (0x2b, 0x17, 0x1b), 143 | (0x23, 0x13, 0x13), 144 | (0x17, 0x0b, 0x0b), 145 | (0x0f, 0x07, 0x07), 146 | (0xbb, 0x73, 0x9f), 147 | (0xaf, 0x6b, 0x8f), 148 | (0xa3, 0x5f, 0x83), 149 | (0x97, 0x57, 0x77), 150 | (0x8b, 0x4f, 0x6b), 151 | (0x7f, 0x4b, 0x5f), 152 | (0x73, 0x43, 0x53), 153 | (0x6b, 0x3b, 0x4b), 154 | (0x5f, 0x33, 0x3f), 155 | (0x53, 0x2b, 0x37), 156 | (0x47, 0x23, 0x2b), 157 | (0x3b, 0x1f, 0x23), 158 | (0x2f, 0x17, 0x1b), 159 | (0x23, 0x13, 0x13), 160 | (0x17, 0x0b, 0x0b), 161 | (0x0f, 0x07, 0x07), 162 | (0xdb, 0xc3, 0xbb), 163 | (0xcb, 0xb3, 0xa7), 164 | (0xbf, 0xa3, 0x9b), 165 | (0xaf, 0x97, 0x8b), 166 | (0xa3, 0x87, 0x7b), 167 | (0x97, 0x7b, 0x6f), 168 | (0x87, 0x6f, 0x5f), 169 | (0x7b, 0x63, 0x53), 170 | (0x6b, 0x57, 0x47), 171 | (0x5f, 0x4b, 0x3b), 172 | (0x53, 0x3f, 0x33), 173 | (0x43, 0x33, 0x27), 174 | (0x37, 0x2b, 0x1f), 175 | (0x27, 0x1f, 0x17), 176 | (0x1b, 0x13, 0x0f), 177 | (0x0f, 0x0b, 0x07), 178 | (0x6f, 0x83, 0x7b), 179 | (0x67, 0x7b, 0x6f), 180 | (0x5f, 0x73, 0x67), 181 | (0x57, 0x6b, 0x5f), 182 | (0x4f, 0x63, 0x57), 183 | (0x47, 0x5b, 0x4f), 184 | (0x3f, 0x53, 0x47), 185 | (0x37, 0x4b, 0x3f), 186 | (0x2f, 0x43, 0x37), 187 | (0x2b, 0x3b, 0x2f), 188 | (0x23, 0x33, 0x27), 189 | (0x1f, 0x2b, 0x1f), 190 | (0x17, 0x23, 0x17), 191 | (0x0f, 0x1b, 0x13), 192 | (0x0b, 0x13, 0x0b), 193 | (0x07, 0x0b, 0x07), 194 | (0xff, 0xf3, 0x1b), 195 | (0xef, 0xdf, 0x17), 196 | (0xdb, 0xcb, 0x13), 197 | (0xcb, 0xb7, 0x0f), 198 | (0xbb, 0xa7, 0x0f), 199 | (0xab, 0x97, 0x0b), 200 | (0x9b, 0x83, 0x07), 201 | (0x8b, 0x73, 0x07), 202 | (0x7b, 0x63, 0x07), 203 | (0x6b, 0x53, 0x00), 204 | (0x5b, 0x47, 0x00), 205 | (0x4b, 0x37, 0x00), 206 | (0x3b, 0x2b, 0x00), 207 | (0x2b, 0x1f, 0x00), 208 | (0x1b, 0x0f, 0x00), 209 | (0x0b, 0x07, 0x00), 210 | (0x00, 0x00, 0xff), 211 | (0x0b, 0x0b, 0xef), 212 | (0x13, 0x13, 0xdf), 213 | (0x1b, 0x1b, 0xcf), 214 | (0x23, 0x23, 0xbf), 215 | (0x2b, 0x2b, 0xaf), 216 | (0x2f, 0x2f, 0x9f), 217 | (0x2f, 0x2f, 0x8f), 218 | (0x2f, 0x2f, 0x7f), 219 | (0x2f, 0x2f, 0x6f), 220 | (0x2f, 0x2f, 0x5f), 221 | (0x2b, 0x2b, 0x4f), 222 | (0x23, 0x23, 0x3f), 223 | (0x1b, 0x1b, 0x2f), 224 | (0x13, 0x13, 0x1f), 225 | (0x0b, 0x0b, 0x0f), 226 | (0x2b, 0x00, 0x00), 227 | (0x3b, 0x00, 0x00), 228 | (0x4b, 0x07, 0x00), 229 | (0x5f, 0x07, 0x00), 230 | (0x6f, 0x0f, 0x00), 231 | (0x7f, 0x17, 0x07), 232 | (0x93, 0x1f, 0x07), 233 | (0xa3, 0x27, 0x0b), 234 | (0xb7, 0x33, 0x0f), 235 | (0xc3, 0x4b, 0x1b), 236 | (0xcf, 0x63, 0x2b), 237 | (0xdb, 0x7f, 0x3b), 238 | (0xe3, 0x97, 0x4f), 239 | (0xe7, 0xab, 0x5f), 240 | (0xef, 0xbf, 0x77), 241 | (0xf7, 0xd3, 0x8b), 242 | (0xa7, 0x7b, 0x3b), 243 | (0xb7, 0x9b, 0x37), 244 | (0xc7, 0xc3, 0x37), 245 | (0xe7, 0xe3, 0x57), 246 | (0x7f, 0xbf, 0xff), 247 | (0xab, 0xe7, 0xff), 248 | (0xd7, 0xff, 0xff), 249 | (0x67, 0x00, 0x00), 250 | (0x8b, 0x00, 0x00), 251 | (0xb3, 0x00, 0x00), 252 | (0xd7, 0x00, 0x00), 253 | (0xff, 0x00, 0x00), 254 | (0xff, 0xf3, 0x93), 255 | (0xff, 0xf7, 0xc7), 256 | (0xff, 0xff, 0xff), 257 | (0x9f, 0x5b, 0x53), 258 | ) 259 | -------------------------------------------------------------------------------- /hexen2pal.py: -------------------------------------------------------------------------------- 1 | palette = ( 2 | (0x00, 0x00, 0x00), 3 | (0x00, 0x00, 0x00), 4 | (0x08, 0x08, 0x08), 5 | (0x10, 0x10, 0x10), 6 | (0x18, 0x18, 0x18), 7 | 8 | (0x20, 0x20, 0x20), 9 | (0x28, 0x28, 0x28), 10 | (0x30, 0x30, 0x30), 11 | (0x38, 0x38, 0x38), 12 | (0x40, 0x40, 0x40), 13 | 14 | (0x48, 0x48, 0x48), 15 | (0x50, 0x50, 0x50), 16 | (0x54, 0x54, 0x54), 17 | (0x58, 0x58, 0x58), 18 | (0x60, 0x60, 0x60), 19 | (0x68, 0x68, 0x68), 20 | 21 | (0x70, 0x70, 0x70), 22 | (0x78, 0x78, 0x78), 23 | (0x80, 0x80, 0x80), 24 | (0x88, 0x88, 0x88), 25 | (0x94, 0x94, 0x94), 26 | 27 | (0x9C, 0x9C, 0x9C), 28 | (0xA8, 0xA8, 0xA8), 29 | (0xB4, 0xB4, 0xB4), 30 | (0xB8, 0xB8, 0xB8), 31 | (0xC4, 0xC4, 0xC4), 32 | 33 | (0xCC, 0xCC, 0xCC), 34 | (0xD4, 0xD4, 0xD4), 35 | (0xE0, 0xE0, 0xE0), 36 | (0xE8, 0xE8, 0xE8), 37 | (0xF0, 0xF0, 0xF0), 38 | (0xFC, 0xFC, 0xFC), 39 | 40 | (0x08, 0x08, 0x0C), 41 | (0x10, 0x10, 0x14), 42 | (0x18, 0x18, 0x1C), 43 | (0x1C, 0x20, 0x24), 44 | (0x24, 0x24, 0x2C), 45 | 46 | (0x2C, 0x2C, 0x34), 47 | (0x30, 0x34, 0x3C), 48 | (0x38, 0x38, 0x44), 49 | (0x40, 0x40, 0x48), 50 | (0x4C, 0x4C, 0x58), 51 | 52 | (0x5C, 0x5C, 0x68), 53 | (0x6C, 0x70, 0x80), 54 | (0x80, 0x84, 0x98), 55 | (0x98, 0x9C, 0xB0), 56 | (0xA8, 0xAC, 0xC4), 57 | (0xBC, 0xC4, 0xDC), 58 | 59 | (0x20, 0x18, 0x14), 60 | (0x28, 0x20, 0x1C), 61 | (0x30, 0x24, 0x20), 62 | (0x34, 0x2C, 0x28), 63 | (0x3C, 0x34, 0x2C), 64 | 65 | (0x44, 0x38, 0x34), 66 | (0x4C, 0x40, 0x38), 67 | (0x54, 0x48, 0x40), 68 | (0x5C, 0x4C, 0x48), 69 | (0x64, 0x54, 0x4C), 70 | 71 | (0x6C, 0x5C, 0x54), 72 | (0x70, 0x60, 0x58), 73 | (0x78, 0x68, 0x60), 74 | (0x80, 0x70, 0x64), 75 | (0x88, 0x74, 0x6C), 76 | (0x90, 0x7C, 0x70), 77 | 78 | (0x14, 0x18, 0x14), 79 | (0x1C, 0x20, 0x1C), 80 | (0x20, 0x24, 0x20), 81 | (0x28, 0x2C, 0x28), 82 | (0x2C, 0x30, 0x2C), 83 | 84 | (0x30, 0x38, 0x30), 85 | (0x38, 0x40, 0x38), 86 | (0x40, 0x44, 0x40), 87 | (0x44, 0x4C, 0x44), 88 | (0x54, 0x5C, 0x54), 89 | 90 | (0x68, 0x70, 0x68), 91 | (0x78, 0x80, 0x78), 92 | (0x8C, 0x94, 0x88), 93 | (0x9C, 0xA4, 0x98), 94 | (0xAC, 0xB4, 0xA8), 95 | (0xBC, 0xC4, 0xB8), 96 | 97 | (0x30, 0x20, 0x08), 98 | (0x3C, 0x28, 0x08), 99 | (0x48, 0x30, 0x10), 100 | (0x54, 0x38, 0x14), 101 | (0x5C, 0x40, 0x1C), 102 | 103 | (0x64, 0x48, 0x24), 104 | (0x6C, 0x50, 0x2C), 105 | (0x78, 0x5C, 0x34), 106 | (0x88, 0x68, 0x3C), 107 | (0x94, 0x74, 0x48), 108 | 109 | (0xA0, 0x80, 0x54), 110 | (0xA8, 0x88, 0x5C), 111 | (0xB4, 0x90, 0x64), 112 | (0xBC, 0x98, 0x6C), 113 | (0xC4, 0xA0, 0x74), 114 | (0xCC, 0xA8, 0x7C), 115 | 116 | (0x10, 0x14, 0x10), 117 | (0x14, 0x1C, 0x14), 118 | (0x18, 0x20, 0x18), 119 | (0x1C, 0x24, 0x1C), 120 | (0x20, 0x2C, 0x20), 121 | 122 | (0x24, 0x30, 0x24), 123 | (0x28, 0x38, 0x28), 124 | (0x2C, 0x3C, 0x2C), 125 | (0x30, 0x44, 0x30), 126 | (0x34, 0x4C, 0x34), 127 | 128 | (0x3C, 0x54, 0x3C), 129 | (0x44, 0x5C, 0x40), 130 | (0x4C, 0x64, 0x48), 131 | (0x54, 0x6C, 0x4C), 132 | (0x5C, 0x74, 0x54), 133 | (0x64, 0x80, 0x5C), 134 | 135 | (0x18, 0x0C, 0x08), 136 | (0x20, 0x10, 0x08), 137 | (0x28, 0x14, 0x08), 138 | (0x34, 0x18, 0x0C), 139 | (0x3C, 0x1C, 0x0C), 140 | 141 | (0x44, 0x20, 0x0C), 142 | (0x4C, 0x24, 0x10), 143 | (0x54, 0x2C, 0x14), 144 | (0x5C, 0x30, 0x18), 145 | (0x64, 0x38, 0x1C), 146 | 147 | (0x70, 0x40, 0x20), 148 | (0x78, 0x48, 0x24), 149 | (0x80, 0x50, 0x2C), 150 | (0x90, 0x5C, 0x38), 151 | (0xA8, 0x70, 0x48), 152 | (0xC0, 0x84, 0x58), 153 | 154 | (0x18, 0x04, 0x04), 155 | (0x24, 0x04, 0x04), 156 | (0x30, 0x00, 0x00), 157 | (0x3C, 0x00, 0x00), 158 | (0x44, 0x00, 0x00), 159 | 160 | (0x50, 0x00, 0x00), 161 | (0x58, 0x00, 0x00), 162 | (0x64, 0x00, 0x00), 163 | (0x70, 0x00, 0x00), 164 | (0x84, 0x00, 0x00), 165 | 166 | (0x98, 0x00, 0x00), 167 | (0xAC, 0x00, 0x00), 168 | (0xC0, 0x00, 0x00), 169 | (0xD4, 0x00, 0x00), 170 | (0xE8, 0x00, 0x00), 171 | (0xFC, 0x00, 0x00), 172 | 173 | (0x10, 0x0C, 0x20), 174 | (0x1C, 0x14, 0x30), 175 | (0x20, 0x1C, 0x38), 176 | (0x28, 0x24, 0x44), 177 | (0x34, 0x2C, 0x50), 178 | 179 | (0x3C, 0x38, 0x5C), 180 | (0x44, 0x40, 0x68), 181 | (0x50, 0x48, 0x74), 182 | (0x58, 0x54, 0x80), 183 | (0x64, 0x60, 0x8C), 184 | 185 | (0x6C, 0x6C, 0x98), 186 | (0x78, 0x74, 0xA4), 187 | (0x84, 0x84, 0xB0), 188 | (0x90, 0x90, 0xBC), 189 | (0x9C, 0x9C, 0xC8), 190 | (0xAC, 0xAC, 0xD4), 191 | 192 | (0x24, 0x14, 0x04), 193 | (0x34, 0x18, 0x04), 194 | (0x44, 0x20, 0x04), 195 | (0x50, 0x28, 0x00), 196 | (0x64, 0x30, 0x04), 197 | 198 | (0x7C, 0x3C, 0x04), 199 | (0x8C, 0x48, 0x04), 200 | (0x9C, 0x58, 0x08), 201 | (0xAC, 0x64, 0x08), 202 | (0xBC, 0x74, 0x0C), 203 | 204 | (0xCC, 0x80, 0x0C), 205 | (0xDC, 0x90, 0x10), 206 | (0xEC, 0xA0, 0x14), 207 | (0xFC, 0xB8, 0x38), 208 | (0xF8, 0xC8, 0x50), 209 | (0xF8, 0xDC, 0x78), 210 | 211 | (0x14, 0x10, 0x04), 212 | (0x1C, 0x18, 0x08), 213 | (0x24, 0x20, 0x08), 214 | (0x2C, 0x28, 0x0C), 215 | (0x34, 0x30, 0x10), 216 | 217 | (0x38, 0x38, 0x10), 218 | (0x40, 0x40, 0x14), 219 | (0x44, 0x48, 0x18), 220 | (0x48, 0x50, 0x1C), 221 | (0x50, 0x5C, 0x20), 222 | 223 | (0x54, 0x68, 0x28), 224 | (0x58, 0x74, 0x2C), 225 | (0x5C, 0x80, 0x34), 226 | (0x5C, 0x8C, 0x34), 227 | (0x5C, 0x94, 0x38), 228 | (0x60, 0xA0, 0x40), 229 | 230 | (0x3C, 0x10, 0x10), 231 | (0x48, 0x18, 0x18), 232 | (0x54, 0x1C, 0x1C), 233 | (0x64, 0x24, 0x24), 234 | (0x70, 0x2C, 0x2C), 235 | 236 | (0x7C, 0x34, 0x30), 237 | (0x8C, 0x40, 0x38), 238 | (0x98, 0x4C, 0x40), 239 | (0x2C, 0x14, 0x08), 240 | (0x38, 0x1C, 0x0C), 241 | 242 | (0x48, 0x20, 0x10), 243 | (0x54, 0x28, 0x14), 244 | (0x60, 0x2C, 0x1C), 245 | (0x70, 0x34, 0x20), 246 | (0x7C, 0x38, 0x28), 247 | (0x8C, 0x40, 0x30), 248 | 249 | (0x18, 0x14, 0x10), 250 | (0x24, 0x1C, 0x14), 251 | (0x2C, 0x24, 0x1C), 252 | (0x38, 0x2C, 0x20), 253 | (0x40, 0x34, 0x24), 254 | 255 | (0x48, 0x3C, 0x2C), 256 | (0x50, 0x44, 0x30), 257 | (0x5C, 0x4C, 0x34), 258 | (0x64, 0x54, 0x3C), 259 | (0x70, 0x5C, 0x44), 260 | 261 | (0x78, 0x64, 0x48), 262 | (0x84, 0x70, 0x50), 263 | (0x90, 0x78, 0x58), 264 | (0x98, 0x80, 0x60), 265 | (0xA0, 0x88, 0x68), 266 | (0xA8, 0x94, 0x70), 267 | 268 | (0x24, 0x18, 0x0C), 269 | (0x2C, 0x20, 0x10), 270 | (0x34, 0x28, 0x14), 271 | (0x3C, 0x2C, 0x14), 272 | (0x48, 0x34, 0x18), 273 | 274 | (0x50, 0x3C, 0x1C), 275 | (0x58, 0x44, 0x1C), 276 | (0x68, 0x4C, 0x20), 277 | (0x94, 0x60, 0x38), 278 | (0xA0, 0x6C, 0x40), 279 | 280 | (0xAC, 0x74, 0x48), 281 | (0xB4, 0x7C, 0x50), 282 | (0xC0, 0x84, 0x58), 283 | (0xCC, 0x8C, 0x5C), 284 | (0xD8, 0x9C, 0x6C), 285 | (0x3C, 0x14, 0x5C), 286 | 287 | (0x64, 0x24, 0x74), 288 | (0xA8, 0x48, 0xA4), 289 | (0xCC, 0x6C, 0xC0), 290 | (0x04, 0x54, 0x04), 291 | (0x04, 0x84, 0x04), 292 | 293 | (0x00, 0xB4, 0x00), 294 | (0x00, 0xD8, 0x00), 295 | (0x04, 0x04, 0x90), 296 | (0x10, 0x44, 0xCC), 297 | (0x24, 0x84, 0xE0), 298 | 299 | (0x58, 0xA8, 0xE8), 300 | (0xD8, 0x04, 0x04), 301 | (0xF4, 0x48, 0x00), 302 | (0xFC, 0x80, 0x00), 303 | (0xFC, 0xAC, 0x18), 304 | (0xFC, 0xFC, 0xFC), 305 | 306 | ) 307 | -------------------------------------------------------------------------------- /qfplist.py: -------------------------------------------------------------------------------- 1 | # vim:ts=4:et 2 | # ##### BEGIN GPL LICENSE BLOCK ##### 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software Foundation, 16 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 17 | # 18 | # ##### END GPL LICENSE BLOCK ##### 19 | 20 | # 21 | 22 | quotables = ("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" 23 | + "abcdefghijklmnopqrstuvwxyz!#$%&*+-./:?@|~_^") 24 | 25 | 26 | class PListError(Exception): 27 | def __init__(self, line, message): 28 | Exception.__init__(self, "%d: %s" % (line, message)) 29 | self.line = line 30 | 31 | 32 | class pldata: 33 | def __init__(self, src=''): 34 | self.src = src 35 | self.pos = 0 36 | self.end = len(self.src) 37 | self.line = 1 38 | 39 | def skip_space(self): 40 | while self.pos < self.end: 41 | c = self.src[self.pos] 42 | if not c.isspace(): 43 | if c == '/' and self.pos < self.end - 1: # comments 44 | if self.src[self.pos + 1] == '/': # // coment 45 | self.pos += 2 46 | while self.pos < self.end: 47 | c = self.src[self.pos] 48 | if c == '\n': 49 | break 50 | self.pos += 1 51 | if self.pos >= self.end: 52 | raise PListError(self.line, 53 | "Reached end of string in comment") 54 | elif self.src[self.pos + 1] == '*': # /* comment */ 55 | start_line = self.line 56 | self.pos += 2 57 | while self.pos < self.end: 58 | c = self.src[self.pos] 59 | if c == '\n': 60 | self.line += 1 61 | elif (c == '*' and self.pos < self.end - 1 62 | and self.src[self.pos + 1] == '/'): 63 | self.pos += 1 64 | break 65 | self.pos += 1 66 | if self.pos >= self.end: 67 | raise PListError(start_line, 68 | "Reached end of string in comment") 69 | else: 70 | return True 71 | else: 72 | return True 73 | if c == '\n': 74 | self.line += 1 75 | self.pos += 1 76 | raise PListError(self.line, "Reached end of string") 77 | 78 | def parse_quoted_string(self): 79 | start_line = self.line 80 | long_string = False 81 | escaped = 0 82 | shrink = 0 83 | hexa = False 84 | self.pos += 1 85 | start = self.pos 86 | if (self.pos < self.end - 1 and self.src[self.pos] == '"' 87 | and self.src[self.pos + 1] == '"'): 88 | self.pos += 2 89 | long_string = True 90 | start += 2 91 | while self.pos < self.end: 92 | c = self.src[self.pos] 93 | if escaped: 94 | if escaped == 1 and c == '0': 95 | escaped += 1 96 | hexa = False 97 | elif escaped > 1: 98 | if escaped == 2 and c == 'x': 99 | hexa = True 100 | shring += 1 101 | escaped += 1 102 | elif hex and c.isxdigit(): 103 | shrink += 1 104 | escaped += 1 105 | elif c in range(0, 8): 106 | shrink += 1 107 | escaped += 1 108 | else: 109 | self.pos -= 1 110 | escaped = 0 111 | else: 112 | escaped = 0 113 | else: 114 | if c == '\\': 115 | escaped = 1 116 | shrink += 1 117 | elif (c == '"' 118 | and (not long_string 119 | or (self.pos < self.end - 2 120 | and self.src[self.pos + 1] == '"' 121 | and self.src[self.pos + 2] == '"'))): 122 | break 123 | if c == '\n': 124 | self.line += 1 125 | self.pos += 1 126 | if self.pos >= self.end: 127 | raise PListError(start_line, 128 | "Reached end of string while parsing quoted string") 129 | if self.pos - start - shrink == 0: 130 | return "" 131 | s = self.src[start:self.pos] 132 | self.pos += 1 133 | if long_string: 134 | self.pos += 2 135 | return eval('"""' + s + '"""') 136 | 137 | def parse_unquoted_string(self): 138 | start = self.pos 139 | while self.pos < self.end: 140 | if self.src[self.pos] not in quotables: 141 | break 142 | self.pos += 1 143 | return self.src[start:self.pos] 144 | 145 | def parse_data(self): 146 | start_line = self.line 147 | self.pos += 1 148 | start = self.pos 149 | nibbles = 0 150 | while self.pos < self.end: 151 | if self.src[self.pos].isxdigit: 152 | nibbles += 1 153 | self.pos += 1 154 | continue 155 | if self.src[self.pos] == '>': 156 | if nibbles & 1: 157 | raise PListError(self.line, 158 | "Invalid data, missing nibble") 159 | s = self.src[start:self.pos] 160 | self.pos += 1 161 | return binascii.a2b_hex(s) 162 | raise PListError(self.line, 163 | "Invalid character in data") 164 | raise PListError(start_line, 165 | "Reached end of string while parsing data") 166 | 167 | def parse(self): 168 | self.skip_space() 169 | if self.src[self.pos] == '{': 170 | item = {} 171 | self.pos += 1 172 | while self.skip_space() and self.src[self.pos] != '}': 173 | key = self.parse() 174 | if type(key) != str: 175 | raise PListError(self.line, 176 | "Key is not a string") 177 | self.skip_space() 178 | if self.src[self.pos] != '=': 179 | raise PListError(self.line, 180 | "Unexpected character (expected '=')") 181 | self.pos += 1 182 | value = self.parse() 183 | if self.src[self.pos] == ';': 184 | self.pos += 1 185 | elif self.src[self.pos] != '}': 186 | raise PListError(self.line, 187 | "Unexpected character (wanted ';' or '}')") 188 | item[key] = value 189 | if self.pos >= self.end: 190 | raise PListError(self.line, 191 | "Unexpected end of string when parsing dictionary") 192 | self.pos += 1 193 | return item 194 | elif self.src[self.pos] == '(': 195 | item = [] 196 | self.pos += 1 197 | while self.skip_space() and self.src[self.pos] != ')': 198 | value = self.parse() 199 | self.skip_space() 200 | if self.src[self.pos] == ',': 201 | self.pos += 1 202 | elif self.src[self.pos] != ')': 203 | raise PListError(self.line, 204 | "Unexpected character (wanted ',' or ')')") 205 | item.append(value) 206 | self.pos += 1 207 | return item 208 | elif self.src[self.pos] == '<': 209 | return self.parse_data() 210 | elif self.src[self.pos] == '"': 211 | return self.parse_quoted_string() 212 | else: 213 | return self.parse_unquoted_string() 214 | 215 | def write_string(self, item): 216 | quote = False 217 | for i in item: 218 | if i not in quotables: 219 | quote = True 220 | break 221 | if quote: 222 | item = repr(item) 223 | # repr uses ', we want " 224 | item = '"' + item[1:-1].replace('"', '\\"') + '"' 225 | self.data.append(item) 226 | 227 | def write_item(self, item, level): 228 | if type(item) == dict: 229 | if not item: 230 | self.data.append("{ }") 231 | return 232 | self.data.append("{\n") 233 | for i in item.items(): 234 | self.data.append('\t' * (level + 1)) 235 | self.write_string(i[0]) 236 | self.data.append(' = ') 237 | self.write_item(i[1], level + 1) 238 | self.data.append(';\n') 239 | self.data.append('\t' * (level)) 240 | self.data.append("}") 241 | elif type(item) in (list, tuple): 242 | if not item: 243 | self.data.append("( )") 244 | return 245 | self.data.append("(\n") 246 | for n, i in enumerate(item): 247 | self.data.append('\t' * (level + 1)) 248 | self.write_item(i, level + 1) 249 | if n < len(item) - 1: 250 | self.data.append(',\n') 251 | self.data.append('\n') 252 | self.data.append('\t' * (level)) 253 | self.data.append(")") 254 | elif type(item) == bytes: 255 | self.data.append('<') 256 | self.data.append(binascii.b2a_hex(item)) 257 | self.data.append('>') 258 | elif type(item) == str: 259 | self.write_string(item) 260 | elif type(item) in [int, float]: 261 | self.write_string(str(item)) 262 | else: 263 | raise PListError(0, "unsupported type") 264 | 265 | def write(self, item): 266 | self.data = [] 267 | self.write_item(item, 0) 268 | return ''.join(self.data) 269 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # vim:ts=4:et 2 | # ##### BEGIN GPL LICENSE BLOCK ##### 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software Foundation, 16 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 17 | # 18 | # ##### END GPL LICENSE BLOCK ##### 19 | 20 | # 21 | 22 | from bpy_extras.io_utils import ExportHelper, ImportHelper, path_reference_mode, axis_conversion 23 | from bpy.props import FloatVectorProperty, PointerProperty 24 | from bpy.props import BoolProperty, FloatProperty, StringProperty, EnumProperty, CollectionProperty 25 | import bpy 26 | 27 | bl_info = { 28 | "name": "Quake and Hexen II MDL format", 29 | "author": "Bill Currie, Victor Feitosa", 30 | "blender": (2, 80, 0), 31 | "version": (1, 0, 0), 32 | "api": 35622, 33 | "location": "File > Import-Export", 34 | "description": "Import-Export Quake and HexenII MDL files (version 6 mdl files)", 35 | "warning": "alpha version", 36 | "wiki_url": "", 37 | "tracker_url": "", 38 | "category": "Import-Export"} 39 | 40 | # To support reload properly, try to access a package var, if it's there, 41 | # reload everything 42 | if "bpy" in locals(): 43 | import imp 44 | if "import_mdl" in locals(): 45 | imp.reload(import_mdl) 46 | if "export_mdl" in locals(): 47 | imp.reload(export_mdl) 48 | 49 | 50 | SYNCTYPE = ( 51 | ('ST_SYNC', "Syncronized", "Automatic animations are all together"), 52 | ('ST_RAND', "Random", "Automatic animations have random offsets"), 53 | ) 54 | 55 | PALETTES = ( 56 | ('QUAKE', "Quake palette", "Import/Export to Quake"), 57 | ('HEXEN2', "Hexen II palette", "Import/Export to Hexen II"), 58 | ) 59 | 60 | 61 | class QFMDLEffects(bpy.types.PropertyGroup): 62 | # Quake effects 63 | rocket: BoolProperty( 64 | name="EF_ROCKET", 65 | description="Leave a rocket trail", 66 | ) 67 | grenade: BoolProperty( 68 | name="EF_GRENADE", 69 | description="Leave a grenade trail", 70 | ) 71 | gib: BoolProperty( 72 | name="EF_GIB", 73 | description="Leave a trail of blood", 74 | ) 75 | rotate: BoolProperty( 76 | name="EF_ROTATE", 77 | description="Rotates model like an pickup", 78 | ) 79 | tracer: BoolProperty( 80 | name="EF_TRACER", 81 | description="Green split trail", 82 | ) 83 | zombie_gib: BoolProperty( 84 | name="EF_ZOMGIB", 85 | description="Leave a smaller blood trail", 86 | ) 87 | tracer2: BoolProperty( 88 | name="EF_TRACER2", 89 | description="Orange split trail", 90 | ) 91 | tracer3: BoolProperty( 92 | name="EF_TRACER3", 93 | description="Purple split trail", 94 | ) 95 | 96 | # Hexen II effects 97 | fireball: BoolProperty( 98 | name="EF_FIREBALL", 99 | description="Yellow transparent fireball trail", 100 | ) 101 | ice: BoolProperty( 102 | name="EF_ICE", 103 | description="Blue white ice trail with gravity", 104 | ) 105 | mipmap: BoolProperty( 106 | name="EF_MIP_MAP", 107 | description="Model has mip maps", 108 | ) 109 | spit: BoolProperty( 110 | name="EF_SPIT", 111 | description="Black transparent trail with negative light", 112 | ) 113 | transp: BoolProperty( 114 | name="EF_TRANSPARENT", 115 | description="Transparent sprite", 116 | ) 117 | spell: BoolProperty( 118 | name="EF_SPELL", 119 | description="Vertical spray of particles", 120 | ) 121 | solid: BoolProperty( 122 | name="EF_HOLEY", 123 | description="Solid model with black color", 124 | ) 125 | trans: BoolProperty( 126 | name="EF_SPECIAL_TRANS", 127 | description="Model with alpha channel", 128 | ) 129 | billboard: BoolProperty( 130 | name="EF_FACE_VIEW", 131 | description="Model is always facing the camera", 132 | ) 133 | vorpal: BoolProperty( 134 | name="EF_VORP_MISSILE", 135 | description="Leaves trail at top and bottom of model", 136 | ) 137 | setstaff: BoolProperty( 138 | name="EF_SET_STAFF", 139 | description="Trail that bobs left and right", 140 | ) 141 | magicmis: BoolProperty( 142 | name="EF_MAGICMISSILE", 143 | description="Blue white particles with gravity", 144 | ) 145 | boneshard: BoolProperty( 146 | name="EF_BONESHARD", 147 | description="Brown particles with gravity", 148 | ) 149 | scarab: BoolProperty( 150 | name="EF_SCARAB", 151 | description="White transparent particles with little gravity", 152 | ) 153 | acidball: BoolProperty( 154 | name="EF_ACIDBALL", 155 | description="Green drippy acid particles", 156 | ) 157 | bloodshot: BoolProperty( 158 | name="EF_BLOODSHOT", 159 | description="Blood rain shot trail", 160 | ) 161 | farmipmap: BoolProperty( 162 | name="EF_MIP_MAP_FAR", 163 | description="Model has mip maps for far distances", 164 | ) 165 | 166 | 167 | class QFMDLSettings(bpy.types.PropertyGroup): 168 | eyeposition: FloatVectorProperty( 169 | name="Eye Position", 170 | description="View possion relative to object origin", 171 | ) 172 | synctype: EnumProperty( 173 | items=SYNCTYPE, 174 | name="Sync Type", 175 | description="Add random time offset for automatic animations", 176 | ) 177 | script: StringProperty( 178 | name="Script", 179 | description="Script for animating frames and skins", 180 | ) 181 | xform: BoolProperty( 182 | name="Auto transform", 183 | description="Auto-apply location/rotation/scale when exporting", 184 | default=True, 185 | ) 186 | md16: BoolProperty( 187 | name="16-bit", 188 | description="16 bit vertex coordinates: QuakeForge only", 189 | ) 190 | effects: PointerProperty( 191 | name="MDL Effects", 192 | type=QFMDLEffects, 193 | ) 194 | 195 | 196 | class ImportMDL6(bpy.types.Operator, ImportHelper): 197 | '''Load a Quake MDL File''' 198 | bl_idname = "import_mesh.quake_mdl_v6" 199 | bl_label = "Import MDL" 200 | 201 | filename_ext = ".mdl" 202 | filter_glob: StringProperty(default="*.mdl", options={'HIDDEN'}) 203 | 204 | palette: EnumProperty( 205 | items=PALETTES, 206 | name="MDL Palette", 207 | description="Game color palette", 208 | default="QUAKE" 209 | ) 210 | 211 | import_scale: FloatProperty( 212 | name="Scale factor", 213 | description="Import model scale factor (usually 0.5)", 214 | default=0.1 215 | ) 216 | 217 | def execute(self, context): 218 | from . import import_mdl 219 | keywords = self.as_keywords(ignore=("filter_glob",)) 220 | return import_mdl.import_mdl(self, context, **keywords) 221 | 222 | 223 | class ExportMDL6(bpy.types.Operator, ExportHelper): 224 | '''Save a Quake MDL File''' 225 | 226 | bl_idname = "export_mesh.quake_mdl_v6" 227 | bl_label = "Export MDL" 228 | 229 | filename_ext = ".mdl" 230 | filter_glob: StringProperty(default="*.mdl", options={'HIDDEN'}) 231 | 232 | palette: EnumProperty( 233 | items=PALETTES, 234 | name="MDL Palette", 235 | description="Game color palette", 236 | default="QUAKE", 237 | ) 238 | 239 | export_scale: FloatProperty( 240 | name="Scale factor", 241 | description="Import model scale factor (usually 5)", 242 | default=10, 243 | ) 244 | 245 | @classmethod 246 | def poll(cls, context): 247 | return (context.active_object is not None 248 | and type(context.active_object.data) is bpy.types.Mesh) 249 | 250 | def execute(self, context): 251 | from . import export_mdl 252 | keywords = self.as_keywords(ignore=("check_existing", "filter_glob")) 253 | 254 | try: 255 | return export_mdl.export_mdl(self, context, **keywords) 256 | except IndexError: 257 | self.report( 258 | {"WARNING"}, "Error converting MDL vertices. Do you have any unapplied topology modifiers?") 259 | return {'CANCELLED'} 260 | return export_result 261 | 262 | 263 | class MDL_PT_Panel(bpy.types.Panel): 264 | bl_space_type = 'PROPERTIES' 265 | bl_region_type = 'WINDOW' 266 | bl_context = 'object' 267 | bl_label = 'Quake MDL' 268 | 269 | @classmethod 270 | def poll(cls, context): 271 | obj = context.active_object 272 | 273 | return obj and obj.type == 'MESH' 274 | 275 | def draw(self, context): 276 | layout = self.layout 277 | obj = context.active_object 278 | layout.prop(obj.qfmdl, "eyeposition") 279 | layout.prop(obj.qfmdl, "synctype") 280 | layout.prop(obj.qfmdl, "script") 281 | layout.prop(obj.qfmdl, "xform") 282 | layout.prop(obj.qfmdl, "md16") 283 | 284 | layout.label(text="Quake effects") 285 | 286 | effects = obj.qfmdl.effects 287 | # NOTE: Quake effects 288 | grid = layout.grid_flow(columns=2) 289 | grid.prop(effects, "rocket") 290 | grid.prop(effects, "grenade") 291 | grid.prop(effects, "gib") 292 | grid.prop(effects, "rotate") 293 | grid.prop(effects, "tracer") 294 | grid.prop(effects, "zombie_gib") 295 | grid.prop(effects, "tracer2") 296 | grid.prop(effects, "tracer3") 297 | # NOTE: Hexen II effects 298 | layout.label(text="Hexen II effects") 299 | grid = layout.grid_flow(columns=2) 300 | grid.prop(effects, "fireball") 301 | grid.prop(effects, "ice") 302 | grid.prop(effects, "mipmap") 303 | grid.prop(effects, "spit") 304 | grid.prop(effects, "transp") 305 | grid.prop(effects, "spell") 306 | grid.prop(effects, "solid") 307 | grid.prop(effects, "trans") 308 | grid.prop(effects, "billboard") 309 | grid.prop(effects, "vorpal") 310 | grid.prop(effects, "setstaff") 311 | grid.prop(effects, "magicmis") 312 | grid.prop(effects, "boneshard") 313 | grid.prop(effects, "scarab") 314 | grid.prop(effects, "acidball") 315 | grid.prop(effects, "bloodshot") 316 | grid.prop(effects, "farmipmap") 317 | 318 | 319 | def menu_func_import(self, context): 320 | self.layout.operator(ImportMDL6.bl_idname, 321 | text="Quake / HexenII MDL (.mdl)") 322 | 323 | 324 | def menu_func_export(self, context): 325 | self.layout.operator(ExportMDL6.bl_idname, 326 | text="Quake / HexenII MDL (.mdl)") 327 | 328 | 329 | classes = (QFMDLEffects, QFMDLSettings, ImportMDL6, ExportMDL6, MDL_PT_Panel) 330 | 331 | 332 | def register(): 333 | for cls in classes: 334 | bpy.utils.register_class(cls) 335 | 336 | bpy.types.Object.qfmdl = PointerProperty(type=QFMDLSettings) 337 | 338 | bpy.types.TOPBAR_MT_file_import.append(menu_func_import) 339 | bpy.types.TOPBAR_MT_file_export.append(menu_func_export) 340 | 341 | 342 | def unregister(): 343 | for cls in reversed(classes): 344 | bpy.utils.unregister_class(cls) 345 | 346 | bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) 347 | bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) 348 | 349 | 350 | if __name__ == "__main__": 351 | register() 352 | -------------------------------------------------------------------------------- /export_mdl.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | import bpy 20 | from bpy_extras.object_utils import object_data_add 21 | from mathutils import Vector, Matrix 22 | 23 | from .utils import getPaletteFromName 24 | from .qfplist import pldata, PListError 25 | from .qnorm import map_normal 26 | from .mdl import MDL 27 | from .constants import MDLEffects, MDLSyncType 28 | 29 | 30 | def check_faces(mesh): 31 | # Check that all faces are tris because mdl does not support anything else. 32 | # Because the diagonal on which a quad is split can make a big difference, 33 | # quad to tri conversion will not be done automatically. 34 | faces_ok = True 35 | save_select = [] 36 | for f in mesh.polygons: 37 | save_select.append(f.select) 38 | f.select = False 39 | if len(f.vertices) > 3: 40 | f.select = True 41 | faces_ok = False 42 | if not faces_ok: 43 | mesh.update() 44 | return False 45 | # reset selection to what it was before the check. 46 | for f, s in map(lambda x, y: (x, y), mesh.polygons, save_select): 47 | f.select = s 48 | mesh.update() 49 | return True 50 | 51 | 52 | def convert_image(image, palette): 53 | size = image.size 54 | skin = MDL.Skin() 55 | skin.type = 0 56 | skin.pixels = bytearray(size[0] * size[1]) # preallocate 57 | cache = {} 58 | pixels = image.pixels[:] 59 | for y in range(size[1]): 60 | for x in range(size[0]): 61 | outind = y * size[0] + x 62 | # quake textures are top to bottom, but blender images 63 | # are bottom to top 64 | inind = ((size[1] - 1 - y) * size[0] + x) * 4 65 | rgb = pixels[inind: inind + 3] # ignore alpha 66 | rgb = tuple(map(lambda x: int(x * 255 + 0.5), rgb)) 67 | if rgb not in cache: 68 | best = (3*256*256, -1) 69 | for i, p in enumerate(palette): 70 | if i > 255: # should never happen 71 | break 72 | r = 0 73 | for x in map(lambda a, b: (a - b) ** 2, rgb, p): 74 | r += x 75 | if r < best[0]: 76 | best = (r, i) 77 | cache[rgb] = best[1] 78 | skin.pixels[outind] = cache[rgb] 79 | return skin 80 | 81 | 82 | def null_skin(size): 83 | skin = MDL.Skin() 84 | skin.type = 0 85 | skin.pixels = bytearray(size[0] * size[1]) # black skin 86 | return skin 87 | 88 | 89 | def active_uv(mesh): 90 | for uvt in mesh.uv_layers: 91 | if uvt.active: 92 | return uvt 93 | return None 94 | 95 | 96 | def make_skin(mdl, mesh, palette): 97 | mdl.skinwidth, mdl.skinheight = (4, 4) 98 | skin = null_skin((mdl.skinwidth, mdl.skinheight)) 99 | 100 | materials = bpy.context.object.data.materials 101 | 102 | if len(materials) > 0: 103 | for mat in materials: 104 | allTextureNodes = list( 105 | filter(lambda node: node.type == "TEX_IMAGE", 106 | mat.node_tree.nodes)) 107 | if len(allTextureNodes) > 1: # === skingroup 108 | skingroup = MDL.Skin() 109 | skingroup.type = 1 110 | skingroup.skins = [] 111 | skingroup.times = [] 112 | sortedNodes = list(allTextureNodes) 113 | sortedNodes.sort(key=lambda x: x.location[1], reverse=True) 114 | for node in sortedNodes: 115 | if node.type == "TEX_IMAGE": 116 | image = node.image 117 | mdl.skinwidth, mdl.skinheight = image.size 118 | skin = convert_image(image, palette) 119 | skingroup.skins.append(skin) 120 | # hardcoded at the moment 121 | skingroup.times.append(0.1) 122 | mdl.skins.append(skingroup) 123 | elif len(allTextureNodes) == 1: # === single skin 124 | for node in allTextureNodes: 125 | if node.type == "TEX_IMAGE": 126 | image = node.image 127 | if (image.size[0] > 0 and image.size[1] > 0): 128 | mdl.skinwidth, mdl.skinheight = ( 129 | image.size[0], image.size[1]) 130 | skin = convert_image(image, palette) 131 | mdl.skins.append(skin) 132 | else: 133 | # add empty skin - no texture nodes 134 | mdl.skins.append(skin) 135 | else: 136 | # add empty skin - no materials 137 | mdl.skins.append(skin) 138 | 139 | 140 | def build_tris(mesh): 141 | # mdl files have a 1:1 relationship between stverts and 3d verts. 142 | # a bit sucky, but it does allow faces to take less memory 143 | # 144 | # modelgen's algorithm for generating UVs is very efficient in that no 145 | # vertices are duplicated (thanks to the onseam flag), but it can result 146 | # in fairly nasty UV layouts, and worse: the artist has no control over 147 | # the layout. However, there seems to be nothing in the mdl format 148 | # preventing the use of duplicate 3d vertices to allow complete freedom 149 | # of the UV layout. 150 | uvfaces = mesh.uv_layers.active.data 151 | stverts = [] 152 | tris = [] 153 | vertmap = [] # map mdl vert num to blender vert num (for 3d verts) 154 | vuvdict = {} 155 | for face in mesh.polygons: 156 | fv = list(face.vertices) 157 | uv = uvfaces[face.loop_start:face.loop_start + face.loop_total] 158 | uv = list(map(lambda a: a.uv, uv)) 159 | face_tris = [] 160 | for i in range(1, len(fv) - 1): 161 | # blender's and quake's vertex order are opposed 162 | face_tris.append([(fv[0], tuple(uv[0])), 163 | (fv[i + 1], tuple(uv[i + 1])), 164 | (fv[i], tuple(uv[i]))]) 165 | for ft in face_tris: 166 | tv = [] 167 | for vuv in ft: 168 | if vuv not in vuvdict: 169 | vuvdict[vuv] = len(stverts) 170 | vertmap.append(vuv[0]) 171 | stverts.append(vuv[1]) 172 | tv.append(vuvdict[vuv]) 173 | tris.append(MDL.Tri(tv)) 174 | return tris, stverts, vertmap 175 | 176 | 177 | def convert_stverts(mdl, stverts): 178 | for i, st in enumerate(stverts): 179 | s, t = st 180 | # quake textures are top to bottom, but blender images 181 | # are bottom to top 182 | s = round(s * (mdl.skinwidth - 1) + 0.5) 183 | t = round((1 - t) * (mdl.skinheight - 1) + 0.5) 184 | # ensure st is within the skin 185 | s = ((s % mdl.skinwidth) + mdl.skinwidth) % mdl.skinwidth 186 | t = ((t % mdl.skinheight) + mdl.skinheight) % mdl.skinheight 187 | 188 | stverts[i] = MDL.STVert((s, t)) 189 | 190 | 191 | def make_frame(mesh, vertmap, findex): 192 | frame = MDL.Frame() 193 | frame.name = "frame" + str(findex) 194 | 195 | if bpy.context.object.data.shape_keys: 196 | shape_keys_amount = len(bpy.context.object.data.shape_keys.key_blocks) 197 | if shape_keys_amount > findex: 198 | frame.name = bpy.context.object.data.shape_keys.key_blocks[round( 199 | findex)].name 200 | 201 | for v in vertmap: 202 | mv = mesh.vertices[v] 203 | vert = MDL.Vert(tuple(mv.co), map_normal(mv.normal)) 204 | frame.add_vert(vert) 205 | return frame 206 | 207 | 208 | def scale_verts(mdl): 209 | tf = MDL.Frame() 210 | for f in mdl.frames: 211 | tf.add_frame(f, 0.0) # let the frame class do the dirty work for us 212 | size = Vector(tf.maxs) - Vector(tf.mins) 213 | rsqr = tuple(map(lambda a, b: max(abs(a), abs(b)) ** 2, tf.mins, tf.maxs)) 214 | mdl.boundingradius = ((rsqr[0] + rsqr[1] + rsqr[2]) ** 0.5) 215 | mdl.scale_origin = tf.mins 216 | mdl.scale = tuple(map(lambda x: x / 255.0, size)) 217 | for f in mdl.frames: 218 | f.scale(mdl) 219 | 220 | 221 | def calc_average_area(mdl): 222 | frame = mdl.frames[0] 223 | if frame.type: 224 | frame = frame.frames[0] 225 | totalarea = 0.0 226 | for tri in mdl.tris: 227 | verts = tuple(map(lambda i: frame.verts[i], tri.verts)) 228 | a = Vector(verts[0].r) - Vector(verts[1].r) 229 | b = Vector(verts[2].r) - Vector(verts[1].r) 230 | c = a.cross(b) 231 | totalarea += (c @ c) ** 0.5 / 2.0 232 | return totalarea / len(mdl.tris) 233 | 234 | 235 | def parse_effects(fx_group): 236 | effects = fx_group.__annotations__.keys() 237 | flags = 0 238 | for i, v in enumerate(effects): 239 | fx = getattr(fx_group, v) 240 | if fx: 241 | flags += MDLEffects(v).value 242 | return flags 243 | 244 | 245 | def get_properties(operator, mdl, obj, export_scale): 246 | mdl.eyeposition = tuple( 247 | map(lambda v: v*export_scale, obj.qfmdl.eyeposition)) 248 | mdl.synctype = MDLSyncType[obj.qfmdl.synctype].value 249 | mdl.flags = parse_effects(obj.qfmdl.effects) 250 | if obj.qfmdl.md16: 251 | mdl.ident = "MD16" 252 | return True 253 | 254 | 255 | def process_skin(mdl, skin, palette, ingroup=False): 256 | if 'skins' in skin: 257 | if ingroup: 258 | raise ValueError("nested skin group") 259 | intervals = ['0.0'] 260 | if 'intervals' in skin: 261 | intervals += list(skin['intervals']) 262 | intervals = list(map(lambda x: float(x), intervals)) 263 | while len(intervals) < len(skin['skins']): 264 | intervals.append(intervals[-1] + 0.1) 265 | sk = MDL.Skin() 266 | sk.type = 1 267 | sk.times = intervals[1:len(skin['skins']) + 1] 268 | sk.skins = [] 269 | for s in skin['skins']: 270 | sk.skins.append(process_skin(mdl, s, palette, ingroup=True)) 271 | return sk 272 | else: 273 | # FIXME error handling 274 | name = skin['name'] 275 | image = bpy.data.images[name] 276 | if hasattr(mdl, 'skinwidth'): 277 | if (mdl.skinwidth != image.size[0] 278 | or mdl.skinheight != image.size[1]): 279 | raise ValueError("%s: different skin size (%d %d) (%d %d)" 280 | % (name, mdl.skinwidth, mdl.skinheight, 281 | int(image.size[0]), int(image.size[1]))) 282 | else: 283 | mdl.skinwidth, mdl.skinheight = image.size 284 | sk = convert_image(image, palette) 285 | return sk 286 | 287 | 288 | def process_frame(mdl, scene, frame, vertmap, ingroup=False, 289 | frameno=None, name='frame'): 290 | if frameno is None: 291 | frameno = scene.frame_current + scene.frame_subframe 292 | if 'frameno' in frame: 293 | frameno = float(frame['frameno']) 294 | if 'name' in frame: 295 | name = frame['name'] 296 | if 'frames' in frame: 297 | if ingroup: 298 | raise ValueError("nested frames group") 299 | intervals = ['0.0'] 300 | if 'intervals' in frame: 301 | intervals += list(frame['intervals']) 302 | intervals = list(map(lambda x: float(x), intervals)) 303 | while len(intervals) < len(frame['frames']) + 1: 304 | intervals.append(intervals[-1] + 0.1) 305 | fr = MDL.Frame() 306 | for i, f in enumerate(frame['frames']): 307 | fr.add_frame(process_frame(mdl, scene, f, vertmap, True, 308 | frameno + i, name + str(i + 1)), 309 | intervals[i + 1]) 310 | if 'intervals' in frame: 311 | return fr 312 | mdl.frames += fr.frames[:-1] 313 | return fr.frames[-1] 314 | scene.frame_set(int(frameno), subframe=(frameno - int(frameno))) 315 | mesh = mdl.obj.to_mesh(preserve_all_data_layers=True) # wysiwyg? 316 | if mdl.obj.qfmdl.xform: 317 | mesh.transform(mdl.obj.matrix_world) 318 | fr = make_frame(mesh, vertmap, frameno) 319 | fr.name = name 320 | return fr 321 | 322 | 323 | def export_mdl(operator, context, filepath, palette, export_scale): 324 | obj = context.active_object 325 | obj.update_from_editmode() 326 | depsgraph = context.evaluated_depsgraph_get() 327 | ob_eval = obj.evaluated_get(depsgraph) 328 | objname = ob_eval.name_full 329 | 330 | palette = getPaletteFromName(palette) 331 | 332 | mdl = MDL(obj.name) 333 | mdl.obj = obj 334 | if not get_properties(operator, mdl, obj, export_scale): 335 | return {'CANCELLED'} 336 | bpy.context.active_object.name = objname 337 | mesh = bpy.context.active_object.to_mesh() 338 | mdl.tris, mdl.stverts, vertmap = build_tris(mesh) 339 | if not mdl.skins or (mdl.skinwidth): 340 | make_skin(mdl, mesh, palette) 341 | if not mdl.frames: 342 | start_frame = context.scene.frame_start 343 | end_frame = context.scene.frame_end + 1 344 | for fnum in range(start_frame, end_frame): 345 | context.scene.frame_set(fnum) 346 | obj.update_from_editmode() 347 | depsgraph = context.evaluated_depsgraph_get() 348 | ob_eval = obj.evaluated_get(depsgraph) 349 | mesh = ob_eval.to_mesh() 350 | if mdl.obj.qfmdl.xform: 351 | mesh.transform(mdl.obj.matrix_world) 352 | eframe = make_frame(mesh, vertmap, fnum) 353 | mdl.frames.append(eframe) 354 | 355 | mdl.numverts = len(mdl.frames[0].verts) 356 | convert_stverts(mdl, mdl.stverts) 357 | mdl.scale_factor = export_scale 358 | mdl.size = calc_average_area(mdl) 359 | scale_verts(mdl) 360 | mdl.write(filepath) 361 | 362 | return {'FINISHED'} 363 | -------------------------------------------------------------------------------- /mdl.py: -------------------------------------------------------------------------------- 1 | # vim:ts=4:et 2 | # ##### BEGIN GPL LICENSE BLOCK ##### 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software Foundation, 16 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 17 | # 18 | # ##### END GPL LICENSE BLOCK ##### 19 | 20 | # 21 | 22 | from struct import unpack, pack 23 | from .constants import MDLEffects, MDLSyncType 24 | from .utils import read_byte, read_bytestring, read_float, read_int, read_string, read_ushort, write_byte, write_bytestring, write_float, write_int, write_string 25 | 26 | 27 | class MDL: 28 | class Skin: 29 | def __init__(self): 30 | self.name = '' 31 | 32 | def info(self): 33 | info = {} 34 | if self.type: 35 | if self.times: 36 | info['intervals'] = list(map(lambda t: str(t), self.times)) 37 | info['skins'] = [] 38 | for s in self.skins: 39 | info['skins'].append(s.info()) 40 | if self.name: 41 | info['name'] = self.name 42 | return info 43 | 44 | def read(self, mdl, sub=0): 45 | self.width, self.height = mdl.skinwidth, mdl.skinheight 46 | if sub: 47 | self.type = 0 48 | self.read_pixels(mdl) 49 | return self 50 | self.type = read_int(mdl.file) 51 | if self.type: 52 | # skin group 53 | num = read_int(mdl.file) 54 | self.times = read_float(mdl.file, num) 55 | self.skins = [] 56 | for _ in range(num): 57 | self.skins.append(MDL.Skin().read(mdl, 1)) 58 | num -= 1 59 | return self 60 | self.read_pixels(mdl) 61 | return self 62 | 63 | def write(self, mdl, sub=0): 64 | if not sub: 65 | write_int(mdl.file, self.type) 66 | if self.type: 67 | write_int(mdl.file, len(self.skins)) 68 | write_float(mdl.file, self.times) 69 | for subskin in self.skins: 70 | subskin.write(mdl, 1) 71 | return 72 | write_bytestring(mdl.file, self.pixels) 73 | 74 | def read_pixels(self, mdl): 75 | size = self.width * self.height 76 | self.pixels = read_bytestring(mdl.file, size) 77 | 78 | class STVert: 79 | def __init__(self, st=None, onseam=False): 80 | if not st: 81 | st = (0, 0) 82 | self.onseam = onseam 83 | self.s, self.t = st 84 | pass 85 | 86 | def read(self, mdl): 87 | self.onseam = read_int(mdl.file) 88 | self.s, self.t = read_int(mdl.file, 2) 89 | return self 90 | 91 | def write(self, mdl): 92 | write_int(mdl.file, self.onseam) 93 | write_int(mdl.file, (self.s, self.t)) 94 | 95 | class Tri: 96 | def __init__(self, verts=None, facesfront=True): 97 | if not verts: 98 | verts = (0, 0, 0) 99 | self.facesfront = facesfront 100 | self.verts = verts 101 | 102 | def read(self, mdl): 103 | self.facesfront = read_int(mdl.file) 104 | self.verts = read_int(mdl.file, 3) 105 | return self 106 | 107 | def write(self, mdl): 108 | write_int(mdl.file, self.facesfront) 109 | write_int(mdl.file, self.verts) 110 | 111 | class NTri: 112 | def __init__(self, verts=None, facesfront=True, stverts=None): 113 | if not verts: 114 | verts = (0, 0, 0) 115 | if not stverts: 116 | stverts = (0, 0, 0) 117 | 118 | self.facesfront = facesfront 119 | self.verts = verts 120 | self.stverts = stverts 121 | 122 | def read(self, mdl): 123 | self.facesfront = read_int(mdl.file) 124 | self.verts = read_ushort(mdl.file, 3) 125 | self.stverts = read_ushort(mdl.file, 3) 126 | return self 127 | 128 | def write(self, mdl): 129 | write_int(mdl.file, self.facesfront) 130 | write_int(mdl.file, self.verts) 131 | 132 | class Frame: 133 | def __init__(self): 134 | self.type = 0 135 | self.name = "" 136 | self.mins = [0, 0, 0] 137 | self.maxs = [0, 0, 0] 138 | self.verts = [] 139 | self.frames = [] 140 | self.times = [] 141 | 142 | def info(self): 143 | info = {} 144 | if self.type: 145 | if self.times: 146 | info['intervals'] = list(map(lambda t: str(t), self.times)) 147 | info['frames'] = [] 148 | for f in self.frames: 149 | info['frames'].append(f.info()) 150 | if hasattr(self, 'frameno'): 151 | info['frameno'] = str(self.frameno) 152 | if self.name: 153 | info['name'] = self.name 154 | return info 155 | 156 | def add_vert(self, vert): 157 | self.verts.append(vert) 158 | for i, v in enumerate(vert.r): 159 | self.mins[i] = min(self.mins[i], v) 160 | self.maxs[i] = max(self.maxs[i], v) 161 | 162 | def add_frame(self, frame, time): 163 | self.type = 1 164 | self.frames.append(frame) 165 | self.times.append(time) 166 | for i in range(3): 167 | self.mins[i] = min(self.mins[i], frame.mins[i]) 168 | self.maxs[i] = max(self.maxs[i], frame.maxs[i]) 169 | 170 | def scale(self, mdl): 171 | self.mins = tuple(map(lambda x, s, t: int((x - t) / s), 172 | self.mins, mdl.scale, mdl.scale_origin)) 173 | self.maxs = tuple(map(lambda x, s, t: int((x - t) / s), 174 | self.maxs, mdl.scale, mdl.scale_origin)) 175 | if self.type: 176 | for subframe in self.frames: 177 | subframe.scale(mdl) 178 | else: 179 | for vert in self.verts: 180 | vert.scale(mdl) 181 | 182 | def read(self, mdl, numverts, sub=0): 183 | if sub: 184 | self.type = 0 185 | else: 186 | self.type = read_int(mdl.file) 187 | if self.type: 188 | num = read_int(mdl.file) 189 | self.read_bounds(mdl) 190 | self.times = read_float(mdl.file, num) 191 | self.frames = [] 192 | for _ in range(num): 193 | self.frames.append(MDL.Frame().read(mdl, numverts, 1)) 194 | return self 195 | self.read_bounds(mdl) 196 | self.read_name(mdl) 197 | self.read_verts(mdl, numverts) 198 | return self 199 | 200 | def write(self, mdl, sub=0): 201 | if not sub: 202 | write_int(mdl.file, self.type) 203 | if self.type: 204 | write_int(mdl.file, len(self.frames)) 205 | self.write_bounds(mdl) 206 | write_float(mdl.file, self.times) 207 | for frame in self.frames: 208 | frame.write(mdl, 1) 209 | return 210 | self.write_bounds(mdl) 211 | self.write_name(mdl) 212 | self.write_verts(mdl) 213 | 214 | def read_name(self, mdl): 215 | if mdl.version >= 6: 216 | name = read_string(mdl.file, 16) 217 | else: 218 | name = "" 219 | if "\0" in name: 220 | name = name[:name.index("\0")] 221 | self.name = name 222 | 223 | def write_name(self, mdl): 224 | if mdl.version >= 6: 225 | write_string(mdl.file, self.name, 16) 226 | 227 | def read_bounds(self, mdl): 228 | self.mins = read_byte(mdl.file, 4)[:3] # discard normal index 229 | self.maxs = read_byte(mdl.file, 4)[:3] # discard normal index 230 | 231 | def write_bounds(self, mdl): 232 | write_byte(mdl.file, self.mins + (0,)) 233 | write_byte(mdl.file, self.maxs + (0,)) 234 | 235 | def read_verts(self, mdl, num): 236 | self.verts = [] 237 | for i in range(num): 238 | self.verts.append(MDL.Vert().read(mdl)) 239 | if mdl.ident == 'MD16': 240 | for i in range(num): 241 | v = MDL.Vert().read(mdl) 242 | r = tuple(map(lambda a, b: a + b / 256.0, 243 | self.verts[i].r, v.r)) 244 | self.verts[i].r = r 245 | 246 | def write_verts(self, mdl): 247 | for vert in self.verts: 248 | vert.write(mdl, True) 249 | if mdl.ident == 'MD16': 250 | for vert in self.verts: 251 | vert.write(mdl, False) 252 | 253 | class Vert: 254 | def __init__(self, r=None, ni=0): 255 | if not r: 256 | r = (0, 0, 0) 257 | self.r = r 258 | self.ni = ni 259 | pass 260 | 261 | def read(self, mdl): 262 | self.r = read_byte(mdl.file, 3) 263 | self.ni = read_byte(mdl.file) 264 | return self 265 | 266 | def write(self, mdl, high=True): 267 | if mdl.ident == 'MD16' and not high: 268 | r = tuple(map(lambda a: int(a * 256) & 255, self.r)) 269 | else: 270 | r = tuple(map(lambda a: int(a) & 255, self.r)) 271 | write_byte(mdl.file, r) 272 | write_byte(mdl.file, self.ni) 273 | 274 | def scale(self, mdl): 275 | self.r = tuple(map(lambda x, s, t: (x - t) / s, 276 | self.r, mdl.scale, mdl.scale_origin)) 277 | 278 | def __init__(self, name="mdl", md16=False): 279 | self.name = name 280 | self.ident = md16 and "MD16" or "IDPO" 281 | self.version = 6 # write only version 6 (nothing usable uses 3) 282 | self.scale = (1.0, 1.0, 1.0) # FIXME 283 | self.scale_origin = (0.0, 0.0, 0.0) # FIXME 284 | self.boundingradius = 1.0 # FIXME 285 | self.eyeposition = (0.0, 0.0, 0.0) # FIXME 286 | self.synctype = MDLSyncType["ST_SYNC"].value 287 | self.flags = 0 288 | self.size = 0 289 | self.skins = [] 290 | self.numverts = 0 291 | self.stverts = [] 292 | self.tris = [] 293 | self.frames = [] 294 | self.scale_factor = 1.0 295 | 296 | def read(self, filepath): 297 | # Reading MDL file header 298 | self.file = open(filepath, "rb") 299 | self.name = filepath.split('/')[-1] 300 | self.name = self.name.split('.')[0] 301 | self.ident = read_string(self.file, 4) 302 | self.version = read_int(self.file) 303 | if self.ident not in ["IDPO", "MD16", "RAPO"] or self.version not in [3, 6, 50]: 304 | return None 305 | self.scale = read_float(self.file, 3) 306 | self.scale_origin = read_float(self.file, 3) 307 | self.boundingradius = read_float(self.file) 308 | self.eyeposition = read_float(self.file, 3) 309 | numskins = read_int(self.file) 310 | self.skinwidth, self.skinheight = read_int(self.file, 2) 311 | numverts, numtris, numframes = read_int(self.file, 3) 312 | self.synctype = read_int(self.file) 313 | if self.version >= 6: 314 | self.flags = read_int(self.file) 315 | self.size = read_float(self.file) 316 | 317 | if self.version == 6: 318 | self.num_st_verts = numverts 319 | if self.version == 50: 320 | self.num_st_verts = read_int(self.file) 321 | 322 | # read in the skin data 323 | self.skins = [] 324 | for _ in range(numskins): 325 | self.skins.append(MDL.Skin().read(self)) 326 | 327 | # read in the st verts (uv map) 328 | self.stverts = [] 329 | for _ in range(self.num_st_verts): 330 | self.stverts.append(MDL.STVert().read(self)) 331 | # read in the tris 332 | self.tris = [] 333 | if (self.version < 50): 334 | for _ in range(numtris): 335 | self.tris.append(MDL.Tri().read(self)) 336 | else: 337 | for _ in range(numtris): 338 | self.tris.append(MDL.NTri().read(self)) 339 | # read in the frames 340 | self.frames = [] 341 | for _ in range(numframes): 342 | self.frames.append(MDL.Frame().read(self, numverts)) 343 | return self 344 | 345 | def write(self, filepath): 346 | self.file = open(filepath, "wb") 347 | write_string(self.file, self.ident, 4) 348 | write_int(self.file, self.version) 349 | write_float(self.file, tuple( 350 | v * self.scale_factor for v in self.scale)) 351 | write_float(self.file, list( 352 | v * self.scale_factor for v in self.scale_origin)) 353 | write_float(self.file, self.boundingradius * self.scale_factor) 354 | write_float(self.file, tuple( 355 | v * self.scale_factor for v in self.eyeposition)) 356 | write_int(self.file, len(self.skins)) 357 | write_int(self.file, self.skinwidth) 358 | write_int(self.file, self.skinheight) 359 | write_int(self.file, self.numverts) 360 | write_int(self.file, len(self.tris)) 361 | write_int(self.file, len(self.frames)) 362 | write_int(self.file, self.synctype) 363 | if self.version >= 6: 364 | write_int(self.file, self.flags) 365 | write_float(self.file, self.size) 366 | # write out the skin data 367 | for skin in self.skins: 368 | skin.write(self) 369 | # write out the st verts (uv map) 370 | for stvert in self.stverts: 371 | stvert.write(self) 372 | # write out the tris 373 | for tri in self.tris: 374 | tri.write(self) 375 | # write out the frames 376 | for frame in self.frames: 377 | frame.write(self) 378 | -------------------------------------------------------------------------------- /import_mdl.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | import bpy 20 | import importlib 21 | from bpy_extras.object_utils import object_data_add 22 | from mathutils import Vector, Matrix 23 | 24 | from .constants import MDLEffects, MDLSyncType 25 | from .mdl import MDL 26 | from .qfplist import pldata 27 | 28 | 29 | def make_verts(mdl, framenum, subframenum=0): 30 | ''' 31 | Create vertices for the specified frame. If frame type is truthy, 32 | the current frame is a group of frames and will load the frame 33 | group instead of the single frame. 34 | ''' 35 | 36 | frame = mdl.frames[framenum] 37 | if frame.type: 38 | frame = frame.frames[subframenum] 39 | verts = [] 40 | s = Vector([v for v in mdl.scale]) * mdl.scale_factor 41 | o = Vector([v for v in mdl.scale_origin]) * mdl.scale_factor 42 | m = Matrix(( 43 | (s.x, 0, 0, o.x), 44 | (0, s.y, 0, o.y), 45 | (0, 0, s.z, o.z), 46 | (0, 0, 0, 1) 47 | )) 48 | for v in frame.verts: 49 | verts.append(m @ Vector(v.r)) 50 | return verts 51 | 52 | 53 | def make_faces(mdl): 54 | ''' 55 | Create the faces according to the mdl description, referencing 56 | the verts created. 57 | This also creates the uv coords for the faces 58 | ''' 59 | faces = [] 60 | uvs = [] 61 | for tri in mdl.tris: 62 | # list of vertices in a tri 63 | tv = list(tri.verts) 64 | sts = [] # UV coords 65 | if mdl.version < 50: 66 | for v in tri.verts: 67 | # UV vertex 68 | stv = mdl.stverts[v] 69 | s = stv.s 70 | t = stv.t 71 | if stv.onseam and not tri.facesfront: 72 | s += mdl.skinwidth / 2 73 | # quake textures are top to bottom, but blender images 74 | # are bottom to top 75 | sts.append(( 76 | s * 1.0 / mdl.skinwidth, 77 | 1 - t * 1.0 / mdl.skinheight, 78 | )) 79 | else: 80 | for v in tri.stverts: 81 | # UV vertex from a mdl v50 82 | stv = mdl.stverts[v] 83 | s, t = (stv.s, stv.t) 84 | if stv.onseam and not tri.facesfront: 85 | s += mdl.skinwidth / 2 86 | sts.append(( 87 | s * 1.0 / mdl.skinwidth, 88 | 1 - t * 1.0 / mdl.skinheight, 89 | )) 90 | 91 | # blender's and quake's vertex order seem to be opposed 92 | tv.reverse() 93 | sts.reverse() 94 | # annoyingly, blender can't have 0 in the final vertex, so rotate the 95 | # face vertices and uvs 96 | if not tv[2]: 97 | tv = [tv[2]] + tv[:2] 98 | sts = [sts[2]] + sts[:2] 99 | faces.append(tv) 100 | uvs.append(sts) 101 | return faces, uvs 102 | 103 | 104 | def load_skins(mdl, palette): 105 | ''' 106 | Loads the texture pixel of the MDL model 107 | ''' 108 | def load_skin(skin, name): 109 | skin.name = name 110 | img = bpy.data.images.new(name, mdl.skinwidth, mdl.skinheight) 111 | mdl.images.append(img) 112 | p = [0.0] * mdl.skinwidth * mdl.skinheight * 4 113 | d = skin.pixels 114 | for j in range(mdl.skinheight): 115 | for k in range(mdl.skinwidth): 116 | c = palette[d[j * mdl.skinwidth + k]] 117 | # quake textures are top to bottom, but blender images 118 | # are bottom to top 119 | ln = ((mdl.skinheight - 1 - j) * mdl.skinwidth + k) * 4 120 | p[ln + 0] = c[0] / 255.0 # Red 121 | p[ln + 1] = c[1] / 255.0 # Green 122 | p[ln + 2] = c[2] / 255.0 # Blue 123 | p[ln + 3] = 1.0 # Alpha 124 | 125 | img.pixels[:] = p[:] 126 | img.pack() 127 | img.use_fake_user = True 128 | 129 | mdl.images = [] 130 | for i, skin in enumerate(mdl.skins): 131 | if skin.type: 132 | for j, subskin in enumerate(skin.skins): 133 | load_skin(subskin, "%s_%d_%d" % (mdl.name, i, j)) 134 | else: 135 | load_skin(skin, "%s_%d" % (mdl.name, i)) 136 | 137 | 138 | def setup_skins(mdl, uvs, palette): 139 | ''' 140 | Setup skin slots and materials and sets UV coordinates in 141 | the loaded texture 142 | ''' 143 | load_skins(mdl, palette) 144 | img = mdl.images[0] # use the first skin for now 145 | mdl.mesh.uv_layers.new(name=mdl.name) 146 | uvloop = mdl.mesh.uv_layers[0] 147 | 148 | # UV Loading============= 149 | for i, _ in enumerate(uvs): 150 | poly = mdl.mesh.polygons[i] 151 | mdl_uv = uvs[i] 152 | for j, k in enumerate(poly.loop_indices): 153 | uvloop.data[k].uv = mdl_uv[j] 154 | 155 | # Material and texture loading======= 156 | mat = bpy.data.materials.new(mdl.name) 157 | mat.use_nodes = True 158 | 159 | mat_out = mat.node_tree.nodes['Material Output'] 160 | emi = mat.node_tree.nodes.new('ShaderNodeEmission') 161 | tex = mat.node_tree.nodes.new('ShaderNodeTexImage') 162 | tex.image = img 163 | tex.interpolation = 'Closest' 164 | 165 | mat.node_tree.links.new(emi.inputs['Color'], tex.outputs['Color']) 166 | mat.node_tree.links.new(mat_out.inputs['Surface'], emi.outputs['Emission']) 167 | 168 | # Remove old BSDF node 169 | bsdf = mat.node_tree.nodes['Principled BSDF'] 170 | mat.node_tree.nodes.remove(bsdf) 171 | 172 | mdl.mesh.materials.append(mat) 173 | 174 | 175 | def make_shape_key(mdl, framenum, subframenum=0): 176 | ''' 177 | Construct a shape key for the particular frame or subframe in 178 | the Blender model 179 | ''' 180 | frame = mdl.frames[framenum] 181 | name = "%s_%d" % (mdl.name, framenum) 182 | if frame.type: 183 | frame = frame.frames[subframenum] 184 | name = "%s_%d_%d" % (mdl.name, framenum, subframenum) 185 | if frame.name: 186 | name = frame.name 187 | else: 188 | frame.name = name 189 | frame.key = mdl.obj.shape_key_add(name=name) 190 | frame.key.value = 0.0 191 | mdl.keys.append(frame.key) 192 | s = Vector([v for v in mdl.scale]) * mdl.scale_factor 193 | o = Vector([v for v in mdl.scale_origin]) * mdl.scale_factor 194 | m = Matrix(((s.x, 0, 0, o.x), 195 | (0, s.y, 0, o.y), 196 | (0, 0, s.z, o.z), 197 | (0, 0, 0, 1))) 198 | for i, v in enumerate(frame.verts): 199 | frame.key.data[i].co = m @ Vector(v.r) 200 | 201 | 202 | def build_shape_keys(mdl): 203 | ''' 204 | Build all the shape keys of a MDL frames into the Blender model 205 | ''' 206 | mdl.keys = [] 207 | mdl.obj.shape_key_add(name="Basis") 208 | mdl.mesh.shape_keys.name = mdl.name 209 | mdl.obj.active_shape_key_index = 0 210 | for i, frame in enumerate(mdl.frames): 211 | frame = mdl.frames[i] 212 | if frame.type: 213 | for j in range(len(frame.frames)): 214 | make_shape_key(mdl=mdl, framenum=i, 215 | subframenum=j) 216 | else: 217 | make_shape_key(mdl=mdl, framenum=i) 218 | 219 | 220 | def set_keys(act, data): 221 | ''' 222 | Set keyframe of animation 223 | ''' 224 | for d in data: 225 | key, co = d 226 | dp = """key_blocks["%s"].value""" % key.name 227 | fc = act.fcurves.new(data_path=dp) 228 | fc.keyframe_points.add(len(co)) 229 | for i in range(len(co)): 230 | fc.keyframe_points[i].co = co[i] 231 | fc.keyframe_points[i].interpolation = 'LINEAR' 232 | 233 | 234 | def build_actions(mdl): 235 | ''' 236 | Build animation actions 237 | ''' 238 | sk = mdl.mesh.shape_keys 239 | ad = sk.animation_data_create() 240 | track = ad.nla_tracks.new() 241 | track.name = mdl.name 242 | start_frame = 1 243 | for frame in mdl.frames: 244 | act = bpy.data.actions.new(frame.name) 245 | data = [] 246 | other_keys = mdl.keys[:] 247 | if frame.type: 248 | for j, subframe in enumerate(frame.frames): 249 | subframe.frameno = start_frame + j 250 | co = [] 251 | if j > 1: 252 | co.append((1.0, 0.0)) 253 | if j > 0: 254 | co.append((j * 1.0, 0.0)) 255 | co.append(((j + 1) * 1.0, 1.0)) 256 | if j < len(frame.frames) - 2: 257 | co.append(((j + 2) * 1.0, 0.0)) 258 | if j < len(frame.frames) - 1: 259 | co.append((len(frame.frames) * 1.0, 0.0)) 260 | data.append((subframe.key, co)) 261 | if subframe.key in other_keys: 262 | del (other_keys[other_keys.index(subframe.key)]) 263 | co = [(1.0, 0.0), (len(frame.frames) * 1.0, 0.0)] 264 | for k in other_keys: 265 | data.append((k, co)) 266 | else: 267 | subframe.frameno = start_frame + j 268 | data.append((frame.key, [(1.0, 1.0)])) 269 | if frame.key in other_keys: 270 | del (other_keys[other_keys.index(frame.key)]) 271 | co = [(1.0, 0.0)] 272 | for k in other_keys: 273 | data.append((k, co)) 274 | set_keys(act, data) 275 | track.strips.new(act.name, start_frame, act) 276 | start_frame += int(act.frame_range[1]) 277 | 278 | 279 | def merge_frames(mdl): 280 | def get_base(name): 281 | i = 0 282 | while i < len(name) and name[i] not in "0123456789": 283 | i += 1 284 | return name[:i] 285 | 286 | i = 0 287 | while i < len(mdl.frames): 288 | if mdl.frames[i].type: 289 | i += 1 290 | continue 291 | base = get_base(mdl.frames[i].name) 292 | j = i + 1 293 | while j < len(mdl.frames): 294 | if mdl.frames[j].type: 295 | break 296 | if get_base(mdl.frames[j].name) != base: 297 | break 298 | j += 1 299 | f = MDL.Frame() 300 | f.name = base 301 | f.type = 1 302 | f.frames = mdl.frames[i:j] 303 | mdl.frames[i:j] = [f] 304 | i += 1 305 | 306 | 307 | def write_text(mdl): 308 | ''' 309 | Creates text animation and configuration file for the imported model 310 | ''' 311 | header = """ 312 | /* This script represents the animation data within the model file. It 313 | is generated automatically on import, and is optional when exporting. 314 | If no script is used when exporting, frames will be exported one per 315 | blender frame from frame 1 to the current frame (inclusive), and only 316 | one skin will be exported. 317 | 318 | The fundamental format of the script is documented at 319 | http://quakeforge.net/doxygen/property-list.html 320 | 321 | The expected layout is a top-level dictionary with two expected 322 | entries: 323 | frames array of frame entries. If missing, frames will be handled 324 | as if there were no script. 325 | skins array of skin entries. If missing, skins will be handled 326 | as if there were no script. 327 | 328 | A frame entry is a dictionary with the following fields: 329 | name The name of the frame to be written to the mdl file. In a 330 | frame group, this will form the base for sub-frame names 331 | (name + relative frame number: eg, frame1) if the 332 | sub-frame does not have a name field. (string) 333 | frameno The blender frame to use for the captured animation. In a 334 | frame group, this will be used as the base frame for any 335 | sub-frames that do not specify a frame. While fractional 336 | frames are supported, YMMV. (string:float) 337 | frames Array of frame entries. If present, the current frame 338 | entry is a frame group, and the frame entries specify 339 | sub-frames. (array of dictionary) 340 | NOTE: only top-level frames may be frame groups 341 | intervals Array of frame end times for frame groups. No meaning 342 | in blender, but the quake engine uses them for client-side 343 | animations. Times must be ascending, but any step > 0 is 344 | valid. Ignored for single frames. If not present in a 345 | frame group, the sub-frames of the group will be written 346 | as single frames (in order to undo the auto-group feature 347 | of the importer). Excess times will be ignored, missing 348 | times will be generated at 0.1 349 | second intervals. 350 | (array of string:float). 351 | 352 | A skin entry is a dictionary with the following fields: 353 | name The name of the blender image to be used as the skin. 354 | Ignored for skin groups (animated skins). (string) 355 | skins Array of skin entries. If present, the current skin 356 | entry is a skin group (animated skin), and the skin 357 | entries specify sub-skin. (array of dictionary) 358 | NOTE: only top-level skins may be skins groups 359 | intervals Array of skin end times for skin groups. No meaning 360 | in blender, but the quake engine uses them for client-side 361 | animations. Times must be ascending, but any step > 0 is 362 | valid. Ignored for single skins. If not present in a 363 | skin group, it will be generated using 0.1 second 364 | intervals. Excess times will be ignored, missing times 365 | will be generated at 0.1 second intervals. 366 | (array of string:float). 367 | */ 368 | """ 369 | d = {'frames': [], 'skins': []} 370 | for f in mdl.frames: 371 | d['frames'].append(f.info()) 372 | for s in mdl.skins: 373 | d['skins'].append(s.info()) 374 | pl = pldata() 375 | string = header + pl.write(d) 376 | 377 | txt = bpy.data.texts.new(mdl.name) 378 | txt.from_string(string) 379 | mdl.text = txt 380 | 381 | 382 | def parse_flags(fx_group, flags): 383 | effects = fx_group.__annotations__.items() 384 | for i, (key, v) in enumerate(effects): 385 | setattr(fx_group, key, 386 | (flags & MDLEffects[v.keywords['name']].value > 0)) 387 | 388 | 389 | def set_properties(mdl): 390 | mdl.obj.qfmdl.eyeposition = tuple( 391 | map(lambda v: v * mdl.scale_factor, mdl.eyeposition)) 392 | try: 393 | mdl.obj.qfmdl.synctype = MDLSyncType(mdl.synctype).name 394 | except IndexError: 395 | mdl.obj.qfmdl.synctype = 'ST_SYNC' 396 | 397 | parse_flags(mdl.obj.qfmdl.effects, mdl.flags) 398 | mdl.obj.qfmdl.script = mdl.text.name # FIXME really want the text object 399 | mdl.obj.qfmdl.md16 = (mdl.ident == "MD16") 400 | 401 | 402 | def import_mdl(operator, context, filepath, palette, import_scale): 403 | bpy.context.preferences.edit.use_global_undo = False 404 | 405 | palette_module_name = "..{0}pal".format(palette.lower()) 406 | palette = importlib.import_module(palette_module_name, __name__).palette 407 | 408 | for obj in bpy.context.scene.objects: 409 | obj.select_set(False) 410 | 411 | mdl = MDL() 412 | if not mdl.read(filepath): 413 | operator.report({'ERROR'}, 414 | "Unrecognized format: %s %d" % (mdl.ident, mdl.version)) 415 | return {'CANCELLED'} 416 | faces, uvs = make_faces(mdl) 417 | verts = make_verts(mdl, 0) 418 | mdl.mesh = bpy.data.meshes.new(mdl.name) 419 | mdl.mesh.from_pydata(verts, [], faces) 420 | mdl.obj = bpy.data.objects.new(mdl.name, mdl.mesh) 421 | coll = context.view_layer.active_layer_collection.collection 422 | coll.objects.link(mdl.obj) 423 | bpy.context.view_layer.objects.active = mdl.obj 424 | mdl.obj.select_set(True) 425 | setup_skins(mdl, uvs, palette) 426 | mdl.scale_factor = import_scale 427 | if len(mdl.frames) > 1 or mdl.frames[0].type: 428 | build_shape_keys(mdl) 429 | merge_frames(mdl) 430 | build_actions(mdl) 431 | write_text(mdl) 432 | set_properties(mdl) 433 | 434 | mdl.mesh.update() 435 | 436 | bpy.context.preferences.edit.use_global_undo = True 437 | return {'FINISHED'} 438 | -------------------------------------------------------------------------------- /version_cheatsheet.md: -------------------------------------------------------------------------------- 1 | Blender 2.80 add-on update cheat sheet 2 | ??? = not verified or not correct in many cases, but may work 3 | *** = verified to work in most or all cases 4 | 5 | ??? alpha needed 6 | -mat.diffuse_color = (red_v, grn_v, blu_v) # viewport color 7 | +mat.diffuse_color = (red_v, grn_v, blu_v, alpha_v) # viewport color 8 | 9 | ??? viewnumpad 10 | -col.operator("view3d.viewnumpad", text="View Camera", icon='CAMERA_DATA').type = 'CAMERA' 11 | +col.operator("view3d.view_camera", text="View Camera", icon='CAMERA_DATA') 12 | 13 | ??? viewnumpad > view_axis 14 | -bpy.ops.view3d.viewnumpad(type='BOTTOM') 15 | +bpy.ops.view3d.view_axis(type='BOTTOM') 16 | 17 | ??? get_rna 18 | -properties = op.get_rna().bl_rna.properties.items() 19 | +properties = op.get_rna_type().properties.items() 20 | 21 | ??? viewport_shade > shading.type 22 | -pie.prop(context.space_data, "viewport_shade", expand=True) 23 | +pie.prop(context.space_data.shading, "type", expand=True) 24 | -if context.space_data.viewport_shade == 'RENDERED': 25 | +if context.space_data.shading.type == 'RENDERED': 26 | 27 | ??? TOOLS > UI (Tools panel > N panel) 28 | -bl_region_type = "TOOLS" 29 | +bl_region_type = "UI" 30 | 31 | ??? View 32 | -bl_region_type = 'View' 33 | -bl_category = 'Tools' 34 | +bl_region_type = 'UI' 35 | +bl_category = 'View' 36 | otherwise... 37 | RuntimeError: Error: Registering panel class: 'view3d.blah' has category 'View' 38 | 39 | ??? Lamp > Light 40 | -"OUTPUT_LAMP" 41 | -"ShaderNodeOutputLamp" 42 | +"OUTPUT_LIGHT" 43 | +"ShaderNodeOutputLight" 44 | 45 | ??? lamps > lights 46 | -bpy.data.lamps 47 | +bpy.data.lights 48 | 49 | *** select > select_set (only changed for objects, not verts/edges/faces) 50 | -obj.select = False 51 | +obj.select_set(False) 52 | otherwise... 53 | AttributeError: 'Object' object has no attribute 'select' 54 | possible regex: 55 | find: (\.select\s*=\s*)(True|False) 56 | repl: .select_set\(\2\) 57 | 58 | *** select > select_get (only changed for objects, not verts/edges/faces) 59 | -if o.select: 60 | +if o.select_get(): 61 | -if not obj.select: 62 | +if not obj.select_get(): 63 | -if o.select is True: 64 | +if o.select_get() is True: 65 | possible regex: 66 | find: (\.select)(\s*)(is|==)(\s*)(True|False) 67 | repl: .select_get\(\)(\2)(\3)(\4)(\5) 68 | 69 | ??? backdrop_x / backdrop_y > backdrop_offset[0] / backdrop_offset[1] 70 | -context.space_data.backdrop_x = 0 71 | -context.space_data.backdrop_y = 0 72 | +context.space_data.backdrop_offset[0] = 0 73 | +context.space_data.backdrop_offset[1] = 0 74 | 75 | ??? ops.delete context (int > string) 76 | -bmesh.ops.delete(bm, geom=edges, context=1) 77 | -bmesh.ops.delete(bm, geom=edges, context=2) 78 | -bmesh.ops.delete(bm, geom=edges, context=3) 79 | +bmesh.ops.delete(bm, geom=edges, context="VERTS") 80 | +bmesh.ops.delete(bm, geom=edges, context="EDGES") 81 | +bmesh.ops.delete(bm, geom=edges, context="FACES") 82 | otherwise... 83 | TypeError: delete: keyword "context" expected a string, not int 84 | 85 | ??? tessface > loop_triangles 86 | -bmesh.update_edit_mesh(object.data, tessface=True, destructive=True) 87 | +bmesh.update_edit_mesh(object.data, loop_triangles=True, destructive=True) 88 | 89 | ??? to_mesh 90 | ob = bpy.context.active_object 91 | depsgraph = bpy.context.evaluated_depsgraph_get() 92 | -mesh = ob.evaluated_get(depsgraph).to_mesh() 93 | +ob_eval = ob.evaluated_get(depsgraph) 94 | +mesh = ob_eval.to_mesh() 95 | 96 | ??? to_mesh (2) 97 | obj = bpy.context.active_object 98 | -mesh = obj.to_mesh(context.scene, True, 'PREVIEW') 99 | +depsgraph = context.evaluated_depsgraph_get() 100 | +mesh = obj.evaluated_get(depsgraph).to_mesh() 101 | 102 | ??? meshes.remove 103 | -bpy.data.meshes.remove(mesh) 104 | +ob_eval.to_mesh_clear() 105 | 106 | ??? active 107 | -plane = context.scene.objects.active 108 | +plane = context.active_object 109 | 110 | ??? active (2) 111 | -bpy.context.scene.objects.active = obj 112 | +bpy.context.view_layer.objects.active = obj 113 | otherwise... 114 | AttributeError: bpy_prop_collection: attribute "active" not found 115 | #bpy.context.view_layer.objects.active = bpy.data.objects['Light'] ?? 116 | #bpy.context.view_layer.objects.active = None ?? 117 | #bpy.context.active_object ? 118 | 119 | ??? object_bases 120 | -context.scene.object_bases 121 | +obj = context.view_layer.objects.active ??? 122 | 123 | ??? keymaps: name, space_type, region_type (keyword) 124 | -def keymap(self, name="Window", space_type='EMPTY', region_type='WINDOW'): 125 | +def keymap(self, name_="Window", space_type_='EMPTY', region_type_='WINDOW'): 126 | self.km = bpy.context.window_manager.keyconfigs.addon.keymaps.new( 127 | - name, space_type, region_type) 128 | + name=name_, space_type=space_type_, region_type=region_type_) 129 | otherwise... 130 | TypeError: KeyMaps.new(): required parameter "space_type" to be a keyword argument! 131 | 132 | ??? frame_set subframe (keyword) 133 | -context.scene.frame_set(int(frame[1]), frame[0]) 134 | +context.scene.frame_set(int(frame[1]), subframe=frame[0]) 135 | 136 | ??? prop text (keyword) 137 | -row.prop(self, "filter_auto_focus", "", icon='VIEWZOOM') 138 | +row.prop(self, "filter_auto_focus", text="", icon='VIEWZOOM') 139 | 140 | ??? label text (keyword) 141 | -row.label(context.object.name) 142 | +row.label(text=context.object.name) 143 | -box.label("From curve") 144 | +box.label(text="From curve") 145 | 146 | *** label text 147 | -col.label("Sharpness") 148 | +col.label(text="Sharpness") 149 | otherwise... 150 | TypeError: UILayout.label(): required parameter "text" to be a keyword argument! 151 | 152 | ??? popup_menu title (keyword) 153 | -context.window_manager.popup_menu(self.menu, "Icon Viewer") 154 | +context.window_manager.popup_menu(self.menu, title="Icon Viewer") 155 | 156 | ??? operator text (keyword) 157 | -row.operator(IV_OT_panel_menu_call.bl_idname, "", icon='COLLAPSEMENU') 158 | +row.operator(IV_OT_panel_menu_call.bl_idname, text="", icon='COLLAPSEMENU') 159 | 160 | *** operator text (keyword) 161 | -row.operator("wm.url_open", "Google", icon='SOLO_ON' 162 | - ).url = "https://www.google.com" 163 | +row.operator("wm.url_open", text="Google", icon='SOLO_ON' 164 | + ).url = "https://www.google.com" 165 | 166 | *** prop text (keyword) 167 | -row.prop(self, "ctrl", "Ctrl", toggle=True) 168 | +row.prop(self, "ctrl", text="Ctrl", toggle=True) 169 | 170 | *** prop text (keyword) 171 | -col.prop(self.overlay, "alignment", "") 172 | +col.prop(self.overlay, "alignment", text="") 173 | 174 | ??? show_x_ray > show_in_front 175 | -col.prop(context.active_object, 'show_x_ray', toggle=False, text='X Ray') 176 | +col.prop(context.active_object, 'show_in_front', toggle=False, text='X Ray') 177 | 178 | ??? hide > hide_viewport 179 | -bpy.context.object.hide 180 | +bpy.context.object.hide_viewport 181 | 182 | ??? Group > Collection 183 | -bpy.types.Group() 184 | +bpy.types.Collection() 185 | 186 | ??? groups > collections 187 | -bpy.data.groups 188 | +bpy.data.collections 189 | 190 | ??? dupli_group > instance_collection 191 | -context.active_object.dupli_group 192 | +context.active_object.instance_collection 193 | 194 | ??? link 195 | -scene.objects.link(newCurve) 196 | +coll = context.view_layer.active_layer_collection.collection 197 | +coll.objects.link(newCurve) 198 | 199 | ??? link 200 | -bpy.context.scene.objects.link(newCurve) 201 | +bpy.context.collection.objects.link(newCurve) 202 | otherwise... 203 | AttributeError: 'bpy_prop_collection' object has no attribute 'link' 204 | 205 | ??? unlink 206 | -bpy.context.scene.objects.unlink(obj) 207 | +bpy.context.collection.objects.unlink(obj) 208 | 209 | ??? draw_type > display_type 210 | -bpy.context.object.data.draw_type = 'BBONE' 211 | +bpy.context.object.data.display_type = 'BBONE' 212 | 213 | ??? draw_size > display_size 214 | -bpy.data.cameras[cam_data_name].draw_size = 1.0 215 | +bpy.data.cameras[cam_data_name].display_size = 1.0 216 | 217 | ??? uv_textures name 218 | -me.uv_textures.new("Leaves") 219 | +me.uv_layers.new(name="Leaves") 220 | 221 | ??? transform_apply location rotation scale 222 | -bpy.ops.object.transform_apply(rotation=True, scale=True) 223 | +bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) 224 | 225 | ??? ops _add layers 226 | -bpy.ops.mesh.primitive_cube_add(view_align=False, enter_editmode=False, 227 | - location=(0.0, 0.0, 0.0), rotation=(0.0, 0.0, 0.0), layers=current_layers) 228 | +bpy.ops.mesh.primitive_cube_add(view_align=False, enter_editmode=False, 229 | + location=(0.0, 0.0, 0.0), rotation=(0.0, 0.0, 0.0)) 230 | 231 | ??? view_align 232 | bpy.ops.curve.primitive_nurbs_path_add( 233 | - view_align=False, enter_editmode=False, location=(0, 0, 0) 234 | + align='WORLD', enter_editmode=False, location=(0, 0, 0) 235 | ) 236 | 237 | ??? mesh.primitive: radius > size 238 | -bpy.ops.mesh.primitive_ico_sphere_add(size=0.2) 239 | +bpy.ops.mesh.primitive_ico_sphere_add(radius=0.4) 240 | 241 | ??? space_data transform_orientation 242 | -orientation = bpy.context.space_data.transform_orientation 243 | -custom = bpy.context.space_data.current_orientation 244 | -bpy.context.space_data.transform_orientation = 'GLOBAL' 245 | +orient_slot = bpy.context.scene.transform_orientation_slots[0] 246 | +custom = orient_slot.custom_orientation 247 | +bpy.context.scene.transform_orientation_slots[0].type = 'GLOBAL' 248 | otherwise... 249 | AttributeError: 'SpaceView3D' object has no attribute 'transform_orientation' 250 | 251 | ??? show_manipulator > show_gizmo 252 | -bpy.context.space_data.show_manipulator = False 253 | +bpy.context.space_data.show_gizmo = True 254 | otherwise... 255 | AttributeError: 'SpaceView3D' object has no attribute 'show_manipulator' 256 | 257 | ??? translate proportional (str) > use_proportional_edit (bool) 258 | -bpy.ops.transform.translate(proportional='DISABLED') 259 | +bpy.ops.transform.translate(use_proportional_edit=False) 260 | 261 | ??? constraint_orientation > orient_type 262 | -bpy.ops.transform.resize('INVOKE_DEFAULT', constraint_axis=axis, constraint_orientation='GLOBAL') 263 | +bpy.ops.transform.resize('INVOKE_DEFAULT', constraint_axis=axis, orient_type='GLOBAL') 264 | 265 | *** user_preferences > preferences 266 | -addons = bpy.context.user_preferences.addons 267 | +addons = bpy.context.preferences.addons 268 | 269 | ??? user_preferences 270 | -bpy.context.user_preferences.system.use_weight_color_range 271 | +bpy.context.preferences.view.use_weight_color_range 272 | "input.active_keyconfig", "keymap.active_keyconfig" 273 | "input.show_ui_keyconfig", "keymap.show_ui_keyconfig" 274 | "system.author", "filepaths.author" 275 | "system.color_picker_type", "view.color_picker_type" 276 | "system.font_path_ui", "view.font_path_ui" 277 | "system.font_path_ui_mono", "view.font_path_ui_mono" 278 | "system.language", "view.language" 279 | "system.text_hinting", "view.text_hinting" 280 | "system.use_international_fonts", "view.use_international_fonts" 281 | "system.use_scripts_auto_execute", "filepaths.use_scripts_auto_execute" 282 | "system.use_tabs_as_spaces", "filepaths.use_tabs_as_spaces" 283 | "system.use_text_antialiasing", "view.use_text_antialiasing" 284 | "system.use_translate_interface", "view.use_translate_interface" 285 | "system.use_translate_new_dataname", "view.use_translate_new_dataname" 286 | "system.use_translate_tooltips", "view.use_translate_tooltips" 287 | "system.use_weight_color_range", "view.use_weight_color_range" 288 | "system.weight_color_range", "view.weight_color_range" 289 | "view.use_auto_perspective", "input.use_auto_perspective" 290 | "view.use_camera_lock_parent", "input.use_camera_lock_parent" 291 | "view.use_cursor_lock_adjust", "input.use_cursor_lock_adjust" 292 | "view.use_mouse_depth_cursor", "input.use_mouse_depth_cursor" 293 | "view.use_mouse_depth_navigate", "input.use_mouse_depth_navigate" 294 | "view.use_quit_dialog", "view.use_save_prompt" 295 | "view.use_rotate_around_active", "input.use_rotate_around_active" 296 | "view.use_zoom_to_mouse", "input.use_zoom_to_mouse" 297 | 298 | ??? wm.addon_enable > preferences.addon_enable 299 | -bpy.ops.wm.addon_enable(module=module) 300 | +bpy.ops.preferences.addon_enable(module=module) 301 | 302 | ??? wm.addon_userpref_show > preferences.addon_show 303 | -layout.operator("wm.addon_userpref_show", 304 | +layout.operator("preferences.addon_show", 305 | 306 | ??? select_mouse 307 | -mouse_right = context.user_preferences.inputs.select_mouse 308 | +wm = context.window_manager 309 | +keyconfig = wm.keyconfigs.active 310 | +mouse_right = getattr(keyconfig.preferences, "select_mouse", "LEFT") 311 | 312 | *** split: percentage > factor 313 | -split = row.split(percentage=0.10, align=True) 314 | +split = row.split(factor=0.10, align=True) 315 | 316 | *** row: align (keyword) 317 | -row = col.row(True) 318 | +row = col.row(align=True) 319 | otherwise... 320 | TypeError: UILayout.row(): required parameter "align" to be a keyword argument! 321 | 322 | *** scene_update_pre > depsgraph_update_pre 323 | -bpy.app.handlers.scene_update_pre 324 | +bpy.app.handlers.depsgraph_update_pre 325 | 326 | *** scene_update_post > depsgraph_update_post 327 | -bpy.app.handlers.scene_update_post.append(check_drivers) 328 | +bpy.app.handlers.depsgraph_update_post.append(check_drivers) 329 | 330 | ??? scene.update > view_layer.update 331 | -bpy.context.scene.update() 332 | +bpy.context.view_layer.update() 333 | 334 | ??? use_occlude_geometry > shading.show_xray 335 | -space_data.use_occlude_geometry 336 | +space_data.shading.show_xray 337 | otherwise... 338 | AttributeError: 'SpaceView3D' object has no attribute 'use_occlude_geometry' 339 | 340 | ??? Matrix Multiplication: * > @ 341 | -normal = rotation * mathutils.Vector((0.0, 0.0, 1.0)) 342 | +normal = rotation @ mathutils.Vector((0.0, 0.0, 1.0)) 343 | 344 | ??? viewport_shade > space_data.shading 345 | -self.snap_face = context.space_data.viewport_shade not in {'BOUNDBOX', 'WIREFRAME'} 346 | -self.sctx.set_snap_mode(True, True, self.snap_face) 347 | +shading = context.space_data.shading 348 | +self.snap_face = not (snap_edge_and_vert and 349 | + (shading.show_xray or shading.type == 'WIREFRAME')) 350 | 351 | *** Property (annotations, class def only, not in methods) 352 | -b_prop = bpy.props.BoolProperty(name="Prop Name", default=True) 353 | +b_prop: bpy.props.BoolProperty(name="Prop Name", default=True) 354 | "BoolProperty", "BoolVectorProperty", "CollectionProperty", 355 | "EnumProperty", "FloatProperty", "FloatVectorProperty", 356 | "IntProperty", "IntVectorProperty", 357 | "PointerProperty", "StringProperty" 358 | otherwise... 359 | Warning: class FOO_OT_bar contains a properties which should be an annotation! 360 | make annotation: FOO_OT_bar.the_int_prop 361 | 362 | ??? .order 363 | -for k in cls.order: 364 | if k.startswith(some_str): 365 | - func, data = getattr(cls, k) 366 | +for k in cls.__annotations__: 367 | if k.startswith(some_str): 368 | + func, data = cls.__annotations__[k] 369 | otherwise... 370 | AttributeError: type object 'FOO_OT_bar' has no attribute 'the_int_prop' 371 | 372 | ??? event_timer_add: time_step (keyword) 373 | -self.timer = wm.event_timer_add(0.05, bpy.context.window) 374 | +self.timer = wm.event_timer_add(time_step=0.05, window=bpy.context.window) 375 | 376 | ??? frame_set: subframe (keyword) 377 | -context.scene.frame_set(10, 0.25) 378 | +context.scene.frame_set(10, subframe=0.25) 379 | 380 | ??? INFO_MT_ > TOPBAR_MT_ 381 | -box.menu("INFO_MT_file_import", icon='IMPORT') 382 | +box.menu("TOPBAR_MT_file_import", icon='IMPORT') 383 | 384 | ??? _specials > _context_menu 385 | -scene = context.scene.mat_specials 386 | +scene = context.scene.mat_context_menu 387 | -bpy.types.VIEW3D_MT_edit_mesh_specials.prepend(menu_func) 388 | +bpy.types.VIEW3D_MT_edit_mesh_context_menu.prepend(menu_func) 389 | 390 | ??? tweak_threshold > drag_threshold 391 | -tt = context.preferences.inputs.tweak_threshold 392 | +tt = context.preferences.inputs.drag_threshold 393 | otherwise... 394 | rna_uiItemR: property not found: PreferencesInput.tweak_threshold 395 | 396 | ??? cursor_location > cursor.location 397 | -endvertex = bpy.context.scene.cursor_location 398 | +endvertex = bpy.context.scene.cursor.location 399 | 400 | ??? snap_element (str) > snap_elements (set) 401 | -bpy.context.tool_settings.snap_element = 'VERTEX' 402 | +bpy.context.tool_settings.snap_elements = {'VERTEX'} 403 | otherwise... 404 | AttributeError: 'ToolSettings' object has no attribute 'snap_element' 405 | 406 | ??? pivot_point 407 | -pivot = bpy.context.space_data.pivot_point 408 | +pivot = bpy.context.tool_settings.transform_pivot_point 409 | otherwise... 410 | AttributeError: 'SpaceView3D' object has no attribute 'pivot_point' 411 | 412 | ??? proportional_edit 413 | ts = context.tool_settings 414 | -ts.proportional_edit = 'ENABLED' 415 | +ts.use_proportional_edit = True 416 | 417 | ??? resetting header_text_set 418 | -bpy.context.area.header_text_set(text='') 419 | +bpy.context.area.header_text_set(None) 420 | 421 | ??? register_module > register_class 422 | -bpy.utils.register_module(__name__) 423 | +for cls in classes: 424 | + bpy.utils.register_class(cls) 425 | 426 | ??? icon 427 | -row.label(text="Use Properties panel", icon='CURSOR') 428 | +row.label(text="Use Properties panel", icon='PIVOT_CURSOR') 429 | "BUTS", "PROPERTIES" 430 | "CURSOR", "PIVOT_CURSOR" 431 | "FULLSCREEN", "WINDOW" 432 | "IMAGE_COL", "IMAGE" 433 | "IPO", "GRAPH" 434 | "LAMP", "LIGHT" 435 | "LAMP_AREA", "LIGHT_AREA" 436 | "LAMP_DATA", "LIGHT_DATA" 437 | "LAMP_HEMI", "LIGHT_HEMI" 438 | "LAMP_POINT", "LIGHT_POINT" 439 | "LAMP_SPOT", "LIGHT_SPOT" 440 | "LAMP_SUN", "LIGHT_SUN" 441 | "LINK", # (maybe use DOT, LAYER_ACTIVE or LAYER_USED) 442 | "LINK_AREA", "LINKED" 443 | "OOPS", "OUTLINER" 444 | "ORTHO", "XRAY" 445 | "OUTLINER_DATA_LAMP", "OUTLINER_DATA_LIGHT" 446 | "OUTLINER_OB_LAMP", "OUTLINER_OB_LIGHT" 447 | "PLUG", "PLUGIN" 448 | "ROTACTIVE", "PIVOT_ACTIVE" 449 | "ROTATECENTER", "PIVOT_MEDIAN" 450 | "ROTATECOLLECTION", "PIVOT_INDIVIDUAL" 451 | "SCRIPTWIN", "PREFERENCES" 452 | "SMOOTH", "SHADING_RENDERED" 453 | "SOLID", "SHADING_SOLID" 454 | "WIRE", "SHADING_WIRE" 455 | 456 | "ALIGN", ?? 457 | "BBOX", ?? 458 | "BORDER_LASSO", ?? 459 | "BORDER_RECT", ?? 460 | "DOTSDOWN", ?? 461 | "DOTSUP", ?? 462 | "EDIT", ?? 463 | "GAME", ?? 464 | "GO_LEFT", ?? 465 | "INLINK", ?? 466 | "MANIPUL", ?? 467 | "MAN_ROT", ?? 468 | "MAN_SCALE", ?? 469 | "MAN_TRANS", ?? 470 | "OOPS", ?? 471 | "OPEN_RECENT", ?? 472 | "ORTHO", ?? 473 | "RADIO", ?? 474 | "RECOVER_AUTO", ?? 475 | "RENDER_REGION", ?? 476 | "ROTACTIVE", ?? 477 | "ROTATE", ?? 478 | "SAVE_AS", ?? 479 | "SAVE_COPY", ?? 480 | "SNAP_SURFACE", ?? 481 | "SPACE2", ?? 482 | "TEMPERATURE", ?? --------------------------------------------------------------------------------